<?php

/**
 * @file classes/core/PKPRequest.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 PKPRequest
 *
 * @ingroup core
 *
 * @brief Class providing operations associated with HTTP requests.
 */

namespace PKP\core;

use APP\core\Application;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use PKP\config\Config;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\Validation;
use PKP\session\Session;
use PKP\session\SessionManager;
use PKP\site\Site;
use PKP\site\SiteDAO;
use PKP\user\User;

class PKPRequest
{
    //
    // Internal state - please do not reference directly
    //
    /** @var PKPRouter router instance used to route this request */
    public $_router = null;

    /** @var Dispatcher dispatcher instance used to dispatch this request */
    public $_dispatcher = null;

    /** @var array the request variables cache (GET/POST) */
    public $_requestVars = null;

    /** @var string request base path */
    public $_basePath;

    /** @var string request path */
    public $_requestPath;

    /** @var bool true if restful URLs are enabled in the config */
    public $_isRestfulUrlsEnabled;

    /** @var string server host */
    public $_serverHost;

    /** @var string request protocol */
    public $_protocol;

    /** @var bool bot flag */
    public $_isBot;

    /** @var string user agent */
    public $_userAgent;


    /**
     * get the router instance
     *
     * @return PKPRouter
     */
    public function &getRouter()
    {
        return $this->_router;
    }

    /**
     * set the router instance
     *
     * @param PKPRouter $router
     */
    public function setRouter($router)
    {
        $this->_router = $router;
    }

    /**
     * Set the dispatcher
     *
     * @param Dispatcher $dispatcher
     */
    public function setDispatcher($dispatcher)
    {
        $this->_dispatcher = $dispatcher;
    }

    /**
     * Get the dispatcher
     *
     * @return Dispatcher
     */
    public function &getDispatcher()
    {
        if (! $this->_dispatcher) {
            $application = Application::get();

            $this->setDispatcher($application->getDispatcher());
        }

        return $this->_dispatcher;
    }


    /**
     * Perform an HTTP redirect to an absolute or relative (to base system URL) URL.
     *
     * @param string $url (exclude protocol for local redirects)
     */
    public function redirectUrl($url)
    {
        if (Hook::call('Request::redirect', [&$url])) {
            return;
        }

        header("Location: {$url}");
        exit;
    }

    /**
     * Request an HTTP redirect via JSON to be used from components.
     *
     * @param string $url
     *
     * @return JSONMessage
     */
    public function redirectUrlJson($url)
    {
        $json = new JSONMessage(true);
        $json->setEvent('redirectRequested', $url);
        return $json;
    }

    /**
     * Redirect to the current URL, forcing the HTTPS protocol to be used.
     */
    public function redirectSSL()
    {
        // Note that we are intentionally skipping PKP processing of REQUEST_URI and QUERY_STRING for a protocol redirect
        // This processing is deferred to the redirected (target) URI
        $url = 'https://' . $this->getServerHost() . $_SERVER['REQUEST_URI'];
        $queryString = $_SERVER['QUERY_STRING'];
        if (!empty($queryString)) {
            $url .= "?{$queryString}";
        }
        $this->redirectUrl($url);
    }

    /**
     * Redirect to the current URL, forcing the HTTP protocol to be used.
     */
    public function redirectNonSSL()
    {
        // Note that we are intentionally skipping PKP processing of REQUEST_URI and QUERY_STRING for a protocol redirect
        // This processing is deferred to the redirected (target) URI
        $url = 'http://' . $this->getServerHost() . $_SERVER['REQUEST_URI'];
        $queryString = $_SERVER['QUERY_STRING'];
        if (!empty($queryString)) {
            $url .= "?{$queryString}";
        }
        $this->redirectUrl($url);
    }

    /**
     * Get the IF_MODIFIED_SINCE date (as a numerical timestamp) if available
     *
     * @return ?int
     */
    public function getIfModifiedSince()
    {
        if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
            return null;
        }
        return strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    }

    /**
     * Get the base URL of the request (excluding script).
     *
     * @param bool $allowProtocolRelative True iff protocol-relative URLs are allowed
     *
     * @return string
     */
    public function getBaseUrl($allowProtocolRelative = false)
    {
        $serverHost = $this->getServerHost(false);
        if ($serverHost !== false) {
            // Auto-detection worked.
            if ($allowProtocolRelative) {
                $baseUrl = '//' . $this->getServerHost() . $this->getBasePath();
            } else {
                $baseUrl = $this->getProtocol() . '://' . $this->getServerHost() . $this->getBasePath();
            }
        } else {
            // Auto-detection didn't work (e.g. this is a command-line call); use configuration param
            $baseUrl = Config::getVar('general', 'base_url');
        }
        Hook::call('Request::getBaseUrl', [&$baseUrl]);
        return $baseUrl;
    }

    /**
     * Get the base path of the request (excluding trailing slash).
     *
     * @return string
     */
    public function getBasePath()
    {
        if (!isset($this->_basePath)) {
            // Strip the PHP filename off of the script's executed path
            // We expect the SCRIPT_NAME to look like /path/to/file.php
            // If the SCRIPT_NAME ends in /, assume this is the directory and the script's actual name
            // is masked as the DirectoryIndex
            // If the SCRIPT_NAME ends in neither / or .php, assume the the script's actual name is masked
            // and we need to avoid stripping the terminal directory
            $path = preg_replace('#/[^/]*$#', '', $_SERVER['SCRIPT_NAME'] . (substr($_SERVER['SCRIPT_NAME'], -1) == '/' || preg_match('#.php$#i', $_SERVER['SCRIPT_NAME']) ? '' : '/'));

            // Encode characters which need to be encoded in a URL.
            // Simply using rawurlencode() doesn't work because it
            // also encodes characters which are valid in a URL (i.e. @, $).
            $parts = explode('/', $path);
            foreach ($parts as $i => $part) {
                $pieces = array_map([$this, 'encodeBasePathFragment'], str_split($part));
                $parts[$i] = implode('', $pieces);
            }
            $this->_basePath = implode('/', $parts);

            if ($this->_basePath == '/' || $this->_basePath == '\\') {
                $this->_basePath = '';
            }
            Hook::call('Request::getBasePath', [&$this->_basePath]);
        }

        return $this->_basePath;
    }

    /**
     * Callback function for getBasePath() to correctly encode (or not encode)
     * a basepath fragment.
     *
     * @param string $fragment
     *
     * @return string
     */
    public function encodeBasePathFragment($fragment)
    {
        if (!preg_match('/[A-Za-z0-9-._~!$&\'()*+,;=:@]/', $fragment)) {
            return rawurlencode($fragment);
        }
        return $fragment;
    }

    /**
     * Deprecated
     *
     * @see PKPPageRouter::getIndexUrl()
     */
    public function getIndexUrl()
    {
        static $indexUrl;

        if (!isset($indexUrl)) {
            $indexUrl = $this->_delegateToRouter('getIndexUrl');

            // Call legacy hook
            Hook::call('Request::getIndexUrl', [&$indexUrl]);
        }

        return $indexUrl;
    }

    /**
     * Get the complete URL to this page, including parameters.
     *
     * @return string
     */
    public function getCompleteUrl()
    {
        static $completeUrl;

        if (!isset($completeUrl)) {
            $completeUrl = $this->getRequestUrl();
            $queryString = $this->getQueryString();
            if (!empty($queryString)) {
                $completeUrl .= "?{$queryString}";
            }
            Hook::call('Request::getCompleteUrl', [&$completeUrl]);
        }

        return $completeUrl;
    }

    /**
     * Get the complete URL of the request.
     *
     * @return string
     */
    public function getRequestUrl()
    {
        static $requestUrl;

        if (!isset($requestUrl)) {
            $requestUrl = $this->getProtocol() . '://' . $this->getServerHost() . $this->getRequestPath();
            Hook::call('Request::getRequestUrl', [&$requestUrl]);
        }

        return $requestUrl;
    }

    /**
     * Get the complete set of URL parameters to the current request.
     *
     * @return string
     */
    public function getQueryString()
    {
        static $queryString;

        if (!isset($queryString)) {
            $queryString = $_SERVER['QUERY_STRING'] ?? '';
            Hook::call('Request::getQueryString', [&$queryString]);
        }

        return $queryString;
    }

    /**
     * Get the complete set of URL parameters to the current request as an
     * associative array.
     *
     * @return array
     */
    public function getQueryArray()
    {
        $queryString = $this->getQueryString();
        $queryArray = [];

        if (isset($queryString)) {
            parse_str($queryString, $queryArray);
        }

        return $queryArray;
    }

    /**
     * Get the completed path of the request.
     *
     * @return string
     */
    public function getRequestPath()
    {
        if (!isset($this->_requestPath)) {
            if ($this->isRestfulUrlsEnabled()) {
                $this->_requestPath = $this->getBasePath();
            } else {
                $this->_requestPath = $_SERVER['SCRIPT_NAME'] ?? '';
            }

            $this->_requestPath .= $_SERVER['PATH_INFO'] ?? '';
            Hook::call('Request::getRequestPath', [&$this->_requestPath]);
        }
        return $this->_requestPath;
    }

    /**
     * Get the server hostname in the request.
     *
     * @param string $default Default hostname (defaults to localhost)
     * @param bool $includePort Whether to include non-standard port number; default true
     *
     * @return string
     */
    public function getServerHost($default = null, $includePort = true)
    {
        if ($default === null) {
            $default = 'localhost';
        }

        if (!isset($this->_serverHost)) {
            $this->_serverHost = $_SERVER['HTTP_X_FORWARDED_HOST']
                ?? ($_SERVER['HTTP_HOST']
                ?? ($_SERVER['SERVER_NAME']
                ?? $default));
            // in case of multiple host entries in the header (e.g. multiple reverse proxies) take the first entry
            $this->_serverHost = strtok($this->_serverHost, ',');
            Hook::call('Request::getServerHost', [&$this->_serverHost, &$default, &$includePort]);
        }
        if (!$includePort) {
            // Strip the port number, if one is included. (#3912)
            return preg_replace("/:\d*$/", '', $this->_serverHost);
        }
        return $this->_serverHost;
    }

    /**
     * Get the protocol used for the request (HTTP or HTTPS).
     *
     * @return string
     */
    public function getProtocol()
    {
        if (!isset($this->_protocol)) {
            $this->_protocol = (!isset($_SERVER['HTTPS']) || strtolower_codesafe($_SERVER['HTTPS']) != 'on') ? 'http' : 'https';
            Hook::call('Request::getProtocol', [&$this->_protocol]);
        }
        return $this->_protocol;
    }

    /**
     * Get the request method
     *
     * @return string
     */
    public function getRequestMethod()
    {
        return ($_SERVER['REQUEST_METHOD'] ?? '');
    }

    /**
     * Determine whether the request is a POST request
     *
     * @return bool
     */
    public function isPost()
    {
        return ($this->getRequestMethod() == 'POST');
    }

    /**
     * Determine whether the request is a GET request
     *
     * @return bool
     */
    public function isGet()
    {
        return ($this->getRequestMethod() == 'GET');
    }

    /**
     * Determine whether a CSRF token is present and correct.
     *
     * @return bool
     */
    public function checkCSRF()
    {
        $session = $this->getSession();
        return $this->getUserVar('csrfToken') == $session->getCSRFToken();
    }

    /**
     * Get the remote IP address of the current request.
     *
     * @return string
     */
    public function getRemoteAddr()
    {
        $ipaddr = & Registry::get('remoteIpAddr'); // Reference required.
        if (is_null($ipaddr)) {
            if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) &&
                Config::getVar('general', 'trust_x_forwarded_for', true) &&
                preg_match_all('/([0-9.a-fA-F:]+)/', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
            } elseif (isset($_SERVER['REMOTE_ADDR']) &&
                preg_match_all('/([0-9.a-fA-F:]+)/', $_SERVER['REMOTE_ADDR'], $matches)) {
            } elseif (preg_match_all('/([0-9.a-fA-F:]+)/', getenv('REMOTE_ADDR'), $matches)) {
            } else {
                $ipaddr = '';
            }

            if (!isset($ipaddr)) {
                // If multiple addresses are listed, take the last. (Supports ipv6.)
                $ipaddr = $matches[0][count($matches[0]) - 1];
            }
            Hook::call('Request::getRemoteAddr', [&$ipaddr]);
        }
        return $ipaddr;
    }

    /**
     * Get the remote domain of the current request
     *
     * @return string
     */
    public function getRemoteDomain()
    {
        static $remoteDomain;
        if (!isset($remoteDomain)) {
            $remoteDomain = null;
            $remoteDomain = @getHostByAddr($this->getRemoteAddr());
            Hook::call('Request::getRemoteDomain', [&$remoteDomain]);
        }
        return $remoteDomain;
    }

    /**
     * Get the user agent of the current request.
     *
     * @return string
     */
    public function getUserAgent()
    {
        if (!isset($this->_userAgent)) {
            if (isset($_SERVER['HTTP_USER_AGENT'])) {
                $this->_userAgent = $_SERVER['HTTP_USER_AGENT'];
            }
            if (!isset($this->_userAgent) || empty($this->_userAgent)) {
                $this->_userAgent = getenv('HTTP_USER_AGENT');
            }
            if (!isset($this->_userAgent) || $this->_userAgent == false) {
                $this->_userAgent = '';
            }
            Hook::call('Request::getUserAgent', [&$this->_userAgent]);
        }
        return $this->_userAgent;
    }

    /**
     * Determine whether the user agent is a bot or not.
     *
     * @return bool
     */
    public function isBot()
    {
        if (!isset($this->_isBot)) {
            $userAgent = $this->getUserAgent();
            $this->_isBot = Core::isUserAgentBot($userAgent);
        }
        return $this->_isBot;
    }

    /**
     * Check if the HTTP_DNT (Do Not Track) is set
     */
    public function getDoNotTrack(): bool
    {
        return (array_key_exists('HTTP_DNT', $_SERVER) && ((int) $_SERVER['HTTP_DNT'] === 1));
    }

    /**
     * Return true if RESTFUL_URLS is enabled.
     */
    public function isRestfulUrlsEnabled()
    {
        if (!isset($this->_isRestfulUrlsEnabled)) {
            $this->_isRestfulUrlsEnabled = Config::getVar('general', 'restful_urls') ? true : false;
        }
        return $this->_isRestfulUrlsEnabled;
    }

    /**
     * Get site data.
     *
     */
    public function getSite(): ?Site
    {
        $site = & Registry::get('site', true, null);
        /** @var SiteDAO */
        $siteDao = DAORegistry::getDAO('SiteDAO');
        return $site ??= $siteDao->getSite();
    }

    /**
     * Get the user session associated with the current request.
     */
    public function getSession(): Session
    {
        $session = & Registry::get('session', true, null);
        return $session ??= SessionManager::getManager()->getUserSession();
    }

    /**
     * Get the user associated with the current request.
     */
    public function getUser(): ?User
    {
        $user = & Registry::get('user', true, null);
        if ($user) {
            return $user;
        }

        // Attempt to load user from API token
        if (($handler = $this->getRouter()->getHandler())
            && ($token = $handler->getApiToken())
            && ($apiUser = Repo::user()->getByApiKey($token))
            && $apiUser->getData('apiKeyEnabled')
        ) {
            return $user = $apiUser;
        }

        // Attempts to retrieve a logged user
        if (Validation::isLoggedIn()) {
            $user = SessionManager::getManager()->getUserSession()->getUser();
        }

        return $user;
    }

    /**
     * Get the value of a GET/POST variable.
     */
    public function getUserVar($key)
    {
        // special treatment for APIRouter. APIHandler gets to fetch parameter first
        $router = $this->getRouter();
        if ($router instanceof \PKP\core\APIRouter && (!is_null($handler = $router->getHandler()))) {
            /** @var APIHandler */
            $handler = $router->getHandler();
            $value = $handler->getParameter($key);
            if (!is_null($value)) {
                return $value;
            }
        }

        // Get all vars (already cleaned)
        $vars = $this->getUserVars();
        return $vars[$key] ?? null;
    }

    /**
     * Get all GET/POST variables as an array
     *
     * @return array
     */
    public function &getUserVars()
    {
        $this->_requestVars ??= array_map(fn ($s) => is_string($s) ? trim($s) : $s, array_merge($_GET, $_POST));
        return $this->_requestVars;
    }

    /**
     * Get the value of a GET/POST variable generated using the Smarty
     * html_select_date and/or html_select_time function.
     *
     * @param string $prefix
     * @param int $defaultDay
     * @param int $defaultMonth
     * @param int $defaultYear
     * @param int $defaultHour
     * @param int $defaultMinute
     * @param int $defaultSecond
     *
     * @return ?int Linux timestamp
     */
    public function getUserDateVar($prefix, $defaultDay = null, $defaultMonth = null, $defaultYear = null, $defaultHour = 0, $defaultMinute = 0, $defaultSecond = 0)
    {
        $monthPart = $this->getUserVar($prefix . 'Month');
        $dayPart = $this->getUserVar($prefix . 'Day');
        $yearPart = $this->getUserVar($prefix . 'Year');
        $hourPart = $this->getUserVar($prefix . 'Hour');
        $minutePart = $this->getUserVar($prefix . 'Minute');
        $secondPart = $this->getUserVar($prefix . 'Second');

        switch ($this->getUserVar($prefix . 'Meridian')) {
            case 'pm':
                if (is_numeric($hourPart) && $hourPart != 12) {
                    $hourPart += 12;
                }
                break;
            case 'am':
            default:
                // Do nothing.
                break;
        }

        if (empty($dayPart)) {
            $dayPart = $defaultDay;
        }
        if (empty($monthPart)) {
            $monthPart = $defaultMonth;
        }
        if (empty($yearPart)) {
            $yearPart = $defaultYear;
        }
        if (empty($hourPart)) {
            $hourPart = $defaultHour;
        }
        if (empty($minutePart)) {
            $minutePart = $defaultMinute;
        }
        if (empty($secondPart)) {
            $secondPart = $defaultSecond;
        }

        if (empty($monthPart) || empty($dayPart) || empty($yearPart)) {
            return null;
        }
        return mktime($hourPart, $minutePart, $secondPart, $monthPart, $dayPart, $yearPart);
    }

    /**
     * Get the value of a cookie variable.
     */
    public function getCookieVar($key)
    {
        if (isset($_COOKIE[$key])) {
            return $_COOKIE[$key];
        } else {
            return null;
        }
    }

    /**
     * Set a cookie variable.
     *
     * @param string $key
     * @param int $expire (optional)
     */
    public function setCookieVar($key, $value, $expire = 0)
    {
        $basePath = $this->getBasePath();
        if (!$basePath) {
            $basePath = '/';
        }

        setcookie($key, $value, $expire, $basePath);
        $_COOKIE[$key] = $value;
    }

    /**
     * Redirect to the specified page within a PKP Application.
     * Shorthand for a common call to $request->redirect($dispatcher->url($request, PKPApplication::ROUTE_PAGE, ...)).
     *
     * @param mixed $context The optional contextual paths
     * @param string $page The name of the op to redirect to.
     * @param string $op optional The name of the op to redirect to.
     * @param mixed $path string or array containing path info for redirect.
     * @param array $params Map of name => value pairs for additional parameters
     * @param string $anchor Name of desired anchor on the target page
     */
    public function redirect($context = null, $page = null, $op = null, $path = null, $params = null, $anchor = null)
    {
        $dispatcher = $this->getDispatcher();
        $this->redirectUrl($dispatcher->url($this, PKPApplication::ROUTE_PAGE, $context, $page, $op, $path, $params, $anchor));
    }

    /**
     * Get the current "context" (press/journal/etc) object.
     *
     * @see PKPPageRouter::getContext()
     */
    public function getContext(): ?Context
    {
        return $this->_delegateToRouter('getContext');
    }

    /**
     * Deprecated
     *
     * @see PKPPageRouter::getRequestedPage()
     */
    public function getRequestedPage()
    {
        return $this->_delegateToRouter('getRequestedPage');
    }

    /**
     * Deprecated
     *
     * @see PKPPageRouter::getRequestedOp()
     */
    public function getRequestedOp()
    {
        return $this->_delegateToRouter('getRequestedOp');
    }

    /**
     * Deprecated
     *
     * @see PKPPageRouter::getRequestedArgs()
     */
    public function getRequestedArgs()
    {
        return $this->_delegateToRouter('getRequestedArgs');
    }

    /**
     * Deprecated
     *
     * @see PKPPageRouter::url()
     *
     * @param null|mixed $context
     * @param null|mixed $page
     * @param null|mixed $op
     * @param null|mixed $path
     * @param null|mixed $params
     * @param null|mixed $anchor
     */
    public function url(
        $context = null,
        $page = null,
        $op = null,
        $path = null,
        $params = null,
        $anchor = null,
        $escape = false
    ) {
        return $this->_delegateToRouter(
            'url',
            $context,
            $page,
            $op,
            $path,
            $params,
            $anchor,
            $escape
        );
    }

    /**
     * Get the URL to the public file uploads directory
     */
    public function getPublicFilesUrl(?Context $context = null): string
    {
        $publicFileManager = new PublicFileManager();

        return join('/', [
            $this->getBaseUrl(),
            $context
                ? $publicFileManager->getContextFilesPath($context->getId())
                : $publicFileManager->getSiteFilesPath()
        ]);
    }

    /**
     * This method exists to maintain backwards compatibility
     * with calls to methods that have been factored into the
     * Router implementations.
     *
     * It delegates the call to the router and returns the result.
     *
     * NB: This method is protected and may not be used by
     * external classes. It should also only be used in legacy
     * methods.
     *
     * @return mixed depends on the called method
     */
    public function &_delegateToRouter($method)
    {
        // This call is deprecated. We don't trigger a
        // deprecation error, though, as there are so
        // many instances of this error that it has a
        // performance impact and renders the error
        // log virtually useless when deprecation
        // warnings are switched on.
        // FIXME: Fix enough instances of this error so that
        // we can put a deprecation warning in here.
        $router = $this->getRouter();

        if (is_null($router)) {
            assert(false);
            $nullValue = null;
            return $nullValue;
        }

        // Construct the method call
        $callable = [$router, $method];

        // Get additional parameters but replace
        // the first parameter (currently the
        // method to be called) with the request
        // as all router methods required the request
        // as their first parameter.
        $parameters = func_get_args();
        $parameters[0] = & $this;

        $returner = call_user_func_array($callable, $parameters);
        return $returner;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\core\PKPRequest', '\PKPRequest');
}
