Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.37% |
169 / 187 |
|
61.90% |
13 / 21 |
CRAP | |
0.00% |
0 / 1 |
Router | |
90.37% |
169 / 187 |
|
61.90% |
13 / 21 |
68.77 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
fetchCacheData | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
getCacheKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getConfigHash | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getRoutesFromFiles | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
getRouteFileTimestamps | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
getAllRoutes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getMatchers | |
85.00% |
17 / 20 |
|
0.00% |
0 / 1 |
9.27 | |||
getRelativePath | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
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 | |
98.15% |
53 / 54 |
|
0.00% |
0 / 1 |
13 | |||
getAllowedMethods | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
createHandler | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
instantiateHandlerObject | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
4.59 | |||
executeHandler | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
setCors | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setStats | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setBodyData | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest; |
4 | |
5 | use AppendIterator; |
6 | use BagOStuff; |
7 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
8 | use MediaWiki\Config\ServiceOptions; |
9 | use MediaWiki\HookContainer\HookContainer; |
10 | use MediaWiki\MainConfigNames; |
11 | use MediaWiki\Permissions\Authority; |
12 | use MediaWiki\Profiler\ProfilingContext; |
13 | use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; |
14 | use MediaWiki\Rest\Handler\RedirectHandler; |
15 | use MediaWiki\Rest\PathTemplateMatcher\PathMatcher; |
16 | use MediaWiki\Rest\Reporter\ErrorReporter; |
17 | use MediaWiki\Rest\Validator\Validator; |
18 | use MediaWiki\Session\Session; |
19 | use NullStatsdDataFactory; |
20 | use Throwable; |
21 | use Wikimedia\Message\MessageValue; |
22 | use Wikimedia\ObjectFactory\ObjectFactory; |
23 | |
24 | /** |
25 | * The REST router is responsible for gathering handler configuration, matching |
26 | * an input path and HTTP method against the defined routes, and constructing |
27 | * and executing the relevant handler for a request. |
28 | */ |
29 | class Router { |
30 | /** @var string[] */ |
31 | private $routeFiles; |
32 | |
33 | /** @var array */ |
34 | private $extraRoutes; |
35 | |
36 | /** @var array|null */ |
37 | private $routesFromFiles; |
38 | |
39 | /** @var int[]|null */ |
40 | private $routeFileTimestamps; |
41 | |
42 | /** @var string */ |
43 | private $baseUrl; |
44 | |
45 | /** @var string */ |
46 | private $privateBaseUrl; |
47 | |
48 | /** @var string */ |
49 | private $rootPath; |
50 | |
51 | /** @var \BagOStuff */ |
52 | private $cacheBag; |
53 | |
54 | /** @var PathMatcher[]|null Path matchers by method */ |
55 | private $matchers; |
56 | |
57 | /** @var string|null */ |
58 | private $configHash; |
59 | |
60 | /** @var ResponseFactory */ |
61 | private $responseFactory; |
62 | |
63 | /** @var BasicAuthorizerInterface */ |
64 | private $basicAuth; |
65 | |
66 | /** @var Authority */ |
67 | private $authority; |
68 | |
69 | /** @var ObjectFactory */ |
70 | private $objectFactory; |
71 | |
72 | /** @var Validator */ |
73 | private $restValidator; |
74 | |
75 | /** @var CorsUtils|null */ |
76 | private $cors; |
77 | |
78 | /** @var ErrorReporter */ |
79 | private $errorReporter; |
80 | |
81 | /** @var HookContainer */ |
82 | private $hookContainer; |
83 | |
84 | /** @var Session */ |
85 | private $session; |
86 | |
87 | /** @var StatsdDataFactoryInterface */ |
88 | private $stats; |
89 | |
90 | /** @var ServiceOptions */ |
91 | private $options; |
92 | |
93 | /** |
94 | * @internal |
95 | * @var array |
96 | */ |
97 | public const CONSTRUCTOR_OPTIONS = [ |
98 | MainConfigNames::CanonicalServer, |
99 | MainConfigNames::InternalServer, |
100 | MainConfigNames::RestPath, |
101 | // From RootSpecHandler::CONSTRUCTOR_OPTIONS |
102 | MainConfigNames::RightsUrl, |
103 | MainConfigNames::RightsText, |
104 | MainConfigNames::EmergencyContact, |
105 | MainConfigNames::Sitename, |
106 | ]; |
107 | |
108 | /** |
109 | * @param string[] $routeFiles List of names of JSON files containing routes |
110 | * @param array $extraRoutes Extension route array |
111 | * @param ServiceOptions $options |
112 | * @param BagOStuff $cacheBag A cache in which to store the matcher trees |
113 | * @param ResponseFactory $responseFactory |
114 | * @param BasicAuthorizerInterface $basicAuth |
115 | * @param Authority $authority |
116 | * @param ObjectFactory $objectFactory |
117 | * @param Validator $restValidator |
118 | * @param ErrorReporter $errorReporter |
119 | * @param HookContainer $hookContainer |
120 | * @param Session $session |
121 | * @internal |
122 | */ |
123 | public function __construct( |
124 | $routeFiles, |
125 | $extraRoutes, |
126 | ServiceOptions $options, |
127 | BagOStuff $cacheBag, |
128 | ResponseFactory $responseFactory, |
129 | BasicAuthorizerInterface $basicAuth, |
130 | Authority $authority, |
131 | ObjectFactory $objectFactory, |
132 | Validator $restValidator, |
133 | ErrorReporter $errorReporter, |
134 | HookContainer $hookContainer, |
135 | Session $session |
136 | ) { |
137 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
138 | |
139 | $this->routeFiles = $routeFiles; |
140 | $this->extraRoutes = $extraRoutes; |
141 | $this->options = $options; |
142 | $this->baseUrl = $options->get( MainConfigNames::CanonicalServer ); |
143 | $this->privateBaseUrl = $options->get( MainConfigNames::InternalServer ); |
144 | $this->rootPath = $options->get( MainConfigNames::RestPath ); |
145 | $this->cacheBag = $cacheBag; |
146 | $this->responseFactory = $responseFactory; |
147 | $this->basicAuth = $basicAuth; |
148 | $this->authority = $authority; |
149 | $this->objectFactory = $objectFactory; |
150 | $this->restValidator = $restValidator; |
151 | $this->errorReporter = $errorReporter; |
152 | $this->hookContainer = $hookContainer; |
153 | $this->session = $session; |
154 | |
155 | $this->stats = new NullStatsdDataFactory(); |
156 | } |
157 | |
158 | /** |
159 | * Get the cache data, or false if it is missing or invalid |
160 | * |
161 | * @return bool|array |
162 | */ |
163 | private function fetchCacheData() { |
164 | $cacheData = $this->cacheBag->get( $this->getCacheKey() ); |
165 | if ( $cacheData && $cacheData['CONFIG-HASH'] === $this->getConfigHash() ) { |
166 | unset( $cacheData['CONFIG-HASH'] ); |
167 | return $cacheData; |
168 | } else { |
169 | return false; |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * @return string The cache key |
175 | */ |
176 | private function getCacheKey() { |
177 | return $this->cacheBag->makeKey( __CLASS__, '4' ); |
178 | } |
179 | |
180 | /** |
181 | * Get a config version hash for cache invalidation |
182 | * |
183 | * @return string |
184 | */ |
185 | private function getConfigHash() { |
186 | if ( $this->configHash === null ) { |
187 | $this->configHash = md5( json_encode( [ |
188 | $this->extraRoutes, |
189 | $this->getRouteFileTimestamps() |
190 | ] ) ); |
191 | } |
192 | return $this->configHash; |
193 | } |
194 | |
195 | /** |
196 | * Load the defined JSON files and return the merged routes |
197 | * |
198 | * @return array |
199 | */ |
200 | private function getRoutesFromFiles() { |
201 | if ( $this->routesFromFiles === null ) { |
202 | $this->routeFileTimestamps = []; |
203 | foreach ( $this->routeFiles as $fileName ) { |
204 | $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); |
205 | $routes = json_decode( file_get_contents( $fileName ), true ); |
206 | if ( $this->routesFromFiles === null ) { |
207 | $this->routesFromFiles = $routes; |
208 | } else { |
209 | $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes ); |
210 | } |
211 | } |
212 | } |
213 | return $this->routesFromFiles; |
214 | } |
215 | |
216 | /** |
217 | * Get an array of last modification times of the defined route files. |
218 | * |
219 | * @return int[] Last modification times |
220 | */ |
221 | private function getRouteFileTimestamps() { |
222 | if ( $this->routeFileTimestamps === null ) { |
223 | $this->routeFileTimestamps = []; |
224 | foreach ( $this->routeFiles as $fileName ) { |
225 | $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); |
226 | } |
227 | } |
228 | return $this->routeFileTimestamps; |
229 | } |
230 | |
231 | /** |
232 | * Get an iterator for all defined routes, including loading the routes from |
233 | * the JSON files. |
234 | * |
235 | * @unstable |
236 | * |
237 | * @return AppendIterator |
238 | */ |
239 | public function getAllRoutes() { |
240 | $iterator = new AppendIterator; |
241 | $iterator->append( new \ArrayIterator( $this->getRoutesFromFiles() ) ); |
242 | $iterator->append( new \ArrayIterator( $this->extraRoutes ) ); |
243 | return $iterator; |
244 | } |
245 | |
246 | /** |
247 | * Get an array of PathMatcher objects indexed by HTTP method |
248 | * |
249 | * @return PathMatcher[] |
250 | */ |
251 | private function getMatchers() { |
252 | if ( $this->matchers === null ) { |
253 | $cacheData = $this->fetchCacheData(); |
254 | $matchers = []; |
255 | if ( $cacheData ) { |
256 | foreach ( $cacheData as $method => $data ) { |
257 | $matchers[$method] = PathMatcher::newFromCache( $data ); |
258 | } |
259 | } else { |
260 | foreach ( $this->getAllRoutes() as $spec ) { |
261 | $methods = $spec['method'] ?? [ 'GET' ]; |
262 | if ( !is_array( $methods ) ) { |
263 | $methods = [ $methods ]; |
264 | } |
265 | foreach ( $methods as $method ) { |
266 | if ( !isset( $matchers[$method] ) ) { |
267 | $matchers[$method] = new PathMatcher; |
268 | } |
269 | $matchers[$method]->add( $spec['path'], $spec ); |
270 | } |
271 | } |
272 | |
273 | $cacheData = [ 'CONFIG-HASH' => $this->getConfigHash() ]; |
274 | foreach ( $matchers as $method => $matcher ) { |
275 | $cacheData[$method] = $matcher->getCacheData(); |
276 | } |
277 | $this->cacheBag->set( $this->getCacheKey(), $cacheData ); |
278 | } |
279 | $this->matchers = $matchers; |
280 | } |
281 | return $this->matchers; |
282 | } |
283 | |
284 | /** |
285 | * Remove the path prefix $this->rootPath. Return the part of the path with the |
286 | * prefix removed, or false if the prefix did not match. |
287 | * |
288 | * @param string $path |
289 | * @return false|string |
290 | */ |
291 | private function getRelativePath( $path ) { |
292 | if ( !str_starts_with( $path, $this->rootPath ) ) { |
293 | return false; |
294 | } |
295 | return substr( $path, strlen( $this->rootPath ) ); |
296 | } |
297 | |
298 | /** |
299 | * Returns the path part of the route URL for the given route, including the root path. |
300 | * Intended for use in relative redirects. |
301 | * |
302 | * @since 1.42 |
303 | * |
304 | * @param string $route |
305 | * @param array $pathParams |
306 | * @param array $queryParams |
307 | * |
308 | * @return string |
309 | * @see getPrivateRouteUrl |
310 | */ |
311 | public function getRoutePath( |
312 | string $route, |
313 | array $pathParams = [], |
314 | array $queryParams = [] |
315 | ): string { |
316 | $route = $this->substPathParams( $route, $pathParams ); |
317 | $path = $this->rootPath . $route; |
318 | return wfAppendQuery( $path, $queryParams ); |
319 | } |
320 | |
321 | /** |
322 | * Returns a full URL for the given route. |
323 | * Intended for use in redirects and when including links to endpoints in output. |
324 | * |
325 | * @param string $route |
326 | * @param array $pathParams |
327 | * @param array $queryParams |
328 | * |
329 | * @return string |
330 | * @see getPrivateRouteUrl |
331 | * |
332 | */ |
333 | public function getRouteUrl( |
334 | string $route, |
335 | array $pathParams = [], |
336 | array $queryParams = [] |
337 | ): string { |
338 | return $this->baseUrl . $this->getRoutePath( $route, $pathParams, $queryParams ); |
339 | } |
340 | |
341 | /** |
342 | * Returns a full private URL for the given route. |
343 | * Private URLs are for use within the local subnet, they may use host names or ports |
344 | * or paths that are not publicly accessible. |
345 | * Intended for use in redirects and when including links to endpoints in output. |
346 | * |
347 | * @note Only private endpoints should use this method for redirects or links to |
348 | * include on the response! Public endpoints should not expose the URLs |
349 | * of private endpoints to the public! |
350 | * |
351 | * @since 1.39 |
352 | * @see getRouteUrl |
353 | * |
354 | * @param string $route |
355 | * @param array $pathParams |
356 | * @param array $queryParams |
357 | * |
358 | * @return string |
359 | */ |
360 | public function getPrivateRouteUrl( |
361 | string $route, |
362 | array $pathParams = [], |
363 | array $queryParams = [] |
364 | ): string { |
365 | return $this->privateBaseUrl . $this->getRoutePath( $route, $pathParams, $queryParams ); |
366 | } |
367 | |
368 | /** |
369 | * @param string $route |
370 | * @param array $pathParams |
371 | * |
372 | * @return string |
373 | */ |
374 | protected function substPathParams( string $route, array $pathParams ): string { |
375 | foreach ( $pathParams as $param => $value ) { |
376 | // NOTE: we use rawurlencode here, since execute() uses rawurldecode(). |
377 | // Spaces in path params must be encoded to %20 (not +). |
378 | // Slashes must be encoded as %2F. |
379 | $route = str_replace( '{' . $param . '}', rawurlencode( (string)$value ), $route ); |
380 | } |
381 | |
382 | return $route; |
383 | } |
384 | |
385 | /** |
386 | * Find the handler for a request and execute it |
387 | * |
388 | * @param RequestInterface $request |
389 | * @return ResponseInterface |
390 | */ |
391 | public function execute( RequestInterface $request ) { |
392 | $path = $request->getUri()->getPath(); |
393 | $relPath = $this->getRelativePath( $path ); |
394 | if ( $relPath === false ) { |
395 | return $this->responseFactory->createLocalizedHttpError( 404, |
396 | ( new MessageValue( 'rest-prefix-mismatch' ) ) |
397 | ->plaintextParams( $path, $this->rootPath ) |
398 | ); |
399 | } |
400 | |
401 | $requestMethod = $request->getMethod(); |
402 | $matchers = $this->getMatchers(); |
403 | $matcher = $matchers[$requestMethod] ?? null; |
404 | $match = $matcher ? $matcher->match( $relPath ) : null; |
405 | |
406 | // For a HEAD request, execute the GET handler instead if one exists. |
407 | // The webserver will discard the body. |
408 | if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) { |
409 | $match = $matchers['GET']->match( $relPath ); |
410 | } |
411 | |
412 | if ( !$match ) { |
413 | // Check for 405 wrong method |
414 | $allowed = $this->getAllowedMethods( $relPath ); |
415 | |
416 | // Check for CORS Preflight. This response will *not* allow the request unless |
417 | // an Access-Control-Allow-Origin header is added to this response. |
418 | if ( $this->cors && $requestMethod === 'OPTIONS' ) { |
419 | return $this->cors->createPreflightResponse( $allowed ); |
420 | } |
421 | |
422 | if ( $allowed ) { |
423 | $response = $this->responseFactory->createLocalizedHttpError( 405, |
424 | ( new MessageValue( 'rest-wrong-method' ) ) |
425 | ->textParams( $requestMethod ) |
426 | ->commaListParams( $allowed ) |
427 | ->numParams( count( $allowed ) ) |
428 | ); |
429 | $response->setHeader( 'Allow', $allowed ); |
430 | return $response; |
431 | } else { |
432 | // Did not match with any other method, must be 404 |
433 | return $this->responseFactory->createLocalizedHttpError( 404, |
434 | ( new MessageValue( 'rest-no-match' ) ) |
435 | ->plaintextParams( $relPath ) |
436 | ); |
437 | } |
438 | } |
439 | |
440 | $pathForMetrics = '(unknown)'; |
441 | $handler = null; |
442 | try { |
443 | // Use rawurldecode so a "+" in path params is not interpreted as a space character. |
444 | $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) ); |
445 | $handler = $this->createHandler( $request, $match['userData'] ); |
446 | |
447 | $this->setBodyData( $request, $handler ); |
448 | |
449 | // Replace any characters that may have a special meaning in the metrics DB. |
450 | $pathForMetrics = $handler->getPath(); |
451 | $pathForMetrics = strtr( $pathForMetrics, '{}:', '-' ); |
452 | $pathForMetrics = strtr( $pathForMetrics, '/.', '_' ); |
453 | |
454 | $statTime = microtime( true ); |
455 | |
456 | $response = $this->executeHandler( $handler ); |
457 | } catch ( HttpException $e ) { |
458 | $response = $this->responseFactory->createFromException( $e ); |
459 | } catch ( Throwable $e ) { |
460 | $this->errorReporter->reportError( $e, $handler, $request ); |
461 | $response = $this->responseFactory->createFromException( $e ); |
462 | } |
463 | |
464 | // gather metrics |
465 | $statusCode = $response->getStatusCode(); |
466 | if ( $response->getStatusCode() >= 400 ) { |
467 | // count how often we return which error code |
468 | $this->stats->increment( "rest_api_errors.$pathForMetrics.$requestMethod.$statusCode" ); |
469 | } else { |
470 | // measure how long it takes to generate a response |
471 | $microtime = ( microtime( true ) - $statTime ) * 1000; |
472 | $this->stats->timing( |
473 | "rest_api_latency.$pathForMetrics.$requestMethod.$statusCode", |
474 | $microtime |
475 | ); |
476 | } |
477 | |
478 | return $response; |
479 | } |
480 | |
481 | /** |
482 | * Get the allow methods for a path. |
483 | * |
484 | * @param string $relPath |
485 | * @return array |
486 | */ |
487 | private function getAllowedMethods( string $relPath ): array { |
488 | // Check for 405 wrong method |
489 | $allowed = []; |
490 | foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) { |
491 | if ( $allowedMatcher->match( $relPath ) ) { |
492 | $allowed[] = $allowedMethod; |
493 | } |
494 | } |
495 | |
496 | return array_unique( |
497 | in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed |
498 | ); |
499 | } |
500 | |
501 | /** |
502 | * Create a handler from its spec |
503 | * @param RequestInterface $request |
504 | * @param array $spec |
505 | * @return Handler |
506 | */ |
507 | private function createHandler( RequestInterface $request, array $spec ): Handler { |
508 | $handler = $this->instantiateHandlerObject( $spec ); |
509 | |
510 | // TODO: split the init method, so instantiateHandlerObject can inject the spec. |
511 | $handler->init( $this, $request, $spec, $this->authority, $this->responseFactory, |
512 | $this->hookContainer, $this->session |
513 | ); |
514 | |
515 | return $handler; |
516 | } |
517 | |
518 | /** |
519 | * Creates a handler from the given spec, but does not initialize it. |
520 | * @param array $spec |
521 | * @return Handler |
522 | */ |
523 | public function instantiateHandlerObject( array $spec ): Handler { |
524 | if ( !isset( $spec['class'] ) && !isset( $spec['factory'] ) ) { |
525 | // Inject well known handle class for shorthand definition |
526 | if ( isset( $spec['redirect'] ) ) { |
527 | $spec['class'] = RedirectHandler::class; |
528 | } else { |
529 | throw new RouteDefinitionException( |
530 | 'Route handler definition must specify "class" or ' . |
531 | '"factory" or "redirect"' |
532 | ); |
533 | } |
534 | } |
535 | |
536 | /** @var $handler Handler (annotation for PHPStorm) */ |
537 | // @phan-suppress-next-line PhanTypeInvalidCallableArraySize |
538 | $handler = $this->objectFactory->createObject( |
539 | $spec, |
540 | [ 'assertClass' => Handler::class ] |
541 | ); |
542 | |
543 | return $handler; |
544 | } |
545 | |
546 | /** |
547 | * Execute a fully-constructed handler |
548 | * |
549 | * @param Handler $handler |
550 | * @return ResponseInterface |
551 | * @throws HttpException |
552 | */ |
553 | private function executeHandler( $handler ): ResponseInterface { |
554 | ProfilingContext::singleton()->init( MW_ENTRY_POINT, $handler->getPath() ); |
555 | // Check for basic authorization, to avoid leaking data from private wikis |
556 | $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler ); |
557 | if ( $authResult ) { |
558 | return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] ); |
559 | } |
560 | |
561 | // Check session (and session provider) |
562 | $handler->checkSession(); |
563 | |
564 | // Validate the parameters |
565 | $handler->validate( $this->restValidator ); |
566 | |
567 | // Check conditional request headers |
568 | $earlyResponse = $handler->checkPreconditions(); |
569 | if ( $earlyResponse ) { |
570 | return $earlyResponse; |
571 | } |
572 | |
573 | // Run the main part of the handler |
574 | $response = $handler->execute(); |
575 | if ( !( $response instanceof ResponseInterface ) ) { |
576 | $response = $this->responseFactory->createFromReturnValue( $response ); |
577 | } |
578 | |
579 | // Set Last-Modified and ETag headers in the response if available |
580 | $handler->applyConditionalResponseHeaders( $response ); |
581 | |
582 | $handler->applyCacheControl( $response ); |
583 | |
584 | return $response; |
585 | } |
586 | |
587 | /** |
588 | * @param CorsUtils $cors |
589 | * @return self |
590 | */ |
591 | public function setCors( CorsUtils $cors ): self { |
592 | $this->cors = $cors; |
593 | |
594 | return $this; |
595 | } |
596 | |
597 | /** |
598 | * @param StatsdDataFactoryInterface $stats |
599 | * |
600 | * @return self |
601 | */ |
602 | public function setStats( StatsdDataFactoryInterface $stats ): self { |
603 | $this->stats = $stats; |
604 | |
605 | return $this; |
606 | } |
607 | |
608 | private function setBodyData( RequestInterface $request, Handler $handler ) { |
609 | // fail if the request method is in nobodymethod but has body |
610 | $requestMethod = $request->getMethod(); |
611 | if ( in_array( $requestMethod, RequestInterface::NO_BODY_METHODS ) ) { |
612 | // check if the request has a body |
613 | if ( $request->hasBody() ) { |
614 | // NOTE: Don't throw, see T359509. |
615 | // TODO: Ignore only empty bodies, log a warning or fail if |
616 | // there is actual content. |
617 | return; |
618 | } |
619 | } |
620 | |
621 | // fail if the request method expects a body but has no body |
622 | if ( in_array( $requestMethod, RequestInterface::BODY_METHODS ) ) { |
623 | // check if it has no body |
624 | if ( !$request->hasBody() ) { |
625 | throw new LocalizedHttpException( new MessageValue( "rest-request-body-expected", [ $requestMethod ] ), |
626 | 411 |
627 | ); |
628 | } |
629 | } |
630 | |
631 | // call parsedbody |
632 | if ( $request->hasBody() ) { |
633 | $parsedBody = $handler->parseBodyData( $request ); |
634 | // Set the parsed body data on the request object |
635 | $request->setParsedBody( $parsedBody ); |
636 | } |
637 | } |
638 | |
639 | } |