Completed
Push — 2.1 ( c952e8...98ed49 )
by Carsten
10:00
created

Response::sendCookies()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.2222
c 0
b 0
f 0
ccs 0
cts 17
cp 0
cc 7
eloc 13
nc 8
nop 0
crap 56
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\Url;
14
use yii\helpers\FileHelper;
15
use yii\helpers\StringHelper;
16
17
/**
18
 * The web Response class represents an HTTP response
19
 *
20
 * It holds the [[headers]], [[cookies]] and [[content]] that is to be sent to the client.
21
 * It also controls the HTTP [[statusCode|status code]].
22
 *
23
 * Response is configured as an application component in [[\yii\web\Application]] by default.
24
 * You can access that instance via `Yii::$app->response`.
25
 *
26
 * You can modify its configuration by adding an array to your application config under `components`
27
 * as it is shown in the following example:
28
 *
29
 * ```php
30
 * 'response' => [
31
 *     'format' => yii\web\Response::FORMAT_JSON,
32
 *     'charset' => 'UTF-8',
33
 *     // ...
34
 * ]
35
 * ```
36
 *
37
 * @property CookieCollection $cookies The cookie collection. This property is read-only.
38
 * @property string $downloadHeaders The attachment file name. This property is write-only.
39
 * @property HeaderCollection $headers The header collection. This property is read-only.
40
 * @property boolean $isClientError Whether this response indicates a client error. This property is
41
 * read-only.
42
 * @property boolean $isEmpty Whether this response is empty. This property is read-only.
43
 * @property boolean $isForbidden Whether this response indicates the current request is forbidden. This
44
 * property is read-only.
45
 * @property boolean $isInformational Whether this response is informational. This property is read-only.
46
 * @property boolean $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only.
47
 * @property boolean $isNotFound Whether this response indicates the currently requested resource is not
48
 * found. This property is read-only.
49
 * @property boolean $isOk Whether this response is OK. This property is read-only.
50
 * @property boolean $isRedirection Whether this response is a redirection. This property is read-only.
51
 * @property boolean $isServerError Whether this response indicates a server error. This property is
52
 * read-only.
53
 * @property boolean $isSuccessful Whether this response is successful. This property is read-only.
54
 * @property integer $statusCode The HTTP status code to send with the response.
55
 *
56
 * @author Qiang Xue <[email protected]>
57
 * @author Carsten Brandt <[email protected]>
58
 * @since 2.0
59
 */
60
class Response extends \yii\base\Response
61
{
62
    /**
63
     * @event ResponseEvent an event that is triggered at the beginning of [[send()]].
64
     */
65
    const EVENT_BEFORE_SEND = 'beforeSend';
66
    /**
67
     * @event ResponseEvent an event that is triggered at the end of [[send()]].
68
     */
69
    const EVENT_AFTER_SEND = 'afterSend';
70
    /**
71
     * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]].
72
     * You may respond to this event to filter the response content before it is sent to the client.
73
     */
74
    const EVENT_AFTER_PREPARE = 'afterPrepare';
75
    const FORMAT_RAW = 'raw';
76
    const FORMAT_HTML = 'html';
77
    const FORMAT_JSON = 'json';
78
    const FORMAT_JSONP = 'jsonp';
79
    const FORMAT_XML = 'xml';
80
81
    /**
82
     * @var string the response format. This determines how to convert [[data]] into [[content]]
83
     * when the latter is not set. The value of this property must be one of the keys declared in the [[formatters] array.
84
     * By default, the following formats are supported:
85
     *
86
     * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion.
87
     *   No extra HTTP header will be added.
88
     * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion.
89
     *   The "Content-Type" header will set as "text/html".
90
     * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type"
91
     *   header will be set as "application/json".
92
     * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type"
93
     *   header will be set as "text/javascript". Note that in this case `$data` must be an array
94
     *   with "data" and "callback" elements. The former refers to the actual data to be sent,
95
     *   while the latter refers to the name of the JavaScript callback.
96
     * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]]
97
     *   for more details.
98
     *
99
     * You may customize the formatting process or support additional formats by configuring [[formatters]].
100
     * @see formatters
101
     */
102
    public $format = self::FORMAT_HTML;
103
    /**
104
     * @var string the MIME type (e.g. `application/json`) from the request ACCEPT header chosen for this response.
105
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
106
     */
107
    public $acceptMimeType;
108
    /**
109
     * @var array the parameters (e.g. `['q' => 1, 'version' => '1.0']`) associated with the [[acceptMimeType|chosen MIME type]].
110
     * This is a list of name-value pairs associated with [[acceptMimeType]] from the ACCEPT HTTP header.
111
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
112
     */
113
    public $acceptParams = [];
114
    /**
115
     * @var array the formatters for converting data into the response content of the specified [[format]].
116
     * The array keys are the format names, and the array values are the corresponding configurations
117
     * for creating the formatter objects.
118
     * @see format
119
     * @see defaultFormatters
120
     */
121
    public $formatters = [];
122
    /**
123
     * @var mixed the original response data. When this is not null, it will be converted into [[content]]
124
     * according to [[format]] when the response is being sent out.
125
     * @see content
126
     */
127
    public $data;
128
    /**
129
     * @var string the response content. When [[data]] is not null, it will be converted into [[content]]
130
     * according to [[format]] when the response is being sent out.
131
     * @see data
132
     */
133
    public $content;
134
    /**
135
     * @var resource|array the stream to be sent. This can be a stream handle or an array of stream handle,
136
     * the begin position and the end position. Note that when this property is set, the [[data]] and [[content]]
137
     * properties will be ignored by [[send()]].
138
     */
139
    public $stream;
140
    /**
141
     * @var string the charset of the text response. If not set, it will use
142
     * the value of [[Application::charset]].
143
     */
144
    public $charset;
145
    /**
146
     * @var string the HTTP status description that comes together with the status code.
147
     * @see httpStatuses
148
     */
149
    public $statusText = 'OK';
150
    /**
151
     * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`,
152
     * or '1.1' if that is not available.
153
     */
154
    public $version;
155
    /**
156
     * @var boolean whether the response has been sent. If this is true, calling [[send()]] will do nothing.
157
     */
158
    public $isSent = false;
159
    /**
160
     * @var array list of HTTP status codes and the corresponding texts
161
     */
162
    public static $httpStatuses = [
163
        100 => 'Continue',
164
        101 => 'Switching Protocols',
165
        102 => 'Processing',
166
        118 => 'Connection timed out',
167
        200 => 'OK',
168
        201 => 'Created',
169
        202 => 'Accepted',
170
        203 => 'Non-Authoritative',
171
        204 => 'No Content',
172
        205 => 'Reset Content',
173
        206 => 'Partial Content',
174
        207 => 'Multi-Status',
175
        208 => 'Already Reported',
176
        210 => 'Content Different',
177
        226 => 'IM Used',
178
        300 => 'Multiple Choices',
179
        301 => 'Moved Permanently',
180
        302 => 'Found',
181
        303 => 'See Other',
182
        304 => 'Not Modified',
183
        305 => 'Use Proxy',
184
        306 => 'Reserved',
185
        307 => 'Temporary Redirect',
186
        308 => 'Permanent Redirect',
187
        310 => 'Too many Redirect',
188
        400 => 'Bad Request',
189
        401 => 'Unauthorized',
190
        402 => 'Payment Required',
191
        403 => 'Forbidden',
192
        404 => 'Not Found',
193
        405 => 'Method Not Allowed',
194
        406 => 'Not Acceptable',
195
        407 => 'Proxy Authentication Required',
196
        408 => 'Request Time-out',
197
        409 => 'Conflict',
198
        410 => 'Gone',
199
        411 => 'Length Required',
200
        412 => 'Precondition Failed',
201
        413 => 'Request Entity Too Large',
202
        414 => 'Request-URI Too Long',
203
        415 => 'Unsupported Media Type',
204
        416 => 'Requested range unsatisfiable',
205
        417 => 'Expectation failed',
206
        418 => 'I\'m a teapot',
207
        421 => 'Misdirected Request',
208
        422 => 'Unprocessable entity',
209
        423 => 'Locked',
210
        424 => 'Method failure',
211
        425 => 'Unordered Collection',
212
        426 => 'Upgrade Required',
213
        428 => 'Precondition Required',
214
        429 => 'Too Many Requests',
215
        431 => 'Request Header Fields Too Large',
216
        449 => 'Retry With',
217
        450 => 'Blocked by Windows Parental Controls',
218
        500 => 'Internal Server Error',
219
        501 => 'Not Implemented',
220
        502 => 'Bad Gateway or Proxy Error',
221
        503 => 'Service Unavailable',
222
        504 => 'Gateway Time-out',
223
        505 => 'HTTP Version not supported',
224
        507 => 'Insufficient storage',
225
        508 => 'Loop Detected',
226
        509 => 'Bandwidth Limit Exceeded',
227
        510 => 'Not Extended',
228
        511 => 'Network Authentication Required',
229
    ];
230
231
    /**
232
     * @var integer the HTTP status code to send with the response.
233
     */
234
    private $_statusCode = 200;
235
    /**
236
     * @var HeaderCollection
237
     */
238
    private $_headers;
239
240
241
    /**
242
     * Initializes this component.
243
     */
244 89
    public function init()
245
    {
246 89
        if ($this->version === null) {
247 89
            if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
248
                $this->version = '1.0';
249
            } else {
250 89
                $this->version = '1.1';
251
            }
252 89
        }
253 89
        if ($this->charset === null) {
254 89
            $this->charset = Yii::$app->charset;
255 89
        }
256 89
        $this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
257 89
    }
258
259
    /**
260
     * @return integer the HTTP status code to send with the response.
261
     */
262 5
    public function getStatusCode()
263
    {
264 5
        return $this->_statusCode;
265
    }
266
267
    /**
268
     * Sets the response status code.
269
     * This method will set the corresponding status text if `$text` is null.
270
     * @param integer $value the status code
271
     * @param string $text the status text. If not set, it will be set automatically based on the status code.
272
     * @throws InvalidParamException if the status code is invalid.
273
     */
274 5
    public function setStatusCode($value, $text = null)
275
    {
276 5
        if ($value === null) {
277
            $value = 200;
278
        }
279 5
        $this->_statusCode = (int) $value;
280 5
        if ($this->getIsInvalid()) {
281
            throw new InvalidParamException("The HTTP status code is invalid: $value");
282
        }
283 5
        if ($text === null) {
284 5
            $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : '';
285 5
        } else {
286
            $this->statusText = $text;
287
        }
288 5
    }
289
290
    /**
291
     * Returns the header collection.
292
     * The header collection contains the currently registered HTTP headers.
293
     * @return HeaderCollection the header collection
294
     */
295 45
    public function getHeaders()
296
    {
297 45
        if ($this->_headers === null) {
298 45
            $this->_headers = new HeaderCollection;
299 45
        }
300 45
        return $this->_headers;
301
    }
302
303
    /**
304
     * Sends the response to the client.
305
     */
306 4
    public function send()
307
    {
308 4
        if ($this->isSent) {
309
            return;
310
        }
311 4
        $this->trigger(self::EVENT_BEFORE_SEND);
312 4
        $this->prepare();
313 4
        $this->trigger(self::EVENT_AFTER_PREPARE);
314 4
        $this->sendHeaders();
315 4
        $this->sendContent();
316 4
        $this->trigger(self::EVENT_AFTER_SEND);
317 4
        $this->isSent = true;
318 4
    }
319
320
    /**
321
     * Clears the headers, cookies, content, status code of the response.
322
     */
323
    public function clear()
324
    {
325
        $this->_headers = null;
326
        $this->_cookies = null;
327
        $this->_statusCode = 200;
328
        $this->statusText = 'OK';
329
        $this->data = null;
330
        $this->stream = null;
331
        $this->content = null;
332
        $this->isSent = false;
333
    }
334
335
    /**
336
     * Sends the response headers to the client
337
     */
338 4
    protected function sendHeaders()
339
    {
340 4
        if (headers_sent()) {
341 4
            return;
342
        }
343
        if ($this->_headers) {
344
            $headers = $this->getHeaders();
345
            foreach ($headers as $name => $values) {
346
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
347
                // set replace for first occurrence of header but false afterwards to allow multiple
348
                $replace = true;
349
                foreach ($values as $value) {
350
                    header("$name: $value", $replace);
351
                    $replace = false;
352
                }
353
            }
354
        }
355
        $statusCode = $this->getStatusCode();
356
        header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
357
        $this->sendCookies();
358
    }
359
360
    /**
361
     * Sends the cookies to the client.
362
     */
363
    protected function sendCookies()
364
    {
365
        if ($this->_cookies === null) {
366
            return;
367
        }
368
        $request = Yii::$app->getRequest();
369
        if ($request->enableCookieValidation) {
370
            if ($request->cookieValidationKey == '') {
371
                throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
372
            }
373
            $validationKey = $request->cookieValidationKey;
374
        }
375
        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...
376
            $value = $cookie->value;
377
            if ($cookie->expire != 1  && isset($validationKey)) {
378
                $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
379
            }
380
            setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
381
        }
382
    }
383
384
    /**
385
     * Sends the response content to the client
386
     */
387 4
    protected function sendContent()
388
    {
389 4
        if ($this->stream === null) {
390 1
            echo $this->content;
391
392 1
            return;
393
        }
394
395 3
        set_time_limit(0); // Reset time limit for big files
396 3
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
397
398 3
        if (is_array($this->stream)) {
399 3
            list ($handle, $begin, $end) = $this->stream;
400 3
            fseek($handle, $begin);
401 3
            while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
402 3
                if ($pos + $chunkSize > $end) {
403 3
                    $chunkSize = $end - $pos + 1;
404 3
                }
405 3
                echo fread($handle, $chunkSize);
406 3
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
407 3
            }
408 3
            fclose($handle);
409 3
        } else {
410
            while (!feof($this->stream)) {
411
                echo fread($this->stream, $chunkSize);
412
                flush();
413
            }
414
            fclose($this->stream);
415
        }
416 3
    }
417
418
    /**
419
     * Sends a file to the browser.
420
     *
421
     * Note that this method only prepares the response for file sending. The file is not sent
422
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
423
     *
424
     * @param string $filePath the path of the file to be sent.
425
     * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
426
     * @param array $options additional options for sending the file. The following options are supported:
427
     *
428
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
429
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
430
     *    meaning a download dialog will pop up.
431
     *
432
     * @return $this the response object itself
433
     */
434 7
    public function sendFile($filePath, $attachmentName = null, $options = [])
435
    {
436 7
        if (!isset($options['mimeType'])) {
437 7
            $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);
438 7
        }
439 7
        if ($attachmentName === null) {
440 7
            $attachmentName = basename($filePath);
441 7
        }
442 7
        $handle = fopen($filePath, 'rb');
443 7
        $this->sendStreamAsFile($handle, $attachmentName, $options);
444
445 3
        return $this;
446
    }
447
448
    /**
449
     * Sends the specified content as a file to the browser.
450
     *
451
     * Note that this method only prepares the response for file sending. The file is not sent
452
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
453
     *
454
     * @param string $content the content to be sent. The existing [[content]] will be discarded.
455
     * @param string $attachmentName the file name shown to the user.
456
     * @param array $options additional options for sending the file. The following options are supported:
457
     *
458
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
459
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
460
     *    meaning a download dialog will pop up.
461
     *
462
     * @return $this the response object itself
463
     * @throws HttpException if the requested range is not satisfiable
464
     */
465 1
    public function sendContentAsFile($content, $attachmentName, $options = [])
466
    {
467 1
        $headers = $this->getHeaders();
468
469 1
        $contentLength = StringHelper::byteLength($content);
470 1
        $range = $this->getHttpRange($contentLength);
471
472 1
        if ($range === false) {
473
            $headers->set('Content-Range', "bytes */$contentLength");
474
            throw new HttpException(416, 'Requested range not satisfiable');
475
        }
476
477 1
        list($begin, $end) = $range;
478 1
        if ($begin != 0 || $end != $contentLength - 1) {
479
            $this->setStatusCode(206);
480
            $headers->set('Content-Range', "bytes $begin-$end/$contentLength");
481
            $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1);
482
        } else {
483 1
            $this->setStatusCode(200);
484 1
            $this->content = $content;
485
        }
486
487 1
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
488 1
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
489
490 1
        $this->format = self::FORMAT_RAW;
491
492 1
        return $this;
493
    }
494
495
    /**
496
     * Sends the specified stream as a file to the browser.
497
     *
498
     * Note that this method only prepares the response for file sending. The file is not sent
499
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
500
     *
501
     * @param resource $handle the handle of the stream to be sent.
502
     * @param string $attachmentName the file name shown to the user.
503
     * @param array $options additional options for sending the file. The following options are supported:
504
     *
505
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
506
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
507
     *    meaning a download dialog will pop up.
508
     *  - `fileSize`: the size of the content to stream this is useful when size of the content is known
509
     *    and the content is not seekable. Defaults to content size using `ftell()`.
510
     *    This option is available since version 2.0.4.
511
     *
512
     * @return $this the response object itself
513
     * @throws HttpException if the requested range cannot be satisfied.
514
     */
515 7
    public function sendStreamAsFile($handle, $attachmentName, $options = [])
516
    {
517 7
        $headers = $this->getHeaders();
518 7
        if (isset($options['fileSize'])) {
519
            $fileSize = $options['fileSize'];
520
        } else {
521 7
            fseek($handle, 0, SEEK_END);
522 7
            $fileSize = ftell($handle);
523
        }
524
525 7
        $range = $this->getHttpRange($fileSize);
526 7
        if ($range === false) {
527 4
            $headers->set('Content-Range', "bytes */$fileSize");
528 4
            throw new HttpException(416, 'Requested range not satisfiable');
529
        }
530
531 3
        list($begin, $end) = $range;
532 3
        if ($begin != 0 || $end != $fileSize - 1) {
533 3
            $this->setStatusCode(206);
534 3
            $headers->set('Content-Range', "bytes $begin-$end/$fileSize");
535 3
        } else {
536
            $this->setStatusCode(200);
537
        }
538
539 3
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
540 3
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
541
542 3
        $this->format = self::FORMAT_RAW;
543 3
        $this->stream = [$handle, $begin, $end];
544
545 3
        return $this;
546
    }
547
548
    /**
549
     * Sets a default set of HTTP headers for file downloading purpose.
550
     * @param string $attachmentName the attachment file name
551
     * @param string $mimeType the MIME type for the response. If null, `Content-Type` header will NOT be set.
552
     * @param boolean $inline whether the browser should open the file within the browser window. Defaults to false,
553
     * meaning a download dialog will pop up.
554
     * @param integer $contentLength the byte length of the file being downloaded. If null, `Content-Length` header will NOT be set.
555
     * @return $this the response object itself
556
     */
557 4
    public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null)
558
    {
559 4
        $headers = $this->getHeaders();
560
561 4
        $disposition = $inline ? 'inline' : 'attachment';
562 4
        $headers->setDefault('Pragma', 'public')
563 4
            ->setDefault('Accept-Ranges', 'bytes')
564 4
            ->setDefault('Expires', '0')
565 4
            ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
566 4
            ->setDefault('Content-Disposition', "$disposition; filename=\"$attachmentName\"");
567
568 4
        if ($mimeType !== null) {
569 4
            $headers->setDefault('Content-Type', $mimeType);
570 4
        }
571
572 4
        if ($contentLength !== null) {
573 4
            $headers->setDefault('Content-Length', $contentLength);
574 4
        }
575
576 4
        return $this;
577
    }
578
579
    /**
580
     * Determines the HTTP range given in the request.
581
     * @param integer $fileSize the size of the file that will be used to validate the requested HTTP range.
582
     * @return array|boolean the range (begin, end), or false if the range request is invalid.
583
     */
584 8
    protected function getHttpRange($fileSize)
585
    {
586 8
        if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') {
587 1
            return [0, $fileSize - 1];
588
        }
589 7
        if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) {
590 1
            return false;
591
        }
592 6
        if ($matches[1] === '') {
593 2
            $start = $fileSize - $matches[2];
594 2
            $end = $fileSize - 1;
595 6
        } elseif ($matches[2] !== '') {
596 2
            $start = $matches[1];
597 2
            $end = $matches[2];
598 2
            if ($end >= $fileSize) {
599
                $end = $fileSize - 1;
600
            }
601 2
        } else {
602 2
            $start = $matches[1];
603 2
            $end = $fileSize - 1;
604
        }
605 6
        if ($start < 0 || $start > $end) {
606 3
            return false;
607
        } else {
608 3
            return [$start, $end];
609
        }
610
    }
611
612
    /**
613
     * Sends existing file to a browser as a download using x-sendfile.
614
     *
615
     * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
616
     * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
617
     * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
618
     * increase in performance as the web application is allowed to terminate earlier while the webserver is
619
     * handling the request.
620
     *
621
     * The request is sent to the server through a special non-standard HTTP-header.
622
     * When the web server encounters the presence of such header it will discard all output and send the file
623
     * specified by that header using web server internals including all optimizations like caching-headers.
624
     *
625
     * As this header directive is non-standard different directives exists for different web servers applications:
626
     *
627
     * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
628
     * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
629
     * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
630
     * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
631
     * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
632
     *
633
     * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
634
     * a proper xHeader should be sent.
635
     *
636
     * **Note**
637
     *
638
     * This option allows to download files that are not under web folders, and even files that are otherwise protected
639
     * (deny from all) like `.htaccess`.
640
     *
641
     * **Side effects**
642
     *
643
     * If this option is disabled by the web server, when this method is called a download configuration dialog
644
     * will open but the downloaded file will have 0 bytes.
645
     *
646
     * **Known issues**
647
     *
648
     * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
649
     * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
650
     * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
651
     *
652
     * **Example**
653
     *
654
     * ```php
655
     * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg');
656
     * ```
657
     *
658
     * @param string $filePath file name with full path
659
     * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
660
     * @param array $options additional options for sending the file. The following options are supported:
661
     *
662
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
663
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
664
     *    meaning a download dialog will pop up.
665
     *  - xHeader: string, the name of the x-sendfile header. Defaults to "X-Sendfile".
666
     *
667
     * @return $this the response object itself
668
     */
669
    public function xSendFile($filePath, $attachmentName = null, $options = [])
670
    {
671
        if ($attachmentName === null) {
672
            $attachmentName = basename($filePath);
673
        }
674
        if (isset($options['mimeType'])) {
675
            $mimeType = $options['mimeType'];
676
        } elseif (($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
677
            $mimeType = 'application/octet-stream';
678
        }
679
        if (isset($options['xHeader'])) {
680
            $xHeader = $options['xHeader'];
681
        } else {
682
            $xHeader = 'X-Sendfile';
683
        }
684
685
        $disposition = empty($options['inline']) ? 'attachment' : 'inline';
686
        $this->getHeaders()
687
            ->setDefault($xHeader, $filePath)
688
            ->setDefault('Content-Type', $mimeType)
689
            ->setDefault('Content-Disposition', "{$disposition}; filename=\"{$attachmentName}\"");
690
691
        $this->format = self::FORMAT_RAW;
692
693
        return $this;
694
    }
695
696
    /**
697
     * Redirects the browser to the specified URL.
698
     *
699
     * This method adds a "Location" header to the current response. Note that it does not send out
700
     * the header until [[send()]] is called. In a controller action you may use this method as follows:
701
     *
702
     * ```php
703
     * return Yii::$app->getResponse()->redirect($url);
704
     * ```
705
     *
706
     * In other places, if you want to send out the "Location" header immediately, you should use
707
     * the following code:
708
     *
709
     * ```php
710
     * Yii::$app->getResponse()->redirect($url)->send();
711
     * return;
712
     * ```
713
     *
714
     * In AJAX mode, this normally will not work as expected unless there are some
715
     * client-side JavaScript code handling the redirection. To help achieve this goal,
716
     * this method will send out a "X-Redirect" header instead of "Location".
717
     *
718
     * If you use the "yii" JavaScript module, it will handle the AJAX redirection as
719
     * described above. Otherwise, you should write the following JavaScript code to
720
     * handle the redirection:
721
     *
722
     * ```javascript
723
     * $document.ajaxComplete(function (event, xhr, settings) {
724
     *     var url = xhr.getResponseHeader('X-Redirect');
725
     *     if (url) {
726
     *         window.location = url;
727
     *     }
728
     * });
729
     * ```
730
     *
731
     * @param string|array $url the URL to be redirected to. This can be in one of the following formats:
732
     *
733
     * - a string representing a URL (e.g. "http://example.com")
734
     * - a string representing a URL alias (e.g. "@example.com")
735
     * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`).
736
     *   Note that the route is with respect to the whole application, instead of relative to a controller or module.
737
     *   [[Url::to()]] will be used to convert the array into a URL.
738
     *
739
     * Any relative URL will be converted into an absolute one by prepending it with the host info
740
     * of the current request.
741
     *
742
     * @param integer $statusCode the HTTP status code. Defaults to 302.
743
     * See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
744
     * for details about HTTP status code
745
     * @param boolean $checkAjax whether to specially handle AJAX (and PJAX) requests. Defaults to true,
746
     * meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser
747
     * to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as
748
     * an AJAX/PJAX response, may NOT cause browser redirection.
749
     * Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent.
750
     * @return $this the response object itself
751
     */
752 1
    public function redirect($url, $statusCode = 302, $checkAjax = true)
753
    {
754 1
        if (is_array($url) && isset($url[0])) {
755
            // ensure the route is absolute
756 1
            $url[0] = '/' . ltrim($url[0], '/');
757 1
        }
758 1
        $url = Url::to($url);
759 1
        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
760 1
            $url = Yii::$app->getRequest()->getHostInfo() . $url;
761 1
        }
762
763 1
        if ($checkAjax) {
764 1
            if (Yii::$app->getRequest()->getIsAjax()) {
765 1
                if (Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null && $statusCode === 302) {
766
                    // Ajax 302 redirect in IE does not work. Change status code to 200. See https://github.com/yiisoft/yii2/issues/9670
767
                    $statusCode = 200;
768
                }
769 1
                if (Yii::$app->getRequest()->getIsPjax()) {
770
                    $this->getHeaders()->set('X-Pjax-Url', $url);
771
                } else {
772 1
                    $this->getHeaders()->set('X-Redirect', $url);
773
                }
774 1
            } else {
775 1
                $this->getHeaders()->set('Location', $url);
776
            }
777 1
        } else {
778
            $this->getHeaders()->set('Location', $url);
779
        }
780
781 1
        $this->setStatusCode($statusCode);
782
783 1
        return $this;
784
    }
785
786
    /**
787
     * Refreshes the current page.
788
     * The effect of this method call is the same as the user pressing the refresh button of his browser
789
     * (without re-posting data).
790
     *
791
     * In a controller action you may use this method like this:
792
     *
793
     * ```php
794
     * return Yii::$app->getResponse()->refresh();
795
     * ```
796
     *
797
     * @param string $anchor the anchor that should be appended to the redirection URL.
798
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
799
     * @return Response the response object itself
800
     */
801
    public function refresh($anchor = '')
802
    {
803
        return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor);
804
    }
805
806
    private $_cookies;
807
808
    /**
809
     * Returns the cookie collection.
810
     * Through the returned cookie collection, you add or remove cookies as follows,
811
     *
812
     * ```php
813
     * // add a cookie
814
     * $response->cookies->add(new Cookie([
815
     *     'name' => $name,
816
     *     'value' => $value,
817
     * ]);
818
     *
819
     * // remove a cookie
820
     * $response->cookies->remove('name');
821
     * // alternatively
822
     * unset($response->cookies['name']);
823
     * ```
824
     *
825
     * @return CookieCollection the cookie collection.
826
     */
827 21
    public function getCookies()
828
    {
829 21
        if ($this->_cookies === null) {
830 21
            $this->_cookies = new CookieCollection;
831 21
        }
832 21
        return $this->_cookies;
833
    }
834
835
    /**
836
     * @return boolean whether this response has a valid [[statusCode]].
837
     */
838 5
    public function getIsInvalid()
839
    {
840 5
        return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
841
    }
842
843
    /**
844
     * @return boolean whether this response is informational
845
     */
846
    public function getIsInformational()
847
    {
848
        return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200;
849
    }
850
851
    /**
852
     * @return boolean whether this response is successful
853
     */
854
    public function getIsSuccessful()
855
    {
856
        return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
857
    }
858
859
    /**
860
     * @return boolean whether this response is a redirection
861
     */
862 1
    public function getIsRedirection()
863
    {
864 1
        return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
865
    }
866
867
    /**
868
     * @return boolean whether this response indicates a client error
869
     */
870
    public function getIsClientError()
871
    {
872
        return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
873
    }
874
875
    /**
876
     * @return boolean whether this response indicates a server error
877
     */
878
    public function getIsServerError()
879
    {
880
        return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
881
    }
882
883
    /**
884
     * @return boolean whether this response is OK
885
     */
886
    public function getIsOk()
887
    {
888
        return $this->getStatusCode() == 200;
889
    }
890
891
    /**
892
     * @return boolean whether this response indicates the current request is forbidden
893
     */
894
    public function getIsForbidden()
895
    {
896
        return $this->getStatusCode() == 403;
897
    }
898
899
    /**
900
     * @return boolean whether this response indicates the currently requested resource is not found
901
     */
902
    public function getIsNotFound()
903
    {
904
        return $this->getStatusCode() == 404;
905
    }
906
907
    /**
908
     * @return boolean whether this response is empty
909
     */
910
    public function getIsEmpty()
911
    {
912
        return in_array($this->getStatusCode(), [201, 204, 304]);
913
    }
914
915
    /**
916
     * @return array the formatters that are supported by default
917
     */
918 89
    protected function defaultFormatters()
919
    {
920
        return [
921 89
            self::FORMAT_HTML => HtmlResponseFormatter::class,
922 89
            self::FORMAT_XML => XmlResponseFormatter::class,
923 89
            self::FORMAT_JSON => JsonResponseFormatter::class,
924 89
            self::FORMAT_JSONP => [
925 89
                'class' => JsonResponseFormatter::class,
926 89
                'useJsonp' => true,
927 89
            ],
928 89
        ];
929
    }
930
931
    /**
932
     * Prepares for sending the response.
933
     * The default implementation will convert [[data]] into [[content]] and set headers accordingly.
934
     * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported
935
     */
936 4
    protected function prepare()
937
    {
938 4
        if ($this->stream !== null) {
939 3
            return;
940
        }
941
942 1
        if (isset($this->formatters[$this->format])) {
943
            $formatter = $this->formatters[$this->format];
944
            if (!is_object($formatter)) {
945
                $this->formatters[$this->format] = $formatter = Yii::createObject($formatter);
946
            }
947
            if ($formatter instanceof ResponseFormatterInterface) {
948
                $formatter->format($this);
949
            } else {
950
                throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface.");
951
            }
952 1
        } elseif ($this->format === self::FORMAT_RAW) {
953 1
            if ($this->data !== null) {
954
                $this->content = $this->data;
955
            }
956 1
        } else {
957
            throw new InvalidConfigException("Unsupported response format: {$this->format}");
958
        }
959
960 1
        if (is_array($this->content)) {
961
            throw new InvalidParamException('Response content must not be an array.');
962 1
        } elseif (is_object($this->content)) {
963
            if (method_exists($this->content, '__toString')) {
964
                $this->content = $this->content->__toString();
965
            } else {
966
                throw new InvalidParamException('Response content must be a string or an object implementing __toString().');
967
            }
968
        }
969 1
    }
970
}
971