PageRenderTime 25ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/vendor/Luracast/Restler/Restler.php

https://github.com/justericgg/Restler
PHP | 1451 lines | 945 code | 90 blank | 416 comment | 167 complexity | 9a405f8b8b86944ea6fae98c02da5a34 MD5 | raw file
  1. <?php
  2. namespace Luracast\Restler;
  3. use Exception;
  4. use InvalidArgumentException;
  5. use Luracast\Restler\Data\ApiMethodInfo;
  6. use Luracast\Restler\Data\ValidationInfo;
  7. use Luracast\Restler\Data\Validator;
  8. use Luracast\Restler\Format\iFormat;
  9. use Luracast\Restler\Format\iDecodeStream;
  10. use Luracast\Restler\Format\UrlEncodedFormat;
  11. /**
  12. * REST API Server. It is the server part of the Restler framework.
  13. * inspired by the RestServer code from
  14. * <http://jacwright.com/blog/resources/RestServer.txt>
  15. *
  16. * @category Framework
  17. * @package Restler
  18. * @author R.Arul Kumaran <arul@luracast.com>
  19. * @copyright 2010 Luracast
  20. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  21. * @link http://luracast.com/products/restler/
  22. * @version 3.0.0rc5
  23. */
  24. class Restler extends EventDispatcher
  25. {
  26. const VERSION = '3.0.0rc5';
  27. // ==================================================================
  28. //
  29. // Public variables
  30. //
  31. // ------------------------------------------------------------------
  32. /**
  33. * Reference to the last exception thrown
  34. * @var RestException
  35. */
  36. public $exception = null;
  37. /**
  38. * Used in production mode to store the routes and more
  39. *
  40. * @var iCache
  41. */
  42. public $cache;
  43. /**
  44. * URL of the currently mapped service
  45. *
  46. * @var string
  47. */
  48. public $url;
  49. /**
  50. * Http request method of the current request.
  51. * Any value between [GET, PUT, POST, DELETE]
  52. *
  53. * @var string
  54. */
  55. public $requestMethod;
  56. /**
  57. * Requested data format.
  58. * Instance of the current format class
  59. * which implements the iFormat interface
  60. *
  61. * @var iFormat
  62. * @example jsonFormat, xmlFormat, yamlFormat etc
  63. */
  64. public $requestFormat;
  65. /**
  66. * Response data format.
  67. *
  68. * Instance of the current format class
  69. * which implements the iFormat interface
  70. *
  71. * @var iFormat
  72. * @example jsonFormat, xmlFormat, yamlFormat etc
  73. */
  74. public $responseFormat;
  75. /**
  76. * Http status code
  77. *
  78. * @var int
  79. */
  80. public $responseCode=200;
  81. /**
  82. * @var string base url of the api service
  83. */
  84. protected $baseUrl;
  85. /**
  86. * @var bool Used for waiting till verifying @format
  87. * before throwing content negotiation failed
  88. */
  89. protected $requestFormatDiffered = false;
  90. /**
  91. * method information including metadata
  92. *
  93. * @var ApiMethodInfo
  94. */
  95. public $apiMethodInfo;
  96. /**
  97. * @var int for calculating execution time
  98. */
  99. protected $startTime;
  100. /**
  101. * When set to false, it will run in debug mode and parse the
  102. * class files every time to map it to the URL
  103. *
  104. * @var boolean
  105. */
  106. protected $productionMode = false;
  107. public $refreshCache = false;
  108. /**
  109. * Caching of url map is enabled or not
  110. *
  111. * @var boolean
  112. */
  113. protected $cached;
  114. /**
  115. * @var int
  116. */
  117. protected $apiVersion = 1;
  118. /**
  119. * @var int
  120. */
  121. protected $requestedApiVersion = 1;
  122. /**
  123. * @var int
  124. */
  125. protected $apiMinimumVersion = 1;
  126. /**
  127. * @var array
  128. */
  129. protected $apiVersionMap = array();
  130. /**
  131. * Associated array that maps formats to their respective format class name
  132. *
  133. * @var array
  134. */
  135. protected $formatMap = array();
  136. /**
  137. * List of the Mime Types that can be produced as a response by this API
  138. *
  139. * @var array
  140. */
  141. protected $writableMimeTypes = array();
  142. /**
  143. * List of the Mime Types that are supported for incoming requests by this API
  144. *
  145. * @var array
  146. */
  147. protected $readableMimeTypes = array();
  148. /**
  149. * Associated array that maps formats to their respective format class name
  150. *
  151. * @var array
  152. */
  153. protected $formatOverridesMap = array('extensions' => array());
  154. /**
  155. * list of filter classes
  156. *
  157. * @var array
  158. */
  159. protected $filterClasses = array();
  160. /**
  161. * instances of filter classes that are executed after authentication
  162. *
  163. * @var array
  164. */
  165. protected $postAuthFilterClasses = array();
  166. // ==================================================================
  167. //
  168. // Protected variables
  169. //
  170. // ------------------------------------------------------------------
  171. /**
  172. * Data sent to the service
  173. *
  174. * @var array
  175. */
  176. protected $requestData = array();
  177. /**
  178. * list of authentication classes
  179. *
  180. * @var array
  181. */
  182. protected $authClasses = array();
  183. /**
  184. * list of error handling classes
  185. *
  186. * @var array
  187. */
  188. protected $errorClasses = array();
  189. protected $authenticated = false;
  190. protected $authVerified = false;
  191. /**
  192. * @var mixed
  193. */
  194. protected $responseData;
  195. /**
  196. * Constructor
  197. *
  198. * @param boolean $productionMode When set to false, it will run in
  199. * debug mode and parse the class files
  200. * every time to map it to the URL
  201. *
  202. * @param bool $refreshCache will update the cache when set to true
  203. */
  204. public function __construct($productionMode = false, $refreshCache = false)
  205. {
  206. parent::__construct();
  207. $this->startTime = time();
  208. Util::$restler = $this;
  209. Scope::set('Restler', $this);
  210. $this->productionMode = $productionMode;
  211. if (is_null(Defaults::$cacheDirectory)) {
  212. Defaults::$cacheDirectory = dirname($_SERVER['SCRIPT_FILENAME']) .
  213. DIRECTORY_SEPARATOR . 'cache';
  214. }
  215. $this->cache = new Defaults::$cacheClass();
  216. $this->refreshCache = $refreshCache;
  217. // use this to rebuild cache every time in production mode
  218. if ($productionMode && $refreshCache) {
  219. $this->cached = false;
  220. }
  221. }
  222. /**
  223. * Main function for processing the api request
  224. * and return the response
  225. *
  226. * @throws Exception when the api service class is missing
  227. * @throws RestException to send error response
  228. */
  229. public function handle()
  230. {
  231. try {
  232. try {
  233. try {
  234. $this->get();
  235. } catch (Exception $e) {
  236. $this->requestData
  237. = array(Defaults::$fullRequestDataName => array());
  238. if (!$e instanceof RestException) {
  239. $e = new RestException(
  240. 500,
  241. $this->productionMode ? null : $e->getMessage(),
  242. array(),
  243. $e
  244. );
  245. }
  246. $this->route();
  247. throw $e;
  248. }
  249. if (Defaults::$useVendorMIMEVersioning)
  250. $this->responseFormat = $this->negotiateResponseFormat();
  251. $this->route();
  252. } catch (Exception $e) {
  253. $this->negotiate();
  254. if (!$e instanceof RestException) {
  255. $e = new RestException(
  256. 500,
  257. $this->productionMode ? null : $e->getMessage(),
  258. array(),
  259. $e
  260. );
  261. }
  262. throw $e;
  263. }
  264. $this->negotiate();
  265. $this->preAuthFilter();
  266. $this->authenticate();
  267. $this->postAuthFilter();
  268. $this->validate();
  269. $this->preCall();
  270. $this->call();
  271. $this->compose();
  272. $this->postCall();
  273. $this->respond();
  274. } catch (Exception $e) {
  275. try{
  276. $this->message($e);
  277. } catch (Exception $e2) {
  278. $this->message($e2);
  279. }
  280. }
  281. }
  282. /**
  283. * read the request details
  284. *
  285. * Find out the following
  286. * - baseUrl
  287. * - url requested
  288. * - version requested (if url based versioning)
  289. * - http verb/method
  290. * - negotiate content type
  291. * - request data
  292. * - set defaults
  293. */
  294. protected function get()
  295. {
  296. $this->dispatch('get');
  297. if (empty($this->formatMap)) {
  298. $this->setSupportedFormats('JsonFormat');
  299. }
  300. $this->url = $this->getPath();
  301. $this->requestMethod = Util::getRequestMethod();
  302. $this->requestFormat = $this->getRequestFormat();
  303. $this->requestData = $this->getRequestData(false);
  304. //parse defaults
  305. foreach ($_GET as $key => $value) {
  306. if (isset(Defaults::$aliases[$key])) {
  307. $_GET[Defaults::$aliases[$key]] = $value;
  308. unset($_GET[$key]);
  309. $key = Defaults::$aliases[$key];
  310. }
  311. if (in_array($key, Defaults::$overridables)) {
  312. Defaults::setProperty($key, $value);
  313. }
  314. }
  315. }
  316. /**
  317. * Returns a list of the mime types (e.g. ["application/json","application/xml"]) that the API can respond with
  318. * @return array
  319. */
  320. public function getWritableMimeTypes()
  321. {
  322. return $this->writableMimeTypes;
  323. }
  324. /**
  325. * Returns the list of Mime Types for the request that the API can understand
  326. * @return array
  327. */
  328. public function getReadableMimeTypes()
  329. {
  330. return $this->readableMimeTypes;
  331. }
  332. /**
  333. * Call this method and pass all the formats that should be supported by
  334. * the API Server. Accepts multiple parameters
  335. *
  336. * @param string ,... $formatName class name of the format class that
  337. * implements iFormat
  338. *
  339. * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
  340. * @throws Exception
  341. */
  342. public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/)
  343. {
  344. $args = func_get_args();
  345. $extensions = array();
  346. $throwException = $this->requestFormatDiffered;
  347. $this->writableMimeTypes = $this->readableMimeTypes = array();
  348. foreach ($args as $className) {
  349. $obj = Scope::get($className);
  350. if (!$obj instanceof iFormat)
  351. throw new Exception('Invalid format class; must implement ' .
  352. 'iFormat interface');
  353. if ($throwException && get_class($obj) == get_class($this->requestFormat)) {
  354. $throwException = false;
  355. }
  356. foreach ($obj->getMIMEMap() as $mime => $extension) {
  357. if($obj->isWritable()){
  358. $this->writableMimeTypes[]=$mime;
  359. $extensions[".$extension"] = true;
  360. }
  361. if($obj->isReadable())
  362. $this->readableMimeTypes[]=$mime;
  363. if (!isset($this->formatMap[$extension]))
  364. $this->formatMap[$extension] = $className;
  365. if (!isset($this->formatMap[$mime]))
  366. $this->formatMap[$mime] = $className;
  367. }
  368. }
  369. if ($throwException) {
  370. throw new RestException(
  371. 403,
  372. 'Content type `' . $this->requestFormat->getMIME() . '` is not supported.'
  373. );
  374. }
  375. $this->formatMap['default'] = $args[0];
  376. $this->formatMap['extensions'] = array_keys($extensions);
  377. }
  378. /**
  379. * Call this method and pass all the formats that can be used to override
  380. * the supported formats using `@format` comment. Accepts multiple parameters
  381. *
  382. * @param string ,... $formatName class name of the format class that
  383. * implements iFormat
  384. *
  385. * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...);
  386. * @throws Exception
  387. */
  388. public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/)
  389. {
  390. $args = func_get_args();
  391. $extensions = array();
  392. foreach ($args as $className) {
  393. $obj = Scope::get($className);
  394. if (!$obj instanceof iFormat)
  395. throw new Exception('Invalid format class; must implement ' .
  396. 'iFormat interface');
  397. foreach ($obj->getMIMEMap() as $mime => $extension) {
  398. if (!isset($this->formatOverridesMap[$extension]))
  399. $this->formatOverridesMap[$extension] = $className;
  400. if (!isset($this->formatOverridesMap[$mime]))
  401. $this->formatOverridesMap[$mime] = $className;
  402. if($obj->isWritable())
  403. $extensions[".$extension"] = true;
  404. }
  405. }
  406. $this->formatOverridesMap['extensions'] = array_keys($extensions);
  407. }
  408. /**
  409. * Parses the request url and get the api path
  410. *
  411. * @return string api path
  412. */
  413. protected function getPath()
  414. {
  415. // fix SCRIPT_NAME for PHP 5.4 built-in web server
  416. if (false === strpos($_SERVER['SCRIPT_NAME'], '.php'))
  417. $_SERVER['SCRIPT_NAME']
  418. = '/' . Util::removeCommonPath($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']);
  419. $fullPath = urldecode($_SERVER['REQUEST_URI']);
  420. $path = Util::removeCommonPath(
  421. $fullPath,
  422. $_SERVER['SCRIPT_NAME']
  423. );
  424. $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80';
  425. $https = $port == '443' ||
  426. (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB
  427. (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on');
  428. $baseUrl = ($https ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'];
  429. if (!$https && $port != '80' || !$https && $port != '443')
  430. $baseUrl .= ':' . $port;
  431. $this->baseUrl = rtrim($baseUrl
  432. . substr($fullPath, 0, strlen($fullPath) - strlen($path)), '/');
  433. $path = rtrim(strtok($path, '?'), '/'); //remove query string and trailing slash if found any
  434. $path = str_replace(
  435. array_merge(
  436. $this->formatMap['extensions'],
  437. $this->formatOverridesMap['extensions']
  438. ),
  439. '',
  440. $path
  441. );
  442. if (Defaults::$useUrlBasedVersioning && strlen($path) && $path{0} == 'v') {
  443. $version = intval(substr($path, 1));
  444. if ($version && $version <= $this->apiVersion) {
  445. $this->requestedApiVersion = $version;
  446. $path = explode('/', $path, 2);
  447. $path = $path[1];
  448. }
  449. } else {
  450. $this->requestedApiVersion = $this->apiMinimumVersion;
  451. }
  452. return $path;
  453. }
  454. /**
  455. * Parses the request to figure out format of the request data
  456. *
  457. * @throws RestException
  458. * @return iFormat any class that implements iFormat
  459. * @example JsonFormat
  460. */
  461. protected function getRequestFormat()
  462. {
  463. $format = null ;
  464. // check if client has sent any information on request format
  465. if (
  466. !empty($_SERVER['CONTENT_TYPE']) ||
  467. (
  468. !empty($_SERVER['HTTP_CONTENT_TYPE']) &&
  469. $_SERVER['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']
  470. )
  471. ) {
  472. $mime = $_SERVER['CONTENT_TYPE'];
  473. if (false !== $pos = strpos($mime, ';')) {
  474. $mime = substr($mime, 0, $pos);
  475. }
  476. if ($mime == UrlEncodedFormat::MIME)
  477. $format = Scope::get('UrlEncodedFormat');
  478. elseif (isset($this->formatMap[$mime])) {
  479. $format = Scope::get($this->formatMap[$mime]);
  480. $format->setMIME($mime);
  481. } elseif (!$this->requestFormatDiffered && isset($this->formatOverridesMap[$mime])) {
  482. //if our api method is not using an @format comment
  483. //to point to this $mime, we need to throw 403 as in below
  484. //but since we don't know that yet, we need to defer that here
  485. $format = Scope::get($this->formatOverridesMap[$mime]);
  486. $format->setMIME($mime);
  487. $this->requestFormatDiffered = true;
  488. } else {
  489. throw new RestException(
  490. 403,
  491. "Content type `$mime` is not supported."
  492. );
  493. }
  494. }
  495. if(!$format){
  496. $format = Scope::get($this->formatMap['default']);
  497. }
  498. return $format;
  499. }
  500. public function getRequestStream()
  501. {
  502. static $tempStream = false;
  503. if (!$tempStream) {
  504. $tempStream = fopen('php://temp', 'r+');
  505. $rawInput = fopen('php://input', 'r');
  506. stream_copy_to_stream($rawInput, $tempStream);
  507. }
  508. rewind($tempStream);
  509. return $tempStream;
  510. }
  511. /**
  512. * Parses the request data and returns it
  513. *
  514. * @param bool $includeQueryParameters
  515. *
  516. * @return array php data
  517. */
  518. public function getRequestData($includeQueryParameters = true)
  519. {
  520. $get = UrlEncodedFormat::decoderTypeFix($_GET);
  521. if ($this->requestMethod == 'PUT'
  522. || $this->requestMethod == 'PATCH'
  523. || $this->requestMethod == 'POST'
  524. ) {
  525. if (!empty($this->requestData)) {
  526. return $includeQueryParameters
  527. ? $this->requestData + $get
  528. : $this->requestData;
  529. }
  530. $stream = $this->getRequestStream();
  531. if($stream === FALSE)
  532. return array();
  533. $r = $this->requestFormat instanceof iDecodeStream
  534. ? $this->requestFormat->decodeStream($stream)
  535. : $this->requestFormat->decode(stream_get_contents($stream));
  536. $r = is_array($r)
  537. ? array_merge($r, array(Defaults::$fullRequestDataName => $r))
  538. : array(Defaults::$fullRequestDataName => $r);
  539. return $includeQueryParameters
  540. ? $r + $get
  541. : $r;
  542. }
  543. return $includeQueryParameters ? $get : array(); //no body
  544. }
  545. /**
  546. * Find the api method to execute for the requested Url
  547. */
  548. protected function route()
  549. {
  550. $this->dispatch('route');
  551. $params = $this->getRequestData();
  552. //backward compatibility for restler 2 and below
  553. if (!Defaults::$smartParameterParsing) {
  554. $params = $params + array(Defaults::$fullRequestDataName => $params);
  555. }
  556. $this->apiMethodInfo = $o = Routes::find(
  557. $this->url, $this->requestMethod,
  558. $this->requestedApiVersion, $params
  559. );
  560. //set defaults based on api method comments
  561. if (isset($o->metadata)) {
  562. foreach (Defaults::$fromComments as $key => $defaultsKey) {
  563. if (array_key_exists($key, $o->metadata)) {
  564. $value = $o->metadata[$key];
  565. Defaults::setProperty($defaultsKey, $value);
  566. }
  567. }
  568. }
  569. if (!isset($o->className))
  570. throw new RestException(404);
  571. if(isset($this->apiVersionMap[$o->className])){
  572. Scope::$classAliases[Util::getShortName($o->className)]
  573. = $this->apiVersionMap[$o->className][$this->requestedApiVersion];
  574. }
  575. foreach ($this->authClasses as $auth) {
  576. if (isset($this->apiVersionMap[$auth])) {
  577. Scope::$classAliases[$auth] = $this->apiVersionMap[$auth][$this->requestedApiVersion];
  578. } elseif (isset($this->apiVersionMap[Scope::$classAliases[$auth]])) {
  579. Scope::$classAliases[$auth]
  580. = $this->apiVersionMap[Scope::$classAliases[$auth]][$this->requestedApiVersion];
  581. }
  582. }
  583. }
  584. /**
  585. * Negotiate the response details such as
  586. * - cross origin resource sharing
  587. * - media type
  588. * - charset
  589. * - language
  590. */
  591. protected function negotiate()
  592. {
  593. $this->dispatch('negotiate');
  594. $this->negotiateCORS();
  595. $this->responseFormat = $this->negotiateResponseFormat();
  596. $this->negotiateCharset();
  597. $this->negotiateLanguage();
  598. }
  599. protected function negotiateCORS()
  600. {
  601. if (
  602. $this->requestMethod == 'OPTIONS'
  603. && Defaults::$crossOriginResourceSharing
  604. ) {
  605. if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
  606. header('Access-Control-Allow-Methods: '
  607. . Defaults::$accessControlAllowMethods);
  608. if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
  609. header('Access-Control-Allow-Headers: '
  610. . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
  611. header('Access-Control-Allow-Origin: ' .
  612. (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin));
  613. header('Access-Control-Allow-Credentials: true');
  614. exit(0);
  615. }
  616. }
  617. // ==================================================================
  618. //
  619. // Protected functions
  620. //
  621. // ------------------------------------------------------------------
  622. /**
  623. * Parses the request to figure out the best format for response.
  624. * Extension, if present, overrides the Accept header
  625. *
  626. * @throws RestException
  627. * @return iFormat
  628. * @example JsonFormat
  629. */
  630. protected function negotiateResponseFormat()
  631. {
  632. $metadata = Util::nestedValue($this, 'apiMethodInfo', 'metadata');
  633. //check if the api method insists on response format using @format comment
  634. if ($metadata && isset($metadata['format'])) {
  635. $formats = explode(',', (string)$metadata['format']);
  636. foreach ($formats as $i => $f) {
  637. $f = trim($f);
  638. if (!in_array($f, $this->formatOverridesMap))
  639. throw new RestException(
  640. 500,
  641. "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first."
  642. );
  643. $formats[$i] = $f;
  644. }
  645. call_user_func_array(array($this, 'setSupportedFormats'), $formats);
  646. }
  647. // check if client has specified an extension
  648. /** @var $format iFormat*/
  649. $format = null;
  650. $extensions = explode(
  651. '.',
  652. parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
  653. );
  654. while ($extensions) {
  655. $extension = array_pop($extensions);
  656. $extension = explode('/', $extension);
  657. $extension = array_shift($extension);
  658. if ($extension && isset($this->formatMap[$extension])) {
  659. $format = Scope::get($this->formatMap[$extension]);
  660. $format->setExtension($extension);
  661. // echo "Extension $extension";
  662. return $format;
  663. }
  664. }
  665. // check if client has sent list of accepted data formats
  666. if (isset($_SERVER['HTTP_ACCEPT'])) {
  667. $acceptList = Util::sortByPriority($_SERVER['HTTP_ACCEPT']);
  668. foreach ($acceptList as $accept => $quality) {
  669. if (isset($this->formatMap[$accept])) {
  670. $format = Scope::get($this->formatMap[$accept]);
  671. $format->setMIME($accept);
  672. //echo "MIME $accept";
  673. // Tell cache content is based on Accept header
  674. @header('Vary: Accept');
  675. return $format;
  676. } elseif (false !== ($index = strrpos($accept, '+'))) {
  677. $mime = substr($accept, 0, $index);
  678. if (is_string(Defaults::$apiVendor)
  679. && 0 === stripos($mime,
  680. 'application/vnd.'
  681. . Defaults::$apiVendor . '-v')
  682. ) {
  683. $extension = substr($accept, $index + 1);
  684. if (isset($this->formatMap[$extension])) {
  685. //check the MIME and extract version
  686. $version = intval(substr($mime,
  687. 18 + strlen(Defaults::$apiVendor)));
  688. if ($version > 0 && $version <= $this->apiVersion) {
  689. $this->requestedApiVersion = $version;
  690. $format = Scope::get($this->formatMap[$extension]);
  691. $format->setExtension($extension);
  692. // echo "Extension $extension";
  693. Defaults::$useVendorMIMEVersioning = true;
  694. @header('Vary: Accept');
  695. return $format;
  696. }
  697. }
  698. }
  699. }
  700. }
  701. } else {
  702. // RFC 2616: If no Accept header field is
  703. // present, then it is assumed that the
  704. // client accepts all media types.
  705. $_SERVER['HTTP_ACCEPT'] = '*/*';
  706. }
  707. if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) {
  708. if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/*')) {
  709. $format = Scope::get('JsonFormat');
  710. } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/*')) {
  711. $format = Scope::get('XmlFormat');
  712. } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], '*/*')) {
  713. $format = Scope::get($this->formatMap['default']);
  714. }
  715. }
  716. if (empty($format)) {
  717. // RFC 2616: If an Accept header field is present, and if the
  718. // server cannot send a response which is acceptable according to
  719. // the combined Accept field value, then the server SHOULD send
  720. // a 406 (not acceptable) response.
  721. $format = Scope::get($this->formatMap['default']);
  722. $this->responseFormat = $format;
  723. throw new RestException(
  724. 406,
  725. 'Content negotiation failed. ' .
  726. 'Try `' . $format->getMIME() . '` instead.'
  727. );
  728. } else {
  729. // Tell cache content is based at Accept header
  730. @header("Vary: Accept");
  731. return $format;
  732. }
  733. }
  734. protected function negotiateCharset()
  735. {
  736. if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
  737. $found = false;
  738. $charList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_CHARSET']);
  739. foreach ($charList as $charset => $quality) {
  740. if (in_array($charset, Defaults::$supportedCharsets)) {
  741. $found = true;
  742. Defaults::$charset = $charset;
  743. break;
  744. }
  745. }
  746. if (!$found) {
  747. if (strpos($_SERVER['HTTP_ACCEPT_CHARSET'], '*') !== false) {
  748. //use default charset
  749. } else {
  750. throw new RestException(
  751. 406,
  752. 'Content negotiation failed. ' .
  753. 'Requested charset is not supported'
  754. );
  755. }
  756. }
  757. }
  758. }
  759. protected function negotiateLanguage()
  760. {
  761. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  762. $found = false;
  763. $langList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_LANGUAGE']);
  764. foreach ($langList as $lang => $quality) {
  765. foreach (Defaults::$supportedLanguages as $supported) {
  766. if (strcasecmp($supported, $lang) == 0) {
  767. $found = true;
  768. Defaults::$language = $supported;
  769. break 2;
  770. }
  771. }
  772. }
  773. if (!$found) {
  774. if (strpos($_SERVER['HTTP_ACCEPT_LANGUAGE'], '*') !== false) {
  775. //use default language
  776. } else {
  777. //ignore
  778. }
  779. }
  780. }
  781. }
  782. /**
  783. * Filer api calls before authentication
  784. */
  785. protected function preAuthFilter()
  786. {
  787. if (empty($this->filterClasses)) {
  788. return;
  789. }
  790. $this->dispatch('preAuthFilter');
  791. foreach ($this->filterClasses as $filterClass) {
  792. /**
  793. * @var iFilter
  794. */
  795. $filterObj = Scope::get($filterClass);
  796. if (!$filterObj instanceof iFilter) {
  797. throw new RestException (
  798. 500, 'Filter Class ' .
  799. 'should implement iFilter');
  800. } else if (!($ok = $filterObj->__isAllowed())) {
  801. if (is_null($ok)
  802. && $filterObj instanceof iUseAuthentication
  803. ) {
  804. //handle at authentication stage
  805. $this->postAuthFilterClasses[] = $filterClass;
  806. continue;
  807. }
  808. throw new RestException(403); //Forbidden
  809. }
  810. }
  811. }
  812. protected function authenticate()
  813. {
  814. $o = & $this->apiMethodInfo;
  815. $accessLevel = max(Defaults::$apiAccessLevel,
  816. $o->accessLevel);
  817. try {
  818. if ($accessLevel || count($this->postAuthFilterClasses)) {
  819. $this->dispatch('authenticate');
  820. if (!count($this->authClasses)) {
  821. throw new RestException(
  822. 403,
  823. 'at least one Authentication Class is required'
  824. );
  825. }
  826. foreach ($this->authClasses as $authClass) {
  827. $authObj = Scope::get($authClass);
  828. if (!method_exists($authObj,
  829. Defaults::$authenticationMethod)
  830. ) {
  831. throw new RestException (
  832. 500, 'Authentication Class ' .
  833. 'should implement iAuthenticate');
  834. } elseif (
  835. !$authObj->{Defaults::$authenticationMethod}()
  836. ) {
  837. throw new RestException(401);
  838. }
  839. }
  840. $this->authenticated = true;
  841. }
  842. $this->authVerified = true;
  843. } catch (RestException $e) {
  844. $this->authVerified = true;
  845. if ($accessLevel > 1) { //when it is not a hybrid api
  846. throw ($e);
  847. } else {
  848. $this->authenticated = false;
  849. }
  850. }
  851. }
  852. /**
  853. * Filer api calls after authentication
  854. */
  855. protected function postAuthFilter()
  856. {
  857. if(empty($this->postAuthFilterClasses)) {
  858. return;
  859. }
  860. $this->dispatch('postAuthFilter');
  861. foreach ($this->postAuthFilterClasses as $filterClass) {
  862. Scope::get($filterClass);
  863. }
  864. }
  865. protected function validate()
  866. {
  867. if (!Defaults::$autoValidationEnabled) {
  868. return;
  869. }
  870. $this->dispatch('validate');
  871. $o = & $this->apiMethodInfo;
  872. foreach ($o->metadata['param'] as $index => $param) {
  873. $info = & $param [CommentParser::$embeddedDataName];
  874. if (!isset ($info['validate'])
  875. || $info['validate'] != false
  876. ) {
  877. if (isset($info['method'])) {
  878. $info ['apiClassInstance'] = Scope::get($o->className);
  879. }
  880. //convert to instance of ValidationInfo
  881. $info = new ValidationInfo($param);
  882. $validator = Defaults::$validatorClass;
  883. //if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) {
  884. //changed the above test to below for addressing this php bug
  885. //https://bugs.php.net/bug.php?id=53727
  886. if (function_exists("$validator::validate")) {
  887. throw new \UnexpectedValueException(
  888. '`Defaults::$validatorClass` must implement `iValidate` interface'
  889. );
  890. }
  891. $valid = $o->parameters[$index];
  892. $o->parameters[$index] = null;
  893. if (empty(Validator::$exceptions))
  894. $o->metadata['param'][$index]['autofocus'] = true;
  895. $valid = $validator::validate(
  896. $valid, $info
  897. );
  898. $o->parameters[$index] = $valid;
  899. unset($o->metadata['param'][$index]['autofocus']);
  900. }
  901. }
  902. }
  903. protected function call()
  904. {
  905. $this->dispatch('call');
  906. $o = & $this->apiMethodInfo;
  907. $accessLevel = max(Defaults::$apiAccessLevel,
  908. $o->accessLevel);
  909. $object = Scope::get($o->className);
  910. switch ($accessLevel) {
  911. case 3 : //protected method
  912. $reflectionMethod = new \ReflectionMethod(
  913. $object,
  914. $o->methodName
  915. );
  916. $reflectionMethod->setAccessible(true);
  917. $result = $reflectionMethod->invokeArgs(
  918. $object,
  919. $o->parameters
  920. );
  921. break;
  922. default :
  923. $result = call_user_func_array(array(
  924. $object,
  925. $o->methodName
  926. ), $o->parameters);
  927. }
  928. $this->responseData = $result;
  929. }
  930. protected function compose()
  931. {
  932. $this->dispatch('compose');
  933. $this->composeHeaders();
  934. /**
  935. * @var iCompose Default Composer
  936. */
  937. $compose = Scope::get(Defaults::$composeClass);
  938. $this->responseData = is_null($this->responseData) &&
  939. Defaults::$emptyBodyForNullResponse
  940. ? ''
  941. : $this->responseFormat->encode(
  942. $compose->response($this->responseData),
  943. !$this->productionMode
  944. );
  945. }
  946. public function composeHeaders(RestException $e = null)
  947. {
  948. //only GET method should be cached if allowed by API developer
  949. $expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0;
  950. if(!is_array(Defaults::$headerCacheControl))
  951. Defaults::$headerCacheControl = array(Defaults::$headerCacheControl);
  952. $cacheControl = Defaults::$headerCacheControl[0];
  953. if ($expires > 0) {
  954. $cacheControl = $this->apiMethodInfo->accessLevel
  955. ? 'private, ' : 'public, ';
  956. $cacheControl .= end(Defaults::$headerCacheControl);
  957. $cacheControl = str_replace('{expires}', $expires, $cacheControl);
  958. $expires = gmdate('D, d M Y H:i:s \G\M\T', time() + $expires);
  959. }
  960. @header('Cache-Control: ' . $cacheControl);
  961. @header('Expires: ' . $expires);
  962. @header('X-Powered-By: Luracast Restler v' . Restler::VERSION);
  963. if (Defaults::$crossOriginResourceSharing
  964. && isset($_SERVER['HTTP_ORIGIN'])
  965. ) {
  966. header('Access-Control-Allow-Origin: ' .
  967. (Defaults::$accessControlAllowOrigin == '*'
  968. ? $_SERVER['HTTP_ORIGIN']
  969. : Defaults::$accessControlAllowOrigin)
  970. );
  971. header('Access-Control-Allow-Credentials: true');
  972. header('Access-Control-Max-Age: 86400');
  973. }
  974. $this->responseFormat->setCharset(Defaults::$charset);
  975. $charset = $this->responseFormat->getCharset()
  976. ? : Defaults::$charset;
  977. @header('Content-Type: ' . (
  978. Defaults::$useVendorMIMEVersioning
  979. ? 'application/vnd.'
  980. . Defaults::$apiVendor
  981. . "-v{$this->requestedApiVersion}"
  982. . '+' . $this->responseFormat->getExtension()
  983. : $this->responseFormat->getMIME())
  984. . '; charset=' . $charset
  985. );
  986. @header('Content-Language: ' . Defaults::$language);
  987. if (isset($this->apiMethodInfo->metadata['header'])) {
  988. foreach ($this->apiMethodInfo->metadata['header'] as $header)
  989. @header($header, true);
  990. }
  991. $code = 200;
  992. if (!Defaults::$suppressResponseCode) {
  993. if ($e) {
  994. $code = $e->getCode();
  995. } elseif (isset($this->apiMethodInfo->metadata['status'])) {
  996. $code = $this->apiMethodInfo->metadata['status'];
  997. }
  998. }
  999. $this->responseCode = $code;
  1000. @header(
  1001. "{$_SERVER['SERVER_PROTOCOL']} $code " .
  1002. (isset(RestException::$codes[$code]) ? RestException::$codes[$code] : '')
  1003. );
  1004. }
  1005. protected function respond()
  1006. {
  1007. $this->dispatch('respond');
  1008. //handle throttling
  1009. if (Defaults::$throttle) {
  1010. $elapsed = time() - $this->startTime;
  1011. if (Defaults::$throttle / 1e3 > $elapsed) {
  1012. usleep(1e6 * (Defaults::$throttle / 1e3 - $elapsed));
  1013. }
  1014. }
  1015. echo $this->responseData;
  1016. $this->dispatch('complete');
  1017. if ($this->responseCode == 401) {
  1018. $authString = count($this->authClasses)
  1019. ? Scope::get($this->authClasses[0])->__getWWWAuthenticateString()
  1020. : 'Unknown';
  1021. @header('WWW-Authenticate: ' . $authString, false);
  1022. }
  1023. exit;
  1024. }
  1025. protected function message(Exception $exception)
  1026. {
  1027. $this->dispatch('message');
  1028. if (!$exception instanceof RestException) {
  1029. $exception = new RestException(
  1030. 500,
  1031. $this->productionMode ? null : $exception->getMessage(),
  1032. array(),
  1033. $exception
  1034. );
  1035. }
  1036. $this->exception = $exception;
  1037. $method = 'handle' . $exception->getCode();
  1038. $handled = false;
  1039. foreach ($this->errorClasses as $className) {
  1040. if (method_exists($className, $method)) {
  1041. $obj = Scope::get($className);
  1042. if ($obj->$method())
  1043. $handled = true;
  1044. }
  1045. }
  1046. if ($handled) {
  1047. return;
  1048. }
  1049. if (!isset($this->responseFormat)) {
  1050. $this->responseFormat = Scope::get('JsonFormat');
  1051. }
  1052. $this->composeHeaders($exception);
  1053. /**
  1054. * @var iCompose Default Composer
  1055. */
  1056. $compose = Scope::get(Defaults::$composeClass);
  1057. $this->responseData = $this->responseFormat->encode(
  1058. $compose->message($exception),
  1059. !$this->productionMode
  1060. );
  1061. $this->respond();
  1062. }
  1063. /**
  1064. * Provides backward compatibility with older versions of Restler
  1065. *
  1066. * @param int $version restler version
  1067. *
  1068. * @throws \OutOfRangeException
  1069. */
  1070. public function setCompatibilityMode($version = 2)
  1071. {
  1072. if ($version <= intval(self::VERSION) && $version > 0) {
  1073. require __DIR__."/compatibility/restler{$version}.php";
  1074. return;
  1075. }
  1076. throw new \OutOfRangeException();
  1077. }
  1078. /**
  1079. * @param int $version maximum version number supported
  1080. * by the api
  1081. * @param int $minimum minimum version number supported
  1082. * (optional)
  1083. *
  1084. * @throws InvalidArgumentException
  1085. * @return void
  1086. */
  1087. public function setAPIVersion($version = 1, $minimum = 1)
  1088. {
  1089. if (!is_int($version) && $version < 1) {
  1090. throw new InvalidArgumentException
  1091. ('version should be an integer greater than 0');
  1092. }
  1093. $this->apiVersion = $version;
  1094. if (is_int($minimum)) {
  1095. $this->apiMinimumVersion = $minimum;
  1096. }
  1097. }
  1098. /**
  1099. * Classes implementing iFilter interface can be added for filtering out
  1100. * the api consumers.
  1101. *
  1102. * It can be used for rate limiting based on usage from a specific ip
  1103. * address or filter by country, device etc.
  1104. *
  1105. * @param $className
  1106. */
  1107. public function addFilterClass($className)
  1108. {
  1109. $this->filterClasses[] = $className;
  1110. }
  1111. /**
  1112. * protected methods will need at least one authentication class to be set
  1113. * in order to allow that method to be executed
  1114. *
  1115. * @param string $className of the authentication class
  1116. * @param string $resourcePath optional url prefix for mapping
  1117. */
  1118. public function addAuthenticationClass($className, $resourcePath = null)
  1119. {
  1120. $this->authClasses[] = $className;
  1121. $this->addAPIClass($className, $resourcePath);
  1122. }
  1123. /**
  1124. * Add api classes through this method.
  1125. *
  1126. * All the public methods that do not start with _ (underscore)
  1127. * will be will be exposed as the public api by default.
  1128. *
  1129. * All the protected methods that do not start with _ (underscore)
  1130. * will exposed as protected api which will require authentication
  1131. *
  1132. * @param string $className name of the service class
  1133. * @param string $resourcePath optional url prefix for mapping, uses
  1134. * lowercase version of the class name when
  1135. * not specified
  1136. *
  1137. * @return null
  1138. *
  1139. * @throws Exception when supplied with invalid class name
  1140. */
  1141. public function addAPIClass($className, $resourcePath = null)
  1142. {
  1143. try{
  1144. if ($this->productionMode && is_null($this->cached)) {
  1145. $routes = $this->cache->get('routes');
  1146. if (isset($routes) && is_array($routes)) {
  1147. $this->apiVersionMap = $routes['apiVersionMap'];
  1148. unset($routes['apiVersionMap']);
  1149. Routes::fromArray($routes);
  1150. $this->cached = true;
  1151. } else {
  1152. $this->cached = false;
  1153. }
  1154. }
  1155. if (isset(Scope::$classAliases[$className])) {
  1156. $className = Scope::$classAliases[$className];
  1157. }
  1158. if (!$this->cached) {
  1159. $maxVersionMethod = '__getMaximumSupportedVersion';
  1160. if (class_exists($className)) {
  1161. if (method_exists($className, $maxVersionMethod)) {
  1162. $max = $className::$maxVersionMethod();
  1163. for ($i = 1; $i <= $max; $i++) {
  1164. $this->apiVersionMap[$className][$i] = $className;
  1165. }
  1166. } else {
  1167. $this->apiVersionMap[$className][1] = $className;
  1168. }
  1169. }
  1170. //versioned api
  1171. if (false !== ($index = strrpos($className, '\\'))) {
  1172. $name = substr($className, 0, $index)
  1173. . '\\v{$version}' . substr($className, $index);
  1174. } else if (false !== ($index = strrpos($className, '_'))) {
  1175. $name = substr($className, 0, $index)
  1176. . '_v{$version}' . substr($className, $index);
  1177. } else {
  1178. $name = 'v{$version}\\' . $className;
  1179. }
  1180. for ($version = $this->apiMinimumVersion;
  1181. $version <= $this->apiVersion;
  1182. $version++) {
  1183. $versionedClassName = str_replace('{$version}', $version,
  1184. $name);
  1185. if (class_exists($versionedClassName)) {
  1186. Routes::addAPIClass($versionedClassName,
  1187. Util::getResourcePath(
  1188. $className,
  1189. $resourcePath
  1190. ),
  1191. $version
  1192. );
  1193. if (method_exists($versionedClassName, $maxVersionMethod)) {
  1194. $max = $versionedClassName::$maxVersionMethod();
  1195. for ($i = $version; $i <= $max; $i++) {
  1196. $this->apiVersionMap[$className][$i] = $versionedClassName;
  1197. }
  1198. } else {
  1199. $this->apiVersionMap[$className][$version] = $versionedClassName;
  1200. }
  1201. } elseif (isset($this->apiVersionMap[$className][$version])) {
  1202. Routes::addAPIClass($this->apiVersionMap[$className][$version],
  1203. Util::getResourcePath(
  1204. $className,
  1205. $resourcePath
  1206. ),
  1207. $version
  1208. );
  1209. }
  1210. }
  1211. }
  1212. } catch (Exception $e) {
  1213. $e = new Exception(
  1214. "addAPIClass('$className') failed. ".$e->getMessage(),
  1215. $e->getCode(),
  1216. $e
  1217. );
  1218. $this->setSupportedFormats('JsonFormat');
  1219. $this->message($e);
  1220. }
  1221. }
  1222. /**
  1223. * Add class for custom error handling
  1224. *
  1225. * @param string $className of the error handling class
  1226. */
  1227. public function addErrorClass($className)
  1228. {
  1229. $this->errorClasses[] = $className;
  1230. }
  1231. /**
  1232. * Associated array that maps formats to their respective format class name
  1233. *
  1234. * @return array
  1235. */
  1236. public function getFormatMap()
  1237. {
  1238. return $this->formatMap;
  1239. }
  1240. /**
  1241. * API version requested by the client
  1242. * @return int
  1243. */
  1244. public function getRequestedApiVersion()
  1245. {
  1246. return $this->requestedApiVersion;
  1247. }
  1248. /**
  1249. * When false, restler will run in debug mode and parse the class files
  1250. * every time to map it to the URL
  1251. *
  1252. * @return bool
  1253. */
  1254. public function getProductionMode()
  1255. {
  1256. return $this->productionMode;
  1257. }
  1258. /**
  1259. * Chosen API version
  1260. *
  1261. * @return int
  1262. */
  1263. public function getApiVersion()
  1264. {
  1265. return $this->apiVersion;
  1266. }
  1267. /**
  1268. * Base Url of the API Service
  1269. *
  1270. * @return string
  1271. *
  1272. * @example http://localhost/restler3
  1273. * @example http://restler3.com
  1274. */
  1275. public function getBaseUrl()
  1276. {
  1277. return $this->baseUrl;
  1278. }
  1279. /**
  1280. * List of events that fired already
  1281. *
  1282. * @return array
  1283. */
  1284. public function getEvents()
  1285. {
  1286. return $this->events;
  1287. }
  1288. /**
  1289. * Magic method to expose some protected variables
  1290. *
  1291. * @param string $name name of the hidden property
  1292. *
  1293. * @return null|mixed
  1294. */
  1295. public function __get($name)
  1296. {
  1297. if ($name{0} == '_') {
  1298. $hiddenProperty = substr($name, 1);
  1299. if (isset($this->$hiddenProperty)) {
  1300. return $this->$hiddenProperty;
  1301. }
  1302. }
  1303. return null;
  1304. }
  1305. /**
  1306. * Store the url map cache if needed
  1307. */
  1308. public function __destruct()
  1309. {
  1310. if ($this->productionMode && !$this->cached) {
  1311. $this->cache->set(
  1312. 'routes',
  1313. Routes::toArray() +
  1314. array('apiVersionMap' => $this->apiVersionMap)
  1315. );
  1316. }
  1317. }
  1318. /**
  1319. * pre call
  1320. *
  1321. * call _pre_{methodName)_{extension} if exists with the same parameters as
  1322. * the api method
  1323. *
  1324. * @example _pre_get_json
  1325. *
  1326. */
  1327. protected function preCall()
  1328. {
  1329. $o = & $this->apiMethodInfo;
  1330. $preCall = '_pre_' . $o->methodName . '_'
  1331. . $this->requestFormat->getExtension();
  1332. if (method_exists($o->className, $preCall)) {
  1333. $this->dispatch('preCall');
  1334. call_user_func_array(array(
  1335. Scope::get($o->className),
  1336. $preCall
  1337. ), $o->parameters);
  1338. }
  1339. }
  1340. /**
  1341. * post call
  1342. *
  1343. * call _post_{methodName}_{extension} if exists with the composed and
  1344. * serialized (applying the repose format) response data
  1345. *
  1346. * @example _post_get_json
  1347. */
  1348. protected function postCall()
  1349. {
  1350. $o = & $this->apiMethodInfo;
  1351. $postCall = '_post_' . $o->methodName . '_' .
  1352. $this->responseFormat->getExtension();
  1353. if (method_exists($o->className, $postCall)) {
  1354. $this->dispatch('postCall');
  1355. $this->responseData = call_user_func(array(
  1356. Scope::get($o->className),
  1357. $postCall
  1358. ), $this->responseData);
  1359. }
  1360. }
  1361. }