MediaWiki master
RouteFileModule.php
Go to the documentation of this file.
1<?php
2
4
5use AppendIterator;
6use ArrayIterator;
7use Iterator;
8use LogicException;
18use Wikimedia\ObjectFactory\ObjectFactory;
19
63class RouteFileModule extends Module {
64
66 private array $routeFiles;
67
71 private array $extraRoutes;
72
77 private ?array $routesFromFiles = null;
78
80 private ?array $routeFileTimestamps = null;
81
83 private ?string $configHash = null;
84
86 private ?array $matchers = null;
87
96 public function __construct(
97 array $routeFiles,
98 array $extraRoutes,
99 Router $router,
100 string $pathPrefix,
102 BasicAuthorizerInterface $basicAuth,
103 ObjectFactory $objectFactory,
104 Validator $restValidator,
105 ErrorReporter $errorReporter
106 ) {
107 parent::__construct(
108 $router,
111 $basicAuth,
112 $objectFactory,
113 $restValidator,
114 $errorReporter
115 );
116 $this->routeFiles = $routeFiles;
117 $this->extraRoutes = $extraRoutes;
118 }
119
120 public function getCacheData(): array {
121 $cacheData = [];
122
123 foreach ( $this->getMatchers() as $method => $matcher ) {
124 $cacheData[$method] = $matcher->getCacheData();
125 }
126
127 $cacheData[self::CACHE_CONFIG_HASH_KEY] = $this->getConfigHash();
128 return $cacheData;
129 }
130
131 public function initFromCacheData( array $cacheData ): bool {
132 if ( $cacheData[self::CACHE_CONFIG_HASH_KEY] !== $this->getConfigHash() ) {
133 return false;
134 }
135
136 unset( $cacheData[self::CACHE_CONFIG_HASH_KEY] );
137 $this->matchers = [];
138
139 foreach ( $cacheData as $method => $data ) {
140 $this->matchers[$method] = PathMatcher::newFromCache( $data );
141 }
142
143 return true;
144 }
145
151 private function getConfigHash(): string {
152 if ( $this->configHash === null ) {
153 $this->configHash = md5( json_encode( [
154 'version' => 5,
155 'extraRoutes' => $this->extraRoutes,
156 'fileTimestamps' => $this->getRouteFileTimestamps()
157 ] ) );
158 }
159 return $this->configHash;
160 }
161
169 private function getRoutesFromFiles(): array {
170 if ( $this->routesFromFiles !== null ) {
171 return $this->routesFromFiles;
172 }
173
174 $this->routesFromFiles = [];
175 $this->routeFileTimestamps = [];
176 foreach ( $this->routeFiles as $fileName ) {
177 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
178
179 $routes = $this->loadRoutes( $fileName );
180
181 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
182 }
183
184 return $this->routesFromFiles;
185 }
186
196 private function loadRoutes( string $fileName ) {
197 $spec = $this->loadJsonFile( $fileName );
198
199 if ( isset( $spec['routes'] ) ) {
200 if ( !isset( $spec['module'] ) ) {
201 throw new ModuleConfigurationException(
202 "Missing module name in $fileName"
203 );
204 }
205
206 if ( $spec['module'] !== $this->pathPrefix ) {
207 // The Router gave us a route file that doesn't match the module name.
208 // This is a programming error, the Router should get this right.
209 throw new LogicException(
210 "Module name mismatch in $fileName: " .
211 "expected {$this->pathPrefix} but got {$spec['module']}."
212 );
213 }
214
215 // intermediate format with meta-data
216 $routes = $spec['routes'];
217 } else {
218 // old, flat format
219 $routes = $spec;
220 }
221
222 return $routes;
223 }
224
230 private function getRouteFileTimestamps(): array {
231 if ( $this->routeFileTimestamps === null ) {
232 $this->routeFileTimestamps = [];
233 foreach ( $this->routeFiles as $fileName ) {
234 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
235 }
236 }
237 return $this->routeFileTimestamps;
238 }
239
245 public function getDefinedPaths(): array {
246 $paths = [];
247 foreach ( $this->getAllRoutes() as $spec ) {
248 $key = $spec['path'];
249
250 $methods = isset( $spec['method'] ) ? (array)$spec['method'] : [ 'GET' ];
251
252 $paths[$key] = array_merge( $paths[$key] ?? [], $methods );
253 }
254
255 return $paths;
256 }
257
261 private function getAllRoutes() {
262 $iterator = new AppendIterator;
263 $iterator->append( new ArrayIterator( $this->getRoutesFromFiles() ) );
264 $iterator->append( new ArrayIterator( $this->extraRoutes ) );
265 return $iterator;
266 }
267
273 private function getMatchers() {
274 if ( $this->matchers === null ) {
275 $routeDefs = $this->getAllRoutes();
276
277 $matchers = [];
278
279 foreach ( $routeDefs as $spec ) {
280 $methods = $spec['method'] ?? [ 'GET' ];
281 if ( !is_array( $methods ) ) {
282 $methods = [ $methods ];
283 }
284 foreach ( $methods as $method ) {
285 if ( !isset( $matchers[$method] ) ) {
286 $matchers[$method] = new PathMatcher;
287 }
288 $matchers[$method]->add( $spec['path'], $spec );
289 }
290 }
291 $this->matchers = $matchers;
292 }
293
294 return $this->matchers;
295 }
296
300 public function findHandlerMatch(
301 string $path,
302 string $requestMethod
303 ): array {
304 $matchers = $this->getMatchers();
305 $matcher = $matchers[$requestMethod] ?? null;
306 $match = $matcher ? $matcher->match( $path ) : null;
307
308 if ( !$match ) {
309 // Return allowed methods, to support CORS and 405 responses.
310 return [
311 'found' => false,
312 'methods' => $this->getAllowedMethods( $path ),
313 ];
314 } else {
315 $spec = $match['userData'];
316
317 if ( !isset( $spec['class'] ) && !isset( $spec['factory'] ) ) {
318 // Inject well known handler class for shorthand definition
319 if ( isset( $spec['redirect'] ) ) {
320 $spec['class'] = RedirectHandler::class;
321 } else {
322 throw new RouteDefinitionException(
323 'Route handler definition must specify "class" or ' .
324 '"factory" or "redirect"'
325 );
326 }
327 }
328
329 return [
330 'found' => true,
331 'spec' => $spec,
332 'params' => $match['params'] ?? [],
333 'config' => $spec,
334 'path' => $spec['path'],
335 ];
336 }
337 }
338
346 public function getAllowedMethods( string $relPath ): array {
347 $allowed = [];
348 foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
349 if ( $allowedMatcher->match( $relPath ) ) {
350 $allowed[] = $allowedMethod;
351 }
352 }
353
354 return array_unique(
355 in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
356 );
357 }
358
359}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
A REST module represents a collection of endpoints.
Definition Module.php:32
ResponseFactory $responseFactory
Definition Module.php:40
A Module that is based on route definition files.
getCacheData()
Return data that can later be used to initialize a new instance of this module in a fast and efficien...
__construct(array $routeFiles, array $extraRoutes, Router $router, string $pathPrefix, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator, ErrorReporter $errorReporter)
initFromCacheData(array $cacheData)
Initialize from the given cache data if possible.
findHandlerMatch(string $path, string $requestMethod)
Determines which handler to use for the given path and returns an array describing the handler and in...
getAllowedMethods(string $relPath)
Get the allowed methods for a path.
Exception indicating incorrect REST module configuration.
A tree-based path routing algorithm.
static newFromCache( $data)
Create a PathMatcher from cache data.
Generates standardized response objects.
The REST router is responsible for gathering module configuration, matching an input path against the...
Definition Router.php:28
Wrapper for ParamValidator.
Definition Validator.php:36
An interface used by Router to ensure that the client has "basic" access, i.e.
An ErrorReporter internally reports an error that happened during the handling of a request.