<?php
/**
* @file classes/core/PKPRouter.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPRouter
*
* @see PKPPageRouter
* @see PKPComponentRouter
*
* @ingroup core
*
* @brief Basic router class that has functionality common to all routers.
*
* NB: All handlers provide the common basic workflow. The router
* calls the following methods in the given order.
* 1) constructor:
* Handlers should establish a mapping of remote
* operations to roles that may access them. They do
* so by calling PKPHandler::addRoleAssignment().
* 2) authorize():
* Authorizes the request, among other things based
* on the result of the role assignment created
* during object instantiation. If authorization fails
* then die with a fatal error or execute the "call-
* on-deny" advice if one has been defined in the
* authorization policy that denied access.
* 3) validate():
* Let the handler execute non-fatal data integrity
* checks (FIXME: currently only for component handlers).
* Please make sure that data integrity checks that can
* lead to denial of access are being executed in the
* authorize() step via authorization policies and not
* here.
* 4) initialize():
* Let the handler initialize its internal state based
* on authorized and valid data. Authorization and integrity
* checks should be kept out of here to get a clear separation
* of concerns.
* 5) execution:
* Executes the requested handler operation. The mapping
* of requests to operations depends on the router
* implementation (see the class doc of specific router
* implementations for more details).
* 6) client response:
* Handlers should return a string value that will then be
* returned to the client as a response. Handler operations
* should not output the response directly to the client so
* that we can run filter operations on the output if required.
* Outputting text from handler operations to the client
* is possible but deprecated.
*/
namespace PKP\core;
use APP\core\Application;
use Exception;
use PKP\config\Config;
use PKP\context\Context;
use PKP\context\ContextDAO;
use PKP\db\DAORegistry;
use PKP\handler\PKPHandler;
use PKP\plugins\Hook;
abstract class PKPRouter
{
//
// Internal state cache variables
// NB: Please do not access directly but
// only via their respective getters/setters
//
protected Application $_application;
protected Dispatcher $_dispatcher;
protected ?string $_contextPath = null;
public ?Context $_context = null;
public ?PKPHandler $_handler = null;
/** @var string */
public $_indexUrl;
/**
* Constructor
*/
public function __construct()
{
}
/**
* get the application
*/
public function getApplication(): Application
{
return $this->_application;
}
/**
* set the application
*/
public function setApplication(Application $application)
{
$this->_application = $application;
}
/**
* get the dispatcher
*/
public function getDispatcher(): \PKP\core\Dispatcher
{
return $this->_dispatcher;
}
/**
* set the dispatcher
*/
public function setDispatcher(\PKP\core\Dispatcher $dispatcher)
{
$this->_dispatcher = $dispatcher;
}
/**
* Set the handler object for later retrieval.
*/
public function setHandler(PKPHandler $handler)
{
$this->_handler = $handler;
}
/**
* Get the handler object.
*/
public function getHandler(): ?PKPHandler
{
return $this->_handler;
}
/**
* Determines whether this router can route the given request.
*/
public function supports(PKPRequest $request): bool
{
// Default implementation returns always true
return true;
}
/**
* Determine whether or not this request is cacheable
*/
public function isCacheable(PKPRequest $request): bool
{
// Default implementation returns always false
return false;
}
/**
* A generic method to return a context path (e.g. a Press or a Journal path)
*/
public function getRequestedContextPath(PKPRequest $request): string
{
// Determine the context path
if ($this->_contextPath === null) {
$this->_contextPath = Core::getContextPath($_SERVER['PATH_INFO'] ?? '');
Hook::call('Router::getRequestedContextPath', [&$this->_contextPath]);
}
return $this->_contextPath;
}
/**
* A Generic call to a context defining object (e.g. a Journal, Press, or Server)
*
* @param PKPRequest $request the request to be routed
* @param bool $forceReload (optional) Reset a context even if it's already been loaded
*
* @return Context
*/
public function getContext($request, $forceReload = false)
{
if ($forceReload || !isset($this->_context)) {
// Retrieve the requested context path (this validates the path)
$path = $this->getRequestedContextPath($request);
// Resolve the path to the context
if ($path === 'index' || $path === '' || $path === Application::CONTEXT_ID_ALL) {
$this->_context = null;
} else {
// FIXME: Can't just use Application::get()->getContextDAO() without test breakage
/** @var ContextDAO */
$contextDao = DAORegistry::getDAO(ucfirst(Application::get()->getContextName()) . 'DAO');
// Retrieve the context from the DAO (by path)
$this->_context = $contextDao->getByPath($path);
// If the context couldn't be retrieved, it's a 404 error.
if (!$this->_context) {
$this->getDispatcher()?->handle404();
}
}
}
return $this->_context;
}
/**
* Get the URL to the index script.
*
* @param PKPRequest $request the request to be routed
*
* @return string
*/
public function getIndexUrl($request)
{
if (!isset($this->_indexUrl)) {
if ($request->isRestfulUrlsEnabled()) {
$this->_indexUrl = $request->getBaseUrl();
} else {
$this->_indexUrl = $request->getBaseUrl() . '/index.php';
}
Hook::call('Router::getIndexUrl', [&$this->_indexUrl]);
}
return $this->_indexUrl;
}
//
// Protected template methods to be implemented by sub-classes.
//
/**
* Determine the filename to use for a local cache file.
*
* @param PKPRequest $request
*
* @return string
*/
public function getCacheFilename($request)
{
throw new Exception('Unimplemented');
}
/**
* Routes a given request to a handler operation
*
* @param PKPRequest $request
*/
abstract public function route($request);
/**
* Build a handler request URL into PKPApplication.
*
* @param PKPRequest $request the request to be routed
* @param mixed $newContext Optional contextual paths
* @param string $handler Optional name of the handler to invoke
* @param string $op Optional name of operation to invoke
* @param mixed $path Optional string or array of args to pass to handler
* @param array $params Optional set of name => value pairs to pass as user parameters
* @param string $anchor Optional name of anchor to add to URL
* @param bool $escape Whether or not to escape ampersands, square brackets, etc. for this URL; default false.
*
* @return string the URL
*/
abstract public function url(
PKPRequest $request,
?string $newContext = null,
$handler = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
);
/**
* Handle an authorization failure.
*
* @param PKPRequest $request
* @param string $authorizationMessage a translation key with the authorization
* failure message.
*/
abstract public function handleAuthorizationFailure(
$request,
$authorizationMessage,
array $messageParams = []
);
//
// Private helper methods
//
/**
* This is the method that implements the basic
* life-cycle of a handler request:
* 1) authorization
* 2) validation
* 3) initialization
* 4) execution
* 5) client response
*
* @param callable|array $serviceEndpoint the handler operation
* @param PKPRequest $request
* @param array $args
* @param bool $validate whether or not to execute the
* validation step.
*/
public function _authorizeInitializeAndCallRequest(&$serviceEndpoint, $request, &$args, $validate = true)
{
$dispatcher = $this->getDispatcher();
// It's conceivable that a call has gotten this far without
// actually being callable, e.g. a component has been named
// that does not exist and that no plugin has registered.
if (!is_callable($serviceEndpoint)) {
$dispatcher->handle404();
}
// Pass the dispatcher to the handler.
$serviceEndpoint[0]->setDispatcher($dispatcher);
// Authorize the request.
$roleAssignments = $serviceEndpoint[0]->getRoleAssignments();
assert(is_array($roleAssignments));
if ($serviceEndpoint[0]->authorize($request, $args, $roleAssignments)) {
// Execute class-wide data integrity checks.
if ($validate) {
$serviceEndpoint[0]->validate($request, $args);
}
// Let the handler initialize itself.
$serviceEndpoint[0]->initialize($request, $args);
// Call the service endpoint.
$result = call_user_func($serviceEndpoint, $args, $request);
} else {
// Authorization failed - try to retrieve a user
// message.
$authorizationMessage = $serviceEndpoint[0]->getLastAuthorizationMessage();
// Set a generic authorization message if no
// specific authorization message was set.
if ($authorizationMessage == '') {
$authorizationMessage = 'user.authorization.accessDenied';
}
// Handle the authorization failure.
$result = $this->handleAuthorizationFailure($request, $authorizationMessage);
}
// Return the result of the operation to the client.
if (is_string($result)) {
echo $result;
} elseif ($result instanceof \PKP\core\JSONMessage) {
header('Content-Type: application/json');
echo $result->getString();
}
}
/**
* Build the base URL and add the context part of the URL.
*
* The new URL will be based on the current request's context
* if no new context is given.
*
* The base URL for a given primary context can be overridden
* in the config file using the 'base_url[context]' syntax in the
* config file's 'general' section.
*
* @param PKPRequest $request the request to be routed
* @param mixed $newContext (optional) context that differs from
* the current request's context
*
* @return array An array consisting of the base url as the first
* entry and the context as the remaining entries.
*/
public function _urlGetBaseAndContext($request, ?string $newContext = null)
{
if (isset($newContext)) {
// A new context has been set so use it.
$contextValue = rawurlencode($newContext);
} else {
// No new context has been set so determine
// the current request's context
$contextObject = $this->getContext($request);
$contextValue = $contextObject?->getPath() ?? 'index';
}
// Check whether the base URL is overridden.
if ($overriddenBaseUrl = Config::getVar('general', "base_url[{$contextValue}]")) {
return [$overriddenBaseUrl, []];
}
return [$this->getIndexUrl($request), [$contextValue]];
}
/**
* Build the additional parameters part of the URL.
*
* @param PKPRequest $request the request to be routed
* @param array $params (optional) the parameter list to be
* transformed to a url part.
* @param bool $escape (optional) Whether or not to escape structural elements
*
* @return array the encoded parameters or an empty array
* if no parameters were given.
*/
public function _urlGetAdditionalParameters($request, $params = null, $escape = true)
{
$additionalParameters = [];
if (!empty($params)) {
assert(is_array($params));
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $element) {
$additionalParameters[] = $key . ($escape ? '%5B%5D=' : '[]=') . rawurlencode($element);
}
} else {
$additionalParameters[] = $key . '=' . rawurlencode($value ?? '');
}
}
}
return $additionalParameters;
}
/**
* Creates a valid URL from parts.
*
* @param string $baseUrl the protocol, domain and initial path/parameters, no anchors allowed here
* @param array $pathInfoArray strings to be concatenated as path info
* @param array $queryParametersArray strings to be concatenated as query string
* @param ?string $anchor an additional anchor
* @param bool $escape whether to escape ampersands
*
* @return string the URL
*/
public function _urlFromParts(string $baseUrl, array $pathInfoArray = [], array $queryParametersArray = [], ?string $anchor = '', bool $escape = false)
{
// Parse the base url
$baseUrlParts = parse_url($baseUrl);
assert(isset($baseUrlParts['host']) && !isset($baseUrlParts['fragment']));
// Reconstruct the base url without path and query
$baseUrl = (isset($baseUrlParts['scheme']) ? $baseUrlParts['scheme'] . ':' : '') . '//';
if (isset($baseUrlParts['user'])) {
$baseUrl .= $baseUrlParts['user'];
if (isset($baseUrlParts['pass'])) {
$baseUrl .= ':' . $baseUrlParts['pass'];
}
$baseUrl .= '@';
}
$baseUrl .= $baseUrlParts['host'];
if (isset($baseUrlParts['port'])) {
$baseUrl .= ':' . $baseUrlParts['port'];
}
$baseUrl .= '/';
// Add path info from the base URL to the path info array (if any).
if (isset($baseUrlParts['path'])) {
$pathInfoArray = array_merge(explode('/', trim($baseUrlParts['path'], '/')), $pathInfoArray);
}
// Add query parameters from the base URL to the query parameter array (if any).
if (isset($baseUrlParts['query'])) {
$queryParametersArray = array_merge(explode('&', $baseUrlParts['query']), $queryParametersArray);
}
// Expand path info
$pathInfo = implode('/', $pathInfoArray);
// Expand query parameters
$amp = $escape ? '&' : '&';
$queryParameters = implode($amp, $queryParametersArray);
$queryParameters = empty($queryParameters) ? '' : '?' . $queryParameters;
// Assemble and return the final URL
return $baseUrl . $pathInfo . $queryParameters . $anchor;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPRouter', '\PKPRouter');
}
|