/vendor/Luracast/Restler/Restler.php
PHP | 1451 lines | 945 code | 90 blank | 416 comment | 167 complexity | 9a405f8b8b86944ea6fae98c02da5a34 MD5 | raw file
- <?php
- namespace Luracast\Restler;
- use Exception;
- use InvalidArgumentException;
- use Luracast\Restler\Data\ApiMethodInfo;
- use Luracast\Restler\Data\ValidationInfo;
- use Luracast\Restler\Data\Validator;
- use Luracast\Restler\Format\iFormat;
- use Luracast\Restler\Format\iDecodeStream;
- use Luracast\Restler\Format\UrlEncodedFormat;
- /**
- * REST API Server. It is the server part of the Restler framework.
- * inspired by the RestServer code from
- * <http://jacwright.com/blog/resources/RestServer.txt>
- *
- * @category Framework
- * @package Restler
- * @author R.Arul Kumaran <arul@luracast.com>
- * @copyright 2010 Luracast
- * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
- * @link http://luracast.com/products/restler/
- * @version 3.0.0rc5
- */
- class Restler extends EventDispatcher
- {
- const VERSION = '3.0.0rc5';
- // ==================================================================
- //
- // Public variables
- //
- // ------------------------------------------------------------------
- /**
- * Reference to the last exception thrown
- * @var RestException
- */
- public $exception = null;
- /**
- * Used in production mode to store the routes and more
- *
- * @var iCache
- */
- public $cache;
- /**
- * URL of the currently mapped service
- *
- * @var string
- */
- public $url;
- /**
- * Http request method of the current request.
- * Any value between [GET, PUT, POST, DELETE]
- *
- * @var string
- */
- public $requestMethod;
- /**
- * Requested data format.
- * Instance of the current format class
- * which implements the iFormat interface
- *
- * @var iFormat
- * @example jsonFormat, xmlFormat, yamlFormat etc
- */
- public $requestFormat;
- /**
- * Response data format.
- *
- * Instance of the current format class
- * which implements the iFormat interface
- *
- * @var iFormat
- * @example jsonFormat, xmlFormat, yamlFormat etc
- */
- public $responseFormat;
- /**
- * Http status code
- *
- * @var int
- */
- public $responseCode=200;
- /**
- * @var string base url of the api service
- */
- protected $baseUrl;
- /**
- * @var bool Used for waiting till verifying @format
- * before throwing content negotiation failed
- */
- protected $requestFormatDiffered = false;
- /**
- * method information including metadata
- *
- * @var ApiMethodInfo
- */
- public $apiMethodInfo;
- /**
- * @var int for calculating execution time
- */
- protected $startTime;
- /**
- * When set to false, it will run in debug mode and parse the
- * class files every time to map it to the URL
- *
- * @var boolean
- */
- protected $productionMode = false;
- public $refreshCache = false;
- /**
- * Caching of url map is enabled or not
- *
- * @var boolean
- */
- protected $cached;
- /**
- * @var int
- */
- protected $apiVersion = 1;
- /**
- * @var int
- */
- protected $requestedApiVersion = 1;
- /**
- * @var int
- */
- protected $apiMinimumVersion = 1;
- /**
- * @var array
- */
- protected $apiVersionMap = array();
- /**
- * Associated array that maps formats to their respective format class name
- *
- * @var array
- */
- protected $formatMap = array();
- /**
- * List of the Mime Types that can be produced as a response by this API
- *
- * @var array
- */
- protected $writableMimeTypes = array();
- /**
- * List of the Mime Types that are supported for incoming requests by this API
- *
- * @var array
- */
- protected $readableMimeTypes = array();
- /**
- * Associated array that maps formats to their respective format class name
- *
- * @var array
- */
- protected $formatOverridesMap = array('extensions' => array());
- /**
- * list of filter classes
- *
- * @var array
- */
- protected $filterClasses = array();
- /**
- * instances of filter classes that are executed after authentication
- *
- * @var array
- */
- protected $postAuthFilterClasses = array();
- // ==================================================================
- //
- // Protected variables
- //
- // ------------------------------------------------------------------
- /**
- * Data sent to the service
- *
- * @var array
- */
- protected $requestData = array();
- /**
- * list of authentication classes
- *
- * @var array
- */
- protected $authClasses = array();
- /**
- * list of error handling classes
- *
- * @var array
- */
- protected $errorClasses = array();
- protected $authenticated = false;
- protected $authVerified = false;
- /**
- * @var mixed
- */
- protected $responseData;
- /**
- * Constructor
- *
- * @param boolean $productionMode When set to false, it will run in
- * debug mode and parse the class files
- * every time to map it to the URL
- *
- * @param bool $refreshCache will update the cache when set to true
- */
- public function __construct($productionMode = false, $refreshCache = false)
- {
- parent::__construct();
- $this->startTime = time();
- Util::$restler = $this;
- Scope::set('Restler', $this);
- $this->productionMode = $productionMode;
- if (is_null(Defaults::$cacheDirectory)) {
- Defaults::$cacheDirectory = dirname($_SERVER['SCRIPT_FILENAME']) .
- DIRECTORY_SEPARATOR . 'cache';
- }
- $this->cache = new Defaults::$cacheClass();
- $this->refreshCache = $refreshCache;
- // use this to rebuild cache every time in production mode
- if ($productionMode && $refreshCache) {
- $this->cached = false;
- }
- }
- /**
- * Main function for processing the api request
- * and return the response
- *
- * @throws Exception when the api service class is missing
- * @throws RestException to send error response
- */
- public function handle()
- {
- try {
- try {
- try {
- $this->get();
- } catch (Exception $e) {
- $this->requestData
- = array(Defaults::$fullRequestDataName => array());
- if (!$e instanceof RestException) {
- $e = new RestException(
- 500,
- $this->productionMode ? null : $e->getMessage(),
- array(),
- $e
- );
- }
- $this->route();
- throw $e;
- }
- if (Defaults::$useVendorMIMEVersioning)
- $this->responseFormat = $this->negotiateResponseFormat();
- $this->route();
- } catch (Exception $e) {
- $this->negotiate();
- if (!$e instanceof RestException) {
- $e = new RestException(
- 500,
- $this->productionMode ? null : $e->getMessage(),
- array(),
- $e
- );
- }
- throw $e;
- }
- $this->negotiate();
- $this->preAuthFilter();
- $this->authenticate();
- $this->postAuthFilter();
- $this->validate();
- $this->preCall();
- $this->call();
- $this->compose();
- $this->postCall();
- $this->respond();
- } catch (Exception $e) {
- try{
- $this->message($e);
- } catch (Exception $e2) {
- $this->message($e2);
- }
- }
- }
- /**
- * read the request details
- *
- * Find out the following
- * - baseUrl
- * - url requested
- * - version requested (if url based versioning)
- * - http verb/method
- * - negotiate content type
- * - request data
- * - set defaults
- */
- protected function get()
- {
- $this->dispatch('get');
- if (empty($this->formatMap)) {
- $this->setSupportedFormats('JsonFormat');
- }
- $this->url = $this->getPath();
- $this->requestMethod = Util::getRequestMethod();
- $this->requestFormat = $this->getRequestFormat();
- $this->requestData = $this->getRequestData(false);
- //parse defaults
- foreach ($_GET as $key => $value) {
- if (isset(Defaults::$aliases[$key])) {
- $_GET[Defaults::$aliases[$key]] = $value;
- unset($_GET[$key]);
- $key = Defaults::$aliases[$key];
- }
- if (in_array($key, Defaults::$overridables)) {
- Defaults::setProperty($key, $value);
- }
- }
- }
- /**
- * Returns a list of the mime types (e.g. ["application/json","application/xml"]) that the API can respond with
- * @return array
- */
- public function getWritableMimeTypes()
- {
- return $this->writableMimeTypes;
- }
- /**
- * Returns the list of Mime Types for the request that the API can understand
- * @return array
- */
- public function getReadableMimeTypes()
- {
- return $this->readableMimeTypes;
- }
- /**
- * Call this method and pass all the formats that should be supported by
- * the API Server. Accepts multiple parameters
- *
- * @param string ,... $formatName class name of the format class that
- * implements iFormat
- *
- * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
- * @throws Exception
- */
- public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/)
- {
- $args = func_get_args();
- $extensions = array();
- $throwException = $this->requestFormatDiffered;
- $this->writableMimeTypes = $this->readableMimeTypes = array();
- foreach ($args as $className) {
- $obj = Scope::get($className);
- if (!$obj instanceof iFormat)
- throw new Exception('Invalid format class; must implement ' .
- 'iFormat interface');
- if ($throwException && get_class($obj) == get_class($this->requestFormat)) {
- $throwException = false;
- }
- foreach ($obj->getMIMEMap() as $mime => $extension) {
- if($obj->isWritable()){
- $this->writableMimeTypes[]=$mime;
- $extensions[".$extension"] = true;
- }
- if($obj->isReadable())
- $this->readableMimeTypes[]=$mime;
- if (!isset($this->formatMap[$extension]))
- $this->formatMap[$extension] = $className;
- if (!isset($this->formatMap[$mime]))
- $this->formatMap[$mime] = $className;
- }
- }
- if ($throwException) {
- throw new RestException(
- 403,
- 'Content type `' . $this->requestFormat->getMIME() . '` is not supported.'
- );
- }
- $this->formatMap['default'] = $args[0];
- $this->formatMap['extensions'] = array_keys($extensions);
- }
- /**
- * Call this method and pass all the formats that can be used to override
- * the supported formats using `@format` comment. Accepts multiple parameters
- *
- * @param string ,... $formatName class name of the format class that
- * implements iFormat
- *
- * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...);
- * @throws Exception
- */
- public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/)
- {
- $args = func_get_args();
- $extensions = array();
- foreach ($args as $className) {
- $obj = Scope::get($className);
- if (!$obj instanceof iFormat)
- throw new Exception('Invalid format class; must implement ' .
- 'iFormat interface');
- foreach ($obj->getMIMEMap() as $mime => $extension) {
- if (!isset($this->formatOverridesMap[$extension]))
- $this->formatOverridesMap[$extension] = $className;
- if (!isset($this->formatOverridesMap[$mime]))
- $this->formatOverridesMap[$mime] = $className;
- if($obj->isWritable())
- $extensions[".$extension"] = true;
- }
- }
- $this->formatOverridesMap['extensions'] = array_keys($extensions);
- }
- /**
- * Parses the request url and get the api path
- *
- * @return string api path
- */
- protected function getPath()
- {
- // fix SCRIPT_NAME for PHP 5.4 built-in web server
- if (false === strpos($_SERVER['SCRIPT_NAME'], '.php'))
- $_SERVER['SCRIPT_NAME']
- = '/' . Util::removeCommonPath($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']);
- $fullPath = urldecode($_SERVER['REQUEST_URI']);
- $path = Util::removeCommonPath(
- $fullPath,
- $_SERVER['SCRIPT_NAME']
- );
- $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80';
- $https = $port == '443' ||
- (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB
- (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on');
- $baseUrl = ($https ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'];
- if (!$https && $port != '80' || !$https && $port != '443')
- $baseUrl .= ':' . $port;
- $this->baseUrl = rtrim($baseUrl
- . substr($fullPath, 0, strlen($fullPath) - strlen($path)), '/');
- $path = rtrim(strtok($path, '?'), '/'); //remove query string and trailing slash if found any
- $path = str_replace(
- array_merge(
- $this->formatMap['extensions'],
- $this->formatOverridesMap['extensions']
- ),
- '',
- $path
- );
- if (Defaults::$useUrlBasedVersioning && strlen($path) && $path{0} == 'v') {
- $version = intval(substr($path, 1));
- if ($version && $version <= $this->apiVersion) {
- $this->requestedApiVersion = $version;
- $path = explode('/', $path, 2);
- $path = $path[1];
- }
- } else {
- $this->requestedApiVersion = $this->apiMinimumVersion;
- }
- return $path;
- }
- /**
- * Parses the request to figure out format of the request data
- *
- * @throws RestException
- * @return iFormat any class that implements iFormat
- * @example JsonFormat
- */
- protected function getRequestFormat()
- {
- $format = null ;
- // check if client has sent any information on request format
- if (
- !empty($_SERVER['CONTENT_TYPE']) ||
- (
- !empty($_SERVER['HTTP_CONTENT_TYPE']) &&
- $_SERVER['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']
- )
- ) {
- $mime = $_SERVER['CONTENT_TYPE'];
- if (false !== $pos = strpos($mime, ';')) {
- $mime = substr($mime, 0, $pos);
- }
- if ($mime == UrlEncodedFormat::MIME)
- $format = Scope::get('UrlEncodedFormat');
- elseif (isset($this->formatMap[$mime])) {
- $format = Scope::get($this->formatMap[$mime]);
- $format->setMIME($mime);
- } elseif (!$this->requestFormatDiffered && isset($this->formatOverridesMap[$mime])) {
- //if our api method is not using an @format comment
- //to point to this $mime, we need to throw 403 as in below
- //but since we don't know that yet, we need to defer that here
- $format = Scope::get($this->formatOverridesMap[$mime]);
- $format->setMIME($mime);
- $this->requestFormatDiffered = true;
- } else {
- throw new RestException(
- 403,
- "Content type `$mime` is not supported."
- );
- }
- }
- if(!$format){
- $format = Scope::get($this->formatMap['default']);
- }
- return $format;
- }
- public function getRequestStream()
- {
- static $tempStream = false;
- if (!$tempStream) {
- $tempStream = fopen('php://temp', 'r+');
- $rawInput = fopen('php://input', 'r');
- stream_copy_to_stream($rawInput, $tempStream);
- }
- rewind($tempStream);
- return $tempStream;
- }
- /**
- * Parses the request data and returns it
- *
- * @param bool $includeQueryParameters
- *
- * @return array php data
- */
- public function getRequestData($includeQueryParameters = true)
- {
- $get = UrlEncodedFormat::decoderTypeFix($_GET);
- if ($this->requestMethod == 'PUT'
- || $this->requestMethod == 'PATCH'
- || $this->requestMethod == 'POST'
- ) {
- if (!empty($this->requestData)) {
- return $includeQueryParameters
- ? $this->requestData + $get
- : $this->requestData;
- }
- $stream = $this->getRequestStream();
- if($stream === FALSE)
- return array();
- $r = $this->requestFormat instanceof iDecodeStream
- ? $this->requestFormat->decodeStream($stream)
- : $this->requestFormat->decode(stream_get_contents($stream));
- $r = is_array($r)
- ? array_merge($r, array(Defaults::$fullRequestDataName => $r))
- : array(Defaults::$fullRequestDataName => $r);
- return $includeQueryParameters
- ? $r + $get
- : $r;
- }
- return $includeQueryParameters ? $get : array(); //no body
- }
- /**
- * Find the api method to execute for the requested Url
- */
- protected function route()
- {
- $this->dispatch('route');
- $params = $this->getRequestData();
- //backward compatibility for restler 2 and below
- if (!Defaults::$smartParameterParsing) {
- $params = $params + array(Defaults::$fullRequestDataName => $params);
- }
- $this->apiMethodInfo = $o = Routes::find(
- $this->url, $this->requestMethod,
- $this->requestedApiVersion, $params
- );
- //set defaults based on api method comments
- if (isset($o->metadata)) {
- foreach (Defaults::$fromComments as $key => $defaultsKey) {
- if (array_key_exists($key, $o->metadata)) {
- $value = $o->metadata[$key];
- Defaults::setProperty($defaultsKey, $value);
- }
- }
- }
- if (!isset($o->className))
- throw new RestException(404);
- if(isset($this->apiVersionMap[$o->className])){
- Scope::$classAliases[Util::getShortName($o->className)]
- = $this->apiVersionMap[$o->className][$this->requestedApiVersion];
- }
- foreach ($this->authClasses as $auth) {
- if (isset($this->apiVersionMap[$auth])) {
- Scope::$classAliases[$auth] = $this->apiVersionMap[$auth][$this->requestedApiVersion];
- } elseif (isset($this->apiVersionMap[Scope::$classAliases[$auth]])) {
- Scope::$classAliases[$auth]
- = $this->apiVersionMap[Scope::$classAliases[$auth]][$this->requestedApiVersion];
- }
- }
- }
- /**
- * Negotiate the response details such as
- * - cross origin resource sharing
- * - media type
- * - charset
- * - language
- */
- protected function negotiate()
- {
- $this->dispatch('negotiate');
- $this->negotiateCORS();
- $this->responseFormat = $this->negotiateResponseFormat();
- $this->negotiateCharset();
- $this->negotiateLanguage();
- }
- protected function negotiateCORS()
- {
- if (
- $this->requestMethod == 'OPTIONS'
- && Defaults::$crossOriginResourceSharing
- ) {
- if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
- header('Access-Control-Allow-Methods: '
- . Defaults::$accessControlAllowMethods);
- if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
- header('Access-Control-Allow-Headers: '
- . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
- header('Access-Control-Allow-Origin: ' .
- (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin));
- header('Access-Control-Allow-Credentials: true');
- exit(0);
- }
- }
- // ==================================================================
- //
- // Protected functions
- //
- // ------------------------------------------------------------------
- /**
- * Parses the request to figure out the best format for response.
- * Extension, if present, overrides the Accept header
- *
- * @throws RestException
- * @return iFormat
- * @example JsonFormat
- */
- protected function negotiateResponseFormat()
- {
- $metadata = Util::nestedValue($this, 'apiMethodInfo', 'metadata');
- //check if the api method insists on response format using @format comment
- if ($metadata && isset($metadata['format'])) {
- $formats = explode(',', (string)$metadata['format']);
- foreach ($formats as $i => $f) {
- $f = trim($f);
- if (!in_array($f, $this->formatOverridesMap))
- throw new RestException(
- 500,
- "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first."
- );
- $formats[$i] = $f;
- }
- call_user_func_array(array($this, 'setSupportedFormats'), $formats);
- }
- // check if client has specified an extension
- /** @var $format iFormat*/
- $format = null;
- $extensions = explode(
- '.',
- parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
- );
- while ($extensions) {
- $extension = array_pop($extensions);
- $extension = explode('/', $extension);
- $extension = array_shift($extension);
- if ($extension && isset($this->formatMap[$extension])) {
- $format = Scope::get($this->formatMap[$extension]);
- $format->setExtension($extension);
- // echo "Extension $extension";
- return $format;
- }
- }
- // check if client has sent list of accepted data formats
- if (isset($_SERVER['HTTP_ACCEPT'])) {
- $acceptList = Util::sortByPriority($_SERVER['HTTP_ACCEPT']);
- foreach ($acceptList as $accept => $quality) {
- if (isset($this->formatMap[$accept])) {
- $format = Scope::get($this->formatMap[$accept]);
- $format->setMIME($accept);
- //echo "MIME $accept";
- // Tell cache content is based on Accept header
- @header('Vary: Accept');
- return $format;
- } elseif (false !== ($index = strrpos($accept, '+'))) {
- $mime = substr($accept, 0, $index);
- if (is_string(Defaults::$apiVendor)
- && 0 === stripos($mime,
- 'application/vnd.'
- . Defaults::$apiVendor . '-v')
- ) {
- $extension = substr($accept, $index + 1);
- if (isset($this->formatMap[$extension])) {
- //check the MIME and extract version
- $version = intval(substr($mime,
- 18 + strlen(Defaults::$apiVendor)));
- if ($version > 0 && $version <= $this->apiVersion) {
- $this->requestedApiVersion = $version;
- $format = Scope::get($this->formatMap[$extension]);
- $format->setExtension($extension);
- // echo "Extension $extension";
- Defaults::$useVendorMIMEVersioning = true;
- @header('Vary: Accept');
- return $format;
- }
- }
- }
- }
- }
- } else {
- // RFC 2616: If no Accept header field is
- // present, then it is assumed that the
- // client accepts all media types.
- $_SERVER['HTTP_ACCEPT'] = '*/*';
- }
- if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) {
- if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/*')) {
- $format = Scope::get('JsonFormat');
- } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/*')) {
- $format = Scope::get('XmlFormat');
- } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], '*/*')) {
- $format = Scope::get($this->formatMap['default']);
- }
- }
- if (empty($format)) {
- // RFC 2616: If an Accept header field is present, and if the
- // server cannot send a response which is acceptable according to
- // the combined Accept field value, then the server SHOULD send
- // a 406 (not acceptable) response.
- $format = Scope::get($this->formatMap['default']);
- $this->responseFormat = $format;
- throw new RestException(
- 406,
- 'Content negotiation failed. ' .
- 'Try `' . $format->getMIME() . '` instead.'
- );
- } else {
- // Tell cache content is based at Accept header
- @header("Vary: Accept");
- return $format;
- }
- }
- protected function negotiateCharset()
- {
- if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
- $found = false;
- $charList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_CHARSET']);
- foreach ($charList as $charset => $quality) {
- if (in_array($charset, Defaults::$supportedCharsets)) {
- $found = true;
- Defaults::$charset = $charset;
- break;
- }
- }
- if (!$found) {
- if (strpos($_SERVER['HTTP_ACCEPT_CHARSET'], '*') !== false) {
- //use default charset
- } else {
- throw new RestException(
- 406,
- 'Content negotiation failed. ' .
- 'Requested charset is not supported'
- );
- }
- }
- }
- }
- protected function negotiateLanguage()
- {
- if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
- $found = false;
- $langList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_LANGUAGE']);
- foreach ($langList as $lang => $quality) {
- foreach (Defaults::$supportedLanguages as $supported) {
- if (strcasecmp($supported, $lang) == 0) {
- $found = true;
- Defaults::$language = $supported;
- break 2;
- }
- }
- }
- if (!$found) {
- if (strpos($_SERVER['HTTP_ACCEPT_LANGUAGE'], '*') !== false) {
- //use default language
- } else {
- //ignore
- }
- }
- }
- }
- /**
- * Filer api calls before authentication
- */
- protected function preAuthFilter()
- {
- if (empty($this->filterClasses)) {
- return;
- }
- $this->dispatch('preAuthFilter');
- foreach ($this->filterClasses as $filterClass) {
- /**
- * @var iFilter
- */
- $filterObj = Scope::get($filterClass);
- if (!$filterObj instanceof iFilter) {
- throw new RestException (
- 500, 'Filter Class ' .
- 'should implement iFilter');
- } else if (!($ok = $filterObj->__isAllowed())) {
- if (is_null($ok)
- && $filterObj instanceof iUseAuthentication
- ) {
- //handle at authentication stage
- $this->postAuthFilterClasses[] = $filterClass;
- continue;
- }
- throw new RestException(403); //Forbidden
- }
- }
- }
- protected function authenticate()
- {
- $o = & $this->apiMethodInfo;
- $accessLevel = max(Defaults::$apiAccessLevel,
- $o->accessLevel);
- try {
- if ($accessLevel || count($this->postAuthFilterClasses)) {
- $this->dispatch('authenticate');
- if (!count($this->authClasses)) {
- throw new RestException(
- 403,
- 'at least one Authentication Class is required'
- );
- }
- foreach ($this->authClasses as $authClass) {
- $authObj = Scope::get($authClass);
- if (!method_exists($authObj,
- Defaults::$authenticationMethod)
- ) {
- throw new RestException (
- 500, 'Authentication Class ' .
- 'should implement iAuthenticate');
- } elseif (
- !$authObj->{Defaults::$authenticationMethod}()
- ) {
- throw new RestException(401);
- }
- }
- $this->authenticated = true;
- }
- $this->authVerified = true;
- } catch (RestException $e) {
- $this->authVerified = true;
- if ($accessLevel > 1) { //when it is not a hybrid api
- throw ($e);
- } else {
- $this->authenticated = false;
- }
- }
- }
- /**
- * Filer api calls after authentication
- */
- protected function postAuthFilter()
- {
- if(empty($this->postAuthFilterClasses)) {
- return;
- }
- $this->dispatch('postAuthFilter');
- foreach ($this->postAuthFilterClasses as $filterClass) {
- Scope::get($filterClass);
- }
- }
- protected function validate()
- {
- if (!Defaults::$autoValidationEnabled) {
- return;
- }
- $this->dispatch('validate');
- $o = & $this->apiMethodInfo;
- foreach ($o->metadata['param'] as $index => $param) {
- $info = & $param [CommentParser::$embeddedDataName];
- if (!isset ($info['validate'])
- || $info['validate'] != false
- ) {
- if (isset($info['method'])) {
- $info ['apiClassInstance'] = Scope::get($o->className);
- }
- //convert to instance of ValidationInfo
- $info = new ValidationInfo($param);
- $validator = Defaults::$validatorClass;
- //if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) {
- //changed the above test to below for addressing this php bug
- //https://bugs.php.net/bug.php?id=53727
- if (function_exists("$validator::validate")) {
- throw new \UnexpectedValueException(
- '`Defaults::$validatorClass` must implement `iValidate` interface'
- );
- }
- $valid = $o->parameters[$index];
- $o->parameters[$index] = null;
- if (empty(Validator::$exceptions))
- $o->metadata['param'][$index]['autofocus'] = true;
- $valid = $validator::validate(
- $valid, $info
- );
- $o->parameters[$index] = $valid;
- unset($o->metadata['param'][$index]['autofocus']);
- }
- }
- }
- protected function call()
- {
- $this->dispatch('call');
- $o = & $this->apiMethodInfo;
- $accessLevel = max(Defaults::$apiAccessLevel,
- $o->accessLevel);
- $object = Scope::get($o->className);
- switch ($accessLevel) {
- case 3 : //protected method
- $reflectionMethod = new \ReflectionMethod(
- $object,
- $o->methodName
- );
- $reflectionMethod->setAccessible(true);
- $result = $reflectionMethod->invokeArgs(
- $object,
- $o->parameters
- );
- break;
- default :
- $result = call_user_func_array(array(
- $object,
- $o->methodName
- ), $o->parameters);
- }
- $this->responseData = $result;
- }
- protected function compose()
- {
- $this->dispatch('compose');
- $this->composeHeaders();
- /**
- * @var iCompose Default Composer
- */
- $compose = Scope::get(Defaults::$composeClass);
- $this->responseData = is_null($this->responseData) &&
- Defaults::$emptyBodyForNullResponse
- ? ''
- : $this->responseFormat->encode(
- $compose->response($this->responseData),
- !$this->productionMode
- );
- }
- public function composeHeaders(RestException $e = null)
- {
- //only GET method should be cached if allowed by API developer
- $expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0;
- if(!is_array(Defaults::$headerCacheControl))
- Defaults::$headerCacheControl = array(Defaults::$headerCacheControl);
- $cacheControl = Defaults::$headerCacheControl[0];
- if ($expires > 0) {
- $cacheControl = $this->apiMethodInfo->accessLevel
- ? 'private, ' : 'public, ';
- $cacheControl .= end(Defaults::$headerCacheControl);
- $cacheControl = str_replace('{expires}', $expires, $cacheControl);
- $expires = gmdate('D, d M Y H:i:s \G\M\T', time() + $expires);
- }
- @header('Cache-Control: ' . $cacheControl);
- @header('Expires: ' . $expires);
- @header('X-Powered-By: Luracast Restler v' . Restler::VERSION);
- if (Defaults::$crossOriginResourceSharing
- && isset($_SERVER['HTTP_ORIGIN'])
- ) {
- header('Access-Control-Allow-Origin: ' .
- (Defaults::$accessControlAllowOrigin == '*'
- ? $_SERVER['HTTP_ORIGIN']
- : Defaults::$accessControlAllowOrigin)
- );
- header('Access-Control-Allow-Credentials: true');
- header('Access-Control-Max-Age: 86400');
- }
- $this->responseFormat->setCharset(Defaults::$charset);
- $charset = $this->responseFormat->getCharset()
- ? : Defaults::$charset;
- @header('Content-Type: ' . (
- Defaults::$useVendorMIMEVersioning
- ? 'application/vnd.'
- . Defaults::$apiVendor
- . "-v{$this->requestedApiVersion}"
- . '+' . $this->responseFormat->getExtension()
- : $this->responseFormat->getMIME())
- . '; charset=' . $charset
- );
- @header('Content-Language: ' . Defaults::$language);
- if (isset($this->apiMethodInfo->metadata['header'])) {
- foreach ($this->apiMethodInfo->metadata['header'] as $header)
- @header($header, true);
- }
- $code = 200;
- if (!Defaults::$suppressResponseCode) {
- if ($e) {
- $code = $e->getCode();
- } elseif (isset($this->apiMethodInfo->metadata['status'])) {
- $code = $this->apiMethodInfo->metadata['status'];
- }
- }
- $this->responseCode = $code;
- @header(
- "{$_SERVER['SERVER_PROTOCOL']} $code " .
- (isset(RestException::$codes[$code]) ? RestException::$codes[$code] : '')
- );
- }
- protected function respond()
- {
- $this->dispatch('respond');
- //handle throttling
- if (Defaults::$throttle) {
- $elapsed = time() - $this->startTime;
- if (Defaults::$throttle / 1e3 > $elapsed) {
- usleep(1e6 * (Defaults::$throttle / 1e3 - $elapsed));
- }
- }
- echo $this->responseData;
- $this->dispatch('complete');
- if ($this->responseCode == 401) {
- $authString = count($this->authClasses)
- ? Scope::get($this->authClasses[0])->__getWWWAuthenticateString()
- : 'Unknown';
- @header('WWW-Authenticate: ' . $authString, false);
- }
- exit;
- }
- protected function message(Exception $exception)
- {
- $this->dispatch('message');
- if (!$exception instanceof RestException) {
- $exception = new RestException(
- 500,
- $this->productionMode ? null : $exception->getMessage(),
- array(),
- $exception
- );
- }
- $this->exception = $exception;
- $method = 'handle' . $exception->getCode();
- $handled = false;
- foreach ($this->errorClasses as $className) {
- if (method_exists($className, $method)) {
- $obj = Scope::get($className);
- if ($obj->$method())
- $handled = true;
- }
- }
- if ($handled) {
- return;
- }
- if (!isset($this->responseFormat)) {
- $this->responseFormat = Scope::get('JsonFormat');
- }
- $this->composeHeaders($exception);
- /**
- * @var iCompose Default Composer
- */
- $compose = Scope::get(Defaults::$composeClass);
- $this->responseData = $this->responseFormat->encode(
- $compose->message($exception),
- !$this->productionMode
- );
- $this->respond();
- }
- /**
- * Provides backward compatibility with older versions of Restler
- *
- * @param int $version restler version
- *
- * @throws \OutOfRangeException
- */
- public function setCompatibilityMode($version = 2)
- {
- if ($version <= intval(self::VERSION) && $version > 0) {
- require __DIR__."/compatibility/restler{$version}.php";
- return;
- }
- throw new \OutOfRangeException();
- }
- /**
- * @param int $version maximum version number supported
- * by the api
- * @param int $minimum minimum version number supported
- * (optional)
- *
- * @throws InvalidArgumentException
- * @return void
- */
- public function setAPIVersion($version = 1, $minimum = 1)
- {
- if (!is_int($version) && $version < 1) {
- throw new InvalidArgumentException
- ('version should be an integer greater than 0');
- }
- $this->apiVersion = $version;
- if (is_int($minimum)) {
- $this->apiMinimumVersion = $minimum;
- }
- }
- /**
- * Classes implementing iFilter interface can be added for filtering out
- * the api consumers.
- *
- * It can be used for rate limiting based on usage from a specific ip
- * address or filter by country, device etc.
- *
- * @param $className
- */
- public function addFilterClass($className)
- {
- $this->filterClasses[] = $className;
- }
- /**
- * protected methods will need at least one authentication class to be set
- * in order to allow that method to be executed
- *
- * @param string $className of the authentication class
- * @param string $resourcePath optional url prefix for mapping
- */
- public function addAuthenticationClass($className, $resourcePath = null)
- {
- $this->authClasses[] = $className;
- $this->addAPIClass($className, $resourcePath);
- }
- /**
- * Add api classes through this method.
- *
- * All the public methods that do not start with _ (underscore)
- * will be will be exposed as the public api by default.
- *
- * All the protected methods that do not start with _ (underscore)
- * will exposed as protected api which will require authentication
- *
- * @param string $className name of the service class
- * @param string $resourcePath optional url prefix for mapping, uses
- * lowercase version of the class name when
- * not specified
- *
- * @return null
- *
- * @throws Exception when supplied with invalid class name
- */
- public function addAPIClass($className, $resourcePath = null)
- {
- try{
- if ($this->productionMode && is_null($this->cached)) {
- $routes = $this->cache->get('routes');
- if (isset($routes) && is_array($routes)) {
- $this->apiVersionMap = $routes['apiVersionMap'];
- unset($routes['apiVersionMap']);
- Routes::fromArray($routes);
- $this->cached = true;
- } else {
- $this->cached = false;
- }
- }
- if (isset(Scope::$classAliases[$className])) {
- $className = Scope::$classAliases[$className];
- }
- if (!$this->cached) {
- $maxVersionMethod = '__getMaximumSupportedVersion';
- if (class_exists($className)) {
- if (method_exists($className, $maxVersionMethod)) {
- $max = $className::$maxVersionMethod();
- for ($i = 1; $i <= $max; $i++) {
- $this->apiVersionMap[$className][$i] = $className;
- }
- } else {
- $this->apiVersionMap[$className][1] = $className;
- }
- }
- //versioned api
- if (false !== ($index = strrpos($className, '\\'))) {
- $name = substr($className, 0, $index)
- . '\\v{$version}' . substr($className, $index);
- } else if (false !== ($index = strrpos($className, '_'))) {
- $name = substr($className, 0, $index)
- . '_v{$version}' . substr($className, $index);
- } else {
- $name = 'v{$version}\\' . $className;
- }
- for ($version = $this->apiMinimumVersion;
- $version <= $this->apiVersion;
- $version++) {
- $versionedClassName = str_replace('{$version}', $version,
- $name);
- if (class_exists($versionedClassName)) {
- Routes::addAPIClass($versionedClassName,
- Util::getResourcePath(
- $className,
- $resourcePath
- ),
- $version
- );
- if (method_exists($versionedClassName, $maxVersionMethod)) {
- $max = $versionedClassName::$maxVersionMethod();
- for ($i = $version; $i <= $max; $i++) {
- $this->apiVersionMap[$className][$i] = $versionedClassName;
- }
- } else {
- $this->apiVersionMap[$className][$version] = $versionedClassName;
- }
- } elseif (isset($this->apiVersionMap[$className][$version])) {
- Routes::addAPIClass($this->apiVersionMap[$className][$version],
- Util::getResourcePath(
- $className,
- $resourcePath
- ),
- $version
- );
- }
- }
- }
- } catch (Exception $e) {
- $e = new Exception(
- "addAPIClass('$className') failed. ".$e->getMessage(),
- $e->getCode(),
- $e
- );
- $this->setSupportedFormats('JsonFormat');
- $this->message($e);
- }
- }
- /**
- * Add class for custom error handling
- *
- * @param string $className of the error handling class
- */
- public function addErrorClass($className)
- {
- $this->errorClasses[] = $className;
- }
- /**
- * Associated array that maps formats to their respective format class name
- *
- * @return array
- */
- public function getFormatMap()
- {
- return $this->formatMap;
- }
- /**
- * API version requested by the client
- * @return int
- */
- public function getRequestedApiVersion()
- {
- return $this->requestedApiVersion;
- }
- /**
- * When false, restler will run in debug mode and parse the class files
- * every time to map it to the URL
- *
- * @return bool
- */
- public function getProductionMode()
- {
- return $this->productionMode;
- }
- /**
- * Chosen API version
- *
- * @return int
- */
- public function getApiVersion()
- {
- return $this->apiVersion;
- }
- /**
- * Base Url of the API Service
- *
- * @return string
- *
- * @example http://localhost/restler3
- * @example http://restler3.com
- */
- public function getBaseUrl()
- {
- return $this->baseUrl;
- }
- /**
- * List of events that fired already
- *
- * @return array
- */
- public function getEvents()
- {
- return $this->events;
- }
- /**
- * Magic method to expose some protected variables
- *
- * @param string $name name of the hidden property
- *
- * @return null|mixed
- */
- public function __get($name)
- {
- if ($name{0} == '_') {
- $hiddenProperty = substr($name, 1);
- if (isset($this->$hiddenProperty)) {
- return $this->$hiddenProperty;
- }
- }
- return null;
- }
- /**
- * Store the url map cache if needed
- */
- public function __destruct()
- {
- if ($this->productionMode && !$this->cached) {
- $this->cache->set(
- 'routes',
- Routes::toArray() +
- array('apiVersionMap' => $this->apiVersionMap)
- );
- }
- }
- /**
- * pre call
- *
- * call _pre_{methodName)_{extension} if exists with the same parameters as
- * the api method
- *
- * @example _pre_get_json
- *
- */
- protected function preCall()
- {
- $o = & $this->apiMethodInfo;
- $preCall = '_pre_' . $o->methodName . '_'
- . $this->requestFormat->getExtension();
- if (method_exists($o->className, $preCall)) {
- $this->dispatch('preCall');
- call_user_func_array(array(
- Scope::get($o->className),
- $preCall
- ), $o->parameters);
- }
- }
- /**
- * post call
- *
- * call _post_{methodName}_{extension} if exists with the composed and
- * serialized (applying the repose format) response data
- *
- * @example _post_get_json
- */
- protected function postCall()
- {
- $o = & $this->apiMethodInfo;
- $postCall = '_post_' . $o->methodName . '_' .
- $this->responseFormat->getExtension();
- if (method_exists($o->className, $postCall)) {
- $this->dispatch('postCall');
- $this->responseData = call_user_func(array(
- Scope::get($o->className),
- $postCall
- ), $this->responseData);
- }
- }
- }