diff --git a/app/Config/App.php b/app/Config/App.php index 14afe2ee798f..4baf5c90ff2d 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -24,6 +24,20 @@ class App extends BaseConfig */ public string $baseURL = 'http://localhost:8080/'; + /** + * Allowed Hostnames in the Site URL other than the hostname in the baseURL. + * If you want to accept multiple Hostnames, set this. + * + * E.g. When your site URL ($baseURL) is 'http://example.com/', and your site + * also accepts 'http://media.example.com/' and + * 'http://accounts.example.com/': + * ['media.example.com', 'accounts.example.com'] + * + * @var string[] + * @phpstan-var list + */ + public array $allowedHostnames = []; + /** * -------------------------------------------------------------------------- * Index File diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 74afaf929d6e..9e19563ab8b0 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -192,14 +192,12 @@ public function detectLocale($config) * Sets up our URI object based on the information we have. This is * either provided by the user in the baseURL Config setting, or * determined from the environment as needed. + * + * @deprecated $protocol and $baseURL are deprecated. No longer used. */ protected function detectURI(string $protocol, string $baseURL) { - // Passing the config is unnecessary but left for legacy purposes - $config = clone $this->config; - $config->baseURL = $baseURL; - - $this->setPath($this->detectPath($protocol), $config); + $this->setPath($this->detectPath($this->config->uriProtocol), $this->config); } /** @@ -270,7 +268,7 @@ protected function parseRequestURI(): string } // This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct - // URI is found, and also fixes the QUERY_STRING getServer var and $_GET array. + // URI is found, and also fixes the QUERY_STRING Server var and $_GET array. if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) { $query = explode('?', $query, 2); $uri = $query[0]; @@ -400,19 +398,26 @@ public function setPath(string $path, ?App $config = null) // It's possible the user forgot a trailing slash on their // baseURL, so let's help them out. - $baseURL = $config->baseURL === '' ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/'; + $baseURL = ($config->baseURL === '') ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/'; - // Based on our baseURL provided by the developer - // set our current domain name, scheme + // Based on our baseURL and allowedHostnames provided by the developer + // and HTTP_HOST, set our current domain name, scheme. if ($baseURL !== '') { + $host = $this->determineHost($config, $baseURL); + + // Set URI::$baseURL + $uri = new URI($baseURL); + $currentBaseURL = (string) $uri->setHost($host); + $this->uri->setBaseURL($currentBaseURL); + $this->uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME)); - $this->uri->setHost(parse_url($baseURL, PHP_URL_HOST)); + $this->uri->setHost($host); $this->uri->setPort(parse_url($baseURL, PHP_URL_PORT)); // Ensure we have any query vars $this->uri->setQuery($_SERVER['QUERY_STRING'] ?? ''); - // Check if the baseURL scheme needs to be coerced into its secure version + // Check if the scheme needs to be coerced into its secure version if ($config->forceGlobalSecureRequests && $this->uri->getScheme() === 'http') { $this->uri->setScheme('https'); } @@ -425,6 +430,27 @@ public function setPath(string $path, ?App $config = null) return $this; } + private function determineHost(App $config, string $baseURL): string + { + $host = parse_url($baseURL, PHP_URL_HOST); + + if (empty($config->allowedHostnames)) { + return $host; + } + + // Update host if it is valid. + $httpHostPort = $this->getServer('HTTP_HOST'); + if ($httpHostPort !== null) { + [$httpHost] = explode(':', $httpHostPort, 2); + + if (in_array($httpHost, $config->allowedHostnames, true)) { + $host = $httpHost; + } + } + + return $host; + } + /** * Returns the path relative to SCRIPT_NAME, * running detection as necessary. diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 2c511b40ca08..690dd19454f7 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -11,8 +11,8 @@ namespace CodeIgniter\HTTP; +use BadMethodCallException; use CodeIgniter\HTTP\Exceptions\HTTPException; -use InvalidArgumentException; /** * Abstraction for a uniform resource identifier (URI). @@ -36,6 +36,11 @@ class URI */ protected $uriString; + /** + * The Current baseURL. + */ + private ?string $baseURL = null; + /** * List of URI segments. * @@ -83,6 +88,11 @@ class URI /** * URI path. * + * Note: The constructor of the IncomingRequest class changes the path of + * the URI object held by the IncomingRequest class to a path relative + * to the SCRIPT_NAME. If the baseURL contains subfolders, this value + * will be different from the current URI path. + * * @var string */ protected $path; @@ -232,9 +242,12 @@ public static function removeDotSegments(string $path): string /** * Constructor. * - * @param string $uri + * @param string|null $uri The URI to parse. + * + * @throws HTTPException * - * @throws InvalidArgumentException + * @TODO null for param $uri should be removed. + * See https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface */ public function __construct(?string $uri = null) { @@ -273,6 +286,8 @@ public function useRawQueryString(bool $raw = true) * Sets and overwrites any current URI information. * * @return URI + * + * @throws HTTPException */ public function setURI(?string $uri = null) { @@ -744,6 +759,30 @@ public function setPath(string $path) return $this; } + /** + * Sets the current baseURL. + * + * @interal + */ + public function setBaseURL(string $baseURL): void + { + $this->baseURL = $baseURL; + } + + /** + * Returns the current baseURL. + * + * @interal + */ + public function getBaseURL(): string + { + if ($this->baseURL === null) { + throw new BadMethodCallException('The $baseURL is not set.'); + } + + return $this->baseURL; + } + /** * Sets the path portion of the URI based on segments. * diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index 97c4c9576f8b..0c1288c1cc5b 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -9,6 +9,8 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\URI; use CodeIgniter\Router\Exceptions\RouterException; @@ -19,14 +21,15 @@ if (! function_exists('_get_uri')) { /** - * Used by the other URL functions to build a - * framework-specific URI based on the App config. + * Used by the other URL functions to build a framework-specific URI + * based on $request->getUri()->getBaseURL() and the App config. * - * @internal Outside of the framework this should not be used directly. + * @internal Outside the framework this should not be used directly. * * @param string $relativePath May include queries or fragments * - * @throws InvalidArgumentException For invalid paths or config + * @throws HTTPException For invalid paths. + * @throws InvalidArgumentException For invalid config. */ function _get_uri(string $relativePath = '', ?App $config = null): URI { @@ -37,7 +40,7 @@ function _get_uri(string $relativePath = '', ?App $config = null): URI } // If a full URI was passed then convert it - if (is_int(strpos($relativePath, '://'))) { + if (strpos($relativePath, '://') !== false) { $full = new URI($relativePath); $relativePath = URI::createURIString( null, @@ -51,7 +54,14 @@ function _get_uri(string $relativePath = '', ?App $config = null): URI $relativePath = URI::removeDotSegments($relativePath); // Build the full URL based on $config and $relativePath - $url = rtrim($config->baseURL, '/ ') . '/'; + $request = Services::request(); + + if ($request instanceof CLIRequest) { + /** @var App $config */ + $url = rtrim($config->baseURL, '/ ') . '/'; + } else { + $url = $request->getUri()->getBaseURL(); + } // Check for an index page if ($config->indexPage !== '') { diff --git a/system/Test/Mock/MockIncomingRequest.php b/system/Test/Mock/MockIncomingRequest.php index 05c9587d4170..03ab0d6c8b29 100644 --- a/system/Test/Mock/MockIncomingRequest.php +++ b/system/Test/Mock/MockIncomingRequest.php @@ -15,8 +15,4 @@ class MockIncomingRequest extends IncomingRequest { - protected function detectURI($protocol, $baseURL) - { - // Do nothing... - } } diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 6f91ad6ad93d..8d66bf666de4 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -40,6 +40,8 @@ protected function setUp(): void { parent::setUp(); + $this->resetServices(); + $_SERVER['REQUEST_METHOD'] = 'GET'; $this->config = new App(); @@ -48,7 +50,12 @@ protected function setUp(): void $this->routes = new RouteCollection(Services::locator(), new Modules()); Services::injectMock('routes', $this->routes); - $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); + $this->request = new MockIncomingRequest( + $this->config, + new URI('http://example.com'), + null, + new UserAgent() + ); Services::injectMock('request', $this->request); } diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 0b40abda5605..d0061c35211e 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -34,9 +34,9 @@ protected function setUp(): void { $this->server = $_SERVER; - Services::reset(); - parent::setUp(); + + $this->resetServices(); } protected function tearDown(): void @@ -164,6 +164,8 @@ public function testSetLink() $config->baseURL = 'http://example.com/test/'; Factories::injectMock('config', 'App', $config); + $this->resetServices(); + $response = new Response($config); $pager = Services::pager(); diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index 5f04d36bd4e2..1caafc192f46 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -58,12 +58,38 @@ protected function tearDown(): void public function testCurrentURLReturnsBasicURL() { - // Since we're on a CLI, we must provide our own URI + $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['SCRIPT_NAME'] = '/public/index.php'; + $this->config->baseURL = 'http://example.com/public'; $this->assertSame('http://example.com/public/index.php/', current_url()); } + public function testCurrentURLReturnsAllowedHostname() + { + $_SERVER['HTTP_HOST'] = 'www.example.jp'; + $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['SCRIPT_NAME'] = '/public/index.php'; + + $this->config->baseURL = 'http://example.com/public'; + $this->config->allowedHostnames = ['www.example.jp']; + + $this->assertSame('http://www.example.jp/public/index.php/', current_url()); + } + + public function testCurrentURLReturnsBaseURLIfNotAllowedHostname() + { + $_SERVER['HTTP_HOST'] = 'invalid.example.org'; + $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['SCRIPT_NAME'] = '/public/index.php'; + + $this->config->baseURL = 'http://example.com/public'; + $this->config->allowedHostnames = ['www.example.jp']; + + $this->assertSame('http://example.com/public/index.php/', current_url()); + } + public function testCurrentURLReturnsObject() { // Since we're on a CLI, we must provide our own URI diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php index 68e53e52bc10..1f6f081b4dc2 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php @@ -281,9 +281,6 @@ public function testBaseURLService() $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/ci/v4/x/y'; - $uri = new URI('http://example.com/ci/v4/x/y'); - Services::injectMock('uri', $uri); - $this->config->baseURL = 'http://example.com/ci/v4/'; $request = Services::request($this->config); Services::injectMock('request', $request); @@ -291,4 +288,42 @@ public function testBaseURLService() $this->assertSame('http://example.com/ci/v4/index.php/controller/method', site_url('controller/method', null, $this->config)); $this->assertSame('http://example.com/ci/v4/controller/method', base_url('controller/method', null)); } + + public function testSiteURLWithAllowedHostname() + { + $_SERVER['HTTP_HOST'] = 'www.example.jp'; + $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['SCRIPT_NAME'] = '/public/index.php'; + + $this->config->baseURL = 'http://example.com/public/'; + $this->config->allowedHostnames = ['www.example.jp']; + + // URI object are updated in IncomingRequest constructor. + $request = Services::incomingrequest($this->config); + Services::injectMock('request', $request); + + $this->assertSame( + 'http://www.example.jp/public/index.php/controller/method', + site_url('controller/method', null, $this->config) + ); + } + + public function testBaseURLWithAllowedHostname() + { + $_SERVER['HTTP_HOST'] = 'www.example.jp'; + $_SERVER['REQUEST_URI'] = '/public'; + $_SERVER['SCRIPT_NAME'] = '/public/index.php'; + + $this->config->baseURL = 'http://example.com/public/'; + $this->config->allowedHostnames = ['www.example.jp']; + + // URI object are updated in IncomingRequest constructor. + $request = Services::incomingrequest($this->config); + Services::injectMock('request', $request); + + $this->assertSame( + 'http://www.example.jp/public/controller/method', + base_url('controller/method', null) + ); + } } diff --git a/tests/system/Pager/PagerTest.php b/tests/system/Pager/PagerTest.php index 47476e56001f..c6461d79647e 100644 --- a/tests/system/Pager/PagerTest.php +++ b/tests/system/Pager/PagerTest.php @@ -464,6 +464,7 @@ public function testBasedURI() { $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['REQUEST_URI'] = '/ci/v4/x/y'; + $_SERVER['SCRIPT_NAME'] = '/ci/v4/index.php'; $_GET = []; $config = new App(); @@ -471,7 +472,13 @@ public function testBasedURI() $config->indexPage = 'fc.php'; Factories::injectMock('config', 'App', $config); - $request = Services::request($config); + $request = new IncomingRequest( + $config, + new URI(), + 'php://input', + new UserAgent() + ); + $request = $request->withMethod('GET'); Services::injectMock('request', $request); $this->config = new PagerConfig(); diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index 0cd0789464e8..2e98f97a52b9 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -244,6 +244,12 @@ Error Handling - To *temporarily* enable throwing of deprecations, set the environment variable ``CODEIGNITER_SCREAM_DEPRECATIONS`` to a truthy value. - ``Config\Logger::$threshold`` is now, by default, environment-specific. For production environment, default threshold is still ``4`` but changed to ``9`` for other environments. +Multiple Domain Support +======================= + +- Added ``Config\App::$allowedHostnames`` to set hostnames other than the hostname in the baseURL. +- If you set ``Config\App::$allowedHostnames``, URL-related functions such as :php:func:`base_url()`, :php:func:`current_url()`, :php:func:`site_url()` will return the URL with the hostname set in ``Config\App::$allowedHostnames`` if the current URL matches. + Others ====== diff --git a/user_guide_src/source/helpers/url_helper.rst b/user_guide_src/source/helpers/url_helper.rst index 92ae1df3cfd6..2dc337b2335f 100644 --- a/user_guide_src/source/helpers/url_helper.rst +++ b/user_guide_src/source/helpers/url_helper.rst @@ -20,14 +20,17 @@ The following functions are available: .. php:function:: site_url([$uri = ''[, $protocol = null[, $altConfig = null]]]) - :param mixed $uri: URI string or array of URI segments + :param array|string $uri: URI string or array of URI segments :param string $protocol: Protocol, e.g., 'http' or 'https' :param \\Config\\App $altConfig: Alternate configuration to use :returns: Site URL :rtype: string - Returns your site URL, as specified in your config file. The index.php - file (or whatever you have set as your site **indexPage** in your config + .. note:: Since v4.3.0, if you set ``Config\App::$allowedHostnames``, + this returns the URL with the hostname set in it if the current URL matches. + + Returns your site URL, as specified in your config file. The **index.php** + file (or whatever you have set as your site ``Config\App::$indexPage`` in your config file) will be added to the URL, as will any URI segments you pass to the function. @@ -41,7 +44,7 @@ The following functions are available: .. literalinclude:: url_helper/001.php The above example would return something like: - *http://example.com/index.php/news/local/123* + **http://example.com/index.php/news/local/123** Here is an example of segments passed as an array: @@ -53,17 +56,20 @@ The following functions are available: .. php:function:: base_url([$uri = ''[, $protocol = null]]) - :param mixed $uri: URI string or array of URI segments + :param array|string $uri: URI string or array of URI segments :param string $protocol: Protocol, e.g., 'http' or 'https' :returns: Base URL :rtype: string + .. note:: Since v4.3.0, if you set ``Config\App::$allowedHostnames``, + this returns the URL with the hostname set in it if the current URL matches. + Returns your site base URL, as specified in your config file. Example: .. literalinclude:: url_helper/003.php This function returns the same thing as :php:func:`site_url()`, without - the *indexPage* being appended. + the ``Config\App::$indexPage`` being appended. Also like :php:func:`site_url()`, you can supply segments as a string or an array. Here is a string example: @@ -71,7 +77,7 @@ The following functions are available: .. literalinclude:: url_helper/004.php The above example would return something like: - *http://example.com/blog/post/123* + **http://example.com/blog/post/123** This is useful because unlike :php:func:`site_url()`, you can supply a string to a file, such as an image or stylesheet. For example: @@ -79,7 +85,7 @@ The following functions are available: .. literalinclude:: url_helper/005.php This would give you something like: - *http://example.com/images/icons/edit.png* + **http://example.com/images/icons/edit.png** .. php:function:: current_url([$returnObject = false[, $request = null]]) @@ -89,14 +95,18 @@ The following functions are available: :rtype: string|\\CodeIgniter\\HTTP\\URI Returns the full URL (including segments) of the page being currently viewed. + However for security reasons, it is created based on the ``Config\App`` settings, and not intended to match the browser URL. + Since v4.3.0, if you set ``Config\App::$allowedHostnames``, + this returns the URL with the hostname set in it if the current URL matches. + .. note:: Calling this function is the same as doing this: .. literalinclude:: url_helper/006.php -.. important:: Prior to v4.1.2, this function had a bug causing it to ignore the configuration on ``App::$indexPage``. + .. important:: Prior to v4.1.2, this function had a bug causing it to ignore the configuration on ``Config\App::$indexPage``. .. php:function:: previous_url([$returnObject = false]) diff --git a/user_guide_src/source/installation/upgrade_430.rst b/user_guide_src/source/installation/upgrade_430.rst index faa63aed3683..63e685518bd7 100644 --- a/user_guide_src/source/installation/upgrade_430.rst +++ b/user_guide_src/source/installation/upgrade_430.rst @@ -193,6 +193,14 @@ Others Breaking Enhancements ********************* +Multiple Domain Support +======================= + +- If you set ``Config\App::$allowedHostnames``, URL-related functions such as :php:func:`base_url()`, :php:func:`current_url()`, :php:func:`site_url()` will return the URL with the hostname set in ``Config\App::$allowedHostnames`` if the current URL matches. + +Others +====== + - Since void HTML elements (e.g. ````) in ``html_helper``, ``form_helper`` or common functions have been changed to be HTML5-compatible by default and you need to be compatible with XHTML, you must set the ``$html5`` property in **app/Config/DocTypes.php** to ``false``. - Since the launch of Spark Commands was extracted from ``CodeIgniter\CodeIgniter``, there may be problems running these commands if the ``Services::codeigniter()`` service has been overridden. - The return type of ``CodeIgniter\Database\Database::loadForge()`` has been changed to ``Forge``. Extending classes should likewise change the type.