Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.06% |
179 / 208 |
|
80.00% |
24 / 30 |
CRAP | |
0.00% |
0 / 1 |
Router | |
86.06% |
179 / 208 |
|
80.00% |
24 / 30 |
81.90 | |
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% |
22 / 22 |
|
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 HttpStatus; |
6 | use MediaWiki\Config\ServiceOptions; |
7 | use MediaWiki\HookContainer\HookContainer; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\MainConfigSchema; |
10 | use MediaWiki\Permissions\Authority; |
11 | use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; |
12 | use MediaWiki\Rest\Module\ExtraRoutesModule; |
13 | use MediaWiki\Rest\Module\Module; |
14 | use MediaWiki\Rest\Module\SpecBasedModule; |
15 | use MediaWiki\Rest\PathTemplateMatcher\ModuleConfigurationException; |
16 | use MediaWiki\Rest\Reporter\ErrorReporter; |
17 | use MediaWiki\Rest\Validator\Validator; |
18 | use MediaWiki\Session\Session; |
19 | use Throwable; |
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 | * @return ?array |
201 | */ |
202 | private function fetchCachedModuleMap(): ?array { |
203 | $moduleMapCacheKey = $this->getModuleMapCacheKey(); |
204 | $cacheData = $this->cacheBag->get( $moduleMapCacheKey ); |
205 | if ( $cacheData && $cacheData[Module::CACHE_CONFIG_HASH_KEY] === $this->getModuleMapHash() ) { |
206 | unset( $cacheData[Module::CACHE_CONFIG_HASH_KEY] ); |
207 | return $cacheData; |
208 | } else { |
209 | return null; |
210 | } |
211 | } |
212 | |
213 | private function fetchCachedModuleData( string $module ): ?array { |
214 | $moduleDataCacheKey = $this->getModuleDataCacheKey( $module ); |
215 | $cacheData = $this->cacheBag->get( $moduleDataCacheKey ); |
216 | return $cacheData ?: null; |
217 | } |
218 | |
219 | private function cacheModuleMap( array $map ) { |
220 | $map[Module::CACHE_CONFIG_HASH_KEY] = $this->getModuleMapHash(); |
221 | $moduleMapCacheKey = $this->getModuleMapCacheKey(); |
222 | $this->cacheBag->set( $moduleMapCacheKey, $map ); |
223 | } |
224 | |
225 | private function cacheModuleData( string $module, array $map ) { |
226 | $moduleDataCacheKey = $this->getModuleDataCacheKey( $module ); |
227 | $this->cacheBag->set( $moduleDataCacheKey, $map ); |
228 | } |
229 | |
230 | private function getModuleDataCacheKey( string $module ): string { |
231 | if ( $module === '' ) { |
232 | // Proper key for the prefix-less module. |
233 | $module = '-'; |
234 | } |
235 | return $this->cacheBag->makeKey( __CLASS__, 'module', $module ); |
236 | } |
237 | |
238 | private function getModuleMapCacheKey(): string { |
239 | return $this->cacheBag->makeKey( __CLASS__, 'map', '1' ); |
240 | } |
241 | |
242 | /** |
243 | * Get a config version hash for cache invalidation |
244 | */ |
245 | private function getModuleMapHash(): string { |
246 | if ( $this->configHash === null ) { |
247 | $this->configHash = md5( json_encode( [ |
248 | $this->extraRoutes, |
249 | $this->getModuleFileTimestamps() |
250 | ] ) ); |
251 | } |
252 | return $this->configHash; |
253 | } |
254 | |
255 | private function buildModuleMap(): array { |
256 | $modules = []; |
257 | $noPrefixFiles = []; |
258 | $id = ''; // should not be used, make Phan happy |
259 | |
260 | foreach ( $this->routeFiles as $file ) { |
261 | // NOTE: we end up loading the file here (for the meta-data) as well |
262 | // as in the Module object (for the routes). But since we have |
263 | // caching on both levels, that shouldn't matter. |
264 | $spec = Module::loadJsonFile( $file ); |
265 | |
266 | if ( isset( $spec['mwapi'] ) || isset( $spec['moduleId'] ) || isset( $spec['routes'] ) ) { |
267 | // OpenAPI 3, with some extras like the "module" field |
268 | if ( !isset( $spec['moduleId'] ) ) { |
269 | throw new ModuleConfigurationException( |
270 | "Missing 'moduleId' field in $file" |
271 | ); |
272 | } |
273 | |
274 | $id = $spec['moduleId']; |
275 | |
276 | $moduleInfo = [ |
277 | 'class' => SpecBasedModule::class, |
278 | 'pathPrefix' => $id, |
279 | 'specFile' => $file |
280 | ]; |
281 | } else { |
282 | // Old-style route file containing a flat list of routes. |
283 | $noPrefixFiles[] = $file; |
284 | $moduleInfo = null; |
285 | } |
286 | |
287 | if ( $moduleInfo ) { |
288 | if ( isset( $modules[$id] ) ) { |
289 | $otherFiles = implode( ' and ', $modules[$id]['routeFiles'] ); |
290 | throw new ModuleConfigurationException( |
291 | "Duplicate module $id in $file, also used in $otherFiles" |
292 | ); |
293 | } |
294 | |
295 | $modules[$id] = $moduleInfo; |
296 | } |
297 | } |
298 | |
299 | // The prefix-less module will be used when no prefix is matched. |
300 | // It provides a mechanism to integrate extra routes and route files |
301 | // registered by extensions. |
302 | if ( $noPrefixFiles || $this->extraRoutes ) { |
303 | $modules[''] = [ |
304 | 'class' => ExtraRoutesModule::class, |
305 | 'pathPrefix' => '', |
306 | 'routeFiles' => $noPrefixFiles, |
307 | 'extraRoutes' => $this->extraRoutes, |
308 | ]; |
309 | } |
310 | |
311 | return $modules; |
312 | } |
313 | |
314 | /** |
315 | * Get an array of last modification times of the defined route files. |
316 | * |
317 | * @return int[] Last modification times |
318 | */ |
319 | private function getModuleFileTimestamps() { |
320 | if ( $this->moduleFileTimestamps === null ) { |
321 | $this->moduleFileTimestamps = []; |
322 | foreach ( $this->routeFiles as $fileName ) { |
323 | $this->moduleFileTimestamps[$fileName] = filemtime( $fileName ); |
324 | } |
325 | } |
326 | return $this->moduleFileTimestamps; |
327 | } |
328 | |
329 | private function getModuleMap(): array { |
330 | if ( !$this->moduleMap ) { |
331 | $map = $this->fetchCachedModuleMap(); |
332 | |
333 | if ( !$map ) { |
334 | $map = $this->buildModuleMap(); |
335 | $this->cacheModuleMap( $map ); |
336 | } |
337 | |
338 | $this->moduleMap = $map; |
339 | } |
340 | return $this->moduleMap; |
341 | } |
342 | |
343 | private function getModuleInfo( $module ): ?array { |
344 | $map = $this->getModuleMap(); |
345 | return $map[$module] ?? null; |
346 | } |
347 | |
348 | /** |
349 | * @return string[] |
350 | */ |
351 | public function getModuleIds(): array { |
352 | return array_keys( $this->getModuleMap() ); |
353 | } |
354 | |
355 | public function getModuleForPath( string $fullPath ): ?Module { |
356 | [ $moduleName, ] = $this->splitPath( $fullPath ); |
357 | return $this->getModule( $moduleName ); |
358 | } |
359 | |
360 | public function getModule( string $name ): ?Module { |
361 | if ( isset( $this->modules[$name] ) ) { |
362 | return $this->modules[$name]; |
363 | } |
364 | |
365 | $info = $this->getModuleInfo( $name ); |
366 | |
367 | if ( !$info ) { |
368 | return null; |
369 | } |
370 | |
371 | $module = $this->instantiateModule( $info, $name ); |
372 | |
373 | $cacheData = $this->fetchCachedModuleData( $name ); |
374 | |
375 | if ( $cacheData !== null ) { |
376 | $cacheOk = $module->initFromCacheData( $cacheData ); |
377 | } else { |
378 | $cacheOk = false; |
379 | } |
380 | |
381 | if ( !$cacheOk ) { |
382 | $cacheData = $module->getCacheData(); |
383 | $this->cacheModuleData( $name, $cacheData ); |
384 | } |
385 | |
386 | if ( $this->cors ) { |
387 | $module->setCors( $this->cors ); |
388 | } |
389 | |
390 | if ( $this->stats ) { |
391 | $module->setStats( $this->stats ); |
392 | } |
393 | |
394 | $this->modules[$name] = $module; |
395 | return $module; |
396 | } |
397 | |
398 | /** |
399 | * @since 1.42 |
400 | */ |
401 | public function getRoutePath( |
402 | string $routeWithModulePrefix, |
403 | array $pathParams = [], |
404 | array $queryParams = [] |
405 | ): string { |
406 | $routeWithModulePrefix = $this->substPathParams( $routeWithModulePrefix, $pathParams ); |
407 | $path = $this->rootPath . $routeWithModulePrefix; |
408 | return wfAppendQuery( $path, $queryParams ); |
409 | } |
410 | |
411 | public function getRouteUrl( |
412 | string $routeWithModulePrefix, |
413 | array $pathParams = [], |
414 | array $queryParams = [] |
415 | ): string { |
416 | return $this->baseUrl . $this->getRoutePath( $routeWithModulePrefix, $pathParams, $queryParams ); |
417 | } |
418 | |
419 | public function getPrivateRouteUrl( |
420 | string $routeWithModulePrefix, |
421 | array $pathParams = [], |
422 | array $queryParams = [] |
423 | ): string { |
424 | return $this->privateBaseUrl . $this->getRoutePath( $routeWithModulePrefix, $pathParams, $queryParams ); |
425 | } |
426 | |
427 | /** |
428 | * @param string $route |
429 | * @param array $pathParams |
430 | * |
431 | * @return string |
432 | */ |
433 | protected function substPathParams( string $route, array $pathParams ): string { |
434 | foreach ( $pathParams as $param => $value ) { |
435 | // NOTE: we use rawurlencode here, since execute() uses rawurldecode(). |
436 | // Spaces in path params must be encoded to %20 (not +). |
437 | // Slashes must be encoded as %2F. |
438 | $route = str_replace( '{' . $param . '}', rawurlencode( (string)$value ), $route ); |
439 | } |
440 | return $route; |
441 | } |
442 | |
443 | public function execute( RequestInterface $request ): ResponseInterface { |
444 | try { |
445 | $fullPath = $request->getUri()->getPath(); |
446 | $response = $this->doExecute( $fullPath, $request ); |
447 | } catch ( HttpException $e ) { |
448 | $extraData = []; |
449 | if ( $this->isRestbaseCompatEnabled( $request ) |
450 | && $e instanceof LocalizedHttpException |
451 | ) { |
452 | $extraData = $this->getRestbaseCompatErrorData( $request, $e ); |
453 | } |
454 | $response = $this->responseFactory->createFromException( $e, $extraData ); |
455 | } catch ( Throwable $e ) { |
456 | $this->errorReporter->reportError( $e, null, $request ); |
457 | $response = $this->responseFactory->createFromException( $e ); |
458 | } |
459 | |
460 | // TODO: Only send the vary header for handlers that opt into |
461 | // restbase compat! |
462 | $this->varyOnRestbaseCompat( $response ); |
463 | |
464 | return $response; |
465 | } |
466 | |
467 | private function doExecute( string $fullPath, RequestInterface $request ): ResponseInterface { |
468 | [ $modulePrefix, $path ] = $this->splitPath( $fullPath ); |
469 | |
470 | // If there is no path at all, redirect to "/". |
471 | // That's the minimal path that can be routed. |
472 | if ( $modulePrefix === '' && $path === '' ) { |
473 | $target = $this->getRoutePath( '/' ); |
474 | return $this->responseFactory->createRedirect( $target, 308 ); |
475 | } |
476 | |
477 | $module = $this->getModule( $modulePrefix ); |
478 | |
479 | if ( !$module ) { |
480 | throw new LocalizedHttpException( |
481 | MessageValue::new( 'rest-unknown-module' )->plaintextParams( $modulePrefix ), |
482 | 404, |
483 | [ 'prefix' => $modulePrefix ] |
484 | ); |
485 | } |
486 | |
487 | return $module->execute( $path, $request ); |
488 | } |
489 | |
490 | /** |
491 | * Prepare the handler by injecting relevant service objects and state |
492 | * into $handler. |
493 | * |
494 | * @internal |
495 | */ |
496 | public function prepareHandler( Handler $handler ) { |
497 | // Injecting services in the Router class means we don't have to inject |
498 | // them into each Module. |
499 | $handler->initServices( |
500 | $this->authority, |
501 | $this->responseFactory, |
502 | $this->hookContainer |
503 | ); |
504 | |
505 | $handler->initSession( $this->session ); |
506 | } |
507 | |
508 | /** |
509 | * @param CorsUtils $cors |
510 | * @return self |
511 | */ |
512 | public function setCors( CorsUtils $cors ): self { |
513 | $this->cors = $cors; |
514 | |
515 | return $this; |
516 | } |
517 | |
518 | /** |
519 | * @internal |
520 | * |
521 | * @param StatsFactory $stats |
522 | * |
523 | * @return self |
524 | */ |
525 | public function setStats( StatsFactory $stats ): self { |
526 | $this->stats = $stats; |
527 | |
528 | return $this; |
529 | } |
530 | |
531 | /** |
532 | * @param array $info |
533 | * @param string $name |
534 | */ |
535 | private function instantiateModule( array $info, string $name ): Module { |
536 | if ( $info['class'] === SpecBasedModule::class ) { |
537 | $module = new SpecBasedModule( |
538 | $info['specFile'], |
539 | $this, |
540 | $info['pathPrefix'] ?? $name, |
541 | $this->responseFactory, |
542 | $this->basicAuth, |
543 | $this->objectFactory, |
544 | $this->restValidator, |
545 | $this->errorReporter |
546 | ); |
547 | } else { |
548 | $module = new ExtraRoutesModule( |
549 | $info['routeFiles'] ?? [], |
550 | $info['extraRoutes'] ?? [], |
551 | $this, |
552 | $this->responseFactory, |
553 | $this->basicAuth, |
554 | $this->objectFactory, |
555 | $this->restValidator, |
556 | $this->errorReporter |
557 | ); |
558 | } |
559 | |
560 | return $module; |
561 | } |
562 | |
563 | /** |
564 | * @internal |
565 | * |
566 | * @return bool |
567 | */ |
568 | public function isRestbaseCompatEnabled( RequestInterface $request ): bool { |
569 | // See T374136 |
570 | return $request->getHeaderLine( 'x-restbase-compat' ) === 'true'; |
571 | } |
572 | |
573 | private function varyOnRestbaseCompat( ResponseInterface $response ) { |
574 | // See T374136 |
575 | $response->addHeader( 'Vary', 'x-restbase-compat' ); |
576 | } |
577 | |
578 | /** |
579 | * @internal |
580 | * |
581 | * @return array |
582 | */ |
583 | public function getRestbaseCompatErrorData( RequestInterface $request, LocalizedHttpException $e ): array { |
584 | $msg = $e->getMessageValue(); |
585 | |
586 | // Match error fields emitted by the RESTBase endpoints. |
587 | // EntryPoint::getTextFormatters() ensures 'en' is always available. |
588 | return [ |
589 | 'type' => "MediaWikiError/" . |
590 | str_replace( ' ', '_', HttpStatus::getMessage( $e->getCode() ) ), |
591 | 'title' => $msg->getKey(), |
592 | 'method' => strtolower( $request->getMethod() ), |
593 | 'detail' => $this->responseFactory->getFormattedMessage( $msg, 'en' ), |
594 | 'uri' => (string)$request->getUri() |
595 | ]; |
596 | } |
597 | } |