Completed
Push — bash-completion ( 164352...1454d3 )
by Carsten
86:55 queued 83:59
created

Response::getIsServerError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
cc 2
eloc 2
nc 2
nop 0
crap 6
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\InvalidConfigException;
12
use yii\base\InvalidParamException;
13
use yii\helpers\Inflector;
14
use yii\helpers\Url;
15
use yii\helpers\FileHelper;
16
use yii\helpers\StringHelper;
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
44
 * read-only.
45
 * @property bool $isEmpty Whether this response is empty. This property is read-only.
46
 * @property bool $isForbidden Whether this response indicates the current request is forbidden. This
47
 * property is read-only.
48
 * @property bool $isInformational Whether this response is informational. This property is read-only.
49
 * @property bool $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only.
50
 * @property bool $isNotFound Whether this response indicates the currently requested resource is not
51
 * found. This property is read-only.
52
 * @property bool $isOk Whether this response is OK. This property is read-only.
53
 * @property bool $isRedirection Whether this response is a redirection. This property is read-only.
54
 * @property bool $isServerError Whether this response indicates a server error. This property is
55
 * read-only.
56
 * @property bool $isSuccessful Whether this response is successful. This property is read-only.
57
 * @property int $statusCode The HTTP status code to send with the response.
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 ResponseEvent an event that is triggered at the beginning of [[send()]].
67
     */
68
    const EVENT_BEFORE_SEND = 'beforeSend';
69
    /**
70
     * @event ResponseEvent an event that is triggered at the end of [[send()]].
71
     */
72
    const EVENT_AFTER_SEND = 'afterSend';
73
    /**
74
     * @event ResponseEvent 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 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 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. Note that when this property is set, the [[data]] and [[content]]
140
     * properties will be ignored by [[send()]].
141
     */
142
    public $stream;
143
    /**
144
     * @var string the charset of the text response. If not set, it will use
145
     * the value of [[Application::charset]].
146
     */
147
    public $charset;
148
    /**
149
     * @var string the HTTP status description that comes together with the status code.
150
     * @see httpStatuses
151
     */
152
    public $statusText = 'OK';
153
    /**
154
     * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`,
155
     * or '1.1' if that is not available.
156
     */
157
    public $version;
158
    /**
159
     * @var bool whether the response has been sent. If this is true, calling [[send()]] will do nothing.
160
     */
161
    public $isSent = false;
162
    /**
163
     * @var array list of HTTP status codes and the corresponding texts
164
     */
165
    public static $httpStatuses = [
166
        100 => 'Continue',
167
        101 => 'Switching Protocols',
168
        102 => 'Processing',
169
        118 => 'Connection timed out',
170
        200 => 'OK',
171
        201 => 'Created',
172
        202 => 'Accepted',
173
        203 => 'Non-Authoritative',
174
        204 => 'No Content',
175
        205 => 'Reset Content',
176
        206 => 'Partial Content',
177
        207 => 'Multi-Status',
178
        208 => 'Already Reported',
179
        210 => 'Content Different',
180
        226 => 'IM Used',
181
        300 => 'Multiple Choices',
182
        301 => 'Moved Permanently',
183
        302 => 'Found',
184
        303 => 'See Other',
185
        304 => 'Not Modified',
186
        305 => 'Use Proxy',
187
        306 => 'Reserved',
188
        307 => 'Temporary Redirect',
189
        308 => 'Permanent Redirect',
190
        310 => 'Too many Redirect',
191
        400 => 'Bad Request',
192
        401 => 'Unauthorized',
193
        402 => 'Payment Required',
194
        403 => 'Forbidden',
195
        404 => 'Not Found',
196
        405 => 'Method Not Allowed',
197
        406 => 'Not Acceptable',
198
        407 => 'Proxy Authentication Required',
199
        408 => 'Request Time-out',
200
        409 => 'Conflict',
201
        410 => 'Gone',
202
        411 => 'Length Required',
203
        412 => 'Precondition Failed',
204
        413 => 'Request Entity Too Large',
205
        414 => 'Request-URI Too Long',
206
        415 => 'Unsupported Media Type',
207
        416 => 'Requested range unsatisfiable',
208
        417 => 'Expectation failed',
209
        418 => 'I\'m a teapot',
210
        421 => 'Misdirected Request',
211
        422 => 'Unprocessable entity',
212
        423 => 'Locked',
213
        424 => 'Method failure',
214
        425 => 'Unordered Collection',
215
        426 => 'Upgrade Required',
216
        428 => 'Precondition Required',
217
        429 => 'Too Many Requests',
218
        431 => 'Request Header Fields Too Large',
219
        449 => 'Retry With',
220
        450 => 'Blocked by Windows Parental Controls',
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 113
    public function init()
248
    {
249 113
        if ($this->version === null) {
250 113
            if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
251
                $this->version = '1.0';
252
            } else {
253 113
                $this->version = '1.1';
254
            }
255 113
        }
256 113
        if ($this->charset === null) {
257 113
            $this->charset = Yii::$app->charset;
258 113
        }
259 113
        $this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
260 113
    }
261
262
    /**
263
     * @return int the HTTP status code to send with the response.
264
     */
265 9
    public function getStatusCode()
266
    {
267 9
        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 InvalidParamException if the status code is invalid.
276
     */
277 9
    public function setStatusCode($value, $text = null)
278
    {
279 9
        if ($value === null) {
280
            $value = 200;
281
        }
282 9
        $this->_statusCode = (int) $value;
283 9
        if ($this->getIsInvalid()) {
284
            throw new InvalidParamException("The HTTP status code is invalid: $value");
285
        }
286 9
        if ($text === null) {
287 5
            $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : '';
288 5
        } else {
289 4
            $this->statusText = $text;
290
        }
291 9
    }
292
293
    /**
294
     * Returns the header collection.
295
     * The header collection contains the currently registered HTTP headers.
296
     * @return HeaderCollection the header collection
297
     */
298 56
    public function getHeaders()
299
    {
300 56
        if ($this->_headers === null) {
301 56
            $this->_headers = new HeaderCollection;
302 56
        }
303 56
        return $this->_headers;
304
    }
305
306
    /**
307
     * Sends the response to the client.
308
     */
309 7
    public function send()
310
    {
311 7
        if ($this->isSent) {
312 3
            return;
313
        }
314 7
        $this->trigger(self::EVENT_BEFORE_SEND);
315 7
        $this->prepare();
316 7
        $this->trigger(self::EVENT_AFTER_PREPARE);
317 7
        $this->sendHeaders();
318 7
        $this->sendContent();
319 7
        $this->trigger(self::EVENT_AFTER_SEND);
320 7
        $this->isSent = true;
321 7
    }
322
323
    /**
324
     * Clears the headers, cookies, content, status code of the response.
325
     */
326
    public function clear()
327
    {
328
        $this->_headers = null;
329
        $this->_cookies = null;
330
        $this->_statusCode = 200;
331
        $this->statusText = 'OK';
332
        $this->data = null;
333
        $this->stream = null;
334
        $this->content = null;
335
        $this->isSent = false;
336
    }
337
338
    /**
339
     * Sends the response headers to the client
340
     */
341 7
    protected function sendHeaders()
342
    {
343 7
        if (headers_sent()) {
344 7
            return;
345
        }
346
        if ($this->_headers) {
347
            $headers = $this->getHeaders();
348
            foreach ($headers as $name => $values) {
349
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
350
                // set replace for first occurrence of header but false afterwards to allow multiple
351
                $replace = true;
352
                foreach ($values as $value) {
353
                    header("$name: $value", $replace);
354
                    $replace = false;
355
                }
356
            }
357
        }
358
        $statusCode = $this->getStatusCode();
359
        header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
360
        $this->sendCookies();
361
    }
362
363
    /**
364
     * Sends the cookies to the client.
365
     */
366
    protected function sendCookies()
367
    {
368
        if ($this->_cookies === null) {
369
            return;
370
        }
371
        $request = Yii::$app->getRequest();
372
        if ($request->enableCookieValidation) {
373
            if ($request->cookieValidationKey == '') {
374
                throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
375
            }
376
            $validationKey = $request->cookieValidationKey;
377
        }
378
        foreach ($this->getCookies() as $cookie) {
0 ignored issues
show
Bug introduced by
The expression $this->getCookies() of type object<yii\web\CookieCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
379
            $value = $cookie->value;
380
            if ($cookie->expire != 1  && isset($validationKey)) {
381
                $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
382
            }
383
            setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
384
        }
385
    }
386
387
    /**
388
     * Sends the response content to the client
389
     */
390 7
    protected function sendContent()
391
    {
392 7
        if ($this->stream === null) {
393 4
            echo $this->content;
394
395 4
            return;
396
        }
397
398 3
        set_time_limit(0); // Reset time limit for big files
399 3
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
400
401 3
        if (is_array($this->stream)) {
402 3
            list ($handle, $begin, $end) = $this->stream;
403 3
            fseek($handle, $begin);
404 3
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
405 3
                if ($pos + $chunkSize > $end) {
406 3
                    $chunkSize = $end - $pos + 1;
407 3
                }
408 3
                echo fread($handle, $chunkSize);
409 3
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
410 3
            }
411 3
            fclose($handle);
412 3
        } else {
413
            while (!feof($this->stream)) {
414
                echo fread($this->stream, $chunkSize);
415
                flush();
416
            }
417
            fclose($this->stream);
418
        }
419 3
    }
420
421
    /**
422
     * Sends a file to the browser.
423
     *
424
     * Note that this method only prepares the response for file sending. The file is not sent
425
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
426
     *
427
     * The following is an example implementation of a controller action that allows requesting files from a directory
428
     * that is not accessible from web:
429
     *
430
     * ```php
431
     * public function actionFile($filename)
432
     * {
433
     *     $storagePath = Yii::getAlias('@app/files');
434
     *
435
     *     // check filename for allowed chars (do not allow ../ to avoid security issue: downloading arbitrary files)
436
     *     if (!preg_match('/^[a-z0-9]+\.[a-z0-9]+$/i', $filename) || !is_file("$storagePath/$filename")) {
437
     *         throw new \yii\web\NotFoundHttpException('The file does not exists.');
438
     *     }
439
     *     return Yii::$app->response->sendFile("$storagePath/$filename", $filename);
440
     * }
441
     * ```
442
     *
443
     * @param string $filePath the path of the file to be sent.
444
     * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
445
     * @param array $options additional options for sending the file. The following options are supported:
446
     *
447
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
448
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
449
     *    meaning a download dialog will pop up.
450
     *
451
     * @return $this the response object itself
452
     * @see sendContentAsFile()
453
     * @see sendStreamAsFile()
454
     * @see xSendFile()
455
     */
456 7
    public function sendFile($filePath, $attachmentName = null, $options = [])
457
    {
458 7
        if (!isset($options['mimeType'])) {
459 7
            $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);
460 7
        }
461 7
        if ($attachmentName === null) {
462 7
            $attachmentName = basename($filePath);
463 7
        }
464 7
        $handle = fopen($filePath, 'rb');
465 7
        $this->sendStreamAsFile($handle, $attachmentName, $options);
466
467 3
        return $this;
468
    }
469
470
    /**
471
     * Sends the specified content as a file to the browser.
472
     *
473
     * Note that this method only prepares the response for file sending. The file is not sent
474
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
475
     *
476
     * @param string $content the content to be sent. The existing [[content]] will be discarded.
477
     * @param string $attachmentName the file name shown to the user.
478
     * @param array $options additional options for sending the file. The following options are supported:
479
     *
480
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
481
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
482
     *    meaning a download dialog will pop up.
483
     *
484
     * @return $this the response object itself
485
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
486
     * @see sendFile() for an example implementation.
487
     */
488 1
    public function sendContentAsFile($content, $attachmentName, $options = [])
489
    {
490 1
        $headers = $this->getHeaders();
491
492 1
        $contentLength = StringHelper::byteLength($content);
493 1
        $range = $this->getHttpRange($contentLength);
494
495 1
        if ($range === false) {
496
            $headers->set('Content-Range', "bytes */$contentLength");
497
            throw new RangeNotSatisfiableHttpException();
498
        }
499
500 1
        list($begin, $end) = $range;
501 1
        if ($begin != 0 || $end != $contentLength - 1) {
502
            $this->setStatusCode(206);
503
            $headers->set('Content-Range', "bytes $begin-$end/$contentLength");
504
            $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1);
505
        } else {
506 1
            $this->setStatusCode(200);
507 1
            $this->content = $content;
508
        }
509
510 1
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
511 1
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
512
513 1
        $this->format = self::FORMAT_RAW;
514
515 1
        return $this;
516
    }
517
518
    /**
519
     * Sends the specified stream as a file to the browser.
520
     *
521
     * Note that this method only prepares the response for file sending. The file is not sent
522
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
523
     *
524
     * @param resource $handle the handle of the stream to be sent.
525
     * @param string $attachmentName the file name shown to the user.
526
     * @param array $options additional options for sending the file. The following options are supported:
527
     *
528
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
529
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
530
     *    meaning a download dialog will pop up.
531
     *  - `fileSize`: the size of the content to stream this is useful when size of the content is known
532
     *    and the content is not seekable. Defaults to content size using `ftell()`.
533
     *    This option is available since version 2.0.4.
534
     *
535
     * @return $this the response object itself
536
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
537
     * @see sendFile() for an example implementation.
538
     */
539 7
    public function sendStreamAsFile($handle, $attachmentName, $options = [])
540
    {
541 7
        $headers = $this->getHeaders();
542 7
        if (isset($options['fileSize'])) {
543
            $fileSize = $options['fileSize'];
544
        } else {
545 7
            fseek($handle, 0, SEEK_END);
546 7
            $fileSize = ftell($handle);
547
        }
548
549 7
        $range = $this->getHttpRange($fileSize);
550 7
        if ($range === false) {
551 4
            $headers->set('Content-Range', "bytes */$fileSize");
552 4
            throw new RangeNotSatisfiableHttpException();
553
        }
554
555 3
        list($begin, $end) = $range;
556 3
        if ($begin != 0 || $end != $fileSize - 1) {
557 3
            $this->setStatusCode(206);
558 3
            $headers->set('Content-Range', "bytes $begin-$end/$fileSize");
559 3
        } else {
560
            $this->setStatusCode(200);
561
        }
562
563 3
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
564 3
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
565
566 3
        $this->format = self::FORMAT_RAW;
567 3
        $this->stream = [$handle, $begin, $end];
568
569 3
        return $this;
570
    }
571
572
    /**
573
     * Sets a default set of HTTP headers for file downloading purpose.
574
     * @param string $attachmentName the attachment file name
575
     * @param string $mimeType the MIME type for the response. If null, `Content-Type` header will NOT be set.
576
     * @param bool $inline whether the browser should open the file within the browser window. Defaults to false,
577
     * meaning a download dialog will pop up.
578
     * @param int $contentLength the byte length of the file being downloaded. If null, `Content-Length` header will NOT be set.
579
     * @return $this the response object itself
580
     */
581 4
    public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null)
582
    {
583 4
        $headers = $this->getHeaders();
584
585 4
        $disposition = $inline ? 'inline' : 'attachment';
586 4
        $headers->setDefault('Pragma', 'public')
587 4
            ->setDefault('Accept-Ranges', 'bytes')
588 4
            ->setDefault('Expires', '0')
589 4
            ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
590 4
            ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName));
591
592 4
        if ($mimeType !== null) {
593 4
            $headers->setDefault('Content-Type', $mimeType);
594 4
        }
595
596 4
        if ($contentLength !== null) {
597 4
            $headers->setDefault('Content-Length', $contentLength);
598 4
        }
599
600 4
        return $this;
601
    }
602
603
    /**
604
     * Determines the HTTP range given in the request.
605
     * @param int $fileSize the size of the file that will be used to validate the requested HTTP range.
606
     * @return array|bool the range (begin, end), or false if the range request is invalid.
607
     */
608 8
    protected function getHttpRange($fileSize)
609
    {
610 8
        if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') {
611 1
            return [0, $fileSize - 1];
612
        }
613 7
        if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) {
614 1
            return false;
615
        }
616 6
        if ($matches[1] === '') {
617 2
            $start = $fileSize - $matches[2];
618 2
            $end = $fileSize - 1;
619 6
        } elseif ($matches[2] !== '') {
620 2
            $start = $matches[1];
621 2
            $end = $matches[2];
622 2
            if ($end >= $fileSize) {
623
                $end = $fileSize - 1;
624
            }
625 2
        } else {
626 2
            $start = $matches[1];
627 2
            $end = $fileSize - 1;
628
        }
629 6
        if ($start < 0 || $start > $end) {
630 3
            return false;
631
        } else {
632 3
            return [$start, $end];
633
        }
634
    }
635
636
    /**
637
     * Sends existing file to a browser as a download using x-sendfile.
638
     *
639
     * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
640
     * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
641
     * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
642
     * increase in performance as the web application is allowed to terminate earlier while the webserver is
643
     * handling the request.
644
     *
645
     * The request is sent to the server through a special non-standard HTTP-header.
646
     * When the web server encounters the presence of such header it will discard all output and send the file
647
     * specified by that header using web server internals including all optimizations like caching-headers.
648
     *
649
     * As this header directive is non-standard different directives exists for different web servers applications:
650
     *
651
     * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
652
     * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
653
     * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
654
     * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
655
     * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
656
     *
657
     * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
658
     * a proper xHeader should be sent.
659
     *
660
     * **Note**
661
     *
662
     * This option allows to download files that are not under web folders, and even files that are otherwise protected
663
     * (deny from all) like `.htaccess`.
664
     *
665
     * **Side effects**
666
     *
667
     * If this option is disabled by the web server, when this method is called a download configuration dialog
668
     * will open but the downloaded file will have 0 bytes.
669
     *
670
     * **Known issues**
671
     *
672
     * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
673
     * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
674
     * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
675
     *
676
     * **Example**
677
     *
678
     * ```php
679
     * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg');
680
     * ```
681
     *
682
     * @param string $filePath file name with full path
683
     * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
684
     * @param array $options additional options for sending the file. The following options are supported:
685
     *
686
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
687
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
688
     *    meaning a download dialog will pop up.
689
     *  - xHeader: string, the name of the x-sendfile header. Defaults to "X-Sendfile".
690
     *
691
     * @return $this the response object itself
692
     * @see sendFile()
693
     */
694
    public function xSendFile($filePath, $attachmentName = null, $options = [])
695
    {
696
        if ($attachmentName === null) {
697
            $attachmentName = basename($filePath);
698
        }
699
        if (isset($options['mimeType'])) {
700
            $mimeType = $options['mimeType'];
701
        } elseif (($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
702
            $mimeType = 'application/octet-stream';
703
        }
704
        if (isset($options['xHeader'])) {
705
            $xHeader = $options['xHeader'];
706
        } else {
707
            $xHeader = 'X-Sendfile';
708
        }
709
710
        $disposition = empty($options['inline']) ? 'attachment' : 'inline';
711
        $this->getHeaders()
712
            ->setDefault($xHeader, $filePath)
713
            ->setDefault('Content-Type', $mimeType)
714
            ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName));
715
716
        $this->format = self::FORMAT_RAW;
717
718
        return $this;
719
    }
720
721
    /**
722
     * Returns Content-Disposition header value that is safe to use with both old and new browsers
723
     *
724
     * Fallback name:
725
     *
726
     * - Causes issues if contains non-ASCII characters with codes less than 32 or more than 126.
727
     * - Causes issues if contains urlencoded characters (starting with `%`) or `%` character. Some browsers interpret
728
     *   `filename="X"` as urlencoded name, some don't.
729
     * - Causes issues if contains path separator characters such as `\` or `/`.
730
     * - Since value is wrapped with `"`, it should be escaped as `\"`.
731
     * - Since input could contain non-ASCII characters, fallback is obtained by transliteration.
732
     *
733
     * UTF name:
734
     *
735
     * - Causes issues if contains path separator characters such as `\` or `/`.
736
     * - Should be urlencoded since headers are ASCII-only.
737
     * - Could be omitted if it exactly matches fallback name.
738
     *
739
     * @param string $disposition
740
     * @param string $attachmentName
741
     * @return string
742
     *
743
     * @since 2.0.10
744
     */
745 4
    protected function getDispositionHeaderValue($disposition, $attachmentName)
746
    {
747 4
        $fallbackName = str_replace('"', '\\"', str_replace(['%', '/', '\\'], '_', Inflector::transliterate($attachmentName, Inflector::TRANSLITERATE_LOOSE)));
748 4
        $utfName = rawurlencode(str_replace(['%', '/', '\\'], '', $attachmentName));
749
750 4
        $dispositionHeader = "{$disposition}; filename=\"{$fallbackName}\"";
751 4
        if ($utfName !== $fallbackName) {
752
            $dispositionHeader .= "; filename*=utf-8''{$utfName}";
753
        }
754 4
        return $dispositionHeader;
755
    }
756
757
    /**
758
     * Redirects the browser to the specified URL.
759
     *
760
     * This method adds a "Location" header to the current response. Note that it does not send out
761
     * the header until [[send()]] is called. In a controller action you may use this method as follows:
762
     *
763
     * ```php
764
     * return Yii::$app->getResponse()->redirect($url);
765
     * ```
766
     *
767
     * In other places, if you want to send out the "Location" header immediately, you should use
768
     * the following code:
769
     *
770
     * ```php
771
     * Yii::$app->getResponse()->redirect($url)->send();
772
     * return;
773
     * ```
774
     *
775
     * In AJAX mode, this normally will not work as expected unless there are some
776
     * client-side JavaScript code handling the redirection. To help achieve this goal,
777
     * this method will send out a "X-Redirect" header instead of "Location".
778
     *
779
     * If you use the "yii" JavaScript module, it will handle the AJAX redirection as
780
     * described above. Otherwise, you should write the following JavaScript code to
781
     * handle the redirection:
782
     *
783
     * ```javascript
784
     * $document.ajaxComplete(function (event, xhr, settings) {
785
     *     var url = xhr.getResponseHeader('X-Redirect');
786
     *     if (url) {
787
     *         window.location = url;
788
     *     }
789
     * });
790
     * ```
791
     *
792
     * @param string|array $url the URL to be redirected to. This can be in one of the following formats:
793
     *
794
     * - a string representing a URL (e.g. "http://example.com")
795
     * - a string representing a URL alias (e.g. "@example.com")
796
     * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`).
797
     *   Note that the route is with respect to the whole application, instead of relative to a controller or module.
798
     *   [[Url::to()]] will be used to convert the array into a URL.
799
     *
800
     * Any relative URL will be converted into an absolute one by prepending it with the host info
801
     * of the current request.
802
     *
803
     * @param int $statusCode the HTTP status code. Defaults to 302.
804
     * See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
805
     * for details about HTTP status code
806
     * @param bool $checkAjax whether to specially handle AJAX (and PJAX) requests. Defaults to true,
807
     * meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser
808
     * to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as
809
     * an AJAX/PJAX response, may NOT cause browser redirection.
810
     * Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent.
811
     * @return $this the response object itself
812
     */
813 1
    public function redirect($url, $statusCode = 302, $checkAjax = true)
814
    {
815 1
        if (is_array($url) && isset($url[0])) {
816
            // ensure the route is absolute
817 1
            $url[0] = '/' . ltrim($url[0], '/');
818 1
        }
819 1
        $url = Url::to($url);
820 1
        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
821 1
            $url = Yii::$app->getRequest()->getHostInfo() . $url;
822 1
        }
823
824 1
        if ($checkAjax) {
825 1
            if (Yii::$app->getRequest()->getIsAjax()) {
826 1
                if (Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null && $statusCode === 302) {
827
                    // Ajax 302 redirect in IE does not work. Change status code to 200. See https://github.com/yiisoft/yii2/issues/9670
828
                    $statusCode = 200;
829
                }
830 1
                if (Yii::$app->getRequest()->getIsPjax()) {
831
                    $this->getHeaders()->set('X-Pjax-Url', $url);
832
                } else {
833 1
                    $this->getHeaders()->set('X-Redirect', $url);
834
                }
835 1
            } else {
836 1
                $this->getHeaders()->set('Location', $url);
837
            }
838 1
        } else {
839
            $this->getHeaders()->set('Location', $url);
840
        }
841
842 1
        $this->setStatusCode($statusCode);
843
844 1
        return $this;
845
    }
846
847
    /**
848
     * Refreshes the current page.
849
     * The effect of this method call is the same as the user pressing the refresh button of his browser
850
     * (without re-posting data).
851
     *
852
     * In a controller action you may use this method like this:
853
     *
854
     * ```php
855
     * return Yii::$app->getResponse()->refresh();
856
     * ```
857
     *
858
     * @param string $anchor the anchor that should be appended to the redirection URL.
859
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
860
     * @return Response the response object itself
861
     */
862
    public function refresh($anchor = '')
863
    {
864
        return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor);
865
    }
866
867
    private $_cookies;
868
869
    /**
870
     * Returns the cookie collection.
871
     * Through the returned cookie collection, you add or remove cookies as follows,
872
     *
873
     * ```php
874
     * // add a cookie
875
     * $response->cookies->add(new Cookie([
876
     *     'name' => $name,
877
     *     'value' => $value,
878
     * ]);
879
     *
880
     * // remove a cookie
881
     * $response->cookies->remove('name');
882
     * // alternatively
883
     * unset($response->cookies['name']);
884
     * ```
885
     *
886
     * @return CookieCollection the cookie collection.
887
     */
888 27
    public function getCookies()
889
    {
890 27
        if ($this->_cookies === null) {
891 27
            $this->_cookies = new CookieCollection;
892 27
        }
893 27
        return $this->_cookies;
894
    }
895
896
    /**
897
     * @return bool whether this response has a valid [[statusCode]].
898
     */
899 9
    public function getIsInvalid()
900
    {
901 9
        return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
902
    }
903
904
    /**
905
     * @return bool whether this response is informational
906
     */
907
    public function getIsInformational()
908
    {
909
        return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200;
910
    }
911
912
    /**
913
     * @return bool whether this response is successful
914
     */
915
    public function getIsSuccessful()
916
    {
917
        return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
918
    }
919
920
    /**
921
     * @return bool whether this response is a redirection
922
     */
923 1
    public function getIsRedirection()
924
    {
925 1
        return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
926
    }
927
928
    /**
929
     * @return bool whether this response indicates a client error
930
     */
931
    public function getIsClientError()
932
    {
933
        return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
934
    }
935
936
    /**
937
     * @return bool whether this response indicates a server error
938
     */
939
    public function getIsServerError()
940
    {
941
        return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
942
    }
943
944
    /**
945
     * @return bool whether this response is OK
946
     */
947
    public function getIsOk()
948
    {
949
        return $this->getStatusCode() == 200;
950
    }
951
952
    /**
953
     * @return bool whether this response indicates the current request is forbidden
954
     */
955
    public function getIsForbidden()
956
    {
957
        return $this->getStatusCode() == 403;
958
    }
959
960
    /**
961
     * @return bool whether this response indicates the currently requested resource is not found
962
     */
963
    public function getIsNotFound()
964
    {
965
        return $this->getStatusCode() == 404;
966
    }
967
968
    /**
969
     * @return bool whether this response is empty
970
     */
971
    public function getIsEmpty()
972
    {
973
        return in_array($this->getStatusCode(), [201, 204, 304]);
974
    }
975
976
    /**
977
     * @return array the formatters that are supported by default
978
     */
979 113
    protected function defaultFormatters()
980
    {
981
        return [
982 113
            self::FORMAT_HTML => 'yii\web\HtmlResponseFormatter',
983 113
            self::FORMAT_XML => 'yii\web\XmlResponseFormatter',
984 113
            self::FORMAT_JSON => 'yii\web\JsonResponseFormatter',
985 113
            self::FORMAT_JSONP => [
986 113
                'class' => 'yii\web\JsonResponseFormatter',
987 113
                'useJsonp' => true,
988 113
            ],
989 113
        ];
990
    }
991
992
    /**
993
     * Prepares for sending the response.
994
     * The default implementation will convert [[data]] into [[content]] and set headers accordingly.
995
     * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported
996
     */
997 7
    protected function prepare()
998
    {
999 7
        if ($this->stream !== null) {
1000 3
            return;
1001
        }
1002
1003 4
        if (isset($this->formatters[$this->format])) {
1004 3
            $formatter = $this->formatters[$this->format];
1005 3
            if (!is_object($formatter)) {
1006 3
                $this->formatters[$this->format] = $formatter = Yii::createObject($formatter);
1007 3
            }
1008 3
            if ($formatter instanceof ResponseFormatterInterface) {
1009 3
                $formatter->format($this);
1010 3
            } else {
1011
                throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface.");
1012
            }
1013 4
        } elseif ($this->format === self::FORMAT_RAW) {
1014 1
            if ($this->data !== null) {
1015
                $this->content = $this->data;
1016
            }
1017 1
        } else {
1018
            throw new InvalidConfigException("Unsupported response format: {$this->format}");
1019
        }
1020
1021 4
        if (is_array($this->content)) {
1022
            throw new InvalidParamException('Response content must not be an array.');
1023 4
        } elseif (is_object($this->content)) {
1024
            if (method_exists($this->content, '__toString')) {
1025
                $this->content = $this->content->__toString();
1026
            } else {
1027
                throw new InvalidParamException('Response content must be a string or an object implementing __toString().');
1028
            }
1029
        }
1030 4
    }
1031
}
1032