3 namespace MediaWiki\Rest
;
6 use InvalidArgumentException
;
7 use MediaWiki\Language\LanguageCode
;
8 use MWExceptionHandler
;
11 use Wikimedia\Message\ITextFormatter
;
12 use Wikimedia\Message\MessageValue
;
15 * Generates standardized response objects.
17 class ResponseFactory
{
18 private const CT_HTML
= 'text/html; charset=utf-8';
19 private const CT_JSON
= 'application/json';
21 /** @var ITextFormatter[] */
22 private $textFormatters;
24 /** @var bool Whether to send exception backtraces to the client */
25 private $showExceptionDetails = false;
28 * @param ITextFormatter[] $textFormatters
30 * If there is a relative preference among the input text formatters, the formatters should
31 * be ordered from most to least preferred.
33 public function __construct( $textFormatters ) {
34 $this->textFormatters
= $textFormatters;
38 * Control whether web responses may include a exception messager and backtrace
40 * @see $wgShowExceptionDetails
42 * @param bool $showExceptionDetails
44 public function setShowExceptionDetails( bool $showExceptionDetails ): void
{
45 $this->showExceptionDetails
= $showExceptionDetails;
49 * Encode a stdClass object or array to a JSON string
51 * @param array|stdClass|\JsonSerializable $value
53 * @throws JsonEncodingException
55 public function encodeJson( $value ) {
56 $json = json_encode( $value,
57 JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE
);
58 if ( $json === false ) {
59 throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
65 * Create an unspecified response. It is the caller's responsibility to set specifics
66 * like response code, content type etc.
69 public function create() {
70 return new Response();
74 * Create a successful JSON response.
75 * @param array|stdClass|\JsonSerializable $value JSON value
76 * @param string|null $contentType HTTP content type (should be 'application/json+...')
77 * or null for plain 'application/json'
80 public function createJson( $value, $contentType = null ) {
81 $contentType ??
= self
::CT_JSON
;
82 $response = new Response( $this->encodeJson( $value ) );
83 $response->setHeader( 'Content-Type', $contentType );
88 * Create a 204 (No Content) response, used to indicate that an operation which does
89 * not return anything (e.g. a PUT request) was successful.
91 * Headers are generally interpreted to refer to the target of the operation. E.g. if
92 * this was a PUT request, the caller of this method might want to add an ETag header
93 * describing the created resource.
97 public function createNoContent() {
98 $response = new Response();
99 $response->setStatus( 204 );
104 * Creates a permanent (301) redirect.
105 * This indicates that the caller of the API should update their indexes and call
106 * the new URL in the future. 301 redirects tend to get cached and are hard to undo.
107 * Client behavior for methods other than GET/HEAD is not well-defined and this type
108 * of response should be avoided in such cases.
109 * @param string $target Redirect target (an absolute URL)
112 public function createPermanentRedirect( $target ) {
113 $response = $this->createRedirect( $target, 301 );
118 * Creates a temporary (302) redirect.
119 * HTTP 302 was underspecified and has been superseded by 303 (when the redirected request
120 * should be a GET, regardless of what the current request is) and 307 (when the method should
121 * not be changed), but might still be needed for HTTP 1.0 clients or to match legacy behavior.
122 * @param string $target Redirect target (an absolute URL)
124 * @see self::createTemporaryRedirect()
125 * @see self::createSeeOther()
127 public function createLegacyTemporaryRedirect( $target ) {
128 $response = $this->createRedirect( $target, 302 );
133 * Creates a redirect specifying the code.
134 * This indicates that the operation the client was trying to perform can temporarily
135 * be achieved by using a different URL. Clients will preserve the request method when
136 * retrying the request with the new URL.
137 * @param string $target Redirect target
138 * @param int $code Status code
141 public function createRedirect( $target, $code ) {
142 $response = $this->createRedirectBase( $target );
143 $response->setStatus( $code );
148 * Creates a temporary (307) redirect.
149 * This indicates that the operation the client was trying to perform can temporarily
150 * be achieved by using a different URL. Clients will preserve the request method when
151 * retrying the request with the new URL.
152 * @param string $target Redirect target (an absolute URL)
155 public function createTemporaryRedirect( $target ) {
156 $response = $this->createRedirect( $target, 307 );
161 * Creates a See Other (303) redirect.
162 * This indicates that the target resource might be of interest to the client, without
163 * necessarily implying that it is the same resource. The client will always use GET
164 * (or HEAD) when following the redirection. Useful for GET-after-POST.
165 * @param string $target Redirect target (an absolute URL)
168 public function createSeeOther( $target ) {
169 $response = $this->createRedirect( $target, 303 );
174 * Create a 304 (Not Modified) response, used when the client has an up-to-date cached response.
176 * Per RFC 7232 the response should contain all Cache-Control, Content-Location, Date,
177 * ETag, Expires, and Vary headers that would have been sent with the 200 OK answer
178 * if the requesting client did not have a valid cached response. This is the responsibility
179 * of the caller of this method.
183 public function createNotModified() {
184 $response = new Response();
185 $response->setStatus( 304 );
190 * Create a HTTP 4xx or 5xx response.
191 * @param int $errorCode HTTP error code
192 * @param array $bodyData An array of data to be included in the JSON response
195 public function createHttpError( $errorCode, array $bodyData = [] ) {
196 if ( $errorCode < 400 ||
$errorCode >= 600 ) {
197 throw new InvalidArgumentException( 'error code must be 4xx or 5xx' );
199 $response = $this->createJson( $bodyData +
[
200 'httpCode' => $errorCode,
201 'httpReason' => HttpStatus
::getMessage( $errorCode )
203 // TODO add link to error code documentation
204 $response->setStatus( $errorCode );
209 * Create an HTTP 4xx or 5xx response with error message localisation
211 * @param int $errorCode
212 * @param MessageValue $messageValue
213 * @param array $extraData An array of additional data to be included in the JSON response
217 public function createLocalizedHttpError(
219 MessageValue
$messageValue,
220 array $extraData = []
222 return $this->createHttpError(
224 array_merge( $extraData, $this->formatMessage( $messageValue ) )
229 * Turn a throwable into a JSON error response.
231 * @param Throwable $exception
232 * @param array $extraData if present, used to generate a RESTbase-style response
235 public function createFromException( Throwable
$exception, array $extraData = [] ) {
236 if ( $exception instanceof LocalizedHttpException
) {
237 $response = $this->createLocalizedHttpError(
238 $exception->getCode(),
239 $exception->getMessageValue(),
240 $exception->getErrorData() +
$extraData +
[
241 'errorKey' => $exception->getErrorKey(),
244 } elseif ( $exception instanceof ResponseException
) {
245 return $exception->getResponse();
246 } elseif ( $exception instanceof RedirectException
) {
247 $response = $this->createRedirect( $exception->getTarget(), $exception->getCode() );
248 } elseif ( $exception instanceof HttpException
) {
249 if ( in_array( $exception->getCode(), [ 204, 304 ], true ) ) {
250 $response = $this->create();
251 $response->setStatus( $exception->getCode() );
253 $response = $this->createHttpError(
254 $exception->getCode(),
256 [ 'message' => $exception->getMessage() ],
257 $exception->getErrorData()
261 } elseif ( $this->showExceptionDetails
) {
262 $response = $this->createHttpError( 500, [
263 'message' => 'Error: exception of type ' . get_class( $exception ) . ': '
264 . $exception->getMessage(),
265 'exception' => MWExceptionHandler
::getStructuredExceptionData(
267 MWExceptionHandler
::CAUGHT_BY_OTHER
270 // XXX: should we try to do something useful with ILocalizedException?
271 // XXX: should we try to do something useful with common MediaWiki errors like ReadOnlyError?
273 $response = $this->createHttpError( 500, [
274 'message' => 'Error: exception of type ' . get_class( $exception ),
281 * Create a JSON response from an arbitrary value.
282 * This is a fallback; it's preferable to use createJson() instead.
283 * @param mixed $value A structure containing only scalars, arrays and stdClass objects
285 * @throws InvalidArgumentException When $value cannot be reasonably represented as JSON
287 public function createFromReturnValue( $value ) {
288 $originalValue = $value;
289 if ( is_scalar( $value ) ) {
290 $data = [ 'value' => $value ];
291 } elseif ( is_array( $value ) ||
$value instanceof stdClass
) {
294 $type = get_debug_type( $originalValue );
295 throw new InvalidArgumentException( __METHOD__
. ": Invalid return value type $type" );
297 $response = $this->createJson( $data );
302 * Create a redirect response with type / response code unspecified.
303 * @param string $target Redirect target (an absolute URL)
306 protected function createRedirectBase( $target ) {
307 $response = new Response( $this->getHyperLink( $target ) );
308 $response->setHeader( 'Content-Type', self
::CT_HTML
);
309 $response->setHeader( 'Location', $target );
314 * Returns a minimal HTML document that links to the given URL, as suggested by
315 * RFC 7231 for 3xx responses.
316 * @param string $url An absolute URL
319 protected function getHyperLink( $url ) {
320 $url = htmlspecialchars( $url, ENT_COMPAT
);
321 return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
325 * Tries to return the formatted string(s) for a message value object using the
326 * response factory's text formatters. The returned array will either be empty (if there are
327 * no text formatters), or have exactly one key, "messageTranslations", whose value
328 * is an array of formatted strings, keyed by the associated language code.
330 * @param MessageValue $messageValue the message value object to format
334 public function formatMessage( MessageValue
$messageValue ): array {
335 if ( !$this->textFormatters
) {
340 foreach ( $this->textFormatters
as $formatter ) {
341 $lang = LanguageCode
::bcp47( $formatter->getLangCode() );
342 $messageText = $formatter->format( $messageValue );
343 $translations[$lang] = $messageText;
345 return [ 'messageTranslations' => $translations ];
349 * Tries to return one formatted string for a message value object. Return value will be:
350 * 1) the formatted string for $preferredLang, if $preferredLang is supplied and the
351 * formatted string for that language is available.
352 * 2) the first available formatted string, if any are available.
353 * 3) the message key string, if no formatted strings are available.
354 * Callers who need more specific control should call formatMessage() instead.
356 * @param MessageValue $messageValue the message value object to format
357 * @param string $preferredlang preferred language for the formatted string, if available
361 public function getFormattedMessage(
362 MessageValue
$messageValue, string $preferredlang = ''
364 $strings = $this->formatMessage( $messageValue );
366 return $messageValue->getKey();
369 $strings = $strings['messageTranslations'];
370 if ( $preferredlang && array_key_exists( $preferredlang, $strings ) ) {
371 return $strings[ $preferredlang ];
373 return reset( $strings );
378 * Returns OpenAPI schema response components object,
379 * providing information about the structure of some standard responses,
380 * for use in path specs.
382 * @see https://swagger.io/specification/#components-object
383 * @see https://swagger.io/specification/#response-object
387 public static function getResponseComponents(): array {
390 'GenericErrorResponse' => [
391 'description' => 'Generic error response',
393 'application/json' => [
395 '$ref' => '#/components/schemas/GenericErrorResponseModel'
402 'GenericErrorResponseModel' => [
403 'description' => 'Generic error response body',
404 'required' => [ 'httpCode' ],
415 'messageTranslations' => [
417 'additionalProperties' => [