Response::prepare()   B
last analyzed

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

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

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

482
                echo fread(/** @scrutinizer ignore-type */ $this->stream, $chunkSize);
Loading history...
483
                flush();
484
            }
485
            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

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

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

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

904
                $normalizedUrl = $request->/** @scrutinizer ignore-call */ getHostInfo() . $normalizedUrl;
Loading history...
905
            }
906
        }
907
908 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

908
        if ($checkAjax && $request->/** @scrutinizer ignore-call */ getIsAjax()) {
Loading history...
909
            if (
910 7
                in_array($statusCode, [301, 302])
911 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...
912
            ) {
913 3
                $statusCode = 200;
914
            }
915 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

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

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