1 <?php
  2 
  3 /**
  4  * Class for rex_socket responses.
  5  *
  6  * @author gharlan
  7  *
  8  * @package redaxo\core
  9  */
 10 class rex_socket_response
 11 {
 12     private $stream;
 13     private $chunked = false;
 14     private $chunkPos = 0;
 15     private $chunkLength = 0;
 16     private $statusCode;
 17     private $statusMessage;
 18     private $header = '';
 19     private $headers = [];
 20     private $body;
 21 
 22     /**
 23      * Constructor.
 24      *
 25      * @param resource $stream Socket stream
 26      *
 27      * @throws InvalidArgumentException
 28      */
 29     public function __construct($stream)
 30     {
 31         if (!is_resource($stream)) {
 32             throw new InvalidArgumentException(sprintf('Expecting $stream to be a resource, but %s given!', gettype($stream)));
 33         }
 34 
 35         $this->stream = $stream;
 36 
 37         while (!feof($this->stream) && strpos($this->header, "\r\n\r\n") === false) {
 38             $this->header .= fgets($this->stream);
 39         }
 40         $this->header = rtrim($this->header);
 41         if (preg_match('@^HTTP/1\.\d ([0-9]+) (\V+)@', $this->header, $matches)) {
 42             $this->statusCode = (int) ($matches[1]);
 43             $this->statusMessage = $matches[2];
 44         }
 45         $this->chunked = stripos($this->header, 'transfer-encoding: chunked') !== false;
 46     }
 47 
 48     /**
 49      * Returns the HTTP status code, e.g. 200.
 50      *
 51      * @return int
 52      */
 53     public function getStatusCode()
 54     {
 55         return $this->statusCode;
 56     }
 57 
 58     /**
 59      * Returns the HTTP status message, e.g. "OK".
 60      *
 61      * @return string
 62      */
 63     public function getStatusMessage()
 64     {
 65         return $this->statusMessage;
 66     }
 67 
 68     /**
 69      * Returns wether the status is "200 OK".
 70      *
 71      * @return bool
 72      */
 73     public function isOk()
 74     {
 75         return $this->statusCode == 200;
 76     }
 77 
 78     /**
 79      * Returns wether the status class is "Informational".
 80      *
 81      * @return bool
 82      */
 83     public function isInformational()
 84     {
 85         return $this->statusCode >= 100 && $this->statusCode < 200;
 86     }
 87 
 88     /**
 89      * Returns wether the status class is "Success".
 90      *
 91      * @return bool
 92      */
 93     public function isSuccessful()
 94     {
 95         return $this->statusCode >= 200 && $this->statusCode < 300;
 96     }
 97 
 98     /**
 99      * Returns wether the status class is "Redirection".
100      *
101      * @return bool
102      */
103     public function isRedirection()
104     {
105         return $this->statusCode >= 300 && $this->statusCode < 400;
106     }
107 
108     /**
109      * Returns wether the status class is "Client Error".
110      *
111      * @return bool
112      */
113     public function isClientError()
114     {
115         return $this->statusCode >= 400 && $this->statusCode < 500;
116     }
117 
118     /**
119      * Returns wether the status class is "Server Error".
120      *
121      * @return bool
122      */
123     public function isServerError()
124     {
125         return $this->statusCode >= 500 && $this->statusCode < 600;
126     }
127 
128     /**
129      * Returns wether the status is invalid.
130      *
131      * @return bool
132      */
133     public function isInvalid()
134     {
135         return $this->statusCode < 100 || $this->statusCode >= 600;
136     }
137 
138     /**
139      * Returns the header for the given key, or the entire header if no key is given.
140      *
141      * @param string $key     Header key
142      * @param string $default Default value (is returned if the header is not set)
143      *
144      * @return string
145      */
146     public function getHeader($key = null, $default = null)
147     {
148         if ($key === null) {
149             return $this->header;
150         }
151         $key = strtolower($key);
152         if (isset($this->headers[$key])) {
153             return $this->headers[$key];
154         }
155         if (preg_match('@^' . preg_quote($key, '@') . ': (\V*)@im', $this->header, $matches)) {
156             return $this->headers[$key] = $matches[1];
157         }
158         return $this->headers[$key] = $default;
159     }
160 
161     /**
162      * Returns up to `$length` bytes from the body, or `false` if the end is reached.
163      *
164      * @param int $length Max number of bytes
165      *
166      * @return bool|string
167      */
168     public function getBufferedBody($length = 1024)
169     {
170         if (feof($this->stream)) {
171             return false;
172         }
173         if ($this->chunked) {
174             if ($this->chunkPos == 0) {
175                 $this->chunkLength = hexdec(fgets($this->stream));
176                 if ($this->chunkLength == 0) {
177                     return false;
178                 }
179             }
180             $pos = ftell($this->stream);
181             $buf = fread($this->stream, min($length, $this->chunkLength - $this->chunkPos));
182             $this->chunkPos += ftell($this->stream) - $pos;
183             if ($this->chunkPos >= $this->chunkLength) {
184                 fgets($this->stream);
185                 $this->chunkPos = 0;
186                 $this->chunkLength = 0;
187             }
188             return $buf;
189         }
190         return fread($this->stream, $length);
191     }
192 
193     /**
194      * Returns the entire body.
195      *
196      * @return string
197      */
198     public function getBody()
199     {
200         if ($this->body === null) {
201             while (($buf = $this->getBufferedBody()) !== false) {
202                 $this->body .= $buf;
203             }
204         }
205         return $this->body;
206     }
207 
208     /**
209      * Writes the body to the given resource.
210      *
211      * @param string|resource $resource File path or file pointer
212      *
213      * @return bool `true` on success, `false` on failure
214      */
215     public function writeBodyTo($resource)
216     {
217         $close = false;
218         if (is_string($resource) && rex_dir::create(dirname($resource))) {
219             $resource = fopen($resource, 'w');
220             $close = true;
221         }
222         if (!is_resource($resource)) {
223             return false;
224         }
225         $success = true;
226         while ($success && ($buf = $this->getBufferedBody()) !== false) {
227             $success = (bool) fwrite($resource, $buf);
228         }
229         if ($close) {
230             fclose($resource);
231         }
232         return $success;
233     }
234 }
235