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 $objectFactory;
56 
58  private $restValidator;
59 
61  private $hookContainer;
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,
129  $this->getRouteFileTimestamps()
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  }
153  return $this->routesFromFiles;
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  $route = str_replace( '{' . $param . '}', urlencode( $value ), $route );
251  }
252 
253  $url = $this->baseUrl . $this->rootPath . $route;
254  return wfAppendQuery( $url, $queryParams );
255  }
256 
263  public function execute( RequestInterface $request ) {
264  $path = $request->getUri()->getPath();
265  $relPath = $this->getRelativePath( $path );
266  if ( $relPath === false ) {
267  return $this->responseFactory->createLocalizedHttpError( 404,
268  ( new MessageValue( 'rest-prefix-mismatch' ) )
269  ->plaintextParams( $path, $this->rootPath )
270  );
271  }
272 
273  $requestMethod = $request->getMethod();
274  $matchers = $this->getMatchers();
275  $matcher = $matchers[$requestMethod] ?? null;
276  $match = $matcher ? $matcher->match( $relPath ) : null;
277 
278  // For a HEAD request, execute the GET handler instead if one exists.
279  // The webserver will discard the body.
280  if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
281  $match = $matchers['GET']->match( $relPath );
282  }
283 
284  if ( !$match ) {
285  // Check for 405 wrong method
286  $allowed = [];
287  foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
288  if ( $allowedMethod === $requestMethod ) {
289  continue;
290  }
291  if ( $allowedMatcher->match( $relPath ) ) {
292  $allowed[] = $allowedMethod;
293  }
294  }
295  if ( $allowed ) {
296  $response = $this->responseFactory->createLocalizedHttpError( 405,
297  ( new MessageValue( 'rest-wrong-method' ) )
298  ->textParams( $requestMethod )
299  ->commaListParams( $allowed )
300  ->numParams( count( $allowed ) )
301  );
302  $response->setHeader( 'Allow', $allowed );
303  return $response;
304  } else {
305  // Did not match with any other method, must be 404
306  return $this->responseFactory->createLocalizedHttpError( 404,
307  ( new MessageValue( 'rest-no-match' ) )
308  ->plaintextParams( $relPath )
309  );
310  }
311  }
312 
313  $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
314  $handler = $this->createHandler( $request, $match['userData'] );
315 
316  try {
317  return $this->executeHandler( $handler );
318  } catch ( HttpException $e ) {
319  return $this->responseFactory->createFromException( $e );
320  }
321  }
322 
329  private function createHandler( RequestInterface $request, array $spec ): Handler {
330  $objectFactorySpec = array_intersect_key( $spec,
331  [ 'factory' => true, 'class' => true, 'args' => true, 'services' => true ] );
333  $handler = $this->objectFactory->createObject( $objectFactorySpec );
334  $handler->init( $this, $request, $spec, $this->responseFactory, $this->hookContainer );
335 
336  return $handler;
337  }
338 
345  private function executeHandler( $handler ): ResponseInterface {
346  // Check for basic authorization, to avoid leaking data from private wikis
347  $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
348  if ( $authResult ) {
349  return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
350  }
351 
352  // Validate the parameters
353  $handler->validate( $this->restValidator );
354 
355  // Check conditional request headers
356  $earlyResponse = $handler->checkPreconditions();
357  if ( $earlyResponse ) {
358  return $earlyResponse;
359  }
360 
361  // Run the main part of the handler
362  $response = $handler->execute();
363  if ( !( $response instanceof ResponseInterface ) ) {
364  $response = $this->responseFactory->createFromReturnValue( $response );
365  }
366 
367  // Set Last-Modified and ETag headers in the response if available
368  $handler->applyConditionalResponseHeaders( $response );
369 
370  return $response;
371  }
372 }
MediaWiki\Rest\Router\$responseFactory
ResponseFactory $responseFactory
Definition: Router.php:49
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:31
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
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:103
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:70
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:329
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:229
MediaWiki\Rest\Router\getConfigHash
getConfigHash()
Get a config version hash for cache invalidation.
Definition: Router.php:125
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:16
MediaWiki\Rest\Handler
Definition: Handler.php:11
MediaWiki\Rest\Router\execute
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition: Router.php:263
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\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:183
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:248
MediaWiki\Rest\Router\__construct
__construct( $routeFiles, $extraRoutes, $baseUrl, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator, HookContainer $hookContainer=null)
Definition: Router.php:76
MediaWiki\Rest\Router\executeHandler
executeHandler( $handler)
Execute a fully-constructed handler.
Definition: Router.php:345
MediaWiki\Rest
MediaWiki\Rest\Router\$cacheBag
BagOStuff $cacheBag
Definition: Router.php:40
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:55
MediaWiki\Rest\Router\getAllRoutes
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition: Router.php:177
MediaWiki\Rest\Router\getMatchers
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition: Router.php:189
$path
$path
Definition: NoLocalSettings.php:25
MediaWiki\Rest\Router\getCacheKey
getCacheKey()
Definition: Router.php:116
MediaWiki\Rest\Router\$routeFileTimestamps
int[] null $routeFileTimestamps
Definition: Router.php:31
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
MediaWiki\Rest\Router\getRouteFileTimestamps
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition: Router.php:161
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:61
MediaWiki\Rest\Router\getRoutesFromFiles
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition: Router.php:140
MediaWiki\Rest\Router\$restValidator
Validator $restValidator
Definition: Router.php:58