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;
11 use Wikimedia\ObjectFactory;
12 
18 class Router {
20  private $routeFiles;
21 
23  private $extraRoutes;
24 
27 
30 
32  private $rootPath;
33 
35  private $cacheBag;
36 
38  private $matchers;
39 
41  private $configHash;
42 
45 
47  private $basicAuth;
48 
50  private $objectFactory;
51 
53  private $restValidator;
54 
69  ) {
70  $this->routeFiles = $routeFiles;
71  $this->extraRoutes = $extraRoutes;
72  $this->rootPath = $rootPath;
73  $this->cacheBag = $cacheBag;
74  $this->responseFactory = $responseFactory;
75  $this->basicAuth = $basicAuth;
76  $this->objectFactory = $objectFactory;
77  $this->restValidator = $restValidator;
78  }
79 
85  private function fetchCacheData() {
86  $cacheData = $this->cacheBag->get( $this->getCacheKey() );
87  if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) {
88  unset( $cacheData['CONFIG-HASH'] );
89  return $cacheData;
90  } else {
91  return false;
92  }
93  }
94 
98  private function getCacheKey() {
99  return $this->cacheBag->makeKey( __CLASS__, '1' );
100  }
101 
107  private function getConfigHash() {
108  if ( $this->configHash === null ) {
109  $this->configHash = md5( json_encode( [
110  $this->extraRoutes,
111  $this->getRouteFileTimestamps()
112  ] ) );
113  }
114  return $this->configHash;
115  }
116 
122  private function getRoutesFromFiles() {
123  if ( $this->routesFromFiles === null ) {
124  $this->routeFileTimestamps = [];
125  foreach ( $this->routeFiles as $fileName ) {
126  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
127  $routes = json_decode( file_get_contents( $fileName ), true );
128  if ( $this->routesFromFiles === null ) {
129  $this->routesFromFiles = $routes;
130  } else {
131  $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
132  }
133  }
134  }
135  return $this->routesFromFiles;
136  }
137 
143  private function getRouteFileTimestamps() {
144  if ( $this->routeFileTimestamps === null ) {
145  $this->routeFileTimestamps = [];
146  foreach ( $this->routeFiles as $fileName ) {
147  $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
148  }
149  }
151  }
152 
159  private function getAllRoutes() {
160  $iterator = new AppendIterator;
161  $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) );
162  $iterator->append( new \ArrayIterator( $this->extraRoutes ) );
163  return $iterator;
164  }
165 
171  private function getMatchers() {
172  if ( $this->matchers === null ) {
173  $cacheData = $this->fetchCacheData();
174  $matchers = [];
175  if ( $cacheData ) {
176  foreach ( $cacheData as $method => $data ) {
177  $matchers[$method] = PathMatcher::newFromCache( $data );
178  }
179  } else {
180  foreach ( $this->getAllRoutes() as $spec ) {
181  $methods = $spec['method'] ?? [ 'GET' ];
182  if ( !is_array( $methods ) ) {
183  $methods = [ $methods ];
184  }
185  foreach ( $methods as $method ) {
186  if ( !isset( $matchers[$method] ) ) {
187  $matchers[$method] = new PathMatcher;
188  }
189  $matchers[$method]->add( $spec['path'], $spec );
190  }
191  }
192 
193  $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ];
194  foreach ( $matchers as $method => $matcher ) {
195  $cacheData[$method] = $matcher->getCacheData();
196  }
197  $this->cacheBag->set( $this->getCacheKey(), $cacheData );
198  }
199  $this->matchers = $matchers;
200  }
201  return $this->matchers;
202  }
203 
211  private function getRelativePath( $path ) {
212  if ( strlen( $this->rootPath ) > strlen( $path ) ||
213  substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0
214  ) {
215  return false;
216  }
217  return substr( $path, strlen( $this->rootPath ) );
218  }
219 
226  public function execute( RequestInterface $request ) {
227  $path = $request->getUri()->getPath();
228  $relPath = $this->getRelativePath( $path );
229  if ( $relPath === false ) {
230  return $this->responseFactory->createLocalizedHttpError( 404,
231  ( new MessageValue( 'rest-prefix-mismatch' ) )
232  ->plaintextParams( $path, $this->rootPath )
233  );
234  }
235 
236  $requestMethod = $request->getMethod();
237  $matchers = $this->getMatchers();
238  $matcher = $matchers[$requestMethod] ?? null;
239  $match = $matcher ? $matcher->match( $relPath ) : null;
240 
241  // For a HEAD request, execute the GET handler instead if one exists.
242  // The webserver will discard the body.
243  if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
244  $match = $matchers['GET']->match( $relPath );
245  }
246 
247  if ( !$match ) {
248  // Check for 405 wrong method
249  $allowed = [];
250  foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
251  if ( $allowedMethod === $requestMethod ) {
252  continue;
253  }
254  if ( $allowedMatcher->match( $relPath ) ) {
255  $allowed[] = $allowedMethod;
256  }
257  }
258  if ( $allowed ) {
259  $response = $this->responseFactory->createLocalizedHttpError( 405,
260  ( new MessageValue( 'rest-wrong-method' ) )
261  ->textParams( $requestMethod )
262  ->commaListParams( $allowed )
263  ->numParams( count( $allowed ) )
264  );
265  $response->setHeader( 'Allow', $allowed );
266  return $response;
267  } else {
268  // Did not match with any other method, must be 404
269  return $this->responseFactory->createLocalizedHttpError( 404,
270  ( new MessageValue( 'rest-no-match' ) )
271  ->plaintextParams( $relPath )
272  );
273  }
274  }
275 
276  $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
277  $spec = $match['userData'];
278  $objectFactorySpec = array_intersect_key( $spec,
279  [ 'factory' => true, 'class' => true, 'args' => true, 'services' => true ] );
281  $handler = $this->objectFactory->createObject( $objectFactorySpec );
282  $handler->init( $this, $request, $spec, $this->responseFactory );
283 
284  try {
285  return $this->executeHandler( $handler );
286  } catch ( HttpException $e ) {
287  return $this->responseFactory->createFromException( $e );
288  }
289  }
290 
297  private function executeHandler( $handler ): ResponseInterface {
298  // Check for basic authorization, to avoid leaking data from private wikis
299  $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
300  if ( $authResult ) {
301  return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
302  }
303 
304  // Validate the parameters
305  $handler->validate( $this->restValidator );
306 
307  // Check conditional request headers
308  $earlyResponse = $handler->checkPreconditions();
309  if ( $earlyResponse ) {
310  return $earlyResponse;
311  }
312 
313  // Run the main part of the handler
314  $response = $handler->execute();
315  if ( !( $response instanceof ResponseInterface ) ) {
316  $response = $this->responseFactory->createFromReturnValue( $response );
317  }
318 
319  // Set Last-Modified and ETag headers in the response if available
320  $handler->applyConditionalResponseHeaders( $response );
321 
322  return $response;
323  }
324 }
MediaWiki\Rest\Router\$responseFactory
ResponseFactory $responseFactory
Definition: Router.php:44
MediaWiki\Rest\ResponseFactory
Generates standardized response objects.
Definition: ResponseFactory.php:18
MediaWiki\Rest\Router\$routesFromFiles
array null $routesFromFiles
Definition: Router.php:26
MediaWiki\Rest\Validator\Validator
Wrapper for ParamValidator.
Definition: Validator.php:30
$response
$response
Definition: opensearch_desc.php:38
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:85
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:47
MediaWiki\Rest\Router\$extraRoutes
array $extraRoutes
Definition: Router.php:23
MediaWiki\Rest\Router\$matchers
PathMatcher[] null $matchers
Path matchers by method.
Definition: Router.php:38
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:63
MediaWiki\Rest\Router\$routeFiles
string[] $routeFiles
Definition: Router.php:20
MediaWiki\Rest\Router\$configHash
string null $configHash
Definition: Router.php:41
MediaWiki\Rest\Router\getRelativePath
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition: Router.php:211
MediaWiki\Rest\Router\getConfigHash
getConfigHash()
Get a config version hash for cache invalidation.
Definition: Router.php:107
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:14
MediaWiki\Rest\Router\execute
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition: Router.php:226
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:18
MediaWiki\Rest\RequestInterface\getMethod
getMethod()
Retrieves the HTTP method of the request.
MediaWiki\Rest\Router\executeHandler
executeHandler( $handler)
Execute a fully-constructed handler.
Definition: Router.php:297
MediaWiki\Rest
MediaWiki\Rest\Router\$cacheBag
BagOStuff $cacheBag
Definition: Router.php:35
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:39
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:10
MediaWiki\Rest\Router\__construct
__construct( $routeFiles, $extraRoutes, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator)
Definition: Router.php:65
MediaWiki\Rest\PathTemplateMatcher\PathMatcher
A tree-based path routing algorithm.
Definition: PathMatcher.php:16
MediaWiki\Rest\Router\$objectFactory
ObjectFactory $objectFactory
Definition: Router.php:50
MediaWiki\Rest\Router\getAllRoutes
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files.
Definition: Router.php:159
MediaWiki\Rest\Router\getMatchers
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition: Router.php:171
$path
$path
Definition: NoLocalSettings.php:25
MediaWiki\Rest\Router\getCacheKey
getCacheKey()
Definition: Router.php:98
MediaWiki\Rest\Router\$routeFileTimestamps
int[] null $routeFileTimestamps
Definition: Router.php:29
MediaWiki\Rest\Router\getRouteFileTimestamps
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition: Router.php:143
MediaWiki\Rest\Router\$rootPath
string $rootPath
Definition: Router.php:32
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\getRoutesFromFiles
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition: Router.php:122
MediaWiki\Rest\Router\$restValidator
Validator $restValidator
Definition: Router.php:53