Restler.php 59 KB

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