299 lines
9.0 KiB
PHP
299 lines
9.0 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file classes/core/Dispatcher.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 Dispatcher
|
|
*
|
|
* @ingroup core
|
|
*
|
|
* @brief Class dispatching HTTP requests to handlers.
|
|
*/
|
|
|
|
namespace PKP\core;
|
|
|
|
use APP\core\Services;
|
|
use PKP\config\Config;
|
|
use PKP\plugins\Hook;
|
|
use PKP\plugins\PluginRegistry;
|
|
use PKP\services\PKPSchemaService;
|
|
|
|
class Dispatcher
|
|
{
|
|
/** @var PKPApplication */
|
|
public $_application;
|
|
|
|
/** @var array an array of Router implementation class names */
|
|
public $_routerNames = [];
|
|
|
|
/** @var array an array of Router instances */
|
|
public $_routerInstances = [];
|
|
|
|
/** @var PKPRouter */
|
|
public $_router;
|
|
|
|
/** @var PKPRequest Used for a callback hack - NOT GENERALLY SET. */
|
|
public $_requestCallbackHack;
|
|
|
|
/**
|
|
* Get the application
|
|
*
|
|
* @return PKPApplication
|
|
*/
|
|
public function &getApplication()
|
|
{
|
|
return $this->_application;
|
|
}
|
|
|
|
/**
|
|
* Set the application
|
|
*
|
|
* @param PKPApplication $application
|
|
*/
|
|
public function setApplication($application)
|
|
{
|
|
$this->_application = $application;
|
|
}
|
|
|
|
/**
|
|
* Get the router names
|
|
*
|
|
* @return array an array of Router names
|
|
*/
|
|
public function &getRouterNames()
|
|
{
|
|
return $this->_routerNames;
|
|
}
|
|
|
|
/**
|
|
* Add a router name.
|
|
*
|
|
* NB: Routers will be called in the order that they
|
|
* have been added to the dispatcher. The first router
|
|
* that supports the request will be called. The last
|
|
* router should always be a "catch-all" router that
|
|
* supports all types of requests.
|
|
*
|
|
* NB: Routers must be part of the core package
|
|
* to be accepted by this dispatcher implementation.
|
|
*
|
|
* @param string $routerName a class name of a router
|
|
* to be given the chance to route the request.
|
|
* NB: These are class names and not instantiated objects. We'll
|
|
* use lazy instantiation to reduce the performance/memory impact
|
|
* to a minimum.
|
|
* @param string $shortcut a shortcut name for the router
|
|
* to be used for quick router instance retrieval.
|
|
*/
|
|
public function addRouterName($routerName, $shortcut)
|
|
{
|
|
assert(is_array($this->_routerNames) && is_string($routerName));
|
|
$this->_routerNames[$shortcut] = $routerName;
|
|
}
|
|
|
|
/**
|
|
* Determine the correct router for this request. Then
|
|
* let the router dispatch the request to the appropriate
|
|
* handler method.
|
|
*
|
|
* @param PKPRequest $request
|
|
*/
|
|
public function dispatch($request)
|
|
{
|
|
// Make sure that we have at least one router configured
|
|
$routerNames = $this->getRouterNames();
|
|
assert(count($routerNames) > 0);
|
|
|
|
// Go through all configured routers by priority
|
|
// and find out whether one supports the incoming request
|
|
/** @var PKPRouter */
|
|
$router = null;
|
|
foreach ($routerNames as $shortcut => $routerCandidateName) {
|
|
$routerCandidate = & $this->_instantiateRouter($routerCandidateName, $shortcut);
|
|
|
|
// Does this router support the current request?
|
|
if ($routerCandidate->supports($request)) {
|
|
// Inject router and dispatcher into request
|
|
$request->setRouter($routerCandidate);
|
|
$request->setDispatcher($this);
|
|
|
|
// We've found our router and can go on
|
|
// to handle the request.
|
|
$router = & $routerCandidate;
|
|
$this->_router = & $router;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// None of the router handles this request? This is a development-time
|
|
// configuration error.
|
|
if (is_null($router)) {
|
|
fatalError('None of the configured routers supports this request.');
|
|
}
|
|
|
|
// Can we serve a cached response?
|
|
if ($router->isCacheable($request)) {
|
|
$this->_requestCallbackHack = & $request;
|
|
if (Config::getVar('cache', 'web_cache')) {
|
|
if ($this->_displayCached($router, $request)) {
|
|
exit;
|
|
} // Success
|
|
ob_start([$this, '_cacheContent']);
|
|
}
|
|
} else {
|
|
if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') {
|
|
header('HTTP/1.0 403 Forbidden');
|
|
echo '403: Forbidden<br><br>Pre-fetching not allowed.';
|
|
exit;
|
|
}
|
|
}
|
|
|
|
PluginRegistry::loadCategory('generic', true);
|
|
PluginRegistry::loadCategory('pubIds', true);
|
|
|
|
Hook::call('Dispatcher::dispatch', [$request]);
|
|
|
|
// Reload the context after generic plugins have loaded so that changes to
|
|
// the context schema can take place
|
|
$contextSchema = Services::get('schema')->get(PKPSchemaService::SCHEMA_CONTEXT, true);
|
|
$request->getRouter()->getContext($request, true);
|
|
|
|
$router->route($request);
|
|
}
|
|
|
|
/**
|
|
* Build a handler request URL into PKPApplication.
|
|
*
|
|
* @param PKPRequest $request the request to be routed
|
|
* @param string $shortcut the short name of the router that should be used to construct the URL
|
|
* @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 for this URL; default false.
|
|
*
|
|
* @return string the URL
|
|
*/
|
|
public function url(
|
|
PKPRequest $request,
|
|
string $shortcut,
|
|
?string $newContext = null,
|
|
$handler = null,
|
|
$op = null,
|
|
$path = null,
|
|
$params = null,
|
|
$anchor = null,
|
|
$escape = false
|
|
) {
|
|
// Instantiate the requested router
|
|
assert(isset($this->_routerNames[$shortcut]));
|
|
$routerName = $this->_routerNames[$shortcut];
|
|
$router = & $this->_instantiateRouter($routerName, $shortcut);
|
|
|
|
return $router->url($request, $newContext, $handler, $op, $path, $params, $anchor, $escape);
|
|
}
|
|
|
|
//
|
|
// Private helper methods
|
|
//
|
|
|
|
/**
|
|
* Instantiate a router
|
|
*
|
|
* @param string $routerName
|
|
* @param string $shortcut
|
|
*/
|
|
public function &_instantiateRouter($routerName, $shortcut)
|
|
{
|
|
if (!isset($this->_routerInstances[$shortcut])) {
|
|
// Instantiate the router
|
|
$router = new $routerName();
|
|
if (!$router instanceof \PKP\core\PKPRouter) {
|
|
throw new \Exception('Cannot instantiate requested router. Routers must belong to the core package and be of type "PKPRouter".');
|
|
}
|
|
$router->setApplication($this->_application);
|
|
$router->setDispatcher($this);
|
|
|
|
// Save the router instance for later re-use
|
|
$this->_routerInstances[$shortcut] = & $router;
|
|
}
|
|
|
|
return $this->_routerInstances[$shortcut];
|
|
}
|
|
|
|
/**
|
|
* Display the request contents from cache.
|
|
*
|
|
* @param PKPRouter $router
|
|
*/
|
|
public function _displayCached($router, $request)
|
|
{
|
|
$filename = $router->getCacheFilename($request);
|
|
if (!file_exists($filename)) {
|
|
return false;
|
|
}
|
|
|
|
// Allow a caching proxy to work its magic if possible
|
|
$ifModifiedSince = $request->getIfModifiedSince();
|
|
if ($ifModifiedSince !== null && $ifModifiedSince >= filemtime($filename)) {
|
|
header('HTTP/1.1 304 Not Modified');
|
|
exit;
|
|
}
|
|
|
|
$data = file_get_contents($filename);
|
|
$i = strpos($data, ':');
|
|
$time = substr($data, 0, $i);
|
|
$contents = substr($data, $i + 1);
|
|
|
|
if (time() > $time + Config::getVar('cache', 'web_cache_hours') * 60 * 60) {
|
|
return false;
|
|
}
|
|
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
|
|
echo $contents;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Cache content as a local file.
|
|
*
|
|
* @param string $contents
|
|
*
|
|
* @return string
|
|
*/
|
|
public function _cacheContent($contents)
|
|
{
|
|
assert($this->_router instanceof \PKP\core\PKPRouter);
|
|
if ($contents == '') {
|
|
return $contents;
|
|
} // Do not cache empties
|
|
$filename = $this->_router->getCacheFilename($this->_requestCallbackHack);
|
|
$fp = fopen($filename, 'w');
|
|
if ($fp) {
|
|
fwrite($fp, time() . ':' . $contents);
|
|
fclose($fp);
|
|
}
|
|
return $contents;
|
|
}
|
|
|
|
/**
|
|
* Handle a 404 error (page not found).
|
|
*/
|
|
public static function handle404()
|
|
{
|
|
header('HTTP/1.0 404 Not Found');
|
|
fatalError('404 Not Found');
|
|
}
|
|
}
|
|
|
|
if (!PKP_STRICT_MODE) {
|
|
class_alias('\PKP\core\Dispatcher', '\Dispatcher');
|
|
}
|