1<?php 2 3namespace MediaWiki\Rest; 4 5use MediaWiki\HookContainer\HookContainer; 6use MediaWiki\HookContainer\HookRunner; 7use MediaWiki\Permissions\Authority; 8use MediaWiki\Rest\Validator\BodyValidator; 9use MediaWiki\Rest\Validator\NullBodyValidator; 10use MediaWiki\Rest\Validator\Validator; 11 12/** 13 * Base class for REST route handlers. 14 * 15 * @stable to extend. 16 */ 17abstract class Handler { 18 19 /** 20 * (string) ParamValidator constant to specify the source of the parameter. 21 * Value must be 'path', 'query', or 'post'. 22 * 'post' refers to application/x-www-form-urlencoded or multipart/form-data encoded parameters 23 * in the body of a POST request (in other words, parameters in PHP's $_POST). For other kinds 24 * of POST parameters, such as JSON fields, use BodyValidator instead of ParamValidator. 25 */ 26 public const PARAM_SOURCE = 'rest-param-source'; 27 28 /** @var Router */ 29 private $router; 30 31 /** @var RequestInterface */ 32 private $request; 33 34 /** @var Authority */ 35 private $authority; 36 37 /** @var array */ 38 private $config; 39 40 /** @var ResponseFactory */ 41 private $responseFactory; 42 43 /** @var array|null */ 44 private $validatedParams; 45 46 /** @var mixed */ 47 private $validatedBody; 48 49 /** @var ConditionalHeaderUtil */ 50 private $conditionalHeaderUtil; 51 52 /** @var HookContainer */ 53 private $hookContainer; 54 55 /** @var HookRunner */ 56 private $hookRunner; 57 58 /** 59 * Initialise with dependencies from the Router. This is called after construction. 60 * @param Router $router 61 * @param RequestInterface $request 62 * @param array $config 63 * @param Authority $authority 64 * @param ResponseFactory $responseFactory 65 * @param HookContainer $hookContainer 66 * @internal 67 */ 68 final public function init( Router $router, RequestInterface $request, array $config, 69 Authority $authority, ResponseFactory $responseFactory, HookContainer $hookContainer 70 ) { 71 $this->router = $router; 72 $this->request = $request; 73 $this->authority = $authority; 74 $this->config = $config; 75 $this->responseFactory = $responseFactory; 76 $this->hookContainer = $hookContainer; 77 $this->hookRunner = new HookRunner( $hookContainer ); 78 $this->postInitSetup(); 79 } 80 81 /** 82 * Get the Router. The return type declaration causes it to raise 83 * a fatal error if init() has not yet been called. 84 * @return Router 85 */ 86 protected function getRouter(): Router { 87 return $this->router; 88 } 89 90 /** 91 * Get the URL of this handler's endpoint. 92 * Supports the substitution of path parameters, and additions of query parameters. 93 * 94 * @see Router::getRouteUrl() 95 * 96 * @param string[] $pathParams Path parameters to be injected into the path 97 * @param string[] $queryParams Query parameters to be attached to the URL 98 * 99 * @return string 100 */ 101 protected function getRouteUrl( $pathParams = [], $queryParams = [] ): string { 102 $path = $this->getConfig()['path']; 103 return $this->router->getRouteUrl( $path, $pathParams, $queryParams ); 104 } 105 106 /** 107 * URL-encode titles in a "pretty" way. 108 * 109 * Keeps intact ;@$!*(),~: (urlencode does not, but wfUrlencode does). 110 * Encodes spaces as underscores (wfUrlencode does not). 111 * Encodes slashes (wfUrlencode does not, but keeping them messes with REST pathes). 112 * Encodes pluses (this is not necessary, and may change). 113 * 114 * @see wfUrlencode 115 * 116 * @param string $title 117 * 118 * @return string 119 */ 120 protected function urlEncodeTitle( $title ) { 121 $title = str_replace( ' ', '_', $title ); 122 $title = urlencode( $title ); 123 124 // %3B_a_%40_b_%24_c_%21_d_%2A_e_%28_f_%29_g_%2C_h_~_i_%3A 125 $replace = [ '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%7E', '%3A' ]; 126 $with = [ ';', '@', '$', '!', '*', '(', ')', ',', '~', ':' ]; 127 128 return str_replace( $replace, $with, $title ); 129 } 130 131 /** 132 * Get the current request. The return type declaration causes it to raise 133 * a fatal error if init() has not yet been called. 134 * 135 * @return RequestInterface 136 */ 137 public function getRequest(): RequestInterface { 138 return $this->request; 139 } 140 141 /** 142 * Get the current acting authority. The return type declaration causes it to raise 143 * a fatal error if init() has not yet been called. 144 * 145 * @since 1.36 146 * @return Authority 147 */ 148 public function getAuthority(): Authority { 149 return $this->authority; 150 } 151 152 /** 153 * Get the configuration array for the current route. The return type 154 * declaration causes it to raise a fatal error if init() has not 155 * been called. 156 * 157 * @return array 158 */ 159 public function getConfig(): array { 160 return $this->config; 161 } 162 163 /** 164 * Get the ResponseFactory which can be used to generate Response objects. 165 * This will raise a fatal error if init() has not been 166 * called. 167 * 168 * @return ResponseFactory 169 */ 170 public function getResponseFactory(): ResponseFactory { 171 return $this->responseFactory; 172 } 173 174 /** 175 * Validate the request parameters/attributes and body. If there is a validation 176 * failure, a response with an error message should be returned or an 177 * HttpException should be thrown. 178 * 179 * @stable to override 180 * @param Validator $restValidator 181 * @throws HttpException On validation failure. 182 */ 183 public function validate( Validator $restValidator ) { 184 $validatedParams = $restValidator->validateParams( $this->getParamSettings() ); 185 $validatedBody = $restValidator->validateBody( $this->request, $this ); 186 $this->validatedParams = $validatedParams; 187 $this->validatedBody = $validatedBody; 188 $this->postValidationSetup(); 189 } 190 191 /** 192 * Get a ConditionalHeaderUtil object. 193 * 194 * On the first call to this method, the object will be initialized with 195 * validator values by calling getETag(), getLastModified() and 196 * hasRepresentation(). 197 * 198 * @return ConditionalHeaderUtil 199 */ 200 protected function getConditionalHeaderUtil() { 201 if ( $this->conditionalHeaderUtil === null ) { 202 $this->conditionalHeaderUtil = new ConditionalHeaderUtil; 203 $this->conditionalHeaderUtil->setValidators( 204 $this->getETag(), 205 $this->getLastModified(), 206 $this->hasRepresentation() 207 ); 208 } 209 return $this->conditionalHeaderUtil; 210 } 211 212 /** 213 * Check the conditional request headers and generate a response if appropriate. 214 * This is called by the Router before execute() and may be overridden. 215 * 216 * @stable to override 217 * 218 * @return ResponseInterface|null 219 */ 220 public function checkPreconditions() { 221 $status = $this->getConditionalHeaderUtil()->checkPreconditions( $this->getRequest() ); 222 if ( $status ) { 223 $response = $this->getResponseFactory()->create(); 224 $response->setStatus( $status ); 225 return $response; 226 } 227 228 return null; 229 } 230 231 /** 232 * Modify the response, adding Last-Modified and ETag headers as indicated 233 * the values previously returned by ETag and getLastModified(). This is 234 * called after execute() returns, and may be overridden. 235 * 236 * @stable to override 237 * 238 * @param ResponseInterface $response 239 */ 240 public function applyConditionalResponseHeaders( ResponseInterface $response ) { 241 $this->getConditionalHeaderUtil()->applyResponseHeaders( $response ); 242 } 243 244 /** 245 * Fetch ParamValidator settings for parameters 246 * 247 * Every setting must include self::PARAM_SOURCE to specify which part of 248 * the request is to contain the parameter. 249 * 250 * Can be used for validating parameters inside an application/x-www-form-urlencoded or 251 * multipart/form-data POST body (i.e. parameters which would be present in PHP's $_POST 252 * array). For validating other kinds of request bodies, override getBodyValidator(). 253 * 254 * @stable to override 255 * 256 * @return array[] Associative array mapping parameter names to 257 * ParamValidator settings arrays 258 */ 259 public function getParamSettings() { 260 return []; 261 } 262 263 /** 264 * Fetch the BodyValidator 265 * 266 * @stable to override 267 * 268 * @param string $contentType Content type of the request. 269 * @return BodyValidator 270 */ 271 public function getBodyValidator( $contentType ) { 272 return new NullBodyValidator(); 273 } 274 275 /** 276 * Fetch the validated parameters. This must be called after validate() is 277 * called. During execute() is fine. 278 * 279 * @return array Array mapping parameter names to validated values 280 * @throws \RuntimeException If validate() has not been called 281 */ 282 public function getValidatedParams() { 283 if ( $this->validatedParams === null ) { 284 throw new \RuntimeException( 'getValidatedParams() called before validate()' ); 285 } 286 return $this->validatedParams; 287 } 288 289 /** 290 * Fetch the validated body 291 * @return mixed Value returned by the body validator, or null if validate() was 292 * not called yet, validation failed, there was no body, or the body was form data. 293 */ 294 public function getValidatedBody() { 295 return $this->validatedBody; 296 } 297 298 /** 299 * Get a HookContainer, for running extension hooks or for hook metadata. 300 * 301 * @since 1.35 302 * @return HookContainer 303 */ 304 protected function getHookContainer() { 305 return $this->hookContainer; 306 } 307 308 /** 309 * Get a HookRunner for running core hooks. 310 * 311 * @internal This is for use by core only. Hook interfaces may be removed 312 * without notice. 313 * @since 1.35 314 * @return HookRunner 315 */ 316 protected function getHookRunner() { 317 return $this->hookRunner; 318 } 319 320 /** 321 * The subclass should override this to provide the maximum last modified 322 * timestamp for the current request. This is called before execute() in 323 * order to decide whether to send a 304. 324 * 325 * The timestamp can be in any format accepted by ConvertibleTimestamp, or 326 * null to indicate that the timestamp is unknown. 327 * 328 * @stable to override 329 * 330 * @return bool|string|int|float|\DateTime|null 331 */ 332 protected function getLastModified() { 333 return null; 334 } 335 336 /** 337 * The subclass should override this to provide an ETag for the current 338 * request. This is called before execute() in order to decide whether to 339 * send a 304. 340 * 341 * This must be a complete ETag, including double quotes. 342 * 343 * See RFC 7232 § 2.3 for semantics. 344 * 345 * @stable to override 346 * 347 * @return string|null 348 */ 349 protected function getETag() { 350 return null; 351 } 352 353 /** 354 * The subclass should override this to indicate whether the resource 355 * exists. This is used for wildcard validators, for example "If-Match: *" 356 * fails if the resource does not exist. 357 * 358 * @stable to override 359 * 360 * @return bool|null 361 */ 362 protected function hasRepresentation() { 363 return null; 364 } 365 366 /** 367 * Indicates whether this route requires read rights. 368 * 369 * The handler should override this if it does not need to read from the 370 * wiki. This is uncommon, but may be useful for login and other account 371 * management APIs. 372 * 373 * @stable to override 374 * 375 * @return bool 376 */ 377 public function needsReadAccess() { 378 return true; 379 } 380 381 /** 382 * Indicates whether this route requires write access. 383 * 384 * The handler should override this if the route does not need to write to 385 * the database. 386 * 387 * This should return true for routes that may require synchronous database writes. 388 * Modules that do not need such writes should also not rely on master database access, 389 * since only read queries are needed and each master DB is a single point of failure. 390 * 391 * @stable to override 392 * 393 * @return bool 394 */ 395 public function needsWriteAccess() { 396 return true; 397 } 398 399 /** 400 * The handler can override this to do any necessary setup after init() 401 * is called to inject the dependencies. 402 * 403 * @stable to override 404 */ 405 protected function postInitSetup() { 406 } 407 408 /** 409 * The handler can override this to do any necessary setup after validate() 410 * has been called. This gives the handler an opportunity to do initialization 411 * based on parameters before pre-execution calls like getLastModified() or getETag(). 412 * 413 * @stable to override 414 * @since 1.36 415 */ 416 protected function postValidationSetup() { 417 } 418 419 /** 420 * Execute the handler. This is called after parameter validation. The 421 * return value can either be a Response or any type accepted by 422 * ResponseFactory::createFromReturnValue(). 423 * 424 * To automatically construct an error response, execute() should throw a 425 * \MediaWiki\Rest\HttpException. Such exceptions will not be logged like 426 * a normal exception. 427 * 428 * If execute() throws any other kind of exception, the exception will be 429 * logged and a generic 500 error page will be shown. 430 * 431 * @stable to override 432 * 433 * @return mixed 434 */ 435 abstract public function execute(); 436} 437