Skip to content

Commit

Permalink
Merge pull request #7256 from kenjis/feat-SiteURIFactory
Browse files Browse the repository at this point in the history
feat: add SiteURIFactory
  • Loading branch information
kenjis authored Jul 25, 2023
2 parents c599769 + 84b4a31 commit 988906c
Show file tree
Hide file tree
Showing 6 changed files with 676 additions and 0 deletions.
9 changes: 9 additions & 0 deletions system/HTTP/IncomingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ protected function detectURI(string $protocol, string $baseURL)
/**
* Detects the relative path based on
* the URIProtocol Config setting.
*
* @deprecated 4.4.0 Moved to SiteURIFactory.
*/
public function detectPath(string $protocol = ''): string
{
Expand Down Expand Up @@ -265,6 +267,8 @@ public function detectPath(string $protocol = ''): string
* fixing the query string if necessary.
*
* @return string The URI it found.
*
* @deprecated 4.4.0 Moved to SiteURIFactory.
*/
protected function parseRequestURI(): string
{
Expand Down Expand Up @@ -323,6 +327,8 @@ protected function parseRequestURI(): string
* Parse QUERY_STRING
*
* Will parse QUERY_STRING and automatically detect the URI from it.
*
* @deprecated 4.4.0 Moved to SiteURIFactory.
*/
protected function parseQueryString(): string
{
Expand Down Expand Up @@ -495,6 +501,9 @@ public function setPath(string $path, ?App $config = null)
return $this;
}

/**
* @deprecated 4.4.0 Moved to SiteURIFactory.
*/
private function determineHost(App $config, string $baseURL): string
{
$host = parse_url($baseURL, PHP_URL_HOST);
Expand Down
245 changes: 245 additions & 0 deletions system/HTTP/SiteURIFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\HTTP;

use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\Superglobals;
use Config\App;

final class SiteURIFactory
{
private App $appConfig;
private Superglobals $superglobals;

public function __construct(App $appConfig, Superglobals $superglobals)
{
$this->appConfig = $appConfig;
$this->superglobals = $superglobals;
}

/**
* Create the current URI object from superglobals.
*
* This method updates superglobal $_SERVER and $_GET.
*/
public function createFromGlobals(): SiteURI
{
$routePath = $this->detectRoutePath();

return $this->createURIFromRoutePath($routePath);
}

/**
* Create the SiteURI object from URI string.
*
* @internal Used for testing purposes only.
*/
public function createFromString(string $uri): SiteURI
{
// Validate URI
if (filter_var($uri, FILTER_VALIDATE_URL) === false) {
throw HTTPException::forUnableToParseURI($uri);
}

$parts = parse_url($uri);

if ($parts === false) {
throw HTTPException::forUnableToParseURI($uri);
}

$query = $fragment = '';
if (isset($parts['query'])) {
$query = '?' . $parts['query'];
}
if (isset($parts['fragment'])) {
$fragment = '#' . $parts['fragment'];
}

$relativePath = $parts['path'] . $query . $fragment;
$host = $this->getValidHost($parts['host']);

return new SiteURI($this->appConfig, $relativePath, $host, $parts['scheme']);
}

/**
* Detects the current URI path relative to baseURL based on the URIProtocol
* Config setting.
*
* @param string $protocol URIProtocol
*
* @return string The route path
*
* @internal Used for testing purposes only.
*/
public function detectRoutePath(string $protocol = ''): string
{
if ($protocol === '') {
$protocol = $this->appConfig->uriProtocol;
}

switch ($protocol) {
case 'REQUEST_URI':
$routePath = $this->parseRequestURI();
break;

case 'QUERY_STRING':
$routePath = $this->parseQueryString();
break;

case 'PATH_INFO':
default:
$routePath = $this->superglobals->server($protocol) ?? $this->parseRequestURI();
break;
}

return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/');
}

/**
* Will parse the REQUEST_URI and automatically detect the URI from it,
* fixing the query string if necessary.
*
* This method updates superglobal $_SERVER and $_GET.
*
* @return string The route path (before normalization).
*/
private function parseRequestURI(): string
{
if (
$this->superglobals->server('REQUEST_URI') === null
|| $this->superglobals->server('SCRIPT_NAME') === null
) {
return '';
}

// parse_url() returns false if no host is present, but the path or query
// string contains a colon followed by a number. So we attach a dummy
// host since REQUEST_URI does not include the host. This allows us to
// parse out the query string and path.
$parts = parse_url('http://dummy' . $this->superglobals->server('REQUEST_URI'));
$query = $parts['query'] ?? '';
$path = $parts['path'] ?? '';

// Strip the SCRIPT_NAME path from the URI
if (
$path !== '' && $this->superglobals->server('SCRIPT_NAME') !== ''
&& pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php'
) {
// Compare each segment, dropping them until there is no match
$segments = $keep = explode('/', $path);

foreach (explode('/', $this->superglobals->server('SCRIPT_NAME')) as $i => $segment) {
// If these segments are not the same then we're done
if (! isset($segments[$i]) || $segment !== $segments[$i]) {
break;
}

array_shift($keep);
}

$path = implode('/', $keep);
}

// 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 Server var and $_GET array.
if (trim($path, '/') === '' && strncmp($query, '/', 1) === 0) {
$parts = explode('?', $query, 2);
$path = $parts[0];
$newQuery = $query[1] ?? '';

$this->superglobals->setServer('QUERY_STRING', $newQuery);
} else {
$this->superglobals->setServer('QUERY_STRING', $query);
}

// Update our global GET for values likely to have been changed
parse_str($this->superglobals->server('QUERY_STRING'), $get);
$this->superglobals->setGetArray($get);

return URI::removeDotSegments($path);
}

/**
* Will parse QUERY_STRING and automatically detect the URI from it.
*
* This method updates superglobal $_SERVER and $_GET.
*
* @return string The route path (before normalization).
*/
private function parseQueryString(): string
{
$query = $this->superglobals->server('QUERY_STRING') ?? (string) getenv('QUERY_STRING');

if (trim($query, '/') === '') {
return '/';
}

if (strncmp($query, '/', 1) === 0) {
$parts = explode('?', $query, 2);
$path = $parts[0];
$newQuery = $parts[1] ?? '';

$this->superglobals->setServer('QUERY_STRING', $newQuery);
} else {
$path = $query;
}

// Update our global GET for values likely to have been changed
parse_str($this->superglobals->server('QUERY_STRING'), $get);
$this->superglobals->setGetArray($get);

return URI::removeDotSegments($path);
}

/**
* Create current URI object.
*
* @param string $routePath URI path relative to baseURL
*/
private function createURIFromRoutePath(string $routePath): SiteURI
{
$query = $this->superglobals->server('QUERY_STRING') ?? '';

$relativePath = $query !== '' ? $routePath . '?' . $query : $routePath;

return new SiteURI($this->appConfig, $relativePath, $this->getHost());
}

/**
* @return string|null The current hostname. Returns null if no valid host.
*/
private function getHost(): ?string
{
$httpHostPort = $this->superglobals->server('HTTP_HOST') ?? null;

if ($httpHostPort !== null) {
[$httpHost] = explode(':', $httpHostPort, 2);

return $this->getValidHost($httpHost);
}

return null;
}

/**
* @return string|null The valid hostname. Returns null if not valid.
*/
private function getValidHost(string $host): ?string
{
if (in_array($host, $this->appConfig->allowedHostnames, true)) {
return $host;
}

return null;
}
}
60 changes: 60 additions & 0 deletions system/Superglobals.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter;

/**
* Superglobals manipulation.
*
* @internal
*/
final class Superglobals
{
private array $server;
private array $get;

public function __construct(?array $server = null, ?array $get = null)
{
$this->server = $server ?? $_SERVER;
$this->get = $get ?? $_GET;
}

public function server(string $key): ?string
{
return $this->server[$key] ?? null;
}

public function setServer(string $key, string $value): void
{
$this->server[$key] = $value;
$_SERVER[$key] = $value;
}

/**
* @return array|string|null
*/
public function get(string $key)
{
return $this->get[$key] ?? null;
}

public function setGet(string $key, string $value): void
{
$this->get[$key] = $value;
$_GET[$key] = $value;
}

public function setGetArray(array $array): void
{
$this->get = $array;
$_GET = $array;
}
}
Loading

0 comments on commit 988906c

Please sign in to comment.