Completed
Push — readme-redesign ( e2fd40...17eb05 )
by Alexander
108:51 queued 68:52
created

Response::sendContent()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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