Skip to content

Commit 8e08820

Browse files
authored
Merge pull request #7422 from kenjis/fix-auto-routing-improved-setTranslateURIDashes-4.4
fix: [Auto Routing Improved] one controller method has more than one URI when $translateURIDashes is true
2 parents bbc64f3 + c7b9da2 commit 8e08820

File tree

5 files changed

+239
-18
lines changed

5 files changed

+239
-18
lines changed

system/Router/AutoRouterImproved.php

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,29 @@ final class AutoRouterImproved implements AutoRouterInterface
7070
*/
7171
private string $defaultMethod;
7272

73+
/**
74+
* The URI segments.
75+
*/
76+
private array $segments = [];
77+
78+
/**
79+
* The position of the Controller in the URI segments.
80+
* Null for the default controller.
81+
*/
82+
private ?int $controllerPos = null;
83+
84+
/**
85+
* The position of the Method in the URI segments.
86+
* Null for the default method.
87+
*/
88+
private ?int $methodPos = null;
89+
90+
/**
91+
* The position of the first Parameter in the URI segments.
92+
* Null for the no parameters.
93+
*/
94+
private ?int $paramPos = null;
95+
7396
/**
7497
* @param class-string[] $protectedControllers
7598
* @param string $defaultController Short classname
@@ -108,17 +131,21 @@ private function createSegments(string $uri)
108131
* If there is a controller corresponding to the first segment, the search
109132
* ends there. The remaining segments are parameters to the controller.
110133
*
111-
* @param array $segments URI segments
112-
*
113134
* @return bool true if a controller class is found.
114135
*/
115-
private function searchFirstController(array $segments): bool
136+
private function searchFirstController(): bool
116137
{
138+
$segments = $this->segments;
139+
117140
$controller = '\\' . $this->namespace;
118141

142+
$controllerPos = -1;
143+
119144
while ($segments !== []) {
120145
$segment = array_shift($segments);
121-
$class = $this->translateURIDashes(ucfirst($segment));
146+
$controllerPos++;
147+
148+
$class = $this->translateURIDashes(ucfirst($segment));
122149

123150
// as soon as we encounter any segment that is not PSR-4 compliant, stop searching
124151
if (! $this->isValidSegment($class)) {
@@ -128,9 +155,14 @@ private function searchFirstController(array $segments): bool
128155
$controller .= '\\' . $class;
129156

130157
if (class_exists($controller)) {
131-
$this->controller = $controller;
158+
$this->controller = $controller;
159+
$this->controllerPos = $controllerPos;
160+
132161
// The first item may be a method name.
133162
$this->params = $segments;
163+
if ($segments !== []) {
164+
$this->paramPos = $this->controllerPos + 1;
165+
}
134166

135167
return true;
136168
}
@@ -142,15 +174,21 @@ private function searchFirstController(array $segments): bool
142174
/**
143175
* Search for the last default controller corresponding to the URI segments.
144176
*
145-
* @param array $segments URI segments
146-
*
147177
* @return bool true if a controller class is found.
148178
*/
149-
private function searchLastDefaultController(array $segments): bool
179+
private function searchLastDefaultController(): bool
150180
{
151-
$params = [];
181+
$segments = $this->segments;
182+
183+
$segmentCount = count($this->segments);
184+
$paramPos = null;
185+
$params = [];
152186

153187
while ($segments !== []) {
188+
if ($segmentCount > count($segments)) {
189+
$paramPos = count($segments);
190+
}
191+
154192
$namespaces = array_map(
155193
fn ($segment) => $this->translateURIDashes(ucfirst($segment)),
156194
$segments
@@ -164,6 +202,10 @@ private function searchLastDefaultController(array $segments): bool
164202
$this->controller = $controller;
165203
$this->params = $params;
166204

205+
if ($params !== []) {
206+
$this->paramPos = $paramPos;
207+
}
208+
167209
return true;
168210
}
169211

@@ -179,6 +221,10 @@ private function searchLastDefaultController(array $segments): bool
179221
$this->controller = $controller;
180222
$this->params = $params;
181223

224+
if ($params !== []) {
225+
$this->paramPos = 0;
226+
}
227+
182228
return true;
183229
}
184230

@@ -200,19 +246,19 @@ public function getRoute(string $uri, string $httpVerb): array
200246
$defaultMethod = $httpVerb . ucfirst($this->defaultMethod);
201247
$this->method = $defaultMethod;
202248

203-
$segments = $this->createSegments($uri);
249+
$this->segments = $this->createSegments($uri);
204250

205251
// Check for Module Routes.
206252
if (
207-
$segments !== []
253+
$this->segments !== []
208254
&& ($routingConfig = config(Routing::class))
209-
&& array_key_exists($segments[0], $routingConfig->moduleRoutes)
255+
&& array_key_exists($this->segments[0], $routingConfig->moduleRoutes)
210256
) {
211-
$uriSegment = array_shift($segments);
257+
$uriSegment = array_shift($this->segments);
212258
$this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\');
213259
}
214260

215-
if ($this->searchFirstController($segments)) {
261+
if ($this->searchFirstController()) {
216262
// Controller is found.
217263
$baseControllerName = class_basename($this->controller);
218264

@@ -224,14 +270,15 @@ public function getRoute(string $uri, string $httpVerb): array
224270
'Cannot access the default controller "' . $this->controller . '" with the controller name URI path.'
225271
);
226272
}
227-
} elseif ($this->searchLastDefaultController($segments)) {
273+
} elseif ($this->searchLastDefaultController()) {
228274
// The default Controller is found.
229275
$baseControllerName = class_basename($this->controller);
230276
} else {
231277
// No Controller is found.
232278
throw new PageNotFoundException('No controller is found for: ' . $uri);
233279
}
234280

281+
// The first item may be a method name.
235282
$params = $this->params;
236283

237284
$methodParam = array_shift($params);
@@ -246,6 +293,15 @@ public function getRoute(string $uri, string $httpVerb): array
246293
$this->method = $method;
247294
$this->params = $params;
248295

296+
// Update the positions.
297+
$this->methodPos = $this->paramPos;
298+
if ($params === []) {
299+
$this->paramPos = null;
300+
}
301+
if ($this->paramPos !== null) {
302+
$this->paramPos++;
303+
}
304+
249305
// Prevent access to default controller's method
250306
if (strtolower($baseControllerName) === strtolower($this->defaultController)) {
251307
throw new PageNotFoundException(
@@ -273,6 +329,10 @@ public function getRoute(string $uri, string $httpVerb): array
273329
// Ensure the controller does not have _remap() method.
274330
$this->checkRemap();
275331

332+
// Ensure the URI segments for the controller and method do not contain
333+
// underscores when $translateURIDashes is true.
334+
$this->checkUnderscore($uri);
335+
276336
// Check parameter count
277337
try {
278338
$this->checkParameters($uri);
@@ -285,6 +345,20 @@ public function getRoute(string $uri, string $httpVerb): array
285345
return [$this->directory, $this->controller, $this->method, $this->params];
286346
}
287347

348+
/**
349+
* @internal For test purpose only.
350+
*
351+
* @return array<string, int|null>
352+
*/
353+
public function getPos(): array
354+
{
355+
return [
356+
'controller' => $this->controllerPos,
357+
'method' => $this->methodPos,
358+
'params' => $this->paramPos,
359+
];
360+
}
361+
288362
/**
289363
* Get the directory path from the controller and set it to the property.
290364
*
@@ -368,6 +442,28 @@ private function checkRemap(): void
368442
}
369443
}
370444

445+
private function checkUnderscore(string $uri): void
446+
{
447+
if ($this->translateURIDashes === false) {
448+
return;
449+
}
450+
451+
$paramPos = $this->paramPos ?? count($this->segments);
452+
453+
for ($i = 0; $i < $paramPos; $i++) {
454+
if (strpos($this->segments[$i], '_') !== false) {
455+
throw new PageNotFoundException(
456+
'AutoRouterImproved prohibits access to the URI'
457+
. ' containing underscores ("' . $this->segments[$i] . '")'
458+
. ' when $translateURIDashes is enabled.'
459+
. ' Please use the dash.'
460+
. ' Handler:' . $this->controller . '::' . $this->method
461+
. ', URI:' . $uri
462+
);
463+
}
464+
}
465+
}
466+
371467
/**
372468
* Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
373469
*
@@ -378,10 +474,10 @@ private function isValidSegment(string $segment): bool
378474
return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
379475
}
380476

381-
private function translateURIDashes(string $classname): string
477+
private function translateURIDashes(string $segment): string
382478
{
383479
return $this->translateURIDashes
384-
? str_replace('-', '_', $classname)
385-
: $classname;
480+
? str_replace('-', '_', $segment)
481+
: $segment;
386482
}
387483
}

0 commit comments

Comments
 (0)