Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
116 / 116 |
|
91.85% |
124 / 135 |
|
20.91% |
46 / 220 |
|
50.00% |
8 / 16 |
CRAP | |
0.00% |
0 / 1 |
| Router | |
100.00% |
116 / 116 |
|
91.85% |
124 / 135 |
|
20.91% |
46 / 220 |
|
100.00% |
16 / 16 |
1607.51 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
85.71% |
6 / 7 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
2.50 | |||
| addRoute | |
100.00% |
12 / 12 |
|
100.00% |
10 / 10 |
|
27.78% |
5 / 18 |
|
100.00% |
1 / 1 |
14.42 | |||
| group | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addStatic | |
100.00% |
15 / 15 |
|
90.91% |
10 / 11 |
|
62.50% |
5 / 8 |
|
100.00% |
1 / 1 |
4.84 | |||
| asset | |
100.00% |
10 / 10 |
|
91.67% |
11 / 12 |
|
55.56% |
5 / 9 |
|
100.00% |
1 / 1 |
7.19 | |||
| url | |
100.00% |
8 / 8 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| match | |
100.00% |
19 / 19 |
|
100.00% |
23 / 23 |
|
2.48% |
3 / 121 |
|
100.00% |
1 / 1 |
102.74 | |||
| applyGlobalPrefix | |
100.00% |
3 / 3 |
|
83.33% |
5 / 6 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
3.33 | |||
| prependHost | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| queryString | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| normalizeQuery | |
100.00% |
16 / 16 |
|
100.00% |
10 / 10 |
|
66.67% |
4 / 6 |
|
100.00% |
1 / 1 |
5.93 | |||
| queryValue | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| getCacheBuster | |
100.00% |
9 / 9 |
|
84.62% |
11 / 13 |
|
30.77% |
4 / 13 |
|
100.00% |
1 / 1 |
13.30 | |||
| splitStaticPath | |
100.00% |
4 / 4 |
|
77.78% |
7 / 9 |
|
33.33% |
2 / 6 |
|
100.00% |
1 / 1 |
3.19 | |||
| assertSafeStaticPath | |
100.00% |
6 / 6 |
|
85.71% |
12 / 14 |
|
15.00% |
3 / 20 |
|
100.00% |
1 / 1 |
13.83 | |||
| isInsideDirectory | |
100.00% |
1 / 1 |
|
75.00% |
3 / 4 |
|
50.00% |
1 / 2 |
|
100.00% |
1 / 1 |
1.12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Router; |
| 6 | |
| 7 | use Celemas\Router\Exception\InvalidArgumentException; |
| 8 | use Celemas\Router\Exception\MethodNotAllowedException; |
| 9 | use Celemas\Router\Exception\NotFoundException; |
| 10 | use Celemas\Router\Exception\RuntimeException; |
| 11 | use Closure; |
| 12 | use Override; |
| 13 | use Psr\Http\Message\ServerRequestInterface as Request; |
| 14 | use Stringable; |
| 15 | |
| 16 | /** @psalm-api */ |
| 17 | class Router implements RouteAdder |
| 18 | { |
| 19 | use AddsRoutes; |
| 20 | |
| 21 | protected readonly string $globalPrefix; |
| 22 | |
| 23 | public function __construct(string $globalPrefix = '') |
| 24 | { |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 27 | } |
| 28 | |
| 29 | protected const string ANY = 'ANY'; |
| 30 | |
| 31 | /** @var array<string, list<Route>> */ |
| 32 | protected array $routes = []; |
| 33 | |
| 34 | /** @var array<string, StaticRoute> */ |
| 35 | protected array $staticRoutes = []; |
| 36 | |
| 37 | /** @var array<string, Route> */ |
| 38 | protected array $names = []; |
| 39 | |
| 40 | #[Override] |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 58 | } |
| 59 | |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 64 | } |
| 65 | |
| 66 | #[Override] |
| 67 | public function group( |
| 68 | string $patternPrefix, |
| 69 | Closure $createClosure, |
| 70 | string $namePrefix = '', |
| 71 | ): void { |
| 72 | $group = Group::make($patternPrefix, $createClosure, $namePrefix); |
| 73 | $group->register($this); |
| 74 | } |
| 75 | |
| 76 | public function addStatic( |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 82 | $name = $prefix; |
| 83 | } |
| 84 | |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 86 | throw new RuntimeException( |
| 87 | 'Duplicate static route: ' |
| 88 | . $name |
| 89 | . '. If you want to use the same ' |
| 90 | . 'url prefix you have to create static routes with names.', |
| 91 | ); |
| 92 | } |
| 93 | |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 97 | dir: $dir, |
| 98 | ); |
| 99 | } else { |
| 100 | throw new RuntimeException("The static directory does not exist: {$dir}"); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | public function asset( |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 113 | throw new NotFoundException('Static route not found: ' . $name); |
| 114 | } |
| 115 | |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param array<string, mixed> $params |
| 132 | * @param array<string, mixed> $query |
| 133 | */ |
| 134 | public function url( |
| 135 | string $name, |
| 136 | array $params = [], |
| 137 | array $query = [], |
| 138 | ?string $host = null, |
| 139 | ): string { |
| 140 | $route = $this->names[$name] ?? null; |
| 141 | |
| 142 | if (!$route) { |
| 143 | throw new NotFoundException('Route not found: ' . $name); |
| 144 | } |
| 145 | |
| 146 | $url = $this->applyGlobalPrefix($route->url($params)); |
| 147 | $queryString = $this->queryString($query); |
| 148 | |
| 149 | if ($queryString !== '') { |
| 150 | $url .= '?' . $queryString; |
| 151 | } |
| 152 | |
| 153 | return $this->prependHost($url, $host); |
| 154 | } |
| 155 | |
| 156 | public function match(Request $request): RouteMatch |
| 157 | { |
| 158 | $url = rawurldecode($request->getUri()->getPath()); |
| 159 | $requestMethod = strtoupper($request->getMethod()); |
| 160 | |
| 161 | foreach ([$requestMethod, self::ANY] as $method) { |
| 162 | foreach ($this->routes[$method] ?? [] as $route) { |
| 163 | $params = $route->match($url, $this->globalPrefix); |
| 164 | |
| 165 | if ($params !== null) { |
| 166 | return new RouteMatch($route, $params, $requestMethod); |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /** @var list<string> $allowedMethods */ |
| 172 | $allowedMethods = []; |
| 173 | |
| 174 | foreach ($this->routes as $method => $routes) { |
| 175 | if ($method === $requestMethod || $method === self::ANY) { |
| 176 | continue; |
| 177 | } |
| 178 | |
| 179 | foreach ($routes as $route) { |
| 180 | if ($route->match($url, $this->globalPrefix) === null) { |
| 181 | continue; |
| 182 | } |
| 183 | |
| 184 | $allowedMethods[] = $method; |
| 185 | |
| 186 | break; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | if (count($allowedMethods) > 0) { |
| 191 | throw new MethodNotAllowedException($allowedMethods); |
| 192 | } |
| 193 | |
| 194 | throw new NotFoundException(); |
| 195 | } |
| 196 | |
| 197 | private function applyGlobalPrefix(string $path): string |
| 198 | { |
| 199 | if ($this->globalPrefix === '') { |
| 200 | return $path; |
| 201 | } |
| 202 | |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 204 | } |
| 205 | |
| 206 | private function prependHost(string $path, ?string $host): string |
| 207 | { |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 209 | } |
| 210 | |
| 211 | /** @param array<string, mixed> $query */ |
| 212 | private function queryString(array $query): string |
| 213 | { |
| 214 | $normalized = $this->normalizeQuery($query); |
| 215 | |
| 216 | return http_build_query($normalized, '', '&', PHP_QUERY_RFC3986); |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * @param array<string, mixed> $query |
| 221 | * @return array<string, bool|int|float|string|list<bool|int|float|string>> |
| 222 | */ |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 253 | |
| 254 | private function queryValue(mixed $value, string $name): bool|int|float|string |
| 255 | { |
| 256 | if (is_scalar($value)) { |
| 257 | return $value; |
| 258 | } |
| 259 | |
| 260 | if ($value instanceof Stringable) { |
| 261 | return (string) $value; |
| 262 | } |
| 263 | |
| 264 | throw new InvalidArgumentException('Query parameter must be scalar or a list of scalars: ' |
| 265 | . $name); |
| 266 | } |
| 267 | |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 273 | return ''; |
| 274 | } |
| 275 | |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 280 | return ''; |
| 281 | } |
| 282 | |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 287 | |
| 288 | /** @return array{string, bool} */ |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 294 | return [$path, false]; |
| 295 | } |
| 296 | |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 298 | } |
| 299 | |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 305 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 306 | } |
| 307 | |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | private function isInsideDirectory(string $file, string $dir): bool |
| 316 | { |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 318 | } |
| 319 | } |
Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not
necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once.
Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 23 | public function __construct(string $globalPrefix = '') |
| 24 | { |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 27 | } |
| 23 | public function __construct(string $globalPrefix = '') |
| 24 | { |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 27 | } |
| 23 | public function __construct(string $globalPrefix = '') |
| 24 | { |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 27 | } |
| 23 | public function __construct(string $globalPrefix = '') |
| 24 | { |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 25 | $globalPrefix = trim($globalPrefix, '/'); |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 26 | $this->globalPrefix = $globalPrefix === '' ? '' : '/' . $globalPrefix; |
| 27 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 52 | $this->routes[self::ANY][] = $route; |
| 53 | } |
| 54 | |
| 55 | if ($name) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 57 | throw new RuntimeException('Duplicate route name: ' . $name); |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 56 | if (array_key_exists($name, $this->names)) { |
| 60 | $this->names[$name] = $route; |
| 61 | } |
| 62 | |
| 63 | return $route; |
| 63 | return $route; |
| 64 | } |
| 41 | public function addRoute(Route $route): Route |
| 42 | { |
| 43 | $name = $route->name(); |
| 44 | $noMethodGiven = true; |
| 45 | |
| 46 | foreach ($route->methods() as $method) { |
| 46 | foreach ($route->methods() as $method) { |
| 47 | $noMethodGiven = false; |
| 48 | $this->routes[$method][] = $route; |
| 49 | } |
| 50 | |
| 51 | if ($noMethodGiven) { |
| 55 | if ($name) { |
| 63 | return $route; |
| 64 | } |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 82 | $name = $prefix; |
| 83 | } |
| 84 | |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 86 | throw new RuntimeException( |
| 87 | 'Duplicate static route: ' |
| 88 | . $name |
| 89 | . '. If you want to use the same ' |
| 90 | . 'url prefix you have to create static routes with names.', |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 82 | $name = $prefix; |
| 83 | } |
| 84 | |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 102 | } |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 82 | $name = $prefix; |
| 83 | } |
| 84 | |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 102 | } |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 82 | $name = $prefix; |
| 83 | } |
| 84 | |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 100 | throw new RuntimeException("The static directory does not exist: {$dir}"); |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 86 | throw new RuntimeException( |
| 87 | 'Duplicate static route: ' |
| 88 | . $name |
| 89 | . '. If you want to use the same ' |
| 90 | . 'url prefix you have to create static routes with names.', |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 102 | } |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 94 | if (is_dir($dir)) { |
| 95 | $this->staticRoutes[$name] = new StaticRoute( |
| 96 | prefix: $this->globalPrefix . '/' . trim($prefix, '/') . '/', |
| 102 | } |
| 77 | string $prefix, |
| 78 | string $dir, |
| 79 | string $name = '', |
| 80 | ): void { |
| 81 | if ($name === '') { |
| 85 | if (array_key_exists($name, $this->staticRoutes)) { |
| 94 | if (is_dir($dir)) { |
| 100 | throw new RuntimeException("The static directory does not exist: {$dir}"); |
| 197 | private function applyGlobalPrefix(string $path): string |
| 198 | { |
| 199 | if ($this->globalPrefix === '') { |
| 200 | return $path; |
| 197 | private function applyGlobalPrefix(string $path): string |
| 198 | { |
| 199 | if ($this->globalPrefix === '') { |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 204 | } |
| 197 | private function applyGlobalPrefix(string $path): string |
| 198 | { |
| 199 | if ($this->globalPrefix === '') { |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 203 | return $path === '/' ? $this->globalPrefix : $this->globalPrefix . $path; |
| 204 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 305 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 305 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 305 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 305 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 300 | private function assertSafeStaticPath(string $path): void |
| 301 | { |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 302 | $decodedPath = str_replace('\\', '/', rawurldecode($path)); |
| 303 | |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 304 | if (str_contains($decodedPath, "\0")) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 308 | foreach (explode('/', $decodedPath) as $segment) { |
| 309 | if ($segment === '..') { |
| 310 | throw new InvalidArgumentException('Static path must stay inside static root'); |
| 311 | } |
| 312 | } |
| 313 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 113 | throw new NotFoundException('Static route not found: ' . $name); |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 123 | $path .= ($hasQuery ? '&' : '?') . 'v=' . $buster; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 120 | $buster = $this->getCacheBuster($route->dir, $file); |
| 121 | |
| 122 | if ($buster !== '') { |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 105 | string $name, |
| 106 | string $path, |
| 107 | bool $bust = false, |
| 108 | ?string $host = null, |
| 109 | ): string { |
| 110 | $route = $this->staticRoutes[$name] ?? null; |
| 111 | |
| 112 | if (!$route) { |
| 116 | [$file, $hasQuery] = $this->splitStaticPath($path); |
| 117 | $this->assertSafeStaticPath($file); |
| 118 | |
| 119 | if ($bust) { |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 127 | return $this->prependHost($route->prefix . trim($path, '/'), $host); |
| 128 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 273 | return ''; |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 280 | return ''; |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 280 | return ''; |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 280 | return ''; |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 280 | return ''; |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 268 | protected function getCacheBuster(string $dir, string $path): string |
| 269 | { |
| 270 | $root = realpath($dir); |
| 271 | |
| 272 | if ($root === false) { |
| 276 | $ds = DIRECTORY_SEPARATOR; |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 277 | $file = realpath($root . $ds . ltrim(str_replace('/', $ds, $path), $ds)); |
| 278 | |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 279 | if ($file === false || !$this->isInsideDirectory($file, $root)) { |
| 283 | $mtime = filemtime($file); |
| 284 | |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 285 | return $mtime === false ? '' : hash('xxh32', (string) $mtime); |
| 286 | } |
| 68 | string $patternPrefix, |
| 69 | Closure $createClosure, |
| 70 | string $namePrefix = '', |
| 71 | ): void { |
| 72 | $group = Group::make($patternPrefix, $createClosure, $namePrefix); |
| 73 | $group->register($this); |
| 74 | } |
| 315 | private function isInsideDirectory(string $file, string $dir): bool |
| 316 | { |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 318 | } |
| 315 | private function isInsideDirectory(string $file, string $dir): bool |
| 316 | { |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 317 | return str_starts_with($file, rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); |
| 318 | } |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 233 | if (is_array($value)) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 223 | private function normalizeQuery(array $query): array |
| 224 | { |
| 225 | $normalized = []; |
| 226 | |
| 227 | /** @psalm-suppress MixedAssignment -- query values are intentionally mixed and validated below */ |
| 228 | foreach ($query as $name => $value) { |
| 228 | foreach ($query as $name => $value) { |
| 229 | if ($value === null) { |
| 230 | continue; |
| 231 | } |
| 232 | |
| 233 | if (is_array($value)) { |
| 234 | if (!array_is_list($value)) { |
| 235 | throw new InvalidArgumentException( |
| 236 | 'Query parameter must be scalar or a list of scalars: ' . $name, |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | $normalized[$name] = array_map( |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |
| 242 | $value, |
| 243 | ); |
| 244 | |
| 245 | continue; |
| 246 | } |
| 247 | |
| 248 | $normalized[$name] = $this->queryValue($value, $name); |
| 249 | } |
| 250 | |
| 251 | return $normalized; |
| 252 | } |
| 206 | private function prependHost(string $path, ?string $host): string |
| 207 | { |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 209 | } |
| 206 | private function prependHost(string $path, ?string $host): string |
| 207 | { |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 208 | return $host === null ? $path : rtrim($host, '/') . $path; |
| 209 | } |
| 212 | private function queryString(array $query): string |
| 213 | { |
| 214 | $normalized = $this->normalizeQuery($query); |
| 215 | |
| 216 | return http_build_query($normalized, '', '&', PHP_QUERY_RFC3986); |
| 217 | } |
| 254 | private function queryValue(mixed $value, string $name): bool|int|float|string |
| 255 | { |
| 256 | if (is_scalar($value)) { |
| 257 | return $value; |
| 254 | private function queryValue(mixed $value, string $name): bool|int|float|string |
| 255 | { |
| 256 | if (is_scalar($value)) { |
| 260 | if ($value instanceof Stringable) { |
| 261 | return (string) $value; |
| 254 | private function queryValue(mixed $value, string $name): bool|int|float|string |
| 255 | { |
| 256 | if (is_scalar($value)) { |
| 260 | if ($value instanceof Stringable) { |
| 264 | throw new InvalidArgumentException('Query parameter must be scalar or a list of scalars: ' |
| 265 | . $name); |
| 266 | } |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 294 | return [$path, false]; |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 298 | } |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 298 | } |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 294 | return [$path, false]; |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 298 | } |
| 289 | private function splitStaticPath(string $path): array |
| 290 | { |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 291 | $queryStart = strpos($path, '?'); |
| 292 | |
| 293 | if ($queryStart === false) { |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 297 | return [substr($path, 0, $queryStart), true]; |
| 298 | } |
| 135 | string $name, |
| 136 | array $params = [], |
| 137 | array $query = [], |
| 138 | ?string $host = null, |
| 139 | ): string { |
| 140 | $route = $this->names[$name] ?? null; |
| 141 | |
| 142 | if (!$route) { |
| 143 | throw new NotFoundException('Route not found: ' . $name); |
| 135 | string $name, |
| 136 | array $params = [], |
| 137 | array $query = [], |
| 138 | ?string $host = null, |
| 139 | ): string { |
| 140 | $route = $this->names[$name] ?? null; |
| 141 | |
| 142 | if (!$route) { |
| 146 | $url = $this->applyGlobalPrefix($route->url($params)); |
| 147 | $queryString = $this->queryString($query); |
| 148 | |
| 149 | if ($queryString !== '') { |
| 150 | $url .= '?' . $queryString; |
| 151 | } |
| 152 | |
| 153 | return $this->prependHost($url, $host); |
| 153 | return $this->prependHost($url, $host); |
| 154 | } |
| 135 | string $name, |
| 136 | array $params = [], |
| 137 | array $query = [], |
| 138 | ?string $host = null, |
| 139 | ): string { |
| 140 | $route = $this->names[$name] ?? null; |
| 141 | |
| 142 | if (!$route) { |
| 146 | $url = $this->applyGlobalPrefix($route->url($params)); |
| 147 | $queryString = $this->queryString($query); |
| 148 | |
| 149 | if ($queryString !== '') { |
| 153 | return $this->prependHost($url, $host); |
| 154 | } |
| 241 | fn(mixed $item): bool|int|float|string => $this->queryValue($item, $name), |