[FEATURE] Introduce Request/Response based on PSR-7
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Http / Stream.php
1 <?php
2 namespace TYPO3\CMS\Core\Http;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\StreamInterface;
18
19 /**
20 * Default implementation for the StreamInterface of the PSR-7 standard
21 * Acts mainly as a decorator class for streams/resources.
22 *
23 * Highly inspired by https://github.com/phly/http/
24 *
25 * @internal Note that this is not public API yet.
26 */
27 class Stream implements StreamInterface {
28
29 /**
30 * The actual PHP resource
31 * @var resource
32 */
33 protected $resource;
34
35 /**
36 * @var string|resource
37 */
38 protected $stream;
39
40 /**
41 * Constructor setting up the PHP resource
42 *
43 * @param string|resource $stream
44 * @param string $mode Mode with which to open stream
45 * @throws \InvalidArgumentException
46 */
47 public function __construct($stream, $mode = 'r') {
48 $this->stream = $stream;
49 if (is_resource($stream)) {
50 $this->resource = $stream;
51 } elseif (is_string($stream)) {
52 $this->resource = fopen($stream, $mode);
53 } else {
54 throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717284);
55 }
56 }
57
58 /**
59 * Reads all data from the stream into a string, from the beginning to end.
60 *
61 * This method MUST attempt to seek to the beginning of the stream before
62 * reading data and read the stream until the end is reached.
63 *
64 * Warning: This could attempt to load a large amount of data into memory.
65 *
66 * This method MUST NOT raise an exception in order to conform with PHP's
67 * string casting operations.
68 *
69 * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
70 * @return string
71 */
72 public function __toString() {
73 if (!$this->isReadable()) {
74 return '';
75 }
76 try {
77 $this->rewind();
78 return $this->getContents();
79 } catch (\RuntimeException $e) {
80 return '';
81 }
82 }
83
84 /**
85 * Closes the stream and any underlying resources.
86 *
87 * @return void
88 */
89 public function close() {
90 if (!$this->resource) {
91 return;
92 }
93 $resource = $this->detach();
94 fclose($resource);
95 }
96
97 /**
98 * Separates any underlying resources from the stream.
99 *
100 * After the stream has been detached, the stream is in an unusable state.
101 *
102 * @return resource|null Underlying PHP stream, if any
103 */
104 public function detach() {
105 $resource = $this->resource;
106 $this->resource = NULL;
107 return $resource;
108 }
109
110 /**
111 * Get the size of the stream if known.
112 *
113 * @return int|null Returns the size in bytes if known, or null if unknown.
114 */
115 public function getSize() {
116 if ($this->resource === NULL) {
117 return NULL;
118 }
119 $stats = fstat($this->resource);
120 return $stats['size'];
121 }
122
123 /**
124 * Returns the current position of the file read/write pointer
125 *
126 * @return int Position of the file pointer
127 * @throws \RuntimeException on error.
128 */
129 public function tell() {
130 if (!$this->resource) {
131 throw new \RuntimeException('No resource available; cannot tell position', 1436717285);
132 }
133 $result = ftell($this->resource);
134 if (!is_int($result)) {
135 throw new \RuntimeException('Error occurred during tell operation', 1436717286);
136 }
137 return $result;
138 }
139
140 /**
141 * Returns true if the stream is at the end of the stream.
142 *
143 * @return bool
144 */
145 public function eof() {
146 if (!$this->resource) {
147 return TRUE;
148 }
149 return feof($this->resource);
150 }
151
152 /**
153 * Returns whether or not the stream is seekable.
154 *
155 * @return bool
156 */
157 public function isSeekable() {
158 if (!$this->resource) {
159 return FALSE;
160 }
161 return (bool)$this->getMetadata('seekable');
162 }
163
164 /**
165 * Seek to a position in the stream.
166 *
167 * @link http://www.php.net/manual/en/function.fseek.php
168 *
169 * @param int $offset Stream offset
170 * @param int $whence Specifies how the cursor position will be calculated
171 * based on the seek offset. Valid values are identical to the built-in
172 * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
173 * offset bytes SEEK_CUR: Set position to current location plus offset
174 * SEEK_END: Set position to end-of-stream plus offset.
175 *
176 * @throws \RuntimeException on failure.
177 */
178 public function seek($offset, $whence = SEEK_SET) {
179 if (!$this->resource) {
180 throw new \RuntimeException('No resource available; cannot seek position', 1436717287);
181 }
182
183 if (!$this->isSeekable()) {
184 throw new \RuntimeException('Stream is not seekable', 1436717288);
185 }
186 $result = fseek($this->resource, $offset, $whence);
187 if ($result !== 0) {
188 throw new \RuntimeException('Error seeking within stream', 1436717289);
189 }
190 }
191
192 /**
193 * Seek to the beginning of the stream.
194 *
195 * If the stream is not seekable, this method will raise an exception;
196 * otherwise, it will perform a seek(0).
197 *
198 * @see seek()
199 * @link http://www.php.net/manual/en/function.fseek.php
200 * @throws \RuntimeException on failure.
201 */
202 public function rewind() {
203 $this->seek(0);
204 }
205
206 /**
207 * Returns whether or not the stream is writable.
208 *
209 * @return bool
210 */
211 public function isWritable() {
212 if (!$this->resource) {
213 return FALSE;
214 }
215 $uri = $this->getMetadata('uri');
216 return is_writable($uri);
217 }
218
219 /**
220 * Write data to the stream.
221 *
222 * @param string $string The string that is to be written.
223 * @return int Returns the number of bytes written to the stream.
224 * @throws \RuntimeException on failure.
225 */
226 public function write($string) {
227 if (!$this->resource) {
228 throw new \RuntimeException('No resource available; cannot write', 1436717290);
229 }
230 $result = fwrite($this->resource, $string);
231 if ($result === FALSE) {
232 throw new \RuntimeException('Error writing to stream', 1436717291);
233 }
234 return $result;
235 }
236
237 /**
238 * Returns whether or not the stream is readable.
239 *
240 * @return bool
241 */
242 public function isReadable() {
243 if (!$this->resource) {
244 return FALSE;
245 }
246 $mode = $this->getMetadata('mode');
247 return (strpos($mode, 'r') !== FALSE || strpos($mode, '+') !== FALSE);
248 }
249
250 /**
251 * Read data from the stream.
252 *
253 * @param int $length Read up to $length bytes from the object and return
254 * them. Fewer than $length bytes may be returned if underlying stream
255 * call returns fewer bytes.
256 * @return string Returns the data read from the stream, or an empty string
257 * if no bytes are available.
258 * @throws \RuntimeException if an error occurs.
259 */
260 public function read($length) {
261 if (!$this->resource) {
262 throw new \RuntimeException('No resource available; cannot read', 1436717292);
263 }
264 if (!$this->isReadable()) {
265 throw new \RuntimeException('Stream is not readable', 1436717293);
266 }
267 $result = fread($this->resource, $length);
268 if ($result === FALSE) {
269 throw new \RuntimeException('Error reading stream', 1436717294);
270 }
271 return $result;
272 }
273
274 /**
275 * Returns the remaining contents in a string
276 *
277 * @return string
278 * @throws \RuntimeException if unable to read or an error occurs while
279 * reading.
280 */
281 public function getContents() {
282 if (!$this->isReadable()) {
283 return '';
284 }
285 $result = stream_get_contents($this->resource);
286 if ($result === FALSE) {
287 throw new \RuntimeException('Error reading from stream', 1436717295);
288 }
289 return $result;
290 }
291
292 /**
293 * Get stream metadata as an associative array or retrieve a specific key.
294 *
295 * The keys returned are identical to the keys returned from PHP's
296 * stream_get_meta_data() function.
297 *
298 * @link http://php.net/manual/en/function.stream-get-meta-data.php
299 *
300 * @param string $key Specific metadata to retrieve.
301 *
302 * @return array|mixed|null Returns an associative array if no key is
303 * provided. Returns a specific key value if a key is provided and the
304 * value is found, or null if the key is not found.
305 */
306 public function getMetadata($key = NULL) {
307 $metadata = stream_get_meta_data($this->resource);
308 if ($key === NULL) {
309 return $metadata;
310 }
311 if (!isset($metadata[$key])) {
312 return NULL;
313 }
314 return $metadata[$key];
315 }
316
317 /**
318 * Attach a new stream/resource to the instance.
319 *
320 * @param string|resource $resource
321 * @param string $mode
322 * @throws \InvalidArgumentException for stream identifier that cannot be cast to a resource
323 * @throws \InvalidArgumentException for non-resource stream
324 */
325 public function attach($resource, $mode = 'r') {
326 $error = NULL;
327 if (!is_resource($resource) && is_string($resource)) {
328 set_error_handler(function ($e) use (&$error) {
329 $error = $e;
330 }, E_WARNING);
331 $resource = fopen($resource, $mode);
332 restore_error_handler();
333 }
334 if ($error) {
335 throw new \InvalidArgumentException('Invalid stream reference provided', 1436717296);
336 }
337 if (!is_resource($resource)) {
338 throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717297);
339 }
340 $this->resource = $resource;
341 }
342
343 }