Passed
Pull Request — master (#20107)
by
unknown
13:53
created

Response::prepare()   B

Complexity

Conditions 11
Paths 21

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 12.0935

Importance

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

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

1182
        $result = preg_match("/.*nonce-(.+?)'/", /** @scrutinizer ignore-type */ $header, $matches);
Loading history...
1183 1
        if ($result === false) {
1184
            return;
1185
        }
1186
1187 1
        $this->_cspNonce = $matches[1];
1188 1
    }
1189
}
1190