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;
12 use Wikimedia\ObjectFactory;
13 
19 class Router {
21  private $routeFiles;
22 
24  private $extraRoutes;
25 
28 
31 
33  private $baseUrl;
34 
36  private $rootPath;
37 
39  private $cacheBag;
40 
42  private $matchers;
43 
45  private $configHash;
46 
49 
51  private $basicAuth;
52 
54  private $objectFactory;
55 
57  private $restValidator;
58 
60  private $hookContainer;
61 
79  ) {
80  $this->routeFiles = $routeFiles;
81  $this->extraRoutes = $extraRoutes;
82  $this->baseUrl = $baseUrl;
83  $this->rootPath = $rootPath;
84  $this->cacheBag = $cacheBag;
85  $this->responseFactory = $responseFactory;
86  $this->basicAuth = $basicAuth;
87  $this->objectFactory = $objectFactory;
88  $this->restValidator = $restValidator;
89  $this->hookContainer = $hookContainer;
90  }
91 
97  private function fetchCacheData() {
98  $cacheData = $this->cacheBag->get( $this->getCacheKey() );
99  if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
100  unset( $cacheData['CONFIG-HASH'] );
101  return $cacheData;
102  } else {
103  return false;
104  }
105  }
106 
110  private function getCacheKey() {
111  return $this->cacheBag->makeKey( __CLASS__, '1' );
112  }
113 
119  private function getConfigHash() {
120  if ( $this->configHash === null ) {
121  $this->configHash = md5( json_encode( [
122  $this->extraRoutes,
123  $this->getRouteFileTimestamps()
124  ] ) );
125  }
126  return $this->configHash;
127  }
128 
134  private function getRoutesFromFiles() {
135  if ( $this->routesFromFiles === null ) {
136  $this->routeFileTimestamps = [];
137  foreach ( $this->routeFiles as $fileName ) {
138  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
139  $routes = json_decode( file_get_contents( $fileName ), true );
140  if ( $this->routesFromFiles === null ) {
141  $this->routesFromFiles = $routes;
142  } else {
143  $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
144  }
145  }
146  }
147  return $this->routesFromFiles;
148  }
149 
155  private function getRouteFileTimestamps() {
156  if ( $this->routeFileTimestamps === null ) {
157  $this->routeFileTimestamps = [];
158  foreach ( $this->routeFiles as $fileName ) {
159  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
160  }
161  }
163  }
164 
171  private function getAllRoutes() {
172  $iterator = new AppendIterator;
173  $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
174  $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
175  return $iterator;
176  }
177 
183  private function getMatchers() {
184  if ( $this->matchers === null ) {
185  $cacheData = $this->fetchCacheData();
186  $matchers = [];
187  if ( $cacheData ) {
188  foreach ( $cacheData as $method => $data ) {
189  $matchers[$method] = PathMatcher::newFromCache( $data );
190  }
191  } else {
192  foreach ( $this->getAllRoutes() as $spec ) {
193  $methods = $spec['method'] ?? [ 'GET' ];
194  if ( !is_array( $methods ) ) {
195  $methods = [ $methods ];
196  }
197  foreach ( $methods as $method ) {
198  if ( !isset( $matchers[$method] ) ) {
199  $matchers[$method] = new PathMatcher;
200  }
201  $matchers[$method]->add( $spec['path'], $spec );
202  }
203  }
204 
205  $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
206  foreach ( $matchers as $method => $matcher ) {
207  $cacheData[$method] = $matcher->getCacheData();
208  }
209  $this->cacheBag->set( $this->getCacheKey(), $cacheData );
210  }
211  $this->matchers = $matchers;
212  }
213  return $this->matchers;
214  }
215 
223  private function getRelativePath( $path ) {
224  if ( strlen( $this->rootPath ) > strlen( $path ) ||
225  substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
226  ) {
227  return false;
228  }
229  return substr( $path, strlen( $this->rootPath ) );
230  }
231 
242  public function getRouteUrl( $route, $pathParams = [], $queryParams = [] ) {
243  foreach ( $pathParams as $param => $value ) {
244  // NOTE: we use rawurlencode here, since execute() uses rawurldecode().
245  // Spaces in path params must be encoded to %20 (not +).
246  $route = str_replace( '{' . $param . '}', rawurlencode( $value ), $route );
247  }
248 
249  $url = $this->baseUrl . $this->rootPath . $route;
250  return wfAppendQuery( $url, $queryParams );
251  }
252 
259  public function execute( RequestInterface $request ) {
260  $path = $request->getUri()->getPath();
261  $relPath = $this->getRelativePath( $path );
262  if ( $relPath === false ) {
263  return $this->responseFactory->createLocalizedHttpError( 404,
264  ( new MessageValue( 'rest-prefix-mismatch' ) )
265  ->plaintextParams( $path, $this->rootPath )
266  );
267  }
268 
269  $requestMethod = $request->getMethod();
270  $matchers = $this->getMatchers();
271  $matcher = $matchers[$requestMethod] ?? null;
272  $match = $matcher ? $matcher->match( $relPath ) : null;
273 
274  // For a HEAD request, execute the GET handler instead if one exists.
275  // The webserver will discard the body.
276  if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
277  $match = $matchers['GET']->match( $relPath );
278  }
279 
280  if ( !$match ) {
281  // Check for 405 wrong method
282  $allowed = [];
283  foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
284  if ( $allowedMethod === $requestMethod ) {
285  continue;
286  }
287  if ( $allowedMatcher->match( $relPath ) ) {
288  $allowed[] = $allowedMethod;
289  }
290  }
291  if ( $allowed ) {
292  $response = $this->responseFactory->createLocalizedHttpError( 405,
293  ( new MessageValue( 'rest-wrong-method' ) )
294  ->textParams( $requestMethod )
295  ->commaListParams( $allowed )
296  ->numParams( count( $allowed ) )
297  );
298  $response->setHeader( 'Allow', $allowed );
299  return $response;
300  } else {
301  // Did not match with any other method, must be 404
302  return $this->responseFactory->createLocalizedHttpError( 404,
303  ( new MessageValue( 'rest-no-match' ) )
304  ->plaintextParams( $relPath )
305  );
306  }
307  }
308 
309  // Use rawurldecode so a "+" in path params is not interpreted as a space character.
310  $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
311  $handler = $this->createHandler( $request, $match['userData'] );
312 
313  try {
314  return $this->executeHandler( $handler );
315  } catch ( HttpException $e ) {
316  return $this->responseFactory->createFromException( $e );
317  }
318  }
319 
326  private function createHandler( RequestInterface $request, array $spec ): Handler {
327  $objectFactorySpec = array_intersect_key( $spec,
328  [ 'factory' => true, 'class' => true, 'args' => true, 'services' => true ] );
330  $handler = $this->objectFactory->createObject( $objectFactorySpec );
331  $handler->init( $this, $request, $spec, $this->responseFactory, $this->hookContainer );
332 
333  return $handler;
334  }
335 
342  private function executeHandler( $handler ): ResponseInterface {
343  // Check for basic authorization, to avoid leaking data from private wikis
344  $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
345  if ( $authResult ) {
346  return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
347  }
348 
349  // Validate the parameters
350  $handler->validate( $this->restValidator );
351 
352  // Check conditional request headers
353  $earlyResponse = $handler->checkPreconditions();
354  if ( $earlyResponse ) {
355  return $earlyResponse;
356  }
357 
358  // Run the main part of the handler
359  $response = $handler->execute();
360  if ( !( $response instanceof ResponseInterface ) ) {
361  $response = $this->responseFactory->createFromReturnValue( $response );
362  }
363 
364  // Set Last-Modified and ETag headers in the response if available
365  $handler->applyConditionalResponseHeaders( $response );
366 
367  return $response;
368  }
369 }
MediaWiki\Rest\Router\$responseFactory
ResponseFactory $responseFactory
Definition: Router.php:48
MediaWiki\Rest\ResponseFactory
Generates standardized response objects.
Definition: ResponseFactory.php:17
MediaWiki\Rest\Router\$routesFromFiles
array null $routesFromFiles
Definition: Router.php:27
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\Router\fetchCacheData
fetchCacheData()
Get the cache data, or false if it is missing or invalid.
Definition: Router.php:97
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:51
MediaWiki\Rest\Router\$extraRoutes
array $extraRoutes
Definition: Router.php:24
MediaWiki\Rest\Router\$matchers
PathMatcher[] null $matchers
Path matchers by method.
Definition: Router.php:42
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:71
MediaWiki\Rest\Router\$routeFiles
string[] $routeFiles
Definition: Router.php:21
MediaWiki\Rest\Router\createHandler
createHandler(RequestInterface $request, array $spec)
Create a handler from its spec.
Definition: Router.php:326
MediaWiki\Rest\Router\$configHash
string null $configHash
Definition: Router.php:45
MediaWiki\Rest\Router\getRelativePath
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition: Router.php:223
MediaWiki\Rest\Router\getConfigHash
getConfigHash()
Get a config version hash for cache invalidation.
Definition: Router.php:119
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:16
MediaWiki\Rest\Router\execute
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition: Router.php:259
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:438
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:19
MediaWiki\Rest\Router\$baseUrl
string $baseUrl
Definition: Router.php:33
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:242
MediaWiki\Rest\Router\executeHandler
executeHandler( $handler)
Execute a fully-constructed handler.
Definition: Router.php:342
MediaWiki\Rest
MediaWiki\Rest\Router\$cacheBag
BagOStuff $cacheBag
Definition: Router.php:39
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\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:54
MediaWiki\Rest\Router\getAllRoutes
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition: Router.php:171
MediaWiki\Rest\Router\getMatchers
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition: Router.php:183
$path
$path
Definition: NoLocalSettings.php:25
MediaWiki\Rest\Router\getCacheKey
getCacheKey()
Definition: Router.php:110
MediaWiki\Rest\Router\$routeFileTimestamps
int[] null $routeFileTimestamps
Definition: Router.php:30
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
MediaWiki\Rest\Router\__construct
__construct( $routeFiles, $extraRoutes, $baseUrl, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator, HookContainer $hookContainer)
Definition: Router.php:75
MediaWiki\Rest\Router\getRouteFileTimestamps
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition: Router.php:155
MediaWiki\Rest\Router\$rootPath
string $rootPath
Definition: Router.php:36
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:60
MediaWiki\Rest\Router\getRoutesFromFiles
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition: Router.php:134
MediaWiki\Rest\Router\$restValidator
Validator $restValidator
Definition: Router.php:57