Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.19% |
181 / 210 |
|
80.00% |
24 / 30 |
CRAP | |
0.00% |
0 / 1 |
Router | |
86.19% |
181 / 210 |
|
80.00% |
24 / 30 |
81.54 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
getRelativePath | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
splitPath | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
fetchCachedModuleMap | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
fetchCachedModuleData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
cacheModuleMap | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
cacheModuleData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getModuleDataCacheKey | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getModuleMapCacheKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getModuleMapHash | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
buildModuleMap | |
78.79% |
26 / 33 |
|
0.00% |
0 / 1 |
10.95 | |||
getModuleFileTimestamps | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getModuleMap | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getModuleInfo | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getModuleIds | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getModuleForPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getModule | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
7.06 | |||
getRoutePath | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getRouteUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPrivateRouteUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
substPathParams | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
execute | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
5.73 | |||
doExecute | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
5.16 | |||
prepareHandler | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
setCors | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setStats | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
instantiateModule | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
2 | |||
isRestbaseCompatEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
varyOnRestbaseCompat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRestbaseCompatErrorData | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest; |
4 | |
5 | use MediaWiki\Config\ServiceOptions; |
6 | use MediaWiki\HookContainer\HookContainer; |
7 | use MediaWiki\MainConfigNames; |
8 | use MediaWiki\MainConfigSchema; |
9 | use MediaWiki\Permissions\Authority; |
10 | use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; |
11 | use MediaWiki\Rest\Module\ExtraRoutesModule; |
12 | use MediaWiki\Rest\Module\Module; |
13 | use MediaWiki\Rest\Module\SpecBasedModule; |
14 | use MediaWiki\Rest\PathTemplateMatcher\ModuleConfigurationException; |
15 | use MediaWiki\Rest\Reporter\ErrorReporter; |
16 | use MediaWiki\Rest\Validator\Validator; |
17 | use MediaWiki\Session\Session; |
18 | use Throwable; |
19 | use Wikimedia\Http\HttpStatus; |
20 | use Wikimedia\Message\MessageValue; |
21 | use Wikimedia\ObjectCache\BagOStuff; |
22 | use Wikimedia\ObjectFactory\ObjectFactory; |
23 | use Wikimedia\Stats\StatsFactory; |
24 | |
25 | /** |
26 | * The REST router is responsible for gathering module configuration, matching |
27 | * an input path against the defined modules, and constructing |
28 | * and executing the relevant module for a request. |
29 | */ |
30 | class Router { |
31 | private const PREFIX_PATTERN = '!^/([-_.\w]+(?:/v\d+)?)(/.*)$!'; |
32 | |
33 | /** @var string[] */ |
34 | private $routeFiles; |
35 | |
36 | /** @var array[] */ |
37 | private $extraRoutes; |
38 | |
39 | /** @var null|array[] */ |
40 | private $moduleMap = null; |
41 | |
42 | /** @var Module[] */ |
43 | private $modules = []; |
44 | |
45 | /** @var int[]|null */ |
46 | private $moduleFileTimestamps = null; |
47 | |
48 | /** @var string */ |
49 | private $baseUrl; |
50 | |
51 | /** @var string */ |
52 | private $privateBaseUrl; |
53 | |
54 | /** @var string */ |
55 | private $rootPath; |
56 | |
57 | /** @var string */ |
58 | private $scriptPath; |
59 | |
60 | /** @var string|null */ |
61 | private $configHash = null; |
62 | |
63 | /** @var CorsUtils|null */ |
64 | private $cors; |
65 | |
66 | private BagOStuff $cacheBag; |
67 | private ResponseFactory $responseFactory; |
68 | private BasicAuthorizerInterface $basicAuth; |
69 | private Authority $authority; |
70 | private ObjectFactory $objectFactory; |
71 | private Validator $restValidator; |
72 | private ErrorReporter $errorReporter; |
73 | private HookContainer $hookContainer; |
74 | private Session $session; |
75 | |
76 | /** @var ?StatsFactory */ |
77 | private $stats = null; |
78 | |
79 | /** |
80 | * @internal |
81 | */ |
82 | public const CONSTRUCTOR_OPTIONS = [ |
83 | MainConfigNames::CanonicalServer, |
84 | MainConfigNames::InternalServer, |
85 | MainConfigNames::RestPath, |
86 | MainConfigNames::ScriptPath, |
87 | ]; |
88 | |
89 | /** |
90 | * @param string[] $routeFiles |
91 | * @param array[] $extraRoutes |
92 | * @param ServiceOptions $options |
93 | * @param BagOStuff $cacheBag A cache in which to store the matcher trees |
94 | * @param ResponseFactory $responseFactory |
95 | * @param BasicAuthorizerInterface $basicAuth |
96 | * @param Authority $authority |
97 | * @param ObjectFactory $objectFactory |
98 | * @param Validator $restValidator |
99 | * @param ErrorReporter $errorReporter |
100 | * @param HookContainer $hookContainer |
101 | * @param Session $session |
102 | * @internal |
103 | */ |
104 | public function __construct( |
105 | array $routeFiles, |
106 | array $extraRoutes, |
107 | ServiceOptions $options, |
108 | BagOStuff $cacheBag, |
109 | ResponseFactory $responseFactory, |
110 | BasicAuthorizerInterface $basicAuth, |
111 | Authority $authority, |
112 | ObjectFactory $objectFactory, |
113 | Validator $restValidator, |
114 | ErrorReporter $errorReporter, |
115 | HookContainer $hookContainer, |
116 | Session $session |
117 | ) { |
118 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
119 | |
120 | $this->routeFiles = $routeFiles; |
121 | $this->extraRoutes = $extraRoutes; |
122 | $this->baseUrl = $options->get( MainConfigNames::CanonicalServer ); |
123 | $this->privateBaseUrl = $options->get( MainConfigNames::InternalServer ); |
124 | $this->rootPath = $options->get( MainConfigNames::RestPath ); |
125 | $this->scriptPath = $options->get( MainConfigNames::ScriptPath ); |
126 | $this->cacheBag = $cacheBag; |
127 | $this->responseFactory = $responseFactory; |
128 | $this->basicAuth = $basicAuth; |
129 | $this->authority = $authority; |
130 | $this->objectFactory = $objectFactory; |
131 | $this->restValidator = $restValidator; |
132 | $this->errorReporter = $errorReporter; |
133 | $this->hookContainer = $hookContainer; |
134 | $this->session = $session; |
135 | } |
136 | |
137 | /** |
138 | * Remove the REST path prefix. Return the part of the path with the |
139 | * prefix removed, or false if the prefix did not match. |
140 | * Both the $this->rootPath and the default REST path are accepted, |
141 | * so on a site that uses /api as the RestPath, requests to /w/rest.php |
142 | * still work. This is equivalent to supporting both /wiki and /w/index.php |
143 | * for page views. |
144 | * |
145 | * @param string $path |
146 | * @return false|string |
147 | */ |
148 | private function getRelativePath( $path ) { |
149 | $allowed = [ |
150 | $this->rootPath, |
151 | MainConfigSchema::getDefaultRestPath( $this->scriptPath ) |
152 | ]; |
153 | |
154 | foreach ( $allowed as $prefix ) { |
155 | if ( str_starts_with( $path, $prefix ) ) { |
156 | return substr( $path, strlen( $prefix ) ); |
157 | } |
158 | } |
159 | |
160 | return false; |
161 | } |
162 | |
163 | /** |
164 | * @param string $fullPath |
165 | * |
166 | * @return string[] [ string $module, string $path ] |
167 | */ |
168 | private function splitPath( string $fullPath ): array { |
169 | $pathWithModule = $this->getRelativePath( $fullPath ); |
170 | |
171 | if ( $pathWithModule === false ) { |
172 | throw new LocalizedHttpException( |
173 | ( new MessageValue( 'rest-prefix-mismatch' ) ) |
174 | ->plaintextParams( $fullPath, $this->rootPath ), |
175 | 404 |
176 | ); |
177 | } |
178 | |
179 | if ( preg_match( self::PREFIX_PATTERN, $pathWithModule, $matches ) ) { |
180 | [ , $module, $pathUnderModule ] = $matches; |
181 | } else { |
182 | // No prefix found in the given path, assume prefix-less module. |
183 | $module = ''; |
184 | $pathUnderModule = $pathWithModule; |
185 | } |
186 | |
187 | if ( $module !== '' && !$this->getModuleInfo( $module ) ) { |
188 | // Prefix doesn't match any module, try the prefix-less module... |
189 | // TODO: At some point in the future, we'll want to warn and redirect... |
190 | $module = ''; |
191 | $pathUnderModule = $pathWithModule; |
192 | } |
193 | |
194 | return [ $module, $pathUnderModule ]; |
195 | } |
196 | |
197 | /** |
198 | * Get the cache data, or false if it is missing or invalid |
199 | */ |
200 | private function fetchCachedModuleMap(): ?array { |
201 | $moduleMapCacheKey = $this->getModuleMapCacheKey(); |
202 | $cacheData = $this->cacheBag->get( $moduleMapCacheKey ); |
203 | if ( $cacheData && $cacheData[Module::CACHE_CONFIG_HASH_KEY] === $this->getModuleMapHash() ) { |
204 | unset( $cacheData[Module::CACHE_CONFIG_HASH_KEY] ); |
205 | return $cacheData; |
206 | } else { |
207 | return null; |
208 | } |
209 | } |
210 | |
211 | private function fetchCachedModuleData( string $module ): ?array { |
212 | $moduleDataCacheKey = $this->getModuleDataCacheKey( $module ); |
213 | $cacheData = $this->cacheBag->get( $moduleDataCacheKey ); |
214 | return $cacheData ?: null; |
215 | } |
216 | |
217 | private function cacheModuleMap( array $map ) { |
218 | $map[Module::CACHE_CONFIG_HASH_KEY] = $this->getModuleMapHash(); |
219 | $moduleMapCacheKey = $this->getModuleMapCacheKey(); |
220 | $this->cacheBag->set( $moduleMapCacheKey, $map ); |
221 | } |
222 | |
223 | private function cacheModuleData( string $module, array $map ) { |
224 | $moduleDataCacheKey = $this->getModuleDataCacheKey( $module ); |
225 | $this->cacheBag->set( $moduleDataCacheKey, $map ); |
226 | } |
227 | |
228 | private function getModuleDataCacheKey( string $module ): string { |
229 | if ( $module === '' ) { |
230 | // Proper key for the prefix-less module. |
231 | $module = '-'; |
232 | } |
233 | return $this->cacheBag->makeKey( __CLASS__, 'module', $module ); |
234 | } |
235 | |
236 | private function getModuleMapCacheKey(): string { |
237 | return $this->cacheBag->makeKey( __CLASS__, 'map', '1' ); |
238 | } |
239 | |
240 | /** |
241 | * Get a config version hash for cache invalidation |
242 | */ |
243 | private function getModuleMapHash(): string { |
244 | if ( $this->configHash === null ) { |
245 | $this->configHash = md5( json_encode( [ |
246 | $this->extraRoutes, |
247 | $this->getModuleFileTimestamps() |
248 | ] ) ); |
249 | } |
250 | return $this->configHash; |
251 | } |
252 | |
253 | private function buildModuleMap(): array { |
254 | $modules = []; |
255 | $noPrefixFiles = []; |
256 | $id = ''; // should not be used, make Phan happy |
257 | |
258 | foreach ( $this->routeFiles as $file ) { |
259 | // NOTE: we end up loading the file here (for the meta-data) as well |
260 | // as in the Module object (for the routes). But since we have |
261 | // caching on both levels, that shouldn't matter. |
262 | $spec = Module::loadJsonFile( $file ); |
263 | |
264 | if ( isset( $spec['mwapi'] ) || isset( $spec['moduleId'] ) || isset( $spec['routes'] ) ) { |
265 | // OpenAPI 3, with some extras like the "module" field |
266 | if ( !isset( $spec['moduleId'] ) ) { |
267 | throw new ModuleConfigurationException( |
268 | "Missing 'moduleId' field in $file" |
269 | ); |
270 | } |
271 | |
272 | $id = $spec['moduleId']; |
273 | |
274 | $moduleInfo = [ |
275 | 'class' => SpecBasedModule::class, |
276 | 'pathPrefix' => $id, |
277 | 'specFile' => $file |
278 | ]; |
279 | } else { |
280 | // Old-style route file containing a flat list of routes. |
281 | $noPrefixFiles[] = $file; |
282 | $moduleInfo = null; |
283 | } |
284 | |
285 | if ( $moduleInfo ) { |
286 | if ( isset( $modules[$id] ) ) { |
287 | $otherFiles = implode( ' and ', $modules[$id]['routeFiles'] ); |
288 | throw new ModuleConfigurationException( |
289 | "Duplicate module $id in $file, also used in $otherFiles" |
290 | ); |
291 | } |
292 | |
293 | $modules[$id] = $moduleInfo; |
294 | } |
295 | } |
296 | |
297 | // The prefix-less module will be used when no prefix is matched. |
298 | // It provides a mechanism to integrate extra routes and route files |
299 | // registered by extensions. |
300 | if ( $noPrefixFiles || $this->extraRoutes ) { |
301 | $modules[''] = [ |
302 | 'class' => ExtraRoutesModule::class, |
303 | 'pathPrefix' => '', |
304 | 'routeFiles' => $noPrefixFiles, |
305 | 'extraRoutes' => $this->extraRoutes, |
306 | ]; |
307 | } |
308 | |
309 | return $modules; |
310 | } |
311 | |
312 | /** |
313 | * Get an array of last modification times of the defined route files. |
314 | * |
315 | * @return int[] Last modification times |
316 | */ |
317 | private function getModuleFileTimestamps() { |
318 | if ( $this->moduleFileTimestamps === null ) { |
319 | $this->moduleFileTimestamps = []; |
320 | foreach ( $this->routeFiles as $fileName ) { |
321 | $this->moduleFileTimestamps[$fileName] = filemtime( $fileName ); |
322 | } |
323 | } |
324 | return $this->moduleFileTimestamps; |
325 | } |
326 | |
327 | private function getModuleMap(): array { |
328 | if ( !$this->moduleMap ) { |
329 | $map = $this->fetchCachedModuleMap(); |
330 | |
331 | if ( !$map ) { |
332 | $map = $this->buildModuleMap(); |
333 | $this->cacheModuleMap( $map ); |
334 | } |
335 | |
336 | $this->moduleMap = $map; |
337 | } |
338 | return $this->moduleMap; |
339 | } |
340 | |
341 | private function getModuleInfo( string $module ): ?array { |
342 | $map = $this->getModuleMap(); |
343 | return $map[$module] ?? null; |
344 | } |
345 | |
346 | /** |
347 | * @return string[] |
348 | */ |
349 | public function getModuleIds(): array { |
350 | return array_keys( $this->getModuleMap() ); |
351 | } |
352 | |
353 | public function getModuleForPath( string $fullPath ): ?Module { |
354 | [ $moduleName, ] = $this->splitPath( $fullPath ); |
355 | return $this->getModule( $moduleName ); |
356 | } |
357 | |
358 | public function getModule( string $name ): ?Module { |
359 | if ( isset( $this->modules[$name] ) ) { |
360 | return $this->modules[$name]; |
361 | } |
362 | |
363 | $info = $this->getModuleInfo( $name ); |
364 | |
365 | if ( !$info ) { |
366 | return null; |
367 | } |
368 | |
369 | $module = $this->instantiateModule( $info, $name ); |
370 | |
371 | $cacheData = $this->fetchCachedModuleData( $name ); |
372 | |
373 | if ( $cacheData !== null ) { |
374 | $cacheOk = $module->initFromCacheData( $cacheData ); |
375 | } else { |
376 | $cacheOk = false; |
377 | } |
378 | |
379 | if ( !$cacheOk ) { |
380 | $cacheData = $module->getCacheData(); |
381 | $this->cacheModuleData( $name, $cacheData ); |
382 | } |
383 | |
384 | if ( $this->cors ) { |
385 | $module->setCors( $this->cors ); |
386 | } |
387 | |
388 | if ( $this->stats ) { |
389 | $module->setStats( $this->stats ); |
390 | } |
391 | |
392 | $this->modules[$name] = $module; |
393 | return $module; |
394 | } |
395 | |
396 | /** |
397 | * @since 1.42 |
398 | */ |
399 | public function getRoutePath( |
400 | string $routeWithModulePrefix, |
401 | array $pathParams = [], |
402 | array $queryParams = [] |
403 | ): string { |
404 | $routeWithModulePrefix = $this->substPathParams( $routeWithModulePrefix, $pathParams ); |
405 | $path = $this->rootPath . $routeWithModulePrefix; |
406 | return wfAppendQuery( $path, $queryParams ); |
407 | } |
408 | |
409 | public function getRouteUrl( |
410 | string $routeWithModulePrefix, |
411 | array $pathParams = [], |
412 | array $queryParams = [] |
413 | ): string { |
414 | return $this->baseUrl . $this->getRoutePath( $routeWithModulePrefix, $pathParams, $queryParams ); |
415 | } |
416 | |
417 | public function getPrivateRouteUrl( |
418 | string $routeWithModulePrefix, |
419 | array $pathParams = [], |
420 | array $queryParams = [] |
421 | ): string { |
422 | return $this->privateBaseUrl . $this->getRoutePath( $routeWithModulePrefix, $pathParams, $queryParams ); |
423 | } |
424 | |
425 | /** |
426 | * @param string $route |
427 | * @param array $pathParams |
428 | * |
429 | * @return string |
430 | */ |
431 | protected function substPathParams( string $route, array $pathParams ): string { |
432 | foreach ( $pathParams as $param => $value ) { |
433 | // NOTE: we use rawurlencode here, since execute() uses rawurldecode(). |
434 | // Spaces in path params must be encoded to %20 (not +). |
435 | // Slashes must be encoded as %2F. |
436 | $route = str_replace( '{' . $param . '}', rawurlencode( (string)$value ), $route ); |
437 | } |
438 | return $route; |
439 | } |
440 | |
441 | public function execute( RequestInterface $request ): ResponseInterface { |
442 | try { |
443 | $fullPath = $request->getUri()->getPath(); |
444 | $response = $this->doExecute( $fullPath, $request ); |
445 | } catch ( HttpException $e ) { |
446 | $extraData = []; |
447 | if ( $this->isRestbaseCompatEnabled( $request ) |
448 | && $e instanceof LocalizedHttpException |
449 | ) { |
450 | $extraData = $this->getRestbaseCompatErrorData( $request, $e ); |
451 | } |
452 | $response = $this->responseFactory->createFromException( $e, $extraData ); |
453 | } catch ( Throwable $e ) { |
454 | $this->errorReporter->reportError( $e, null, $request ); |
455 | $response = $this->responseFactory->createFromException( $e ); |
456 | } |
457 | |
458 | // TODO: Only send the vary header for handlers that opt into |
459 | // restbase compat! |
460 | $this->varyOnRestbaseCompat( $response ); |
461 | |
462 | return $response; |
463 | } |
464 | |
465 | private function doExecute( string $fullPath, RequestInterface $request ): ResponseInterface { |
466 | [ $modulePrefix, $path ] = $this->splitPath( $fullPath ); |
467 | |
468 | // If there is no path at all, redirect to "/". |
469 | // That's the minimal path that can be routed. |
470 | if ( $modulePrefix === '' && $path === '' ) { |
471 | $target = $this->getRoutePath( '/' ); |
472 | return $this->responseFactory->createRedirect( $target, 308 ); |
473 | } |
474 | |
475 | $module = $this->getModule( $modulePrefix ); |
476 | |
477 | if ( !$module ) { |
478 | throw new LocalizedHttpException( |
479 | MessageValue::new( 'rest-unknown-module' )->plaintextParams( $modulePrefix ), |
480 | 404, |
481 | [ 'prefix' => $modulePrefix ] |
482 | ); |
483 | } |
484 | |
485 | return $module->execute( $path, $request ); |
486 | } |
487 | |
488 | /** |
489 | * Prepare the handler by injecting relevant service objects and state |
490 | * into $handler. |
491 | * |
492 | * @internal |
493 | */ |
494 | public function prepareHandler( Handler $handler ) { |
495 | // Injecting services in the Router class means we don't have to inject |
496 | // them into each Module. |
497 | $handler->initServices( |
498 | $this->authority, |
499 | $this->responseFactory, |
500 | $this->hookContainer |
501 | ); |
502 | |
503 | $handler->initSession( $this->session ); |
504 | } |
505 | |
506 | public function setCors( CorsUtils $cors ): self { |
507 | $this->cors = $cors; |
508 | |
509 | return $this; |
510 | } |
511 | |
512 | /** |
513 | * @internal |
514 | * |
515 | * @param StatsFactory $stats |
516 | * |
517 | * @return self |
518 | */ |
519 | public function setStats( StatsFactory $stats ): self { |
520 | $this->stats = $stats; |
521 | |
522 | return $this; |
523 | } |
524 | |
525 | private function instantiateModule( array $info, string $name ): Module { |
526 | if ( $info['class'] === SpecBasedModule::class ) { |
527 | $module = new SpecBasedModule( |
528 | $info['specFile'], |
529 | $this, |
530 | $info['pathPrefix'] ?? $name, |
531 | $this->responseFactory, |
532 | $this->basicAuth, |
533 | $this->objectFactory, |
534 | $this->restValidator, |
535 | $this->errorReporter, |
536 | $this->hookContainer |
537 | ); |
538 | } else { |
539 | $module = new ExtraRoutesModule( |
540 | $info['routeFiles'] ?? [], |
541 | $info['extraRoutes'] ?? [], |
542 | $this, |
543 | $this->responseFactory, |
544 | $this->basicAuth, |
545 | $this->objectFactory, |
546 | $this->restValidator, |
547 | $this->errorReporter, |
548 | $this->hookContainer |
549 | ); |
550 | } |
551 | |
552 | return $module; |
553 | } |
554 | |
555 | /** |
556 | * @internal |
557 | * |
558 | * @return bool |
559 | */ |
560 | public function isRestbaseCompatEnabled( RequestInterface $request ): bool { |
561 | // See T374136 |
562 | return $request->getHeaderLine( 'x-restbase-compat' ) === 'true'; |
563 | } |
564 | |
565 | private function varyOnRestbaseCompat( ResponseInterface $response ) { |
566 | // See T374136 |
567 | $response->addHeader( 'Vary', 'x-restbase-compat' ); |
568 | } |
569 | |
570 | /** |
571 | * @internal |
572 | * |
573 | * @return array |
574 | */ |
575 | public function getRestbaseCompatErrorData( RequestInterface $request, LocalizedHttpException $e ): array { |
576 | $msg = $e->getMessageValue(); |
577 | |
578 | // Match error fields emitted by the RESTBase endpoints. |
579 | // EntryPoint::getTextFormatters() ensures 'en' is always available. |
580 | return [ |
581 | 'type' => "MediaWikiError/" . |
582 | str_replace( ' ', '_', HttpStatus::getMessage( $e->getCode() ) ), |
583 | 'title' => $msg->getKey(), |
584 | 'method' => strtolower( $request->getMethod() ), |
585 | 'detail' => $this->responseFactory->getFormattedMessage( $msg, 'en' ), |
586 | 'uri' => (string)$request->getUri() |
587 | ]; |
588 | } |
589 | } |