Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.99% covered (success)
90.99%
303 / 333
77.78% covered (warning)
77.78%
42 / 54
CRAP
0.00% covered (danger)
0.00%
0 / 1
Handler
90.99% covered (success)
90.99%
303 / 333
77.78% covered (warning)
77.78%
42 / 54
135.24
0.00% covered (danger)
0.00%
0 / 1
 initContext
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 initServices
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 initSession
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 initForExecute
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 processRequestBody
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedPathParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getRouter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 urlEncodeTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAuthority
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResponseFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSession
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDeprecated
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getDeprecatedDate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 applyDeprecationHeader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 detectExtraneousBodyFields
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 checkSession
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getJsonLocalizer
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getConditionalHeaderUtil
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 checkPreconditions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 applyConditionalResponseHeaders
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 applyCacheControl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 getParamSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeaderParamSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBodyParamSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpenApiSpec
88.57% covered (warning)
88.57%
31 / 35
0.00% covered (danger)
0.00%
0 / 1
11.18
 getRequestSpec
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getRequestBodySchema
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
8.02
 getResponseBodySchema
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getResponseHeaderSchemas
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getResponseHeaderSettings
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getResponseBodySchemaFileName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generateResponseSpec
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getBodyValidator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValidatedParams
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getValidatedBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseBodyData
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
13
 recursiveUtfCleanup
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getSupportedRequestTypes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getETag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRepresentation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsReadAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsWriteAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireSafeAgainstCsrf
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 postInitSetup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 postValidationSetup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace MediaWiki\Rest;
4
5use DateTime;
6use MediaWiki\Debug\MWDebug;
7use MediaWiki\HookContainer\HookContainer;
8use MediaWiki\HookContainer\HookRunner;
9use MediaWiki\Permissions\Authority;
10use MediaWiki\Rest\Module\Module;
11use MediaWiki\Rest\Validator\BodyValidator;
12use MediaWiki\Rest\Validator\NullBodyValidator;
13use MediaWiki\Rest\Validator\Validator;
14use MediaWiki\Session\Session;
15use UtfNormal\Validator as UtfNormalValidator;
16use Wikimedia\Assert\Assert;
17use Wikimedia\Message\MessageValue;
18use Wikimedia\ParamValidator\ParamValidator;
19
20/**
21 * Base class for REST route handlers.
22 *
23 * @stable to extend.
24 */
25abstract class Handler {
26
27    /**
28     * @see Validator::KNOWN_PARAM_SOURCES
29     */
30    public const KNOWN_PARAM_SOURCES = Validator::KNOWN_PARAM_SOURCES;
31
32    /**
33     * @see Validator::PARAM_SOURCE
34     */
35    public const PARAM_SOURCE = Validator::PARAM_SOURCE;
36
37    /**
38     * @see Validator::PARAM_DESCRIPTION
39     */
40    public const PARAM_DESCRIPTION = Validator::PARAM_DESCRIPTION;
41
42    public const OPENAPI_DESCRIPTION_KEY = 'description';
43
44    /** @var Module */
45    private $module;
46
47    /** @var RequestInterface */
48    private $request;
49
50    /** @var Authority */
51    private $authority;
52
53    /** @var string */
54    private $path;
55
56    /** @var array */
57    private $config;
58
59    /** @var array */
60    private $openApiSpec;
61
62    /** @var ResponseFactory */
63    private $responseFactory;
64
65    /** @var array|null */
66    private $validatedParams;
67
68    /** @var mixed|null */
69    private $validatedBody;
70
71    /** @var ConditionalHeaderUtil */
72    private $conditionalHeaderUtil;
73
74    /** @var JsonLocalizer */
75    private $jsonLocalizer;
76
77    /** @var HookContainer */
78    private $hookContainer;
79
80    /** @var Session */
81    private $session;
82
83    /** @var HookRunner */
84    private $hookRunner;
85
86    /**
87     * Injects information about the handler's context in the Module.
88     * The framework should call this right after the object was constructed.
89     *
90     * First function of the initialization function, must be called before
91     * initServices().
92     *
93     * @param Module $module
94     * @param string $path
95     * @param array $routeConfig information about the route declaration.
96     * @param array $openApiSpec OpenAPI meta-data, such as the description.
97     *
98     * @internal
99     */
100    final public function initContext(
101        Module $module,
102        string $path,
103        array $routeConfig,
104        array $openApiSpec = []
105    ) {
106        Assert::precondition(
107            $this->authority === null,
108            'initContext() must be called before initServices()'
109        );
110
111        $this->module = $module;
112        $this->path = $path;
113        $this->config = $routeConfig;
114        $this->openApiSpec = $openApiSpec;
115    }
116
117    /**
118     * Inject service objects.
119     *
120     * Second function of the initialization function, must be called after
121     * initContext() and before initSession().
122     *
123     * @param Authority $authority
124     * @param ResponseFactory $responseFactory
125     * @param HookContainer $hookContainer
126     *
127     * @internal
128     */
129    final public function initServices(
130        Authority $authority, ResponseFactory $responseFactory, HookContainer $hookContainer
131    ) {
132        // Warn if a subclass overrides getBodyValidator()
133        MWDebug::detectDeprecatedOverride(
134            $this,
135            __CLASS__,
136            'getBodyValidator',
137            '1.43'
138        );
139
140        Assert::precondition(
141            $this->module !== null,
142            'initServices() must not be called before initContext()'
143        );
144        Assert::precondition(
145            $this->session === null,
146            'initServices() must be called before initSession()'
147        );
148
149        $this->authority = $authority;
150        $this->responseFactory = $responseFactory;
151        $this->hookContainer = $hookContainer;
152        $this->hookRunner = new HookRunner( $hookContainer );
153    }
154
155    /**
156     * Inject session information.
157     *
158     * Third function of the initialization function, must be called after
159     * initServices() and before initForExecute().
160     *
161     * @param Session $session
162     *
163     * @internal
164     */
165    final public function initSession( Session $session ) {
166        Assert::precondition(
167            $this->authority !== null,
168            'initSession() must not be called before initContext()'
169        );
170        Assert::precondition(
171            $this->request === null,
172            'initSession() must be called before initForExecute()'
173        );
174
175        $this->session = $session;
176    }
177
178    /**
179     * Initialise for execution based on the given request.
180     *
181     * Last function of the initialization function, must be called after
182     * initSession() and before validate() and checkPreconditions().
183     *
184     * This function will call postInitSetup() to allow subclasses to
185     * perform their own initialization.
186     *
187     * The request object is updated with parsed body data if needed.
188     *
189     * @internal
190     *
191     * @param RequestInterface $request
192     *
193     * @throws HttpException if the handler does not accept the request for
194     *         some reason.
195     */
196    final public function initForExecute( RequestInterface $request ) {
197        Assert::precondition(
198            $this->session !== null,
199            'initForExecute() must not be called before initSession()'
200        );
201
202        if ( $request->getParsedBody() === null ) {
203            $this->processRequestBody( $request );
204        }
205
206        $this->request = $request;
207
208        $this->postInitSetup();
209    }
210
211    /**
212     * Process the request's request body and set the parsed body data
213     * if appropriate.
214     *
215     * @see parseBodyData()
216     *
217     * @throws HttpException if the request body is not acceptable.
218     */
219    private function processRequestBody( RequestInterface $request ) {
220        // fail if the request method is in NO_BODY_METHODS but has body
221        $requestMethod = $request->getMethod();
222        if ( in_array( $requestMethod, RequestInterface::NO_BODY_METHODS ) ) {
223            // check if the request has a body
224            if ( $request->hasBody() ) {
225                // NOTE: Don't throw, see T359509.
226                // TODO: Ignore only empty bodies, log a warning or fail if
227                //       there is actual content.
228                return;
229            }
230        }
231
232        // fail if the request method expects a body but has no body
233        if ( in_array( $requestMethod, RequestInterface::BODY_METHODS ) ) {
234            // check if it has no body
235            if ( !$request->hasBody() ) {
236                throw new LocalizedHttpException(
237                    new MessageValue(
238                        "rest-request-body-expected",
239                        [ $requestMethod ]
240                    ),
241                    411
242                );
243            }
244        }
245
246        // call parsedbody
247        if ( $request->hasBody() ) {
248            $parsedBody = $this->parseBodyData( $request );
249            // Set the parsed body data on the request object
250            $request->setParsedBody( $parsedBody );
251        }
252    }
253
254    /**
255     * Returns the path this handler is bound to relative to the module prefix.
256     * Includes path variables.
257     */
258    public function getPath(): string {
259        return $this->path;
260    }
261
262    /**
263     * Get a list of parameter placeholders present in the route's path
264     * as returned by getPath(). Note that this is independent of the parameters
265     * defined by getParamSettings(): required path parameters defined in
266     * getParamSettings() should be present in the path, but there is no
267     * mechanism to ensure that they are.
268     *
269     * @return string[]
270     */
271    public function getSupportedPathParams(): array {
272        $path = $this->getPath();
273
274        preg_match_all( '/\{(.*?)\}/', $path, $matches, PREG_PATTERN_ORDER );
275
276        return $matches[1] ?? [];
277    }
278
279    protected function getRouter(): Router {
280        return $this->module->getRouter();
281    }
282
283    /**
284     * Get the Module this handler belongs to.
285     * Will fail hard if called before initContext().
286     */
287    protected function getModule(): Module {
288        return $this->module;
289    }
290
291    /**
292     * Get the URL of this handler's endpoint.
293     * Supports the substitution of path parameters, and additions of query parameters.
294     *
295     * @see Router::getRouteUrl()
296     *
297     * @param string[] $pathParams Path parameters to be injected into the path
298     * @param string[] $queryParams Query parameters to be attached to the URL
299     *
300     * @return string
301     */
302    protected function getRouteUrl( $pathParams = [], $queryParams = [] ): string {
303        $path = $this->getPath();
304        return $this->getRouter()->getRouteUrl( $path, $pathParams, $queryParams );
305    }
306
307    /**
308     * URL-encode titles in a "pretty" way.
309     *
310     * Keeps intact ;@$!*(),~: (urlencode does not, but wfUrlencode does).
311     * Encodes spaces as underscores (wfUrlencode does not).
312     * Encodes slashes (wfUrlencode does not, but keeping them messes with REST paths).
313     * Encodes pluses (this is not necessary, and may change).
314     *
315     * @see wfUrlencode
316     *
317     * @param string $title
318     *
319     * @return string
320     */
321    protected function urlEncodeTitle( $title ) {
322        $title = str_replace( ' ', '_', $title );
323        $title = urlencode( $title );
324
325        // %3B_a_%40_b_%24_c_%21_d_%2A_e_%28_f_%29_g_%2C_h_~_i_%3A
326        $replace = [ '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%7E', '%3A' ];
327        $with = [ ';', '@', '$', '!', '*', '(', ')', ',', '~', ':' ];
328
329        return str_replace( $replace, $with, $title );
330    }
331
332    /**
333     * Get the current request. The return type declaration causes it to raise
334     * a fatal error if initForExecute() has not yet been called.
335     */
336    public function getRequest(): RequestInterface {
337        return $this->request;
338    }
339
340    /**
341     * Get the current acting authority. The return type declaration causes it to raise
342     * a fatal error if initServices() has not yet been called.
343     *
344     * @since 1.36
345     * @return Authority
346     */
347    public function getAuthority(): Authority {
348        return $this->authority;
349    }
350
351    /**
352     * Get the configuration array for the current route. The return type
353     * declaration causes it to raise a fatal error if initContext() has not
354     * been called.
355     */
356    public function getConfig(): array {
357        return $this->config;
358    }
359
360    /**
361     * Get the ResponseFactory which can be used to generate Response objects.
362     * This will raise a fatal error if initServices() has not been
363     * called.
364     */
365    public function getResponseFactory(): ResponseFactory {
366        return $this->responseFactory;
367    }
368
369    /**
370     * Get the Session.
371     * This will raise a fatal error if initSession() has not been
372     * called.
373     */
374    public function getSession(): Session {
375        return $this->session;
376    }
377
378    /**
379     * Indicates whether this is deprecated.
380     *
381     * Whenever possible, module deprecation is preferred to endpoint deprecation.
382     * Modules and endpoints are normally deprecated in module or route definition .json files
383     * rather than by overriding this function.
384     *
385     * @since 1.45
386     * @stable to override
387     * @return bool
388     */
389    protected function isDeprecated(): bool {
390        return isset( $this->getModule()->getModuleDescription()['info']['deprecationSettings'] ) ||
391            isset( $this->openApiSpec['deprecationSettings'] );
392    }
393
394    /**
395     * Returns the timestamp at which this was or will be deprecated, or null if none.
396     *
397     * Whenever possible, module deprecation is preferred to endpoint deprecation.
398     * Modules and endpoints are normally deprecated in module or route definition .json files
399     * rather than by overriding this function.
400     *
401     * @since 1.45
402     * @stable to override
403     * @return ?int deprecation date, as a unix timestamp, or null if none
404     */
405    protected function getDeprecatedDate(): ?int {
406        return $this->getModule()->getModuleDescription()['info']['deprecationSettings']['since']
407            ?? $this->openApiSpec['deprecationSettings']['since']
408            ?? null;
409    }
410
411    /**
412     * Validate the request parameters/attributes and body. If there is a validation
413     * failure, a response with an error message should be returned or an
414     * HttpException should be thrown.
415     *
416     * @stable to override
417     * @param Validator $restValidator
418     * @throws HttpException On validation failure.
419     */
420    public function validate( Validator $restValidator ) {
421        $allParamSettings = array_merge( $this->getParamSettings(), $this->getHeaderParamSettings() );
422        $this->validatedParams = $restValidator->validateParams( $allParamSettings );
423
424        $bodyType = $this->request->getBodyType();
425        $legacyBodyValidator = $bodyType === null ? null
426            : $this->getBodyValidator( $bodyType );
427
428        if ( $legacyBodyValidator && !$legacyBodyValidator instanceof NullBodyValidator ) {
429            $this->validatedBody = $restValidator->validateBody( $this->request, $this );
430        } else {
431            // Allow type coercion if the request body is form data.
432            // For JSON requests, insist on proper types.
433            $enforceTypes = !in_array(
434                $this->request->getBodyType(),
435                RequestInterface::FORM_DATA_CONTENT_TYPES
436            );
437
438            $this->validatedBody = $restValidator->validateBodyParams(
439                $this->getBodyParamSettings(),
440                $enforceTypes
441            );
442
443            // If there is a body, check if it contains extra fields.
444            if ( $this->getRequest()->hasBody() ) {
445                $this->detectExtraneousBodyFields( $restValidator );
446            }
447        }
448
449        $this->postValidationSetup();
450    }
451
452    /**
453     * Apply Deprecation header per RFC 9745.
454     *
455     * @since 1.45
456     * @stable to override
457     * @see https://www.rfc-editor.org/rfc/rfc9745.txt
458     *
459     * @param ResponseInterface $response
460     */
461    public function applyDeprecationHeader( ResponseInterface $response ) {
462        $dd = $this->getDeprecatedDate();
463        if ( $dd !== null && !$response->getHeaderLine( 'Deprecation' ) ) {
464            $response->setHeader( ResponseHeaders::DEPRECATION, '@' . $dd );
465        }
466    }
467
468    /**
469     * Subclasses may override this to disable or modify checks for extraneous
470     * body fields.
471     *
472     * @since 1.42
473     * @stable to override
474     * @param Validator $restValidator
475     * @throws HttpException On validation failure.
476     */
477    protected function detectExtraneousBodyFields( Validator $restValidator ) {
478        $parsedBody = $this->getRequest()->getParsedBody();
479
480        if ( !$parsedBody ) {
481            // nothing to do
482            return;
483        }
484
485        $restValidator->detectExtraneousBodyFields(
486            $this->getBodyParamSettings(),
487            $parsedBody
488        );
489    }
490
491    /**
492     * Check the session (and session provider)
493     * @throws HttpException on failed check
494     * @internal
495     */
496    public function checkSession() {
497        if ( !$this->session->getProvider()->safeAgainstCsrf() ) {
498            if ( $this->requireSafeAgainstCsrf() ) {
499                throw new LocalizedHttpException(
500                    new MessageValue( 'rest-requires-safe-against-csrf' ),
501                    400
502                );
503            }
504        } elseif ( !empty( $this->validatedBody['token'] ) ) {
505            throw new LocalizedHttpException(
506                new MessageValue( 'rest-extraneous-csrf-token' ),
507                400
508            );
509        }
510    }
511
512    /**
513     * Get a JsonLocalizer object.
514     *
515     * @return JsonLocalizer
516     */
517    protected function getJsonLocalizer(): JsonLocalizer {
518        Assert::precondition(
519            $this->responseFactory !== null,
520            'getJsonLocalizer() must not be called before initServices()'
521        );
522
523        if ( $this->jsonLocalizer === null ) {
524            $this->jsonLocalizer = new JsonLocalizer( $this->responseFactory );
525        }
526
527        return $this->jsonLocalizer;
528    }
529
530    /**
531     * Get a ConditionalHeaderUtil object.
532     *
533     * On the first call to this method, the object will be initialized with
534     * validator values by calling getETag(), getLastModified() and
535     * hasRepresentation().
536     *
537     * @return ConditionalHeaderUtil
538     */
539    protected function getConditionalHeaderUtil() {
540        if ( $this->conditionalHeaderUtil === null ) {
541            $this->conditionalHeaderUtil = new ConditionalHeaderUtil;
542
543            // NOTE: It would be nicer to have Handler implement a
544            // ConditionalHeaderValues interface that defines methods that
545            // ConditionalHeaderUtil can call. But the relevant methods already
546            // exist in Handler as protected and stable to override.
547            // We can't make them public without breaking all subclasses that
548            // override them. So we pass closures for now.
549            $this->conditionalHeaderUtil->setValidators(
550                $this->getETag( ... ),
551                $this->getLastModified( ... ),
552                $this->hasRepresentation( ... )
553            );
554        }
555        return $this->conditionalHeaderUtil;
556    }
557
558    /**
559     * Check the conditional request headers and generate a response if appropriate.
560     * This is called by the Router before execute() and may be overridden.
561     *
562     * @stable to override
563     *
564     * @return ResponseInterface|null
565     */
566    public function checkPreconditions() {
567        $status = $this->getConditionalHeaderUtil()->checkPreconditions( $this->getRequest() );
568        if ( $status ) {
569            $response = $this->getResponseFactory()->create();
570            $response->setStatus( $status );
571            $this->applyConditionalResponseHeaders( $response );
572            return $response;
573        }
574
575        return null;
576    }
577
578    /**
579     * Apply verifier headers to the response, per RFC 7231 §7.2.
580     * This is called after execute() returns.
581     *
582     * For GET and HEAD requests, the default behavior is to set the ETag and
583     * Last-Modified headers based on the values returned by getETag() and
584     * getLastModified() when they were called before execute() was run.
585     *
586     * Other request methods are assumed to be state-changing, so no headers
587     * will be set by default.
588     *
589     * This may be overridden to modify the verifier headers sent in the response.
590     * However, handlers that modify the resource's state would typically just
591     * set the ETag and Last-Modified headers in the execute() method.
592     *
593     * @stable to override
594     *
595     * @param ResponseInterface $response
596     */
597    public function applyConditionalResponseHeaders( ResponseInterface $response ) {
598        $method = $this->getRequest()->getMethod();
599        if ( $method === 'GET' || $method === 'HEAD' ) {
600            $this->getConditionalHeaderUtil()->applyResponseHeaders( $response );
601        }
602    }
603
604    /**
605     * Apply cache control to enforce privacy.
606     */
607    public function applyCacheControl( ResponseInterface $response ) {
608        // NOTE: keep this consistent with the logic in OutputPage::sendCacheControl
609
610        // If the response sets cookies, it must not be cached in proxies.
611        // If there's an active cookie-based session (logged-in user or anonymous user with
612        // session-scoped cookies), it is not safe to cache either, as the session manager may set
613        // cookies in the response, or the response itself may vary on user-specific variables,
614        // for example on private wikis where the 'read' permission is restricted. (T264631)
615        if ( $response->getHeaderLine( 'Set-Cookie' ) || $this->getSession()->isPersistent() ) {
616            $response->setHeader( ResponseHeaders::CACHE_CONTROL, 'private,must-revalidate,s-maxage=0' );
617        }
618
619        if ( !$response->getHeaderLine( ResponseHeaders::CACHE_CONTROL ) ) {
620            $rqMethod = $this->getRequest()->getMethod();
621            if ( $rqMethod !== 'GET' && $rqMethod !== 'HEAD' ) {
622                // Responses to requests other than GET or HEAD should not be cacheable by default.
623                $response->setHeader( ResponseHeaders::CACHE_CONTROL, 'private,no-cache,s-maxage=0' );
624            }
625        }
626    }
627
628    /**
629     * Fetch ParamValidator settings for parameters
630     *
631     * Every setting must include self::PARAM_SOURCE to specify which part of
632     * the request is to contain the parameter.
633     *
634     * Can be used for the request body as well, by setting self::PARAM_SOURCE
635     * to "post". Note that the values of "post" parameters will be accessible
636     * through getValidatedParams(). "post" parameters are used with
637     * form data (application/x-www-form-urlencoded or multipart/form-data).
638     *
639     * For "query" parameters, a PARAM_REQUIRED setting of "false" means the caller
640     * does not have to supply the parameter. For "path" parameters, the path matcher will always
641     * require the caller to supply all path parameters for a route, regardless of the
642     * PARAM_REQUIRED setting. However, "path" parameters may be specified in getParamSettings()
643     * as non-required to indicate that the handler services multiple routes, some of which may
644     * not supply the parameter.
645     *
646     * @stable to override
647     *
648     * @return array[] Associative array mapping parameter names to
649     *  ParamValidator settings arrays
650     */
651    public function getParamSettings() {
652        return [];
653    }
654
655    /**
656     * Fetch ParamValidator settings for request headers
657     *
658     * Every setting must include self::PARAM_SOURCE as 'header' to specify
659     * it's a request header for the endpoint.
660     *
661     * Subclasses that must use the headers from a request should consider
662     * having PARAM_REQUIRED setting of "true", Otherwise if the header's existence
663     * or non-existence doesn't break the code the PARAM_REQUIRED should be set to "false".
664     *
665     * @stable to override
666     *
667     * @return array[] Associative array mapping header names to
668     *  ParamValidator settings arrays
669     */
670    public function getHeaderParamSettings() {
671        return [];
672    }
673
674    /**
675     * Fetch ParamValidator settings for body fields. Parameters defined
676     * by this method are used to validate the request body. The parameter
677     * values will become available through getValidatedBody().
678     *
679     * Subclasses may override this method to specify what fields they support
680     * in the request body. All parameter settings returned by this method must
681     * have self::PARAM_SOURCE set to 'body'.
682     *
683     * @return array[]
684     */
685    public function getBodyParamSettings(): array {
686        return [];
687    }
688
689    /**
690     * Returns an OpenAPI Operation Object specification structure as an associative array.
691     *
692     * @see https://swagger.io/specification/#operation-object
693     *
694     * By default, this will contain information about the supported parameters, as well as
695     * the response for status 200.
696     *
697     * Subclasses may override this to provide additional information.
698     *
699     * @since 1.42
700     * @stable to override
701     *
702     * @param string $method The HTTP method to produce a spec for ("get", "post", etc).
703     *        Useful for handlers that behave differently depending on the
704     *        request method.
705     *
706     * @return array
707     */
708    public function getOpenApiSpec( string $method ): array {
709        $parameters = [];
710
711        $supportedPathParams = array_flip( $this->getSupportedPathParams() );
712
713        foreach ( $this->getParamSettings() as $name => $setting ) {
714            $source = $setting[ Validator::PARAM_SOURCE ] ?? '';
715
716            if ( $source !== 'query' && $source !== 'path' ) {
717                continue;
718            }
719
720            if ( $source === 'path' && !isset( $supportedPathParams[$name] ) ) {
721                // Skip optional path param not used in the current path
722                continue;
723            }
724
725            $setting[ Validator::PARAM_DESCRIPTION ] = $this->getJsonLocalizer()->localizeValue(
726                $setting, Validator::PARAM_DESCRIPTION,
727            );
728
729            $param = Validator::getParameterSpec( $name, $setting );
730
731            $parameters[] = $param;
732        }
733
734        foreach ( $this->getHeaderParamSettings() as $name => $setting ) {
735            $source = $setting[ Validator::PARAM_SOURCE ] ?? '';
736
737            if ( $source !== 'header' ) {
738                continue;
739            }
740
741            $setting[ Validator::PARAM_DESCRIPTION ] = $this->getJsonLocalizer()->localizeValue(
742                $setting, Validator::PARAM_DESCRIPTION,
743            );
744
745            $param = Validator::getParameterSpec( $name, $setting );
746
747            $parameters[] = $param;
748        }
749
750        $spec = [
751            'parameters' => $parameters,
752            'responses' => $this->generateResponseSpec( $method ),
753        ];
754
755        if ( !in_array( $method, RequestInterface::NO_BODY_METHODS ) ) {
756            $requestBody = $this->getRequestSpec( $method );
757            if ( $requestBody ) {
758                $spec['requestBody'] = $requestBody;
759            }
760        }
761
762        // TODO: Allow additional information about parameters and responses to
763        //       be provided in the route definition.
764        $spec += $this->openApiSpec;
765
766        if ( $this->isDeprecated() ) {
767            $spec['deprecated'] = true;
768            unset( $spec['deprecationSettings'] );
769        }
770
771        return $spec;
772    }
773
774    /**
775     * Returns an OpenAPI Request Body Object specification structure as an associative array.
776     *
777     * @see https://swagger.io/specification/#request-body-object
778     *
779     * This is based on the getBodyParamSettings() and getSupportedRequestTypes().
780     *
781     * Subclasses may override this to provide additional information about the
782     * structure of responses, or to add support for additional mediaTypes.
783     *
784     * @stable to override getBodySchema() to generate a schema for each
785     * supported media type as returned by getSupportedBodyTypes().
786     *
787     * @param string $method
788     *
789     * @return ?array
790     */
791    protected function getRequestSpec( string $method ): ?array {
792        $mediaTypes = [];
793
794        foreach ( $this->getSupportedRequestTypes() as $type ) {
795            $schema = $this->getRequestBodySchema( $type );
796
797            if ( $schema ) {
798                $mediaTypes[$type] = [ 'schema' => $schema ];
799            }
800        }
801
802        if ( !$mediaTypes ) {
803            return null;
804        }
805
806        return [
807            // TODO: some DELETE handlers may require a body that contains a token
808            // FIXME: check if there are required body params!
809            'required' => in_array( $method, RequestInterface::BODY_METHODS ),
810            'content' => $mediaTypes
811        ];
812    }
813
814    /**
815     * Returns a content schema per the OpenAPI spec.
816     * @see https://swagger.io/specification/#schema-object
817     *
818     * Per default, this provides schemas for JSON requests and form data, based
819     * on the parameter declarations returned by getParamSettings().
820     *
821     * Subclasses may override this to provide additional information about the
822     * structure of responses, or to add support for additional mediaTypes.
823     *
824     * @stable to override
825     * @return array
826     */
827    protected function getRequestBodySchema( string $mediaType ): array {
828        if ( $mediaType === RequestInterface::FORM_URLENCODED_CONTENT_TYPE ) {
829            $allowedSources = [ 'body', 'post' ];
830        } elseif ( $mediaType === RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE ) {
831            $allowedSources = [ 'body', 'post' ];
832        } else {
833            $allowedSources = [ 'body' ];
834        }
835
836        $paramSettings = $this->getBodyParamSettings();
837
838        $properties = [];
839        $required = [];
840
841        foreach ( $paramSettings as $name => $settings ) {
842            $source = $settings[ Validator::PARAM_SOURCE ] ?? '';
843            $isRequired = $settings[ ParamValidator::PARAM_REQUIRED ] ?? false;
844
845            if ( !in_array( $source, $allowedSources ) ) {
846                // TODO: post parameters also work as body parameters...
847                continue;
848            }
849
850            $properties[$name] = Validator::getParameterSchema( $settings );
851            $properties[$name][self::OPENAPI_DESCRIPTION_KEY] =
852                $this->getJsonLocalizer()->localizeValue( $settings, Validator::PARAM_DESCRIPTION )
853                ?? "$name parameter";
854
855            if ( $isRequired ) {
856                $required[] = $name;
857            }
858        }
859
860        if ( !$properties ) {
861            return [];
862        }
863
864        $schema = [
865            'type' => 'object',
866            'properties' => $properties,
867        ];
868
869        if ( $required ) {
870            $schema['required'] = $required;
871        }
872
873        return $schema;
874    }
875
876    /**
877     * Returns an OpenAPI Schema Object specification structure as an associative array.
878     *
879     * @see https://swagger.io/specification/#schema-object
880     *
881     * Loads and decodes the JSON schema file returned by getResponseBodySchemaFileName().
882     * Returns null if getResponseBodySchemaFileName() returns null.
883     *
884     * @param string $method The HTTP method to produce a spec for ("get", "post", etc).
885     *
886     * @stable to override
887     * @return ?array
888     */
889    protected function getResponseBodySchema( string $method ): ?array {
890        $file = $this->getResponseBodySchemaFileName( $method );
891        return $file ? Module::loadJsonFile( $file ) : null;
892    }
893
894    /**
895     * Fetch Response headers specs for response headers returned by a Handler
896     *
897     * Subclasses that return other headers in addition to the default ones should
898     * extend getResponseHeaderSettings()
899     *
900     * @return array[] Associative array mapping response header names to
901     *  their types and localizable descriptions
902     */
903    private function getResponseHeaderSchemas(): array {
904        $responseHeaderSettings = [];
905        foreach ( $this->getResponseHeaderSettings() as $headerName => $settings ) {
906            // Set description field for localization
907            $settings[ self::OPENAPI_DESCRIPTION_KEY  ] = new MessageValue( $settings[ 'messageKey' ] );
908            // 'messageKey' field no longer required
909            unset( $settings[ 'messageKey' ] );
910            $settings[ self::OPENAPI_DESCRIPTION_KEY ] = $this->getJsonLocalizer()->localizeValue(
911                $settings, self::OPENAPI_DESCRIPTION_KEY,
912            );
913            $responseHeaderSettings[ $headerName ] = [
914                self::OPENAPI_DESCRIPTION_KEY => $settings[ self::OPENAPI_DESCRIPTION_KEY ],
915                'schema' => $settings[ 'schema' ]
916            ];
917        }
918        return $responseHeaderSettings;
919    }
920
921    /**
922     * Fetch the settings array mapping response headers to their descriptions and schemas
923     *
924     * Subclasses that return other headers should extend this function.
925     * Subclasses that use Response headers not defined in the ResponseHeaders class can
926     * hardcode the headers names as keys in this function as well.
927     *
928     * @stable to override
929     *
930     * @return array[] List of Response headers as constants from ResponseHeaders class
931     */
932    public function getResponseHeaderSettings(): array {
933        $responseHeaderSettings = [
934            ResponseHeaders::CACHE_CONTROL => ResponseHeaders::RESPONSE_HEADER_DEFINITIONS[
935                ResponseHeaders::CACHE_CONTROL
936            ]
937        ];
938
939        if ( $this->isDeprecated() ) {
940            $responseHeaderSettings[ ResponseHeaders::DEPRECATION ] = ResponseHeaders::RESPONSE_HEADER_DEFINITIONS[
941                ResponseHeaders::DEPRECATION
942            ];
943        }
944        return $responseHeaderSettings;
945    }
946
947    /**
948     * Returns the absolute path of a JSON file containing an OpenAPI Schema
949     * Object specification structure describing the response body.
950     *
951     * @see https://swagger.io/specification/#schema-object
952     *
953     * Returns null by default. Subclasses that return a JSON response
954     * should override this method to return a schema file path.
955     *
956     * The returned path must be absolute. Use `__DIR__` to construct the
957     * path relative to the handler file, e.g.
958     * `__DIR__ . '/Schema/Foo.json'`.
959     *
960     * @param string $method The HTTP method to produce a spec for ("get", "post", etc).
961     *
962     * @stable to override
963     * @since 1.43
964     * @return ?string
965     */
966    protected function getResponseBodySchemaFileName( string $method ): ?string {
967        return null;
968    }
969
970    /**
971     * Returns an OpenAPI Responses Object specification structure as an associative array.
972     *
973     * @see https://swagger.io/specification/#responses-object
974     *
975     * By default, this will contain basic information response for status 200, 400, and 500.
976     * The getResponseBodySchema() method is used to determine the structure of the response for status 200.
977     *
978     * Subclasses may override this to provide additional information about the structure of responses.
979     *
980     * @param string $method The HTTP method to produce a spec for ("get", "post", etc).
981     *
982     * @stable to override
983     * @return array
984     */
985    protected function generateResponseSpec( string $method ): array {
986        $ok = [ self::OPENAPI_DESCRIPTION_KEY => 'OK' ];
987
988        $bodySchema = $this->getResponseBodySchema( $method );
989
990        if ( $bodySchema ) {
991            $bodySchema = $this->getJsonLocalizer()->localizeJson( $bodySchema );
992            $ok['content']['application/json']['schema'] = $bodySchema;
993        }
994
995        // TODO: For Sitemap index and base tests the responsefactory is null.
996        // Follow up task to investigate this
997        if ( $this->responseFactory !== null ) {
998            $headersSpec = $this->getResponseHeaderSchemas();
999            $ok['headers'] = $headersSpec;
1000        }
1001
1002        // XXX: we should add info about redirects
1003        return [
1004            '200' => $ok,
1005            'default' => [ '$ref' => '#/components/responses/GenericErrorResponse' ],
1006        ];
1007    }
1008
1009    /**
1010     * Fetch the BodyValidator
1011     *
1012     * @deprecated since 1.43, return body properties from getBodyParamSettings().
1013     * Subclasses that need full control over body data parsing should override
1014     * parseBodyData() or implement validation in the execute() method based on
1015     * the unparsed body data returned by getRequest()->getBody().
1016     *
1017     * @param string $contentType Content type of the request.
1018     * @return BodyValidator A {@see NullBodyValidator} in this default implementation
1019     * @throws HttpException It's possible to fail early here when e.g. $contentType is unsupported,
1020     *  or later when {@see BodyValidator::validateBody} is called
1021     */
1022    public function getBodyValidator( $contentType ) {
1023        // NOTE: When removing this method, also remove the BodyValidator interface and
1024        //       all classes implementing it!
1025        return new NullBodyValidator();
1026    }
1027
1028    /**
1029     * Fetch the validated parameters. This must be called after validate() is
1030     * called. During execute() is fine.
1031     *
1032     * @return array Array mapping parameter names to validated values
1033     * @throws \RuntimeException If validate() has not been called
1034     */
1035    public function getValidatedParams() {
1036        if ( $this->validatedParams === null ) {
1037            throw new \RuntimeException( 'getValidatedParams() called before validate()' );
1038        }
1039        return $this->validatedParams;
1040    }
1041
1042    /**
1043     * Fetch the validated body
1044     * @return mixed|null Value returned by the body validator, or null if validate() was
1045     *  not called yet, validation failed, there was no body, or the body was form data.
1046     */
1047    public function getValidatedBody() {
1048        return $this->validatedBody;
1049    }
1050
1051    /**
1052     * Returns the parsed body of the request.
1053     * Should only be called if $request->hasBody() returns true.
1054     *
1055     * The default implementation handles application/x-www-form-urlencoded
1056     * and multipart/form-data by calling $request->getPostParams(),
1057     * if the list returned by getSupportedRequestTypes() includes these types.
1058     *
1059     * The default implementation handles application/json by parsing
1060     * the body content as JSON. Only object structures (maps) are supported,
1061     * other types will trigger an HttpException with status 400.
1062     *
1063     * Other content types will trigger a HttpException with status 415 per
1064     * default.
1065     *
1066     * Subclasses may override this method to support parsing additional
1067     * content types or to disallow content types by throwing an HttpException
1068     * with status 415. Subclasses may also return null to indicate that they
1069     * support reading the content, but intend to handle it as an unparsed
1070     * stream in their implementation of the execute() method.
1071     *
1072     * Subclasses that override this method to support additional request types
1073     * should also override getSupportedRequestTypes() to allow  that support
1074     * to be documented in the OpenAPI spec.
1075     *
1076     * @since 1.42
1077     *
1078     * @throws HttpException If the content type is not supported or the content
1079     *         is malformed.
1080     *
1081     * @return array|null The body content represented as an associative array,
1082     *         or null if the request body is accepted unparsed.
1083     */
1084    public function parseBodyData( RequestInterface $request ): ?array {
1085        // Parse the body based on its content type
1086        $contentType = $request->getBodyType();
1087
1088        // HACK: If the Handler uses a custom BodyValidator, the
1089        // getBodyValidator() is also responsible for checking whether
1090        // the content type is valid, and for parsing the body.
1091        // See T359149.
1092        // TODO: remove once no subclasses override getBodyValidator() anymore
1093        $bodyValidator = $this->getBodyValidator( $contentType ?? 'unknown/unknown' );
1094        if ( !$bodyValidator instanceof NullBodyValidator ) {
1095            // TODO: Trigger a deprecation warning.
1096            return null;
1097        }
1098
1099        $supportedTypes = $this->getSupportedRequestTypes();
1100        if ( $contentType !== null && !in_array( $contentType, $supportedTypes ) ) {
1101            throw new LocalizedHttpException(
1102                new MessageValue( 'rest-unsupported-content-type', [ $contentType ] ),
1103                415
1104            );
1105        }
1106
1107        // if it's supported and ends with "+json", we can probably parse it like a normal application/json request
1108        $contentType = str_ends_with( $contentType ?? '', '+json' )
1109            ? RequestInterface::JSON_CONTENT_TYPE
1110            : $contentType;
1111
1112        switch ( $contentType ) {
1113            case RequestInterface::FORM_URLENCODED_CONTENT_TYPE:
1114            case RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE:
1115                $params = $request->getPostParams();
1116                foreach ( $params as $key => $value ) {
1117                    $params[ $key ] = $this->recursiveUtfCleanup( $value );
1118                    // TODO: Warn if normalization was applied
1119                }
1120                return $params;
1121            case RequestInterface::JSON_CONTENT_TYPE:
1122                $jsonStream = $request->getBody();
1123                $jsonString = (string)$jsonStream;
1124                $normalizedJsonString = UtfNormalValidator::cleanUp( $jsonString );
1125                $parsedBody = json_decode( $normalizedJsonString, true );
1126                if ( !is_array( $parsedBody ) ) {
1127                    throw new LocalizedHttpException(
1128                        new MessageValue(
1129                            'rest-json-body-parse-error',
1130                            [ 'not a valid JSON object' ]
1131                        ),
1132                        400
1133                    );
1134                }
1135                // TODO: Warn if normalization was applied
1136                return $parsedBody;
1137            case null:
1138                // Specifying no Content-Type is fine if the body is empty
1139                if ( $request->getBody()->getSize() === 0 ) {
1140                    return null;
1141                }
1142            // no break, else fall through to the error below.
1143            default:
1144                throw new LocalizedHttpException(
1145                    new MessageValue( 'rest-unsupported-content-type', [ $contentType ?? '(null)' ] ),
1146                    415
1147                );
1148        }
1149    }
1150
1151    /**
1152     * Recursively applies unicode normalization
1153     *
1154     * @param mixed $value
1155     *
1156     * @return mixed
1157     */
1158    private function recursiveUtfCleanup( $value ) {
1159        if ( is_string( $value ) ) {
1160            return UtfNormalValidator::cleanUp( $value );
1161        } elseif ( is_array( $value ) ) {
1162            foreach ( $value as $k => $v ) {
1163                $value[ $k ] = $this->recursiveUtfCleanup( $v );
1164                // TODO: Warn if normalization was applied
1165                // TODO: also normalize key
1166            }
1167
1168            return $value;
1169        } else {
1170            return $value;
1171        }
1172    }
1173
1174    /**
1175     * Returns the content types that should be accepted by parseBodyData().
1176     *
1177     * Subclasses that support request types other than application/json
1178     * should override this method.
1179     *
1180     * If "application/x-www-form-urlencoded" or "multipart/form-data" are
1181     * returned, parseBodyData() will use $request->getPostParams() to determine
1182     * the body data.
1183     *
1184     * @note The return value of this method is ignored for requests
1185     * using a method listed in Validator::NO_BODY_METHODS,
1186     * in particular for the GET method.
1187     *
1188     * @note for backwards compatibility, the default implementation of this
1189     * method will examine the parameter definitions returned by getParamSettings()
1190     * to see if any of the parameters are declared as "post" parameters. If this
1191     * is the case, support for "application/x-www-form-urlencoded" and
1192     * "multipart/form-data" is added. This may change in future releases.
1193     * It is preferred to use "body" parameters and override this method explicitly
1194     * when support for form data is desired.
1195     *
1196     * @stable to override
1197     *
1198     * @return string[] A list of content-types
1199     */
1200    public function getSupportedRequestTypes(): array {
1201        $types = [
1202            RequestInterface::JSON_CONTENT_TYPE
1203        ];
1204
1205        // TODO: remove this once "post" parameters are no longer supported! T362850
1206        foreach ( $this->getParamSettings() as $settings ) {
1207            if ( ( $settings[self::PARAM_SOURCE] ?? null ) === 'post' ) {
1208                $types[] = RequestInterface::FORM_URLENCODED_CONTENT_TYPE;
1209                $types[] = RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE;
1210                break;
1211            }
1212        }
1213
1214        return $types;
1215    }
1216
1217    /**
1218     * Get a HookContainer, for running extension hooks or for hook metadata.
1219     *
1220     * @since 1.35
1221     * @return HookContainer
1222     */
1223    protected function getHookContainer() {
1224        return $this->hookContainer;
1225    }
1226
1227    /**
1228     * Get a HookRunner for running core hooks.
1229     *
1230     * @internal This is for use by core only. Hook interfaces may be removed
1231     *   without notice.
1232     * @since 1.35
1233     * @return HookRunner
1234     */
1235    protected function getHookRunner() {
1236        return $this->hookRunner;
1237    }
1238
1239    /**
1240     * The subclass should override this to provide the maximum last modified
1241     * timestamp of the requested resource. This is called before execute() in
1242     * order to decide whether to send a 304. If the request is going to
1243     * change the state of the resource, the time returned must represent
1244     * the last modification date before the change. In other words, it must
1245     * provide the timestamp of the entity that the change is going to be
1246     * applied to.
1247     *
1248     * For GET and HEAD requests, this value will automatically be included
1249     * in the response in the Last-Modified header.
1250     *
1251     * Handlers that modify the resource and want to return a Last-Modified
1252     * header representing the new state in the response should set the header
1253     * in the execute() method.
1254     *
1255     * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics.
1256     *
1257     * @stable to override
1258     *
1259     * @return string|int|float|DateTime|null
1260     */
1261    protected function getLastModified() {
1262        return null;
1263    }
1264
1265    /**
1266     * The subclass should override this to provide an ETag for the current
1267     * state of the requested resource. This is called before execute() in
1268     * order to decide whether to send a 304. If the request is going to
1269     * change the state of the resource, the ETag returned must represent
1270     * the state before the change. In other words, it must identify
1271     * the entity that the change is going to be applied to.
1272     *
1273     * For GET and HEAD requests, this ETag will also be included in the
1274     * response.
1275     *
1276     * Handlers that modify the resource and want to return an ETag
1277     * header representing the new state in the response should set the header
1278     * in the execute() method. However, note that responses to PUT requests
1279     * must not return an ETag unless the new content of the resource is exactly
1280     * the data that was sent by the client in the request body.
1281     *
1282     * This must be a complete ETag, including double quotes.
1283     * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics.
1284     *
1285     * This method should return null if the resource doesn't exist. It may also
1286     * return null if ETag semantics is not supported by the Handler.
1287     *
1288     * @stable to override
1289     *
1290     * @return string|null
1291     */
1292    protected function getETag() {
1293        return null;
1294    }
1295
1296    /**
1297     * The subclass should override this to indicate whether the resource
1298     * exists. This is used for wildcard validators, for example "If-Match: *"
1299     * fails if the resource does not exist.
1300     *
1301     * If this method returns null, the value returned by getETag() will be used
1302     * to determine whether the resource exists.
1303     *
1304     * In a state-changing request, the return value of this method should
1305     * reflect the state before the requested change is applied.
1306     *
1307     * @stable to override
1308     *
1309     * @return bool|null
1310     */
1311    protected function hasRepresentation() {
1312        return null;
1313    }
1314
1315    /**
1316     * Indicates whether this route requires read rights.
1317     *
1318     * The handler should override this if it does not need to read from the
1319     * wiki. This is uncommon, but may be useful for login and other account
1320     * management APIs.
1321     *
1322     * @stable to override
1323     *
1324     * @return bool
1325     */
1326    public function needsReadAccess() {
1327        return true;
1328    }
1329
1330    /**
1331     * Indicates whether this route requires write access to the wiki.
1332     *
1333     * Handlers may override this method to return false if and only if the operation they
1334     * implement is "safe" per RFC 7231 section 4.2.1. A handler's operation is "safe" if
1335     * it is essentially read-only, i.e. the client does not request nor expect any state
1336     * change that would be observable in the responses to future requests.
1337     *
1338     * Implementations of this method must always return the same value, regardless of the
1339     * parameters passed to the constructor or system state.
1340     *
1341     * Handlers for GET, HEAD, OPTIONS, and TRACE requests should each implement a "safe"
1342     * operation. Handlers of PUT and DELETE requests should each implement a non-"safe"
1343     * operation. Note that handlers of POST requests can implement a "safe" operation,
1344     * particularly in the case where large input parameters are required.
1345     *
1346     * The information provided by this method is used to perform basic authorization checks
1347     * and to determine whether cross-origin requests are safe.
1348     *
1349     * @stable to override
1350     *
1351     * @return bool
1352     */
1353    public function needsWriteAccess() {
1354        return true;
1355    }
1356
1357    /**
1358     * Indicates whether this route can be accessed only by session providers safe vs csrf
1359     *
1360     * The handler should override this if the route must only be accessed by session
1361     * providers that are safe against csrf.
1362     *
1363     * A return value of false does not necessarily mean the route is vulnerable to csrf attacks.
1364     * It means the route can be accessed by session providers that are not automatically safe
1365     * against csrf attacks, so the possibility of csrf attacks must be considered.
1366     *
1367     * @stable to override
1368     *
1369     * @return bool
1370     */
1371    public function requireSafeAgainstCsrf() {
1372        return false;
1373    }
1374
1375    /**
1376     * The handler can override this to do any necessary setup after the init functions
1377     * are called to inject dependencies.
1378     *
1379     * @stable to override
1380     * @throws HttpException if the handler does not accept the request for
1381     *         some reason.
1382     */
1383    protected function postInitSetup() {
1384    }
1385
1386    /**
1387     * The handler can override this to do any necessary setup after validate()
1388     * has been called. This gives the handler an opportunity to do initialization
1389     * based on parameters before pre-execution calls like getLastModified() or getETag().
1390     *
1391     * @stable to override
1392     * @since 1.36
1393     */
1394    protected function postValidationSetup() {
1395    }
1396
1397    /**
1398     * Execute the handler. This is called after parameter validation. The
1399     * return value can either be a Response or any type accepted by
1400     * ResponseFactory::createFromReturnValue().
1401     *
1402     * To automatically construct an error response, execute() should throw a
1403     * \MediaWiki\Rest\HttpException. Such exceptions will not be logged like
1404     * a normal exception.
1405     *
1406     * If execute() throws any other kind of exception, the exception will be
1407     * logged and a generic 500 error page will be shown.
1408     *
1409     * @stable to override
1410     *
1411     * @return mixed
1412     */
1413    abstract public function execute();
1414}