1: <?php
2:
3: namespace Guzzle\Http\Message;
4:
5: use Guzzle\Common\Collection;
6: use Guzzle\Common\Exception\RuntimeException;
7: use Guzzle\Http\EntityBodyInterface;
8: use Guzzle\Http\EntityBody;
9: use Guzzle\Http\Exception\BadResponseException;
10: use Guzzle\Parser\ParserRegistry;
11:
12: /**
13: * Guzzle HTTP response object
14: */
15: class Response extends AbstractMessage
16: {
17: /**
18: * @var array Array of reason phrases and their corresponding status codes
19: */
20: private static $statusTexts = array(
21: 100 => 'Continue',
22: 101 => 'Switching Protocols',
23: 102 => 'Processing',
24: 200 => 'OK',
25: 201 => 'Created',
26: 202 => 'Accepted',
27: 203 => 'Non-Authoritative Information',
28: 204 => 'No Content',
29: 205 => 'Reset Content',
30: 206 => 'Partial Content',
31: 207 => 'Multi-Status',
32: 208 => 'Already Reported',
33: 226 => 'IM Used',
34: 300 => 'Multiple Choices',
35: 301 => 'Moved Permanently',
36: 302 => 'Found',
37: 303 => 'See Other',
38: 304 => 'Not Modified',
39: 305 => 'Use Proxy',
40: 307 => 'Temporary Redirect',
41: 308 => 'Permanent Redirect',
42: 400 => 'Bad Request',
43: 401 => 'Unauthorized',
44: 402 => 'Payment Required',
45: 403 => 'Forbidden',
46: 404 => 'Not Found',
47: 405 => 'Method Not Allowed',
48: 406 => 'Not Acceptable',
49: 407 => 'Proxy Authentication Required',
50: 408 => 'Request Timeout',
51: 409 => 'Conflict',
52: 410 => 'Gone',
53: 411 => 'Length Required',
54: 412 => 'Precondition Failed',
55: 413 => 'Request Entity Too Large',
56: 414 => 'Request-URI Too Long',
57: 415 => 'Unsupported Media Type',
58: 416 => 'Requested Range Not Satisfiable',
59: 417 => 'Expectation Failed',
60: 422 => 'Unprocessable Entity',
61: 423 => 'Locked',
62: 424 => 'Failed Dependency',
63: 425 => 'Reserved for WebDAV advanced collections expired proposal',
64: 426 => 'Upgrade required',
65: 428 => 'Precondition Required',
66: 429 => 'Too Many Requests',
67: 431 => 'Request Header Fields Too Large',
68: 500 => 'Internal Server Error',
69: 501 => 'Not Implemented',
70: 502 => 'Bad Gateway',
71: 503 => 'Service Unavailable',
72: 504 => 'Gateway Timeout',
73: 505 => 'HTTP Version Not Supported',
74: 506 => 'Variant Also Negotiates (Experimental)',
75: 507 => 'Insufficient Storage',
76: 508 => 'Loop Detected',
77: 510 => 'Not Extended',
78: 511 => 'Network Authentication Required',
79: );
80:
81: /**
82: * @var EntityBodyInterface The response body
83: */
84: protected $body;
85:
86: /**
87: * @var string The reason phrase of the response (human readable code)
88: */
89: protected $reasonPhrase;
90:
91: /**
92: * @var string The status code of the response
93: */
94: protected $statusCode;
95:
96: /**
97: * @var string Response protocol
98: */
99: protected $protocol = 'HTTP';
100:
101: /**
102: * @var array Information about the request
103: */
104: protected $info = array();
105:
106: /**
107: * @var RequestInterface Request object that may or may not be set
108: */
109: protected $request = null;
110:
111: /**
112: * @var array Cacheable response codes (see RFC 2616:13.4)
113: */
114: protected $cacheResponseCodes = array(200, 203, 206, 300, 301, 410);
115:
116: /**
117: * @var Response If a redirect was issued or an intermediate response was issued
118: */
119: protected $previous;
120:
121: /**
122: * Create a new Response based on a raw response message
123: *
124: * @param string $message Response message
125: *
126: * @return Response|bool Returns false on error
127: */
128: public static function fromMessage($message)
129: {
130: $data = ParserRegistry::getInstance()->getParser('message')->parseResponse($message);
131: if (!$data) {
132: return false;
133: }
134:
135: $response = new static($data['code'], $data['headers'], $data['body']);
136: $response->setProtocol($data['protocol'], $data['version'])
137: ->setStatus($data['code'], $data['reason_phrase']);
138:
139: // Set the appropriate Content-Length if the one set is inaccurate (e.g. setting to X)
140: $contentLength = (string) $response->getHeader('Content-Length');
141: $actualLength = strlen($data['body']);
142: if (strlen($data['body']) > 0 && $contentLength != $actualLength) {
143: $response->setHeader('Content-Length', $actualLength);
144: }
145:
146: return $response;
147: }
148:
149: /**
150: * Construct the response
151: *
152: * @param string $statusCode The response status code (e.g. 200, 404, etc)
153: * @param Collection|array $headers The response headers
154: * @param string|resource|EntityBodyInterface $body The body of the response
155: *
156: * @throws BadResponseException if an invalid response code is given
157: */
158: public function __construct($statusCode, $headers = null, $body = null)
159: {
160: $this->setStatus($statusCode);
161: $this->params = new Collection();
162: $this->body = EntityBody::factory($body !== null ? $body : '');
163:
164: if ($headers) {
165: if (!is_array($headers) && !($headers instanceof Collection)) {
166: throw new BadResponseException('Invalid headers argument received');
167: }
168: foreach ($headers as $key => $value) {
169: $this->addHeaders(array($key => $value));
170: }
171: }
172: }
173:
174: /**
175: * Convert the response object to a string
176: *
177: * @return string
178: */
179: public function __toString()
180: {
181: return $this->getMessage();
182: }
183:
184: /**
185: * Get the response entity body
186: *
187: * @param bool $asString Set to TRUE to return a string of the body rather than a full body object
188: *
189: * @return EntityBodyInterface|string
190: */
191: public function getBody($asString = false)
192: {
193: return $asString ? (string) $this->body : $this->body;
194: }
195:
196: /**
197: * Set the response entity body
198: *
199: * @param EntityBodyInterface|string $body Body to set
200: *
201: * @return self
202: */
203: public function setBody($body)
204: {
205: $this->body = EntityBody::factory($body);
206:
207: return $this;
208: }
209:
210: /**
211: * Set the protocol and protocol version of the response
212: *
213: * @param string $protocol Response protocol
214: * @param string $version Protocol version
215: *
216: * @return Response
217: */
218: public function setProtocol($protocol, $version)
219: {
220: $this->protocol = $protocol;
221: $this->protocolVersion = $version;
222:
223: return $this;
224: }
225:
226: /**
227: * Get the protocol used for the response (e.g. HTTP)
228: *
229: * @return string
230: */
231: public function getProtocol()
232: {
233: return $this->protocol ?: 'HTTP';
234: }
235:
236: /**
237: * Get the HTTP protocol version
238: *
239: * @return string
240: */
241: public function getProtocolVersion()
242: {
243: return $this->protocolVersion ?: '1.1';
244: }
245:
246: /**
247: * Get a cURL transfer information
248: *
249: * @param string $key A single statistic to check
250: *
251: * @return array|string|null Returns all stats if no key is set, a single stat if a key is set, or null if a key
252: * is set and not found
253: * @link http://www.php.net/manual/en/function.curl-getinfo.php
254: */
255: public function getInfo($key = null)
256: {
257: if ($key === null) {
258: return $this->info;
259: } elseif (array_key_exists($key, $this->info)) {
260: return $this->info[$key];
261: } else {
262: return null;
263: }
264: }
265:
266: /**
267: * Set the transfer information
268: *
269: * @param array $info Array of cURL transfer stats
270: *
271: * @return Response
272: */
273: public function setInfo(array $info)
274: {
275: $this->info = $info;
276:
277: return $this;
278: }
279:
280: /**
281: * Set the response status
282: *
283: * @param int $statusCode Response status code to set
284: * @param string $reasonPhrase Response reason phrase
285: *
286: * @return Response
287: * @throws BadResponseException when an invalid response code is received
288: */
289: public function setStatus($statusCode, $reasonPhrase = '')
290: {
291: $this->statusCode = (int) $statusCode;
292:
293: if (!$reasonPhrase && array_key_exists($this->statusCode, self::$statusTexts)) {
294: $this->reasonPhrase = self::$statusTexts[$this->statusCode];
295: } else {
296: $this->reasonPhrase = $reasonPhrase;
297: }
298:
299: return $this;
300: }
301:
302: /**
303: * Get the response status code
304: *
305: * @return integer
306: */
307: public function getStatusCode()
308: {
309: return $this->statusCode;
310: }
311:
312: /**
313: * Get the entire response as a string
314: *
315: * @return string
316: */
317: public function getMessage()
318: {
319: $message = $this->getRawHeaders();
320:
321: // Only include the body in the message if the size is < 2MB
322: $size = $this->body->getSize();
323: if ($size < 2097152) {
324: $message .= (string) $this->body;
325: }
326:
327: return $message;
328: }
329:
330: /**
331: * Get the the raw message headers as a string
332: *
333: * @return string
334: */
335: public function getRawHeaders()
336: {
337: $headers = 'HTTP/1.1 ' . $this->statusCode . ' ' . $this->reasonPhrase . "\r\n";
338: $lines = $this->getHeaderLines();
339: if (!empty($lines)) {
340: $headers .= implode("\r\n", $lines) . "\r\n";
341: }
342:
343: return $headers . "\r\n";
344: }
345:
346: /**
347: * Get the request object (or null) that is associated with this response
348: *
349: * @return RequestInterface
350: */
351: public function getRequest()
352: {
353: return $this->request;
354: }
355:
356: /**
357: * Get the response reason phrase- a human readable version of the numeric
358: * status code
359: *
360: * @return string
361: */
362: public function getReasonPhrase()
363: {
364: return $this->reasonPhrase;
365: }
366:
367: /**
368: * Get the Accept-Ranges HTTP header
369: *
370: * @return string Returns what partial content range types this server supports.
371: */
372: public function getAcceptRanges()
373: {
374: return $this->getHeader('Accept-Ranges', true);
375: }
376:
377: /**
378: * Get the Age HTTP header
379: *
380: * @param bool $headerOnly Set to TRUE to only retrieve the Age header rather than calculating the age
381: *
382: * @return integer|null Returns the age the object has been in a proxy cache in seconds.
383: */
384: public function getAge($headerOnly = false)
385: {
386: $age = $this->getHeader('Age', true);
387:
388: if (!$headerOnly && $age === null && $this->getDate()) {
389: $age = time() - strtotime($this->getDate());
390: }
391:
392: return $age;
393: }
394:
395: /**
396: * Get the Allow HTTP header
397: *
398: * @return string|null Returns valid actions for a specified resource. To be used for a 405 Method not allowed.
399: */
400: public function getAllow()
401: {
402: return $this->getHeader('Allow', true);
403: }
404:
405: /**
406: * Check if an HTTP method is allowed by checking the Allow response header
407: *
408: * @param string $method Method to check
409: *
410: * @return bool
411: */
412: public function isMethodAllowed($method)
413: {
414: $allow = $this->getHeader('Allow');
415: if ($allow) {
416: foreach (explode(',', $allow) as $allowable) {
417: if (!strcasecmp(trim($allowable), $method)) {
418: return true;
419: }
420: }
421: }
422:
423: return false;
424: }
425:
426: /**
427: * Get the Cache-Control HTTP header
428: *
429: * @return Header|null Returns a Header object that tells all caching mechanisms from server to client whether they
430: * may cache this object.
431: */
432: public function getCacheControl()
433: {
434: return $this->getHeader('Cache-Control');
435: }
436:
437: /**
438: * Get the Connection HTTP header
439: *
440: * @return string
441: */
442: public function getConnection()
443: {
444: return $this->getHeader('Connection', true);
445: }
446:
447: /**
448: * Get the Content-Encoding HTTP header
449: *
450: * @return string|null Returns the type of encoding used on the data. One of compress, deflate, gzip, identity.
451: */
452: public function getContentEncoding()
453: {
454: return $this->getHeader('Content-Encoding', true);
455: }
456:
457: /**
458: * Get the Content-Language HTTP header
459: *
460: * @return string|null Returns the language the content is in.
461: */
462: public function getContentLanguage()
463: {
464: return $this->getHeader('Content-Language', true);
465: }
466:
467: /**
468: * Get the Content-Length HTTP header
469: *
470: * @return integer Returns the length of the response body in bytes
471: */
472: public function getContentLength()
473: {
474: return (int) $this->getHeader('Content-Length', true);
475: }
476:
477: /**
478: * Get the Content-Location HTTP header
479: *
480: * @return string|null Returns an alternate location for the returned data (e.g /index.htm)
481: */
482: public function getContentLocation()
483: {
484: return $this->getHeader('Content-Location', true);
485: }
486:
487: /**
488: * Get the Content-Disposition HTTP header
489: *
490: * @return string|null Returns the Content-Disposition header
491: */
492: public function getContentDisposition()
493: {
494: return (string) $this->getHeader('Content-Disposition')->setGlue(';');
495: }
496:
497: /**
498: * Get the Content-MD5 HTTP header
499: *
500: * @return string|null Returns a Base64-encoded binary MD5 sum of the content of the response.
501: */
502: public function getContentMd5()
503: {
504: return $this->getHeader('Content-MD5', true);
505: }
506:
507: /**
508: * Get the Content-Range HTTP header
509: *
510: * @return string Returns where in a full body message this partial message belongs (e.g. bytes 21010-47021/47022).
511: */
512: public function getContentRange()
513: {
514: return $this->getHeader('Content-Range', true);
515: }
516:
517: /**
518: * Get the Content-Type HTTP header
519: *
520: * @return string Returns the mime type of this content.
521: */
522: public function getContentType()
523: {
524: return $this->getHeader('Content-Type', true);
525: }
526:
527: /**
528: * Checks if the Content-Type is of a certain type. This is useful if the
529: * Content-Type header contains charset information and you need to know if
530: * the Content-Type matches a particular type.
531: *
532: * @param string $type Content type to check against
533: *
534: * @return bool
535: */
536: public function isContentType($type)
537: {
538: return stripos($this->getContentType(), $type) !== false;
539: }
540:
541: /**
542: * Get the Date HTTP header
543: *
544: * @return string|null Returns the date and time that the message was sent.
545: */
546: public function getDate()
547: {
548: return $this->getHeader('Date', true);
549: }
550:
551: /**
552: * Get the ETag HTTP header
553: *
554: * @return string|null Returns an identifier for a specific version of a resource, often a Message digest.
555: */
556: public function getEtag()
557: {
558: return $this->getHeader('ETag', true);
559: }
560:
561: /**
562: * Get the Expires HTTP header
563: *
564: * @return string|null Returns the date/time after which the response is considered stale.
565: */
566: public function getExpires()
567: {
568: return $this->getHeader('Expires', true);
569: }
570:
571: /**
572: * Get the Last-Modified HTTP header
573: *
574: * @return string|null Returns the last modified date for the requested object, in RFC 2822 format
575: * (e.g. Tue, 15 Nov 1994 12:45:26 GMT)
576: */
577: public function getLastModified()
578: {
579: return $this->getHeader('Last-Modified', true);
580: }
581:
582: /**
583: * Get the Location HTTP header
584: *
585: * @return string|null Used in redirection, or when a new resource has been created.
586: */
587: public function getLocation()
588: {
589: return $this->getHeader('Location', true);
590: }
591:
592: /**
593: * Get the Pragma HTTP header
594: *
595: * @return Header|null Returns the implementation-specific headers that may have various effects anywhere along
596: * the request-response chain.
597: */
598: public function getPragma()
599: {
600: return $this->getHeader('Pragma');
601: }
602:
603: /**
604: * Get the Proxy-Authenticate HTTP header
605: *
606: * @return string|null Authentication to access the proxy (e.g. Basic)
607: */
608: public function getProxyAuthenticate()
609: {
610: return $this->getHeader('Proxy-Authenticate', true);
611: }
612:
613: /**
614: * Get the Retry-After HTTP header
615: *
616: * @return int|null If an entity is temporarily unavailable, this instructs the client to try again after a
617: * specified period of time.
618: */
619: public function getRetryAfter()
620: {
621: $time = $this->getHeader('Retry-After', true);
622: if ($time === null) {
623: return null;
624: }
625:
626: if (!is_numeric($time)) {
627: $time = strtotime($time) - time();
628: }
629:
630: return (int) $time;
631: }
632:
633: /**
634: * Get the Server HTTP header
635: *
636: * @return string|null A name for the server
637: */
638: public function getServer()
639: {
640: return $this->getHeader('Server', true);
641: }
642:
643: /**
644: * Get the Set-Cookie HTTP header
645: *
646: * @return Header|null An HTTP cookie.
647: */
648: public function getSetCookie()
649: {
650: return $this->getHeader('Set-Cookie');
651: }
652:
653: /**
654: * Get the Trailer HTTP header
655: *
656: * @return string|null The Trailer general field value indicates that the given set of header fields is present in
657: * the trailer of a message encoded with chunked transfer-coding.
658: */
659: public function getTrailer()
660: {
661: return $this->getHeader('Trailer', true);
662: }
663:
664: /**
665: * Get the Transfer-Encoding HTTP header
666: *
667: * @return string|null The form of encoding used to safely transfer the entity to the user. Currently defined
668: * methods are: chunked
669: */
670: public function getTransferEncoding()
671: {
672: return $this->getHeader('Transfer-Encoding', true);
673: }
674:
675: /**
676: * Get the Vary HTTP header
677: *
678: * @return string|null Tells downstream proxies how to match future request headers to decide whether the cached
679: * response can be used rather than requesting a fresh one from the origin server.
680: */
681: public function getVary()
682: {
683: return $this->getHeader('Vary', true);
684: }
685:
686: /**
687: * Get the Via HTTP header
688: *
689: * @return string|null Informs the client of proxies through which the response was sent.
690: * (e.g. 1.0 fred, 1.1 nowhere.com (Apache/1.1))
691: */
692: public function getVia()
693: {
694: return $this->getHeader('Via', true);
695: }
696:
697: /**
698: * Get the Warning HTTP header
699: *
700: * @return string|null A general warning about possible problems with the entity body.
701: * (e.g. 199 Miscellaneous warning)
702: */
703: public function getWarning()
704: {
705: return $this->getHeader('Warning', true);
706: }
707:
708: /**
709: * Get the WWW-Authenticate HTTP header
710: *
711: * @return string|null Indicates the authentication scheme that should be used to access the requested entity
712: * (e.g. Basic)
713: */
714: public function getWwwAuthenticate()
715: {
716: return $this->getHeader('WWW-Authenticate', true);
717: }
718:
719: /**
720: * Checks if HTTP Status code is a Client Error (4xx)
721: *
722: * @return bool
723: */
724: public function isClientError()
725: {
726: return $this->statusCode >= 400 && $this->statusCode < 500;
727: }
728:
729: /**
730: * Checks if HTTP Status code is Server OR Client Error (4xx or 5xx)
731: *
732: * @return boolean
733: */
734: public function isError()
735: {
736: return $this->isClientError() || $this->isServerError();
737: }
738:
739: /**
740: * Checks if HTTP Status code is Information (1xx)
741: *
742: * @return bool
743: */
744: public function isInformational()
745: {
746: return $this->statusCode < 200;
747: }
748:
749: /**
750: * Checks if HTTP Status code is a Redirect (3xx)
751: *
752: * @return bool
753: */
754: public function isRedirect()
755: {
756: return $this->statusCode >= 300 && $this->statusCode < 400;
757: }
758:
759: /**
760: * Checks if HTTP Status code is Server Error (5xx)
761: *
762: * @return bool
763: */
764: public function isServerError()
765: {
766: return $this->statusCode >= 500 && $this->statusCode < 600;
767: }
768:
769: /**
770: * Checks if HTTP Status code is Successful (2xx | 304)
771: *
772: * @return bool
773: */
774: public function isSuccessful()
775: {
776: return ($this->statusCode >= 200 && $this->statusCode < 300) || $this->statusCode == 304;
777: }
778:
779: /**
780: * Set the request object associated with the response
781: *
782: * @param RequestInterface $request The request object used to generate the response
783: *
784: * @return Response
785: */
786: public function setRequest(RequestInterface $request)
787: {
788: $this->request = $request;
789:
790: return $this;
791: }
792:
793: /**
794: * Check if the response can be cached
795: *
796: * @return bool Returns TRUE if the response can be cached or false if not
797: */
798: public function canCache()
799: {
800: // Check if the response is cacheable based on the code
801: if (!in_array((int) $this->getStatusCode(), $this->cacheResponseCodes)) {
802: return false;
803: }
804:
805: // Make sure a valid body was returned and can be cached
806: if ((!$this->getBody()->isReadable() || !$this->getBody()->isSeekable())
807: && ($this->getContentLength() > 0 || $this->getTransferEncoding() == 'chunked')) {
808: return false;
809: }
810:
811: // Never cache no-store resources (this is a private cache, so private
812: // can be cached)
813: if ($this->hasCacheControlDirective('no-store')) {
814: return false;
815: }
816:
817: return $this->isFresh() || $this->getFreshness() === null || $this->canValidate();
818: }
819:
820: /**
821: * Gets the number of seconds from the current time in which this response is still considered fresh
822: *
823: * @return int|null Returns the number of seconds
824: */
825: public function getMaxAge()
826: {
827: // s-max-age, then max-age, then Expires
828: if ($age = $this->getCacheControlDirective('s-maxage')) {
829: return $age;
830: }
831:
832: if ($age = $this->getCacheControlDirective('max-age')) {
833: return $age;
834: }
835:
836: if ($this->getHeader('Expires')) {
837: return strtotime($this->getExpires()) - time();
838: }
839:
840: return null;
841: }
842:
843: /**
844: * Check if the response is considered fresh.
845: *
846: * A response is considered fresh when its age is less than or equal to the freshness lifetime (maximum age) of the
847: * response.
848: *
849: * @return bool|null
850: */
851: public function isFresh()
852: {
853: $fresh = $this->getFreshness();
854:
855: return $fresh === null ? null : $fresh >= 0;
856: }
857:
858: /**
859: * Check if the response can be validated against the origin server using a conditional GET request.
860: *
861: * @return bool
862: */
863: public function canValidate()
864: {
865: return $this->getEtag() || $this->getLastModified();
866: }
867:
868: /**
869: * Get the freshness of the response by returning the difference of the maximum lifetime of the response and the
870: * age of the response (max-age - age).
871: *
872: * Freshness values less than 0 mean that the response is no longer fresh and is ABS(freshness) seconds expired.
873: * Freshness values of greater than zero is the number of seconds until the response is no longer fresh. A NULL
874: * result means that no freshness information is available.
875: *
876: * @return int
877: */
878: public function getFreshness()
879: {
880: $maxAge = $this->getMaxAge();
881: $age = $this->getAge();
882:
883: return $maxAge && $age ? ($maxAge - $age) : null;
884: }
885:
886: /**
887: * Get the previous response (e.g. Redirect response)
888: *
889: * @return null|Response
890: */
891: public function getPreviousResponse()
892: {
893: return $this->previous;
894: }
895:
896: /**
897: * Set the previous response
898: *
899: * @param Response $response Response to set
900: *
901: * @return self
902: */
903: public function setPreviousResponse(Response $response)
904: {
905: $this->previous = $response;
906:
907: return $this;
908: }
909:
910: /**
911: * Parse the JSON response body and return an array
912: *
913: * @return array|string|int|bool|float
914: * @throws RuntimeException if the response body is not in JSON format
915: */
916: public function json()
917: {
918: $data = json_decode((string) $this->body, true);
919: if (JSON_ERROR_NONE !== json_last_error()) {
920: throw new RuntimeException('Unable to parse response body into JSON: ' . json_last_error());
921: }
922:
923: return $data === null ? array() : $data;
924: }
925:
926: /**
927: * Parse the XML response body and return a SimpleXMLElement
928: *
929: * @return \SimpleXMLElement
930: * @throws RuntimeException if the response body is not in XML format
931: */
932: public function xml()
933: {
934: try {
935: // Allow XML to be retrieved even if there is no response body
936: $xml = new \SimpleXMLElement((string) $this->body ?: '<root />');
937: } catch (\Exception $e) {
938: throw new RuntimeException('Unable to parse response body into XML: ' . $e->getMessage());
939: }
940:
941: return $xml;
942: }
943: }
944: