Skip to content

Commit 157d9cb

Browse files
committed
feat: add SiteURIFactory
1 parent 7f7988c commit 157d9cb

File tree

4 files changed

+578
-0
lines changed

4 files changed

+578
-0
lines changed

system/HTTP/IncomingRequest.php

+9
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ protected function detectURI(string $protocol, string $baseURL)
235235
/**
236236
* Detects the relative path based on
237237
* the URIProtocol Config setting.
238+
*
239+
* @deprecated Moved to SiteURIFactory.
238240
*/
239241
public function detectPath(string $protocol = ''): string
240242
{
@@ -265,6 +267,8 @@ public function detectPath(string $protocol = ''): string
265267
* fixing the query string if necessary.
266268
*
267269
* @return string The URI it found.
270+
*
271+
* @deprecated Moved to SiteURIFactory.
268272
*/
269273
protected function parseRequestURI(): string
270274
{
@@ -323,6 +327,8 @@ protected function parseRequestURI(): string
323327
* Parse QUERY_STRING
324328
*
325329
* Will parse QUERY_STRING and automatically detect the URI from it.
330+
*
331+
* @deprecated Moved to SiteURIFactory.
326332
*/
327333
protected function parseQueryString(): string
328334
{
@@ -495,6 +501,9 @@ public function setPath(string $path, ?App $config = null)
495501
return $this;
496502
}
497503

504+
/**
505+
* @deprecated Moved to SiteURIFactory.
506+
*/
498507
private function determineHost(App $config, string $baseURL): string
499508
{
500509
$host = parse_url($baseURL, PHP_URL_HOST);

system/HTTP/SiteURIFactory.php

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\HTTP;
13+
14+
use CodeIgniter\HTTP\Exceptions\HTTPException;
15+
use Config\App;
16+
17+
class SiteURIFactory
18+
{
19+
/**
20+
* @var array Superglobal SERVER array
21+
*/
22+
private array $server;
23+
24+
private App $appConfig;
25+
26+
/**
27+
* @param array $server Superglobal $_SERVER array
28+
*/
29+
public function __construct(array $server, App $appConfig)
30+
{
31+
$this->server = $server;
32+
$this->appConfig = $appConfig;
33+
}
34+
35+
/**
36+
* Create the current URI object from superglobals.
37+
*
38+
* This method updates superglobal $_SERVER and $_GET.
39+
*/
40+
public function createFromGlobals(): SiteURI
41+
{
42+
$routePath = $this->detectRoutePath();
43+
44+
return $this->createURIFromRoutePath($routePath);
45+
}
46+
47+
/**
48+
* Create the SiteURI object from URI string.
49+
*
50+
* @internal Used for testing purposes only.
51+
*/
52+
public function createFromString(string $uri): SiteURI
53+
{
54+
// Validate URI
55+
if (filter_var($uri, FILTER_VALIDATE_URL) === false) {
56+
throw HTTPException::forUnableToParseURI($uri);
57+
}
58+
59+
$parts = parse_url($uri);
60+
61+
if ($parts === false) {
62+
throw HTTPException::forUnableToParseURI($uri);
63+
}
64+
65+
$query = $fragment = '';
66+
if (isset($parts['query'])) {
67+
$query = '?' . $parts['query'];
68+
}
69+
if (isset($parts['fragment'])) {
70+
$fragment = '#' . $parts['fragment'];
71+
}
72+
73+
$relativePath = $parts['path'] . $query . $fragment;
74+
75+
return new SiteURI($this->appConfig, $relativePath, $parts['host'], $parts['scheme']);
76+
}
77+
78+
/**
79+
* Detects the current URI path relative to baseURL based on the URIProtocol
80+
* Config setting.
81+
*
82+
* @param string $protocol URIProtocol
83+
*
84+
* @return string The route path
85+
*
86+
* @internal Used for testing purposes only.
87+
*/
88+
public function detectRoutePath(string $protocol = ''): string
89+
{
90+
if ($protocol === '') {
91+
$protocol = $this->appConfig->uriProtocol;
92+
}
93+
94+
switch ($protocol) {
95+
case 'REQUEST_URI':
96+
$routePath = $this->parseRequestURI();
97+
break;
98+
99+
case 'QUERY_STRING':
100+
$routePath = $this->parseQueryString();
101+
break;
102+
103+
case 'PATH_INFO':
104+
default:
105+
$routePath = $this->server[$protocol] ?? $this->parseRequestURI();
106+
break;
107+
}
108+
109+
return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/');
110+
}
111+
112+
/**
113+
* Will parse the REQUEST_URI and automatically detect the URI from it,
114+
* fixing the query string if necessary.
115+
*
116+
* This method updates superglobal $_SERVER and $_GET.
117+
*
118+
* @return string The route path (before normalization).
119+
*/
120+
private function parseRequestURI(): string
121+
{
122+
if (! isset($this->server['REQUEST_URI'], $this->server['SCRIPT_NAME'])) {
123+
return '';
124+
}
125+
126+
// parse_url() returns false if no host is present, but the path or query
127+
// string contains a colon followed by a number. So we attach a dummy
128+
// host since REQUEST_URI does not include the host. This allows us to
129+
// parse out the query string and path.
130+
$parts = parse_url('http://dummy' . $this->server['REQUEST_URI']);
131+
$query = $parts['query'] ?? '';
132+
$path = $parts['path'] ?? '';
133+
134+
// Strip the SCRIPT_NAME path from the URI
135+
if (
136+
$path !== '' && isset($this->server['SCRIPT_NAME'][0])
137+
&& pathinfo($this->server['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php'
138+
) {
139+
// Compare each segment, dropping them until there is no match
140+
$segments = $keep = explode('/', $path);
141+
142+
foreach (explode('/', $this->server['SCRIPT_NAME']) as $i => $segment) {
143+
// If these segments are not the same then we're done
144+
if (! isset($segments[$i]) || $segment !== $segments[$i]) {
145+
break;
146+
}
147+
148+
array_shift($keep);
149+
}
150+
151+
$path = implode('/', $keep);
152+
}
153+
154+
// This section ensures that even on servers that require the URI to
155+
// contain the query string (Nginx) a correct URI is found, and also
156+
// fixes the QUERY_STRING Server var and $_GET array.
157+
if (trim($path, '/') === '' && strncmp($query, '/', 1) === 0) {
158+
$parts = explode('?', $query, 2);
159+
$path = $parts[0];
160+
$newQuery = $query[1] ?? '';
161+
162+
$this->server['QUERY_STRING'] = $newQuery;
163+
$this->updateServer('QUERY_STRING', $newQuery);
164+
} else {
165+
$this->server['QUERY_STRING'] = $query;
166+
$this->updateServer('QUERY_STRING', $query);
167+
}
168+
169+
// Update our global GET for values likely to have been changed
170+
parse_str($this->server['QUERY_STRING'], $get);
171+
$this->updateGetArray($get);
172+
173+
return URI::removeDotSegments($path);
174+
}
175+
176+
private function updateServer(string $key, string $value): void
177+
{
178+
$_SERVER[$key] = $value;
179+
}
180+
181+
private function updateGetArray(array $array): void
182+
{
183+
$_GET = $array;
184+
}
185+
186+
/**
187+
* Will parse QUERY_STRING and automatically detect the URI from it.
188+
*
189+
* This method updates superglobal $_SERVER and $_GET.
190+
*
191+
* @return string The route path (before normalization).
192+
*/
193+
private function parseQueryString(): string
194+
{
195+
$query = $this->server['QUERY_STRING'] ?? @getenv('QUERY_STRING');
196+
197+
if (trim($query, '/') === '') {
198+
return '/';
199+
}
200+
201+
if (strncmp($query, '/', 1) === 0) {
202+
$parts = explode('?', $query, 2);
203+
$path = $parts[0];
204+
$newQuery = $parts[1] ?? '';
205+
206+
$this->server['QUERY_STRING'] = $newQuery;
207+
$this->updateServer('QUERY_STRING', $newQuery);
208+
} else {
209+
$path = $query;
210+
}
211+
212+
// Update our global GET for values likely to have been changed
213+
parse_str($this->server['QUERY_STRING'], $get);
214+
$this->updateGetArray($get);
215+
216+
return URI::removeDotSegments($path);
217+
}
218+
219+
/**
220+
* Create current URI object.
221+
*
222+
* @param string $routePath URI path relative to baseURL
223+
*/
224+
private function createURIFromRoutePath(string $routePath): SiteURI
225+
{
226+
$query = $this->server['QUERY_STRING'] ?? '';
227+
228+
$relativePath = $query !== '' ? $routePath . '?' . $query : $routePath;
229+
230+
return new SiteURI($this->appConfig, $relativePath, $this->getHost());
231+
}
232+
233+
/**
234+
* @return string|null The current hostname. Returns null if no host header.
235+
*/
236+
private function getHost(): ?string
237+
{
238+
$host = null;
239+
240+
$httpHostPort = $this->server['HTTP_HOST'] ?? null;
241+
if ($httpHostPort !== null) {
242+
[$httpHost] = explode(':', $httpHostPort, 2);
243+
244+
if (in_array($httpHost, $this->appConfig->allowedHostnames, true)) {
245+
$host = $httpHost;
246+
}
247+
}
248+
249+
return $host;
250+
}
251+
}

0 commit comments

Comments
 (0)