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