Completed
Push — revert-10335-patch-1 ( f15613 )
by Carsten
08:07
created

Response::sendStreamAsFile()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

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