Skip to content

Commit 09f62eb

Browse files
authored
Merge pull request #7252 from kenjis/feat-SiteURL
feat: add SiteURI class
2 parents 247d05e + e529291 commit 09f62eb

File tree

6 files changed

+976
-1
lines changed

6 files changed

+976
-1
lines changed

.github/workflows/test-phpcpd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ jobs:
5555
--exclude system/Database/OCI8/Builder.php
5656
--exclude system/Database/Postgre/Builder.php
5757
--exclude system/Debug/Exceptions.php
58+
--exclude system/HTTP/SiteURI.php
5859
-- app/ public/ system/

system/HTTP/SiteURI.php

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
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 BadMethodCallException;
15+
use CodeIgniter\Exceptions\ConfigException;
16+
use CodeIgniter\HTTP\Exceptions\HTTPException;
17+
use Config\App;
18+
19+
/**
20+
* URI for the application site
21+
*/
22+
class SiteURI extends URI
23+
{
24+
/**
25+
* The current baseURL.
26+
*/
27+
private URI $baseURL;
28+
29+
/**
30+
* The path part of baseURL.
31+
*
32+
* The baseURL "http://example.com/" → '/'
33+
* The baseURL "http://localhost:8888/ci431/public/" → '/ci431/public/'
34+
*/
35+
private string $basePathWithoutIndexPage;
36+
37+
/**
38+
* The Index File.
39+
*/
40+
private string $indexPage;
41+
42+
/**
43+
* List of URI segments in baseURL and indexPage.
44+
*
45+
* If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
46+
* and the baseURL is "http://localhost:8888/ci431/public/", then:
47+
* $baseSegments = [
48+
* 0 => 'ci431',
49+
* 1 => 'public',
50+
* 2 => 'index.php',
51+
* ];
52+
*/
53+
private array $baseSegments;
54+
55+
/**
56+
* List of URI segments after indexPage.
57+
*
58+
* The word "URI Segments" originally means only the URI path part relative
59+
* to the baseURL.
60+
*
61+
* If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
62+
* and the baseURL is "http://localhost:8888/ci431/public/", then:
63+
* $segments = [
64+
* 0 => 'test',
65+
* ];
66+
*
67+
* @var array
68+
*
69+
* @deprecated This property will be private.
70+
*/
71+
protected $segments;
72+
73+
/**
74+
* URI path relative to baseURL.
75+
*
76+
* If the baseURL contains sub folders, this value will be different from
77+
* the current URI path.
78+
*
79+
* This value never starts with '/'.
80+
*/
81+
private string $routePath;
82+
83+
/**
84+
* @param string $relativePath URI path relative to baseURL. May include
85+
* queries or fragments.
86+
* @param string|null $host Optional current hostname.
87+
* @param string|null $scheme Optional scheme. 'http' or 'https'.
88+
* @phpstan-param 'http'|'https'|null $scheme
89+
*/
90+
public function __construct(
91+
App $configApp,
92+
string $relativePath = '',
93+
?string $host = null,
94+
?string $scheme = null
95+
) {
96+
$this->indexPage = $configApp->indexPage;
97+
98+
$this->baseURL = $this->determineBaseURL($configApp, $host, $scheme);
99+
100+
$this->setBasePath();
101+
102+
// Fix routePath, query, fragment
103+
[$routePath, $query, $fragment] = $this->parseRelativePath($relativePath);
104+
105+
// Fix indexPage and routePath
106+
$indexPageRoutePath = $this->getIndexPageRoutePath($routePath);
107+
108+
// Fix the current URI
109+
$uri = $this->baseURL . $indexPageRoutePath;
110+
111+
// applyParts
112+
$parts = parse_url($uri);
113+
if ($parts === false) {
114+
throw HTTPException::forUnableToParseURI($uri);
115+
}
116+
$parts['query'] = $query;
117+
$parts['fragment'] = $fragment;
118+
$this->applyParts($parts);
119+
120+
$this->setRoutePath($routePath);
121+
}
122+
123+
private function parseRelativePath(string $relativePath): array
124+
{
125+
$parts = parse_url('http://dummy/' . $relativePath);
126+
if ($parts === false) {
127+
throw HTTPException::forUnableToParseURI($relativePath);
128+
}
129+
130+
$routePath = $relativePath === '/' ? '/' : ltrim($parts['path'], '/');
131+
132+
$query = $parts['query'] ?? '';
133+
$fragment = $parts['fragment'] ?? '';
134+
135+
return [$routePath, $query, $fragment];
136+
}
137+
138+
private function determineBaseURL(
139+
App $configApp,
140+
?string $host,
141+
?string $scheme
142+
): URI {
143+
$baseURL = $this->normalizeBaseURL($configApp);
144+
145+
$uri = new URI($baseURL);
146+
147+
// Update scheme
148+
if ($scheme !== null) {
149+
$uri->setScheme($scheme);
150+
} elseif ($configApp->forceGlobalSecureRequests) {
151+
$uri->setScheme('https');
152+
}
153+
154+
// Update host
155+
if ($host !== null) {
156+
$uri->setHost($host);
157+
}
158+
159+
return $uri;
160+
}
161+
162+
private function getIndexPageRoutePath(string $routePath): string
163+
{
164+
// Remove starting slash unless it is `/`.
165+
if ($routePath !== '' && $routePath[0] === '/' && $routePath !== '/') {
166+
$routePath = ltrim($routePath, '/');
167+
}
168+
169+
// Check for an index page
170+
$indexPage = '';
171+
if ($this->indexPage !== '') {
172+
$indexPage = $this->indexPage;
173+
174+
// Check if we need a separator
175+
if ($routePath !== '' && $routePath[0] !== '/' && $routePath[0] !== '?') {
176+
$indexPage .= '/';
177+
}
178+
}
179+
180+
$indexPageRoutePath = $indexPage . $routePath;
181+
182+
if ($indexPageRoutePath === '/') {
183+
$indexPageRoutePath = '';
184+
}
185+
186+
return $indexPageRoutePath;
187+
}
188+
189+
private function normalizeBaseURL(App $configApp): string
190+
{
191+
// It's possible the user forgot a trailing slash on their
192+
// baseURL, so let's help them out.
193+
$baseURL = rtrim($configApp->baseURL, '/ ') . '/';
194+
195+
// Validate baseURL
196+
if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) {
197+
throw new ConfigException(
198+
'Config\App::$baseURL is invalid.'
199+
);
200+
}
201+
202+
return $baseURL;
203+
}
204+
205+
/**
206+
* Sets basePathWithoutIndexPage and baseSegments.
207+
*/
208+
private function setBasePath(): void
209+
{
210+
$this->basePathWithoutIndexPage = $this->baseURL->getPath();
211+
212+
$this->baseSegments = $this->convertToSegments($this->basePathWithoutIndexPage);
213+
214+
if ($this->indexPage) {
215+
$this->baseSegments[] = $this->indexPage;
216+
}
217+
}
218+
219+
/**
220+
* @deprecated
221+
*/
222+
public function setBaseURL(string $baseURL): void
223+
{
224+
throw new BadMethodCallException('Cannot use this method.');
225+
}
226+
227+
/**
228+
* @deprecated
229+
*/
230+
public function setURI(?string $uri = null)
231+
{
232+
throw new BadMethodCallException('Cannot use this method.');
233+
}
234+
235+
/**
236+
* Returns the baseURL.
237+
*
238+
* @interal
239+
*/
240+
public function getBaseURL(): string
241+
{
242+
return (string) $this->baseURL;
243+
}
244+
245+
/**
246+
* Returns the URI path relative to baseURL.
247+
*
248+
* @return string The Route path.
249+
*/
250+
public function getRoutePath(): string
251+
{
252+
return $this->routePath;
253+
}
254+
255+
/**
256+
* Formats the URI as a string.
257+
*/
258+
public function __toString(): string
259+
{
260+
return static::createURIString(
261+
$this->getScheme(),
262+
$this->getAuthority(),
263+
$this->getPath(),
264+
$this->getQuery(),
265+
$this->getFragment()
266+
);
267+
}
268+
269+
/**
270+
* Sets the route path (and segments).
271+
*
272+
* @return $this
273+
*/
274+
public function setPath(string $path)
275+
{
276+
$this->setRoutePath($path);
277+
278+
return $this;
279+
}
280+
281+
/**
282+
* Sets the route path (and segments).
283+
*/
284+
private function setRoutePath(string $routePath): void
285+
{
286+
$routePath = $this->filterPath($routePath);
287+
288+
$indexPageRoutePath = $this->getIndexPageRoutePath($routePath);
289+
290+
$this->path = $this->basePathWithoutIndexPage . $indexPageRoutePath;
291+
292+
$this->routePath = ltrim($routePath, '/');
293+
294+
$this->segments = $this->convertToSegments($this->routePath);
295+
}
296+
297+
/**
298+
* Converts path to segments
299+
*/
300+
private function convertToSegments(string $path): array
301+
{
302+
$tempPath = trim($path, '/');
303+
304+
return ($tempPath === '') ? [] : explode('/', $tempPath);
305+
}
306+
307+
/**
308+
* Sets the path portion of the URI based on segments.
309+
*
310+
* @return $this
311+
*
312+
* @deprecated This method will be private.
313+
*/
314+
public function refreshPath()
315+
{
316+
$allSegments = array_merge($this->baseSegments, $this->segments);
317+
$this->path = '/' . $this->filterPath(implode('/', $allSegments));
318+
319+
if ($this->routePath === '/' && $this->path !== '/') {
320+
$this->path .= '/';
321+
}
322+
323+
$this->routePath = $this->filterPath(implode('/', $this->segments));
324+
325+
return $this;
326+
}
327+
328+
/**
329+
* Saves our parts from a parse_url() call.
330+
*/
331+
protected function applyParts(array $parts)
332+
{
333+
if (! empty($parts['host'])) {
334+
$this->host = $parts['host'];
335+
}
336+
if (! empty($parts['user'])) {
337+
$this->user = $parts['user'];
338+
}
339+
if (isset($parts['path']) && $parts['path'] !== '') {
340+
$this->path = $this->filterPath($parts['path']);
341+
}
342+
if (! empty($parts['query'])) {
343+
$this->setQuery($parts['query']);
344+
}
345+
if (! empty($parts['fragment'])) {
346+
$this->fragment = $parts['fragment'];
347+
}
348+
349+
// Scheme
350+
if (isset($parts['scheme'])) {
351+
$this->setScheme(rtrim($parts['scheme'], ':/'));
352+
} else {
353+
$this->setScheme('http');
354+
}
355+
356+
// Port
357+
if (isset($parts['port']) && $parts['port'] !== null) {
358+
// Valid port numbers are enforced by earlier parse_url() or setPort()
359+
$this->port = $parts['port'];
360+
}
361+
362+
if (isset($parts['pass'])) {
363+
$this->password = $parts['pass'];
364+
}
365+
}
366+
}

0 commit comments

Comments
 (0)