PageRenderTime 38ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Api/V1/Engine/Api.php

http://github.com/forkcms/forkcms
PHP | 528 lines | 288 code | 83 blank | 157 comment | 61 complexity | bc477edaf09e19907aeb59050b642b64 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. namespace Api\V1\Engine;
  3. /*
  4. * This file is part of Fork CMS.
  5. *
  6. * For the full copyright and license information, please view the license
  7. * file that was distributed with this source code.
  8. */
  9. use Backend\Core\Engine\Model as BackendModel;
  10. use Symfony\Component\HttpFoundation\Response;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Backend\Core\Engine\Authentication as BackendAuthentication;
  13. use Backend\Core\Engine\User as BackendUser;
  14. /**
  15. * This class defines the API.
  16. */
  17. class Api extends \KernelLoader implements \ApplicationInterface
  18. {
  19. // statuses
  20. const OK = 200;
  21. const BAD_REQUEST = 400;
  22. const NOT_AUTHORIZED = 401;
  23. const FORBIDDEN = 403;
  24. const ERROR = 500;
  25. const NOT_FOUND = 404;
  26. /**
  27. * @var string
  28. */
  29. protected static $content;
  30. /**
  31. * Initializes the entire API; extract class+method from the request, call, and output.
  32. *
  33. * This method exists because the service container needs to be set before
  34. * the rest of API functionality gets loaded.
  35. */
  36. public function initialize()
  37. {
  38. self::$content = null;
  39. /**
  40. * @var Request
  41. */
  42. $request = $this->getContainer()->get('request');
  43. // simulate $_REQUEST
  44. $parameters = array_merge(
  45. (array) $request->query->all(),
  46. (array) $request->request->all()
  47. );
  48. $method = $request->get('method');
  49. if ($method == '') {
  50. return self::output(self::BAD_REQUEST, array('message' => 'No method-parameter provided.'));
  51. }
  52. // process method
  53. $chunks = (array) explode('.', $method, 2);
  54. // validate method
  55. if (!isset($chunks[1])) {
  56. return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
  57. }
  58. // camelcase module name
  59. $chunks[0] = \SpoonFilter::toCamelCase($chunks[0]);
  60. // build the path to the backend API file
  61. if ($chunks[0] == 'Core') {
  62. $class = 'Backend\\Core\\Engine\\Api';
  63. } else {
  64. $class = 'Backend\\Modules\\' . $chunks[0] . '\\Engine\\Api';
  65. }
  66. // check if the file is present? If it isn't present there is a problem
  67. if (!class_exists($class)) {
  68. return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
  69. }
  70. // build config-object-name
  71. $methodName = \SpoonFilter::toCamelCase($chunks[1], '.', true);
  72. // validate if the method exists
  73. if (!is_callable(array($class, $methodName))) {
  74. return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
  75. }
  76. // call the method
  77. try {
  78. // init var
  79. $arguments = null;
  80. // create reflection method
  81. $reflectionMethod = new \ReflectionMethod($class, $methodName);
  82. $parameterDocumentation = array();
  83. // get data from docs
  84. $matches = array();
  85. preg_match_all(
  86. '/@param[\s\t]+(.*)[\s\t]+\$(.*)[\s\t]+(.*)$/Um',
  87. $reflectionMethod->getDocComment(),
  88. $matches
  89. );
  90. // documentation found
  91. if (!empty($matches[0])) {
  92. // loop matches
  93. foreach ($matches[0] as $i => $row) {
  94. // set documentation
  95. $parameterDocumentation[$matches[2][$i]] = array(
  96. 'type' => $matches[1][$i],
  97. 'optional' => (mb_substr_count($matches[1][$i], '[optional]') > 0),
  98. 'description' => $matches[3][$i]
  99. );
  100. }
  101. }
  102. // loop parameters
  103. foreach ($reflectionMethod->getParameters() as $parameter) {
  104. // init var
  105. $name = $parameter->getName();
  106. // check if the parameter is available
  107. if (!$parameter->isOptional() && !isset($parameters[$name])) {
  108. return self::output(self::BAD_REQUEST, array('message' => 'No ' . $name . '-parameter provided.'));
  109. }
  110. // add not-passed arguments
  111. if ($parameter->isOptional() && !isset($parameters[$name])) {
  112. $arguments[] = $parameter->getDefaultValue();
  113. } elseif (isset($parameterDocumentation[$name]['type'])) {
  114. // add argument if we know the type
  115. // get default value
  116. $defaultValue = null;
  117. if ($parameter->isOptional()) {
  118. $defaultValue = $parameter->getDefaultValue();
  119. }
  120. // add argument
  121. $arguments[] = \SpoonFilter::getValue(
  122. $parameters[$name],
  123. null,
  124. $defaultValue,
  125. $parameterDocumentation[$name]['type']
  126. );
  127. } else {
  128. // fallback
  129. $arguments[] = $parameters[$name];
  130. }
  131. }
  132. // get the return
  133. $data = (array) call_user_func_array(array($class, $methodName), (array) $arguments);
  134. // output
  135. if (self::$content === null) {
  136. self::output(self::OK, $data);
  137. }
  138. } catch (\Exception $e) {
  139. // if we are debugging we should see the exceptions
  140. if ($this->getContainer()->getParameter('kernel.debug')) {
  141. if (isset($parameters['debug']) && $parameters['debug'] == 'false') {
  142. // do nothing
  143. } else {
  144. throw $e;
  145. }
  146. }
  147. // output
  148. return self::output(500, array('message' => $e->getMessage()));
  149. }
  150. }
  151. /**
  152. * Callback-method for elements in the return-array
  153. *
  154. * @param mixed $input The value.
  155. * @param string $key The key.
  156. * @param \DOMElement $XML The root-element.
  157. */
  158. private static function arrayToXML(&$input, $key, $XML)
  159. {
  160. // skip attributes
  161. if ($key == '@attributes') {
  162. return;
  163. }
  164. // create element
  165. $element = new \DOMElement($key);
  166. // append
  167. $XML->appendChild($element);
  168. // no value? just stop here
  169. if ($input === null) {
  170. return;
  171. }
  172. // is it an array and are there attributes
  173. if (is_array($input) && isset($input['@attributes'])) {
  174. // loop attributes
  175. foreach ((array) $input['@attributes'] as $name => $value) {
  176. $element->setAttribute($name, $value);
  177. }
  178. // remove attributes
  179. unset($input['@attributes']);
  180. // reset the input if it is a single value
  181. if (count($input) == 1) {
  182. // get keys
  183. $keys = array_keys($input);
  184. // reset
  185. $input = $input[$keys[0]];
  186. }
  187. }
  188. // the input isn't an array
  189. if (!is_array($input)) {
  190. // a string?
  191. if (is_string($input)) {
  192. // characters that require a cdata wrapper
  193. $illegalCharacters = array('&', '<', '>', '"', '\'');
  194. // default we don't wrap with cdata tags
  195. $wrapCdata = false;
  196. // find illegal characters in input string
  197. foreach ($illegalCharacters as $character) {
  198. if (mb_stripos($input, $character) !== false) {
  199. // wrap input with cdata
  200. $wrapCdata = true;
  201. // no need to search further
  202. break;
  203. }
  204. }
  205. // check if value contains illegal chars, if so wrap in CDATA
  206. if ($wrapCdata) {
  207. $element->appendChild(new \DOMCdataSection($input));
  208. } else {
  209. // just regular element
  210. $element->appendChild(new \DOMText($input));
  211. }
  212. } else {
  213. // regular element
  214. $element->appendChild(new \DOMText($input));
  215. }
  216. } else {
  217. // the value is an array
  218. $isNonNumeric = false;
  219. // loop all elements
  220. foreach ($input as $index => $value) {
  221. // non numeric string as key?
  222. if (!is_numeric($index)) {
  223. // reset var
  224. $isNonNumeric = true;
  225. // stop searching
  226. break;
  227. }
  228. }
  229. // is there are named keys they should be handles as elements
  230. if ($isNonNumeric) {
  231. array_walk($input, array('Api\\V1\\Engine\\Api', 'arrayToXML'), $element);
  232. } else {
  233. // numeric elements means this a list of items
  234. // handle the value as an element
  235. foreach ($input as $value) {
  236. if (is_array($value)) {
  237. array_walk($value, array('Api\\V1\\Engine\\Api', 'arrayToXML'), $element);
  238. }
  239. }
  240. }
  241. }
  242. }
  243. /**
  244. * Default authentication
  245. *
  246. * @return bool
  247. */
  248. public static function isAuthorized()
  249. {
  250. // grab data
  251. $email = \SpoonFilter::getGetValue('email', null, '');
  252. $nonce = \SpoonFilter::getGetValue('nonce', null, '');
  253. $secret = \SpoonFilter::getGetValue('secret', null, '');
  254. // data can be available in the POST, so check it
  255. if ($email == '') {
  256. $email = \SpoonFilter::getPostValue('email', null, '');
  257. }
  258. if ($nonce == '') {
  259. $nonce = \SpoonFilter::getPostValue('nonce', null, '');
  260. }
  261. if ($secret == '') {
  262. $secret = \SpoonFilter::getPostValue('secret', null, '');
  263. }
  264. // check if needed elements are available
  265. if ($email === '' || $nonce === '' || $secret === '') {
  266. return self::output(
  267. self::NOT_AUTHORIZED,
  268. array('message' => 'Not authorized.')
  269. );
  270. }
  271. // get the user
  272. try {
  273. $user = new BackendUser(null, $email);
  274. } catch (\Exception $e) {
  275. return self::output(self::FORBIDDEN, array('message' => 'This account does not exist.'));
  276. }
  277. // get settings
  278. $apiAccess = $user->getSetting('api_access', false);
  279. $apiKey = $user->getSetting('api_key');
  280. // no API-access
  281. if (!$apiAccess) {
  282. return self::output(
  283. self::FORBIDDEN,
  284. array('message' => 'Your account isn\'t allowed to use the API. Contact an administrator.')
  285. );
  286. }
  287. // create hash
  288. $hash = BackendAuthentication::getEncryptedString($email . $apiKey, $nonce);
  289. // output
  290. if ($secret != $hash) {
  291. return self::output(self::FORBIDDEN, array('message' => 'Invalid secret.'));
  292. }
  293. // return
  294. return true;
  295. }
  296. /**
  297. * @return Response
  298. */
  299. public function display()
  300. {
  301. return new Response(self::$content, 200);
  302. }
  303. /**
  304. * This is called in backend/modules/<module>/engine/api.php to limit certain calls to
  305. * a given request method.
  306. *
  307. * @param string $method
  308. * @return bool
  309. */
  310. public static function isValidRequestMethod($method)
  311. {
  312. if ($method !== $_SERVER['REQUEST_METHOD']) {
  313. $message = 'Illegal request method, only ' . $method . ' allowed for this method';
  314. return self::output(self::BAD_REQUEST, array('message' => $message));
  315. }
  316. return true;
  317. }
  318. /**
  319. * Output the return
  320. *
  321. * @param int $statusCode The status code.
  322. * @param array $data The data to return.
  323. * @return bool
  324. */
  325. public static function output($statusCode, array $data = null)
  326. {
  327. // get output format
  328. $allowedFormats = array('xml', 'json');
  329. // use XML as a default
  330. $output = 'xml';
  331. // use the accept header if it is provided
  332. if (isset($_SERVER['HTTP_ACCEPT'])) {
  333. $acceptHeader = mb_strtolower($_SERVER['HTTP_ACCEPT']);
  334. if (mb_substr_count($acceptHeader, 'text/xml') > 0) {
  335. $output = 'xml';
  336. }
  337. if (mb_substr_count($acceptHeader, 'application/xml') > 0) {
  338. $output = 'xml';
  339. }
  340. if (mb_substr_count($acceptHeader, 'text/json') > 0) {
  341. $output = 'json';
  342. }
  343. if (mb_substr_count($acceptHeader, 'application/json') > 0) {
  344. $output = 'json';
  345. }
  346. }
  347. // format specified as a GET-parameter will overrule the one provided through the accept headers
  348. $output = \SpoonFilter::getGetValue('format', $allowedFormats, $output);
  349. // if the format was specified in the POST it will overrule all previous formats
  350. $output = \SpoonFilter::getPostValue('format', $allowedFormats, $output);
  351. // return in the requested format
  352. switch ($output) {
  353. // json
  354. case 'json':
  355. self::outputJSON($statusCode, $data);
  356. break;
  357. // xml
  358. default:
  359. self::outputXML($statusCode, $data);
  360. }
  361. return ($statusCode === 200);
  362. }
  363. /**
  364. * Output as JSON
  365. *
  366. * @param int $statusCode The status code.
  367. * @param array $data The data to return.
  368. */
  369. private static function outputJSON($statusCode, array $data = null)
  370. {
  371. // redefine
  372. $statusCode = (int) $statusCode;
  373. // init vars
  374. $charset = BackendModel::getContainer()->getParameter('kernel.charset');
  375. $pathChunks = explode(DIRECTORY_SEPARATOR, trim(dirname(__FILE__), DIRECTORY_SEPARATOR));
  376. $version = $pathChunks[count($pathChunks) - 2];
  377. $version = mb_strtolower($version);
  378. // build array
  379. $JSON = array();
  380. $JSON['meta']['status_code'] = $statusCode;
  381. $JSON['meta']['status'] = ($statusCode === 200) ? 'ok' : 'error';
  382. $JSON['meta']['version'] = FORK_VERSION;
  383. $JSON['meta']['endpoint'] = SITE_URL . '/api/' . $version;
  384. // add data
  385. if ($data !== null) {
  386. $JSON['data'] = $data;
  387. }
  388. // set correct headers
  389. header('HTTP/1.1 ' . self::getHeaderMessage($statusCode));
  390. header('content-type: application/json;charset=' . $charset);
  391. // output JSON
  392. self::$content = json_encode($JSON);
  393. }
  394. /**
  395. * Output as XML
  396. *
  397. * @param int $statusCode The status code.
  398. * @param array $data The data to return.
  399. */
  400. private static function outputXML($statusCode, array $data = null)
  401. {
  402. // redefine
  403. $statusCode = (int) $statusCode;
  404. // init vars
  405. $charset = BackendModel::getContainer()->getParameter('kernel.charset');
  406. $pathChunks = explode(DIRECTORY_SEPARATOR, trim(dirname(__FILE__), DIRECTORY_SEPARATOR));
  407. $version = $pathChunks[count($pathChunks) - 2];
  408. $version = mb_strtolower($version);
  409. // init XML
  410. $XML = new \DOMDocument('1.0', $charset);
  411. // set some properties
  412. $XML->preserveWhiteSpace = false;
  413. $XML->formatOutput = true;
  414. // create root element
  415. $root = $XML->createElement('fork');
  416. // add attributes
  417. $root->setAttribute('status_code', $statusCode);
  418. $root->setAttribute('status', ($statusCode == 200) ? 'ok' : 'error');
  419. $root->setAttribute('version', FORK_VERSION);
  420. $root->setAttribute('endpoint', SITE_URL . '/api/' . $version);
  421. // append
  422. $XML->appendChild($root);
  423. // build XML
  424. array_walk($data, array(__CLASS__, 'arrayToXML'), $root);
  425. // set correct headers
  426. header('HTTP/1.1 ' . self::getHeaderMessage($statusCode));
  427. header('content-type: text/xml;charset=' . $charset);
  428. // output XML
  429. self::$content = $XML->saveXML();
  430. }
  431. /**
  432. * Get the relevant HTTP status message
  433. *
  434. * @param $statusCode
  435. * @return string
  436. */
  437. private static function getHeaderMessage($statusCode)
  438. {
  439. if (!isset(Response::$statusTexts[$statusCode])) {
  440. $statusCode = 500;
  441. }
  442. return $statusCode . ' ' . Response::$statusTexts[$statusCode];
  443. }
  444. }