MediaWiki  master
ResponseFactory.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest;
4 
5 use HttpStatus;
6 use InvalidArgumentException;
7 use LanguageCode;
9 use stdClass;
10 use Throwable;
13 
18  private const CT_PLAIN = 'text/plain; charset=utf-8';
19  private const CT_HTML = 'text/html; charset=utf-8';
20  private const CT_JSON = 'application/json';
21 
23  private $textFormatters;
24 
26  private $sendExceptionBacktrace = false;
27 
31  public function __construct( $textFormatters ) {
32  $this->textFormatters = $textFormatters;
33  }
34 
40  public function setSendExceptionBacktrace( bool $sendExceptionBacktrace ): void {
41  $this->sendExceptionBacktrace = $sendExceptionBacktrace;
42  }
43 
51  public function encodeJson( $value ) {
52  $json = json_encode( $value,
53  JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE );
54  if ( $json === false ) {
55  throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
56  }
57  return $json;
58  }
59 
65  public function create() {
66  return new Response();
67  }
68 
76  public function createJson( $value, $contentType = null ) {
77  $contentType = $contentType ?? self::CT_JSON;
78  $response = new Response( $this->encodeJson( $value ) );
79  $response->setHeader( 'Content-Type', $contentType );
80  return $response;
81  }
82 
93  public function createNoContent() {
94  $response = new Response();
95  $response->setStatus( 204 );
96  return $response;
97  }
98 
108  public function createPermanentRedirect( $target ) {
109  $response = $this->createRedirectBase( $target );
110  $response->setStatus( 301 );
111  return $response;
112  }
113 
124  public function createLegacyTemporaryRedirect( $target ) {
125  $response = $this->createRedirectBase( $target );
126  $response->setStatus( 302 );
127  return $response;
128  }
129 
138  public function createTemporaryRedirect( $target ) {
139  $response = $this->createRedirectBase( $target );
140  $response->setStatus( 307 );
141  return $response;
142  }
143 
152  public function createSeeOther( $target ) {
153  $response = $this->createRedirectBase( $target );
154  $response->setStatus( 303 );
155  return $response;
156  }
157 
168  public function createNotModified() {
169  $response = new Response();
170  $response->setStatus( 304 );
171  return $response;
172  }
173 
181  public function createHttpError( $errorCode, array $bodyData = [] ) {
182  if ( $errorCode < 400 || $errorCode >= 600 ) {
183  throw new InvalidArgumentException( 'error code must be 4xx or 5xx' );
184  }
185  $response = $this->createJson( $bodyData + [
186  'httpCode' => $errorCode,
187  'httpReason' => HttpStatus::getMessage( $errorCode )
188  ] );
189  // TODO add link to error code documentation
190  $response->setStatus( $errorCode );
191  return $response;
192  }
193 
203  public function createLocalizedHttpError(
204  $errorCode,
205  MessageValue $messageValue,
206  array $extraData = []
207  ) {
208  return $this->createHttpError(
209  $errorCode,
210  array_merge( $extraData, $this->formatMessage( $messageValue ) )
211  );
212  }
213 
219  public function createFromException( Throwable $exception ) {
220  if ( $exception instanceof LocalizedHttpException ) {
221  $response = $this->createLocalizedHttpError(
222  $exception->getCode(),
223  $exception->getMessageValue(),
224  (array)$exception->getErrorData()
225  );
226  } elseif ( $exception instanceof ResponseException ) {
227  return $exception->getResponse();
228  } elseif ( $exception instanceof RedirectException ) {
229  $response = $this->createRedirectBase( $exception->getTarget() );
230  $response->setStatus( $exception->getCode() );
231  } elseif ( $exception instanceof HttpException ) {
232  if ( in_array( $exception->getCode(), [ 204, 304 ], true ) ) {
233  $response = $this->create();
234  $response->setStatus( $exception->getCode() );
235  } else {
236  $response = $this->createHttpError(
237  $exception->getCode(),
238  array_merge(
239  [ 'message' => $exception->getMessage() ],
240  (array)$exception->getErrorData()
241  )
242  );
243  }
244  } else {
245  $response = $this->createHttpError( 500, [
246  'message' => 'Error: exception of type ' . get_class( $exception ) . ': '
247  . $exception->getMessage(),
249  $exception,
250  MWExceptionHandler::CAUGHT_BY_OTHER,
251  $this->sendExceptionBacktrace
252  )
253  ] );
254  // XXX: should we try to do something useful with ILocalizedException?
255  // XXX: should we try to do something useful with common MediaWiki errors like ReadOnlyError?
256  }
257  return $response;
258  }
259 
267  public function createFromReturnValue( $value ) {
268  $originalValue = $value;
269  if ( is_scalar( $value ) ) {
270  $data = [ 'value' => $value ];
271  } elseif ( is_array( $value ) || $value instanceof stdClass ) {
272  $data = $value;
273  } else {
274  $type = gettype( $originalValue );
275  if ( $type === 'object' ) {
276  $type = get_class( $originalValue );
277  }
278  throw new InvalidArgumentException( __METHOD__ . ": Invalid return value type $type" );
279  }
280  $response = $this->createJson( $data );
281  return $response;
282  }
283 
289  protected function createRedirectBase( $target ) {
290  $response = new Response( $this->getHyperLink( $target ) );
291  $response->setHeader( 'Content-Type', self::CT_HTML );
292  $response->setHeader( 'Location', $target );
293  return $response;
294  }
295 
302  protected function getHyperLink( $url ) {
303  $url = htmlspecialchars( $url, ENT_COMPAT );
304  return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
305  }
306 
307  public function formatMessage( MessageValue $messageValue ) {
308  if ( !$this->textFormatters ) {
309  // For unit tests
310  return [];
311  }
312  $translations = [];
313  foreach ( $this->textFormatters as $formatter ) {
314  $lang = LanguageCode::bcp47( $formatter->getLangCode() );
315  $messageText = $formatter->format( $messageValue );
316  $translations[$lang] = $messageText;
317  }
318  return [ 'messageTranslations' => $translations ];
319  }
320 
321 }
static getMessage( $code)
Get the message associated with an HTTP response status code.
Definition: HttpStatus.php:34
Methods for dealing with language codes.
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Handler class for MWExceptions.
static getStructuredExceptionData(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, ?bool $includeBacktrace=null)
Get a structured representation of a Throwable.
This is the base exception class for non-fatal exceptions thrown from REST handlers.
This is an exception class that extends HttpException and will generate a redirect when handled.
This is an exception class that wraps a Response and extends HttpException.
Generates standardized response objects.
encodeJson( $value)
Encode a stdClass object or array to a JSON string.
create()
Create an unspecified response.
createFromException(Throwable $exception)
Turn a throwable into a JSON error response.
formatMessage(MessageValue $messageValue)
bool $sendExceptionBacktrace
Whether to send exception backtraces to the client.
createJson( $value, $contentType=null)
Create a successful JSON response.
createTemporaryRedirect( $target)
Creates a temporary (307) redirect.
createHttpError( $errorCode, array $bodyData=[])
Create a HTTP 4xx or 5xx response.
createPermanentRedirect( $target)
Creates a permanent (301) redirect.
createLegacyTemporaryRedirect( $target)
Creates a temporary (302) redirect.
createRedirectBase( $target)
Create a redirect response with type / response code unspecified.
createLocalizedHttpError( $errorCode, MessageValue $messageValue, array $extraData=[])
Create an HTTP 4xx or 5xx response with error message localisation.
createNoContent()
Create a 204 (No Content) response, used to indicate that an operation which does not return anything...
setSendExceptionBacktrace(bool $sendExceptionBacktrace)
Controls whether error responses should include a backtrace.
createFromReturnValue( $value)
Create a JSON response from an arbitrary value.
getHyperLink( $url)
Returns a minimal HTML document that links to the given URL, as suggested by RFC 7231 for 3xx respons...
createSeeOther( $target)
Creates a See Other (303) redirect.
createNotModified()
Create a 304 (Not Modified) response, used when the client has an up-to-date cached response.
Value object representing a message for i18n.
if(!isset( $args[0])) $lang