MediaWiki  master
Router.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest;
4 
5 use AppendIterator;
6 use BagOStuff;
13 use Wikimedia\ObjectFactory;
14 
20 class 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 
58  private $objectFactory;
59 
61  private $restValidator;
62 
64  private $cors;
65 
67  private $hookContainer;
68 
87  ) {
88  $this->routeFiles = $routeFiles;
89  $this->extraRoutes = $extraRoutes;
90  $this->baseUrl = $baseUrl;
91  $this->rootPath = $rootPath;
92  $this->cacheBag = $cacheBag;
93  $this->responseFactory = $responseFactory;
94  $this->basicAuth = $basicAuth;
95  $this->authority = $authority;
96  $this->objectFactory = $objectFactory;
97  $this->restValidator = $restValidator;
98  $this->hookContainer = $hookContainer;
99  }
100 
106  private function fetchCacheData() {
107  $cacheData = $this->cacheBag->get( $this->getCacheKey() );
108  if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
109  unset( $cacheData['CONFIG-HASH'] );
110  return $cacheData;
111  } else {
112  return false;
113  }
114  }
115 
119  private function getCacheKey() {
120  return $this->cacheBag->makeKey( __CLASS__, '1' );
121  }
122 
128  private function getConfigHash() {
129  if ( $this->configHash === null ) {
130  $this->configHash = md5( json_encode( [
131  $this->extraRoutes,
132  $this->getRouteFileTimestamps()
133  ] ) );
134  }
135  return $this->configHash;
136  }
137 
143  private function getRoutesFromFiles() {
144  if ( $this->routesFromFiles === null ) {
145  $this->routeFileTimestamps = [];
146  foreach ( $this->routeFiles as $fileName ) {
147  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
148  $routes = json_decode( file_get_contents( $fileName ), true );
149  if ( $this->routesFromFiles === null ) {
150  $this->routesFromFiles = $routes;
151  } else {
152  $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
153  }
154  }
155  }
156  return $this->routesFromFiles;
157  }
158 
164  private function getRouteFileTimestamps() {
165  if ( $this->routeFileTimestamps === null ) {
166  $this->routeFileTimestamps = [];
167  foreach ( $this->routeFiles as $fileName ) {
168  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
169  }
170  }
172  }
173 
180  private function getAllRoutes() {
181  $iterator = new AppendIterator;
182  $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
183  $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
184  return $iterator;
185  }
186 
192  private function getMatchers() {
193  if ( $this->matchers === null ) {
194  $cacheData = $this->fetchCacheData();
195  $matchers = [];
196  if ( $cacheData ) {
197  foreach ( $cacheData as $method => $data ) {
198  $matchers[$method] = PathMatcher::newFromCache( $data );
199  }
200  } else {
201  foreach ( $this->getAllRoutes() as $spec ) {
202  $methods = $spec['method'] ?? [ 'GET' ];
203  if ( !is_array( $methods ) ) {
204  $methods = [ $methods ];
205  }
206  foreach ( $methods as $method ) {
207  if ( !isset( $matchers[$method] ) ) {
208  $matchers[$method] = new PathMatcher;
209  }
210  $matchers[$method]->add( $spec['path'], $spec );
211  }
212  }
213 
214  $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
215  foreach ( $matchers as $method => $matcher ) {
216  $cacheData[$method] = $matcher->getCacheData();
217  }
218  $this->cacheBag->set( $this->getCacheKey(), $cacheData );
219  }
220  $this->matchers = $matchers;
221  }
222  return $this->matchers;
223  }
224 
232  private function getRelativePath( $path ) {
233  if ( strlen( $this->rootPath ) > strlen( $path ) ||
234  substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
235  ) {
236  return false;
237  }
238  return substr( $path, strlen( $this->rootPath ) );
239  }
240 
251  public function getRouteUrl( $route, $pathParams = [], $queryParams = [] ) {
252  foreach ( $pathParams as $param => $value ) {
253  // NOTE: we use rawurlencode here, since execute() uses rawurldecode().
254  // Spaces in path params must be encoded to %20 (not +).
255  $route = str_replace( '{' . $param . '}', rawurlencode( $value ), $route );
256  }
257 
258  $url = $this->baseUrl . $this->rootPath . $route;
259  return wfAppendQuery( $url, $queryParams );
260  }
261 
268  public function execute( RequestInterface $request ) {
269  $path = $request->getUri()->getPath();
270  $relPath = $this->getRelativePath( $path );
271  if ( $relPath === false ) {
272  return $this->responseFactory->createLocalizedHttpError( 404,
273  ( new MessageValue( 'rest-prefix-mismatch' ) )
274  ->plaintextParams( $path, $this->rootPath )
275  );
276  }
277 
278  $requestMethod = $request->getMethod();
279  $matchers = $this->getMatchers();
280  $matcher = $matchers[$requestMethod] ?? null;
281  $match = $matcher ? $matcher->match( $relPath ) : null;
282 
283  // For a HEAD request, execute the GET handler instead if one exists.
284  // The webserver will discard the body.
285  if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
286  $match = $matchers['GET']->match( $relPath );
287  }
288 
289  if ( !$match ) {
290  // Check for 405 wrong method
291  $allowed = $this->getAllowedMethods( $relPath );
292 
293  // Check for CORS Preflight. This response will *not* allow the request unless
294  // an Access-Control-Allow-Origin header is added to this response.
295  if ( $this->cors && $requestMethod === 'OPTIONS' ) {
296  return $this->cors->createPreflightResponse( $allowed );
297  }
298 
299  if ( $allowed ) {
300  $response = $this->responseFactory->createLocalizedHttpError( 405,
301  ( new MessageValue( 'rest-wrong-method' ) )
302  ->textParams( $requestMethod )
303  ->commaListParams( $allowed )
304  ->numParams( count( $allowed ) )
305  );
306  $response->setHeader( 'Allow', $allowed );
307  return $response;
308  } else {
309  // Did not match with any other method, must be 404
310  return $this->responseFactory->createLocalizedHttpError( 404,
311  ( new MessageValue( 'rest-no-match' ) )
312  ->plaintextParams( $relPath )
313  );
314  }
315  }
316 
317  // Use rawurldecode so a "+" in path params is not interpreted as a space character.
318  $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
319  $handler = $this->createHandler( $request, $match['userData'] );
320 
321  try {
322  return $this->executeHandler( $handler );
323  } catch ( HttpException $e ) {
324  return $this->responseFactory->createFromException( $e );
325  }
326  }
327 
334  private function getAllowedMethods( string $relPath ) : array {
335  // Check for 405 wrong method
336  $allowed = [];
337  foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
338  if ( $allowedMatcher->match( $relPath ) ) {
339  $allowed[] = $allowedMethod;
340  }
341  }
342 
343  return array_unique(
344  in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
345  );
346  }
347 
354  private function createHandler( RequestInterface $request, array $spec ): Handler {
355  $objectFactorySpec = array_intersect_key(
356  $spec,
357  [
358  'factory' => true,
359  'class' => true,
360  'args' => true,
361  'services' => true,
362  'optional_services' => true
363  ]
364  );
366  $handler = $this->objectFactory->createObject( $objectFactorySpec );
367  $handler->init( $this, $request, $spec, $this->authority, $this->responseFactory, $this->hookContainer );
368 
369  return $handler;
370  }
371 
378  private function executeHandler( $handler ): ResponseInterface {
379  // Check for basic authorization, to avoid leaking data from private wikis
380  $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
381  if ( $authResult ) {
382  return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
383  }
384 
385  // Validate the parameters
386  $handler->validate( $this->restValidator );
387 
388  // Check conditional request headers
389  $earlyResponse = $handler->checkPreconditions();
390  if ( $earlyResponse ) {
391  return $earlyResponse;
392  }
393 
394  // Run the main part of the handler
395  $response = $handler->execute();
396  if ( !( $response instanceof ResponseInterface ) ) {
397  $response = $this->responseFactory->createFromReturnValue( $response );
398  }
399 
400  // Set Last-Modified and ETag headers in the response if available
401  $handler->applyConditionalResponseHeaders( $response );
402 
403  return $response;
404  }
405 
410  public function setCors( CorsUtils $cors ) : self {
411  $this->cors = $cors;
412 
413  return $this;
414  }
415 }
MediaWiki\Rest\Router\$responseFactory
ResponseFactory $responseFactory
Definition: Router.php:49
MediaWiki\Rest\Router\$authority
Authority $authority
Definition: Router.php:55
MediaWiki\Rest\ResponseFactory
Generates standardized response objects.
Definition: ResponseFactory.php:17
MediaWiki\Rest\Router\$routesFromFiles
array null $routesFromFiles
Definition: Router.php:28
MediaWiki\Rest\Validator\Validator
Wrapper for ParamValidator.
Definition: Validator.php:32
MediaWiki\Rest\RequestInterface\setPathParams
setPathParams( $params)
Erase all path parameters from the object and set the parameter array to the one specified.
MediaWiki\Rest\CorsUtils
Definition: CorsUtils.php:13
MediaWiki\Rest\Router\fetchCacheData
fetchCacheData()
Get the cache data, or false if it is missing or invalid.
Definition: Router.php:106
MediaWiki\Rest\PathTemplateMatcher\PathMatcher\match
match( $path)
Match a path against the current match trees.
Definition: PathMatcher.php:196
MediaWiki\Rest\Router\$basicAuth
BasicAuthorizerInterface $basicAuth
Definition: Router.php:52
MediaWiki\Rest\Router\$extraRoutes
array $extraRoutes
Definition: Router.php:25
MediaWiki\Rest\Router\$matchers
PathMatcher[] null $matchers
Path matchers by method.
Definition: Router.php:43
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
MediaWiki\Rest\Router\$routeFiles
string[] $routeFiles
Definition: Router.php:22
MediaWiki\Rest\Router\createHandler
createHandler(RequestInterface $request, array $spec)
Create a handler from its spec.
Definition: Router.php:354
MediaWiki\Rest\Router\$configHash
string null $configHash
Definition: Router.php:46
MediaWiki\Rest\Router\getRelativePath
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition: Router.php:232
MediaWiki\Rest\Router\getConfigHash
getConfigHash()
Get a config version hash for cache invalidation.
Definition: Router.php:128
MediaWiki\Rest\Router\setCors
setCors(CorsUtils $cors)
Definition: Router.php:410
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:16
MediaWiki\Rest\Handler
Base class for REST route handlers.
Definition: Handler.php:17
MediaWiki\Rest\Router\execute
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition: Router.php:268
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:443
MediaWiki\Rest\Router\getAllowedMethods
getAllowedMethods(string $relPath)
Get the allow methods for a path.
Definition: Router.php:334
MediaWiki\Rest\PathTemplateMatcher\PathMatcher\add
add( $template, $userData)
Add a template to the matcher.
Definition: PathMatcher.php:149
MediaWiki\Rest\Router
The REST router is responsible for gathering handler configuration, matching an input path and HTTP m...
Definition: Router.php:20
MediaWiki\Rest\Router\$baseUrl
string $baseUrl
Definition: Router.php:34
MediaWiki\Rest\RequestInterface\getMethod
getMethod()
Retrieves the HTTP method of the request.
MediaWiki\Rest\Router\getRouteUrl
getRouteUrl( $route, $pathParams=[], $queryParams=[])
Returns a full URL for the given route.
Definition: Router.php:251
MediaWiki\Rest\Router\executeHandler
executeHandler( $handler)
Execute a fully-constructed handler.
Definition: Router.php:378
MediaWiki\Rest
MediaWiki\Rest\Router\$cacheBag
BagOStuff $cacheBag
Definition: Router.php:40
MediaWiki\Permissions\Authority
@unstable
Definition: Authority.php:30
MediaWiki\Rest\PathTemplateMatcher\PathMatcher\newFromCache
static newFromCache( $data)
Create a PathMatcher from cache data.
Definition: PathMatcher.php:42
MediaWiki\Rest\RequestInterface\getUri
getUri()
Retrieves the URI instance.
MediaWiki\Rest\ResponseInterface
An interface similar to PSR-7's ResponseInterface, the primary difference being that it is mutable.
Definition: ResponseInterface.php:41
MediaWiki\Rest\Router\$cors
CorsUtils null $cors
Definition: Router.php:64
MediaWiki\Rest\RequestInterface
A request interface similar to PSR-7's ServerRequestInterface.
Definition: RequestInterface.php:39
MediaWiki\Rest\HttpException
This is the base exception class for non-fatal exceptions thrown from REST handlers.
Definition: HttpException.php:12
MediaWiki\Rest\PathTemplateMatcher\PathMatcher
A tree-based path routing algorithm.
Definition: PathMatcher.php:16
MediaWiki\Rest\Router\$objectFactory
ObjectFactory $objectFactory
Definition: Router.php:58
MediaWiki\Rest\Router\getAllRoutes
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition: Router.php:180
MediaWiki\Rest\Router\getMatchers
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition: Router.php:192
$path
$path
Definition: NoLocalSettings.php:25
MediaWiki\Rest\Router\__construct
__construct( $routeFiles, $extraRoutes, $baseUrl, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, Authority $authority, ObjectFactory $objectFactory, Validator $restValidator, HookContainer $hookContainer)
Definition: Router.php:83
MediaWiki\Rest\Router\getCacheKey
getCacheKey()
Definition: Router.php:119
MediaWiki\Rest\Router\$routeFileTimestamps
int[] null $routeFileTimestamps
Definition: Router.php:31
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\Rest\Router\getRouteFileTimestamps
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition: Router.php:164
MediaWiki\Rest\Router\$rootPath
string $rootPath
Definition: Router.php:37
MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface
An interface used by Router to ensure that the client has "basic" access, i.e.
Definition: BasicAuthorizerInterface.php:14
MediaWiki\Rest\Router\$hookContainer
HookContainer $hookContainer
Definition: Router.php:67
MediaWiki\Rest\Router\getRoutesFromFiles
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition: Router.php:143
MediaWiki\Rest\Router\$restValidator
Validator $restValidator
Definition: Router.php:61