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