1<?php
2namespace GuzzleHttp\Message;
3
4use GuzzleHttp\Exception\ParseException;
5use GuzzleHttp\Exception\XmlParseException;
6use GuzzleHttp\Stream\StreamInterface;
7use GuzzleHttp\Utils;
8
9/**
10 * Guzzle HTTP response object
11 */
12class Response extends AbstractMessage implements ResponseInterface
13{
14    /** @var array Mapping of status codes to reason phrases */
15    private static $statusTexts = [
16        100 => 'Continue',
17        101 => 'Switching Protocols',
18        102 => 'Processing',
19        200 => 'OK',
20        201 => 'Created',
21        202 => 'Accepted',
22        203 => 'Non-Authoritative Information',
23        204 => 'No Content',
24        205 => 'Reset Content',
25        206 => 'Partial Content',
26        207 => 'Multi-Status',
27        208 => 'Already Reported',
28        226 => 'IM Used',
29        300 => 'Multiple Choices',
30        301 => 'Moved Permanently',
31        302 => 'Found',
32        303 => 'See Other',
33        304 => 'Not Modified',
34        305 => 'Use Proxy',
35        307 => 'Temporary Redirect',
36        308 => 'Permanent Redirect',
37        400 => 'Bad Request',
38        401 => 'Unauthorized',
39        402 => 'Payment Required',
40        403 => 'Forbidden',
41        404 => 'Not Found',
42        405 => 'Method Not Allowed',
43        406 => 'Not Acceptable',
44        407 => 'Proxy Authentication Required',
45        408 => 'Request Timeout',
46        409 => 'Conflict',
47        410 => 'Gone',
48        411 => 'Length Required',
49        412 => 'Precondition Failed',
50        413 => 'Request Entity Too Large',
51        414 => 'Request-URI Too Long',
52        415 => 'Unsupported Media Type',
53        416 => 'Requested Range Not Satisfiable',
54        417 => 'Expectation Failed',
55        422 => 'Unprocessable Entity',
56        423 => 'Locked',
57        424 => 'Failed Dependency',
58        425 => 'Reserved for WebDAV advanced collections expired proposal',
59        426 => 'Upgrade required',
60        428 => 'Precondition Required',
61        429 => 'Too Many Requests',
62        431 => 'Request Header Fields Too Large',
63        500 => 'Internal Server Error',
64        501 => 'Not Implemented',
65        502 => 'Bad Gateway',
66        503 => 'Service Unavailable',
67        504 => 'Gateway Timeout',
68        505 => 'HTTP Version Not Supported',
69        506 => 'Variant Also Negotiates (Experimental)',
70        507 => 'Insufficient Storage',
71        508 => 'Loop Detected',
72        510 => 'Not Extended',
73        511 => 'Network Authentication Required',
74    ];
75
76    /** @var string The reason phrase of the response (human readable code) */
77    private $reasonPhrase;
78
79    /** @var string The status code of the response */
80    private $statusCode;
81
82    /** @var string The effective URL that returned this response */
83    private $effectiveUrl;
84
85    /**
86     * @param int|string      $statusCode The response status code (e.g. 200)
87     * @param array           $headers    The response headers
88     * @param StreamInterface $body       The body of the response
89     * @param array           $options    Response message options
90     *     - reason_phrase: Set a custom reason phrase
91     *     - protocol_version: Set a custom protocol version
92     */
93    public function __construct(
94        $statusCode,
95        array $headers = [],
96        StreamInterface $body = null,
97        array $options = []
98    ) {
99        $this->statusCode = (int) $statusCode;
100        $this->handleOptions($options);
101
102        // Assume a reason phrase if one was not applied as an option
103        if (!$this->reasonPhrase &&
104            isset(self::$statusTexts[$this->statusCode])
105        ) {
106            $this->reasonPhrase = self::$statusTexts[$this->statusCode];
107        }
108
109        if ($headers) {
110            $this->setHeaders($headers);
111        }
112
113        if ($body) {
114            $this->setBody($body);
115        }
116    }
117
118    public function getStatusCode()
119    {
120        return $this->statusCode;
121    }
122
123    public function setStatusCode($code)
124    {
125        return $this->statusCode = (int) $code;
126    }
127
128    public function getReasonPhrase()
129    {
130        return $this->reasonPhrase;
131    }
132
133    public function setReasonPhrase($phrase)
134    {
135        return $this->reasonPhrase = $phrase;
136    }
137
138    public function json(array $config = [])
139    {
140        try {
141            return Utils::jsonDecode(
142                (string) $this->getBody(),
143                isset($config['object']) ? !$config['object'] : true,
144                512,
145                isset($config['big_int_strings']) ? JSON_BIGINT_AS_STRING : 0
146            );
147        } catch (\InvalidArgumentException $e) {
148            throw new ParseException(
149                $e->getMessage(),
150                $this
151            );
152        }
153    }
154
155    public function xml(array $config = [])
156    {
157        $disableEntities = libxml_disable_entity_loader(true);
158        $internalErrors = libxml_use_internal_errors(true);
159
160        try {
161            // Allow XML to be retrieved even if there is no response body
162            $xml = new \SimpleXMLElement(
163                (string) $this->getBody() ?: '<root />',
164                isset($config['libxml_options']) ? $config['libxml_options'] : LIBXML_NONET,
165                false,
166                isset($config['ns']) ? $config['ns'] : '',
167                isset($config['ns_is_prefix']) ? $config['ns_is_prefix'] : false
168            );
169            libxml_disable_entity_loader($disableEntities);
170            libxml_use_internal_errors($internalErrors);
171        } catch (\Exception $e) {
172            libxml_disable_entity_loader($disableEntities);
173            libxml_use_internal_errors($internalErrors);
174            throw new XmlParseException(
175                'Unable to parse response body into XML: ' . $e->getMessage(),
176                $this,
177                $e,
178                (libxml_get_last_error()) ?: null
179            );
180        }
181
182        return $xml;
183    }
184
185    public function getEffectiveUrl()
186    {
187        return $this->effectiveUrl;
188    }
189
190    public function setEffectiveUrl($url)
191    {
192        $this->effectiveUrl = $url;
193    }
194
195    /**
196     * Accepts and modifies the options provided to the response in the
197     * constructor.
198     *
199     * @param array $options Options array passed by reference.
200     */
201    protected function handleOptions(array &$options = [])
202    {
203        parent::handleOptions($options);
204        if (isset($options['reason_phrase'])) {
205            $this->reasonPhrase = $options['reason_phrase'];
206        }
207    }
208}
209