Completed
Push — feature/fix-non-seekable-strea... ( 1ea715...fe0f07 )
by Alexander
11:38 queued 11:24
created

Response::sendContent()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 11.3498

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 10
eloc 21
c 4
b 1
f 0
nc 17
nop 0
dl 0
loc 37
ccs 16
cts 21
cp 0.7619
crap 11.3498
rs 7.6666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidConfigException;
13
use yii\helpers\FileHelper;
14
use yii\helpers\Inflector;
15
use yii\helpers\StringHelper;
16
use yii\helpers\Url;
17
18
/**
19
 * The web Response class represents an HTTP response.
20
 *
21
 * It holds the [[headers]], [[cookies]] and [[content]] that is to be sent to the client.
22
 * It also controls the HTTP [[statusCode|status code]].
23
 *
24
 * Response is configured as an application component in [[\yii\web\Application]] by default.
25
 * You can access that instance via `Yii::$app->response`.
26
 *
27
 * You can modify its configuration by adding an array to your application config under `components`
28
 * as it is shown in the following example:
29
 *
30
 * ```php
31
 * 'response' => [
32
 *     'format' => yii\web\Response::FORMAT_JSON,
33
 *     'charset' => 'UTF-8',
34
 *     // ...
35
 * ]
36
 * ```
37
 *
38
 * For more details and usage information on Response, see the [guide article on responses](guide:runtime-responses).
39
 *
40
 * @property CookieCollection $cookies The cookie collection. This property is read-only.
41
 * @property string $downloadHeaders The attachment file name. This property is write-only.
42
 * @property HeaderCollection $headers The header collection. This property is read-only.
43
 * @property bool $isClientError Whether this response indicates a client error. This property is read-only.
44
 * @property bool $isEmpty Whether this response is empty. This property is read-only.
45
 * @property bool $isForbidden Whether this response indicates the current request is forbidden. This property
46
 * is read-only.
47
 * @property bool $isInformational Whether this response is informational. This property is read-only.
48
 * @property bool $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only.
49
 * @property bool $isNotFound Whether this response indicates the currently requested resource is not found.
50
 * This property is read-only.
51
 * @property bool $isOk Whether this response is OK. This property is read-only.
52
 * @property bool $isRedirection Whether this response is a redirection. This property is read-only.
53
 * @property bool $isServerError Whether this response indicates a server error. This property is read-only.
54
 * @property bool $isSuccessful Whether this response is successful. This property is read-only.
55
 * @property int $statusCode The HTTP status code to send with the response.
56
 * @property \Exception|\Error $statusCodeByException The exception object. This property is write-only.
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 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 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. Note that when this property is set, the [[data]] and [[content]]
139
     * properties will be ignored by [[send()]].
140
     */
141
    public $stream;
142
    /**
143
     * @var string the charset of the text response. If not set, it will use
144
     * the value of [[Application::charset]].
145
     */
146
    public $charset;
147
    /**
148
     * @var string the HTTP status description that comes together with the status code.
149
     * @see httpStatuses
150
     */
151
    public $statusText = 'OK';
152
    /**
153
     * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`,
154
     * or '1.1' if that is not available.
155
     */
156
    public $version;
157
    /**
158
     * @var bool whether the response has been sent. If this is true, calling [[send()]] will do nothing.
159
     */
160
    public $isSent = false;
161
    /**
162
     * @var array list of HTTP status codes and the corresponding texts
163
     */
164
    public static $httpStatuses = [
165
        100 => 'Continue',
166
        101 => 'Switching Protocols',
167
        102 => 'Processing',
168
        118 => 'Connection timed out',
169
        200 => 'OK',
170
        201 => 'Created',
171
        202 => 'Accepted',
172
        203 => 'Non-Authoritative',
173
        204 => 'No Content',
174
        205 => 'Reset Content',
175
        206 => 'Partial Content',
176
        207 => 'Multi-Status',
177
        208 => 'Already Reported',
178
        210 => 'Content Different',
179
        226 => 'IM Used',
180
        300 => 'Multiple Choices',
181
        301 => 'Moved Permanently',
182
        302 => 'Found',
183
        303 => 'See Other',
184
        304 => 'Not Modified',
185
        305 => 'Use Proxy',
186
        306 => 'Reserved',
187
        307 => 'Temporary Redirect',
188
        308 => 'Permanent Redirect',
189
        310 => 'Too many Redirect',
190
        400 => 'Bad Request',
191
        401 => 'Unauthorized',
192
        402 => 'Payment Required',
193
        403 => 'Forbidden',
194
        404 => 'Not Found',
195
        405 => 'Method Not Allowed',
196
        406 => 'Not Acceptable',
197
        407 => 'Proxy Authentication Required',
198
        408 => 'Request Time-out',
199
        409 => 'Conflict',
200
        410 => 'Gone',
201
        411 => 'Length Required',
202
        412 => 'Precondition Failed',
203
        413 => 'Request Entity Too Large',
204
        414 => 'Request-URI Too Long',
205
        415 => 'Unsupported Media Type',
206
        416 => 'Requested range unsatisfiable',
207
        417 => 'Expectation failed',
208
        418 => 'I\'m a teapot',
209
        421 => 'Misdirected Request',
210
        422 => 'Unprocessable entity',
211
        423 => 'Locked',
212
        424 => 'Method failure',
213
        425 => 'Unordered Collection',
214
        426 => 'Upgrade Required',
215
        428 => 'Precondition Required',
216
        429 => 'Too Many Requests',
217
        431 => 'Request Header Fields Too Large',
218
        449 => 'Retry With',
219
        450 => 'Blocked by Windows Parental Controls',
220
        451 => 'Unavailable For Legal Reasons',
221
        500 => 'Internal Server Error',
222
        501 => 'Not Implemented',
223
        502 => 'Bad Gateway or Proxy Error',
224
        503 => 'Service Unavailable',
225
        504 => 'Gateway Time-out',
226
        505 => 'HTTP Version not supported',
227
        507 => 'Insufficient storage',
228
        508 => 'Loop Detected',
229
        509 => 'Bandwidth Limit Exceeded',
230
        510 => 'Not Extended',
231
        511 => 'Network Authentication Required',
232
    ];
233
234
    /**
235
     * @var int the HTTP status code to send with the response.
236
     */
237
    private $_statusCode = 200;
238
    /**
239
     * @var HeaderCollection
240
     */
241
    private $_headers;
242
243
244
    /**
245
     * Initializes this component.
246
     */
247 323
    public function init()
248
    {
249 323
        if ($this->version === null) {
250 323
            if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
251
                $this->version = '1.0';
252
            } else {
253 323
                $this->version = '1.1';
254
            }
255
        }
256 323
        if ($this->charset === null) {
257 323
            $this->charset = Yii::$app->charset;
258
        }
259 323
        $this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
260 323
    }
261
262
    /**
263
     * @return int the HTTP status code to send with the response.
264
     */
265 59
    public function getStatusCode()
266
    {
267 59
        return $this->_statusCode;
268
    }
269
270
    /**
271
     * Sets the response status code.
272
     * This method will set the corresponding status text if `$text` is null.
273
     * @param int $value the status code
274
     * @param string $text the status text. If not set, it will be set automatically based on the status code.
275
     * @throws InvalidArgumentException if the status code is invalid.
276
     * @return $this the response object itself
277
     */
278 52
    public function setStatusCode($value, $text = null)
279
    {
280 52
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
281
            $value = 200;
282
        }
283 52
        $this->_statusCode = (int) $value;
284 52
        if ($this->getIsInvalid()) {
285
            throw new InvalidArgumentException("The HTTP status code is invalid: $value");
286
        }
287 52
        if ($text === null) {
288 48
            $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : '';
289
        } else {
290 4
            $this->statusText = $text;
291
        }
292
293 52
        return $this;
294
    }
295
296
    /**
297
     * Sets the response status code based on the exception.
298
     * @param \Exception|\Error|\Throwable $e the exception object.
299
     * @throws InvalidArgumentException if the status code is invalid.
300
     * @return $this the response object itself
301
     * @since 2.0.12
302
     */
303 18
    public function setStatusCodeByException($e)
304
    {
305 18
        if ($e instanceof HttpException) {
306 10
            $this->setStatusCode($e->statusCode);
307
        } else {
308 8
            $this->setStatusCode(500);
309
        }
310
311 18
        return $this;
312
    }
313
314
    /**
315
     * Returns the header collection.
316
     * The header collection contains the currently registered HTTP headers.
317
     * @return HeaderCollection the header collection
318
     */
319 118
    public function getHeaders()
320
    {
321 118
        if ($this->_headers === null) {
322 118
            $this->_headers = new HeaderCollection();
323
        }
324
325 118
        return $this->_headers;
326
    }
327
328
    /**
329
     * Sends the response to the client.
330
     */
331 32
    public function send()
332
    {
333 32
        if ($this->isSent) {
334 3
            return;
335
        }
336 32
        $this->trigger(self::EVENT_BEFORE_SEND);
337 32
        $this->prepare();
338 32
        $this->trigger(self::EVENT_AFTER_PREPARE);
339 32
        $this->sendHeaders();
340 32
        $this->sendContent();
341 32
        $this->trigger(self::EVENT_AFTER_SEND);
342 32
        $this->isSent = true;
343 32
    }
344
345
    /**
346
     * Clears the headers, cookies, content, status code of the response.
347
     */
348
    public function clear()
349
    {
350
        $this->_headers = null;
351
        $this->_cookies = null;
352
        $this->_statusCode = 200;
353
        $this->statusText = 'OK';
354
        $this->data = null;
355
        $this->stream = null;
356
        $this->content = null;
357
        $this->isSent = false;
358
    }
359
360
    /**
361
     * Sends the response headers to the client.
362
     */
363 32
    protected function sendHeaders()
364
    {
365 32
        if (headers_sent($file, $line)) {
366
            throw new HeadersAlreadySentException($file, $line);
367
        }
368 32
        if ($this->_headers) {
369 29
            foreach ($this->getHeaders() as $name => $values) {
370 29
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
371
                // set replace for first occurrence of header but false afterwards to allow multiple
372 29
                $replace = true;
373 29
                foreach ($values as $value) {
374 29
                    header("$name: $value", $replace);
375 29
                    $replace = false;
376
                }
377
            }
378
        }
379 32
        $statusCode = $this->getStatusCode();
380 32
        header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
381 32
        $this->sendCookies();
382 32
    }
383
384
    /**
385
     * Sends the cookies to the client.
386
     */
387 32
    protected function sendCookies()
388
    {
389 32
        if ($this->_cookies === null) {
390 27
            return;
391
        }
392 5
        $request = Yii::$app->getRequest();
393 5
        if ($request->enableCookieValidation) {
0 ignored issues
show
Bug Best Practice introduced by
The property enableCookieValidation does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
394 5
            if ($request->cookieValidationKey == '') {
0 ignored issues
show
Bug Best Practice introduced by
The property cookieValidationKey does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
395
                throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
396
            }
397 5
            $validationKey = $request->cookieValidationKey;
398
        }
399 5
        foreach ($this->getCookies() as $cookie) {
400 5
            $value = $cookie->value;
401 5
            if ($cookie->expire != 1 && isset($validationKey)) {
402 5
                $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
403
            }
404 5
            if (PHP_VERSION_ID >= 70300) {
405
                setcookie($cookie->name, $value, [
0 ignored issues
show
Bug introduced by
array('expires' => $cook...ookie->sameSite : null) of type array<string,mixed|null> is incompatible with the type integer expected by parameter $expire of setcookie(). ( Ignorable by Annotation )

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

405
                setcookie($cookie->name, $value, /** @scrutinizer ignore-type */ [
Loading history...
406
                    'expires' => $cookie->expire,
407
                    'path' => $cookie->path,
408
                    'domain' => $cookie->domain,
409
                    'secure' => $cookie->secure,
410
                    'httpOnly' => $cookie->httpOnly,
411
                    'sameSite' => !empty($cookie->sameSite) ? $cookie->sameSite : null,
412
                ]);
413
            } else {
414
                // Work around for setting sameSite cookie prior PHP 7.3
415
                // https://stackoverflow.com/questions/39750906/php-setcookie-samesite-strict/46971326#46971326
416 5
                if (!is_null($cookie->sameSite)) {
417 1
                    $cookie->path .= '; samesite=' . $cookie->sameSite;
418
                }
419 5
                setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
420
            }
421
        }
422 5
    }
423
424
    /**
425
     * Sends the response content to the client.
426
     */
427 32
    protected function sendContent()
428
    {
429 32
        if ($this->stream === null) {
430 28
            echo $this->content;
431
432 28
            return;
433
        }
434
435
        // Try to reset time limit for big files
436 4
        if (!function_exists('set_time_limit') || !@set_time_limit(0)) {
437
            Yii::warning('set_time_limit() is not available', __METHOD__);
438
        }
439
440 4
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
441
442 4
        if (is_array($this->stream)) {
443 4
            list($handle, $begin, $end) = $this->stream;
444
445
            // only seek if stream is seekable
446 4
            if ($this->isSeekable($handle)) {
447 3
                fseek($handle, $begin);
448
            }
449
450 4
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
451 3
                if ($pos + $chunkSize > $end) {
452 3
                    $chunkSize = $end - $pos + 1;
453
                }
454 3
                echo fread($handle, $chunkSize);
455 3
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
456
            }
457 4
            fclose($handle);
458
        } else {
459
            while (!feof($this->stream)) {
460
                echo fread($this->stream, $chunkSize);
461
                flush();
462
            }
463
            fclose($this->stream);
464
        }
465 4
    }
466
467
    /**
468
     * Sends a file to the browser.
469
     *
470
     * Note that this method only prepares the response for file sending. The file is not sent
471
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
472
     *
473
     * The following is an example implementation of a controller action that allows requesting files from a directory
474
     * that is not accessible from web:
475
     *
476
     * ```php
477
     * public function actionFile($filename)
478
     * {
479
     *     $storagePath = Yii::getAlias('@app/files');
480
     *
481
     *     // check filename for allowed chars (do not allow ../ to avoid security issue: downloading arbitrary files)
482
     *     if (!preg_match('/^[a-z0-9]+\.[a-z0-9]+$/i', $filename) || !is_file("$storagePath/$filename")) {
483
     *         throw new \yii\web\NotFoundHttpException('The file does not exists.');
484
     *     }
485
     *     return Yii::$app->response->sendFile("$storagePath/$filename", $filename);
486
     * }
487
     * ```
488
     *
489
     * @param string $filePath the path of the file to be sent.
490
     * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
491
     * @param array $options additional options for sending the file. The following options are supported:
492
     *
493
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
494
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
495
     *    meaning a download dialog will pop up.
496
     *
497
     * @return $this the response object itself
498
     * @see sendContentAsFile()
499
     * @see sendStreamAsFile()
500
     * @see xSendFile()
501
     */
502 9
    public function sendFile($filePath, $attachmentName = null, $options = [])
503
    {
504 9
        if (!isset($options['mimeType'])) {
505 9
            $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);
506
        }
507 9
        if ($attachmentName === null) {
508 8
            $attachmentName = basename($filePath);
509
        }
510 9
        $handle = fopen($filePath, 'rb');
511 9
        $this->sendStreamAsFile($handle, $attachmentName, $options);
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of yii\web\Response::sendStreamAsFile() 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

511
        $this->sendStreamAsFile(/** @scrutinizer ignore-type */ $handle, $attachmentName, $options);
Loading history...
512
513 5
        return $this;
514
    }
515
516
    /**
517
     * Sends the specified content as a file to the browser.
518
     *
519
     * Note that this method only prepares the response for file sending. The file is not sent
520
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
521
     *
522
     * @param string $content the content to be sent. The existing [[content]] will be discarded.
523
     * @param string $attachmentName the file name shown to the user.
524
     * @param array $options additional options for sending the file. The following options are supported:
525
     *
526
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
527
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
528
     *    meaning a download dialog will pop up.
529
     *
530
     * @return $this the response object itself
531
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
532
     * @see sendFile() for an example implementation.
533
     */
534 1
    public function sendContentAsFile($content, $attachmentName, $options = [])
535
    {
536 1
        $headers = $this->getHeaders();
537
538 1
        $contentLength = StringHelper::byteLength($content);
539 1
        $range = $this->getHttpRange($contentLength);
540
541 1
        if ($range === false) {
0 ignored issues
show
introduced by
The condition $range === false is always false.
Loading history...
542
            $headers->set('Content-Range', "bytes */$contentLength");
543
            throw new RangeNotSatisfiableHttpException();
544
        }
545
546 1
        list($begin, $end) = $range;
547 1
        if ($begin != 0 || $end != $contentLength - 1) {
548
            $this->setStatusCode(206);
549
            $headers->set('Content-Range', "bytes $begin-$end/$contentLength");
550
            $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1);
551
        } else {
552 1
            $this->setStatusCode(200);
553 1
            $this->content = $content;
554
        }
555
556 1
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
557 1
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
558
559 1
        $this->format = self::FORMAT_RAW;
560
561 1
        return $this;
562
    }
563
564
    /**
565
     * Sends the specified stream as a file to the browser.
566
     *
567
     * Note that this method only prepares the response for file sending. The file is not sent
568
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
569
     *
570
     * @param resource $handle the handle of the stream to be sent.
571
     * @param string $attachmentName the file name shown to the user.
572
     * @param array $options additional options for sending the file. The following options are supported:
573
     *
574
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
575
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
576
     *    meaning a download dialog will pop up.
577
     *  - `fileSize`: the size of the content to stream this is useful when size of the content is known
578
     *    and the content is not seekable. Defaults to content size using `ftell()`.
579
     *    This option is available since version 2.0.4.
580
     *
581
     * @return $this the response object itself
582
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
583
     * @see sendFile() for an example implementation.
584
     */
585 10
    public function sendStreamAsFile($handle, $attachmentName, $options = [])
586
    {
587 10
        $headers = $this->getHeaders();
588 10
        if (isset($options['fileSize'])) {
589
            $fileSize = $options['fileSize'];
590
        } else {
591 10
            if ($this->isSeekable($handle)) {
592 9
                fseek($handle, 0, SEEK_END);
593 9
                $fileSize = ftell($handle);
594
            } else {
595 1
                $fileSize = 0;
596
            }
597
        }
598
599 10
        $range = $this->getHttpRange($fileSize);
600 10
        if ($range === false) {
0 ignored issues
show
introduced by
The condition $range === false is always false.
Loading history...
601 4
            $headers->set('Content-Range', "bytes */$fileSize");
602 4
            throw new RangeNotSatisfiableHttpException();
603
        }
604
605 6
        list($begin, $end) = $range;
606 6
        if ($begin != 0 || $end != $fileSize - 1) {
607 3
            $this->setStatusCode(206);
608 3
            $headers->set('Content-Range', "bytes $begin-$end/$fileSize");
609
        } else {
610 3
            $this->setStatusCode(200);
611
        }
612
613 6
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
614 6
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
615
616 6
        $this->format = self::FORMAT_RAW;
617 6
        $this->stream = [$handle, $begin, $end];
618
619 6
        return $this;
620
    }
621
622
    /**
623
     * Sets a default set of HTTP headers for file downloading purpose.
624
     * @param string $attachmentName the attachment file name
625
     * @param string $mimeType the MIME type for the response. If null, `Content-Type` header will NOT be set.
626
     * @param bool $inline whether the browser should open the file within the browser window. Defaults to false,
627
     * meaning a download dialog will pop up.
628
     * @param int $contentLength the byte length of the file being downloaded. If null, `Content-Length` header will NOT be set.
629
     * @return $this the response object itself
630
     */
631 7
    public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null)
632
    {
633 7
        $headers = $this->getHeaders();
634
635 7
        $disposition = $inline ? 'inline' : 'attachment';
636 7
        $headers->setDefault('Pragma', 'public')
637 7
            ->setDefault('Accept-Ranges', 'bytes')
638 7
            ->setDefault('Expires', '0')
639 7
            ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
640 7
            ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName));
641
642 7
        if ($mimeType !== null) {
643 7
            $headers->setDefault('Content-Type', $mimeType);
644
        }
645
646 7
        if ($contentLength !== null) {
647 7
            $headers->setDefault('Content-Length', $contentLength);
648
        }
649
650 7
        return $this;
651
    }
652
653
    /**
654
     * Determines the HTTP range given in the request.
655
     * @param int $fileSize the size of the file that will be used to validate the requested HTTP range.
656
     * @return array|bool the range (begin, end), or false if the range request is invalid.
657
     */
658 11
    protected function getHttpRange($fileSize)
659
    {
660 11
        $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

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

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

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

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

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

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

882
            if ($request->/** @scrutinizer ignore-call */ getIsAjax()) {
Loading history...
883 7
                if (in_array($statusCode, [301, 302]) && preg_match('/Trident\/|MSIE[ ]/', $request->userAgent)) {
0 ignored issues
show
Bug Best Practice introduced by
The property userAgent does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
884 3
                    $statusCode = 200;
885
                }
886 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

886
                if ($request->/** @scrutinizer ignore-call */ getIsPjax()) {
Loading history...
887 6
                    $this->getHeaders()->set('X-Pjax-Url', $url);
888
                } else {
889 7
                    $this->getHeaders()->set('X-Redirect', $url);
890
                }
891
            } else {
892 9
                $this->getHeaders()->set('Location', $url);
893
            }
894
        } else {
895
            $this->getHeaders()->set('Location', $url);
896
        }
897
898 9
        $this->setStatusCode($statusCode);
899
900 9
        return $this;
901
    }
902
903
    /**
904
     * Refreshes the current page.
905
     * The effect of this method call is the same as the user pressing the refresh button of his browser
906
     * (without re-posting data).
907
     *
908
     * In a controller action you may use this method like this:
909
     *
910
     * ```php
911
     * return Yii::$app->getResponse()->refresh();
912
     * ```
913
     *
914
     * @param string $anchor the anchor that should be appended to the redirection URL.
915
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
916
     * @return Response the response object itself
917
     */
918
    public function refresh($anchor = '')
919
    {
920
        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

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