1<?php
2
3/**
4 * EasyRdf
5 *
6 * LICENSE
7 *
8 * Copyright (c) 2009-2013 Nicholas J Humfrey.
9 * Copyright (c) 2005-2009 Zend Technologies USA Inc.
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions are met:
14 * 1. Redistributions of source code must retain the above copyright
15 *    notice, this list of conditions and the following disclaimer.
16 * 2. Redistributions in binary form must reproduce the above copyright notice,
17 *    this list of conditions and the following disclaimer in the documentation
18 *    and/or other materials provided with the distribution.
19 * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
20 *    promote products derived from this software without specific prior
21 *    written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 * POSSIBILITY OF SUCH DAMAGE.
34 *
35 * @package    EasyRdf
36 * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
37 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc.
38 * @license    http://www.opensource.org/licenses/bsd-license.php
39 */
40
41/**
42 * Class that represents an HTTP 1.0 / 1.1 response message.
43 *
44 * @package    EasyRdf
45 * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
46 *             Copyright (c) 2005-2009 Zend Technologies USA Inc.
47 * @license    http://www.opensource.org/licenses/bsd-license.php
48 */
49class EasyRdf_Http_Response
50{
51
52    /**
53     * The HTTP response status code
54     *
55     * @var int
56     */
57    private $status;
58
59    /**
60     * The HTTP response code as string
61     * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
62     *
63     * @var string
64     */
65    private $message;
66
67    /**
68     * The HTTP response headers array
69     *
70     * @var array
71     */
72    private $headers = array();
73
74    /**
75     * The HTTP response body
76     *
77     * @var string
78     */
79    private $body;
80
81    /**
82     * Constructor.
83     *
84     * @param  int     $status HTTP Status code
85     * @param  array   $headers The HTTP response headers
86     * @param  string  $body The content of the response
87     * @param  string  $version The HTTP Version (1.0 or 1.1)
88     * @param  string  $message The HTTP response Message
89     * @return object  EasyRdf_Http_Response
90     */
91    public function __construct(
92        $status,
93        $headers,
94        $body = null,
95        $version = '1.1',
96        $message = null
97    ) {
98        $this->status = intval($status);
99        $this->body = $body;
100        $this->version = $version;
101        $this->message = $message;
102
103        foreach ($headers as $k => $v) {
104            $k = ucwords(strtolower($k));
105            $this->headers[$k] = $v;
106        }
107    }
108
109    /**
110     * Check whether the response in successful
111     *
112     * @return boolean
113     */
114    public function isSuccessful()
115    {
116        return ($this->status >= 200 && $this->status < 300);
117    }
118
119    /**
120     * Check whether the response is an error
121     *
122     * @return boolean
123     */
124    public function isError()
125    {
126        return ($this->status >= 400 && $this->status < 600);
127    }
128
129    /**
130     * Check whether the response is a redirection
131     *
132     * @return boolean
133     */
134    public function isRedirect()
135    {
136        return ($this->status >= 300 && $this->status < 400);
137    }
138
139    /**
140     * Get the HTTP response status code
141     *
142     * @return int
143     */
144    public function getStatus()
145    {
146        return $this->status;
147    }
148
149    /**
150     * Return a message describing the HTTP response code
151     * (Eg. "OK", "Not Found", "Moved Permanently")
152     *
153     * @return string
154     */
155    public function getMessage()
156    {
157        return $this->message;
158    }
159
160    /**
161     * Get the response body as string
162     *
163     * @return string
164     */
165    public function getBody()
166    {
167        // Decode the body if it was transfer-encoded
168        switch (strtolower($this->getHeader('transfer-encoding'))) {
169            // Handle chunked body
170            case 'chunked':
171                return self::decodeChunkedBody($this->body);
172                break;
173
174            // No transfer encoding, or unknown encoding extension:
175            // return body as is
176            default:
177                return $this->body;
178                break;
179        }
180    }
181
182    /**
183     * Get the raw response body (as transfered "on wire") as string
184     *
185     * If the body is encoded (with Transfer-Encoding, not content-encoding -
186     * IE "chunked" body), gzip compressed, etc. it will not be decoded.
187     *
188     * @return string
189     */
190    public function getRawBody()
191    {
192        return $this->body;
193    }
194
195    /**
196     * Get the HTTP version of the response
197     *
198     * @return string
199     */
200    public function getVersion()
201    {
202        return $this->version;
203    }
204
205    /**
206     * Get the response headers
207     *
208     * @return array
209     */
210    public function getHeaders()
211    {
212        return $this->headers;
213    }
214
215    /**
216     * Get a specific header as string, or null if it is not set
217     *
218     * @param string$header
219     * @return string|array|null
220     */
221    public function getHeader($header)
222    {
223        $header = ucwords(strtolower($header));
224        if (array_key_exists($header, $this->headers)) {
225            return $this->headers[$header];
226        } else {
227            return null;
228        }
229    }
230
231    /**
232     * Get all headers as string
233     *
234     * @param boolean $statusLine Whether to return the first status line (ie "HTTP 200 OK")
235     * @param string  $br         Line breaks (eg. "\n", "\r\n", "<br />")
236     * @return string
237     */
238    public function getHeadersAsString($statusLine = true, $br = "\n")
239    {
240        $str = '';
241
242        if ($statusLine) {
243            $str = "HTTP/{$this->version} {$this->status} {$this->message}{$br}";
244        }
245
246        // Iterate over the headers and stringify them
247        foreach ($this->headers as $name => $value) {
248            if (is_string($value)) {
249                $str .= "{$name}: {$value}{$br}";
250            } elseif (is_array($value)) {
251                foreach ($value as $subval) {
252                    $str .= "{$name}: {$subval}{$br}";
253                }
254            }
255        }
256
257        return $str;
258    }
259
260    /**
261     * Create an EasyRdf_Http_Response object from a HTTP response string
262     *
263     * @param string $responseStr
264     * @return EasyRdf_Http_Response
265     */
266    public static function fromString($responseStr)
267    {
268        // First, split body and headers
269        $matches = preg_split('|(?:\r?\n){2}|m', $responseStr, 2);
270        if ($matches and sizeof($matches) == 2) {
271            list ($headerLines, $body) = $matches;
272        } else {
273            throw new EasyRdf_Exception(
274                "Failed to parse HTTP response."
275            );
276        }
277
278        // Split headers part to lines
279        $headerLines = preg_split('|[\r\n]+|m', $headerLines);
280        $status = array_shift($headerLines);
281        if (preg_match("|^HTTP/([\d\.x]+) (\d+) ([^\r\n]+)|", $status, $m)) {
282            $version = $m[1];
283            $status = $m[2];
284            $message = $m[3];
285        } else {
286            throw new EasyRdf_Exception(
287                "Failed to parse HTTP response status line."
288            );
289        }
290
291        // Process the rest of the header lines
292        $headers = array();
293        foreach ($headerLines as $line) {
294            if (preg_match("|^([\w-]+):\s+(.+)$|", $line, $m)) {
295                $hName = ucwords(strtolower($m[1]));
296                $hValue = $m[2];
297
298                if (isset($headers[$hName])) {
299                    if (! is_array($headers[$hName])) {
300                        $headers[$hName] = array($headers[$hName]);
301                    }
302                    $headers[$hName][] = $hValue;
303                } else {
304                    $headers[$hName] = $hValue;
305                }
306            }
307        }
308
309        return new EasyRdf_Http_Response($status, $headers, $body, $version, $message);
310    }
311
312
313    /**
314     * Decode a "chunked" transfer-encoded body and return the decoded text
315     *
316     * @param string $body
317     * @return string
318     */
319    public static function decodeChunkedBody($body)
320    {
321        $decBody = '';
322
323        while (trim($body)) {
324            if (preg_match('/^([\da-fA-F]+)[^\r\n]*\r\n/sm', $body, $m)) {
325                $length = hexdec(trim($m[1]));
326                $cut = strlen($m[0]);
327                $decBody .= substr($body, $cut, $length);
328                $body = substr($body, $cut + $length + 2);
329            } else {
330                throw new EasyRdf_Exception(
331                    "Failed to decode chunked body in HTTP response."
332                );
333            }
334        }
335
336        return $decBody;
337    }
338
339
340    /**
341     * Get the entire response as string
342     *
343     * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
344     * @return string
345     */
346    public function asString($br = "\n")
347    {
348        return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
349    }
350
351    /**
352     * Implements magic __toString()
353     *
354     * @return string
355     */
356    public function __toString()
357    {
358        return $this->asString();
359    }
360}
361