Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Config/App.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>
*/
public array $allowedHostnames = [];

/**
* --------------------------------------------------------------------------
* Index File
Expand Down
48 changes: 37 additions & 11 deletions system/HTTP/IncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}

/**
Expand DownExpand Up@@ -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];
Expand DownExpand Up@@ -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');
}
Expand All@@ -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.
Expand Down
45 changes: 42 additions & 3 deletions system/HTTP/URI.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,8 @@

namespace CodeIgniter\HTTP;

use BadMethodCallException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Abstraction for a uniform resource identifier (URI).
Expand All@@ -36,6 +36,11 @@ class URI
*/
protected $uriString;

/**
* The Current baseURL.
*/
private ?string $baseURL = null;

/**
* List of URI segments.
*
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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)
{
Expand DownExpand Up@@ -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.
*
Expand Down
22 changes: 16 additions & 6 deletions system/Helpers/url_helper.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
{
Expand All@@ -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,
Expand All@@ -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 !== '') {
Expand Down
4 changes: 0 additions & 4 deletions system/Test/Mock/MockIncomingRequest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,4 @@

class MockIncomingRequest extends IncomingRequest
{
protected function detectURI($protocol, $baseURL)
{
// Do nothing...
}
}
9 changes: 8 additions & 1 deletion tests/system/HTTP/RedirectResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,8 @@ protected function setUp(): void
{
parent::setUp();

$this->resetServices();

$_SERVER['REQUEST_METHOD'] = 'GET';

$this->config = new App();
Expand All@@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/system/HTTP/ResponseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,9 +34,9 @@ protected function setUp(): void
{
$this->server = $_SERVER;

Services::reset();

parent::setUp();

$this->resetServices();
}

protected function tearDown(): void
Expand DownExpand Up@@ -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();

Expand Down
28 changes: 27 additions & 1 deletion tests/system/Helpers/URLHelper/CurrentUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
41 changes: 38 additions & 3 deletions tests/system/Helpers/URLHelper/SiteUrlTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,14 +281,49 @@ 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);

$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)
);
}
}
Loading