Merge "Provide command to adjust phpunit.xml for code coverage"
[lhc/web/wiklou.git] / includes / Rest / ResponseFactory.php
1 <?php
2
3 namespace MediaWiki\Rest;
4
5 use Exception;
6 use HttpStatus;
7 use InvalidArgumentException;
8 use MWExceptionHandler;
9 use stdClass;
10 use Throwable;
11
12 /**
13 * Generates standardized response objects.
14 */
15 class ResponseFactory {
16
17 const CT_PLAIN = 'text/plain; charset=utf-8';
18 const CT_HTML = 'text/html; charset=utf-8';
19 const CT_JSON = 'application/json';
20
21 /**
22 * Encode a stdClass object or array to a JSON string
23 *
24 * @param array|stdClass $value
25 * @return string
26 * @throws JsonEncodingException
27 */
28 public function encodeJson( $value ) {
29 $json = json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
30 if ( $json === false ) {
31 throw new JsonEncodingException( json_last_error_msg(), json_last_error() );
32 }
33 return $json;
34 }
35
36 /**
37 * Create an unspecified response. It is the caller's responsibility to set specifics
38 * like response code, content type etc.
39 * @return Response
40 */
41 public function create() {
42 return new Response();
43 }
44
45 /**
46 * Create a successful JSON response.
47 * @param array|stdClass $value JSON value
48 * @param string|null $contentType HTTP content type (should be 'application/json+...')
49 * or null for plain 'application/json'
50 * @return Response
51 */
52 public function createJson( $value, $contentType = null ) {
53 $contentType = $contentType ?? self::CT_JSON;
54 $response = new Response( $this->encodeJson( $value ) );
55 $response->setHeader( 'Content-Type', $contentType );
56 return $response;
57 }
58
59 /**
60 * Create a 204 (No Content) response, used to indicate that an operation which does
61 * not return anything (e.g. a PUT request) was successful.
62 *
63 * Headers are generally interpreted to refer to the target of the operation. E.g. if
64 * this was a PUT request, the caller of this method might want to add an ETag header
65 * describing the created resource.
66 *
67 * @return Response
68 */
69 public function createNoContent() {
70 $response = new Response();
71 $response->setStatus( 204 );
72 return $response;
73 }
74
75 /**
76 * Creates a permanent (301) redirect.
77 * This indicates that the caller of the API should update their indexes and call
78 * the new URL in the future. 301 redirects tend to get cached and are hard to undo.
79 * Client behavior for methods other than GET/HEAD is not well-defined and this type
80 * of response should be avoided in such cases.
81 * @param string $target Redirect target (an absolute URL)
82 * @return Response
83 */
84 public function createPermanentRedirect( $target ) {
85 $response = $this->createRedirectBase( $target );
86 $response->setStatus( 301 );
87 return $response;
88 }
89
90 /**
91 * Creates a temporary (302) redirect.
92 * HTTP 302 was underspecified and has been superseded by 303 (when the redirected request
93 * should be a GET, regardless of what the current request is) and 307 (when the method should
94 * not be changed), but might still be needed for HTTP 1.0 clients or to match legacy behavior.
95 * @param string $target Redirect target (an absolute URL)
96 * @return Response
97 * @see self::createTemporaryRedirect()
98 * @see self::createSeeOther()
99 */
100 public function createLegacyTemporaryRedirect( $target ) {
101 $response = $this->createRedirectBase( $target );
102 $response->setStatus( 302 );
103 return $response;
104 }
105
106 /**
107 * Creates a temporary (307) redirect.
108 * This indicates that the operation the client was trying to perform can temporarily
109 * be achieved by using a different URL. Clients will preserve the request method when
110 * retrying the request with the new URL.
111 * @param string $target Redirect target (an absolute URL)
112 * @return Response
113 */
114 public function createTemporaryRedirect( $target ) {
115 $response = $this->createRedirectBase( $target );
116 $response->setStatus( 307 );
117 return $response;
118 }
119
120 /**
121 * Creates a See Other (303) redirect.
122 * This indicates that the target resource might be of interest to the client, without
123 * necessarily implying that it is the same resource. The client will always use GET
124 * (or HEAD) when following the redirection. Useful for GET-after-POST.
125 * @param string $target Redirect target (an absolute URL)
126 * @return Response
127 */
128 public function createSeeOther( $target ) {
129 $response = $this->createRedirectBase( $target );
130 $response->setStatus( 303 );
131 return $response;
132 }
133
134 /**
135 * Create a 304 (Not Modified) response, used when the client has an up-to-date cached response.
136 *
137 * Per RFC 7232 the response should contain all Cache-Control, Content-Location, Date,
138 * ETag, Expires, and Vary headers that would have been sent with the 200 OK answer
139 * if the requesting client did not have a valid cached response. This is the responsibility
140 * of the caller of this method.
141 *
142 * @return Response
143 */
144 public function createNotModified() {
145 $response = new Response();
146 $response->setStatus( 304 );
147 return $response;
148 }
149
150 /**
151 * Create a HTTP 4xx or 5xx response.
152 * @param int $errorCode HTTP error code
153 * @param array $bodyData An array of data to be included in the JSON response
154 * @return Response
155 * @throws InvalidArgumentException
156 */
157 public function createHttpError( $errorCode, array $bodyData = [] ) {
158 if ( $errorCode < 400 || $errorCode >= 600 ) {
159 throw new InvalidArgumentException( 'error code must be 4xx or 5xx' );
160 }
161 $response = $this->createJson( $bodyData + [
162 'httpCode' => $errorCode,
163 'httpReason' => HttpStatus::getMessage( $errorCode )
164 ] );
165 // TODO add link to error code documentation
166 $response->setStatus( $errorCode );
167 return $response;
168 }
169
170 /**
171 * Turn an exception into a JSON error response.
172 * @param Exception|Throwable $exception
173 * @return Response
174 */
175 public function createFromException( $exception ) {
176 if ( $exception instanceof HttpException ) {
177 // FIXME can HttpException represent 2xx or 3xx responses?
178 $response = $this->createHttpError(
179 $exception->getCode(),
180 array_merge(
181 [ 'message' => $exception->getMessage() ],
182 (array)$exception->getErrorData()
183 )
184 );
185 } else {
186 $response = $this->createHttpError( 500, [
187 'message' => 'Error: exception of type ' . get_class( $exception ),
188 'exception' => MWExceptionHandler::getStructuredExceptionData( $exception )
189 ] );
190 // FIXME should we try to do something useful with ILocalizedException?
191 // FIXME should we try to do something useful with common MediaWiki errors like ReadOnlyError?
192 }
193 return $response;
194 }
195
196 /**
197 * Create a JSON response from an arbitrary value.
198 * This is a fallback; it's preferable to use createJson() instead.
199 * @param mixed $value A structure containing only scalars, arrays and stdClass objects
200 * @return Response
201 * @throws InvalidArgumentException When $value cannot be reasonably represented as JSON
202 */
203 public function createFromReturnValue( $value ) {
204 $originalValue = $value;
205 if ( is_scalar( $value ) ) {
206 $data = [ 'value' => $value ];
207 } elseif ( is_array( $value ) || $value instanceof stdClass ) {
208 $data = $value;
209 } else {
210 $type = gettype( $originalValue );
211 if ( $type === 'object' ) {
212 $type = get_class( $originalValue );
213 }
214 throw new InvalidArgumentException( __METHOD__ . ": Invalid return value type $type" );
215 }
216 $response = $this->createJson( $data );
217 return $response;
218 }
219
220 /**
221 * Create a redirect response with type / response code unspecified.
222 * @param string $target Redirect target (an absolute URL)
223 * @return Response
224 */
225 protected function createRedirectBase( $target ) {
226 $response = new Response( $this->getHyperLink( $target ) );
227 $response->setHeader( 'Content-Type', self::CT_HTML );
228 $response->setHeader( 'Location', $target );
229 return $response;
230 }
231
232 /**
233 * Returns a minimal HTML document that links to the given URL, as suggested by
234 * RFC 7231 for 3xx responses.
235 * @param string $url An absolute URL
236 * @return string
237 */
238 protected function getHyperLink( $url ) {
239 $url = htmlspecialchars( $url );
240 return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
241 }
242
243 }