• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 15921058889

27 Jun 2025 07:46AM UTC coverage: 84.213% (+0.004%) from 84.209%
15921058889

Pull #9615

github

web-flow
Merge 9e157f09f into 89581dce1
Pull Request #9615: fix: support for multibyte folder names when the app is served from a subfolder

2 of 2 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

20798 of 24697 relevant lines covered (84.21%)

192.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.67
/system/HTTP/SiteURIFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\HTTP\Exceptions\HTTPException;
17
use CodeIgniter\Superglobals;
18
use Config\App;
19

20
/**
21
 * Creates SiteURI using superglobals.
22
 *
23
 * This class also updates superglobal $_SERVER and $_GET.
24
 *
25
 * @see \CodeIgniter\HTTP\SiteURIFactoryTest
26
 */
27
final class SiteURIFactory
28
{
29
    public function __construct(private readonly App $appConfig, private readonly Superglobals $superglobals)
30
    {
31
    }
1,015✔
32

33
    /**
34
     * Create the current URI object from superglobals.
35
     *
36
     * This method updates superglobal $_SERVER and $_GET.
37
     */
38
    public function createFromGlobals(): SiteURI
39
    {
40
        $routePath = $this->detectRoutePath();
975✔
41

42
        return $this->createURIFromRoutePath($routePath);
975✔
43
    }
44

45
    /**
46
     * Create the SiteURI object from URI string.
47
     *
48
     * @internal Used for testing purposes only.
49
     * @testTag
50
     */
51
    public function createFromString(string $uri): SiteURI
52
    {
53
        // Validate URI
54
        if (filter_var($uri, FILTER_VALIDATE_URL) === false) {
63✔
55
            throw HTTPException::forUnableToParseURI($uri);
×
56
        }
57

58
        $parts = parse_url($uri);
63✔
59

60
        if ($parts === false) {
63✔
61
            throw HTTPException::forUnableToParseURI($uri);
×
62
        }
63

64
        $query = $fragment = '';
63✔
65
        if (isset($parts['query'])) {
63✔
66
            $query = '?' . $parts['query'];
2✔
67
        }
68
        if (isset($parts['fragment'])) {
63✔
69
            $fragment = '#' . $parts['fragment'];
×
70
        }
71

72
        $relativePath = ($parts['path'] ?? '') . $query . $fragment;
63✔
73
        $host         = $this->getValidHost($parts['host']);
63✔
74

75
        return new SiteURI($this->appConfig, $relativePath, $host, $parts['scheme']);
63✔
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
     * @testTag
88
     */
89
    public function detectRoutePath(string $protocol = ''): string
90
    {
91
        if ($protocol === '') {
1,007✔
92
            $protocol = $this->appConfig->uriProtocol;
979✔
93
        }
94

95
        $routePath = match ($protocol) {
1,007✔
96
            'REQUEST_URI'  => $this->parseRequestURI(),
1,002✔
97
            'QUERY_STRING' => $this->parseQueryString(),
3✔
98
            default        => $this->superglobals->server($protocol) ?? $this->parseRequestURI(),
2✔
99
        };
1,007✔
100

101
        return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/');
1,007✔
102
    }
103

104
    /**
105
     * Will parse the REQUEST_URI and automatically detect the URI from it,
106
     * fixing the query string if necessary.
107
     *
108
     * This method updates superglobal $_SERVER and $_GET.
109
     *
110
     * @return string The route path (before normalization).
111
     */
112
    private function parseRequestURI(): string
113
    {
114
        if (
115
            $this->superglobals->server('REQUEST_URI') === null
1,003✔
116
            || $this->superglobals->server('SCRIPT_NAME') === null
1,003✔
117
        ) {
118
            return '';
893✔
119
        }
120

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

129
        // Strip the SCRIPT_NAME path from the URI
130
        if (
131
            $path !== '' && $this->superglobals->server('SCRIPT_NAME') !== ''
110✔
132
            && pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php'
110✔
133
        ) {
134
            // Compare each segment, dropping them until there is no match
135
            $segments = explode('/', rawurldecode($path));
91✔
136
            $keep     = explode('/', $path);
91✔
137

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

144
                array_shift($keep);
90✔
145
            }
146

147
            $path = implode('/', $keep);
91✔
148
        }
149

150
        // Cleanup: if indexPage is still visible in the path, remove it
151
        if ($this->appConfig->indexPage !== '' && str_starts_with($path, $this->appConfig->indexPage)) {
110✔
152
            $remainingPath = substr($path, strlen($this->appConfig->indexPage));
10✔
153
            // Only remove if followed by '/' (route) or nothing (root)
154
            if ($remainingPath === '' || str_starts_with($remainingPath, '/')) {
10✔
155
                $path = ltrim($remainingPath, '/');
10✔
156
            }
157
        }
158

159
        // This section ensures that even on servers that require the URI to
160
        // contain the query string (Nginx) a correct URI is found, and also
161
        // fixes the QUERY_STRING Server var and $_GET array.
162
        if (trim($path, '/') === '' && str_starts_with($query, '/')) {
110✔
163
            $parts    = explode('?', $query, 2);
1✔
164
            $path     = $parts[0];
1✔
165
            $newQuery = $query[1] ?? '';
1✔
166

167
            $this->superglobals->setServer('QUERY_STRING', $newQuery);
1✔
168
        } else {
169
            $this->superglobals->setServer('QUERY_STRING', $query);
109✔
170
        }
171

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

176
        return URI::removeDotSegments($path);
110✔
177
    }
178

179
    /**
180
     * Will parse QUERY_STRING and automatically detect the URI from it.
181
     *
182
     * This method updates superglobal $_SERVER and $_GET.
183
     *
184
     * @return string The route path (before normalization).
185
     */
186
    private function parseQueryString(): string
187
    {
188
        $query = $this->superglobals->server('QUERY_STRING') ?? (string) getenv('QUERY_STRING');
3✔
189

190
        if (trim($query, '/') === '') {
3✔
191
            return '/';
1✔
192
        }
193

194
        if (str_starts_with($query, '/')) {
2✔
195
            $parts    = explode('?', $query, 2);
2✔
196
            $path     = $parts[0];
2✔
197
            $newQuery = $parts[1] ?? '';
2✔
198

199
            $this->superglobals->setServer('QUERY_STRING', $newQuery);
2✔
200
        } else {
UNCOV
201
            $path = $query;
×
202
        }
203

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

208
        return URI::removeDotSegments($path);
2✔
209
    }
210

211
    /**
212
     * Create current URI object.
213
     *
214
     * @param string $routePath URI path relative to baseURL
215
     */
216
    private function createURIFromRoutePath(string $routePath): SiteURI
217
    {
218
        $query = $this->superglobals->server('QUERY_STRING') ?? '';
975✔
219

220
        $relativePath = $query !== '' ? $routePath . '?' . $query : $routePath;
975✔
221

222
        return new SiteURI($this->appConfig, $relativePath, $this->getHost());
975✔
223
    }
224

225
    /**
226
     * @return string|null The current hostname. Returns null if no valid host.
227
     */
228
    private function getHost(): ?string
229
    {
230
        $httpHostPort = $this->superglobals->server('HTTP_HOST') ?? null;
975✔
231

232
        if ($httpHostPort !== null) {
975✔
233
            [$httpHost] = explode(':', $httpHostPort, 2);
224✔
234

235
            return $this->getValidHost($httpHost);
224✔
236
        }
237

238
        return null;
751✔
239
    }
240

241
    /**
242
     * @return string|null The valid hostname. Returns null if not valid.
243
     */
244
    private function getValidHost(string $host): ?string
245
    {
246
        if (in_array($host, $this->appConfig->allowedHostnames, true)) {
287✔
247
            return $host;
5✔
248
        }
249

250
        return null;
282✔
251
    }
252
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc