friendica/src/App/BaseURL.php

474 lines
12 KiB
PHP
Raw Normal View History

<?php
2020-02-08 16:16:42 +00:00
/**
2023-01-01 14:36:24 +00:00
* @copyright Copyright (C) 2010-2023, the Friendica project
2020-02-08 16:16:42 +00:00
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\App;
2021-10-26 19:44:29 +00:00
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\System;
use Friendica\Util\Network;
use Friendica\Util\Strings;
use Friendica\Network\HTTPException;
/**
* A class which checks and contains the basic
2019-04-08 21:12:34 +00:00
* environment for the BaseURL (url, urlpath, ssl_policy, hostname, scheme)
*/
class BaseURL
{
/**
* No SSL necessary
*/
const SSL_POLICY_NONE = 0;
/**
* SSL is necessary
*/
const SSL_POLICY_FULL = 1;
/**
* SSL is optional, but preferred
*/
const SSL_POLICY_SELFSIGN = 2;
2019-04-08 21:12:34 +00:00
/**
* Define the Default SSL scheme
*/
const DEFAULT_SSL_SCHEME = self::SSL_POLICY_SELFSIGN;
/**
* The Friendica Config
*
2021-10-26 19:44:29 +00:00
* @var IManageConfigValues
*/
private $config;
/**
* The server side variables
*
* @var array
*/
private $server;
/**
* The hostname of the Base URL
*
* @var string
*/
private $hostname;
/**
* The SSL_POLICY of the Base URL
*
* @var int
*/
private $sslPolicy;
/**
* The URL sub-path of the Base URL
*
* @var string
*/
private $urlPath;
/**
* The full URL
*
* @var string
*/
private $url;
/**
* The current scheme of this call
*
* @var string
*/
private $scheme;
/**
* Returns the hostname of this node
*
* @return string
*/
public function getHostname(): string
{
return $this->hostname;
}
/**
* Returns the current scheme of this call
*
* @return string
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* Returns the SSL policy of this node
*
* @return int
*/
public function getSSLPolicy(): int
{
return $this->sslPolicy;
}
/**
* Returns the sub-path of this URL
*
* @return string
*/
public function getUrlPath(): string
{
return $this->urlPath;
}
/**
* Returns the full URL of this call
*
* Note: $ssl parameter value doesn't directly correlate with the resulting protocol
*
* @param bool $ssl True, if ssl should get used
*
* @return string
*/
public function get(bool $ssl = false): string
{
2019-04-08 21:12:34 +00:00
if ($this->sslPolicy === self::SSL_POLICY_SELFSIGN && $ssl) {
return Network::switchScheme($this->url);
}
return $this->url;
}
/**
* Save current parts of the base Url
*
* @param string? $hostname
* @param int? $sslPolicy
* @param string? $urlPath
*
* @return bool true, if successful
* @TODO Find proper types
*/
public function save($hostname = null, $sslPolicy = null, $urlPath = null): bool
{
2019-04-10 18:38:39 +00:00
$currHostname = $this->hostname;
$currSSLPolicy = $this->sslPolicy;
$currURLPath = $this->urlPath;
$currUrl = $this->url;
2019-04-09 06:48:04 +00:00
if (!empty($hostname) && $hostname !== $this->hostname) {
2019-04-10 18:38:39 +00:00
if ($this->config->set('config', 'hostname', $hostname)) {
$this->hostname = $hostname;
2019-04-10 18:38:39 +00:00
} else {
return false;
}
}
2019-04-09 06:48:04 +00:00
if (isset($sslPolicy) && $sslPolicy !== $this->sslPolicy) {
2019-04-10 18:38:39 +00:00
if ($this->config->set('system', 'ssl_policy', $sslPolicy)) {
$this->sslPolicy = $sslPolicy;
} else {
$this->hostname = $currHostname;
2019-04-10 18:38:39 +00:00
$this->config->set('config', 'hostname', $this->hostname);
return false;
}
}
2019-04-09 06:48:04 +00:00
if (isset($urlPath) && $urlPath !== $this->urlPath) {
2019-04-10 18:38:39 +00:00
if ($this->config->set('system', 'urlpath', $urlPath)) {
$this->urlPath = $urlPath;
} else {
$this->hostname = $currHostname;
$this->sslPolicy = $currSSLPolicy;
$this->config->set('config', 'hostname', $this->hostname);
$this->config->set('system', 'ssl_policy', $this->sslPolicy);
return false;
}
}
$this->determineBaseUrl();
if ($this->url !== $currUrl) {
if (!$this->config->set('system', 'url', $this->url)) {
$this->hostname = $currHostname;
$this->sslPolicy = $currSSLPolicy;
$this->urlPath = $currURLPath;
$this->determineBaseUrl();
$this->config->set('config', 'hostname', $this->hostname);
$this->config->set('system', 'ssl_policy', $this->sslPolicy);
$this->config->set('system', 'urlpath', $this->urlPath);
return false;
}
}
2019-04-10 18:38:39 +00:00
return true;
}
/**
* Save the current url as base URL
*
* @param string $url
*
* @return bool true, if the save was successful
*/
public function saveByURL(string $url): bool
{
$parsed = @parse_url($url);
if (empty($parsed) || empty($parsed['host'])) {
return false;
}
$hostname = $parsed['host'];
if (!empty($hostname) && !empty($parsed['port'])) {
$hostname .= ':' . $parsed['port'];
}
$urlPath = null;
if (!empty($parsed['path'])) {
$urlPath = trim($parsed['path'], '\\/');
}
2019-04-08 21:12:34 +00:00
$sslPolicy = null;
if (!empty($parsed['scheme'])) {
if ($parsed['scheme'] == 'https') {
$sslPolicy = BaseURL::SSL_POLICY_FULL;
}
}
return $this->save($hostname, $sslPolicy, $urlPath);
}
/**
* Checks, if a redirect to the HTTPS site would be necessary
*
* @return bool
*/
public function checkRedirectHttps()
{
return $this->config->get('system', 'force_ssl') &&
($this->getScheme() == "http") &&
intval($this->getSSLPolicy()) == BaseURL::SSL_POLICY_FULL &&
strpos($this->get(), 'https://') === 0 &&
!empty($this->server['REQUEST_METHOD']) &&
$this->server['REQUEST_METHOD'] === 'GET';
}
/**
2021-10-26 20:09:11 +00:00
* @param IManageConfigValues $config The Friendica IConfiguration
* @param array $server The $_SERVER array
*/
2021-10-26 19:44:29 +00:00
public function __construct(IManageConfigValues $config, array $server)
{
$this->config = $config;
$this->server = $server;
$this->determineSchema();
2019-04-08 21:12:34 +00:00
$this->checkConfig();
}
/**
* Check the current config during loading
*/
public function checkConfig()
{
2019-04-08 21:12:34 +00:00
$this->hostname = $this->config->get('config', 'hostname');
$this->urlPath = $this->config->get('system', 'urlpath');
$this->sslPolicy = $this->config->get('system', 'ssl_policy');
$this->url = $this->config->get('system', 'url');
if (empty($this->hostname)) {
$this->determineHostname();
if (!empty($this->hostname)) {
$this->config->set('config', 'hostname', $this->hostname);
}
}
if (!isset($this->urlPath)) {
$this->determineURLPath();
$this->config->set('system', 'urlpath', $this->urlPath);
}
if (!isset($this->sslPolicy)) {
2019-04-08 21:12:34 +00:00
if ($this->scheme == 'https') {
$this->sslPolicy = self::SSL_POLICY_FULL;
} else {
$this->sslPolicy = self::DEFAULT_SSL_SCHEME;
}
$this->config->set('system', 'ssl_policy', $this->sslPolicy);
}
if (empty($this->url)) {
$this->determineBaseUrl();
2019-04-14 12:40:26 +00:00
if (!empty($this->url)) {
$this->config->set('system', 'url', $this->url);
}
}
}
/**
* Determines the hostname of this node if not set already
*/
private function determineHostname()
{
$this->hostname = '';
if (!empty($this->server['SERVER_NAME'])) {
$this->hostname = $this->server['SERVER_NAME'];
if (!empty($this->server['SERVER_PORT']) && $this->server['SERVER_PORT'] != 80 && $this->server['SERVER_PORT'] != 443) {
$this->hostname .= ':' . $this->server['SERVER_PORT'];
}
}
}
/**
* Figure out if we are running at the top of a domain or in a sub-directory
*/
private function determineURLPath()
{
$this->urlPath = '';
/*
* The automatic path detection in this function is currently deactivated,
* see issue https://github.com/friendica/friendica/issues/6679
*
* The problem is that the function seems to be confused with some url.
* These then confuses the detection which changes the url path.
*/
/* Relative script path to the web server root
* Not all of those $_SERVER properties can be present, so we do by inverse priority order
*/
$relative_script_path =
($this->server['REDIRECT_URL'] ?? '') ?:
($this->server['REDIRECT_URI'] ?? '') ?:
($this->server['REDIRECT_SCRIPT_URL'] ?? '') ?:
($this->server['SCRIPT_URL'] ?? '') ?:
$this->server['REQUEST_URI'] ?? '';
/* $relative_script_path gives /relative/path/to/friendica/module/parameter
* QUERY_STRING gives pagename=module/parameter
*
* To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
*/
if (!empty($relative_script_path)) {
// Module
if (!empty($this->server['QUERY_STRING'])) {
$this->urlPath = trim(dirname($relative_script_path, substr_count(trim($this->server['QUERY_STRING'], '/'), '/') + 1), '/');
} else {
// Root page
$this->urlPath = trim($relative_script_path, '/');
}
}
}
/**
* Determine the full URL based on all parts
*/
private function determineBaseUrl()
{
$scheme = 'http';
if ($this->sslPolicy == self::SSL_POLICY_FULL) {
$scheme = 'https';
}
$this->url = $scheme . '://' . $this->hostname . (!empty($this->urlPath) ? '/' . $this->urlPath : '');
}
/**
* Determine the scheme of the current used link
*/
private function determineSchema()
{
$this->scheme = 'http';
if (!empty($this->server['HTTPS']) ||
!empty($this->server['HTTP_FORWARDED']) && preg_match('/proto=https/', $this->server['HTTP_FORWARDED']) ||
!empty($this->server['HTTP_X_FORWARDED_PROTO']) && $this->server['HTTP_X_FORWARDED_PROTO'] == 'https' ||
!empty($this->server['HTTP_X_FORWARDED_SSL']) && $this->server['HTTP_X_FORWARDED_SSL'] == 'on' ||
!empty($this->server['FRONT_END_HTTPS']) && $this->server['FRONT_END_HTTPS'] == 'on' ||
!empty($this->server['SERVER_PORT']) && (intval($this->server['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much?
) {
$this->scheme = 'https';
}
}
/**
* Removes the base url from an url. This avoids some mixed content problems.
*
* @param string $origURL
*
* @return string The cleaned url
*/
public function remove(string $origURL): string
{
// Remove the hostname from the url if it is an internal link
$nurl = Strings::normaliseLink($origURL);
$base = Strings::normaliseLink($this->get());
$url = str_replace($base . '/', '', $nurl);
// if it is an external link return the orignal value
if ($url == Strings::normaliseLink($origURL)) {
return $origURL;
} else {
return $url;
}
}
/**
* Redirects to another module relative to the current Friendica base URL.
* If you want to redirect to a external URL, use System::externalRedirectTo()
*
* @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
* @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
*
* @throws HTTPException\FoundException
* @throws HTTPException\MovedPermanentlyException
* @throws HTTPException\TemporaryRedirectException
*
* @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
*/
public function redirect(string $toUrl = '', bool $ssl = false)
{
if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
}
$redirectTo = $this->get($ssl) . '/' . ltrim($toUrl, '/');
System::externalRedirect($redirectTo);
}
/**
* Returns the base url as string
*/
public function __toString(): string
{
return (string) $this->get();
}
}