<?php

/**
 * @file classes/core/APIRouter.inc.php
 *
 * Copyright (c) 2014-2020 Simon Fraser University
 * Copyright (c) 2000-2020 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class APIRouter
 * @ingroup core
 *
 * @brief Map HTTP requests to a REST API using the Slim microframework.
 *
 * Requests for [index.php]/api are intercepted for site-level API requests,
 * and requests for [index.php]/{contextPath}/api are intercepted for
 * context-level API requests.
 */

import('lib.pkp.classes.core.PKPRouter');
import('classes.core.Request');
import('classes.handler.Handler');

class APIRouter extends PKPRouter {

	/**
	 * Determines path info parts depending of disable_path_info config value
	 * @return array|NULL
	 */
	protected function getPathInfoParts() {
		$pathInfoEnabled = Config::getVar('general', 'disable_path_info') ? false : true;
		if ($pathInfoEnabled && isset($_SERVER['PATH_INFO'])) {
			return explode('/', trim($_SERVER['PATH_INFO'], '/'));
		}

		$request = $this->getApplication()->getRequest();
		$queryString = $request->getQueryString();
		$queryArray = array();
		if (isset($queryString)) {
			parse_str($queryString, $queryArray);
		}

		if (in_array('endpoint', array_keys($queryArray)) && isset($queryArray['journal'])) {
			$endpoint = $queryArray['endpoint'];
			return explode('/', trim($endpoint, '/'));
		}

		return null;
	}

	/**
	 * Determines whether this router can route the given request.
	 * @param $request PKPRequest
	 * @return boolean true, if the router supports this request, otherwise false
	 */
	function supports($request) {
		$pathInfoParts = $this->getPathInfoParts();

		if (!is_null($pathInfoParts) && count($pathInfoParts)>=2 && $pathInfoParts[1] == 'api') {
			// Context-specific API requests: [index.php]/{contextPath}/api
			return true;
		}

		return false;
	}

	/**
	 * Get the API version
	 * @return string
	 */
	function getVersion() {
		$pathInfoParts = $this->getPathInfoParts();
		return Core::cleanFileVar(isset($pathInfoParts[2]) ? $pathInfoParts[2] : '');
	}

	/**
	 * Get the entity being requested
	 * @return string
	 */
	function getEntity() {
		$pathInfoParts = $this->getPathInfoParts();
		return Core::cleanFileVar(isset($pathInfoParts[3]) ? $pathInfoParts[3] : '');
	}

	//
	// Implement template methods from PKPRouter
	//
	/**
	 * @copydoc PKPRouter::route()
	 */
	function route($request) {
		// Ensure slim library is available
		require_once('lib/pkp/lib/vendor/autoload.php');

		$sourceFile = sprintf('api/%s/%s/index.php', $this->getVersion(), $this->getEntity());

		if (!file_exists($sourceFile)) {
			AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API, LOCALE_COMPONENT_APP_API);
			http_response_code('404');
			header('Content-Type: application/json');
			echo json_encode([
				'error' => 'api.404.endpointNotFound',
				'errorMessage' => __('api.404.endpointNotFound'),
			]);
			exit;
		}

		if (!defined('SESSION_DISABLE_INIT')) {
			// Initialize session
			SessionManager::getManager();
		}

		$handler = require ('./'.$sourceFile);
		$this->setHandler($handler);
		$handler->getApp()->run();
	}

	/**
	 * Get the requested operation
	 *
	 * @param $request PKPRequest
	 * @return string
	 */
	function getRequestedOp($request) {
		$handler = $this->getHandler();
		$container = $handler->getApp()->getContainer();
		$router = $container->get('router');
		$slimRequest = $handler->getSlimRequest();
		$routeInfo = $router->dispatch($slimRequest);
		if (isset($routeInfo[1])) {
			$route = $router->lookupRoute($routeInfo[1]);
			$callable = $route->getCallable();
			if (is_array($callable) && count($callable) == 2)
				return $callable[1];
		}
		return '';
	}

	/**
	 * @copydoc PKPRouter::handleAuthorizationFailure()
	 */
	function handleAuthorizationFailure($request, $authorizationMessage) {
		AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API, LOCALE_COMPONENT_APP_API);
		http_response_code('403');
		header('Content-Type: application/json');
		echo json_encode([
			'error' => $authorizationMessage,
			'errorMessage' => __($authorizationMessage),
		]);
		exit;
	}

	/**
	 * @copydoc PKPRouter::url()
	 */
	function url($request, $newContext = null, $endpoint = null, $op = null, $path = null,
			$params = null, $anchor = null, $escape = false) {

		// APIHandlers do not understand $op, $path or $anchor. All routing is baked
		// into the $endpoint string. It only accepts a string as the $newContext,
		// since it relies on this when path info is disabled.
		if (!is_null($op) || !is_null($path) || !is_null($anchor) || !is_scalar($newContext)) {
			throw new Exception('APIRouter::url() should not be called with an op, path or anchor. If a new context is passed, the context path must be passed instead of the context object.');
		}

		//
		// Base URL and Context
		//
		$baseUrlAndContext = $this->_urlGetBaseAndContext($request, $this->_urlCanonicalizeNewContext($newContext));
		$baseUrl = array_shift($baseUrlAndContext);
		$context = $baseUrlAndContext;

		//
		// Additional query parameters
		//
		$additionalParameters = $this->_urlGetAdditionalParameters($request, $params, $escape);

		//
		// Assemble URL
		//
		if ($request->isPathInfoEnabled()) {
			// If path info is enabled, everything but params goes into the path
			$pathInfoArray = array_merge(
				$context,
				['api',	API_VERSION, $endpoint]
			);
			$queryParametersArray = $additionalParameters;
		} else {
			// If path info is disabled, the context and endpoint must be passed as
			// query params, and the context must be concatenated into the endpoint
			$pathInfoArray = array();
			$queryParametersArray = array_merge(
				$context,
				[sprintf('endpoint=/%s/api/%s/%s', $newContext, API_VERSION, $endpoint)],
				$additionalParameters
			);
		}

		return $this->_urlFromParts($baseUrl, $pathInfoArray, $queryParametersArray, $anchor, $escape);
	}
}
