/src/Api/V1/Engine/Api.php
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
- <?php
- namespace Api\V1\Engine;
- /*
- * This file is part of Fork CMS.
- *
- * For the full copyright and license information, please view the license
- * file that was distributed with this source code.
- */
- use Backend\Core\Engine\Model as BackendModel;
- use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\HttpFoundation\Request;
- use Backend\Core\Engine\Authentication as BackendAuthentication;
- use Backend\Core\Engine\User as BackendUser;
- /**
- * This class defines the API.
- */
- class Api extends \KernelLoader implements \ApplicationInterface
- {
- // statuses
- const OK = 200;
- const BAD_REQUEST = 400;
- const NOT_AUTHORIZED = 401;
- const FORBIDDEN = 403;
- const ERROR = 500;
- const NOT_FOUND = 404;
- /**
- * @var string
- */
- protected static $content;
- /**
- * Initializes the entire API; extract class+method from the request, call, and output.
- *
- * This method exists because the service container needs to be set before
- * the rest of API functionality gets loaded.
- */
- public function initialize()
- {
- self::$content = null;
- /**
- * @var Request
- */
- $request = $this->getContainer()->get('request');
- // simulate $_REQUEST
- $parameters = array_merge(
- (array) $request->query->all(),
- (array) $request->request->all()
- );
- $method = $request->get('method');
- if ($method == '') {
- return self::output(self::BAD_REQUEST, array('message' => 'No method-parameter provided.'));
- }
- // process method
- $chunks = (array) explode('.', $method, 2);
- // validate method
- if (!isset($chunks[1])) {
- return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
- }
- // camelcase module name
- $chunks[0] = \SpoonFilter::toCamelCase($chunks[0]);
- // build the path to the backend API file
- if ($chunks[0] == 'Core') {
- $class = 'Backend\\Core\\Engine\\Api';
- } else {
- $class = 'Backend\\Modules\\' . $chunks[0] . '\\Engine\\Api';
- }
- // check if the file is present? If it isn't present there is a problem
- if (!class_exists($class)) {
- return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
- }
- // build config-object-name
- $methodName = \SpoonFilter::toCamelCase($chunks[1], '.', true);
- // validate if the method exists
- if (!is_callable(array($class, $methodName))) {
- return self::output(self::BAD_REQUEST, array('message' => 'Invalid method.'));
- }
- // call the method
- try {
- // init var
- $arguments = null;
- // create reflection method
- $reflectionMethod = new \ReflectionMethod($class, $methodName);
- $parameterDocumentation = array();
- // get data from docs
- $matches = array();
- preg_match_all(
- '/@param[\s\t]+(.*)[\s\t]+\$(.*)[\s\t]+(.*)$/Um',
- $reflectionMethod->getDocComment(),
- $matches
- );
- // documentation found
- if (!empty($matches[0])) {
- // loop matches
- foreach ($matches[0] as $i => $row) {
- // set documentation
- $parameterDocumentation[$matches[2][$i]] = array(
- 'type' => $matches[1][$i],
- 'optional' => (mb_substr_count($matches[1][$i], '[optional]') > 0),
- 'description' => $matches[3][$i]
- );
- }
- }
- // loop parameters
- foreach ($reflectionMethod->getParameters() as $parameter) {
- // init var
- $name = $parameter->getName();
- // check if the parameter is available
- if (!$parameter->isOptional() && !isset($parameters[$name])) {
- return self::output(self::BAD_REQUEST, array('message' => 'No ' . $name . '-parameter provided.'));
- }
- // add not-passed arguments
- if ($parameter->isOptional() && !isset($parameters[$name])) {
- $arguments[] = $parameter->getDefaultValue();
- } elseif (isset($parameterDocumentation[$name]['type'])) {
- // add argument if we know the type
- // get default value
- $defaultValue = null;
- if ($parameter->isOptional()) {
- $defaultValue = $parameter->getDefaultValue();
- }
- // add argument
- $arguments[] = \SpoonFilter::getValue(
- $parameters[$name],
- null,
- $defaultValue,
- $parameterDocumentation[$name]['type']
- );
- } else {
- // fallback
- $arguments[] = $parameters[$name];
- }
- }
- // get the return
- $data = (array) call_user_func_array(array($class, $methodName), (array) $arguments);
- // output
- if (self::$content === null) {
- self::output(self::OK, $data);
- }
- } catch (\Exception $e) {
- // if we are debugging we should see the exceptions
- if ($this->getContainer()->getParameter('kernel.debug')) {
- if (isset($parameters['debug']) && $parameters['debug'] == 'false') {
- // do nothing
- } else {
- throw $e;
- }
- }
- // output
- return self::output(500, array('message' => $e->getMessage()));
- }
- }
- /**
- * Callback-method for elements in the return-array
- *
- * @param mixed $input The value.
- * @param string $key The key.
- * @param \DOMElement $XML The root-element.
- */
- private static function arrayToXML(&$input, $key, $XML)
- {
- // skip attributes
- if ($key == '@attributes') {
- return;
- }
- // create element
- $element = new \DOMElement($key);
- // append
- $XML->appendChild($element);
- // no value? just stop here
- if ($input === null) {
- return;
- }
- // is it an array and are there attributes
- if (is_array($input) && isset($input['@attributes'])) {
- // loop attributes
- foreach ((array) $input['@attributes'] as $name => $value) {
- $element->setAttribute($name, $value);
- }
- // remove attributes
- unset($input['@attributes']);
- // reset the input if it is a single value
- if (count($input) == 1) {
- // get keys
- $keys = array_keys($input);
- // reset
- $input = $input[$keys[0]];
- }
- }
- // the input isn't an array
- if (!is_array($input)) {
- // a string?
- if (is_string($input)) {
- // characters that require a cdata wrapper
- $illegalCharacters = array('&', '<', '>', '"', '\'');
- // default we don't wrap with cdata tags
- $wrapCdata = false;
- // find illegal characters in input string
- foreach ($illegalCharacters as $character) {
- if (mb_stripos($input, $character) !== false) {
- // wrap input with cdata
- $wrapCdata = true;
- // no need to search further
- break;
- }
- }
- // check if value contains illegal chars, if so wrap in CDATA
- if ($wrapCdata) {
- $element->appendChild(new \DOMCdataSection($input));
- } else {
- // just regular element
- $element->appendChild(new \DOMText($input));
- }
- } else {
- // regular element
- $element->appendChild(new \DOMText($input));
- }
- } else {
- // the value is an array
- $isNonNumeric = false;
- // loop all elements
- foreach ($input as $index => $value) {
- // non numeric string as key?
- if (!is_numeric($index)) {
- // reset var
- $isNonNumeric = true;
- // stop searching
- break;
- }
- }
- // is there are named keys they should be handles as elements
- if ($isNonNumeric) {
- array_walk($input, array('Api\\V1\\Engine\\Api', 'arrayToXML'), $element);
- } else {
- // numeric elements means this a list of items
- // handle the value as an element
- foreach ($input as $value) {
- if (is_array($value)) {
- array_walk($value, array('Api\\V1\\Engine\\Api', 'arrayToXML'), $element);
- }
- }
- }
- }
- }
- /**
- * Default authentication
- *
- * @return bool
- */
- public static function isAuthorized()
- {
- // grab data
- $email = \SpoonFilter::getGetValue('email', null, '');
- $nonce = \SpoonFilter::getGetValue('nonce', null, '');
- $secret = \SpoonFilter::getGetValue('secret', null, '');
- // data can be available in the POST, so check it
- if ($email == '') {
- $email = \SpoonFilter::getPostValue('email', null, '');
- }
- if ($nonce == '') {
- $nonce = \SpoonFilter::getPostValue('nonce', null, '');
- }
- if ($secret == '') {
- $secret = \SpoonFilter::getPostValue('secret', null, '');
- }
- // check if needed elements are available
- if ($email === '' || $nonce === '' || $secret === '') {
- return self::output(
- self::NOT_AUTHORIZED,
- array('message' => 'Not authorized.')
- );
- }
- // get the user
- try {
- $user = new BackendUser(null, $email);
- } catch (\Exception $e) {
- return self::output(self::FORBIDDEN, array('message' => 'This account does not exist.'));
- }
- // get settings
- $apiAccess = $user->getSetting('api_access', false);
- $apiKey = $user->getSetting('api_key');
- // no API-access
- if (!$apiAccess) {
- return self::output(
- self::FORBIDDEN,
- array('message' => 'Your account isn\'t allowed to use the API. Contact an administrator.')
- );
- }
- // create hash
- $hash = BackendAuthentication::getEncryptedString($email . $apiKey, $nonce);
- // output
- if ($secret != $hash) {
- return self::output(self::FORBIDDEN, array('message' => 'Invalid secret.'));
- }
- // return
- return true;
- }
- /**
- * @return Response
- */
- public function display()
- {
- return new Response(self::$content, 200);
- }
- /**
- * This is called in backend/modules/<module>/engine/api.php to limit certain calls to
- * a given request method.
- *
- * @param string $method
- * @return bool
- */
- public static function isValidRequestMethod($method)
- {
- if ($method !== $_SERVER['REQUEST_METHOD']) {
- $message = 'Illegal request method, only ' . $method . ' allowed for this method';
- return self::output(self::BAD_REQUEST, array('message' => $message));
- }
- return true;
- }
- /**
- * Output the return
- *
- * @param int $statusCode The status code.
- * @param array $data The data to return.
- * @return bool
- */
- public static function output($statusCode, array $data = null)
- {
- // get output format
- $allowedFormats = array('xml', 'json');
- // use XML as a default
- $output = 'xml';
- // use the accept header if it is provided
- if (isset($_SERVER['HTTP_ACCEPT'])) {
- $acceptHeader = mb_strtolower($_SERVER['HTTP_ACCEPT']);
- if (mb_substr_count($acceptHeader, 'text/xml') > 0) {
- $output = 'xml';
- }
- if (mb_substr_count($acceptHeader, 'application/xml') > 0) {
- $output = 'xml';
- }
- if (mb_substr_count($acceptHeader, 'text/json') > 0) {
- $output = 'json';
- }
- if (mb_substr_count($acceptHeader, 'application/json') > 0) {
- $output = 'json';
- }
- }
- // format specified as a GET-parameter will overrule the one provided through the accept headers
- $output = \SpoonFilter::getGetValue('format', $allowedFormats, $output);
- // if the format was specified in the POST it will overrule all previous formats
- $output = \SpoonFilter::getPostValue('format', $allowedFormats, $output);
- // return in the requested format
- switch ($output) {
- // json
- case 'json':
- self::outputJSON($statusCode, $data);
- break;
- // xml
- default:
- self::outputXML($statusCode, $data);
- }
- return ($statusCode === 200);
- }
- /**
- * Output as JSON
- *
- * @param int $statusCode The status code.
- * @param array $data The data to return.
- */
- private static function outputJSON($statusCode, array $data = null)
- {
- // redefine
- $statusCode = (int) $statusCode;
- // init vars
- $charset = BackendModel::getContainer()->getParameter('kernel.charset');
- $pathChunks = explode(DIRECTORY_SEPARATOR, trim(dirname(__FILE__), DIRECTORY_SEPARATOR));
- $version = $pathChunks[count($pathChunks) - 2];
- $version = mb_strtolower($version);
- // build array
- $JSON = array();
- $JSON['meta']['status_code'] = $statusCode;
- $JSON['meta']['status'] = ($statusCode === 200) ? 'ok' : 'error';
- $JSON['meta']['version'] = FORK_VERSION;
- $JSON['meta']['endpoint'] = SITE_URL . '/api/' . $version;
- // add data
- if ($data !== null) {
- $JSON['data'] = $data;
- }
- // set correct headers
- header('HTTP/1.1 ' . self::getHeaderMessage($statusCode));
- header('content-type: application/json;charset=' . $charset);
- // output JSON
- self::$content = json_encode($JSON);
- }
- /**
- * Output as XML
- *
- * @param int $statusCode The status code.
- * @param array $data The data to return.
- */
- private static function outputXML($statusCode, array $data = null)
- {
- // redefine
- $statusCode = (int) $statusCode;
- // init vars
- $charset = BackendModel::getContainer()->getParameter('kernel.charset');
- $pathChunks = explode(DIRECTORY_SEPARATOR, trim(dirname(__FILE__), DIRECTORY_SEPARATOR));
- $version = $pathChunks[count($pathChunks) - 2];
- $version = mb_strtolower($version);
- // init XML
- $XML = new \DOMDocument('1.0', $charset);
- // set some properties
- $XML->preserveWhiteSpace = false;
- $XML->formatOutput = true;
- // create root element
- $root = $XML->createElement('fork');
- // add attributes
- $root->setAttribute('status_code', $statusCode);
- $root->setAttribute('status', ($statusCode == 200) ? 'ok' : 'error');
- $root->setAttribute('version', FORK_VERSION);
- $root->setAttribute('endpoint', SITE_URL . '/api/' . $version);
- // append
- $XML->appendChild($root);
- // build XML
- array_walk($data, array(__CLASS__, 'arrayToXML'), $root);
- // set correct headers
- header('HTTP/1.1 ' . self::getHeaderMessage($statusCode));
- header('content-type: text/xml;charset=' . $charset);
- // output XML
- self::$content = $XML->saveXML();
- }
- /**
- * Get the relevant HTTP status message
- *
- * @param $statusCode
- * @return string
- */
- private static function getHeaderMessage($statusCode)
- {
- if (!isset(Response::$statusTexts[$statusCode])) {
- $statusCode = 500;
- }
- return $statusCode . ' ' . Response::$statusTexts[$statusCode];
- }
- }