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_HTML = 'text/html; charset=utf-8';
19  private const CT_JSON = 'application/json';
20 
22  private $textFormatters;
23 
25  private $showExceptionDetails = false;
26 
30  public function __construct( $textFormatters ) {
31  $this->textFormatters = $textFormatters;
32  }
33 
41  public function setShowExceptionDetails( bool $showExceptionDetails ): void {
42  $this->showExceptionDetails = $showExceptionDetails;
43  }
44 
52  public function encodeJson( $value ) {
53  $json = json_encode( $value,
54  JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE );
55  if ( $json === false ) {
56  throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
57  }
58  return $json;
59  }
60 
66  public function create() {
67  return new Response();
68  }
69 
77  public function createJson( $value, $contentType = null ) {
78  $contentType ??= self::CT_JSON;
79  $response = new Response( $this->encodeJson( $value ) );
80  $response->setHeader( 'Content-Type', $contentType );
81  return $response;
82  }
83 
94  public function createNoContent() {
95  $response = new Response();
96  $response->setStatus( 204 );
97  return $response;
98  }
99 
109  public function createPermanentRedirect( $target ) {
110  $response = $this->createRedirectBase( $target );
111  $response->setStatus( 301 );
112  return $response;
113  }
114 
125  public function createLegacyTemporaryRedirect( $target ) {
126  $response = $this->createRedirectBase( $target );
127  $response->setStatus( 302 );
128  return $response;
129  }
130 
139  public function createTemporaryRedirect( $target ) {
140  $response = $this->createRedirectBase( $target );
141  $response->setStatus( 307 );
142  return $response;
143  }
144 
153  public function createSeeOther( $target ) {
154  $response = $this->createRedirectBase( $target );
155  $response->setStatus( 303 );
156  return $response;
157  }
158 
169  public function createNotModified() {
170  $response = new Response();
171  $response->setStatus( 304 );
172  return $response;
173  }
174 
182  public function createHttpError( $errorCode, array $bodyData = [] ) {
183  if ( $errorCode < 400 || $errorCode >= 600 ) {
184  throw new InvalidArgumentException( 'error code must be 4xx or 5xx' );
185  }
186  $response = $this->createJson( $bodyData + [
187  'httpCode' => $errorCode,
188  'httpReason' => HttpStatus::getMessage( $errorCode )
189  ] );
190  // TODO add link to error code documentation
191  $response->setStatus( $errorCode );
192  return $response;
193  }
194 
204  public function createLocalizedHttpError(
205  $errorCode,
206  MessageValue $messageValue,
207  array $extraData = []
208  ) {
209  return $this->createHttpError(
210  $errorCode,
211  array_merge( $extraData, $this->formatMessage( $messageValue ) )
212  );
213  }
214 
220  public function createFromException( Throwable $exception ) {
221  if ( $exception instanceof LocalizedHttpException ) {
222  $response = $this->createLocalizedHttpError(
223  $exception->getCode(),
224  $exception->getMessageValue(),
225  $exception->getErrorData() + [
226  'errorKey' => $exception->getErrorKey(),
227  ]
228  );
229  } elseif ( $exception instanceof ResponseException ) {
230  return $exception->getResponse();
231  } elseif ( $exception instanceof RedirectException ) {
232  $response = $this->createRedirectBase( $exception->getTarget() );
233  $response->setStatus( $exception->getCode() );
234  } elseif ( $exception instanceof HttpException ) {
235  if ( in_array( $exception->getCode(), [ 204, 304 ], true ) ) {
236  $response = $this->create();
237  $response->setStatus( $exception->getCode() );
238  } else {
239  $response = $this->createHttpError(
240  $exception->getCode(),
241  array_merge(
242  [ 'message' => $exception->getMessage() ],
243  $exception->getErrorData()
244  )
245  );
246  }
247  } elseif ( $this->showExceptionDetails ) {
248  $response = $this->createHttpError( 500, [
249  'message' => 'Error: exception of type ' . get_class( $exception ) . ': '
250  . $exception->getMessage(),
252  $exception,
253  MWExceptionHandler::CAUGHT_BY_OTHER
254  )
255  ] );
256  // XXX: should we try to do something useful with ILocalizedException?
257  // XXX: should we try to do something useful with common MediaWiki errors like ReadOnlyError?
258  } else {
259  $response = $this->createHttpError( 500, [
260  'message' => 'Error: exception of type ' . get_class( $exception ),
261  ] );
262  }
263  return $response;
264  }
265 
273  public function createFromReturnValue( $value ) {
274  $originalValue = $value;
275  if ( is_scalar( $value ) ) {
276  $data = [ 'value' => $value ];
277  } elseif ( is_array( $value ) || $value instanceof stdClass ) {
278  $data = $value;
279  } else {
280  $type = gettype( $originalValue );
281  if ( $type === 'object' ) {
282  $type = get_class( $originalValue );
283  }
284  throw new InvalidArgumentException( __METHOD__ . ": Invalid return value type $type" );
285  }
286  $response = $this->createJson( $data );
287  return $response;
288  }
289 
295  protected function createRedirectBase( $target ) {
296  $response = new Response( $this->getHyperLink( $target ) );
297  $response->setHeader( 'Content-Type', self::CT_HTML );
298  $response->setHeader( 'Location', $target );
299  return $response;
300  }
301 
308  protected function getHyperLink( $url ) {
309  $url = htmlspecialchars( $url, ENT_COMPAT );
310  return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
311  }
312 
313  public function formatMessage( MessageValue $messageValue ) {
314  if ( !$this->textFormatters ) {
315  // For unit tests
316  return [];
317  }
318  $translations = [];
319  foreach ( $this->textFormatters as $formatter ) {
320  $lang = LanguageCode::bcp47( $formatter->getLangCode() );
321  $messageText = $formatter->format( $messageValue );
322  $translations[$lang] = $messageText;
323  }
324  return [ 'messageTranslations' => $translations ];
325  }
326 
327 }
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 IANA language tag See unit test for examples.
Handler class for MWExceptions.
static getStructuredExceptionData(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
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)
createJson( $value, $contentType=null)
Create a successful JSON response.
setShowExceptionDetails(bool $showExceptionDetails)
Control whether web responses may include a exception messager and backtrace.
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...
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.