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