Source of file RESTfulAPI.php
Size: 16,039 Bytes - Last Modified: 2021-12-24T06:44:03+00:00
/var/www/docs.ssmods.com/process/src/code/RESTfulAPI.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 | <?php use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; use SilverStripe\Control\HTTPResponse; use SilverStripe\Security\Member; /** * SilverStripe 3 RESTful API * * This module implements a RESTful API * with flexible configuration for model querying and response serialization * through independent components. * * @author Thierry Francois @colymba thierry@colymba.com * @copyright Copyright (c) 2013, Thierry Francois * * @license http://opensource.org/licenses/BSD-3-Clause BSD Simplified * * @package RESTfulAPI */ class RESTfulAPI extends ContentController { /** * Lets you select if the API requires authentication for access * null|false = no authentication required * true = authentication required for all HTTP methods * array = authentication required for selected HTTP methods e.g. array('POST', 'PUT', 'DELETE') * * @var boolean|array * @config */ private static $authentication_policy; const ACL_CHECK_CONFIG_ONLY = 'config'; const ACL_CHECK_MODEL_ONLY = 'model'; const ACL_CHECK_CONFIG_AND_MODEL = 'both'; /** * Lets you select if the API will perform access control checks. * false, no ACL required (everything is accessible to anyone) * string, 1 of the 3 ACL policy constants: * - 'ACL_CHECK_CONFIG_ONLY' : Check against class api_access config only * - 'ACL_CHECK_MODEL_ONLY' : Check against DataObject permissions (canView... etc.) * - 'ACL_CHECK_CONFIG_AND_MODEL' : Check against both api_access and DataObject permissions * * Default to check config only. * * @var boolean|string * @config */ private static $access_control_policy = 'ACL_CHECK_CONFIG_ONLY'; /** * Current Authenticator instance * * @var RESTfulAPI_Authenticator */ public $authenticator; /** * Current Permission Manager instance * * @var RESTfulAPI_PermissionManager */ public $authority; /** * Current QueryHandler instance * * @var RESTfulAPI_QueryHandler */ public $queryHandler; /** * Current serializer instance * * @var RESTfulAPI_Serializer */ public $serializer; /** * Injector dependencies * Override in configuration to use your custom classes * * @var array * @config */ private static $dependencies = array( 'authenticator' => '%$RESTfulAPI_TokenAuthenticator', 'authority' => '%$RESTfulAPI_DefaultPermissionManager', 'queryHandler' => '%$RESTfulAPI_DefaultQueryHandler', 'serializer' => '%$RESTfulAPI_BasicSerializer' ); /** * Embedded records setting * Specify which relation ($has_one, $has_many, $many_many) model data should be embedded into the response * * Map of relations to embed for specific record classname * 'RequestedClass' => array('RelationNameToEmbed', 'Another') * * Non embedded response: * { * 'member': { * 'name': 'John', * 'favourites': [1, 2] * } * } * * Response with embedded record: * { * 'member': { * 'name': 'John', * 'favourites': [{ * 'id': 1, * 'name': 'Mark' * },{ * 'id': 2, * 'name': 'Maggie' * }] * } * } * * @var array * @config */ private static $embedded_records; /** * Cross-Origin Resource Sharing (CORS) * API settings for cross domain XMLHTTPRequest * * Enabled true|false enable/disable CORS * Allow-Origin String|Array '*' to allow all, 'http://domain.com' to allow single domain, array('http://domain.com', 'http://site.com') to allow multiple domains * Allow-Headers String '*' to allow all or comma separated list of headers * Allow-Methods String comma separated list of allowed methods * Max-Age Integer Preflight/OPTIONS request caching time in seconds (NOTE has no effect if Authentification is enabled => custom header = always preflight) * * @var array * @config */ private static $cors = array( 'Enabled' => true, 'Allow-Origin' => '*', 'Allow-Headers' => '*', 'Allow-Methods' => 'OPTIONS, POST, GET, PUT, DELETE', 'Max-Age' => 86400 ); /** * URL handler allowed actions * * @var array */ private static $allowed_actions = array( 'index', 'auth', 'acl' ); /** * URL handler definition * * @var array */ private static $url_handlers = array( 'auth/$Action' => 'auth', 'acl/$Action' => 'acl', '$ClassName/$ID' => 'index' ); /** * Returns current query handler instance * * @return RESTfulAPI_QueryHandler QueryHandler instance */ public function getqueryHandler() { return $this->queryHandler; } /** * Returns current serializer instance * * @return RESTfulAPI_Serializer Serializer instance */ public function getserializer() { return $this->serializer; } /** * Current RESTfulAPI instance * * @var RESTfulAPI */ protected static $instance; /** * Constructor.... */ public function __construct() { parent::__construct(); //save current instance in static var self::$instance = $this; } /** * Controller inititalisation * Catches CORS preflight request marked with HTTPMethod 'OPTIONS' */ public function init() { parent::init(); //catch preflight request if ($this->request->httpMethod() === 'OPTIONS') { $answer = $this->answer(null, true); $answer->output(); exit; } } /** * Handles authentications methods * get response from API Authenticator * then passes it on to $answer() * * @param HTTPRequest $request HTTP request */ public function auth(HTTPRequest $request) { $action = $request->param('Action'); if ($this->authenticator) { $className = get_class($this->authenticator); $allowedActions = Config::inst()->get($className, 'allowed_actions'); if (!$allowedActions) { $allowedActions = array(); } if (in_array($action, $allowedActions)) { if (method_exists($this->authenticator, $action)) { $response = $this->authenticator->$action($request); $response = $this->serializer->serialize($response); return $this->answer($response); } else { //let's be shady here instead return $this->error(new RESTfulAPI_Error(403, "Action '$action' not allowed." )); } } else { return $this->error(new RESTfulAPI_Error(403, "Action '$action' not allowed." )); } } } /** * Handles Access Control methods * get response from API PermissionManager * then passes it on to $answer() * * @param HTTPRequest $request HTTP request */ public function acl(HTTPRequest $request) { $action = $request->param('Action'); if ($this->authority) { $className = get_class($this->authority); $allowedActions = Config::inst()->get($className, 'allowed_actions'); if (!$allowedActions) { $allowedActions = array(); } if (in_array($action, $allowedActions)) { if (method_exists($this->authority, $action)) { $response = $this->authority->$action($request); $response = $this->serializer->serialize($response); return $this->answer($response); } else { //let's be shady here instead return $this->error(new RESTfulAPI_Error(403, "Action '$action' not allowed." )); } } else { return $this->error(new RESTfulAPI_Error(403, "Action '$action' not allowed." )); } } } /** * Main API hub switch * All requests pass through here and are redirected depending on HTTP verb and params * * @todo move authentication check to another methode * * @param HTTPRequest $request HTTP request * @return string json object of the models found */ public function index(HTTPRequest $request) { //check authentication if enabled if ($this->authenticator) { $policy = $this->config()->authentication_policy; $authALL = $policy === true; $authMethod = is_array($policy) && in_array($request->httpMethod(), $policy); if ($authALL || $authMethod) { $authResult = $this->authenticator->authenticate($request); if ($authResult instanceof RESTfulAPI_Error) { //Authentication failed return error to client return $this->error($authResult); } } } //pass control to query handler $data = $this->queryHandler->handleQuery($request); //catch + return errors if ($data instanceof RESTfulAPI_Error) { return $this->error($data); } //serialize response $json = $this->serializer->serialize($data); //catch + return errors if ($json instanceof RESTfulAPI_Error) { return $this->error($json); } //all is good reply normally return $this->answer($json); } /** * Output the API response to client * then exit. * * @param string $json Response body * @param boolean $corsPreflight Set to true if this is a XHR preflight request answer. CORS shoud be enabled. */ public function answer($json = null, $corsPreflight = false) { $answer = new HTTPResponse(); //set response body if (!$corsPreflight) { $answer->setBody($json); } //set CORS if needed $answer = $this->setAnswerCORS($answer); $answer->addHeader('Content-Type', $this->serializer->getcontentType()); // save controller's response then return/output $this->response = $answer; return $answer; } /** * Handles formatting and output error message * then exit. * * @param RESTfulAPI_Error $error Error object to return */ public function error(RESTfulAPI_Error $error) { $answer = new HTTPResponse(); $body = $this->serializer->serialize($error->body); $answer->setBody($body); $answer->setStatusCode($error->code, $error->message); $answer->addHeader('Content-Type', $this->serializer->getcontentType()); $answer = $this->setAnswerCORS($answer); // save controller's response then return/output $this->response = $answer; return $answer; } /** * Apply the proper CORS response heardes * to an HTTPResponse * * @param HTTPResponse $answer The updated response if CORS are neabled */ private function setAnswerCORS(HTTPResponse $answer) { $cors = Config::inst()->get('RESTfulAPI', 'cors'); // skip if CORS is not enabled if (!$cors['Enabled']) { return $answer; } //check if Origin is allowed $allowedOrigin = $cors['Allow-Origin']; $requestOrigin = $this->request->getHeader('Origin'); if ($requestOrigin) { if ($cors['Allow-Origin'] === '*') { $allowedOrigin = $requestOrigin; } elseif (is_array($cors['Allow-Origin'])) { if (in_array($requestOrigin, $cors['Allow-Origin'])) { $allowedOrigin = $requestOrigin; } } } $answer->addHeader('Access-Control-Allow-Origin', $allowedOrigin); //allowed headers $allowedHeaders = ''; $requestHeaders = $this->request->getHeader('Access-Control-Request-Headers'); if ($cors['Allow-Headers'] === '*') { $allowedHeaders = $requestHeaders; } else { $allowedHeaders = $cors['Allow-Headers']; } $answer->addHeader('Access-Control-Allow-Headers', $allowedHeaders); //allowed method $answer->addHeader('Access-Control-Allow-Methods', $cors['Allow-Methods']); //max age $answer->addHeader('Access-Control-Max-Age', $cors['Max-Age']); return $answer; } /** * Checks a class or model api access * depending on access_control_policy and the provided model. * - 1st config check * - 2nd permission check if config access passes * * @param string|DataObject $model Model's classname or DataObject * @param string $httpMethod API request HTTP method * @return boolean true if access is granted, false otherwise */ public static function api_access_control($model, $httpMethod = 'GET') { $policy = self::config()->access_control_policy; if ($policy === false) { return true; } // if access control is disabled, skip else { $policy = constant('self::'.$policy); } if ($policy === self::ACL_CHECK_MODEL_ONLY) { $access = true; } else { $access = false; } if ($policy === self::ACL_CHECK_CONFIG_ONLY || $policy === self::ACL_CHECK_CONFIG_AND_MODEL) { if (!is_string($model)) { $className = $model->className; } else { $className = $model; } $access = self::api_access_config_check($className, $httpMethod); } if ($policy === self::ACL_CHECK_MODEL_ONLY || $policy === self::ACL_CHECK_CONFIG_AND_MODEL) { if ($access) { $access = self::model_permission_check($model, $httpMethod); } } return $access; } /** * Checks a model's api_access config. * api_access config can be: * - unset|false, access is always denied * - true, access is always granted * - comma separated list of allowed HTTP methods * * @param string $className Model's classname * @param string $httpMethod API request HTTP method * @return boolean true if access is granted, false otherwise */ private static function api_access_config_check($className, $httpMethod = 'GET') { $access = false; $api_access = singleton($className)->stat('api_access'); if (is_string($api_access)) { $api_access = explode(',', strtoupper($api_access)); if (in_array($httpMethod, $api_access)) { $access = true; } else { $access = false; } } elseif ($api_access === true) { $access = true; } return $access; } /** * Checks a Model's permission for the currently * authenticated user via the Permission Manager dependency. * * For permissions to actually be checked, this means the RESTfulAPI * must have both authenticator and authority dependencies defined. * * If the authenticator component does not return an instance of the Member * null will be passed to the authority component. * * This default to true. * * @param string|DataObject $model Model's classname or DataObject to check permission for * @param string $httpMethod API request HTTP method * @return boolean true if access is granted, false otherwise */ private static function model_permission_check($model, $httpMethod = 'GET') { $access = true; $apiInstance = self::$instance; if ($apiInstance->authenticator && $apiInstance->authority) { $request = $apiInstance->request; $member = $apiInstance->authenticator->getOwner($request); if (!$member instanceof Member) { $member = null; } $access = $apiInstance->authority->checkPermission($model, $member, $httpMethod); if (!is_bool($access)) { $access = true; } } return $access; } } |