Passed
Push — master ( 34708b...6c4d34 )
by Alexander
08:27
created

Response::prepare()   B

Complexity

Conditions 11
Paths 21

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 12.0935

Importance

Changes 0
Metric Value
cc 11
eloc 26
c 0
b 0
f 0
nc 21
nop 0
dl 0
loc 38
ccs 19
cts 24
cp 0.7917
crap 12.0935
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidConfigException;
13
use yii\helpers\FileHelper;
14
use yii\helpers\Inflector;
15
use yii\helpers\StringHelper;
16
use yii\helpers\Url;
17
18
/**
19
 * The web Response class represents an HTTP response.
20
 *
21
 * It holds the [[headers]], [[cookies]] and [[content]] that is to be sent to the client.
22
 * It also controls the HTTP [[statusCode|status code]].
23
 *
24
 * Response is configured as an application component in [[\yii\web\Application]] by default.
25
 * You can access that instance via `Yii::$app->response`.
26
 *
27
 * You can modify its configuration by adding an array to your application config under `components`
28
 * as it is shown in the following example:
29
 *
30
 * ```php
31
 * 'response' => [
32
 *     'format' => yii\web\Response::FORMAT_JSON,
33
 *     'charset' => 'UTF-8',
34
 *     // ...
35
 * ]
36
 * ```
37
 *
38
 * For more details and usage information on Response, see the [guide article on responses](guide:runtime-responses).
39
 *
40
 * @property-read CookieCollection $cookies The cookie collection.
41
 * @property-write string $downloadHeaders The attachment file name.
42
 * @property-read HeaderCollection $headers The header collection.
43
 * @property-read bool $isClientError Whether this response indicates a client error.
44
 * @property-read bool $isEmpty Whether this response is empty.
45
 * @property-read bool $isForbidden Whether this response indicates the current request is forbidden.
46
 * @property-read bool $isInformational Whether this response is informational.
47
 * @property-read bool $isInvalid Whether this response has a valid [[statusCode]].
48
 * @property-read bool $isNotFound Whether this response indicates the currently requested resource is not
49
 * found.
50
 * @property-read bool $isOk Whether this response is OK.
51
 * @property-read bool $isRedirection Whether this response is a redirection.
52
 * @property-read bool $isServerError Whether this response indicates a server error.
53
 * @property-read bool $isSuccessful Whether this response is successful.
54
 * @property int $statusCode The HTTP status code to send with the response.
55
 * @property-write \Throwable $statusCodeByException The exception object.
56
 *
57
 * @author Qiang Xue <[email protected]>
58
 * @author Carsten Brandt <[email protected]>
59
 * @since 2.0
60
 */
61
class Response extends \yii\base\Response
62
{
63
    /**
64
     * @event \yii\base\Event an event that is triggered at the beginning of [[send()]].
65
     */
66
    const EVENT_BEFORE_SEND = 'beforeSend';
67
    /**
68
     * @event \yii\base\Event an event that is triggered at the end of [[send()]].
69
     */
70
    const EVENT_AFTER_SEND = 'afterSend';
71
    /**
72
     * @event \yii\base\Event an event that is triggered right after [[prepare()]] is called in [[send()]].
73
     * You may respond to this event to filter the response content before it is sent to the client.
74
     */
75
    const EVENT_AFTER_PREPARE = 'afterPrepare';
76
    const FORMAT_RAW = 'raw';
77
    const FORMAT_HTML = 'html';
78
    const FORMAT_JSON = 'json';
79
    const FORMAT_JSONP = 'jsonp';
80
    const FORMAT_XML = 'xml';
81
82
    /**
83
     * @var string the response format. This determines how to convert [[data]] into [[content]]
84
     * when the latter is not set. The value of this property must be one of the keys declared in the [[formatters]] array.
85
     * By default, the following formats are supported:
86
     *
87
     * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion.
88
     *   No extra HTTP header will be added.
89
     * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion.
90
     *   The "Content-Type" header will set as "text/html".
91
     * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type"
92
     *   header will be set as "application/json".
93
     * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type"
94
     *   header will be set as "text/javascript". Note that in this case `$data` must be an array
95
     *   with "data" and "callback" elements. The former refers to the actual data to be sent,
96
     *   while the latter refers to the name of the JavaScript callback.
97
     * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]]
98
     *   for more details.
99
     *
100
     * You may customize the formatting process or support additional formats by configuring [[formatters]].
101
     * @see formatters
102
     */
103
    public $format = self::FORMAT_HTML;
104
    /**
105
     * @var string the MIME type (e.g. `application/json`) from the request ACCEPT header chosen for this response.
106
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
107
     */
108
    public $acceptMimeType;
109
    /**
110
     * @var array the parameters (e.g. `['q' => 1, 'version' => '1.0']`) associated with the [[acceptMimeType|chosen MIME type]].
111
     * This is a list of name-value pairs associated with [[acceptMimeType]] from the ACCEPT HTTP header.
112
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
113
     */
114
    public $acceptParams = [];
115
    /**
116
     * @var array the formatters for converting data into the response content of the specified [[format]].
117
     * The array keys are the format names, and the array values are the corresponding configurations
118
     * for creating the formatter objects.
119
     * @see format
120
     * @see defaultFormatters
121
     */
122
    public $formatters = [];
123
    /**
124
     * @var mixed the original response data. When this is not null, it will be converted into [[content]]
125
     * according to [[format]] when the response is being sent out.
126
     * @see content
127
     */
128
    public $data;
129
    /**
130
     * @var string|null the response content. When [[data]] is not null, it will be converted into [[content]]
131
     * according to [[format]] when the response is being sent out.
132
     * @see data
133
     */
134
    public $content;
135
    /**
136
     * @var resource|array|callable the stream to be sent. This can be a stream handle or an array of stream handle,
137
     * the begin position and the end position. Alternatively it can be set to a callable, which returns
138
     * (or [yields](https://www.php.net/manual/en/language.generators.syntax.php)) an array of strings that should
139
     * be echoed and flushed out one by one.
140
     *
141
     * Note that when this property is set, the [[data]] and [[content]] properties will be ignored by [[send()]].
142
     */
143
    public $stream;
144
    /**
145
     * @var string|null the charset of the text response. If not set, it will use
146
     * the value of [[Application::charset]].
147
     */
148
    public $charset;
149
    /**
150
     * @var string the HTTP status description that comes together with the status code.
151
     * @see httpStatuses
152
     */
153
    public $statusText = 'OK';
154
    /**
155
     * @var string|null the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`,
156
     * or '1.1' if that is not available.
157
     */
158
    public $version;
159
    /**
160
     * @var bool whether the response has been sent. If this is true, calling [[send()]] will do nothing.
161
     */
162
    public $isSent = false;
163
    /**
164
     * @var array list of HTTP status codes and the corresponding texts
165
     */
166
    public static $httpStatuses = [
167
        100 => 'Continue',
168
        101 => 'Switching Protocols',
169
        102 => 'Processing',
170
        118 => 'Connection timed out',
171
        200 => 'OK',
172
        201 => 'Created',
173
        202 => 'Accepted',
174
        203 => 'Non-Authoritative',
175
        204 => 'No Content',
176
        205 => 'Reset Content',
177
        206 => 'Partial Content',
178
        207 => 'Multi-Status',
179
        208 => 'Already Reported',
180
        210 => 'Content Different',
181
        226 => 'IM Used',
182
        300 => 'Multiple Choices',
183
        301 => 'Moved Permanently',
184
        302 => 'Found',
185
        303 => 'See Other',
186
        304 => 'Not Modified',
187
        305 => 'Use Proxy',
188
        306 => 'Reserved',
189
        307 => 'Temporary Redirect',
190
        308 => 'Permanent Redirect',
191
        310 => 'Too many Redirect',
192
        400 => 'Bad Request',
193
        401 => 'Unauthorized',
194
        402 => 'Payment Required',
195
        403 => 'Forbidden',
196
        404 => 'Not Found',
197
        405 => 'Method Not Allowed',
198
        406 => 'Not Acceptable',
199
        407 => 'Proxy Authentication Required',
200
        408 => 'Request Time-out',
201
        409 => 'Conflict',
202
        410 => 'Gone',
203
        411 => 'Length Required',
204
        412 => 'Precondition Failed',
205
        413 => 'Request Entity Too Large',
206
        414 => 'Request-URI Too Long',
207
        415 => 'Unsupported Media Type',
208
        416 => 'Requested range unsatisfiable',
209
        417 => 'Expectation failed',
210
        418 => 'I\'m a teapot',
211
        421 => 'Misdirected Request',
212
        422 => 'Unprocessable entity',
213
        423 => 'Locked',
214
        424 => 'Method failure',
215
        425 => 'Unordered Collection',
216
        426 => 'Upgrade Required',
217
        428 => 'Precondition Required',
218
        429 => 'Too Many Requests',
219
        431 => 'Request Header Fields Too Large',
220
        449 => 'Retry With',
221
        450 => 'Blocked by Windows Parental Controls',
222
        451 => 'Unavailable For Legal Reasons',
223
        500 => 'Internal Server Error',
224
        501 => 'Not Implemented',
225
        502 => 'Bad Gateway or Proxy Error',
226
        503 => 'Service Unavailable',
227
        504 => 'Gateway Time-out',
228
        505 => 'HTTP Version not supported',
229
        507 => 'Insufficient storage',
230
        508 => 'Loop Detected',
231
        509 => 'Bandwidth Limit Exceeded',
232
        510 => 'Not Extended',
233
        511 => 'Network Authentication Required',
234
    ];
235
236
    /**
237
     * @var int the HTTP status code to send with the response.
238
     */
239
    private $_statusCode = 200;
240
    /**
241
     * @var HeaderCollection
242
     */
243
    private $_headers;
244
245
246
    /**
247
     * Initializes this component.
248
     */
249 352
    public function init()
250
    {
251 352
        if ($this->version === null) {
252 352
            if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
253
                $this->version = '1.0';
254
            } else {
255 352
                $this->version = '1.1';
256
            }
257
        }
258 352
        if ($this->charset === null) {
259 352
            $this->charset = Yii::$app->charset;
260
        }
261 352
        $this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
262 352
    }
263
264
    /**
265
     * @return int the HTTP status code to send with the response.
266
     */
267 63
    public function getStatusCode()
268
    {
269 63
        return $this->_statusCode;
270
    }
271
272
    /**
273
     * Sets the response status code.
274
     * This method will set the corresponding status text if `$text` is null.
275
     * @param int $value the status code
276
     * @param string|null $text the status text. If not set, it will be set automatically based on the status code.
277
     * @throws InvalidArgumentException if the status code is invalid.
278
     * @return $this the response object itself
279
     */
280 56
    public function setStatusCode($value, $text = null)
281
    {
282 56
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
283
            $value = 200;
284
        }
285 56
        $this->_statusCode = (int) $value;
286 56
        if ($this->getIsInvalid()) {
287
            throw new InvalidArgumentException("The HTTP status code is invalid: $value");
288
        }
289 56
        if ($text === null) {
290 52
            $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : '';
291
        } else {
292 4
            $this->statusText = $text;
293
        }
294
295 56
        return $this;
296
    }
297
298
    /**
299
     * Sets the response status code based on the exception.
300
     * @param \Throwable $e the exception object.
301
     * @throws InvalidArgumentException if the status code is invalid.
302
     * @return $this the response object itself
303
     * @since 2.0.12
304
     */
305 18
    public function setStatusCodeByException($e)
306
    {
307 18
        if ($e instanceof HttpException) {
308 10
            $this->setStatusCode($e->statusCode);
309
        } else {
310 8
            $this->setStatusCode(500);
311
        }
312
313 18
        return $this;
314
    }
315
316
    /**
317
     * Returns the header collection.
318
     * The header collection contains the currently registered HTTP headers.
319
     * @return HeaderCollection the header collection
320
     */
321 129
    public function getHeaders()
322
    {
323 129
        if ($this->_headers === null) {
324 129
            $this->_headers = new HeaderCollection();
325
        }
326
327 129
        return $this->_headers;
328
    }
329
330
    /**
331
     * Sends the response to the client.
332
     */
333 36
    public function send()
334
    {
335 36
        if ($this->isSent) {
336 3
            return;
337
        }
338 36
        $this->trigger(self::EVENT_BEFORE_SEND);
339 36
        $this->prepare();
340 36
        $this->trigger(self::EVENT_AFTER_PREPARE);
341 36
        $this->sendHeaders();
342 36
        $this->sendContent();
343 36
        $this->trigger(self::EVENT_AFTER_SEND);
344 36
        $this->isSent = true;
345 36
    }
346
347
    /**
348
     * Clears the headers, cookies, content, status code of the response.
349
     */
350
    public function clear()
351
    {
352
        $this->_headers = null;
353
        $this->_cookies = null;
354
        $this->_statusCode = 200;
355
        $this->statusText = 'OK';
356
        $this->data = null;
357
        $this->stream = null;
358
        $this->content = null;
359
        $this->isSent = false;
360
    }
361
362
    /**
363
     * Sends the response headers to the client.
364
     */
365 36
    protected function sendHeaders()
366
    {
367 36
        if (headers_sent($file, $line)) {
368
            throw new HeadersAlreadySentException($file, $line);
369
        }
370 36
        if ($this->_headers) {
371 31
            foreach ($this->getHeaders() as $name => $values) {
372 31
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
373
                // set replace for first occurrence of header but false afterwards to allow multiple
374 31
                $replace = true;
375 31
                foreach ($values as $value) {
376 31
                    header("$name: $value", $replace);
377 31
                    $replace = false;
378
                }
379
            }
380
        }
381 36
        $statusCode = $this->getStatusCode();
382 36
        header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
383 36
        $this->sendCookies();
384 36
    }
385
386
    /**
387
     * Sends the cookies to the client.
388
     */
389 36
    protected function sendCookies()
390
    {
391 36
        if ($this->_cookies === null) {
392 31
            return;
393
        }
394 5
        $request = Yii::$app->getRequest();
395 5
        if ($request->enableCookieValidation) {
0 ignored issues
show
Bug Best Practice introduced by
The property enableCookieValidation does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
396 5
            if ($request->cookieValidationKey == '') {
0 ignored issues
show
Bug Best Practice introduced by
The property cookieValidationKey does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
397
                throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
398
            }
399 5
            $validationKey = $request->cookieValidationKey;
400
        }
401 5
        foreach ($this->getCookies() as $cookie) {
402 5
            $value = $cookie->value;
403 5
            if ($cookie->expire != 1 && isset($validationKey)) {
404 5
                $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
405
            }
406 5
            if (PHP_VERSION_ID >= 70300) {
407 5
                setcookie($cookie->name, $value, [
408 5
                    'expires' => $cookie->expire,
409 5
                    'path' => $cookie->path,
410 5
                    'domain' => $cookie->domain,
411 5
                    'secure' => $cookie->secure,
412 5
                    'httpOnly' => $cookie->httpOnly,
413 5
                    'sameSite' => !empty($cookie->sameSite) ? $cookie->sameSite : null,
414
                ]);
415
            } else {
416
                // Work around for setting sameSite cookie prior PHP 7.3
417
                // https://stackoverflow.com/questions/39750906/php-setcookie-samesite-strict/46971326#46971326
418
                $cookiePath = $cookie->path;
419
                if (!is_null($cookie->sameSite)) {
420
                    $cookiePath .= '; samesite=' . $cookie->sameSite;
421
                }
422
                setcookie($cookie->name, $value, $cookie->expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
423
            }
424
        }
425 5
    }
426
427
    /**
428
     * Sends the response content to the client.
429
     */
430 36
    protected function sendContent()
431
    {
432 36
        if ($this->stream === null) {
433 32
            echo $this->content;
434
435 32
            return;
436
        }
437
438
        // Try to reset time limit for big files
439 4
        if (!function_exists('set_time_limit') || !@set_time_limit(0)) {
440
            Yii::warning('set_time_limit() is not available', __METHOD__);
441
        }
442
443 4
        if (is_callable($this->stream)) {
444
            $data = call_user_func($this->stream);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type resource; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

444
            $data = call_user_func(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
445
            foreach ($data as $datum) {
446
                echo $datum;
447
                flush();
448
            }
449
            return;
450
        }
451
452 4
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
453
454 4
        if (is_array($this->stream)) {
455 4
            list($handle, $begin, $end) = $this->stream;
456
457
            // only seek if stream is seekable
458 4
            if ($this->isSeekable($handle)) {
459 3
                fseek($handle, $begin);
460
            }
461
462 4
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
463 3
                if ($pos + $chunkSize > $end) {
464 3
                    $chunkSize = $end - $pos + 1;
465
                }
466 3
                echo fread($handle, $chunkSize);
467 3
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
468
            }
469 4
            fclose($handle);
470
        } else {
471
            while (!feof($this->stream)) {
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type callable; however, parameter $stream of feof() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

471
            while (!feof(/** @scrutinizer ignore-type */ $this->stream)) {
Loading history...
472
                echo fread($this->stream, $chunkSize);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type callable; however, parameter $stream of fread() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

472
                echo fread(/** @scrutinizer ignore-type */ $this->stream, $chunkSize);
Loading history...
473
                flush();
474
            }
475
            fclose($this->stream);
0 ignored issues
show
Bug introduced by
It seems like $this->stream can also be of type callable; however, parameter $stream of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

475
            fclose(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
476
        }
477 4
    }
478
479
    /**
480
     * Sends a file to the browser.
481
     *
482
     * Note that this method only prepares the response for file sending. The file is not sent
483
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
484
     *
485
     * The following is an example implementation of a controller action that allows requesting files from a directory
486
     * that is not accessible from web:
487
     *
488
     * ```php
489
     * public function actionFile($filename)
490
     * {
491
     *     $storagePath = Yii::getAlias('@app/files');
492
     *
493
     *     // check filename for allowed chars (do not allow ../ to avoid security issue: downloading arbitrary files)
494
     *     if (!preg_match('/^[a-z0-9]+\.[a-z0-9]+$/i', $filename) || !is_file("$storagePath/$filename")) {
495
     *         throw new \yii\web\NotFoundHttpException('The file does not exists.');
496
     *     }
497
     *     return Yii::$app->response->sendFile("$storagePath/$filename", $filename);
498
     * }
499
     * ```
500
     *
501
     * @param string $filePath the path of the file to be sent.
502
     * @param string|null $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
503
     * @param array $options additional options for sending the file. The following options are supported:
504
     *
505
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
506
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
507
     *    meaning a download dialog will pop up.
508
     *
509
     * @return $this the response object itself
510
     * @see sendContentAsFile()
511
     * @see sendStreamAsFile()
512
     * @see xSendFile()
513
     */
514 10
    public function sendFile($filePath, $attachmentName = null, $options = [])
515
    {
516 10
        if (!isset($options['mimeType'])) {
517 10
            $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);
518
        }
519 10
        if ($attachmentName === null) {
520 9
            $attachmentName = basename($filePath);
521
        }
522 10
        $handle = fopen($filePath, 'rb');
523 10
        $this->sendStreamAsFile($handle, $attachmentName, $options);
524
525 6
        return $this;
526
    }
527
528
    /**
529
     * Sends the specified content as a file to the browser.
530
     *
531
     * Note that this method only prepares the response for file sending. The file is not sent
532
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
533
     *
534
     * @param string $content the content to be sent. The existing [[content]] will be discarded.
535
     * @param string $attachmentName the file name shown to the user.
536
     * @param array $options additional options for sending the file. The following options are supported:
537
     *
538
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
539
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
540
     *    meaning a download dialog will pop up.
541
     *
542
     * @return $this the response object itself
543
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
544
     * @see sendFile() for an example implementation.
545
     */
546 1
    public function sendContentAsFile($content, $attachmentName, $options = [])
547
    {
548 1
        $headers = $this->getHeaders();
549
550 1
        $contentLength = StringHelper::byteLength($content);
551 1
        $range = $this->getHttpRange($contentLength);
552
553 1
        if ($range === false) {
0 ignored issues
show
introduced by
The condition $range === false is always false.
Loading history...
554
            $headers->set('Content-Range', "bytes */$contentLength");
555
            throw new RangeNotSatisfiableHttpException();
556
        }
557
558 1
        list($begin, $end) = $range;
559 1
        if ($begin != 0 || $end != $contentLength - 1) {
560
            $this->setStatusCode(206);
561
            $headers->set('Content-Range', "bytes $begin-$end/$contentLength");
562
            $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1);
563
        } else {
564 1
            $this->setStatusCode(200);
565 1
            $this->content = $content;
566
        }
567
568 1
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
569 1
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
570
571 1
        $this->format = self::FORMAT_RAW;
572
573 1
        return $this;
574
    }
575
576
    /**
577
     * Sends the specified stream as a file to the browser.
578
     *
579
     * Note that this method only prepares the response for file sending. The file is not sent
580
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
581
     *
582
     * @param resource $handle the handle of the stream to be sent.
583
     * @param string $attachmentName the file name shown to the user.
584
     * @param array $options additional options for sending the file. The following options are supported:
585
     *
586
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
587
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
588
     *    meaning a download dialog will pop up.
589
     *  - `fileSize`: the size of the content to stream this is useful when size of the content is known
590
     *    and the content is not seekable. Defaults to content size using `ftell()`.
591
     *    This option is available since version 2.0.4.
592
     *
593
     * @return $this the response object itself
594
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
595
     * @see sendFile() for an example implementation.
596
     */
597 11
    public function sendStreamAsFile($handle, $attachmentName, $options = [])
598
    {
599 11
        $headers = $this->getHeaders();
600 11
        if (isset($options['fileSize'])) {
601
            $fileSize = $options['fileSize'];
602
        } else {
603 11
            if ($this->isSeekable($handle)) {
604 10
                fseek($handle, 0, SEEK_END);
605 10
                $fileSize = ftell($handle);
606
            } else {
607 1
                $fileSize = 0;
608
            }
609
        }
610
611 11
        $range = $this->getHttpRange($fileSize);
612 11
        if ($range === false) {
0 ignored issues
show
introduced by
The condition $range === false is always false.
Loading history...
613 4
            $headers->set('Content-Range', "bytes */$fileSize");
614 4
            throw new RangeNotSatisfiableHttpException();
615
        }
616
617 7
        list($begin, $end) = $range;
618 7
        if ($begin != 0 || $end != $fileSize - 1) {
619 3
            $this->setStatusCode(206);
620 3
            $headers->set('Content-Range', "bytes $begin-$end/$fileSize");
621
        } else {
622 4
            $this->setStatusCode(200);
623
        }
624
625 7
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
626 7
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
627
628 7
        $this->format = self::FORMAT_RAW;
629 7
        $this->stream = [$handle, $begin, $end];
630
631 7
        return $this;
632
    }
633
634
    /**
635
     * Sets a default set of HTTP headers for file downloading purpose.
636
     * @param string $attachmentName the attachment file name
637
     * @param string|null $mimeType the MIME type for the response. If null, `Content-Type` header will NOT be set.
638
     * @param bool $inline whether the browser should open the file within the browser window. Defaults to false,
639
     * meaning a download dialog will pop up.
640
     * @param int|null $contentLength the byte length of the file being downloaded. If null, `Content-Length` header will NOT be set.
641
     * @return $this the response object itself
642
     */
643 8
    public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null)
644
    {
645 8
        $headers = $this->getHeaders();
646
647 8
        $disposition = $inline ? 'inline' : 'attachment';
648 8
        $headers->setDefault('Pragma', 'public')
649 8
            ->setDefault('Accept-Ranges', 'bytes')
650 8
            ->setDefault('Expires', '0')
651 8
            ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
652 8
            ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName));
653
654 8
        if ($mimeType !== null) {
655 8
            $headers->setDefault('Content-Type', $mimeType);
656
        }
657
658 8
        if ($contentLength !== null) {
659 8
            $headers->setDefault('Content-Length', $contentLength);
660
        }
661
662 8
        return $this;
663
    }
664
665
    /**
666
     * Determines the HTTP range given in the request.
667
     * @param int $fileSize the size of the file that will be used to validate the requested HTTP range.
668
     * @return array|bool the range (begin, end), or false if the range request is invalid.
669
     */
670 12
    protected function getHttpRange($fileSize)
671
    {
672 12
        $rangeHeader = Yii::$app->getRequest()->getHeaders()->get('Range', '-');
0 ignored issues
show
Bug introduced by
The method getHeaders() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

672
        $rangeHeader = Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getHeaders()->get('Range', '-');
Loading history...
673 12
        if ($rangeHeader === '-') {
674 5
            return [0, $fileSize - 1];
675
        }
676 7
        if (!preg_match('/^bytes=(\d*)-(\d*)$/', $rangeHeader, $matches)) {
0 ignored issues
show
Bug introduced by
It seems like $rangeHeader can also be of type array and null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

676
        if (!preg_match('/^bytes=(\d*)-(\d*)$/', /** @scrutinizer ignore-type */ $rangeHeader, $matches)) {
Loading history...
677 1
            return false;
678
        }
679 6
        if ($matches[1] === '') {
680 2
            $start = $fileSize - $matches[2];
681 2
            $end = $fileSize - 1;
682 4
        } elseif ($matches[2] !== '') {
683 2
            $start = $matches[1];
684 2
            $end = $matches[2];
685 2
            if ($end >= $fileSize) {
686 2
                $end = $fileSize - 1;
687
            }
688
        } else {
689 2
            $start = $matches[1];
690 2
            $end = $fileSize - 1;
691
        }
692 6
        if ($start < 0 || $start > $end) {
693 3
            return false;
694
        }
695
696 3
        return [$start, $end];
697
    }
698
699
    /**
700
     * Sends existing file to a browser as a download using x-sendfile.
701
     *
702
     * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
703
     * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
704
     * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
705
     * increase in performance as the web application is allowed to terminate earlier while the webserver is
706
     * handling the request.
707
     *
708
     * The request is sent to the server through a special non-standard HTTP-header.
709
     * When the web server encounters the presence of such header it will discard all output and send the file
710
     * specified by that header using web server internals including all optimizations like caching-headers.
711
     *
712
     * As this header directive is non-standard different directives exists for different web servers applications:
713
     *
714
     * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
715
     * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
716
     * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
717
     * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
718
     * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
719
     *
720
     * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
721
     * a proper xHeader should be sent.
722
     *
723
     * **Note**
724
     *
725
     * This option allows to download files that are not under web folders, and even files that are otherwise protected
726
     * (deny from all) like `.htaccess`.
727
     *
728
     * **Side effects**
729
     *
730
     * If this option is disabled by the web server, when this method is called a download configuration dialog
731
     * will open but the downloaded file will have 0 bytes.
732
     *
733
     * **Known issues**
734
     *
735
     * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
736
     * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
737
     * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
738
     *
739
     * **Example**
740
     *
741
     * ```php
742
     * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg');
743
     * ```
744
     *
745
     * @param string $filePath file name with full path
746
     * @param string|null $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
747
     * @param array $options additional options for sending the file. The following options are supported:
748
     *
749
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
750
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
751
     *    meaning a download dialog will pop up.
752
     *  - xHeader: string, the name of the x-sendfile header. Defaults to "X-Sendfile".
753
     *
754
     * @return $this the response object itself
755
     * @see sendFile()
756
     */
757
    public function xSendFile($filePath, $attachmentName = null, $options = [])
758
    {
759
        if ($attachmentName === null) {
760
            $attachmentName = basename($filePath);
761
        }
762
        if (isset($options['mimeType'])) {
763
            $mimeType = $options['mimeType'];
764
        } elseif (($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
765
            $mimeType = 'application/octet-stream';
766
        }
767
        if (isset($options['xHeader'])) {
768
            $xHeader = $options['xHeader'];
769
        } else {
770
            $xHeader = 'X-Sendfile';
771
        }
772
773
        $disposition = empty($options['inline']) ? 'attachment' : 'inline';
774
        $this->getHeaders()
775
            ->setDefault($xHeader, $filePath)
776
            ->setDefault('Content-Type', $mimeType)
777
            ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName));
778
779
        $this->format = self::FORMAT_RAW;
780
781
        return $this;
782
    }
783
784
    /**
785
     * Returns Content-Disposition header value that is safe to use with both old and new browsers.
786
     *
787
     * Fallback name:
788
     *
789
     * - Causes issues if contains non-ASCII characters with codes less than 32 or more than 126.
790
     * - Causes issues if contains urlencoded characters (starting with `%`) or `%` character. Some browsers interpret
791
     *   `filename="X"` as urlencoded name, some don't.
792
     * - Causes issues if contains path separator characters such as `\` or `/`.
793
     * - Since value is wrapped with `"`, it should be escaped as `\"`.
794
     * - Since input could contain non-ASCII characters, fallback is obtained by transliteration.
795
     *
796
     * UTF name:
797
     *
798
     * - Causes issues if contains path separator characters such as `\` or `/`.
799
     * - Should be urlencoded since headers are ASCII-only.
800
     * - Could be omitted if it exactly matches fallback name.
801
     *
802
     * @param string $disposition
803
     * @param string $attachmentName
804
     * @return string
805
     *
806
     * @since 2.0.10
807
     */
808 8
    protected function getDispositionHeaderValue($disposition, $attachmentName)
809
    {
810 8
        $fallbackName = str_replace(
811 8
            ['%', '/', '\\', '"', "\x7F"],
812 8
            ['_', '_', '_', '\\"', '_'],
813 8
            Inflector::transliterate($attachmentName, Inflector::TRANSLITERATE_LOOSE)
814
        );
815 8
        $utfName = rawurlencode(str_replace(['%', '/', '\\'], '', $attachmentName));
816
817 8
        $dispositionHeader = "{$disposition}; filename=\"{$fallbackName}\"";
818 8
        if ($utfName !== $fallbackName) {
819 1
            $dispositionHeader .= "; filename*=utf-8''{$utfName}";
820
        }
821
822 8
        return $dispositionHeader;
823
    }
824
825
    /**
826
     * Redirects the browser to the specified URL.
827
     *
828
     * This method adds a "Location" header to the current response. Note that it does not send out
829
     * the header until [[send()]] is called. In a controller action you may use this method as follows:
830
     *
831
     * ```php
832
     * return Yii::$app->getResponse()->redirect($url);
833
     * ```
834
     *
835
     * In other places, if you want to send out the "Location" header immediately, you should use
836
     * the following code:
837
     *
838
     * ```php
839
     * Yii::$app->getResponse()->redirect($url)->send();
840
     * return;
841
     * ```
842
     *
843
     * In AJAX mode, this normally will not work as expected unless there are some
844
     * client-side JavaScript code handling the redirection. To help achieve this goal,
845
     * this method will send out a "X-Redirect" header instead of "Location".
846
     *
847
     * If you use the "yii" JavaScript module, it will handle the AJAX redirection as
848
     * described above. Otherwise, you should write the following JavaScript code to
849
     * handle the redirection:
850
     *
851
     * ```javascript
852
     * $document.ajaxComplete(function (event, xhr, settings) {
853
     *     var url = xhr && xhr.getResponseHeader('X-Redirect');
854
     *     if (url) {
855
     *         window.location = url;
856
     *     }
857
     * });
858
     * ```
859
     *
860
     * @param string|array $url the URL to be redirected to. This can be in one of the following formats:
861
     *
862
     * - a string representing a URL (e.g. "http://example.com")
863
     * - a string representing a URL alias (e.g. "@example.com")
864
     * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`).
865
     *   Note that the route is with respect to the whole application, instead of relative to a controller or module.
866
     *   [[Url::to()]] will be used to convert the array into a URL.
867
     *
868
     * Any relative URL that starts with a single forward slash "/" will be converted
869
     * into an absolute one by prepending it with the host info of the current request.
870
     *
871
     * @param int $statusCode the HTTP status code. Defaults to 302.
872
     * See <https://tools.ietf.org/html/rfc2616#section-10>
873
     * for details about HTTP status code
874
     * @param bool $checkAjax whether to specially handle AJAX (and PJAX) requests. Defaults to true,
875
     * meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser
876
     * to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as
877
     * an AJAX/PJAX response, may NOT cause browser redirection.
878
     * Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent.
879
     * @return $this the response object itself
880
     */
881 9
    public function redirect($url, $statusCode = 302, $checkAjax = true)
882
    {
883 9
        if (is_array($url) && isset($url[0])) {
884
            // ensure the route is absolute
885 8
            $url[0] = '/' . ltrim($url[0], '/');
886
        }
887 9
        $request = Yii::$app->getRequest();
888 9
        $normalizedUrl = Url::to($url);
889
        if (
890 9
            $normalizedUrl !== null
891 9
            && strncmp($normalizedUrl, '/', 1) === 0
892 9
            && strncmp($normalizedUrl, '//', 2) !== 0
893
        ) {
894 9
            $normalizedUrl = $request->getHostInfo() . $normalizedUrl;
0 ignored issues
show
Bug introduced by
The method getHostInfo() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

894
            $normalizedUrl = $request->/** @scrutinizer ignore-call */ getHostInfo() . $normalizedUrl;
Loading history...
895
        }
896
897 9
        if ($checkAjax && $request->getIsAjax()) {
0 ignored issues
show
Bug introduced by
The method getIsAjax() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

897
        if ($checkAjax && $request->/** @scrutinizer ignore-call */ getIsAjax()) {
Loading history...
898
            if (
899 7
                in_array($statusCode, [301, 302])
900 7
                && preg_match('/Trident\/|MSIE[ ]/', (string)$request->userAgent)
0 ignored issues
show
Bug Best Practice introduced by
The property userAgent does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
901
            ) {
902 3
                $statusCode = 200;
903
            }
904 7
            if ($request->getIsPjax()) {
0 ignored issues
show
Bug introduced by
The method getIsPjax() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

904
            if ($request->/** @scrutinizer ignore-call */ getIsPjax()) {
Loading history...
905 6
                $this->getHeaders()->set('X-Pjax-Url', $normalizedUrl);
906
            } else {
907 7
                $this->getHeaders()->set('X-Redirect', $normalizedUrl);
908
            }
909
        } else {
910 3
            $this->getHeaders()->set('Location', $normalizedUrl);
911
        }
912
913 9
        $this->setStatusCode($statusCode);
914
915 9
        return $this;
916
    }
917
918
    /**
919
     * Refreshes the current page.
920
     * The effect of this method call is the same as the user pressing the refresh button of his browser
921
     * (without re-posting data).
922
     *
923
     * In a controller action you may use this method like this:
924
     *
925
     * ```php
926
     * return Yii::$app->getResponse()->refresh();
927
     * ```
928
     *
929
     * @param string $anchor the anchor that should be appended to the redirection URL.
930
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
931
     * @return Response the response object itself
932
     */
933
    public function refresh($anchor = '')
934
    {
935
        return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor);
0 ignored issues
show
Bug introduced by
The method getUrl() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

935
        return $this->redirect(Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getUrl() . $anchor);
Loading history...
936
    }
937
938
    private $_cookies;
939
940
    /**
941
     * Returns the cookie collection.
942
     *
943
     * Through the returned cookie collection, you add or remove cookies as follows,
944
     *
945
     * ```php
946
     * // add a cookie
947
     * $response->cookies->add(new Cookie([
948
     *     'name' => $name,
949
     *     'value' => $value,
950
     * ]);
951
     *
952
     * // remove a cookie
953
     * $response->cookies->remove('name');
954
     * // alternatively
955
     * unset($response->cookies['name']);
956
     * ```
957
     *
958
     * @return CookieCollection the cookie collection.
959
     */
960 87
    public function getCookies()
961
    {
962 87
        if ($this->_cookies === null) {
963 87
            $this->_cookies = new CookieCollection();
964
        }
965
966 87
        return $this->_cookies;
967
    }
968
969
    /**
970
     * @return bool whether this response has a valid [[statusCode]].
971
     */
972 56
    public function getIsInvalid()
973
    {
974 56
        return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
975
    }
976
977
    /**
978
     * @return bool whether this response is informational
979
     */
980
    public function getIsInformational()
981
    {
982
        return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200;
983
    }
984
985
    /**
986
     * @return bool whether this response is successful
987
     */
988
    public function getIsSuccessful()
989
    {
990
        return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
991
    }
992
993
    /**
994
     * @return bool whether this response is a redirection
995
     */
996 1
    public function getIsRedirection()
997
    {
998 1
        return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
999
    }
1000
1001
    /**
1002
     * @return bool whether this response indicates a client error
1003
     */
1004
    public function getIsClientError()
1005
    {
1006
        return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
1007
    }
1008
1009
    /**
1010
     * @return bool whether this response indicates a server error
1011
     */
1012
    public function getIsServerError()
1013
    {
1014
        return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
1015
    }
1016
1017
    /**
1018
     * @return bool whether this response is OK
1019
     */
1020
    public function getIsOk()
1021
    {
1022
        return $this->getStatusCode() == 200;
1023
    }
1024
1025
    /**
1026
     * @return bool whether this response indicates the current request is forbidden
1027
     */
1028
    public function getIsForbidden()
1029
    {
1030
        return $this->getStatusCode() == 403;
1031
    }
1032
1033
    /**
1034
     * @return bool whether this response indicates the currently requested resource is not found
1035
     */
1036
    public function getIsNotFound()
1037
    {
1038
        return $this->getStatusCode() == 404;
1039
    }
1040
1041
    /**
1042
     * @return bool whether this response is empty
1043
     */
1044
    public function getIsEmpty()
1045
    {
1046
        return in_array($this->getStatusCode(), [201, 204, 304]);
1047
    }
1048
1049
    /**
1050
     * @return array the formatters that are supported by default
1051
     */
1052 352
    protected function defaultFormatters()
1053
    {
1054
        return [
1055 352
            self::FORMAT_HTML => [
1056 352
                'class' => 'yii\web\HtmlResponseFormatter',
1057
            ],
1058 352
            self::FORMAT_XML => [
1059
                'class' => 'yii\web\XmlResponseFormatter',
1060
            ],
1061 352
            self::FORMAT_JSON => [
1062
                'class' => 'yii\web\JsonResponseFormatter',
1063
            ],
1064 352
            self::FORMAT_JSONP => [
1065
                'class' => 'yii\web\JsonResponseFormatter',
1066
                'useJsonp' => true,
1067
            ],
1068
        ];
1069
    }
1070
1071
    /**
1072
     * Prepares for sending the response.
1073
     * The default implementation will convert [[data]] into [[content]] and set headers accordingly.
1074
     * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported
1075
     *
1076
     * @see https://tools.ietf.org/html/rfc7231#page-53
1077
     * @see https://tools.ietf.org/html/rfc7232#page-18
1078
     */
1079 36
    protected function prepare()
1080
    {
1081 36
        if (in_array($this->getStatusCode(), [204, 304])) {
1082
            // A 204/304 response cannot contain a message body according to rfc7231/rfc7232
1083 6
            $this->content = '';
1084 6
            $this->stream = null;
1085 6
            return;
1086
        }
1087
1088 30
        if ($this->stream !== null) {
1089 4
            return;
1090
        }
1091
1092 26
        if (isset($this->formatters[$this->format])) {
1093 24
            $formatter = $this->formatters[$this->format];
1094 24
            if (!is_object($formatter)) {
1095 24
                $this->formatters[$this->format] = $formatter = Yii::createObject($formatter);
1096
            }
1097 24
            if ($formatter instanceof ResponseFormatterInterface) {
1098 24
                $formatter->format($this);
1099
            } else {
1100 24
                throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface.");
1101
            }
1102 2
        } elseif ($this->format === self::FORMAT_RAW) {
1103 2
            if ($this->data !== null) {
1104 2
                $this->content = $this->data;
1105
            }
1106
        } else {
1107
            throw new InvalidConfigException("Unsupported response format: {$this->format}");
1108
        }
1109
1110 26
        if (is_array($this->content)) {
0 ignored issues
show
introduced by
The condition is_array($this->content) is always false.
Loading history...
1111
            throw new InvalidArgumentException('Response content must not be an array.');
1112 26
        } elseif (is_object($this->content)) {
1113
            if (method_exists($this->content, '__toString')) {
1114
                $this->content = $this->content->__toString();
1115
            } else {
1116
                throw new InvalidArgumentException('Response content must be a string or an object implementing __toString().');
1117
            }
1118
        }
1119 26
    }
1120
1121
    /**
1122
     * Checks if a stream is seekable
1123
     *
1124
     * @param $handle
1125
     * @return bool
1126
     */
1127 11
    private function isSeekable($handle)
1128
    {
1129 11
        if (!is_resource($handle)) {
1130
            return true;
1131
        }
1132
1133 11
        $metaData = stream_get_meta_data($handle);
1134 11
        return isset($metaData['seekable']) && $metaData['seekable'] === true;
1135
    }
1136
}
1137