MediaWiki fundraising/REL1_35
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
56
59
62
80 ) {
81 $this->routeFiles = $routeFiles;
82 $this->extraRoutes = $extraRoutes;
83 $this->baseUrl = $baseUrl;
84 $this->rootPath = $rootPath;
85 $this->cacheBag = $cacheBag;
86 $this->responseFactory = $responseFactory;
87 $this->basicAuth = $basicAuth;
88 $this->objectFactory = $objectFactory;
89 $this->restValidator = $restValidator;
90
91 if ( !$hookContainer ) {
92 // b/c for OAuth extension
93 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
94 }
95 $this->hookContainer = $hookContainer;
96 }
97
103 private function fetchCacheData() {
104 $cacheData = $this->cacheBag->get( $this->getCacheKey() );
105 if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
106 unset( $cacheData['CONFIG-HASH'] );
107 return $cacheData;
108 } else {
109 return false;
110 }
111 }
112
116 private function getCacheKey() {
117 return $this->cacheBag->makeKey( __CLASS__, '1' );
118 }
119
125 private function getConfigHash() {
126 if ( $this->configHash === null ) {
127 $this->configHash = md5( json_encode( [
128 $this->extraRoutes,
130 ] ) );
131 }
132 return $this->configHash;
133 }
134
140 private function getRoutesFromFiles() {
141 if ( $this->routesFromFiles === null ) {
142 $this->routeFileTimestamps = [];
143 foreach ( $this->routeFiles as $fileName ) {
144 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
145 $routes = json_decode( file_get_contents( $fileName ), true );
146 if ( $this->routesFromFiles === null ) {
147 $this->routesFromFiles = $routes;
148 } else {
149 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
150 }
151 }
152 }
154 }
155
161 private function getRouteFileTimestamps() {
162 if ( $this->routeFileTimestamps === null ) {
163 $this->routeFileTimestamps = [];
164 foreach ( $this->routeFiles as $fileName ) {
165 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
166 }
167 }
169 }
170
177 private function getAllRoutes() {
178 $iterator = new AppendIterator;
179 $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
180 $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
181 return $iterator;
182 }
183
189 private function getMatchers() {
190 if ( $this->matchers === null ) {
191 $cacheData = $this->fetchCacheData();
192 $matchers = [];
193 if ( $cacheData ) {
194 foreach ( $cacheData as $method => $data ) {
195 $matchers[$method] = PathMatcher::newFromCache( $data );
196 }
197 } else {
198 foreach ( $this->getAllRoutes() as $spec ) {
199 $methods = $spec['method'] ?? [ 'GET' ];
200 if ( !is_array( $methods ) ) {
201 $methods = [ $methods ];
202 }
203 foreach ( $methods as $method ) {
204 if ( !isset( $matchers[$method] ) ) {
205 $matchers[$method] = new PathMatcher;
206 }
207 $matchers[$method]->add( $spec['path'], $spec );
208 }
209 }
210
211 $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
212 foreach ( $matchers as $method => $matcher ) {
213 $cacheData[$method] = $matcher->getCacheData();
214 }
215 $this->cacheBag->set( $this->getCacheKey(), $cacheData );
216 }
217 $this->matchers = $matchers;
218 }
219 return $this->matchers;
220 }
221
229 private function getRelativePath( $path ) {
230 if ( strlen( $this->rootPath ) > strlen( $path ) ||
231 substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
232 ) {
233 return false;
234 }
235 return substr( $path, strlen( $this->rootPath ) );
236 }
237
248 public function getRouteUrl( $route, $pathParams = [], $queryParams = [] ) {
249 foreach ( $pathParams as $param => $value ) {
250 // NOTE: we use rawurlencode here, since execute() uses rawurldecode().
251 // Spaces in path params must be encoded to %20 (not +).
252 $route = str_replace( '{' . $param . '}', rawurlencode( $value ), $route );
253 }
254
255 $url = $this->baseUrl . $this->rootPath . $route;
256 return wfAppendQuery( $url, $queryParams );
257 }
258
265 public function execute( RequestInterface $request ) {
266 $path = $request->getUri()->getPath();
267 $relPath = $this->getRelativePath( $path );
268 if ( $relPath === false ) {
269 return $this->responseFactory->createLocalizedHttpError( 404,
270 ( new MessageValue( 'rest-prefix-mismatch' ) )
271 ->plaintextParams( $path, $this->rootPath )
272 );
273 }
274
275 $requestMethod = $request->getMethod();
276 $matchers = $this->getMatchers();
277 $matcher = $matchers[$requestMethod] ?? null;
278 $match = $matcher ? $matcher->match( $relPath ) : null;
279
280 // For a HEAD request, execute the GET handler instead if one exists.
281 // The webserver will discard the body.
282 if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
283 $match = $matchers['GET']->match( $relPath );
284 }
285
286 if ( !$match ) {
287 // Check for 405 wrong method
288 $allowed = [];
289 foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
290 if ( $allowedMethod === $requestMethod ) {
291 continue;
292 }
293 if ( $allowedMatcher->match( $relPath ) ) {
294 $allowed[] = $allowedMethod;
295 }
296 }
297 if ( $allowed ) {
298 $response = $this->responseFactory->createLocalizedHttpError( 405,
299 ( new MessageValue( 'rest-wrong-method' ) )
300 ->textParams( $requestMethod )
301 ->commaListParams( $allowed )
302 ->numParams( count( $allowed ) )
303 );
304 $response->setHeader( 'Allow', $allowed );
305 return $response;
306 } else {
307 // Did not match with any other method, must be 404
308 return $this->responseFactory->createLocalizedHttpError( 404,
309 ( new MessageValue( 'rest-no-match' ) )
310 ->plaintextParams( $relPath )
311 );
312 }
313 }
314
315 // Use rawurldecode so a "+" in path params is not interpreted as a space character.
316 $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
317 $handler = $this->createHandler( $request, $match['userData'] );
318
319 try {
320 return $this->executeHandler( $handler );
321 } catch ( HttpException $e ) {
322 return $this->responseFactory->createFromException( $e );
323 }
324 }
325
332 private function createHandler( RequestInterface $request, array $spec ): Handler {
333 $objectFactorySpec = array_intersect_key( $spec,
334 [ 'factory' => true, 'class' => true, 'args' => true, 'services' => true ] );
336 $handler = $this->objectFactory->createObject( $objectFactorySpec );
337 $handler->init( $this, $request, $spec, $this->responseFactory, $this->hookContainer );
338
339 return $handler;
340 }
341
348 private function executeHandler( $handler ): ResponseInterface {
349 // Check for basic authorization, to avoid leaking data from private wikis
350 $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
351 if ( $authResult ) {
352 return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
353 }
354
355 // Validate the parameters
356 $handler->validate( $this->restValidator );
357
358 // Check conditional request headers
359 $earlyResponse = $handler->checkPreconditions();
360 if ( $earlyResponse ) {
361 return $earlyResponse;
362 }
363
364 // Run the main part of the handler
365 $response = $handler->execute();
366 if ( !( $response instanceof ResponseInterface ) ) {
367 $response = $this->responseFactory->createFromReturnValue( $response );
368 }
369
370 // Set Last-Modified and ETag headers in the response if available
371 $handler->applyConditionalResponseHeaders( $response );
372
373 return $response;
374 }
375}
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:71
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Base class for REST route handlers.
Definition Handler.php:16
init(Router $router, RequestInterface $request, array $config, ResponseFactory $responseFactory, HookContainer $hookContainer)
Initialise with dependencies from the Router.
Definition Handler.php:60
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:189
HookContainer $hookContainer
Definition Router.php:61
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition Router.php:177
getConfigHash()
Get a config version hash for cache invalidation.
Definition Router.php:125
executeHandler( $handler)
Execute a fully-constructed handler.
Definition Router.php:348
ObjectFactory $objectFactory
Definition Router.php:55
BagOStuff $cacheBag
Definition Router.php:40
Validator $restValidator
Definition Router.php:58
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition Router.php:161
BasicAuthorizerInterface $basicAuth
Definition Router.php:52
int[] null $routeFileTimestamps
Definition Router.php:31
PathMatcher[] null $matchers
Path matchers by method.
Definition Router.php:43
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition Router.php:229
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition Router.php:265
__construct( $routeFiles, $extraRoutes, $baseUrl, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator, HookContainer $hookContainer=null)
Definition Router.php:76
getRouteUrl( $route, $pathParams=[], $queryParams=[])
Returns a full URL for the given route.
Definition Router.php:248
string null $configHash
Definition Router.php:46
string[] $routeFiles
Definition Router.php:22
ResponseFactory $responseFactory
Definition Router.php:49
fetchCacheData()
Get the cache data, or false if it is missing or invalid.
Definition Router.php:103
createHandler(RequestInterface $request, array $spec)
Create a handler from its spec.
Definition Router.php:332
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition Router.php:140
Wrapper for ParamValidator.
Definition Validator.php:31
Value object representing a message for i18n.
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