MediaWiki  master
Router.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest;
4 
6 use BagOStuff;
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  // @phan-suppress-next-line PhanAccessMethodInternal
300  $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
301  if ( $authResult ) {
302  return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
303  }
304 
305  // Validate the parameters
306  $handler->validate( $this->restValidator );
307 
308  // Check conditional request headers
309  $earlyResponse = $handler->checkPreconditions();
310  if ( $earlyResponse ) {
311  return $earlyResponse;
312  }
313 
314  // Run the main part of the handler
315  $response = $handler->execute();
316  if ( !( $response instanceof ResponseInterface ) ) {
317  $response = $this->responseFactory->createFromReturnValue( $response );
318  }
319 
320  // Set Last-Modified and ETag headers in the response if available
321  $handler->applyConditionalResponseHeaders( $response );
322 
323  return $response;
324  }
325 }
$response
setPathParams( $params)
Erase all path parameters from the object and set the parameter array to the one specified.
PathMatcher [] null $matchers
Path matchers by method.
Definition: Router.php:38
string [] $routeFiles
Definition: Router.php:20
This is the base exception class for non-fatal exceptions thrown from REST handlers.
getRouteFileTimestamps()
Get an array of last modification times of the defined route files.
Definition: Router.php:143
Generates standardized response objects.
An interface used by Router to ensure that the client has "basic" access, i.e.
string null $configHash
Definition: Router.php:41
Value object representing a message for i18n.
getRoutesFromFiles()
Load the defined JSON files and return the merged routes.
Definition: Router.php:122
The REST router is responsible for gathering handler configuration, matching an input path and HTTP m...
Definition: Router.php:18
__construct( $routeFiles, $extraRoutes, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, Validator $restValidator)
Definition: Router.php:65
executeHandler( $handler)
Execute a fully-constructed handler.
Definition: Router.php:297
getMethod()
Retrieves the HTTP method of the request.
A tree-based path routing algorithm.
Definition: PathMatcher.php:16
getMatchers()
Get an array of PathMatcher objects indexed by HTTP method.
Definition: Router.php:171
Validator $restValidator
Definition: Router.php:53
execute(RequestInterface $request)
Find the handler for a request and execute it.
Definition: Router.php:226
array null $routesFromFiles
Definition: Router.php:26
getAllRoutes()
Get an iterator for all defined routes, including loading the routes from the JSON files...
Definition: Router.php:159
ResponseFactory $responseFactory
Definition: Router.php:44
BagOStuff $cacheBag
Definition: Router.php:35
A request interface similar to PSR-7&#39;s ServerRequestInterface.
getUri()
Retrieves the URI instance.
BasicAuthorizerInterface $basicAuth
Definition: Router.php:47
static newFromCache( $data)
Create a PathMatcher from cache data.
Definition: PathMatcher.php:42
Wrapper for ParamValidator.
Definition: Validator.php:29
An interface similar to PSR-7&#39;s ResponseInterface, the primary difference being that it is mutable...
fetchCacheData()
Get the cache data, or false if it is missing or invalid.
Definition: Router.php:85
add( $template, $userData)
Add a template to the matcher.
int [] null $routeFileTimestamps
Definition: Router.php:29
match( $path)
Match a path against the current match trees.
getRelativePath( $path)
Remove the path prefix $this->rootPath.
Definition: Router.php:211
getConfigHash()
Get a config version hash for cache invalidation.
Definition: Router.php:107
ObjectFactory $objectFactory
Definition: Router.php:50