PageRenderTime 44ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/servers/rest/request/RestServer.php

https://github.com/mx504/Provisioner
PHP | 537 lines | 436 code | 58 blank | 43 comment | 97 complexity | ef72782c8b1ac47bd044e711eebfc56e MD5 | raw file
  1. <?php
  2. ////////////////////////////////////////////////////////////////////////////////
  3. //
  4. // Copyright (c) 2009 Jacob Wright
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. ////////////////////////////////////////////////////////////////////////////////
  25. /**
  26. * Constants used in RestServer Class.
  27. */
  28. class RestFormat
  29. {
  30. const PLAIN = 'text/plain';
  31. const HTML = 'text/html';
  32. const AMF = 'applicaton/x-amf';
  33. const JSON = 'application/json';
  34. static public $formats = array(
  35. 'plain' => RestFormat::PLAIN,
  36. 'txt' => RestFormat::PLAIN,
  37. 'html' => RestFormat::HTML,
  38. 'amf' => RestFormat::AMF,
  39. 'json' => RestFormat::JSON,
  40. );
  41. }
  42. /**
  43. * Description of RestServer
  44. *
  45. * @author jacob
  46. */
  47. class RestServer
  48. {
  49. public $url;
  50. public $method;
  51. public $params;
  52. public $format;
  53. public $cacheDir = '.';
  54. public $realm;
  55. public $mode;
  56. public $root;
  57. public $remove_path = '';
  58. protected $map = array();
  59. protected $errorClasses = array();
  60. protected $cached;
  61. /**
  62. * The constructor.
  63. *
  64. * @param string $mode The mode, either debug or production
  65. */
  66. public function __construct($mode = 'debug', $realm = 'Rest Server', $remove_path = '')
  67. {
  68. $this->mode = $mode;
  69. $this->realm = $realm;
  70. $dir = dirname(str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME']));
  71. $this->root = ($dir == '.' ? '' : $dir . '/');
  72. $this->remove_path = $remove_path;
  73. }
  74. public function __destruct()
  75. {
  76. if ($this->mode == 'production' && !$this->cached) {
  77. if (function_exists('apc_store')) {
  78. apc_store('urlMap', $this->map);
  79. } else {
  80. file_put_contents($this->cacheDir . '/urlMap.cache', serialize($this->map));
  81. }
  82. }
  83. }
  84. public function refreshCache()
  85. {
  86. $this->map = array();
  87. $this->cached = false;
  88. }
  89. public function unauthorized($ask = false)
  90. {
  91. if ($ask) {
  92. header("WWW-Authenticate: Basic realm=\"$this->realm\"");
  93. }
  94. throw new RestException(401, "You are not authorized to access this resource.");
  95. }
  96. public function handle()
  97. {
  98. $this->url = $this->getPath();
  99. $this->method = $this->getMethod();
  100. $this->format = $this->getFormat();
  101. if ($this->method == 'PUT' || $this->method == 'POST') {
  102. $this->data = $this->getData();
  103. }
  104. list($obj, $method, $params, $this->params, $noAuth) = $this->findUrl();
  105. if ($obj) {
  106. if (is_string($obj)) {
  107. if (class_exists($obj)) {
  108. $obj = new $obj();
  109. } else {
  110. throw new Exception("Class $obj does not exist");
  111. }
  112. }
  113. $obj->server = $this;
  114. try {
  115. if (method_exists($obj, 'init')) {
  116. $obj->init();
  117. }
  118. if (!$noAuth && method_exists($obj, 'authorize')) {
  119. if (!$obj->authorize()) {
  120. $this->sendData($this->unauthorized(true));
  121. exit;
  122. }
  123. }
  124. $result = call_user_func_array(array($obj, $method), $params);
  125. } catch (RestException $e) {
  126. $this->handleError($e->getCode(), $e->getMessage());
  127. }
  128. if ($result !== null) {
  129. $this->sendData($result);
  130. }
  131. } else {
  132. $this->handleError(404);
  133. }
  134. }
  135. public function addClass($class, $basePath = '')
  136. {
  137. $this->loadCache();
  138. if (!$this->cached) {
  139. if (is_string($class) && !class_exists($class)){
  140. throw new Exception('Invalid method or class');
  141. } elseif (!is_string($class) && !is_object($class)) {
  142. throw new Exception('Invalid method or class; must be a classname or object');
  143. }
  144. if (substr($basePath, 0, 1) == '/') {
  145. $basePath = substr($basePath, 1);
  146. }
  147. if ($basePath && substr($basePath, -1) != '/') {
  148. $basePath .= '/';
  149. }
  150. $this->generateMap($class, $basePath);
  151. }
  152. }
  153. public function addErrorClass($class)
  154. {
  155. $this->errorClasses[] = $class;
  156. }
  157. public function handleError($statusCode, $errorMessage = null)
  158. {
  159. $method = "handle$statusCode";
  160. foreach ($this->errorClasses as $class) {
  161. if (is_object($class)) {
  162. $reflection = new ReflectionObject($class);
  163. } elseif (class_exists($class)) {
  164. $reflection = new ReflectionClass($class);
  165. }
  166. if ($reflection->hasMethod($method))
  167. {
  168. $obj = is_string($class) ? new $class() : $class;
  169. $obj->$method();
  170. return;
  171. }
  172. }
  173. $message = $this->codes[$statusCode] . ($errorMessage && $this->mode == 'debug' ? ': ' . $errorMessage : '');
  174. $this->setStatus($statusCode);
  175. $this->sendData(array('error' => array('code' => $statusCode, 'message' => $message)));
  176. }
  177. protected function loadCache()
  178. {
  179. if ($this->cached !== null) {
  180. return;
  181. }
  182. $this->cached = false;
  183. if ($this->mode == 'production') {
  184. if (function_exists('apc_fetch')) {
  185. $map = apc_fetch('urlMap');
  186. } elseif (file_exists($this->cacheDir . '/urlMap.cache')) {
  187. $map = unserialize(file_get_contents($this->cacheDir . '/urlMap.cache'));
  188. }
  189. if ($map && is_array($map)) {
  190. $this->map = $map;
  191. $this->cached = true;
  192. }
  193. } else {
  194. if (function_exists('apc_delete')) {
  195. apc_delete('urlMap');
  196. } else {
  197. @unlink($this->cacheDir . '/urlMap.cache');
  198. }
  199. }
  200. }
  201. protected function findUrl()
  202. {
  203. $urls = $this->map[$this->method];
  204. if (!$urls) return null;
  205. foreach ($urls as $url => $call) {
  206. $args = $call[2];
  207. if (!strstr($url, '$')) {
  208. if ($url == $this->url) {
  209. if (isset($args['data'])) {
  210. $params = array_fill(0, $args['data'] + 1, null);
  211. $params[$args['data']] = $this->data;
  212. $call[2] = $params;
  213. }
  214. return $call;
  215. }
  216. } else {
  217. $regex = preg_replace('/\\\\\$([\w\d]+)\.\.\./', '(?P<$1>.+)', str_replace('\.\.\.', '...', preg_quote($url)));
  218. $regex = preg_replace('/\\\\\$([\w\d]+)/', '(?P<$1>[^\/]+)', $regex);
  219. if (preg_match(":^$regex$:", urldecode($this->url), $matches)) {
  220. $params = array();
  221. $paramMap = array();
  222. if (isset($args['data'])) {
  223. $params[$args['data']] = $this->data;
  224. }
  225. foreach ($matches as $arg => $match) {
  226. if (is_numeric($arg)) continue;
  227. $paramMap[$arg] = $match;
  228. if (isset($args[$arg])) {
  229. $params[$args[$arg]] = $match;
  230. }
  231. }
  232. ksort($params);
  233. // make sure we have all the params we need
  234. end($params);
  235. $max = key($params);
  236. for ($i = 0; $i < $max; $i++) {
  237. if (!key_exists($i, $params)) {
  238. $params[$i] = null;
  239. }
  240. }
  241. ksort($params);
  242. $call[2] = $params;
  243. $call[3] = $paramMap;
  244. return $call;
  245. }
  246. }
  247. }
  248. }
  249. protected function generateMap($class, $basePath)
  250. {
  251. if (is_object($class)) {
  252. $reflection = new ReflectionObject($class);
  253. } elseif (class_exists($class)) {
  254. $reflection = new ReflectionClass($class);
  255. }
  256. $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
  257. foreach ($methods as $method) {
  258. $doc = $method->getDocComment();
  259. $noAuth = strpos($doc, '@noAuth') !== false;
  260. if (preg_match_all('/@url[ \t]+(GET|POST|PUT|DELETE|HEAD|OPTIONS)[ \t]+\/?(\S*)/s', $doc, $matches, PREG_SET_ORDER)) {
  261. $params = $method->getParameters();
  262. foreach ($matches as $match) {
  263. $httpMethod = $match[1];
  264. $url = $basePath . $match[2];
  265. if ($url && $url[strlen($url) - 1] == '/') {
  266. $url = substr($url, 0, -1);
  267. }
  268. $call = array($class, $method->getName());
  269. $args = array();
  270. foreach ($params as $param) {
  271. $args[$param->getName()] = $param->getPosition();
  272. }
  273. $call[] = $args;
  274. $call[] = null;
  275. $call[] = $noAuth;
  276. $this->map[$httpMethod][$url] = $call;
  277. }
  278. }
  279. }
  280. }
  281. public function getPath()
  282. {
  283. $path = substr(preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']), 1);
  284. $path = str_replace($this->remove_path, '', $path);
  285. if ($path[strlen($path) - 1] == '/') {
  286. $path = substr($path, 0, -1);
  287. }
  288. // remove root from path
  289. if ($this->root) $path = str_replace($this->root, '', $path);
  290. // remove trailing format definition, like /controller/action.json -> /controller/action
  291. $path = preg_replace('/\.(\w+)$/i', '', $path);
  292. return $path;
  293. }
  294. public function getMethod()
  295. {
  296. $method = $_SERVER['REQUEST_METHOD'];
  297. $override = isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) ? $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] : (isset($_GET['method']) ? $_GET['method'] : '');
  298. if ($method == 'POST' && strtoupper($override) == 'PUT') {
  299. $method = 'PUT';
  300. } elseif ($method == 'POST' && strtoupper($override) == 'DELETE') {
  301. $method = 'DELETE';
  302. }
  303. return $method;
  304. }
  305. public function getFormat()
  306. {
  307. $format = RestFormat::PLAIN;
  308. $accept_mod = preg_replace('/\s+/i', '', $_SERVER['HTTP_ACCEPT']); // ensures that exploding the HTTP_ACCEPT string does not get confused by whitespaces
  309. $accept = explode(',', $accept_mod);
  310. if (isset($_REQUEST['format']) || isset($_SERVER['HTTP_FORMAT'])) {
  311. // give GET/POST precedence over HTTP request headers
  312. $override = isset($_SERVER['HTTP_FORMAT']) ? $_SERVER['HTTP_FORMAT'] : '';
  313. $override = isset($_REQUEST['format']) ? $_REQUEST['format'] : $override;
  314. $override = trim($override);
  315. }
  316. // Check for trailing dot-format syntax like /controller/action.format -> action.json
  317. if(preg_match('/\.(\w+)$/i', $_SERVER['REQUEST_URI'], $matches)) {
  318. $override = $matches[1];
  319. }
  320. // Give GET parameters precedence before all other options to alter the format
  321. $override = isset($override) ? $override : '';
  322. $override = isset($_GET['format']) ? $_GET['format'] : $override;
  323. if (isset(RestFormat::$formats[$override])) {
  324. $format = RestFormat::$formats[$override];
  325. } elseif (in_array(RestFormat::AMF, $accept)) {
  326. $format = RestFormat::AMF;
  327. } elseif (in_array(RestFormat::JSON, $accept)) {
  328. $format = RestFormat::JSON;
  329. }
  330. return $format;
  331. }
  332. public function getData()
  333. {
  334. $data = file_get_contents('php://input');
  335. if ($this->format == RestFormat::AMF) {
  336. require_once 'Zend/Amf/Parse/InputStream.php';
  337. require_once 'Zend/Amf/Parse/Amf3/Deserializer.php';
  338. $stream = new Zend_Amf_Parse_InputStream($data);
  339. $deserializer = new Zend_Amf_Parse_Amf3_Deserializer($stream);
  340. $data = $deserializer->readTypeMarker();
  341. } else {
  342. $data = json_decode($data);
  343. }
  344. return $data;
  345. }
  346. public function sendData($data)
  347. {
  348. header("Cache-Control: no-cache, must-revalidate");
  349. header("Expires: 0");
  350. header('Content-Type: ' . $this->format);
  351. if ($this->format == RestFormat::AMF) {
  352. require_once 'Zend/Amf/Parse/OutputStream.php';
  353. require_once 'Zend/Amf/Parse/Amf3/Serializer.php';
  354. $stream = new Zend_Amf_Parse_OutputStream();
  355. $serializer = new Zend_Amf_Parse_Amf3_Serializer($stream);
  356. $serializer->writeTypeMarker($data);
  357. $data = $stream->getStream();
  358. } else {
  359. if (is_object($data) && method_exists($data, '__keepOut')) {
  360. $data = clone $data;
  361. foreach ($data->__keepOut() as $prop) {
  362. unset($data->$prop);
  363. }
  364. }
  365. $data = json_encode($data);
  366. if ($data && $this->mode == 'debug') {
  367. $data = $this->json_format($data);
  368. }
  369. }
  370. echo $data;
  371. }
  372. public function setStatus($code)
  373. {
  374. $code .= ' ' . $this->codes[strval($code)];
  375. header("{$_SERVER['SERVER_PROTOCOL']} $code");
  376. }
  377. // Pretty print some JSON
  378. private function json_format($json)
  379. {
  380. $tab = " ";
  381. $new_json = "";
  382. $indent_level = 0;
  383. $in_string = false;
  384. $len = strlen($json);
  385. for($c = 0; $c < $len; $c++) {
  386. $char = $json[$c];
  387. switch($char) {
  388. case '{':
  389. case '[':
  390. if(!$in_string) {
  391. $new_json .= $char . "\n" . str_repeat($tab, $indent_level+1);
  392. $indent_level++;
  393. } else {
  394. $new_json .= $char;
  395. }
  396. break;
  397. case '}':
  398. case ']':
  399. if(!$in_string) {
  400. $indent_level--;
  401. $new_json .= "\n" . str_repeat($tab, $indent_level) . $char;
  402. } else {
  403. $new_json .= $char;
  404. }
  405. break;
  406. case ',':
  407. if(!$in_string) {
  408. $new_json .= ",\n" . str_repeat($tab, $indent_level);
  409. } else {
  410. $new_json .= $char;
  411. }
  412. break;
  413. case ':':
  414. if(!$in_string) {
  415. $new_json .= ": ";
  416. } else {
  417. $new_json .= $char;
  418. }
  419. break;
  420. case '"':
  421. if($c > 0 && $json[$c-1] != '\\') {
  422. $in_string = !$in_string;
  423. }
  424. default:
  425. $new_json .= $char;
  426. break;
  427. }
  428. }
  429. return $new_json;
  430. }
  431. private $codes = array(
  432. '100' => 'Continue',
  433. '200' => 'OK',
  434. '201' => 'Created',
  435. '202' => 'Accepted',
  436. '203' => 'Non-Authoritative Information',
  437. '204' => 'No Content',
  438. '205' => 'Reset Content',
  439. '206' => 'Partial Content',
  440. '300' => 'Multiple Choices',
  441. '301' => 'Moved Permanently',
  442. '302' => 'Found',
  443. '303' => 'See Other',
  444. '304' => 'Not Modified',
  445. '305' => 'Use Proxy',
  446. '307' => 'Temporary Redirect',
  447. '400' => 'Bad Request',
  448. '401' => 'Unauthorized',
  449. '402' => 'Payment Required',
  450. '403' => 'Forbidden',
  451. '404' => 'Not Found',
  452. '405' => 'Method Not Allowed',
  453. '406' => 'Not Acceptable',
  454. '409' => 'Conflict',
  455. '410' => 'Gone',
  456. '411' => 'Length Required',
  457. '412' => 'Precondition Failed',
  458. '413' => 'Request Entity Too Large',
  459. '414' => 'Request-URI Too Long',
  460. '415' => 'Unsupported Media Type',
  461. '416' => 'Requested Range Not Satisfiable',
  462. '417' => 'Expectation Failed',
  463. '500' => 'Internal Server Error',
  464. '501' => 'Not Implemented',
  465. '503' => 'Service Unavailable'
  466. );
  467. }
  468. class RestException extends Exception
  469. {
  470. public function __construct($code, $message = null)
  471. {
  472. parent::__construct($message, $code);
  473. }
  474. }