owncloud/core

View on GitHub
lib/private/AppFramework/Http/Request.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * @author Bart Visscher <bartv@thisnet.nl>
 * @author Bernhard Posselt <dev@bernhard-posselt.com>
 * @author Joas Schilling <coding@schilljs.com>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Mitar <mitar.git@tnode.com>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Robin Appelman <icewind@owncloud.com>
 * @author Robin McCorkell <robin@mccorkell.me.uk>
 * @author Roeland Jago Douma <rullzer@owncloud.com>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Thomas Tanghus <thomas@tanghus.net>
 * @author Vincent Petry <pvince81@owncloud.com>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\AppFramework\Http;

use ArrayAccess;
use Countable;
use OC\Security\CSRF\CsrfToken;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\TrustedDomainHelper;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;

/**
 * Class for accessing variables in the request.
 * This class provides an immutable object with request variables.
 *
 * @property array $cookies
 * @property array $env
 * @property array $files
 * @property string $method
 * @property array $parameters
 * @property array $server
 */
class Request implements ArrayAccess, Countable, IRequest {
    public const USER_AGENT_IE = '/(MSIE)|(Trident)/';
    public const USER_AGENT_IE_8 = '/MSIE 8.0/';
    // Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
    public const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/';
    // Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
    public const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
    // Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
    public const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
    // Safari User Agent from http://www.useragentstring.com/pages/Safari/
    public const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
    // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
    public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
    public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
    public const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/';
    public const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
    public const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
    public const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/';

    protected $inputStream;
    protected $content;
    protected $items = [];
    protected $allowedKeys = [
        'get',
        'post',
        'files',
        'server',
        'env',
        'cookies',
        'urlParams',
        'parameters',
        'method',
        'requesttoken',
    ];
    /** @var ISecureRandom */
    protected $secureRandom;
    /** @var IConfig */
    protected $config;
    /** @var string */
    protected $requestId = '';
    /** @var ICrypto */
    protected $crypto;
    /** @var CsrfTokenManager|null */
    protected $csrfTokenManager;

    /** @var bool */
    protected $contentDecoded = false;

    protected array $query = [];

    /**
     * @param array $vars An associative array with the following optional values:
     *        - array 'urlParams' the parameters which were matched from the URL
     *        - array 'get' the $_GET array
     *        - array|string 'post' the $_POST array or JSON string
     *        - array 'files' the $_FILES array
     *        - array 'server' the $_SERVER array
     *        - array 'env' the $_ENV array
     *        - array 'cookies' the $_COOKIE array
     *        - string 'method' the request method (GET, POST etc)
     *        - string|false 'requesttoken' the requesttoken or false when not available
     * @see http://www.php.net/manual/en/reserved.variables.php
     */
    public function __construct(
        array $vars= [],
        ISecureRandom $secureRandom = null,
        IConfig $config = null,
        CsrfTokenManager $csrfTokenManager = null,
        $stream = 'php://input'
    ) {
        $this->inputStream = $stream;
        $this->items['params'] = [];
        $this->secureRandom = $secureRandom;
        $this->config = $config;
        $this->csrfTokenManager = $csrfTokenManager;

        if (!\array_key_exists('method', $vars)) {
            $vars['method'] = 'GET';
        }

        foreach ($this->allowedKeys as $name) {
            $this->items[$name] = $vars[$name] ?? [];
        }

        $this->items['parameters'] = \array_merge(
            $this->items['get'],
            $this->items['post'],
            $this->items['urlParams'],
            $this->items['params']
        );

        # fill query array
        $request_uri = $this->server['REQUEST_URI'] ?? '';
        $query = parse_url($request_uri, PHP_URL_QUERY);
        if ($query) {
            parse_str(html_entity_decode($query), $qs);
            $this->query = $qs;
        }
    }
    /**
     * @param array $parameters
     */
    public function setUrlParameters(array $parameters): void {
        $this->items['urlParams'] = $parameters;
        $this->items['parameters'] = \array_merge(
            $this->items['parameters'],
            $this->items['urlParams']
        );
    }

    /**
     * Countable method
     * @return int
     */
    public function count() {
        return \count(\array_keys($this->items['parameters']));
    }

    /**
    * ArrayAccess methods
    *
    * Gives access to the combined GET, POST and urlParams arrays
    *
    * Examples:
    *
    * $var = $request['myvar'];
    *
    * or
    *
    * if(!isset($request['myvar']) {
    *     // Do something
    * }
    *
    * $request['myvar'] = 'something'; // This throws an exception.
    *
    * @param string $offset The key to lookup
    * @return boolean
    */
    public function offsetExists($offset) {
        return isset($this->items['parameters'][$offset]);
    }

    /**
    * @see offsetExists
    */
    public function offsetGet($offset) {
        return $this->items['parameters'][$offset] ?? null;
    }

    /**
    * @see offsetExists
    */
    public function offsetSet($offset, $value) {
        throw new \RuntimeException('You cannot change the contents of the request object');
    }

    /**
    * @see offsetExists
    */
    public function offsetUnset($offset) {
        throw new \RuntimeException('You cannot change the contents of the request object');
    }

    /**
     * Magic property accessors
     * @param string $name
     * @param mixed $value
     */
    public function __set($name, $value) {
        throw new \RuntimeException('You cannot change the contents of the request object');
    }

    /**
    * Access request variables by method and name.
    * Examples:
    *
    * $request->post['myvar']; // Only look for POST variables
    * $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
    * Looks in the combined GET, POST and urlParams array.
    *
    * If you access e.g. ->post but the current HTTP request method
    * is GET a \LogicException will be thrown.
    *
    * @param string $name The key to look for.
    * @return mixed|null
    *@throws \LogicException
    */
    public function __get(string $name) {
        switch ($name) {
            case 'query':
                return $this->query;
            case 'put':
            case 'patch':
            case 'get':
            case 'post':
                if ($this->method !== \strtoupper($name)) {
                    throw new \LogicException(\sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
                }
                return $this->getContent();
            case 'files':
            case 'server':
            case 'env':
            case 'cookies':
            case 'urlParams':
            case 'method':
                return $this->items[$name] ?? null;
            case 'parameters':
            case 'params':
                return $this->getContent();
            default:
                return $this[$name] ?? null;
        }
    }

    /**
     * @param string $name
     * @return bool
     */
    public function __isset($name) {
        if (\in_array($name, $this->allowedKeys, true)) {
            return true;
        }
        return isset($this->items['parameters'][$name]);
    }

    /**
     * @param string $id
     */
    public function __unset($id) {
        throw new \RuntimeException('You cannot change the contents of the request object');
    }

    /**
     * Returns the value for a specific http header.
     *
     * This method returns null if the header did not exist.
     *
     * @param string $name
     * @return string|null
     */
    public function getHeader(string $name): ?string {
        $name = \strtoupper(\str_replace(['-'], ['_'], $name));
        if (isset($this->server['HTTP_' . $name])) {
            return $this->server['HTTP_' . $name];
        }

        // There's a few headers that seem to end up in the top-level
        // server array.
        switch ($name) {
            case 'CONTENT_TYPE':
            case 'CONTENT_LENGTH':
                if (isset($this->server[$name])) {
                    return $this->server[$name];
                }
                break;
        }

        return null;
    }

    /**
     * Lets you access post and get parameters by the index
     * In case of json requests the encoded json body is accessed
     *
     * @param string $key the key which you want to access in the URL Parameter
     *                     placeholder, $_POST or $_GET array.
     *                     The priority how they're returned is the following:
     *                     1. URL parameters
     *                     2. POST parameters
     *                     3. GET parameters
     * @param mixed $default If the key is not found, this value will be returned
     * @return mixed the content of the array
     */
    public function getParam($key, $default = null) {
        return $this->parameters[$key] ?? $default;
    }

    /**
     * Returns all params that were received, be it from the request
     * (as GET or POST) or through the URL by the route
     * @return array the array with all parameters
     */
    public function getParams() {
        return $this->parameters;
    }

    /**
     * Returns the method of the request
     * @return string the method of the request (POST, GET, etc)
     */
    public function getMethod() {
        return $this->method;
    }

    /**
     * Shortcut for accessing an uploaded file through the $_FILES array
     * @param string $key the key that will be taken from the $_FILES array
     * @return array|null the file in the $_FILES element
     */
    public function getUploadedFile($key) {
        return $this->files[$key] ?? null;
    }

    /**
     * Shortcut for getting env variables
     * @param string $key the key that will be taken from the $_ENV array
     * @return array|null the value in the $_ENV element
     */
    public function getEnv($key) {
        return $this->env[$key] ?? null;
    }

    /**
     * Shortcut for getting cookie variables
     * @param string $key the key that will be taken from the $_COOKIE array
     * @return string|null the value in the $_COOKIE element
     */
    public function getCookie($key) {
        return $this->cookies[$key] ?? null;
    }

    /**
     * Returns the request body content.
     *
     * If the HTTP request method is PUT and the body
     * not application/x-www-form-urlencoded or application/json a stream
     * resource is returned, otherwise an array.
     *
     * @return array|string|resource The request body content or a resource to read the body stream.
     *
     * @throws \LogicException
     */
    protected function getContent() {
        // If the content can't be parsed into an array then return a stream resource.
        if ($this->method === 'PUT'
            && $this->getHeader('Content-Length') !== 0
            && $this->getHeader('Content-Length') !== null
            && \strpos($this->getHeader('Content-Type') ?? '', 'application/x-www-form-urlencoded') === false
            && \strpos($this->getHeader('Content-Type') ?? '', 'application/json') === false
        ) {
            if ($this->content === false) {
                throw new \LogicException(
                    '"put" can only be accessed once if not '
                    . 'application/x-www-form-urlencoded or application/json.'
                );
            }
            $this->content = false;
            return \fopen($this->inputStream, 'rb');
        }

        $this->decodeContent();
        return $this->items['parameters'];
    }

    /**
     * Attempt to decode the content and populate parameters
     */
    protected function decodeContent() {
        if ($this->contentDecoded) {
            return;
        }
        $params = [];

        // 'application/json' must be decoded manually.
        if (\strpos($this->getHeader('Content-Type') ?? '', 'application/json') !== false) {
            $params = \json_decode(\file_get_contents($this->inputStream), true);
            if (\is_array($params) && \count($params) > 0) {
                $this->items['params'] = $params;
                if ($this->method === 'POST') {
                    $this->items['post'] = $params;
                }
            }

            // Handle application/x-www-form-urlencoded for methods other than GET
            // or post correctly
        } elseif ($this->method !== 'GET'
                && $this->method !== 'POST'
                && \strpos($this->getHeader('Content-Type') ?? '', 'application/x-www-form-urlencoded') !== false) {
            \parse_str(\file_get_contents($this->inputStream), $params);
            if (\is_array($params)) {
                $this->items['params'] = $params;
            }
        }

        if (\is_array($params)) {
            $this->items['parameters'] = \array_merge($this->items['parameters'], $params);
        }
        $this->contentDecoded = true;
    }

    /**
     * Checks if the CSRF check was correct
     * @return bool true if CSRF check passed
     */
    public function passesCSRFCheck() {
        if ($this->csrfTokenManager === null) {
            return false;
        }

        // Is CSRF protection handled outside of ownCloud?
        if ($this->config->getSystemValue('csrf.disabled', false)) {
            return true;
        }

        if (isset($this->items['get']['requesttoken'])) {
            $token = $this->items['get']['requesttoken'];
        } elseif (isset($this->items['post']['requesttoken'])) {
            $token = $this->items['post']['requesttoken'];
        } elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
            $token = $this->items['server']['HTTP_REQUESTTOKEN'];
        } else {
            //no token found.
            return false;
        }
        $token = new CsrfToken($token);

        return $this->csrfTokenManager->isTokenValid($token);
    }

    /**
     * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
     * If an X-Request-ID header is sent by the client this value will be taken.
     * If `mod_unique_id` is installed this value will be taken.
     * @return string
     */
    public function getId() {
        // allow clients to provide a request id
        if (isset($this->server['HTTP_X_REQUEST_ID'])) {
            $reqId = $this->server['HTTP_X_REQUEST_ID'];
            if (\strlen($reqId) > 19
                && \strlen($reqId) < 200
                && \preg_match('%^[a-zA-Z0-9-+/_=.:]+$%', $reqId)) {
                return $this->server['HTTP_X_REQUEST_ID'];
            }

            throw new \InvalidArgumentException('X-Request-ID must be 20-200 bytes of chars, numbers and -+/_=.:');
        }
        if (isset($this->server['UNIQUE_ID'])) {
            return $this->server['UNIQUE_ID'];
        }

        if (empty($this->requestId)) {
            $validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
            $this->requestId = $this->secureRandom->generate(20, $validChars);
        }

        return $this->requestId;
    }

    /**
     * Returns the remote address, if the connection came from a trusted proxy
     * and `forwarded_for_headers` has been configured then the IP address
     * specified in this header will be returned instead.
     * Do always use this instead of $_SERVER['REMOTE_ADDR']
     * @return string IP address
     */
    public function getRemoteAddress() {
        $remoteAddress = $this->server['REMOTE_ADDR'] ?? '';
        $trustedProxies = $this->config->getSystemValue('trusted_proxies', []);

        if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies)) {
            $forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
                'HTTP_X_FORWARDED_FOR'
                // only have one default, so we cannot ship an insecure product out of the box
            ]);

            foreach ($forwardedForHeaders as $header) {
                if (isset($this->server[$header])) {
                    foreach (\explode(',', $this->server[$header]) as $IP) {
                        $IP = \trim($IP);
                        if (\filter_var($IP, FILTER_VALIDATE_IP) !== false) {
                            return $IP;
                        }
                    }
                }
            }
        }

        return $remoteAddress;
    }

    /**
     * Check overwrite condition
     * @param string $type
     * @return bool
     */
    private function isOverwriteCondition($type = '') {
        $regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
        $remoteAddr = $this->server['REMOTE_ADDR'] ?? '';
        return $regex === '//' || \preg_match($regex, $remoteAddr) === 1
        || $type !== 'protocol';
    }

    /**
     * Returns the server protocol. It respects one or more reverse proxies servers
     * and load balancers
     * @return string Server protocol (http or https)
     */
    public function getServerProtocol() {
        if ($this->config->getSystemValue('overwriteprotocol') !== ''
            && $this->isOverwriteCondition('protocol')) {
            return $this->config->getSystemValue('overwriteprotocol');
        }

        if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
            if (\strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
                $parts = \explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
                $proto = \strtolower(\trim($parts[0]));
            } else {
                $proto = \strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
            }

            // Verify that the protocol is always HTTP or HTTPS
            // default to http if an invalid value is provided
            return $proto === 'https' ? 'https' : 'http';
        }

        if (isset($this->server['HTTPS'])
            && $this->server['HTTPS'] !== null
            && $this->server['HTTPS'] !== 'off'
            && $this->server['HTTPS'] !== '') {
            return 'https';
        }

        return 'http';
    }

    /**
     * Returns the used HTTP protocol.
     *
     * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
     */
    public function getHttpProtocol() {
        $claimedProtocol = \strtoupper($this->server['SERVER_PROTOCOL']);

        $validProtocols = [
            'HTTP/1.0',
            'HTTP/1.1',
            'HTTP/2',
        ];

        if (\in_array($claimedProtocol, $validProtocols, true)) {
            return $claimedProtocol;
        }

        return 'HTTP/1.1';
    }

    /**
     * Returns the request uri, even if the website uses one or more
     * reverse proxies
     * @return string
     */
    public function getRequestUri() {
        $uri = $this->server['REQUEST_URI'] ?? '';
        // remove too many leading slashes - can be caused by reverse proxy configuration
        if (\strpos($uri, '/') === 0) {
            $uri = '/' . \ltrim($uri, '/');
        }
        if ($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
            $uri = $this->getScriptName() . \substr($uri, \strlen($this->server['SCRIPT_NAME']));
        } else {
            $components = \parse_url($uri);
            if ($components === false) {
                // due to https://bugs.php.net/bug.php?id=70942 we have to add a
                // fake schema and host to successfully extract path, query and fragment
                $components = \parse_url("http://localhost$uri");
            }
            $uri = $components['path'];
            if (isset($components['query'])) {
                $uri .= '?'.$components['query'];
            }
            if (isset($components['fragment'])) {
                $uri .= '#'.$components['fragment'];
            }
        }
        return $uri;
    }

    /**
     * Get raw PathInfo from request (not urldecoded)
     * @throws \Exception
     * @return string Path info
     */
    public function getRawPathInfo() {
        $requestUri = $this->server['REQUEST_URI'] ?? '';
        // remove too many leading slashes - can be caused by reverse proxy configuration
        if (\strpos($requestUri, '/') === 0) {
            $requestUri = '/' . \ltrim($requestUri, '/');
        }

        $requestUri = \preg_replace('%/{2,}%', '/', $requestUri);

        // Remove the query string from REQUEST_URI
        if ($pos = \strpos($requestUri, '?')) {
            $requestUri = \substr($requestUri, 0, $pos);
        }

        $scriptName = $this->server['SCRIPT_NAME'];
        $scriptName = \preg_replace('%/{2,}%', '/', $scriptName);
        $pathInfo = $requestUri;

        // strip off the script name's dir and file name
        // FIXME: Sabre does not really belong here
        list($path, $name) = \Sabre\Uri\split($scriptName);
        if (!empty($path)) {
            if ($path === $pathInfo || \strpos($pathInfo, $path.'/') === 0) {
                $pathInfo = \substr($pathInfo, \strlen($path));
            } else {
                throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
            }
        }
        if (\strpos($pathInfo, "/$name") === 0) {
            $pathInfo = \substr($pathInfo, \strlen($name) + 1);
        }
        if (\is_string($name) && \strpos($pathInfo, $name) === 0) {
            $pathInfo = \substr($pathInfo, \strlen($name));
        }

        if ($pathInfo === false || $pathInfo === '/') {
            return '';
        }

        return $pathInfo;
    }

    /**
     * Get PathInfo from request
     * @throws \Exception
     * @return string|false Path info or false when not found
     */
    public function getPathInfo() {
        $pathInfo = $this->server['PATH_INFO'] ?? '';
        if ($pathInfo !== '') {
            return $pathInfo;
        }

        $pathInfo = $this->getRawPathInfo();
        $pathInfo = \rawurldecode($pathInfo);
        $encoding = \mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);

        if ($encoding === 'ISO-8859-1') {
            $pathInfo = \mb_convert_encoding($pathInfo, 'UTF-8', $encoding);
        }

        return $pathInfo;
    }

    /**
     * Returns the script name, even if the website uses one or more
     * reverse proxies
     * @return string the script name
     */
    public function getScriptName() {
        $name = $this->server['SCRIPT_NAME'];
        $overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
        if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
            // FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
            $serverRoot = \str_replace('\\', '/', \substr(__DIR__, 0, -\strlen('lib/private/appframework/http/')));
            $suburi = \str_replace('\\', '/', \substr(\realpath($this->server['SCRIPT_FILENAME']), \strlen($serverRoot)));
            $name = '/' . \ltrim($overwriteWebRoot . $suburi, '/');
        }
        return $name;
    }

    /**
     * Checks whether the user agent matches a given regex
     * @param array $agent array of agent names
     * @return bool true if at least one of the given agent matches, false otherwise
     */
    public function isUserAgent(array $agent) {
        if (!isset($this->server['HTTP_USER_AGENT'])) {
            return false;
        }
        foreach ($agent as $regex) {
            if (\preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the unverified server host from the headers without checking
     * whether it is a trusted domain
     * @return string Server host
     */
    public function getInsecureServerHost() {
        $host = 'localhost';
        if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
            if (\strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
                $parts = \explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
                $host = \trim(\current($parts));
            } else {
                $host = $this->server['HTTP_X_FORWARDED_HOST'];
            }
        } else {
            if (isset($this->server['HTTP_HOST'])) {
                $host = $this->server['HTTP_HOST'];
            } elseif (isset($this->server['SERVER_NAME'])) {
                $host = $this->server['SERVER_NAME'];
            }
        }
        return $host;
    }

    /**
     * Returns the server host from the headers, or the first configured
     * trusted domain if the host isn't in the trusted list
     * @return string Server host
     */
    public function getServerHost() {
        // overwritehost is always trusted
        $host = $this->getOverwriteHost();
        if ($host !== null) {
            return $host;
        }

        // get the host from the headers
        $host = $this->getInsecureServerHost();

        // Verify that the host is a trusted domain if the trusted domains
        // are defined
        // If no trusted domain is provided the first trusted domain is returned
        $trustedDomainHelper = new TrustedDomainHelper($this->config);
        if ($trustedDomainHelper->isTrustedDomain($host)) {
            return $host;
        }

        $trustedList = $this->config->getSystemValue('trusted_domains', []);
        if (!empty($trustedList)) {
            return $trustedList[0];
        }

        return '';
    }

    /**
     * Returns the overwritehost setting from the config if set and
     * if the overwrite condition is met
     * @return string|null overwritehost value or null if not defined or the defined condition
     * isn't met
     */
    private function getOverwriteHost() {
        if ($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
            return $this->config->getSystemValue('overwritehost');
        }
        return null;
    }
}