MediaWiki REL1_37
Router.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Rest;
4
5use AppendIterator;
6use BagOStuff;
13use Wikimedia\ObjectFactory;
14
20class Router {
22 private $routeFiles;
23
25 private $extraRoutes;
26
29
32
34 private $baseUrl;
35
37 private $rootPath;
38
40 private $cacheBag;
41
43 private $matchers;
44
46 private $configHash;
47
50
52 private $basicAuth;
53
55 private $authority;
56
59
62
64 private $cors;
65
68
83 public function __construct(
92 ObjectFactory $objectFactory,
95 ) {
96 $this->routeFiles = $routeFiles;
97 $this->extraRoutes = $extraRoutes;
98 $this->baseUrl = $baseUrl;
99 $this->rootPath = $rootPath;
100 $this->cacheBag = $cacheBag;
101 $this->responseFactory = $responseFactory;
102 $this->basicAuth = $basicAuth;
103 $this->authority = $authority;
104 $this->objectFactory = $objectFactory;
105 $this->restValidator = $restValidator;
106 $this->hookContainer = $hookContainer;
107 }
108
114 private function fetchCacheData() {
115 $cacheData = $this->cacheBag->get( $this->getCacheKey() );
116 if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
117 unset( $cacheData['CONFIG-HASH'] );
118 return $cacheData;
119 } else {
120 return false;
121 }
122 }
123
127 private function getCacheKey() {
128 return $this->cacheBag->makeKey( __CLASS__, '1' );
129 }
130
136 private function getConfigHash() {
137 if ( $this->configHash === null ) {
138 $this->configHash = md5( json_encode( [
139 $this->extraRoutes,
141 ] ) );
142 }
143 return $this->configHash;
144 }
145
151 private function getRoutesFromFiles() {
152 if ( $this->routesFromFiles === null ) {
153 $this->routeFileTimestamps = [];
154 foreach ( $this->routeFiles as $fileName ) {
155 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
156 $routes = json_decode( file_get_contents( $fileName ), true );
157 if ( $this->routesFromFiles === null ) {
158 $this->routesFromFiles = $routes;
159 } else {
160 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
161 }
162 }
163 }
165 }
166
172 private function getRouteFileTimestamps() {
173 if ( $this->routeFileTimestamps === null ) {
174 $this->routeFileTimestamps = [];
175 foreach ( $this->routeFiles as $fileName ) {
176 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
177 }
178 }
180 }
181
188 private function getAllRoutes() {
189 $iterator = new AppendIterator;
190 $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
191 $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
192 return $iterator;
193 }
194
200 private function getMatchers() {
201 if ( $this->matchers === null ) {
202 $cacheData = $this->fetchCacheData();
203 $matchers = [];
204 if ( $cacheData ) {
205 foreach ( $cacheData as $method => $data ) {
206 $matchers[$method] = PathMatcher::newFromCache( $data );
207 }
208 } else {
209 foreach ( $this->getAllRoutes() as $spec ) {
210 $methods = $spec['method'] ?? [ 'GET' ];
211 if ( !is_array( $methods ) ) {
212 $methods = [ $methods ];
213 }
214 foreach ( $methods as $method ) {
215 if ( !isset( $matchers[$method] ) ) {
216 $matchers[$method] = new PathMatcher;
217 }
218 $matchers[$method]->add( $spec['path'], $spec );
219 }
220 }
221
222 $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
223 foreach ( $matchers as $method => $matcher ) {
224 $cacheData[$method] = $matcher->getCacheData();
225 }
226 $this->cacheBag->set( $this->getCacheKey(), $cacheData );
227 }
228 $this->matchers = $matchers;
229 }
230 return $this->matchers;
231 }
232
240 private function getRelativePath( $path ) {
241 if ( strlen( $this->rootPath ) > strlen( $path ) ||
242 substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
243 ) {
244 return false;
245 }
246 return substr( $path, strlen( $this->rootPath ) );
247 }
248
259 public function getRouteUrl( $route, $pathParams = [], $queryParams = [] ) {
260 foreach ( $pathParams as $param => $value ) {
261 // NOTE: we use rawurlencode here, since execute() uses rawurldecode().
262 // Spaces in path params must be encoded to %20 (not +).
263 $route = str_replace( '{' . $param . '}', rawurlencode( $value ), $route );
264 }
265
266 $url = $this->baseUrl . $this->rootPath . $route;
267 return wfAppendQuery( $url, $queryParams );
268 }
269
276 public function execute( RequestInterface $request ) {
277 $path = $request->getUri()->getPath();
278 $relPath = $this->getRelativePath( $path );
279 if ( $relPath === false ) {
280 return $this->responseFactory->createLocalizedHttpError( 404,
281 ( new MessageValue( 'rest-prefix-mismatch' ) )
282 ->plaintextParams( $path, $this->rootPath )
283 );
284 }
285
286 $requestMethod = $request->getMethod();
287 $matchers = $this->getMatchers();
288 $matcher = $matchers[$requestMethod] ?? null;
289 $match = $matcher ? $matcher->match( $relPath ) : null;
290
291 // For a HEAD request, execute the GET handler instead if one exists.
292 // The webserver will discard the body.
293 if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
294 $match = $matchers['GET']->match( $relPath );
295 }
296
297 if ( !$match ) {
298 // Check for 405 wrong method
299 $allowed = $this->getAllowedMethods( $relPath );
300
301 // Check for CORS Preflight. This response will *not* allow the request unless
302 // an Access-Control-Allow-Origin header is added to this response.
303 if ( $this->cors && $requestMethod === 'OPTIONS' ) {
304 return $this->cors->createPreflightResponse( $allowed );
305 }
306
307 if ( $allowed ) {
308 $response = $this->responseFactory->createLocalizedHttpError( 405,
309 ( new MessageValue( 'rest-wrong-method' ) )
310 ->textParams( $requestMethod )
311 ->commaListParams( $allowed )
312 ->numParams( count( $allowed ) )
313 );
314 $response->setHeader( 'Allow', $allowed );
315 return $response;
316 } else {
317 // Did not match with any other method, must be 404
318 return $this->responseFactory->createLocalizedHttpError( 404,
319 ( new MessageValue( 'rest-no-match' ) )
320 ->plaintextParams( $relPath )
321 );
322 }
323 }
324
325 // Use rawurldecode so a "+" in path params is not interpreted as a space character.
326 $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
327 $handler = $this->createHandler( $request, $match['userData'] );
328
329 try {
330 return $this->executeHandler( $handler );
331 } catch ( HttpException $e ) {
332 return $this->responseFactory->createFromException( $e );
333 }
334 }
335
342 private function getAllowedMethods( string $relPath ): array {
343 // Check for 405 wrong method
344 $allowed = [];
345 foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
346 if ( $allowedMatcher->match( $relPath ) ) {
347 $allowed[] = $allowedMethod;
348 }
349 }
350
351 return array_unique(
352 in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
353 );
354 }
355
362 private function createHandler( RequestInterface $request, array $spec ): Handler {
363 $objectFactorySpec = array_intersect_key(
364 $spec,
365 [
366 'factory' => true,
367 'class' => true,
368 'args' => true,
369 'services' => true,
370 'optional_services' => true
371 ]
372 );
374 $handler = $this->objectFactory->createObject( $objectFactorySpec );
375 $handler->init( $this, $request, $spec, $this->authority, $this->responseFactory, $this->hookContainer );
376
377 return $handler;
378 }
379
386 private function executeHandler( $handler ): ResponseInterface {
387 // Check for basic authorization, to avoid leaking data from private wikis
388 $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
389 if ( $authResult ) {
390 return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
391 }
392
393 // Validate the parameters
394 $handler->validate( $this->restValidator );
395
396 // Check conditional request headers
397 $earlyResponse = $handler->checkPreconditions();
398 if ( $earlyResponse ) {
399 return $earlyResponse;
400 }
401
402 // Run the main part of the handler
403 $response = $handler->execute();
404 if ( !( $response instanceof ResponseInterface ) ) {
405 $response = $this->responseFactory->createFromReturnValue( $response );
406 }
407
408 // Set Last-Modified and ETag headers in the response if available
409 $handler->applyConditionalResponseHeaders( $response );
410
411 return $response;
412 }
413
418 public function setCors( CorsUtils $cors ): self {
419 $this->cors = $cors;
420
421 return $this;
422 }
423}
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:86
Base class for REST route handlers.
Definition Handler.php:17
init(Router $router, RequestInterface $request, array $config, Authority $authority, ResponseFactory $responseFactory, HookContainer $hookContainer)
Initialise with dependencies from the Router.
Definition Handler.php:68
This is the base exception class for non-fatal exceptions thrown from REST handlers.
A tree-based path routing algorithm.
add( $template, $userData)
Add a template to the matcher.
static newFromCache( $data)
Create a PathMatcher from cache data.
match( $path)
Match a path against the current match trees.
Generates standardized response objects.
The REST router is responsible for gathering handler configuration, matching an input path and HTTP m...
Definition Router.php:20
array null $routesFromFiles
Definition Router.php:28
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition Router.php:200
getAllowedMethods(string $relPath)
Get the allow methods for a path.
Definition Router.php:342
HookContainer $hookContainer
Definition Router.php:67
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition Router.php:188
getConfigHash()
Get a config version hash for cache invalidation.
Definition Router.php:136
executeHandler( $handler)
Execute a fully-constructed handler.
Definition Router.php:386
ObjectFactory $objectFactory
Definition Router.php:58
BagOStuff $cacheBag
Definition Router.php:40
Validator $restValidator
Definition Router.php:61
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition Router.php:172
BasicAuthorizerInterface $basicAuth
Definition Router.php:52
int[] null $routeFileTimestamps
Definition Router.php:31
PathMatcher[] null $matchers
Path matchers by method.
Definition Router.php:43
Authority $authority
Definition Router.php:55
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition Router.php:240
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition Router.php:276
CorsUtils null $cors
Definition Router.php:64
getRouteUrl( $route, $pathParams=[], $queryParams=[])
Returns a full URL for the given route.
Definition Router.php:259
string null $configHash
Definition Router.php:46
string[] $routeFiles
Definition Router.php:22
setCors(CorsUtils $cors)
Definition Router.php:418
ResponseFactory $responseFactory
Definition Router.php:49
__construct( $routeFiles, $extraRoutes, $baseUrl, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, Authority $authority, ObjectFactory $objectFactory, Validator $restValidator, HookContainer $hookContainer)
Definition Router.php:83
fetchCacheData()
Get the cache data, or false if it is missing or invalid.
Definition Router.php:114
createHandler(RequestInterface $request, array $spec)
Create a handler from its spec.
Definition Router.php:362
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition Router.php:151
Wrapper for ParamValidator.
Definition Validator.php:32
Value object representing a message for i18n.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
An interface used by Router to ensure that the client has "basic" access, i.e.
A request interface similar to PSR-7's ServerRequestInterface.
getMethod()
Retrieves the HTTP method of the request.
getUri()
Retrieves the URI instance.
setPathParams( $params)
Erase all path parameters from the object and set the parameter array to the one specified.
An interface similar to PSR-7's ResponseInterface, the primary difference being that it is mutable.
return true
Definition router.php:92