Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.65% |
280 / 299 |
|
78.43% |
40 / 51 |
CRAP | |
0.00% |
0 / 1 |
| Handler | |
93.65% |
280 / 299 |
|
78.43% |
40 / 51 |
119.45 | |
0.00% |
0 / 1 |
| initContext | |
100.00% |
8 / 8 |
|
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 | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 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 | |||
| isDeprecated | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| getDeprecatedDate | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| validate | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
| applyDeprecationHeader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| detectExtraneousBodyFields | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| checkSession | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
| getJsonLocalizer | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| getConditionalHeaderUtil | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| checkPreconditions | |
100.00% |
7 / 7 |
|
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 | |||
| getOpenApiSpec | |
88.46% |
23 / 26 |
|
0.00% |
0 / 1 |
9.12 | |||
| 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 | |||
| generateResponseSpec | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| 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 | |||
| recursiveUtfCleanup | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| 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 | /** @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 | $this->validatedParams = $restValidator->validateParams( |
| 422 | $this->getParamSettings() |
| 423 | ); |
| 424 | |
| 425 | $bodyType = $this->request->getBodyType(); |
| 426 | $legacyBodyValidator = $bodyType === null ? null |
| 427 | : $this->getBodyValidator( $bodyType ); |
| 428 | |
| 429 | if ( $legacyBodyValidator && !$legacyBodyValidator instanceof NullBodyValidator ) { |
| 430 | $this->validatedBody = $restValidator->validateBody( $this->request, $this ); |
| 431 | } else { |
| 432 | // Allow type coercion if the request body is form data. |
| 433 | // For JSON requests, insist on proper types. |
| 434 | $enforceTypes = !in_array( |
| 435 | $this->request->getBodyType(), |
| 436 | RequestInterface::FORM_DATA_CONTENT_TYPES |
| 437 | ); |
| 438 | |
| 439 | $this->validatedBody = $restValidator->validateBodyParams( |
| 440 | $this->getBodyParamSettings(), |
| 441 | $enforceTypes |
| 442 | ); |
| 443 | |
| 444 | // If there is a body, check if it contains extra fields. |
| 445 | if ( $this->getRequest()->hasBody() ) { |
| 446 | $this->detectExtraneousBodyFields( $restValidator ); |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | $this->postValidationSetup(); |
| 451 | } |
| 452 | |
| 453 | /** |
| 454 | * Apply Deprecation header per RFC 9745. |
| 455 | * |
| 456 | * @since 1.45 |
| 457 | * @stable to override |
| 458 | * @see https://www.rfc-editor.org/rfc/rfc9745.txt |
| 459 | * |
| 460 | * @param ResponseInterface $response |
| 461 | */ |
| 462 | public function applyDeprecationHeader( ResponseInterface $response ) { |
| 463 | $dd = $this->getDeprecatedDate(); |
| 464 | if ( $dd !== null && !$response->getHeaderLine( 'Deprecation' ) ) { |
| 465 | $response->setHeader( 'Deprecation', '@' . $dd ); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | /** |
| 470 | * Subclasses may override this to disable or modify checks for extraneous |
| 471 | * body fields. |
| 472 | * |
| 473 | * @since 1.42 |
| 474 | * @stable to override |
| 475 | * @param Validator $restValidator |
| 476 | * @throws HttpException On validation failure. |
| 477 | */ |
| 478 | protected function detectExtraneousBodyFields( Validator $restValidator ) { |
| 479 | $parsedBody = $this->getRequest()->getParsedBody(); |
| 480 | |
| 481 | if ( !$parsedBody ) { |
| 482 | // nothing to do |
| 483 | return; |
| 484 | } |
| 485 | |
| 486 | $restValidator->detectExtraneousBodyFields( |
| 487 | $this->getBodyParamSettings(), |
| 488 | $parsedBody |
| 489 | ); |
| 490 | } |
| 491 | |
| 492 | /** |
| 493 | * Check the session (and session provider) |
| 494 | * @throws HttpException on failed check |
| 495 | * @internal |
| 496 | */ |
| 497 | public function checkSession() { |
| 498 | if ( !$this->session->getProvider()->safeAgainstCsrf() ) { |
| 499 | if ( $this->requireSafeAgainstCsrf() ) { |
| 500 | throw new LocalizedHttpException( |
| 501 | new MessageValue( 'rest-requires-safe-against-csrf' ), |
| 502 | 400 |
| 503 | ); |
| 504 | } |
| 505 | } elseif ( !empty( $this->validatedBody['token'] ) ) { |
| 506 | throw new LocalizedHttpException( |
| 507 | new MessageValue( 'rest-extraneous-csrf-token' ), |
| 508 | 400 |
| 509 | ); |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | /** |
| 514 | * Get a JsonLocalizer object. |
| 515 | * |
| 516 | * @return JsonLocalizer |
| 517 | */ |
| 518 | protected function getJsonLocalizer(): JsonLocalizer { |
| 519 | Assert::precondition( |
| 520 | $this->responseFactory !== null, |
| 521 | 'getJsonLocalizer() must not be called before initServices()' |
| 522 | ); |
| 523 | |
| 524 | if ( $this->jsonLocalizer === null ) { |
| 525 | $this->jsonLocalizer = new JsonLocalizer( $this->responseFactory ); |
| 526 | } |
| 527 | |
| 528 | return $this->jsonLocalizer; |
| 529 | } |
| 530 | |
| 531 | /** |
| 532 | * Get a ConditionalHeaderUtil object. |
| 533 | * |
| 534 | * On the first call to this method, the object will be initialized with |
| 535 | * validator values by calling getETag(), getLastModified() and |
| 536 | * hasRepresentation(). |
| 537 | * |
| 538 | * @return ConditionalHeaderUtil |
| 539 | */ |
| 540 | protected function getConditionalHeaderUtil() { |
| 541 | if ( $this->conditionalHeaderUtil === null ) { |
| 542 | $this->conditionalHeaderUtil = new ConditionalHeaderUtil; |
| 543 | |
| 544 | // NOTE: It would be nicer to have Handler implement a |
| 545 | // ConditionalHeaderValues interface that defines methods that |
| 546 | // ConditionalHeaderUtil can call. But the relevant methods already |
| 547 | // exist in Handler as protected and stable to override. |
| 548 | // We can't make them public without breaking all subclasses that |
| 549 | // override them. So we pass closures for now. |
| 550 | $this->conditionalHeaderUtil->setValidators( |
| 551 | $this->getETag( ... ), |
| 552 | $this->getLastModified( ... ), |
| 553 | $this->hasRepresentation( ... ) |
| 554 | ); |
| 555 | } |
| 556 | return $this->conditionalHeaderUtil; |
| 557 | } |
| 558 | |
| 559 | /** |
| 560 | * Check the conditional request headers and generate a response if appropriate. |
| 561 | * This is called by the Router before execute() and may be overridden. |
| 562 | * |
| 563 | * @stable to override |
| 564 | * |
| 565 | * @return ResponseInterface|null |
| 566 | */ |
| 567 | public function checkPreconditions() { |
| 568 | $status = $this->getConditionalHeaderUtil()->checkPreconditions( $this->getRequest() ); |
| 569 | if ( $status ) { |
| 570 | $response = $this->getResponseFactory()->create(); |
| 571 | $response->setStatus( $status ); |
| 572 | $this->applyConditionalResponseHeaders( $response ); |
| 573 | return $response; |
| 574 | } |
| 575 | |
| 576 | return null; |
| 577 | } |
| 578 | |
| 579 | /** |
| 580 | * Apply verifier headers to the response, per RFC 7231 §7.2. |
| 581 | * This is called after execute() returns. |
| 582 | * |
| 583 | * For GET and HEAD requests, the default behavior is to set the ETag and |
| 584 | * Last-Modified headers based on the values returned by getETag() and |
| 585 | * getLastModified() when they were called before execute() was run. |
| 586 | * |
| 587 | * Other request methods are assumed to be state-changing, so no headers |
| 588 | * will be set by default. |
| 589 | * |
| 590 | * This may be overridden to modify the verifier headers sent in the response. |
| 591 | * However, handlers that modify the resource's state would typically just |
| 592 | * set the ETag and Last-Modified headers in the execute() method. |
| 593 | * |
| 594 | * @stable to override |
| 595 | * |
| 596 | * @param ResponseInterface $response |
| 597 | */ |
| 598 | public function applyConditionalResponseHeaders( ResponseInterface $response ) { |
| 599 | $method = $this->getRequest()->getMethod(); |
| 600 | if ( $method === 'GET' || $method === 'HEAD' ) { |
| 601 | $this->getConditionalHeaderUtil()->applyResponseHeaders( $response ); |
| 602 | } |
| 603 | } |
| 604 | |
| 605 | /** |
| 606 | * Apply cache control to enforce privacy. |
| 607 | */ |
| 608 | public function applyCacheControl( ResponseInterface $response ) { |
| 609 | // NOTE: keep this consistent with the logic in OutputPage::sendCacheControl |
| 610 | |
| 611 | // If the response sets cookies, it must not be cached in proxies. |
| 612 | // If there's an active cookie-based session (logged-in user or anonymous user with |
| 613 | // session-scoped cookies), it is not safe to cache either, as the session manager may set |
| 614 | // cookies in the response, or the response itself may vary on user-specific variables, |
| 615 | // for example on private wikis where the 'read' permission is restricted. (T264631) |
| 616 | if ( $response->getHeaderLine( 'Set-Cookie' ) || $this->getSession()->isPersistent() ) { |
| 617 | $response->setHeader( 'Cache-Control', 'private,must-revalidate,s-maxage=0' ); |
| 618 | } |
| 619 | |
| 620 | if ( !$response->getHeaderLine( 'Cache-Control' ) ) { |
| 621 | $rqMethod = $this->getRequest()->getMethod(); |
| 622 | if ( $rqMethod !== 'GET' && $rqMethod !== 'HEAD' ) { |
| 623 | // Responses to requests other than GET or HEAD should not be cacheable by default. |
| 624 | $response->setHeader( 'Cache-Control', 'private,no-cache,s-maxage=0' ); |
| 625 | } |
| 626 | } |
| 627 | } |
| 628 | |
| 629 | /** |
| 630 | * Fetch ParamValidator settings for parameters |
| 631 | * |
| 632 | * Every setting must include self::PARAM_SOURCE to specify which part of |
| 633 | * the request is to contain the parameter. |
| 634 | * |
| 635 | * Can be used for the request body as well, by setting self::PARAM_SOURCE |
| 636 | * to "post". Note that the values of "post" parameters will be accessible |
| 637 | * through getValidatedParams(). "post" parameters are used with |
| 638 | * form data (application/x-www-form-urlencoded or multipart/form-data). |
| 639 | * |
| 640 | * For "query" parameters, a PARAM_REQUIRED setting of "false" means the caller |
| 641 | * does not have to supply the parameter. For "path" parameters, the path matcher will always |
| 642 | * require the caller to supply all path parameters for a route, regardless of the |
| 643 | * PARAM_REQUIRED setting. However, "path" parameters may be specified in getParamSettings() |
| 644 | * as non-required to indicate that the handler services multiple routes, some of which may |
| 645 | * not supply the parameter. |
| 646 | * |
| 647 | * @stable to override |
| 648 | * |
| 649 | * @return array[] Associative array mapping parameter names to |
| 650 | * ParamValidator settings arrays |
| 651 | */ |
| 652 | public function getParamSettings() { |
| 653 | return []; |
| 654 | } |
| 655 | |
| 656 | /** |
| 657 | * Fetch ParamValidator settings for body fields. Parameters defined |
| 658 | * by this method are used to validate the request body. The parameter |
| 659 | * values will become available through getValidatedBody(). |
| 660 | * |
| 661 | * Subclasses may override this method to specify what fields they support |
| 662 | * in the request body. All parameter settings returned by this method must |
| 663 | * have self::PARAM_SOURCE set to 'body'. |
| 664 | * |
| 665 | * @return array[] |
| 666 | */ |
| 667 | public function getBodyParamSettings(): array { |
| 668 | return []; |
| 669 | } |
| 670 | |
| 671 | /** |
| 672 | * Returns an OpenAPI Operation Object specification structure as an associative array. |
| 673 | * |
| 674 | * @see https://swagger.io/specification/#operation-object |
| 675 | * |
| 676 | * By default, this will contain information about the supported parameters, as well as |
| 677 | * the response for status 200. |
| 678 | * |
| 679 | * Subclasses may override this to provide additional information. |
| 680 | * |
| 681 | * @since 1.42 |
| 682 | * @stable to override |
| 683 | * |
| 684 | * @param string $method The HTTP method to produce a spec for ("get", "post", etc). |
| 685 | * Useful for handlers that behave differently depending on the |
| 686 | * request method. |
| 687 | * |
| 688 | * @return array |
| 689 | */ |
| 690 | public function getOpenApiSpec( string $method ): array { |
| 691 | $parameters = []; |
| 692 | |
| 693 | $supportedPathParams = array_flip( $this->getSupportedPathParams() ); |
| 694 | |
| 695 | foreach ( $this->getParamSettings() as $name => $setting ) { |
| 696 | $source = $setting[ Validator::PARAM_SOURCE ] ?? ''; |
| 697 | |
| 698 | if ( $source !== 'query' && $source !== 'path' ) { |
| 699 | continue; |
| 700 | } |
| 701 | |
| 702 | if ( $source === 'path' && !isset( $supportedPathParams[$name] ) ) { |
| 703 | // Skip optional path param not used in the current path |
| 704 | continue; |
| 705 | } |
| 706 | |
| 707 | $setting[ Validator::PARAM_DESCRIPTION ] = $this->getJsonLocalizer()->localizeValue( |
| 708 | $setting, Validator::PARAM_DESCRIPTION, |
| 709 | ); |
| 710 | |
| 711 | $param = Validator::getParameterSpec( $name, $setting ); |
| 712 | |
| 713 | $parameters[] = $param; |
| 714 | } |
| 715 | |
| 716 | $spec = [ |
| 717 | 'parameters' => $parameters, |
| 718 | 'responses' => $this->generateResponseSpec( $method ), |
| 719 | ]; |
| 720 | |
| 721 | if ( !in_array( $method, RequestInterface::NO_BODY_METHODS ) ) { |
| 722 | $requestBody = $this->getRequestSpec( $method ); |
| 723 | if ( $requestBody ) { |
| 724 | $spec['requestBody'] = $requestBody; |
| 725 | } |
| 726 | } |
| 727 | |
| 728 | // TODO: Allow additional information about parameters and responses to |
| 729 | // be provided in the route definition. |
| 730 | $spec += $this->openApiSpec; |
| 731 | |
| 732 | if ( $this->isDeprecated() ) { |
| 733 | $spec['deprecated'] = true; |
| 734 | unset( $spec['deprecationSettings'] ); |
| 735 | } |
| 736 | |
| 737 | return $spec; |
| 738 | } |
| 739 | |
| 740 | /** |
| 741 | * Returns an OpenAPI Request Body Object specification structure as an associative array. |
| 742 | * |
| 743 | * @see https://swagger.io/specification/#request-body-object |
| 744 | * |
| 745 | * This is based on the getBodyParamSettings() and getSupportedRequestTypes(). |
| 746 | * |
| 747 | * Subclasses may override this to provide additional information about the |
| 748 | * structure of responses, or to add support for additional mediaTypes. |
| 749 | * |
| 750 | * @stable to override getBodySchema() to generate a schema for each |
| 751 | * supported media type as returned by getSupportedBodyTypes(). |
| 752 | * |
| 753 | * @param string $method |
| 754 | * |
| 755 | * @return ?array |
| 756 | */ |
| 757 | protected function getRequestSpec( string $method ): ?array { |
| 758 | $mediaTypes = []; |
| 759 | |
| 760 | foreach ( $this->getSupportedRequestTypes() as $type ) { |
| 761 | $schema = $this->getRequestBodySchema( $type ); |
| 762 | |
| 763 | if ( $schema ) { |
| 764 | $mediaTypes[$type] = [ 'schema' => $schema ]; |
| 765 | } |
| 766 | } |
| 767 | |
| 768 | if ( !$mediaTypes ) { |
| 769 | return null; |
| 770 | } |
| 771 | |
| 772 | return [ |
| 773 | // TODO: some DELETE handlers may require a body that contains a token |
| 774 | // FIXME: check if there are required body params! |
| 775 | 'required' => in_array( $method, RequestInterface::BODY_METHODS ), |
| 776 | 'content' => $mediaTypes |
| 777 | ]; |
| 778 | } |
| 779 | |
| 780 | /** |
| 781 | * Returns a content schema per the OpenAPI spec. |
| 782 | * @see https://swagger.io/specification/#schema-object |
| 783 | * |
| 784 | * Per default, this provides schemas for JSON requests and form data, based |
| 785 | * on the parameter declarations returned by getParamSettings(). |
| 786 | * |
| 787 | * Subclasses may override this to provide additional information about the |
| 788 | * structure of responses, or to add support for additional mediaTypes. |
| 789 | * |
| 790 | * @stable to override |
| 791 | * @return array |
| 792 | */ |
| 793 | protected function getRequestBodySchema( string $mediaType ): array { |
| 794 | if ( $mediaType === RequestInterface::FORM_URLENCODED_CONTENT_TYPE ) { |
| 795 | $allowedSources = [ 'body', 'post' ]; |
| 796 | } elseif ( $mediaType === RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE ) { |
| 797 | $allowedSources = [ 'body', 'post' ]; |
| 798 | } else { |
| 799 | $allowedSources = [ 'body' ]; |
| 800 | } |
| 801 | |
| 802 | $paramSettings = $this->getBodyParamSettings(); |
| 803 | |
| 804 | $properties = []; |
| 805 | $required = []; |
| 806 | |
| 807 | foreach ( $paramSettings as $name => $settings ) { |
| 808 | $source = $settings[ Validator::PARAM_SOURCE ] ?? ''; |
| 809 | $isRequired = $settings[ ParamValidator::PARAM_REQUIRED ] ?? false; |
| 810 | |
| 811 | if ( !in_array( $source, $allowedSources ) ) { |
| 812 | // TODO: post parameters also work as body parameters... |
| 813 | continue; |
| 814 | } |
| 815 | |
| 816 | $properties[$name] = Validator::getParameterSchema( $settings ); |
| 817 | $properties[$name][self::OPENAPI_DESCRIPTION_KEY] = |
| 818 | $this->getJsonLocalizer()->localizeValue( $settings, Validator::PARAM_DESCRIPTION ) |
| 819 | ?? "$name parameter"; |
| 820 | |
| 821 | if ( $isRequired ) { |
| 822 | $required[] = $name; |
| 823 | } |
| 824 | } |
| 825 | |
| 826 | if ( !$properties ) { |
| 827 | return []; |
| 828 | } |
| 829 | |
| 830 | $schema = [ |
| 831 | 'type' => 'object', |
| 832 | 'properties' => $properties, |
| 833 | ]; |
| 834 | |
| 835 | if ( $required ) { |
| 836 | $schema['required'] = $required; |
| 837 | } |
| 838 | |
| 839 | return $schema; |
| 840 | } |
| 841 | |
| 842 | /** |
| 843 | * Returns an OpenAPI Schema Object specification structure as an associative array. |
| 844 | * |
| 845 | * @see https://swagger.io/specification/#schema-object |
| 846 | * |
| 847 | * Returns null by default. Subclasses that return a JSON response should |
| 848 | * implement this method to return a schema of the response body. |
| 849 | * |
| 850 | * @param string $method The HTTP method to produce a spec for ("get", "post", etc). |
| 851 | * |
| 852 | * @stable to override |
| 853 | * @return ?array |
| 854 | */ |
| 855 | protected function getResponseBodySchema( string $method ): ?array { |
| 856 | $file = $this->getResponseBodySchemaFileName( $method ); |
| 857 | return $file ? Module::loadJsonFile( $file ) : null; |
| 858 | } |
| 859 | |
| 860 | /** |
| 861 | * Returns the path and name of a JSON file containing an OpenAPI Schema Object |
| 862 | * specification structure. |
| 863 | * |
| 864 | * @see https://swagger.io/specification/#schema-object |
| 865 | * |
| 866 | * Returns null by default. Subclasses with a suitable JSON file should implement this method. |
| 867 | * |
| 868 | * @param string $method The HTTP method to produce a spec for ("get", "post", etc). |
| 869 | * |
| 870 | * @stable to override |
| 871 | * @since 1.43 |
| 872 | * @return ?string |
| 873 | */ |
| 874 | protected function getResponseBodySchemaFileName( string $method ): ?string { |
| 875 | return null; |
| 876 | } |
| 877 | |
| 878 | /** |
| 879 | * Returns an OpenAPI Responses Object specification structure as an associative array. |
| 880 | * |
| 881 | * @see https://swagger.io/specification/#responses-object |
| 882 | * |
| 883 | * By default, this will contain basic information response for status 200, 400, and 500. |
| 884 | * The getResponseBodySchema() method is used to determine the structure of the response for status 200. |
| 885 | * |
| 886 | * Subclasses may override this to provide additional information about the structure of responses. |
| 887 | * |
| 888 | * @param string $method The HTTP method to produce a spec for ("get", "post", etc). |
| 889 | * |
| 890 | * @stable to override |
| 891 | * @return array |
| 892 | */ |
| 893 | protected function generateResponseSpec( string $method ): array { |
| 894 | $ok = [ self::OPENAPI_DESCRIPTION_KEY => 'OK' ]; |
| 895 | |
| 896 | $bodySchema = $this->getResponseBodySchema( $method ); |
| 897 | |
| 898 | if ( $bodySchema ) { |
| 899 | $bodySchema = $this->getJsonLocalizer()->localizeJson( $bodySchema ); |
| 900 | $ok['content']['application/json']['schema'] = $bodySchema; |
| 901 | } |
| 902 | |
| 903 | // XXX: we should add info about redirects |
| 904 | return [ |
| 905 | '200' => $ok, |
| 906 | 'default' => [ '$ref' => '#/components/responses/GenericErrorResponse' ], |
| 907 | ]; |
| 908 | } |
| 909 | |
| 910 | /** |
| 911 | * Fetch the BodyValidator |
| 912 | * |
| 913 | * @deprecated since 1.43, return body properties from getBodyParamSettings(). |
| 914 | * Subclasses that need full control over body data parsing should override |
| 915 | * parseBodyData() or implement validation in the execute() method based on |
| 916 | * the unparsed body data returned by getRequest()->getBody(). |
| 917 | * |
| 918 | * @param string $contentType Content type of the request. |
| 919 | * @return BodyValidator A {@see NullBodyValidator} in this default implementation |
| 920 | * @throws HttpException It's possible to fail early here when e.g. $contentType is unsupported, |
| 921 | * or later when {@see BodyValidator::validateBody} is called |
| 922 | */ |
| 923 | public function getBodyValidator( $contentType ) { |
| 924 | // NOTE: When removing this method, also remove the BodyValidator interface and |
| 925 | // all classes implementing it! |
| 926 | return new NullBodyValidator(); |
| 927 | } |
| 928 | |
| 929 | /** |
| 930 | * Fetch the validated parameters. This must be called after validate() is |
| 931 | * called. During execute() is fine. |
| 932 | * |
| 933 | * @return array Array mapping parameter names to validated values |
| 934 | * @throws \RuntimeException If validate() has not been called |
| 935 | */ |
| 936 | public function getValidatedParams() { |
| 937 | if ( $this->validatedParams === null ) { |
| 938 | throw new \RuntimeException( 'getValidatedParams() called before validate()' ); |
| 939 | } |
| 940 | return $this->validatedParams; |
| 941 | } |
| 942 | |
| 943 | /** |
| 944 | * Fetch the validated body |
| 945 | * @return mixed|null Value returned by the body validator, or null if validate() was |
| 946 | * not called yet, validation failed, there was no body, or the body was form data. |
| 947 | */ |
| 948 | public function getValidatedBody() { |
| 949 | return $this->validatedBody; |
| 950 | } |
| 951 | |
| 952 | /** |
| 953 | * Returns the parsed body of the request. |
| 954 | * Should only be called if $request->hasBody() returns true. |
| 955 | * |
| 956 | * The default implementation handles application/x-www-form-urlencoded |
| 957 | * and multipart/form-data by calling $request->getPostParams(), |
| 958 | * if the list returned by getSupportedRequestTypes() includes these types. |
| 959 | * |
| 960 | * The default implementation handles application/json by parsing |
| 961 | * the body content as JSON. Only object structures (maps) are supported, |
| 962 | * other types will trigger an HttpException with status 400. |
| 963 | * |
| 964 | * Other content types will trigger a HttpException with status 415 per |
| 965 | * default. |
| 966 | * |
| 967 | * Subclasses may override this method to support parsing additional |
| 968 | * content types or to disallow content types by throwing an HttpException |
| 969 | * with status 415. Subclasses may also return null to indicate that they |
| 970 | * support reading the content, but intend to handle it as an unparsed |
| 971 | * stream in their implementation of the execute() method. |
| 972 | * |
| 973 | * Subclasses that override this method to support additional request types |
| 974 | * should also override getSupportedRequestTypes() to allow that support |
| 975 | * to be documented in the OpenAPI spec. |
| 976 | * |
| 977 | * @since 1.42 |
| 978 | * |
| 979 | * @throws HttpException If the content type is not supported or the content |
| 980 | * is malformed. |
| 981 | * |
| 982 | * @return array|null The body content represented as an associative array, |
| 983 | * or null if the request body is accepted unparsed. |
| 984 | */ |
| 985 | public function parseBodyData( RequestInterface $request ): ?array { |
| 986 | // Parse the body based on its content type |
| 987 | $contentType = $request->getBodyType(); |
| 988 | |
| 989 | // HACK: If the Handler uses a custom BodyValidator, the |
| 990 | // getBodyValidator() is also responsible for checking whether |
| 991 | // the content type is valid, and for parsing the body. |
| 992 | // See T359149. |
| 993 | // TODO: remove once no subclasses override getBodyValidator() anymore |
| 994 | $bodyValidator = $this->getBodyValidator( $contentType ?? 'unknown/unknown' ); |
| 995 | if ( !$bodyValidator instanceof NullBodyValidator ) { |
| 996 | // TODO: Trigger a deprecation warning. |
| 997 | return null; |
| 998 | } |
| 999 | |
| 1000 | $supportedTypes = $this->getSupportedRequestTypes(); |
| 1001 | if ( $contentType !== null && !in_array( $contentType, $supportedTypes ) ) { |
| 1002 | throw new LocalizedHttpException( |
| 1003 | new MessageValue( 'rest-unsupported-content-type', [ $contentType ] ), |
| 1004 | 415 |
| 1005 | ); |
| 1006 | } |
| 1007 | |
| 1008 | // if it's supported and ends with "+json", we can probably parse it like a normal application/json request |
| 1009 | $contentType = str_ends_with( $contentType ?? '', '+json' ) |
| 1010 | ? RequestInterface::JSON_CONTENT_TYPE |
| 1011 | : $contentType; |
| 1012 | |
| 1013 | switch ( $contentType ) { |
| 1014 | case RequestInterface::FORM_URLENCODED_CONTENT_TYPE: |
| 1015 | case RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE: |
| 1016 | $params = $request->getPostParams(); |
| 1017 | foreach ( $params as $key => $value ) { |
| 1018 | $params[ $key ] = $this->recursiveUtfCleanup( $value ); |
| 1019 | // TODO: Warn if normalization was applied |
| 1020 | } |
| 1021 | return $params; |
| 1022 | case RequestInterface::JSON_CONTENT_TYPE: |
| 1023 | $jsonStream = $request->getBody(); |
| 1024 | $jsonString = (string)$jsonStream; |
| 1025 | $normalizedJsonString = UtfNormalValidator::cleanUp( $jsonString ); |
| 1026 | $parsedBody = json_decode( $normalizedJsonString, true ); |
| 1027 | if ( !is_array( $parsedBody ) ) { |
| 1028 | throw new LocalizedHttpException( |
| 1029 | new MessageValue( |
| 1030 | 'rest-json-body-parse-error', |
| 1031 | [ 'not a valid JSON object' ] |
| 1032 | ), |
| 1033 | 400 |
| 1034 | ); |
| 1035 | } |
| 1036 | // TODO: Warn if normalization was applied |
| 1037 | return $parsedBody; |
| 1038 | case null: |
| 1039 | // Specifying no Content-Type is fine if the body is empty |
| 1040 | if ( $request->getBody()->getSize() === 0 ) { |
| 1041 | return null; |
| 1042 | } |
| 1043 | // no break, else fall through to the error below. |
| 1044 | default: |
| 1045 | throw new LocalizedHttpException( |
| 1046 | new MessageValue( 'rest-unsupported-content-type', [ $contentType ?? '(null)' ] ), |
| 1047 | 415 |
| 1048 | ); |
| 1049 | } |
| 1050 | } |
| 1051 | |
| 1052 | /** |
| 1053 | * Recursively applies unicode normalization |
| 1054 | * |
| 1055 | * @param mixed $value |
| 1056 | * |
| 1057 | * @return mixed |
| 1058 | */ |
| 1059 | private function recursiveUtfCleanup( $value ) { |
| 1060 | if ( is_string( $value ) ) { |
| 1061 | return UtfNormalValidator::cleanUp( $value ); |
| 1062 | } elseif ( is_array( $value ) ) { |
| 1063 | foreach ( $value as $k => $v ) { |
| 1064 | $value[ $k ] = $this->recursiveUtfCleanup( $v ); |
| 1065 | // TODO: Warn if normalization was applied |
| 1066 | // TODO: also normalize key |
| 1067 | } |
| 1068 | |
| 1069 | return $value; |
| 1070 | } else { |
| 1071 | return $value; |
| 1072 | } |
| 1073 | } |
| 1074 | |
| 1075 | /** |
| 1076 | * Returns the content types that should be accepted by parseBodyData(). |
| 1077 | * |
| 1078 | * Subclasses that support request types other than application/json |
| 1079 | * should override this method. |
| 1080 | * |
| 1081 | * If "application/x-www-form-urlencoded" or "multipart/form-data" are |
| 1082 | * returned, parseBodyData() will use $request->getPostParams() to determine |
| 1083 | * the body data. |
| 1084 | * |
| 1085 | * @note The return value of this method is ignored for requests |
| 1086 | * using a method listed in Validator::NO_BODY_METHODS, |
| 1087 | * in particular for the GET method. |
| 1088 | * |
| 1089 | * @note for backwards compatibility, the default implementation of this |
| 1090 | * method will examine the parameter definitions returned by getParamSettings() |
| 1091 | * to see if any of the parameters are declared as "post" parameters. If this |
| 1092 | * is the case, support for "application/x-www-form-urlencoded" and |
| 1093 | * "multipart/form-data" is added. This may change in future releases. |
| 1094 | * It is preferred to use "body" parameters and override this method explicitly |
| 1095 | * when support for form data is desired. |
| 1096 | * |
| 1097 | * @stable to override |
| 1098 | * |
| 1099 | * @return string[] A list of content-types |
| 1100 | */ |
| 1101 | public function getSupportedRequestTypes(): array { |
| 1102 | $types = [ |
| 1103 | RequestInterface::JSON_CONTENT_TYPE |
| 1104 | ]; |
| 1105 | |
| 1106 | // TODO: remove this once "post" parameters are no longer supported! T362850 |
| 1107 | foreach ( $this->getParamSettings() as $settings ) { |
| 1108 | if ( ( $settings[self::PARAM_SOURCE] ?? null ) === 'post' ) { |
| 1109 | $types[] = RequestInterface::FORM_URLENCODED_CONTENT_TYPE; |
| 1110 | $types[] = RequestInterface::MULTIPART_FORM_DATA_CONTENT_TYPE; |
| 1111 | break; |
| 1112 | } |
| 1113 | } |
| 1114 | |
| 1115 | return $types; |
| 1116 | } |
| 1117 | |
| 1118 | /** |
| 1119 | * Get a HookContainer, for running extension hooks or for hook metadata. |
| 1120 | * |
| 1121 | * @since 1.35 |
| 1122 | * @return HookContainer |
| 1123 | */ |
| 1124 | protected function getHookContainer() { |
| 1125 | return $this->hookContainer; |
| 1126 | } |
| 1127 | |
| 1128 | /** |
| 1129 | * Get a HookRunner for running core hooks. |
| 1130 | * |
| 1131 | * @internal This is for use by core only. Hook interfaces may be removed |
| 1132 | * without notice. |
| 1133 | * @since 1.35 |
| 1134 | * @return HookRunner |
| 1135 | */ |
| 1136 | protected function getHookRunner() { |
| 1137 | return $this->hookRunner; |
| 1138 | } |
| 1139 | |
| 1140 | /** |
| 1141 | * The subclass should override this to provide the maximum last modified |
| 1142 | * timestamp of the requested resource. This is called before execute() in |
| 1143 | * order to decide whether to send a 304. If the request is going to |
| 1144 | * change the state of the resource, the time returned must represent |
| 1145 | * the last modification date before the change. In other words, it must |
| 1146 | * provide the timestamp of the entity that the change is going to be |
| 1147 | * applied to. |
| 1148 | * |
| 1149 | * For GET and HEAD requests, this value will automatically be included |
| 1150 | * in the response in the Last-Modified header. |
| 1151 | * |
| 1152 | * Handlers that modify the resource and want to return a Last-Modified |
| 1153 | * header representing the new state in the response should set the header |
| 1154 | * in the execute() method. |
| 1155 | * |
| 1156 | * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics. |
| 1157 | * |
| 1158 | * @stable to override |
| 1159 | * |
| 1160 | * @return string|int|float|DateTime|null |
| 1161 | */ |
| 1162 | protected function getLastModified() { |
| 1163 | return null; |
| 1164 | } |
| 1165 | |
| 1166 | /** |
| 1167 | * The subclass should override this to provide an ETag for the current |
| 1168 | * state of the requested resource. This is called before execute() in |
| 1169 | * order to decide whether to send a 304. If the request is going to |
| 1170 | * change the state of the resource, the ETag returned must represent |
| 1171 | * the state before the change. In other words, it must identify |
| 1172 | * the entity that the change is going to be applied to. |
| 1173 | * |
| 1174 | * For GET and HEAD requests, this ETag will also be included in the |
| 1175 | * response. |
| 1176 | * |
| 1177 | * Handlers that modify the resource and want to return an ETag |
| 1178 | * header representing the new state in the response should set the header |
| 1179 | * in the execute() method. However, note that responses to PUT requests |
| 1180 | * must not return an ETag unless the new content of the resource is exactly |
| 1181 | * the data that was sent by the client in the request body. |
| 1182 | * |
| 1183 | * This must be a complete ETag, including double quotes. |
| 1184 | * See RFC 7231 §7.2 and RFC 7232 §2.3 for semantics. |
| 1185 | * |
| 1186 | * This method should return null if the resource doesn't exist. It may also |
| 1187 | * return null if ETag semantics is not supported by the Handler. |
| 1188 | * |
| 1189 | * @stable to override |
| 1190 | * |
| 1191 | * @return string|null |
| 1192 | */ |
| 1193 | protected function getETag() { |
| 1194 | return null; |
| 1195 | } |
| 1196 | |
| 1197 | /** |
| 1198 | * The subclass should override this to indicate whether the resource |
| 1199 | * exists. This is used for wildcard validators, for example "If-Match: *" |
| 1200 | * fails if the resource does not exist. |
| 1201 | * |
| 1202 | * If this method returns null, the value returned by getETag() will be used |
| 1203 | * to determine whether the resource exists. |
| 1204 | * |
| 1205 | * In a state-changing request, the return value of this method should |
| 1206 | * reflect the state before the requested change is applied. |
| 1207 | * |
| 1208 | * @stable to override |
| 1209 | * |
| 1210 | * @return bool|null |
| 1211 | */ |
| 1212 | protected function hasRepresentation() { |
| 1213 | return null; |
| 1214 | } |
| 1215 | |
| 1216 | /** |
| 1217 | * Indicates whether this route requires read rights. |
| 1218 | * |
| 1219 | * The handler should override this if it does not need to read from the |
| 1220 | * wiki. This is uncommon, but may be useful for login and other account |
| 1221 | * management APIs. |
| 1222 | * |
| 1223 | * @stable to override |
| 1224 | * |
| 1225 | * @return bool |
| 1226 | */ |
| 1227 | public function needsReadAccess() { |
| 1228 | return true; |
| 1229 | } |
| 1230 | |
| 1231 | /** |
| 1232 | * Indicates whether this route requires write access to the wiki. |
| 1233 | * |
| 1234 | * Handlers may override this method to return false if and only if the operation they |
| 1235 | * implement is "safe" per RFC 7231 section 4.2.1. A handler's operation is "safe" if |
| 1236 | * it is essentially read-only, i.e. the client does not request nor expect any state |
| 1237 | * change that would be observable in the responses to future requests. |
| 1238 | * |
| 1239 | * Implementations of this method must always return the same value, regardless of the |
| 1240 | * parameters passed to the constructor or system state. |
| 1241 | * |
| 1242 | * Handlers for GET, HEAD, OPTIONS, and TRACE requests should each implement a "safe" |
| 1243 | * operation. Handlers of PUT and DELETE requests should each implement a non-"safe" |
| 1244 | * operation. Note that handlers of POST requests can implement a "safe" operation, |
| 1245 | * particularly in the case where large input parameters are required. |
| 1246 | * |
| 1247 | * The information provided by this method is used to perform basic authorization checks |
| 1248 | * and to determine whether cross-origin requests are safe. |
| 1249 | * |
| 1250 | * @stable to override |
| 1251 | * |
| 1252 | * @return bool |
| 1253 | */ |
| 1254 | public function needsWriteAccess() { |
| 1255 | return true; |
| 1256 | } |
| 1257 | |
| 1258 | /** |
| 1259 | * Indicates whether this route can be accessed only by session providers safe vs csrf |
| 1260 | * |
| 1261 | * The handler should override this if the route must only be accessed by session |
| 1262 | * providers that are safe against csrf. |
| 1263 | * |
| 1264 | * A return value of false does not necessarily mean the route is vulnerable to csrf attacks. |
| 1265 | * It means the route can be accessed by session providers that are not automatically safe |
| 1266 | * against csrf attacks, so the possibility of csrf attacks must be considered. |
| 1267 | * |
| 1268 | * @stable to override |
| 1269 | * |
| 1270 | * @return bool |
| 1271 | */ |
| 1272 | public function requireSafeAgainstCsrf() { |
| 1273 | return false; |
| 1274 | } |
| 1275 | |
| 1276 | /** |
| 1277 | * The handler can override this to do any necessary setup after the init functions |
| 1278 | * are called to inject dependencies. |
| 1279 | * |
| 1280 | * @stable to override |
| 1281 | * @throws HttpException if the handler does not accept the request for |
| 1282 | * some reason. |
| 1283 | */ |
| 1284 | protected function postInitSetup() { |
| 1285 | } |
| 1286 | |
| 1287 | /** |
| 1288 | * The handler can override this to do any necessary setup after validate() |
| 1289 | * has been called. This gives the handler an opportunity to do initialization |
| 1290 | * based on parameters before pre-execution calls like getLastModified() or getETag(). |
| 1291 | * |
| 1292 | * @stable to override |
| 1293 | * @since 1.36 |
| 1294 | */ |
| 1295 | protected function postValidationSetup() { |
| 1296 | } |
| 1297 | |
| 1298 | /** |
| 1299 | * Execute the handler. This is called after parameter validation. The |
| 1300 | * return value can either be a Response or any type accepted by |
| 1301 | * ResponseFactory::createFromReturnValue(). |
| 1302 | * |
| 1303 | * To automatically construct an error response, execute() should throw a |
| 1304 | * \MediaWiki\Rest\HttpException. Such exceptions will not be logged like |
| 1305 | * a normal exception. |
| 1306 | * |
| 1307 | * If execute() throws any other kind of exception, the exception will be |
| 1308 | * logged and a generic 500 error page will be shown. |
| 1309 | * |
| 1310 | * @stable to override |
| 1311 | * |
| 1312 | * @return mixed |
| 1313 | */ |
| 1314 | abstract public function execute(); |
| 1315 | } |