Skip to content

Commit

Permalink
feat: add SiteURIFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
kenjis committed Feb 20, 2023
1 parent 7699c85 commit d68e8ac
Show file tree
Hide file tree
Showing 4 changed files with 578 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 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 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 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 Moved to SiteURIFactory.
*/
private function determineHost(App $config, string $baseURL): string
{
$host = parse_url($baseURL, PHP_URL_HOST);
Expand Down
251 changes: 251 additions & 0 deletions system/HTTP/SiteURIFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?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 Config\App;

class SiteURIFactory
{
/**
* @var array Superglobal SERVER array
*/
private array $server;

private App $appConfig;

/**
* @param array $server Superglobal $_SERVER array
*/
public function __construct(array $server, App $appConfig)
{
$this->server = $server;
$this->appConfig = $appConfig;
}

/**
* 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;

return new SiteURI($this->appConfig, $relativePath, $parts['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->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 (! isset($this->server['REQUEST_URI'], $this->server['SCRIPT_NAME'])) {
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->server['REQUEST_URI']);
$query = $parts['query'] ?? '';
$path = $parts['path'] ?? '';

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

foreach (explode('/', $this->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->server['QUERY_STRING'] = $newQuery;
$this->updateServer('QUERY_STRING', $newQuery);
} else {
$this->server['QUERY_STRING'] = $query;
$this->updateServer('QUERY_STRING', $query);
}

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

return URI::removeDotSegments($path);
}

private function updateServer(string $key, string $value): void
{
$_SERVER[$key] = $value;
}

private function updateGetArray(array $array): void
{
$_GET = $array;
}

/**
* 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->server['QUERY_STRING'] ?? @getenv('QUERY_STRING');

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

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

$this->server['QUERY_STRING'] = $newQuery;
$this->updateServer('QUERY_STRING', $newQuery);
} else {
$path = $query;
}

// Update our global GET for values likely to have been changed
parse_str($this->server['QUERY_STRING'], $get);
$this->updateGetArray($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->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 host header.
*/
private function getHost(): ?string
{
$host = null;

$httpHostPort = $this->server['HTTP_HOST'] ?? null;
if ($httpHostPort !== null) {
[$httpHost] = explode(':', $httpHostPort, 2);

if (in_array($httpHost, $this->appConfig->allowedHostnames, true)) {
$host = $httpHost;
}
}

return $host;
}
}
Loading

0 comments on commit d68e8ac

Please sign in to comment.