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