Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 149
0.00% covered (danger)
0.00%
0 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
Handler
0.00% covered (danger)
0.00%
0 / 149
0.00% covered (danger)
0.00%
0 / 36
4830
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRouter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRouteUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 urlEncodeTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthority
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResponseFactory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 detectExtraneousBodyFields
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 checkSession
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getConditionalHeaderUtil
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 checkPreconditions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 applyConditionalResponseHeaders
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 applyCacheControl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 getParamSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOpenApiSpec
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getRequestSpec
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getResponseBodySchema
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResponseSpec
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getBodyValidator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getValidatedParams
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getValidatedBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseBodyData
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 getHookContainer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHookRunner
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastModified
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getETag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasRepresentation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsReadAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requireSafeAgainstCsrf
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postInitSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postValidationSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace MediaWiki\Rest;
4
5use DateTime;
6use MediaWiki\HookContainer\HookContainer;
7use MediaWiki\HookContainer\HookRunner;
8use MediaWiki\Permissions\Authority;
9use MediaWiki\Rest\Validator\BodyValidator;
10use MediaWiki\Rest\Validator\JsonBodyValidator;
11use MediaWiki\Rest\Validator\NullBodyValidator;
12use MediaWiki\Rest\Validator\Validator;
13use MediaWiki\Session\Session;
14use Wikimedia\Message\MessageValue;
15
16/**
17 * Base class for REST route handlers.
18 *
19 * @stable to extend.
20 */
21abstract class Handler {
22
23    /**
24     * @see Validator::KNOWN_PARAM_SOURCES
25     */
26    public const KNOWN_PARAM_SOURCES = Validator::KNOWN_PARAM_SOURCES;
27
28    /**
29     * @see Validator::PARAM_SOURCE
30     */
31    public const PARAM_SOURCE = Validator::PARAM_SOURCE;
32
33    /**
34     * @see Validator::PARAM_DESCRIPTION
35     */
36    public const PARAM_DESCRIPTION = Validator::PARAM_DESCRIPTION;
37
38    /** @var Router */
39    private $router;
40
41    /** @var RequestInterface */
42    private $request;
43
44    /** @var Authority */
45    private $authority;
46
47    /** @var array */
48    private $config;
49
50    /** @var ResponseFactory */
51    private $responseFactory;
52
53    /** @var array|null */
54    private $validatedParams;
55
56    /** @var mixed|null */
57    private $validatedBody;
58
59    /** @var ConditionalHeaderUtil */
60    private $conditionalHeaderUtil;
61
62    /** @var HookContainer */
63    private $hookContainer;
64
65    /** @var Session */
66    private $session;
67
68    /** @var HookRunner */
69    private $hookRunner;
70
71    /**
72     * Initialise with dependencies from the Router. This is called after construction.
73     * @param Router $router
74     * @param RequestInterface $request
75     * @param array $config
76     * @param Authority $authority
77     * @param ResponseFactory $responseFactory
78     * @param HookContainer $hookContainer
79     * @param Session $session
80     * @internal
81     */
82    final public function init( Router $router, RequestInterface $request, array $config,
83        Authority $authority, ResponseFactory $responseFactory, HookContainer $hookContainer,
84        Session $session
85    ) {
86        $this->router = $router;
87        $this->request = $request;
88        $this->authority = $authority;
89        $this->config = $config;
90        $this->responseFactory = $responseFactory;
91        $this->hookContainer = $hookContainer;
92        $this->hookRunner = new HookRunner( $hookContainer );
93        $this->session = $session;
94        $this->postInitSetup();
95    }
96
97    /**
98     * Returns the path this handler is bound to, including path variables.
99     *
100     * @return string
101     */
102    public function getPath(): string {
103        return $this->getConfig()['path'];
104    }
105
106    /**
107     * Get the Router. The return type declaration causes it to raise
108     * a fatal error if init() has not yet been called.
109     * @return Router
110     */
111    protected function getRouter(): Router {
112        return $this->router;
113    }
114
115    /**
116     * Get the URL of this handler's endpoint.
117     * Supports the substitution of path parameters, and additions of query parameters.
118     *
119     * @see Router::getRouteUrl()
120     *
121     * @param string[] $pathParams Path parameters to be injected into the path
122     * @param string[] $queryParams Query parameters to be attached to the URL
123     *
124     * @return string
125     */
126    protected function getRouteUrl( $pathParams = [], $queryParams = [] ): string {
127        $path = $this->getConfig()['path'];
128        return $this->router->getRouteUrl( $path, $pathParams, $queryParams );
129    }
130
131    /**
132     * URL-encode titles in a "pretty" way.
133     *
134     * Keeps intact ;@$!*(),~: (urlencode does not, but wfUrlencode does).
135     * Encodes spaces as underscores (wfUrlencode does not).
136     * Encodes slashes (wfUrlencode does not, but keeping them messes with REST paths).
137     * Encodes pluses (this is not necessary, and may change).
138     *
139     * @see wfUrlencode
140     *
141     * @param string $title
142     *
143     * @return string
144     */
145    protected function urlEncodeTitle( $title ) {
146        $title = str_replace( ' ', '_', $title );
147        $title = urlencode( $title );
148
149        // %3B_a_%40_b_%24_c_%21_d_%2A_e_%28_f_%29_g_%2C_h_~_i_%3A
150        $replace = [ '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%7E', '%3A' ];
151        $with = [ ';', '@', '$', '!', '*', '(', ')', ',', '~', ':' ];
152
153        return str_replace( $replace, $with, $title );
154    }
155
156    /**
157     * Get the current request. The return type declaration causes it to raise
158     * a fatal error if init() has not yet been called.
159     *
160     * @return RequestInterface
161     */
162    public function getRequest(): RequestInterface {
163        return $this->request;
164    }
165
166    /**
167     * Get the current acting authority. The return type declaration causes it to raise
168     * a fatal error if init() has not yet been called.
169     *
170     * @since 1.36
171     * @return Authority
172     */
173    public function getAuthority(): Authority {
174        return $this->authority;
175    }
176
177    /**
178     * Get the configuration array for the current route. The return type
179     * declaration causes it to raise a fatal error if init() has not
180     * been called.
181     *
182     * @return array
183     */
184    public function getConfig(): array {
185        return $this->config;
186    }
187
188    /**
189     * Get the ResponseFactory which can be used to generate Response objects.
190     * This will raise a fatal error if init() has not been
191     * called.
192     *
193     * @return ResponseFactory
194     */
195    public function getResponseFactory(): ResponseFactory {
196        return $this->responseFactory;
197    }
198
199    /**
200     * Get the Session.
201     * This will raise a fatal error if init() has not been
202     * called.
203     *
204     * @return Session
205     */
206    public function getSession(): Session {
207        return $this->session;
208    }
209
210    /**
211     * Validate the request parameters/attributes and body. If there is a validation
212     * failure, a response with an error message should be returned or an
213     * HttpException should be thrown.
214     *
215     * @stable to override
216     * @param Validator $restValidator
217     * @throws HttpException On validation failure.
218     */
219    public function validate( Validator $restValidator ) {
220        $paramSettings = $this->getParamSettings();
221        $legacyValidatedBody = $restValidator->validateBody( $this->request, $this );
222
223        $this->validatedParams = $restValidator->validateParams( $paramSettings );
224
225        if ( $legacyValidatedBody !== null ) {
226            // TODO: warn if $bodyParamSettings is not empty
227            // TODO: trigger a deprecation warning
228            $this->validatedBody = $legacyValidatedBody;
229        } else {
230            $this->validatedBody = $restValidator->validateBodyParams( $paramSettings );
231
232            // If there is a body, check if it contains extra fields.
233            if ( $this->getRequest()->hasBody() ) {
234                $this->detectExtraneousBodyFields( $restValidator );
235            }
236        }
237
238        $this->postValidationSetup();
239    }
240
241    /**
242     * Subclasses may override this to disable or modify checks for extraneous
243     * body fields.
244     *
245     * @since 1.42
246     * @stable to override
247     * @param Validator $restValidator
248     * @throws HttpException On validation failure.
249     */
250    protected function detectExtraneousBodyFields( Validator $restValidator ) {
251        $parsedBody = $this->getRequest()->getParsedBody();
252
253        if ( !$parsedBody ) {
254            // nothing to do
255            return;
256        }
257
258        $restValidator->detectExtraneousBodyFields(
259            $this->getParamSettings(),
260            $parsedBody
261        );
262    }
263
264    /**
265     * Check the session (and session provider)
266     * @throws HttpException on failed check
267     * @internal
268     */
269    public function checkSession() {
270        if ( !$this->session->getProvider()->safeAgainstCsrf() ) {
271            if ( $this->requireSafeAgainstCsrf() ) {
272                throw new LocalizedHttpException(
273                    new MessageValue( 'rest-requires-safe-against-csrf' ),
274                    400
275                );
276            }
277        } elseif ( !empty( $this->validatedBody['token'] ) ) {
278            throw new LocalizedHttpException(
279                new MessageValue( 'rest-extraneous-csrf-token' ),
280                400
281            );
282        }
283    }
284
285    /**
286     * Get a ConditionalHeaderUtil object.
287     *
288     * On the first call to this method, the object will be initialized with
289     * validator values by calling getETag(), getLastModified() and
290     * hasRepresentation().
291     *
292     * @return ConditionalHeaderUtil
293     */
294    protected function getConditionalHeaderUtil() {
295        if ( $this->conditionalHeaderUtil === null ) {
296            $this->conditionalHeaderUtil = new ConditionalHeaderUtil;
297            $this->conditionalHeaderUtil->setValidators(
298                $this->getETag(),
299                $this->getLastModified(),
300                $this->hasRepresentation()
301            );
302        }
303        return $this->conditionalHeaderUtil;
304    }
305
306    /**
307     * Check the conditional request headers and generate a response if appropriate.
308     * This is called by the Router before execute() and may be overridden.
309     *
310     * @stable to override
311     *
312     * @return ResponseInterface|null
313     */
314    public function checkPreconditions() {
315        $status = $this->getConditionalHeaderUtil()->checkPreconditions( $this->getRequest() );
316        if ( $status ) {
317            $response = $this->getResponseFactory()->create();
318            $response->setStatus( $status );
319            return $response;
320        }
321
322        return null;
323    }
324
325    /**
326     * Apply verifier headers to the response, per RFC 7231 §7.2.
327     * This is called after execute() returns.
328     *
329     * For GET and HEAD requests, the default behavior is to set the ETag and
330     * Last-Modified headers based on the values returned by getETag() and
331     * getLastModified() when they were called before execute() was run.
332     *
333     * Other request methods are assumed to be state-changing, so no headers
334     * will be set per default.
335     *
336     * This may be overridden to modify the verifier headers sent in the response.
337     * However, handlers that modify the resource's state would typically just
338     * set the ETag and Last-Modified headers in the execute() method.
339     *
340     * @stable to override
341     *
342     * @param ResponseInterface $response
343     */
344    public function applyConditionalResponseHeaders( ResponseInterface $response ) {
345        $method = $this->getRequest()->getMethod();
346        if ( $method === 'GET' || $method === 'HEAD' ) {
347            $this->getConditionalHeaderUtil()->applyResponseHeaders( $response );
348        }
349    }
350
351    /**
352     * Apply cache control to enforce privacy.
353     *
354     * @param ResponseInterface $response
355     */
356    public function applyCacheControl( ResponseInterface $response ) {
357        // NOTE: keep this consistent with the logic in OutputPage::sendCacheControl
358
359        // If the response sets cookies, it must not be cached in proxies.
360        // If there's an active cookie-based session (logged-in user or anonymous user with
361        // session-scoped cookies), it is not safe to cache either, as the session manager may set
362        // cookies in the response, or the response itself may vary on user-specific variables,
363        // for example on private wikis where the 'read' permission is restricted. (T264631)
364        if ( $response->getHeaderLine( 'Set-Cookie' ) || $this->getSession()->isPersistent() ) {
365            $response->setHeader( 'Cache-Control', 'private,must-revalidate,s-maxage=0' );
366        }
367
368        if ( !$response->getHeaderLine( 'Cache-Control' ) ) {
369            $rqMethod = $this->getRequest()->getMethod();
370            if ( $rqMethod !== 'GET' && $rqMethod !== 'HEAD' ) {
371                // Responses to requests other than GET or HEAD should not be cacheable per default.
372                $response->setHeader( 'Cache-Control', 'private,no-cache,s-maxage=0' );
373            }
374        }
375    }
376
377    /**
378     * Fetch ParamValidator settings for parameters
379     *
380     * Every setting must include self::PARAM_SOURCE to specify which part of
381     * the request is to contain the parameter.
382     *
383     * Can be used for validating parameters inside an application/x-www-form-urlencoded or
384     * multipart/form-data POST body (i.e. parameters which would be present in PHP's $_POST
385     * array). For validating other kinds of request bodies, override getBodyValidator().
386     *
387     * @stable to override
388     *
389     * @return array[] Associative array mapping parameter names to
390     *  ParamValidator settings arrays
391     */
392    public function getParamSettings() {
393        return [];
394    }
395
396    /**
397     * Returns an OpenAPI Operation Object specification structure as an associative array.
398     *
399     * @see https://swagger.io/specification/#operation-object
400     *
401     * Per default, this will contain information about the supported parameters, as well as
402     * the response for status 200.
403     *
404     * Subclasses may override this to provide additional information.
405     *
406     * @since 1.42
407     * @stable to override
408     *
409     * @param string $method The HTTP method to produce a spec for ("get", "post", etc).
410     *        Useful for handlers that behave differently depending on the
411     *        request method.
412     *
413     * @return array
414     */
415    public function getOpenApiSpec( string $method ): array {
416        $parameters = [];
417
418        // XXX: Maybe we want to be able to define a spec file in the route definition?
419        // NOTE: the route definition may not be loaded when this is called before init()!
420
421        foreach ( $this->getParamSettings() as $name => $paramSetting ) {
422            $param = Validator::getParameterSpec(
423                $name,
424                $paramSetting
425            );
426
427            $location = $param['in'];
428            if ( $location !== 'post' && $location !== 'body' ) {
429                // 'post' and 'body' are handled in getRequestSpec()
430                // but others are added as normal parameters
431                $parameters[] = $param;
432            }
433        }
434
435        $spec = [
436            'parameters' => $parameters,
437            'responses' => $this->getResponseSpec(),
438        ];
439
440        $requestBody = $this->getRequestSpec();
441        if ( $requestBody ) {
442            $spec['requestBody'] = $requestBody;
443        }
444
445        return $spec;
446    }
447
448    /**
449     * Returns an OpenAPI Request Body Object specification structure as an associative array.
450     * @see https://swagger.io/specification/#request-body-object
451     *
452     * Per default, this calls getBodyValidator() to get a SchemaValidator,
453     * and then calls getBodySpec() on it.
454     * If no SchemaValidator is supported, this returns null;
455     *
456     * Subclasses may override this to provide additional information about the structure of responses.
457     *
458     * @stable to override
459     * @return ?array
460     */
461    protected function getRequestSpec(): ?array {
462        $request = [];
463
464        // XXX: support additional content types?!
465        try {
466            $validator = $this->getBodyValidator( 'application/json' );
467
468            // TODO: all validators should support getBodySpec()!
469            if ( $validator instanceof JsonBodyValidator ) {
470                $schema = $validator->getOpenAPISpec();
471
472                if ( $schema !== [] ) {
473                    $request['content']['application/json']['schema'] = $schema;
474                }
475            }
476        } catch ( HttpException $ex ) {
477            // JSON not supported, ignore.
478        }
479
480        return $request ?: null;
481    }
482
483    /**
484     * Returns an OpenAPI Schema Object specification structure as an associative array.
485     * @see https://swagger.io/specification/#schema-object
486     *
487     * Returns null per default. Subclasses that return a JSON response should
488     * implement this method to return a schema of the response body.
489     *
490     * @stable to override
491     * @return ?array
492     */
493    protected function getResponseBodySchema(): ?array {
494        return null;
495    }
496
497    /**
498     * Returns an OpenAPI Responses Object specification structure as an associative array.
499     * @see https://swagger.io/specification/#responses-object
500     *
501     * Per default, this will contain basic information response for status 200, 400, and 500.
502     * The getResponseBodySchema() method is used to determine the structure of the response for status 200.
503     *
504     * Subclasses may override this to provide additional information about the structure of responses.
505     *
506     * @stable to override
507     * @return array
508     */
509    protected function getResponseSpec(): array {
510        $ok = [ 'description' => 'OK' ];
511
512        $bodySchema = $this->getResponseBodySchema();
513
514        if ( $bodySchema ) {
515            $ok['content']['application/json']['schema'] = $bodySchema;
516        }
517
518        // XXX: we should add info about redirects, and maybe a default for errors?
519        return [
520            '200' => $ok,
521            '400' => [ '$ref' => '#/components/responses/GenericErrorResponse' ],
522            '500' => [ '$ref' => '#/components/responses/GenericErrorResponse' ],
523        ];
524    }
525
526    /**
527     * Fetch the BodyValidator
528     *
529     * @stable to override
530     *
531     * @param string $contentType Content type of the request.
532     * @return BodyValidator A {@see NullBodyValidator} in this default implementation
533     * @throws HttpException It's possible to fail early here when e.g. $contentType is unsupported,
534     *  or later when {@see BodyValidator::validateBody} is called
535     */
536    public function getBodyValidator( $contentType ) {
537        // TODO: Create a JsonBodyValidator if getParamSettings() returns body params.
538        // XXX: also support multipart/form-data and application/x-www-form-urlencoded?
539        return new NullBodyValidator();
540    }
541
542    /**
543     * Fetch the validated parameters. This must be called after validate() is
544     * called. During execute() is fine.
545     *
546     * @return array Array mapping parameter names to validated values
547     * @throws \RuntimeException If validate() has not been called
548     */
549    public function getValidatedParams() {
550        if ( $this->validatedParams === null ) {
551            throw new \RuntimeException( 'getValidatedParams() called before validate()' );
552        }
553        return $this->validatedParams;
554    }
555
556    /**
557     * Fetch the validated body
558     * @return mixed|null Value returned by the body validator, or null if validate() was
559     *  not called yet, validation failed, there was no body, or the body was form data.
560     */
561    public function getValidatedBody() {
562        return $this->validatedBody;
563    }
564
565    /**
566     * Returns the parsed body of the request.
567     * Should only be called if $request->hasBody() returns true.
568     *
569     * The default implementation handles application/x-www-form-urlencoded
570     * and multipart/form-data by calling $request->getPostParams().
571     *
572     * The default implementation handles application/json by parsing
573     * the body content as JSON. Only object structures (maps) are supported,
574     * other types will trigger an HttpException with status 400.
575     *
576     * Other content types will trigger a HttpException with status 415 per
577     * default.
578     *
579     * Subclasses may override this method to support parsing additional
580     * content types or to disallow content types by throwing an HttpException
581     * with status 415. Subclasses may also return null to indicate that they
582     * support reading the content, but intent to handle it as an unparsed
583     * stream in their implementation of the execute() method.
584     *
585     * @since 1.42
586     *
587     * @throws HttpException If the content type is not supported or the content
588     *         is malformed.
589     *
590     * @return array|null The body content represented as an associative array,
591     *         or null if the request body is accepted unparsed.
592     */
593    public function parseBodyData( RequestInterface $request ): ?array {
594        // Parse the body based on its content type
595        $contentType = $request->getBodyType();
596
597        // HACK: If the Handler uses a custom BodyValidator, the
598        // getBodyValidator() is also responsible for checking whether
599        // the content type is valid, and for parsing the body.
600        // See T359149.
601        $bodyValidator = $this->getBodyValidator( $contentType ?? 'unknown/unknown' );
602        if ( !$bodyValidator instanceof NullBodyValidator ) {
603            // TODO: Trigger a deprecation warning.
604            return null;
605        }
606
607        switch ( $contentType ) {
608            case 'application/x-www-form-urlencoded':
609            case 'multipart/form-data':
610                return $request->getPostParams();
611            case 'application/json':
612                $jsonStream = $request->getBody();
613                $parsedBody = json_decode( "$jsonStream", true );
614                if ( !is_array( $parsedBody ) ) {
615                    throw new LocalizedHttpException(
616                        new MessageValue(
617                            'rest-json-body-parse-error',
618                            [ 'not a valid JSON object' ]
619                        ),
620                        400
621                    );
622                }
623                return $parsedBody;
624            case null:
625                // Specifying no Content-Type is fine if the body is empty
626                if ( $request->getBody()->getSize() === 0 ) {
627                    return null;
628                }
629                // no break, else fall through to the error below.
630            default:
631                throw new LocalizedHttpException(
632                    new MessageValue( 'rest-unsupported-content-type', [ $contentType ?? '(null)' ] ),
633                    415
634                );
635        }
636    }
637
638    /**
639     * Get a HookContainer, for running extension hooks or for hook metadata.
640     *
641     * @since 1.35
642     * @return HookContainer
643     */
644    protected function getHookContainer() {
645        return $this->hookContainer;
646    }
647
648    /**
649     * Get a HookRunner for running core hooks.
650     *
651     * @internal This is for use by core only. Hook interfaces may be removed
652     *   without notice.
653     * @since 1.35
654     * @return HookRunner
655     */
656    protected function getHookRunner() {
657        return $this->hookRunner;
658    }
659
660    /**
661     * The subclass should override this to provide the maximum last modified
662     * timestamp of the requested resource. This is called before execute() in
663     * order to decide whether to send a 304. If the request is going to
664     * change the state of the resource, the time returned must represent
665     * the last modification date before the change. In other words, it must
666     * provide the timestamp of the entity that the change is going to be
667     * applied to.
668     *
669     * For GET and HEAD requests, this value will automatically be included
670     * in the response in the Last-Modified header.
671     *
672     * Handlers that modify the resource and want to return a Last-Modified
673     * header representing the new state in the response should set the header
674     * in the execute() method.
675     *
676     * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics.
677     *
678     * @stable to override
679     *
680     * @return bool|string|int|float|DateTime|null
681     */
682    protected function getLastModified() {
683        return null;
684    }
685
686    /**
687     * The subclass should override this to provide an ETag for the current
688     * state of the requested resource. This is called before execute() in
689     * order to decide whether to send a 304. If the request is going to
690     * change the state of the resource, the ETag returned must represent
691     * the state before the change. In other words, it must identify
692     * the entity that the change is going to be applied to.
693     *
694     * For GET and HEAD requests, this ETag will also be included in the
695     * response.
696     *
697     * Handlers that modify the resource and want to return an ETag
698     * header representing the new state in the response should set the header
699     * in the execute() method. However, note that responses to PUT requests
700     * must not return an ETag unless the new content of the resource is exactly
701     * the data that was sent by the client in the request body.
702     *
703     * This must be a complete ETag, including double quotes.
704     * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics.
705     *
706     * @stable to override
707     *
708     * @return string|null
709     */
710    protected function getETag() {
711        return null;
712    }
713
714    /**
715     * The subclass should override this to indicate whether the resource
716     * exists. This is used for wildcard validators, for example "If-Match: *"
717     * fails if the resource does not exist.
718     *
719     * In a state-changing request, the return value of this method should
720     * reflect the state before the requested change is applied.
721     *
722     * @stable to override
723     *
724     * @return bool|null
725     */
726    protected function hasRepresentation() {
727        return null;
728    }
729
730    /**
731     * Indicates whether this route requires read rights.
732     *
733     * The handler should override this if it does not need to read from the
734     * wiki. This is uncommon, but may be useful for login and other account
735     * management APIs.
736     *
737     * @stable to override
738     *
739     * @return bool
740     */
741    public function needsReadAccess() {
742        return true;
743    }
744
745    /**
746     * Indicates whether this route requires write access.
747     *
748     * The handler should override this if the route does not need to write to
749     * the database.
750     *
751     * This should return true for routes that may require synchronous database writes.
752     * Modules that do not need such writes should also not rely on primary database access,
753     * since only read queries are needed and each primary DB is a single point of failure.
754     *
755     * @stable to override
756     *
757     * @return bool
758     */
759    public function needsWriteAccess() {
760        return true;
761    }
762
763    /**
764     * Indicates whether this route can be accessed only by session providers safe vs csrf
765     *
766     * The handler should override this if the route must only be accessed by session
767     * providers that are safe against csrf.
768     *
769     * A return value of false does not necessarily mean the route is vulnerable to csrf attacks.
770     * It means the route can be accessed by session providers that are not automatically safe
771     * against csrf attacks, so the possibility of csrf attacks must be considered.
772     *
773     * @stable to override
774     *
775     * @return bool
776     */
777    public function requireSafeAgainstCsrf() {
778        return false;
779    }
780
781    /**
782     * The handler can override this to do any necessary setup after init()
783     * is called to inject the dependencies.
784     *
785     * @stable to override
786     */
787    protected function postInitSetup() {
788    }
789
790    /**
791     * The handler can override this to do any necessary setup after validate()
792     * has been called. This gives the handler an opportunity to do initialization
793     * based on parameters before pre-execution calls like getLastModified() or getETag().
794     *
795     * @stable to override
796     * @since 1.36
797     */
798    protected function postValidationSetup() {
799    }
800
801    /**
802     * Execute the handler. This is called after parameter validation. The
803     * return value can either be a Response or any type accepted by
804     * ResponseFactory::createFromReturnValue().
805     *
806     * To automatically construct an error response, execute() should throw a
807     * \MediaWiki\Rest\HttpException. Such exceptions will not be logged like
808     * a normal exception.
809     *
810     * If execute() throws any other kind of exception, the exception will be
811     * logged and a generic 500 error page will be shown.
812     *
813     * @stable to override
814     *
815     * @return mixed
816     */
817    abstract public function execute();
818}