friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Http\Message;
4
5use Fig\Http\Message\StatusCodeInterface;
6use Psr\Http\Message\ResponseInterface;
7use Psr\Http\Message\StreamInterface;
8use React\Http\Io\AbstractMessage;
9use React\Http\Io\BufferedBody;
10use React\Http\Io\HttpBodyStream;
11use React\Stream\ReadableStreamInterface;
12
13/**
14 * Represents an outgoing server response message.
15 *
16 * ```php
17 * $response = new React\Http\Message\Response(
18 * React\Http\Message\Response::STATUS_OK,
19 * array(
20 * 'Content-Type' => 'text/html'
21 * ),
22 * "<html>Hello world!</html>\n"
23 * );
24 * ```
25 *
26 * This class implements the
27 * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface)
28 * which in turn extends the
29 * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface).
30 *
31 * On top of this, this class implements the
32 * [PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php)
33 * which means that most common HTTP status codes are available as class
34 * constants with the `STATUS_*` prefix. For instance, the `200 OK` and
35 * `404 Not Found` status codes can used as `Response::STATUS_OK` and
36 * `Response::STATUS_NOT_FOUND` respectively.
37 *
38 * > Internally, this implementation builds on top a base class which is
39 * considered an implementation detail that may change in the future.
40 *
41 * @see \Psr\Http\Message\ResponseInterface
42 */
43final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface
44{
45 /**
46 * Create an HTML response
47 *
48 * ```php
49 * $html = <<<HTML
50 * <!doctype html>
51 * <html>
52 * <body>Hello wörld!</body>
53 * </html>
54 *
55 * HTML;
56 *
57 * $response = React\Http\Message\Response::html($html);
58 * ```
59 *
60 * This is a convenient shortcut method that returns the equivalent of this:
61 *
62 * ```
63 * $response = new React\Http\Message\Response(
64 * React\Http\Message\Response::STATUS_OK,
65 * [
66 * 'Content-Type' => 'text/html; charset=utf-8'
67 * ],
68 * $html
69 * );
70 * ```
71 *
72 * This method always returns a response with a `200 OK` status code and
73 * the appropriate `Content-Type` response header for the given HTTP source
74 * string encoded in UTF-8 (Unicode). It's generally recommended to end the
75 * given plaintext string with a trailing newline.
76 *
77 * If you want to use a different status code or custom HTTP response
78 * headers, you can manipulate the returned response object using the
79 * provided PSR-7 methods or directly instantiate a custom HTTP response
80 * object using the `Response` constructor:
81 *
82 * ```php
83 * $response = React\Http\Message\Response::html(
84 * "<h1>Error</h1>\n<p>Invalid user name given.</p>\n"
85 * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
86 * ```
87 *
88 * @param string $html
89 * @return self
90 */
91 public static function html($html)
92 {
93 return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html);
94 }
95
96 /**
97 * Create a JSON response
98 *
99 * ```php
100 * $response = React\Http\Message\Response::json(['name' => 'Alice']);
101 * ```
102 *
103 * This is a convenient shortcut method that returns the equivalent of this:
104 *
105 * ```
106 * $response = new React\Http\Message\Response(
107 * React\Http\Message\Response::STATUS_OK,
108 * [
109 * 'Content-Type' => 'application/json'
110 * ],
111 * json_encode(
112 * ['name' => 'Alice'],
113 * JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
114 * ) . "\n"
115 * );
116 * ```
117 *
118 * This method always returns a response with a `200 OK` status code and
119 * the appropriate `Content-Type` response header for the given structured
120 * data encoded as a JSON text.
121 *
122 * The given structured data will be encoded as a JSON text. Any `string`
123 * values in the data must be encoded in UTF-8 (Unicode). If the encoding
124 * fails, this method will throw an `InvalidArgumentException`.
125 *
126 * By default, the given structured data will be encoded with the flags as
127 * shown above. This includes pretty printing (PHP 5.4+) and preserving
128 * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is
129 * assumed any additional data overhead is usually compensated by using HTTP
130 * response compression.
131 *
132 * If you want to use a different status code or custom HTTP response
133 * headers, you can manipulate the returned response object using the
134 * provided PSR-7 methods or directly instantiate a custom HTTP response
135 * object using the `Response` constructor:
136 *
137 * ```php
138 * $response = React\Http\Message\Response::json(
139 * ['error' => 'Invalid user name given']
140 * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
141 * ```
142 *
143 * @param mixed $data
144 * @return self
145 * @throws \InvalidArgumentException when encoding fails
146 */
147 public static function json($data)
148 {
149 $json = @\json_encode(
150 $data,
151 (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0)
152 );
153
154 // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5
155 if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) {
156 throw new \InvalidArgumentException(
157 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''),
158 \json_last_error()
159 );
160 }
161
162 return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n");
163 }
164
165 /**
166 * Create a plaintext response
167 *
168 * ```php
169 * $response = React\Http\Message\Response::plaintext("Hello wörld!\n");
170 * ```
171 *
172 * This is a convenient shortcut method that returns the equivalent of this:
173 *
174 * ```
175 * $response = new React\Http\Message\Response(
176 * React\Http\Message\Response::STATUS_OK,
177 * [
178 * 'Content-Type' => 'text/plain; charset=utf-8'
179 * ],
180 * "Hello wörld!\n"
181 * );
182 * ```
183 *
184 * This method always returns a response with a `200 OK` status code and
185 * the appropriate `Content-Type` response header for the given plaintext
186 * string encoded in UTF-8 (Unicode). It's generally recommended to end the
187 * given plaintext string with a trailing newline.
188 *
189 * If you want to use a different status code or custom HTTP response
190 * headers, you can manipulate the returned response object using the
191 * provided PSR-7 methods or directly instantiate a custom HTTP response
192 * object using the `Response` constructor:
193 *
194 * ```php
195 * $response = React\Http\Message\Response::plaintext(
196 * "Error: Invalid user name given.\n"
197 * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
198 * ```
199 *
200 * @param string $text
201 * @return self
202 */
203 public static function plaintext($text)
204 {
205 return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text);
206 }
207
208 /**
209 * Create an XML response
210 *
211 * ```php
212 * $xml = <<<XML
213 * <?xml version="1.0" encoding="utf-8"?>
214 * <body>
215 * <greeting>Hello wörld!</greeting>
216 * </body>
217 *
218 * XML;
219 *
220 * $response = React\Http\Message\Response::xml($xml);
221 * ```
222 *
223 * This is a convenient shortcut method that returns the equivalent of this:
224 *
225 * ```
226 * $response = new React\Http\Message\Response(
227 * React\Http\Message\Response::STATUS_OK,
228 * [
229 * 'Content-Type' => 'application/xml'
230 * ],
231 * $xml
232 * );
233 * ```
234 *
235 * This method always returns a response with a `200 OK` status code and
236 * the appropriate `Content-Type` response header for the given XML source
237 * string. It's generally recommended to use UTF-8 (Unicode) and specify
238 * this as part of the leading XML declaration and to end the given XML
239 * source string with a trailing newline.
240 *
241 * If you want to use a different status code or custom HTTP response
242 * headers, you can manipulate the returned response object using the
243 * provided PSR-7 methods or directly instantiate a custom HTTP response
244 * object using the `Response` constructor:
245 *
246 * ```php
247 * $response = React\Http\Message\Response::xml(
248 * "<error><message>Invalid user name given.</message></error>\n"
249 * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
250 * ```
251 *
252 * @param string $xml
253 * @return self
254 */
255 public static function xml($xml)
256 {
257 return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml);
258 }
259
260 /**
261 * @var bool
262 * @see self::$phrasesMap
263 */
264 private static $phrasesInitialized = false;
265
266 /**
267 * Map of standard HTTP status codes to standard reason phrases.
268 *
269 * This map will be fully populated with all standard reason phrases on
270 * first access. By default, it only contains a subset of HTTP status codes
271 * that have a custom mapping to reason phrases (such as those with dashes
272 * and all caps words). See `self::STATUS_*` for all possible status code
273 * constants.
274 *
275 * @var array<int,string>
276 * @see self::STATUS_*
277 * @see self::getReasonPhraseForStatusCode()
278 */
279 private static $phrasesMap = array(
280 200 => 'OK',
281 203 => 'Non-Authoritative Information',
282 207 => 'Multi-Status',
283 226 => 'IM Used',
284 414 => 'URI Too Large',
285 418 => 'I\'m a teapot',
286 505 => 'HTTP Version Not Supported'
287 );
288
289 /** @var int */
290 private $statusCode;
291
292 /** @var string */
293 private $reasonPhrase;
294
295 /**
296 * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants
297 * @param array<string,string|string[]> $headers additional response headers
298 * @param string|ReadableStreamInterface|StreamInterface $body response body
299 * @param string $version HTTP protocol version (e.g. 1.1/1.0)
300 * @param ?string $reason custom HTTP response phrase
301 * @throws \InvalidArgumentException for an invalid body
302 */
303 public function __construct(
304 $status = self::STATUS_OK,
305 array $headers = array(),
306 $body = '',
307 $version = '1.1',
308 $reason = null
309 ) {
310 if (\is_string($body)) {
311 $body = new BufferedBody($body);
312 } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
313 $body = new HttpBodyStream($body, null);
314 } elseif (!$body instanceof StreamInterface) {
315 throw new \InvalidArgumentException('Invalid response body given');
316 }
317
318 parent::__construct($version, $headers, $body);
319
320 $this->statusCode = (int) $status;
321 $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status);
322 }
323
324 public function getStatusCode()
325 {
326 return $this->statusCode;
327 }
328
329 public function withStatus($code, $reasonPhrase = '')
330 {
331 if ((string) $reasonPhrase === '') {
332 $reasonPhrase = self::getReasonPhraseForStatusCode($code);
333 }
334
335 if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) {
336 return $this;
337 }
338
339 $response = clone $this;
340 $response->statusCode = (int) $code;
341 $response->reasonPhrase = (string) $reasonPhrase;
342
343 return $response;
344 }
345
346 public function getReasonPhrase()
347 {
348 return $this->reasonPhrase;
349 }
350
351 /**
352 * @param int $code
353 * @return string default reason phrase for given status code or empty string if unknown
354 */
355 private static function getReasonPhraseForStatusCode($code)
356 {
357 if (!self::$phrasesInitialized) {
358 self::$phrasesInitialized = true;
359
360 // map all `self::STATUS_` constants from status code to reason phrase
361 // e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found`
362 $ref = new \ReflectionClass(__CLASS__);
363 foreach ($ref->getConstants() as $name => $value) {
364 if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) {
365 self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7))));
366 }
367 }
368 }
369
370 return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
371 }
372
373 /**
374 * [Internal] Parse incoming HTTP protocol message
375 *
376 * @internal
377 * @param string $message
378 * @return self
379 * @throws \InvalidArgumentException if given $message is not a valid HTTP response message
380 */
381 public static function parseMessage($message)
382 {
383 $start = array();
384 if (!\preg_match('#^HTTP/(?<version>\d\.\d) (?<status>\d{3})(?: (?<reason>[^\r\n]*+))?[\r]?+\n#m', $message, $start)) {
385 throw new \InvalidArgumentException('Unable to parse invalid status-line');
386 }
387
388 // only support HTTP/1.1 and HTTP/1.0 requests
389 if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
390 throw new \InvalidArgumentException('Received response with invalid protocol version');
391 }
392
393 // check number of valid header fields matches number of lines + status line
394 $matches = array();
395 $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
396 if (\substr_count($message, "\n") !== $n + 1) {
397 throw new \InvalidArgumentException('Unable to parse invalid response header fields');
398 }
399
400 // format all header fields into associative array
401 $headers = array();
402 foreach ($matches as $match) {
403 $headers[$match[1]][] = $match[2];
404 }
405
406 return new self(
407 (int) $start['status'],
408 $headers,
409 '',
410 $start['version'],
411 isset($start['reason']) ? $start['reason'] : ''
412 );
413 }
414}