Passed
Push — master ( 52cf02...273cba )
by Alexander
18:21 queued 14:33
created

Response::sendContent()   C

Complexity

Conditions 12
Paths 21

Size

Total Lines 46
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 19.3176

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 12
eloc 27
c 3
b 0
f 0
nc 21
nop 0
dl 0
loc 46
ccs 17
cts 27
cp 0.6296
crap 19.3176
rs 6.9666

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

449
            $data = call_user_func(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
450
            foreach ($data as $datum) {
451
                echo $datum;
452
                flush();
453
            }
454
            return;
455
        }
456
457 4
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
458
459 4
        if (is_array($this->stream)) {
460 4
            list($handle, $begin, $end) = $this->stream;
461
462
            // only seek if stream is seekable
463 4
            if ($this->isSeekable($handle)) {
464 3
                fseek($handle, $begin);
465
            }
466
467 4
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
468 3
                if ($pos + $chunkSize > $end) {
469 3
                    $chunkSize = $end - $pos + 1;
470
                }
471 3
                echo fread($handle, $chunkSize);
472 3
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
473
            }
474 4
            fclose($handle);
475
        } else {
476
            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

476
            while (!feof(/** @scrutinizer ignore-type */ $this->stream)) {
Loading history...
477
                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

477
                echo fread(/** @scrutinizer ignore-type */ $this->stream, $chunkSize);
Loading history...
478
                flush();
479
            }
480
            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

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

677
        $rangeHeader = Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getHeaders()->get('Range', '-');
Loading history...
678 12
        if ($rangeHeader === '-') {
679 5
            return [0, $fileSize - 1];
680
        }
681 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; 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

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

895
            $url = $request->/** @scrutinizer ignore-call */ getHostInfo() . $url;
Loading history...
896
        }
897
898 9
        if ($checkAjax) {
899 9
            if ($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

899
            if ($request->/** @scrutinizer ignore-call */ getIsAjax()) {
Loading history...
900 7
                if (in_array($statusCode, [301, 302]) && preg_match('/Trident\/|MSIE[ ]/', $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...
Bug introduced by
It seems like $request->userAgent can also be of type 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

900
                if (in_array($statusCode, [301, 302]) && preg_match('/Trident\/|MSIE[ ]/', /** @scrutinizer ignore-type */ $request->userAgent)) {
Loading history...
901 3
                    $statusCode = 200;
902
                }
903 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

903
                if ($request->/** @scrutinizer ignore-call */ getIsPjax()) {
Loading history...
904 6
                    $this->getHeaders()->set('X-Pjax-Url', $url);
905
                } else {
906 7
                    $this->getHeaders()->set('X-Redirect', $url);
907
                }
908
            } else {
909 9
                $this->getHeaders()->set('Location', $url);
910
            }
911
        } else {
912
            $this->getHeaders()->set('Location', $url);
913
        }
914
915 9
        $this->setStatusCode($statusCode);
916
917 9
        return $this;
918
    }
919
920
    /**
921
     * Refreshes the current page.
922
     * The effect of this method call is the same as the user pressing the refresh button of his browser
923
     * (without re-posting data).
924
     *
925
     * In a controller action you may use this method like this:
926
     *
927
     * ```php
928
     * return Yii::$app->getResponse()->refresh();
929
     * ```
930
     *
931
     * @param string $anchor the anchor that should be appended to the redirection URL.
932
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
933
     * @return Response the response object itself
934
     */
935
    public function refresh($anchor = '')
936
    {
937
        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

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