Skip to content

Commit 988906c

Browse files
authored
Merge pull request #7256 from kenjis/feat-SiteURIFactory
feat: add SiteURIFactory
2 parents c599769 + 84b4a31 commit 988906c

File tree

6 files changed

+676
-0
lines changed

6 files changed

+676
-0
lines changed

system/HTTP/IncomingRequest.php

Lines changed: 9 additions & 0 deletions
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 4.4.0 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 4.4.0 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 4.4.0 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 4.4.0 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

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

system/Superglobals.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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;
13+
14+
/**
15+
* Superglobals manipulation.
16+
*
17+
* @internal
18+
*/
19+
final class Superglobals
20+
{
21+
private array $server;
22+
private array $get;
23+
24+
public function __construct(?array $server = null, ?array $get = null)
25+
{
26+
$this->server = $server ?? $_SERVER;
27+
$this->get = $get ?? $_GET;
28+
}
29+
30+
public function server(string $key): ?string
31+
{
32+
return $this->server[$key] ?? null;
33+
}
34+
35+
public function setServer(string $key, string $value): void
36+
{
37+
$this->server[$key] = $value;
38+
$_SERVER[$key] = $value;
39+
}
40+
41+
/**
42+
* @return array|string|null
43+
*/
44+
public function get(string $key)
45+
{
46+
return $this->get[$key] ?? null;
47+
}
48+
49+
public function setGet(string $key, string $value): void
50+
{
51+
$this->get[$key] = $value;
52+
$_GET[$key] = $value;
53+
}
54+
55+
public function setGetArray(array $array): void
56+
{
57+
$this->get = $array;
58+
$_GET = $array;
59+
}
60+
}

0 commit comments

Comments
 (0)