<?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');
}
