81750bd2474627a774b61d4ea307bfd2af3720bb
[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 {
49 $this->stream = $stream;
50 if (is_resource($stream)) {
51 $this->resource = $stream;
52 } elseif (is_string($stream)) {
53 $this->resource = fopen($stream, $mode);
54 } else {
55 throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717284);
56 }
57 }
58
59 /**
60 * Reads all data from the stream into a string, from the beginning to end.
61 *
62 * This method MUST attempt to seek to the beginning of the stream before
63 * reading data and read the stream until the end is reached.
64 *
65 * Warning: This could attempt to load a large amount of data into memory.
66 *
67 * This method MUST NOT raise an exception in order to conform with PHP's
68 * string casting operations.
69 *
70 * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
71 * @return string
72 */
73 public function __toString()
74 {
75 if (!$this->isReadable()) {
76 return '';
77 }
78 try {
79 $this->rewind();
80 return $this->getContents();
81 } catch (\RuntimeException $e) {
82 return '';
83 }
84 }
85
86 /**
87 * Closes the stream and any underlying resources.
88 *
89 * @return void
90 */
91 public function close()
92 {
93 if (!$this->resource) {
94 return;
95 }
96 $resource = $this->detach();
97 fclose($resource);
98 }
99
100 /**
101 * Separates any underlying resources from the stream.
102 *
103 * After the stream has been detached, the stream is in an unusable state.
104 *
105 * @return resource|null Underlying PHP stream, if any
106 */
107 public function detach()
108 {
109 $resource = $this->resource;
110 $this->resource = null;
111 return $resource;
112 }
113
114 /**
115 * Get the size of the stream if known.
116 *
117 * @return int|null Returns the size in bytes if known, or null if unknown.
118 */
119 public function getSize()
120 {
121 if ($this->resource === null) {
122 return null;
123 }
124 $stats = fstat($this->resource);
125 return $stats['size'];
126 }
127
128 /**
129 * Returns the current position of the file read/write pointer
130 *
131 * @return int Position of the file pointer
132 * @throws \RuntimeException on error.
133 */
134 public function tell()
135 {
136 if (!$this->resource) {
137 throw new \RuntimeException('No resource available; cannot tell position', 1436717285);
138 }
139 $result = ftell($this->resource);
140 if (!is_int($result)) {
141 throw new \RuntimeException('Error occurred during tell operation', 1436717286);
142 }
143 return $result;
144 }
145
146 /**
147 * Returns true if the stream is at the end of the stream.
148 *
149 * @return bool
150 */
151 public function eof()
152 {
153 if (!$this->resource) {
154 return true;
155 }
156 return feof($this->resource);
157 }
158
159 /**
160 * Returns whether or not the stream is seekable.
161 *
162 * @return bool
163 */
164 public function isSeekable()
165 {
166 if (!$this->resource) {
167 return false;
168 }
169 return (bool)$this->getMetadata('seekable');
170 }
171
172 /**
173 * Seek to a position in the stream.
174 *
175 * @link http://www.php.net/manual/en/function.fseek.php
176 *
177 * @param int $offset Stream offset
178 * @param int $whence Specifies how the cursor position will be calculated
179 * based on the seek offset. Valid values are identical to the built-in
180 * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
181 * offset bytes SEEK_CUR: Set position to current location plus offset
182 * SEEK_END: Set position to end-of-stream plus offset.
183 *
184 * @throws \RuntimeException on failure.
185 */
186 public function seek($offset, $whence = SEEK_SET)
187 {
188 if (!$this->resource) {
189 throw new \RuntimeException('No resource available; cannot seek position', 1436717287);
190 }
191
192 if (!$this->isSeekable()) {
193 throw new \RuntimeException('Stream is not seekable', 1436717288);
194 }
195 $result = fseek($this->resource, $offset, $whence);
196 if ($result !== 0) {
197 throw new \RuntimeException('Error seeking within stream', 1436717289);
198 }
199 }
200
201 /**
202 * Seek to the beginning of the stream.
203 *
204 * If the stream is not seekable, this method will raise an exception;
205 * otherwise, it will perform a seek(0).
206 *
207 * @see seek()
208 * @link http://www.php.net/manual/en/function.fseek.php
209 * @throws \RuntimeException on failure.
210 */
211 public function rewind()
212 {
213 $this->seek(0);
214 }
215
216 /**
217 * Returns whether or not the stream is writable.
218 *
219 * @return bool
220 */
221 public function isWritable()
222 {
223 if (!$this->resource) {
224 return false;
225 }
226 $uri = $this->getMetadata('uri');
227 return is_writable($uri);
228 }
229
230 /**
231 * Write data to the stream.
232 *
233 * @param string $string The string that is to be written.
234 * @return int Returns the number of bytes written to the stream.
235 * @throws \RuntimeException on failure.
236 */
237 public function write($string)
238 {
239 if (!$this->resource) {
240 throw new \RuntimeException('No resource available; cannot write', 1436717290);
241 }
242 $result = fwrite($this->resource, $string);
243 if ($result === false) {
244 throw new \RuntimeException('Error writing to stream', 1436717291);
245 }
246 return $result;
247 }
248
249 /**
250 * Returns whether or not the stream is readable.
251 *
252 * @return bool
253 */
254 public function isReadable()
255 {
256 if (!$this->resource) {
257 return false;
258 }
259 $mode = $this->getMetadata('mode');
260 return (strpos($mode, 'r') !== false || strpos($mode, '+') !== false);
261 }
262
263 /**
264 * Read data from the stream.
265 *
266 * @param int $length Read up to $length bytes from the object and return
267 * them. Fewer than $length bytes may be returned if underlying stream
268 * call returns fewer bytes.
269 * @return string Returns the data read from the stream, or an empty string
270 * if no bytes are available.
271 * @throws \RuntimeException if an error occurs.
272 */
273 public function read($length)
274 {
275 if (!$this->resource) {
276 throw new \RuntimeException('No resource available; cannot read', 1436717292);
277 }
278 if (!$this->isReadable()) {
279 throw new \RuntimeException('Stream is not readable', 1436717293);
280 }
281 $result = fread($this->resource, $length);
282 if ($result === false) {
283 throw new \RuntimeException('Error reading stream', 1436717294);
284 }
285 return $result;
286 }
287
288 /**
289 * Returns the remaining contents in a string
290 *
291 * @return string
292 * @throws \RuntimeException if unable to read or an error occurs while
293 * reading.
294 */
295 public function getContents()
296 {
297 if (!$this->isReadable()) {
298 return '';
299 }
300 $result = stream_get_contents($this->resource);
301 if ($result === false) {
302 throw new \RuntimeException('Error reading from stream', 1436717295);
303 }
304 return $result;
305 }
306
307 /**
308 * Get stream metadata as an associative array or retrieve a specific key.
309 *
310 * The keys returned are identical to the keys returned from PHP's
311 * stream_get_meta_data() function.
312 *
313 * @link http://php.net/manual/en/function.stream-get-meta-data.php
314 *
315 * @param string $key Specific metadata to retrieve.
316 *
317 * @return array|mixed|null Returns an associative array if no key is
318 * provided. Returns a specific key value if a key is provided and the
319 * value is found, or null if the key is not found.
320 */
321 public function getMetadata($key = null)
322 {
323 $metadata = stream_get_meta_data($this->resource);
324 if ($key === null) {
325 return $metadata;
326 }
327 if (!isset($metadata[$key])) {
328 return null;
329 }
330 return $metadata[$key];
331 }
332
333 /**
334 * Attach a new stream/resource to the instance.
335 *
336 * @param string|resource $resource
337 * @param string $mode
338 * @throws \InvalidArgumentException for stream identifier that cannot be cast to a resource
339 * @throws \InvalidArgumentException for non-resource stream
340 */
341 public function attach($resource, $mode = 'r')
342 {
343 $error = null;
344 if (!is_resource($resource) && is_string($resource)) {
345 set_error_handler(function ($e) use (&$error) {
346 $error = $e;
347 }, E_WARNING);
348 $resource = fopen($resource, $mode);
349 restore_error_handler();
350 }
351 if ($error) {
352 throw new \InvalidArgumentException('Invalid stream reference provided', 1436717296);
353 }
354 if (!is_resource($resource)) {
355 throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717297);
356 }
357 $this->resource = $resource;
358 }
359 }