forked from codeigniter4/CodeIgniter4
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAutoRouterImproved.php
More file actions
320 lines (265 loc) · 9.93 KB
/
Copy pathAutoRouterImproved.php
File metadata and controls
320 lines (265 loc) · 9.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Router;
use CodeIgniter\Exceptions\PageNotFoundException;
use ReflectionClass;
use ReflectionException;
/**
* New Secure Router for Auto-Routing
*/
final class AutoRouterImproved implements AutoRouterInterface
{
/**
* List of controllers in Defined Routes that should not be accessed via this Auto-Routing.
*
* @var class-string[]
*/
private array $protectedControllers;
/**
* Sub-directory that contains the requested controller class.
*/
private ?string $directory = null;
/**
* Sub-namespace that contains the requested controller class.
*/
private ?string $subNamespace = null;
/**
* The name of the controller class.
*/
private string $controller;
/**
* The name of the method to use.
*/
private string $method;
/**
* An array of params to the controller method.
*/
private array $params = [];
/**
* Whether dashes in URI's should be converted
* to underscores when determining method names.
*/
private bool $translateURIDashes;
/**
* HTTP verb for the request.
*/
private string $httpVerb;
/**
* The namespace for controllers.
*/
private string $namespace;
/**
* The name of the default controller class.
*/
private string $defaultController;
/**
* The name of the default method
*/
private string $defaultMethod;
/**
* @param class-string[] $protectedControllers
* @param string $defaultController Short classname
*/
public function __construct(
array $protectedControllers,
string $namespace,
string $defaultController,
string $defaultMethod,
bool $translateURIDashes,
string $httpVerb
) {
$this->protectedControllers = $protectedControllers;
$this->namespace = rtrim($namespace, '\\') . '\\';
$this->translateURIDashes = $translateURIDashes;
$this->httpVerb = $httpVerb;
$this->defaultController = $defaultController;
$this->defaultMethod = $httpVerb . ucfirst($defaultMethod);
// Set the default values
$this->controller = $this->defaultController;
$this->method = $this->defaultMethod;
}
/**
* Finds controller, method and params from the URI.
*
* @return array [directory_name, controller_name, controller_method, params]
*/
public function getRoute(string $uri): array
{
$segments = explode('/', $uri);
// WARNING: Directories get shifted out of the segments array.
$nonDirSegments = $this->scanControllers($segments);
$controllerSegment = '';
$baseControllerName = $this->defaultController;
// If we don't have any segments left - use the default controller;
// If not empty, then the first segment should be the controller
if (! empty($nonDirSegments)) {
$controllerSegment = array_shift($nonDirSegments);
$baseControllerName = $this->translateURIDashes(ucfirst($controllerSegment));
}
if (! $this->isValidSegment($baseControllerName)) {
throw new PageNotFoundException($baseControllerName . ' is not a valid controller name');
}
// Prevent access to default controller path
if (
strtolower($baseControllerName) === strtolower($this->defaultController)
&& strtolower($controllerSegment) === strtolower($this->defaultController)
) {
throw new PageNotFoundException(
'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.'
);
}
// Use the method name if it exists.
if (! empty($nonDirSegments)) {
$methodSegment = $this->translateURIDashes(array_shift($nonDirSegments));
// Prefix HTTP verb
$this->method = $this->httpVerb . ucfirst($methodSegment);
// Prevent access to default method path
if (strtolower($this->method) === strtolower($this->defaultMethod)) {
throw new PageNotFoundException(
'Cannot access the default method "' . $this->method . '" with the method name URI path.'
);
}
}
if (! empty($nonDirSegments)) {
$this->params = $nonDirSegments;
}
// Ensure the controller stores the fully-qualified class name
$this->controller = '\\' . ltrim(
str_replace(
'/',
'\\',
$this->namespace . $this->subNamespace . $baseControllerName
),
'\\'
);
// Ensure routes registered via $routes->cli() are not accessible via web.
$this->protectDefinedRoutes();
// Check _remap()
$this->checkRemap();
// Check parameters
try {
$this->checkParameters($uri);
} catch (ReflectionException $e) {
throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
}
return [$this->directory, $this->controller, $this->method, $this->params];
}
private function protectDefinedRoutes(): void
{
$controller = strtolower($this->controller);
foreach ($this->protectedControllers as $controllerInRoutes) {
$routeLowerCase = strtolower($controllerInRoutes);
if ($routeLowerCase === $controller) {
throw new PageNotFoundException(
'Cannot access the controller in Defined Routes. Controller: ' . $controllerInRoutes
);
}
}
}
private function checkParameters(string $uri): void
{
$refClass = new ReflectionClass($this->controller);
$refMethod = $refClass->getMethod($this->method);
$refParams = $refMethod->getParameters();
if (! $refMethod->isPublic()) {
throw PageNotFoundException::forMethodNotFound($this->method);
}
if (count($refParams) < count($this->params)) {
throw new PageNotFoundException(
'The param count in the URI are greater than the controller method params.'
. ' Handler:' . $this->controller . '::' . $this->method
. ', URI:' . $uri
);
}
}
private function checkRemap(): void
{
try {
$refClass = new ReflectionClass($this->controller);
$refClass->getMethod('_remap');
throw new PageNotFoundException(
'AutoRouterImproved does not support `_remap()` method.'
. ' Controller:' . $this->controller
);
} catch (ReflectionException $e) {
// Do nothing.
}
}
/**
* Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
*
* @param array $segments URI segments
*
* @return array returns an array of remaining uri segments that don't map onto a directory
*/
private function scanControllers(array $segments): array
{
$segments = array_filter($segments, static fn ($segment) => $segment !== '');
// numerically reindex the array, removing gaps
$segments = array_values($segments);
// Loop through our segments and return as soon as a controller
// is found or when such a directory doesn't exist
$c = count($segments);
while ($c-- > 0) {
$segmentConvert = $this->translateURIDashes(ucfirst($segments[0]));
// as soon as we encounter any segment that is not PSR-4 compliant, stop searching
if (! $this->isValidSegment($segmentConvert)) {
return $segments;
}
$test = $this->namespace . $this->subNamespace . $segmentConvert;
// as long as each segment is *not* a controller file, add it to $this->subNamespace
if (! class_exists($test)) {
$this->setSubNamespace($segmentConvert, true, false);
array_shift($segments);
$this->directory .= $this->directory . $segmentConvert . '/';
continue;
}
return $segments;
}
// This means that all segments were actually directories
return $segments;
}
/**
* Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
*
* regex comes from https://www.php.net/manual/en/language.variables.basics.php
*/
private function isValidSegment(string $segment): bool
{
return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
}
/**
* Sets the sub-namespace that the controller is in.
*
* @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
*/
private function setSubNamespace(?string $namespace = null, bool $append = false, bool $validate = true): void
{
if ($validate) {
$segments = explode('/', trim($namespace, '/'));
foreach ($segments as $segment) {
if (! $this->isValidSegment($segment)) {
return;
}
}
}
if ($append !== true || empty($this->subNamespace)) {
$this->subNamespace = trim($namespace, '/') . '\\';
} else {
$this->subNamespace .= trim($namespace, '/') . '\\';
}
}
private function translateURIDashes(string $classname): string
{
return $this->translateURIDashes
? str_replace('-', '_', $classname)
: $classname;
}
}