* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Server { public const HOST_FROM_SERVER = 1; public const HOST_FROM_HEADER = 2; public const HOST_ALLOW_EMPTY = 4; /** * Cache for the cli status * * @var bool|null */ public static $cli; /** * List of trusted hosts * * @var array */ public static $hosts = []; /** * Returns the server's IP address * * @return string */ public static function address(): string { return static::get('SERVER_ADDR', ''); } /** * Checks if the request is being served by the CLI * * @return bool */ public static function cli(): bool { if (static::$cli !== null) { return static::$cli; } if (defined('STDIN') === true) { return static::$cli = true; } $term = getenv('TERM'); if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') { return static::$cli = true; } return static::$cli = false; } /** * Gets a value from the _SERVER array * * * Server::get('document_root'); * // sample output: /var/www/kirby * * Server::get(); * // returns the whole server array * * * @param mixed $key The key to look for. Pass false or null to * return the entire server array. * @param mixed $default Optional default value, which should be * returned if no element has been found * @return mixed */ public static function get($key = null, $default = null) { if ($key === null) { return $_SERVER; } $key = strtoupper($key); $value = $_SERVER[$key] ?? $default; return static::sanitize($key, $value); } /** * Returns the correct host * * @param bool $forwarded Deprecated. Todo: remove in 3.7.0 * @return string */ public static function host(bool $forwarded = false): string { $hosts[] = static::get('SERVER_NAME'); $hosts[] = static::get('SERVER_ADDR'); // insecure host parameters are only allowed when hosts // are validated against set of host patterns if (empty(static::$hosts) === false) { $hosts[] = static::get('HTTP_HOST'); $hosts[] = static::get('HTTP_X_FORWARDED_HOST'); } // remove empty hosts $hosts = array_filter($hosts); foreach ($hosts as $host) { if (static::isAllowedHost($host) === true) { return explode(':', $host)[0]; } } return ''; } /** * Setter and getter for the the static $hosts property * * $hosts = null -> return all defined hosts * $hosts = Server::HOST_FROM_SERVER -> [] * $hosts = Server::HOST_FROM_HEADER -> ['*'] * $hosts = array -> [array of trusted hosts] * $hosts = string -> [single trusted host] * * @param string|array|int|null $hosts * @return array */ public static function hosts($hosts = null): array { if ($hosts === null) { return static::$hosts; } if (is_int($hosts) && $hosts & static::HOST_FROM_SERVER) { return static::$hosts = []; } if (is_int($hosts) && $hosts & static::HOST_FROM_HEADER) { return static::$hosts = ['*']; } // make sure hosts are always an array $hosts = A::wrap($hosts); // return unique hosts return static::$hosts = array_unique($hosts); } /** * Checks for a https request * * @return bool */ public static function https(): bool { $https = $_SERVER['HTTPS'] ?? null; $off = ['off', null, '', 0, '0', false, 'false', -1, '-1']; // check for various options to send a negative HTTPS header if (in_array($https, $off, true) === false) { return true; } // check for the port if (static::port() === 443) { return true; } return false; } /** * Checks for allowed host names * * @param string $host * @return bool */ public static function isAllowedHost(string $host): bool { if (empty(static::$hosts) === true) { return true; } foreach (static::$hosts as $pattern) { if (empty($pattern) === true) { continue; } if (fnmatch($pattern, $host) === true) { return true; } } return false; } /** * Checks if the server is behind a * proxy server. * * @return bool */ public static function isBehindProxy(): bool { return empty($_SERVER['HTTP_X_FORWARDED_HOST']) === false; } /** * Returns the correct port number * * @param bool $forwarded Deprecated. Todo: remove in 3.7.0 * @return int */ public static function port(bool $forwarded = false): int { $port = null; // handle reverse proxy setups if (static::isBehindProxy() === true) { // based on forwarded port $port = static::get('HTTP_X_FORWARDED_PORT'); // based on the forwarded host if (empty($port) === true) { $port = (int)parse_url(static::get('HTTP_X_FORWARDED_HOST'), PHP_URL_PORT); } // based on the forwarded proto if (empty($port) === true) { if (in_array($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null, ['https', 'https, http']) === true) { $port = 443; } } } // based on the host if (empty($port) === true) { $port = (int)parse_url(static::get('HTTP_HOST'), PHP_URL_PORT); } // based on server port if (empty($port) === true) { $port = static::get('SERVER_PORT'); } return $port ?? 0; } /** * Returns an array with path and query * from the REQUEST_URI * * @return array */ public static function requestUri(): array { $uri = static::get('REQUEST_URI', ''); if (Url::isAbsolute($uri) === true) { $uri = parse_url($uri); } else { // the fake domain is needed to make sure the URL parsing is // always correct. Even if there's a colon in the path for params $uri = parse_url('http://getkirby.com' . $uri); } return [ 'path' => $uri['path'] ?? null, 'query' => $uri['query'] ?? null, ]; } /** * Help to sanitize some _SERVER keys * * @param string $key * @param mixed $value * @return mixed */ public static function sanitize(string $key, $value) { switch ($key) { case 'SERVER_ADDR': case 'SERVER_NAME': case 'HTTP_HOST': case 'HTTP_X_FORWARDED_HOST': $value ??= ''; $value = strtolower($value); $value = strip_tags($value); $value = basename($value); $value = preg_replace('![^\w.:-]+!iu', '', $value); $value = htmlspecialchars($value, ENT_COMPAT); $value = trim($value, '-'); $value = trim($value, '.'); break; case 'SERVER_PORT': case 'HTTP_X_FORWARDED_PORT': $value ??= ''; $value = (int)(preg_replace('![^0-9]+!', '', $value)); break; } return $value; } /** * Returns the path to the php script * within the document root without the * filename of the script. * * i.e. /subfolder/index.php -> subfolder * * This can be used to build the base url * for subfolder installations * * @return string */ public static function scriptPath(): string { if (static::cli() === true) { return ''; } $path = $_SERVER['SCRIPT_NAME'] ?? ''; // replace Windows backslashes $path = str_replace('\\', '/', $path); // remove the script $path = dirname($path); // replace those fucking backslashes again $path = str_replace('\\', '/', $path); // remove the leading and trailing slashes $path = trim($path, '/'); // top-level scripts don't have a path // and dirname() will return '.' if ($path === '.') { $path = ''; } return $path; } }