Completed
Push — 2.1 ( 37580d...257604 )
by
unknown
12:15
created

Response   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1050
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 43.05%

Importance

Changes 0
Metric Value
wmc 121
lcom 1
cbo 16
dl 0
loc 1050
ccs 130
cts 302
cp 0.4305
rs 0.9473
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 7 2
A getStatusCode() 0 4 1
B setStatusCode() 0 16 5
A withStatus() 0 10 3
A getReasonPhrase() 0 4 1
A setStatusCodeByException() 0 9 2
A getContent() 0 4 1
A setContent() 0 6 1
A clear() 0 11 1
B sendHeaders() 0 22 5
B sendCookies() 0 20 7
B sendContentAsFile() 0 28 5
B setDownloadHeaders() 0 28 6
C xSendFile() 0 34 8
A getDispositionHeaderValue() 0 11 2
A refresh() 0 4 1
A getCookies() 0 7 2
A getIsInvalid() 0 4 2
A getIsInformational() 0 4 2
A getIsSuccessful() 0 4 2
A getIsRedirection() 0 4 2
A getIsClientError() 0 4 2
A getIsServerError() 0 4 2
A getIsOk() 0 4 1
A getIsForbidden() 0 4 1
A getIsNotFound() 0 4 1
A getIsEmpty() 0 4 1
A defaultFormatters() 0 18 1
A __clone() 0 10 2
A send() 0 13 2
D sendContent() 0 38 9
A sendFile() 0 13 3
B sendStreamAsFile() 0 35 6
D getHttpRange() 0 29 9
D redirect() 0 33 10
D prepare() 0 39 10

How to fix   Complexity   

Complex Class

Complex classes like Response often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Response, and based on these observations, apply Extract Interface, too.

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 Psr\Http\Message\ResponseInterface;
11
use Yii;
12
use yii\base\InvalidArgumentException;
13
use yii\base\InvalidConfigException;
14
use yii\helpers\FileHelper;
15
use yii\helpers\Inflector;
16
use yii\helpers\StringHelper;
17
use yii\helpers\Url;
18
use yii\http\CookieCollection;
19
use yii\http\HeaderCollection;
20
use yii\http\MemoryStream;
21
use yii\http\MessageTrait;
22
use yii\http\ResourceStream;
23
24
/**
25
 * The web Response class represents an HTTP response
26
 *
27
 * It holds the [[headers]], [[cookies]] and [[content]] that is to be sent to the client.
28
 * It also controls the HTTP [[statusCode|status code]].
29
 *
30
 * Response is configured as an application component in [[\yii\web\Application]] by default.
31
 * You can access that instance via `Yii::$app->response`.
32
 *
33
 * You can modify its configuration by adding an array to your application config under `components`
34
 * as it is shown in the following example:
35
 *
36
 * ```php
37
 * 'response' => [
38
 *     'format' => yii\web\Response::FORMAT_JSON,
39
 *     'charset' => 'UTF-8',
40
 *     // ...
41
 * ]
42
 * ```
43
 *
44
 * For more details and usage information on Response, see the [guide article on responses](guide:runtime-responses).
45
 *
46
 * @property CookieCollection $cookies The cookie collection. This property is read-only.
47
 * @property string $downloadHeaders The attachment file name. This property is write-only.
48
 * @property bool $isClientError Whether this response indicates a client error. This property is read-only.
49
 * @property bool $isEmpty Whether this response is empty. This property is read-only.
50
 * @property bool $isForbidden Whether this response indicates the current request is forbidden. This property
51
 * is read-only.
52
 * @property bool $isInformational Whether this response is informational. This property is read-only.
53
 * @property bool $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only.
54
 * @property bool $isNotFound Whether this response indicates the currently requested resource is not found.
55
 * This property is read-only.
56
 * @property bool $isOk Whether this response is OK. This property is read-only.
57
 * @property bool $isRedirection Whether this response is a redirection. This property is read-only.
58
 * @property bool $isServerError Whether this response indicates a server error. This property is read-only.
59
 * @property bool $isSuccessful Whether this response is successful. This property is read-only.
60
 * @property int $statusCode The HTTP status code to send with the response.
61
 * @property \Exception|\Error $statusCodeByException The exception object. This property is write-only.
62
 * @property string $content body content string.
63
 *
64
 * @author Qiang Xue <[email protected]>
65
 * @author Carsten Brandt <[email protected]>
66
 * @since 2.0
67
 */
68
class Response extends \yii\base\Response implements ResponseInterface
69
{
70
    use MessageTrait;
71
72
    /**
73
     * @event ResponseEvent an event that is triggered at the beginning of [[send()]].
74
     */
75
    const EVENT_BEFORE_SEND = 'beforeSend';
76
    /**
77
     * @event ResponseEvent an event that is triggered at the end of [[send()]].
78
     */
79
    const EVENT_AFTER_SEND = 'afterSend';
80
    /**
81
     * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]].
82
     * You may respond to this event to filter the response content before it is sent to the client.
83
     */
84
    const EVENT_AFTER_PREPARE = 'afterPrepare';
85
    const FORMAT_RAW = 'raw';
86
    const FORMAT_HTML = 'html';
87
    const FORMAT_JSON = 'json';
88
    const FORMAT_JSONP = 'jsonp';
89
    const FORMAT_XML = 'xml';
90
91
    /**
92
     * @var string the response format. This determines how to convert [[data]] into [[content]]
93
     * when the latter is not set. The value of this property must be one of the keys declared in the [[formatters]] array.
94
     * By default, the following formats are supported:
95
     *
96
     * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion.
97
     *   No extra HTTP header will be added.
98
     * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion.
99
     *   The "Content-Type" header will set as "text/html".
100
     * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type"
101
     *   header will be set as "application/json".
102
     * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type"
103
     *   header will be set as "text/javascript". Note that in this case `$data` must be an array
104
     *   with "data" and "callback" elements. The former refers to the actual data to be sent,
105
     *   while the latter refers to the name of the JavaScript callback.
106
     * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]]
107
     *   for more details.
108
     *
109
     * You may customize the formatting process or support additional formats by configuring [[formatters]].
110
     * @see formatters
111
     */
112
    public $format = self::FORMAT_HTML;
113
    /**
114
     * @var string the MIME type (e.g. `application/json`) from the request ACCEPT header chosen for this response.
115
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
116
     */
117
    public $acceptMimeType;
118
    /**
119
     * @var array the parameters (e.g. `['q' => 1, 'version' => '1.0']`) associated with the [[acceptMimeType|chosen MIME type]].
120
     * This is a list of name-value pairs associated with [[acceptMimeType]] from the ACCEPT HTTP header.
121
     * This property is mainly set by [[\yii\filters\ContentNegotiator]].
122
     */
123
    public $acceptParams = [];
124
    /**
125
     * @var array the formatters for converting data into the response content of the specified [[format]].
126
     * The array keys are the format names, and the array values are the corresponding configurations
127
     * for creating the formatter objects.
128
     * @see format
129
     * @see defaultFormatters
130
     */
131
    public $formatters = [];
132
    /**
133
     * @var mixed the original response data. When this is not null, it will be converted into [[content]]
134
     * according to [[format]] when the response is being sent out.
135
     * @see content
136
     */
137
    public $data;
138
    /**
139
     * @var array the stream range to be applied on [[send()]]. This should be an array of the begin position and the end position.
140
     * Note that when this property is set, the [[data]] property will be ignored by [[send()]].
141
     */
142
    public $bodyRange;
143
    /**
144
     * @var string the charset of the text response. If not set, it will use
145
     * the value of [[Application::charset]].
146
     */
147
    public $charset;
148
    /**
149
     * @var string the HTTP status description that comes together with the status code.
150
     * @see httpStatuses
151
     */
152
    public $reasonPhrase = 'OK';
153
    /**
154
     * @var bool whether the response has been sent. If this is true, calling [[send()]] will do nothing.
155
     */
156
    public $isSent = false;
157
    /**
158
     * @var array list of HTTP status codes and the corresponding texts
159
     */
160
    public static $httpStatuses = [
161
        100 => 'Continue',
162
        101 => 'Switching Protocols',
163
        102 => 'Processing',
164
        118 => 'Connection timed out',
165
        200 => 'OK',
166
        201 => 'Created',
167
        202 => 'Accepted',
168
        203 => 'Non-Authoritative',
169
        204 => 'No Content',
170
        205 => 'Reset Content',
171
        206 => 'Partial Content',
172
        207 => 'Multi-Status',
173
        208 => 'Already Reported',
174
        210 => 'Content Different',
175
        226 => 'IM Used',
176
        300 => 'Multiple Choices',
177
        301 => 'Moved Permanently',
178
        302 => 'Found',
179
        303 => 'See Other',
180
        304 => 'Not Modified',
181
        305 => 'Use Proxy',
182
        306 => 'Reserved',
183
        307 => 'Temporary Redirect',
184
        308 => 'Permanent Redirect',
185
        310 => 'Too many Redirect',
186
        400 => 'Bad Request',
187
        401 => 'Unauthorized',
188
        402 => 'Payment Required',
189
        403 => 'Forbidden',
190
        404 => 'Not Found',
191
        405 => 'Method Not Allowed',
192
        406 => 'Not Acceptable',
193
        407 => 'Proxy Authentication Required',
194
        408 => 'Request Time-out',
195
        409 => 'Conflict',
196
        410 => 'Gone',
197
        411 => 'Length Required',
198
        412 => 'Precondition Failed',
199
        413 => 'Request Entity Too Large',
200
        414 => 'Request-URI Too Long',
201
        415 => 'Unsupported Media Type',
202
        416 => 'Requested range unsatisfiable',
203
        417 => 'Expectation failed',
204
        418 => 'I\'m a teapot',
205
        421 => 'Misdirected Request',
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
        451 => 'Unavailable For Legal Reasons',
217
        500 => 'Internal Server Error',
218
        501 => 'Not Implemented',
219
        502 => 'Bad Gateway or Proxy Error',
220
        503 => 'Service Unavailable',
221
        504 => 'Gateway Time-out',
222
        505 => 'HTTP Version not supported',
223
        507 => 'Insufficient storage',
224
        508 => 'Loop Detected',
225
        509 => 'Bandwidth Limit Exceeded',
226
        510 => 'Not Extended',
227
        511 => 'Network Authentication Required',
228
    ];
229
230
    /**
231
     * @var int the HTTP status code to send with the response.
232
     */
233
    private $_statusCode = 200;
234
235
236
    /**
237
     * Initializes this component.
238
     */
239 156
    public function init()
240
    {
241 156
        if ($this->charset === null) {
242 156
            $this->charset = Yii::$app->charset;
243
        }
244 156
        $this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
245 156
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 32
    public function getStatusCode()
251
    {
252 32
        return $this->_statusCode;
253
    }
254
255
    /**
256
     * Sets the response status code.
257
     * This method will set the corresponding status text if `$text` is null.
258
     * @param int $code the status code
259
     * @param string $reasonPhrase the status text. If not set, it will be set automatically based on the status code.
260
     * @throws InvalidArgumentException if the status code is invalid.
261
     * @return $this the response object itself
262
     */
263 30
    public function setStatusCode($code, $reasonPhrase = null)
264
    {
265 30
        if ($code === null) {
266
            $code = 200;
267
        }
268 30
        $this->_statusCode = (int) $code;
269 30
        if ($this->getIsInvalid()) {
270
            throw new InvalidArgumentException("The HTTP status code is invalid: $code");
271
        }
272 30
        if (empty($reasonPhrase)) {
273 29
            $this->reasonPhrase = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : '';
274
        } else {
275 1
            $this->reasonPhrase = $reasonPhrase;
276
        }
277 30
        return $this;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function withStatus($code, $reasonPhrase = '')
284
    {
285
        if ($this->getStatusCode() === $code && $this->reasonPhrase === $reasonPhrase) {
286
            return $this;
287
        }
288
289
        $newInstance = clone $this;
290
        $newInstance->setStatusCode($code, $reasonPhrase);
291
        return $newInstance;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297 13
    public function getReasonPhrase()
298
    {
299 13
        return $this->reasonPhrase;
300
    }
301
302
    /**
303
     * Sets the response status code based on the exception.
304
     * @param \Exception|\Error $e the exception object.
305
     * @throws InvalidArgumentException if the status code is invalid.
306
     * @return $this the response object itself
307
     * @since 2.0.12
308
     */
309 14
    public function setStatusCodeByException($e)
310
    {
311 14
        if ($e instanceof HttpException) {
312 7
            $this->setStatusCode($e->statusCode);
313
        } else {
314 7
            $this->setStatusCode(500);
315
        }
316 14
        return $this;
317
    }
318
319
    /**
320
     * @return string body content string.
321
     * @since 2.1.0
322
     */
323 51
    public function getContent()
324
    {
325 51
        return $this->getBody()->__toString();
326
    }
327
328
    /**
329
     * @param string $content body content string.
330
     * @since 2.1.0
331
     */
332 52
    public function setContent($content)
333
    {
334 52
        $body = new MemoryStream();
335 52
        $body->write($content);
336 52
        $this->setBody($body);
337 52
    }
338
339
    /**
340
     * Sends the response to the client.
341
     */
342 15
    public function send()
343
    {
344 15
        if ($this->isSent) {
345
            return;
346
        }
347 15
        $this->trigger(self::EVENT_BEFORE_SEND);
348 15
        $this->prepare();
349 15
        $this->trigger(self::EVENT_AFTER_PREPARE);
350 15
        $this->sendHeaders();
351 15
        $this->sendContent();
352 15
        $this->trigger(self::EVENT_AFTER_SEND);
353 15
        $this->isSent = true;
354 15
    }
355
356
    /**
357
     * Clears the headers, cookies, content, status code of the response.
358
     */
359
    public function clear()
360
    {
361
        $this->_headerCollection = null;
362
        $this->_cookies = null;
363
        $this->_statusCode = 200;
364
        $this->reasonPhrase = 'OK';
365
        $this->data = null;
366
        $this->bodyRange = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $bodyRange.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
367
        $this->isSent = false;
368
        $this->setBody(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<Psr\Http\Message\...>|object<Closure>|array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
369
    }
370
371
    /**
372
     * Sends the response headers to the client
373
     */
374 15
    protected function sendHeaders()
375
    {
376 15
        if (headers_sent()) {
377 15
            return;
378
        }
379
        if ($this->_headerCollection) {
380
            $headers = $this->getHeaders();
381
            foreach ($headers as $name => $values) {
382
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
383
                // set replace for first occurrence of header but false afterwards to allow multiple
384
                $replace = true;
385
                foreach ($values as $value) {
386
                    header("$name: $value", $replace);
387
                    $replace = false;
388
                }
389
            }
390
        }
391
        $statusCode = $this->getStatusCode();
392
        $protocolVersion = $this->getProtocolVersion();
393
        header("HTTP/{$protocolVersion} {$statusCode} {$this->reasonPhrase}");
394
        $this->sendCookies();
395
    }
396
397
    /**
398
     * Sends the cookies to the client.
399
     */
400
    protected function sendCookies()
401
    {
402
        if ($this->_cookies === null) {
403
            return;
404
        }
405
        $request = Yii::$app->getRequest();
406
        if ($request->enableCookieValidation) {
407
            if ($request->cookieValidationKey == '') {
408
                throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
409
            }
410
            $validationKey = $request->cookieValidationKey;
411
        }
412
        foreach ($this->getCookies() as $cookie) {
0 ignored issues
show
Bug introduced by
The expression $this->getCookies() of type object<yii\http\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...
413
            $value = $cookie->value;
414
            if ($cookie->expire != 1 && isset($validationKey)) {
415
                $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
416
            }
417
            setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
418
        }
419
    }
420
421
    /**
422
     * Sends the response content to the client
423
     */
424 15
    protected function sendContent()
425
    {
426 15
        $body = $this->getBody();
427 15
        if (!$body->isReadable()) {
428
            throw new \RuntimeException('Unable to send content: body stream is not readable.');
429
        }
430
431 15
        set_time_limit(0); // Reset time limit for big files
432 15
        $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
433
434 15
        if (is_array($this->bodyRange)) {
435
            [$begin, $end] = $this->bodyRange;
0 ignored issues
show
Bug introduced by
The variable $begin does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $end does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
436
437
            if (!$body->isSeekable()) {
438
                throw new \RuntimeException('Unable to send content in range: body stream is not seekable.');
439
            }
440
441
            $body->seek($begin);
442
            while (!$body->eof() && ($pos = $body->tell()) <= $end) {
443
                if ($pos + $chunkSize > $end) {
444
                    $chunkSize = $end - $pos + 1;
445
                }
446
                echo $body->read($chunkSize);
447
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
448
            }
449
            $body->close();
450
        } else {
451 15
            if ($body->isSeekable()) {
452 15
                $body->seek(0);
453
            }
454 15
            while (!$body->eof()) {
455 15
                echo $body->read($chunkSize);
456 15
                flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
457
            }
458 15
            $body->close();
459 15
            return;
460
        }
461
    }
462
463
    /**
464
     * Sends a file to the browser.
465
     *
466
     * Note that this method only prepares the response for file sending. The file is not sent
467
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
468
     *
469
     * The following is an example implementation of a controller action that allows requesting files from a directory
470
     * that is not accessible from web:
471
     *
472
     * ```php
473
     * public function actionFile($filename)
474
     * {
475
     *     $storagePath = Yii::getAlias('@app/files');
476
     *
477
     *     // check filename for allowed chars (do not allow ../ to avoid security issue: downloading arbitrary files)
478
     *     if (!preg_match('/^[a-z0-9]+\.[a-z0-9]+$/i', $filename) || !is_file("$storagePath/$filename")) {
479
     *         throw new \yii\web\NotFoundHttpException('The file does not exists.');
480
     *     }
481
     *     return Yii::$app->response->sendFile("$storagePath/$filename", $filename);
482
     * }
483
     * ```
484
     *
485
     * @param string $filePath the path of the file to be sent.
486
     * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
487
     * @param array $options additional options for sending the file. The following options are supported:
488
     *
489
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
490
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
491
     *    meaning a download dialog will pop up.
492
     *
493
     * @return $this the response object itself
494
     * @see sendContentAsFile()
495
     * @see sendStreamAsFile()
496
     * @see xSendFile()
497
     */
498
    public function sendFile($filePath, $attachmentName = null, $options = [])
499
    {
500
        if (!isset($options['mimeType'])) {
501
            $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);
502
        }
503
        if ($attachmentName === null) {
504
            $attachmentName = basename($filePath);
505
        }
506
        $handle = fopen($filePath, 'rb');
507
        $this->sendStreamAsFile($handle, $attachmentName, $options);
508
509
        return $this;
510
    }
511
512
    /**
513
     * Sends the specified content as a file to the browser.
514
     *
515
     * Note that this method only prepares the response for file sending. The file is not sent
516
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
517
     *
518
     * @param string $content the content to be sent. The existing [[content]] will be discarded.
519
     * @param string $attachmentName the file name shown to the user.
520
     * @param array $options additional options for sending the file. The following options are supported:
521
     *
522
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
523
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
524
     *    meaning a download dialog will pop up.
525
     *
526
     * @return $this the response object itself
527
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
528
     * @see sendFile() for an example implementation.
529
     */
530 1
    public function sendContentAsFile($content, $attachmentName, $options = [])
531
    {
532 1
        $contentLength = StringHelper::byteLength($content);
533 1
        $range = $this->getHttpRange($contentLength);
534
535 1
        if ($range === false) {
536
            $this->setHeader('Content-Range', "bytes */$contentLength");
537
            throw new RangeNotSatisfiableHttpException();
538
        }
539
540 1
        [$begin, $end] = $range;
0 ignored issues
show
Bug introduced by
The variable $begin does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $end does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
541 1
        $body = new MemoryStream();
542 1
        if ($begin != 0 || $end != $contentLength - 1) {
543
            $this->setStatusCode(206);
544
            $this->setHeader('Content-Range', "bytes $begin-$end/$contentLength");
545
            $body->write(StringHelper::byteSubstr($content, $begin, $end - $begin + 1));
546
        } else {
547 1
            $this->setStatusCode(200);
548 1
            $body->write($content);
549
        }
550
551 1
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
552 1
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
553
554 1
        $this->format = self::FORMAT_RAW;
555 1
        $this->setBody($body);
556 1
        return $this;
557
    }
558
559
    /**
560
     * Sends the specified stream as a file to the browser.
561
     *
562
     * Note that this method only prepares the response for file sending. The file is not sent
563
     * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action.
564
     *
565
     * @param resource $handle the handle of the stream to be sent.
566
     * @param string $attachmentName the file name shown to the user.
567
     * @param array $options additional options for sending the file. The following options are supported:
568
     *
569
     *  - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'.
570
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
571
     *    meaning a download dialog will pop up.
572
     *  - `fileSize`: the size of the content to stream this is useful when size of the content is known
573
     *    and the content is not seekable. Defaults to content size using `ftell()`.
574
     *    This option is available since version 2.0.4.
575
     *
576
     * @return $this the response object itself
577
     * @throws RangeNotSatisfiableHttpException if the requested range is not satisfiable
578
     * @see sendFile() for an example implementation.
579
     */
580
    public function sendStreamAsFile($handle, $attachmentName, $options = [])
581
    {
582
        if (isset($options['fileSize'])) {
583
            $fileSize = $options['fileSize'];
584
        } else {
585
            fseek($handle, 0, SEEK_END);
586
            $fileSize = ftell($handle);
587
        }
588
589
        $range = $this->getHttpRange($fileSize);
590
        if ($range === false) {
591
            $this->setHeader('Content-Range', "bytes */$fileSize");
592
            throw new RangeNotSatisfiableHttpException();
593
        }
594
595
        [$begin, $end] = $range;
0 ignored issues
show
Bug introduced by
The variable $begin does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $end does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
596
        if ($begin != 0 || $end != $fileSize - 1) {
597
            $this->setStatusCode(206);
598
            $this->setHeader('Content-Range', "bytes $begin-$end/$fileSize");
599
        } else {
600
            $this->setStatusCode(200);
601
        }
602
603
        $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream';
604
        $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1);
605
606
        $this->format = self::FORMAT_RAW;
607
        $this->bodyRange = [$begin, $end];
608
609
        $body = new ResourceStream();
610
        $body->resource = $handle;
611
612
        $this->setBody($body);
613
        return $this;
614
    }
615
616
    /**
617
     * Sets a default set of HTTP headers for file downloading purpose.
618
     * @param string $attachmentName the attachment file name
619
     * @param string $mimeType the MIME type for the response. If null, `Content-Type` header will NOT be set.
620
     * @param bool $inline whether the browser should open the file within the browser window. Defaults to false,
621
     * meaning a download dialog will pop up.
622
     * @param int $contentLength the byte length of the file being downloaded. If null, `Content-Length` header will NOT be set.
623
     * @return $this the response object itself
624
     */
625 1
    public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null)
626
    {
627 1
        $disposition = $inline ? 'inline' : 'attachment';
628
629
        $headers = [
630 1
            'Pragma' => 'public',
631 1
            'Accept-Ranges' => 'bytes',
632 1
            'Expires' => '0',
633 1
            'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
634 1
            'Content-Disposition' => $this->getDispositionHeaderValue($disposition, $attachmentName),
635
        ];
636
637 1
        if ($mimeType !== null) {
638 1
            $headers['Content-Type'] = $mimeType;
639
        }
640
641 1
        if ($contentLength !== null) {
642 1
            $headers['Content-Length'] = $contentLength;
643
        }
644
645 1
        foreach ($headers as $name => $value) {
646 1
            if (!$this->hasHeader($name)) {
647 1
                $this->setHeader($name, $value);
648
            }
649
        }
650
651 1
        return $this;
652
    }
653
654
    /**
655
     * Determines the HTTP range given in the request.
656
     * @param int $fileSize the size of the file that will be used to validate the requested HTTP range.
657
     * @return array|bool the range (begin, end), or false if the range request is invalid.
658
     */
659 1
    protected function getHttpRange($fileSize)
660
    {
661 1
        $rangeHeader = Yii::$app->getRequest()->getHeaderLine('Range');
662
663 1
        if (empty($rangeHeader) || $rangeHeader === '-') {
664 1
            return [0, $fileSize - 1];
665
        }
666
        if (!preg_match('/^bytes=(\d*)-(\d*)$/', $rangeHeader, $matches)) {
667
            return false;
668
        }
669
        if ($matches[1] === '') {
670
            $start = $fileSize - $matches[2];
671
            $end = $fileSize - 1;
672
        } elseif ($matches[2] !== '') {
673
            $start = $matches[1];
674
            $end = $matches[2];
675
            if ($end >= $fileSize) {
676
                $end = $fileSize - 1;
677
            }
678
        } else {
679
            $start = $matches[1];
680
            $end = $fileSize - 1;
681
        }
682
        if ($start < 0 || $start > $end) {
683
            return false;
684
        }
685
686
        return [$start, $end];
687
    }
688
689
    /**
690
     * Sends existing file to a browser as a download using x-sendfile.
691
     *
692
     * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
693
     * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
694
     * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
695
     * increase in performance as the web application is allowed to terminate earlier while the webserver is
696
     * handling the request.
697
     *
698
     * The request is sent to the server through a special non-standard HTTP-header.
699
     * When the web server encounters the presence of such header it will discard all output and send the file
700
     * specified by that header using web server internals including all optimizations like caching-headers.
701
     *
702
     * As this header directive is non-standard different directives exists for different web servers applications:
703
     *
704
     * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
705
     * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
706
     * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
707
     * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
708
     * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
709
     *
710
     * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
711
     * a proper xHeader should be sent.
712
     *
713
     * **Note**
714
     *
715
     * This option allows to download files that are not under web folders, and even files that are otherwise protected
716
     * (deny from all) like `.htaccess`.
717
     *
718
     * **Side effects**
719
     *
720
     * If this option is disabled by the web server, when this method is called a download configuration dialog
721
     * will open but the downloaded file will have 0 bytes.
722
     *
723
     * **Known issues**
724
     *
725
     * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
726
     * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
727
     * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
728
     *
729
     * **Example**
730
     *
731
     * ```php
732
     * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg');
733
     * ```
734
     *
735
     * @param string $filePath file name with full path
736
     * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
737
     * @param array $options additional options for sending the file. The following options are supported:
738
     *
739
     *  - `mimeType`: the MIME type of the content. If not set, it will be guessed based on `$filePath`
740
     *  - `inline`: boolean, whether the browser should open the file within the browser window. Defaults to false,
741
     *    meaning a download dialog will pop up.
742
     *  - xHeader: string, the name of the x-sendfile header. Defaults to "X-Sendfile".
743
     *
744
     * @return $this the response object itself
745
     * @see sendFile()
746
     */
747
    public function xSendFile($filePath, $attachmentName = null, $options = [])
748
    {
749
        if ($attachmentName === null) {
750
            $attachmentName = basename($filePath);
751
        }
752
        if (isset($options['mimeType'])) {
753
            $mimeType = $options['mimeType'];
754
        } elseif (($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
755
            $mimeType = 'application/octet-stream';
756
        }
757
        if (isset($options['xHeader'])) {
758
            $xHeader = $options['xHeader'];
759
        } else {
760
            $xHeader = 'X-Sendfile';
761
        }
762
763
        $disposition = empty($options['inline']) ? 'attachment' : 'inline';
764
765
        $headers = [
766
            $xHeader => $filePath,
767
            'Content-Type' => $mimeType,
768
            'Content-Disposition' => $this->getDispositionHeaderValue($disposition, $attachmentName),
769
        ];
770
771
        foreach ($headers as $name => $value) {
772
            if (!$this->hasHeader($name)) {
773
                $this->setHeader($name, $value);
774
            }
775
        }
776
777
        $this->format = self::FORMAT_RAW;
778
779
        return $this;
780
    }
781
782
    /**
783
     * Returns Content-Disposition header value that is safe to use with both old and new browsers
784
     *
785
     * Fallback name:
786
     *
787
     * - Causes issues if contains non-ASCII characters with codes less than 32 or more than 126.
788
     * - Causes issues if contains urlencoded characters (starting with `%`) or `%` character. Some browsers interpret
789
     *   `filename="X"` as urlencoded name, some don't.
790
     * - Causes issues if contains path separator characters such as `\` or `/`.
791
     * - Since value is wrapped with `"`, it should be escaped as `\"`.
792
     * - Since input could contain non-ASCII characters, fallback is obtained by transliteration.
793
     *
794
     * UTF name:
795
     *
796
     * - Causes issues if contains path separator characters such as `\` or `/`.
797
     * - Should be urlencoded since headers are ASCII-only.
798
     * - Could be omitted if it exactly matches fallback name.
799
     *
800
     * @param string $disposition
801
     * @param string $attachmentName
802
     * @return string
803
     *
804
     * @since 2.0.10
805
     */
806 1
    protected function getDispositionHeaderValue($disposition, $attachmentName)
807
    {
808 1
        $fallbackName = str_replace('"', '\\"', str_replace(['%', '/', '\\'], '_', Inflector::transliterate($attachmentName, Inflector::TRANSLITERATE_LOOSE)));
809 1
        $utfName = rawurlencode(str_replace(['%', '/', '\\'], '', $attachmentName));
810
811 1
        $dispositionHeader = "{$disposition}; filename=\"{$fallbackName}\"";
812 1
        if ($utfName !== $fallbackName) {
813
            $dispositionHeader .= "; filename*=utf-8''{$utfName}";
814
        }
815 1
        return $dispositionHeader;
816
    }
817
818
    /**
819
     * Redirects the browser to the specified URL.
820
     *
821
     * This method adds a "Location" header to the current response. Note that it does not send out
822
     * the header until [[send()]] is called. In a controller action you may use this method as follows:
823
     *
824
     * ```php
825
     * return Yii::$app->getResponse()->redirect($url);
826
     * ```
827
     *
828
     * In other places, if you want to send out the "Location" header immediately, you should use
829
     * the following code:
830
     *
831
     * ```php
832
     * Yii::$app->getResponse()->redirect($url)->send();
833
     * return;
834
     * ```
835
     *
836
     * In AJAX mode, this normally will not work as expected unless there are some
837
     * client-side JavaScript code handling the redirection. To help achieve this goal,
838
     * this method will send out a "X-Redirect" header instead of "Location".
839
     *
840
     * If you use the "yii" JavaScript module, it will handle the AJAX redirection as
841
     * described above. Otherwise, you should write the following JavaScript code to
842
     * handle the redirection:
843
     *
844
     * ```javascript
845
     * $document.ajaxComplete(function (event, xhr, settings) {
846
     *     var url = xhr && xhr.getResponseHeader('X-Redirect');
847
     *     if (url) {
848
     *         window.location = url;
849
     *     }
850
     * });
851
     * ```
852
     *
853
     * @param string|array $url the URL to be redirected to. This can be in one of the following formats:
854
     *
855
     * - a string representing a URL (e.g. "http://example.com")
856
     * - a string representing a URL alias (e.g. "@example.com")
857
     * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`).
858
     *   Note that the route is with respect to the whole application, instead of relative to a controller or module.
859
     *   [[Url::to()]] will be used to convert the array into a URL.
860
     *
861
     * Any relative URL that starts with a single forward slash "/" will be converted
862
     * into an absolute one by prepending it with the host info of the current request.
863
     *
864
     * @param int $statusCode the HTTP status code. Defaults to 302.
865
     * See <https://tools.ietf.org/html/rfc2616#section-10>
866
     * for details about HTTP status code
867
     * @param bool $checkAjax whether to specially handle AJAX (and PJAX) requests. Defaults to true,
868
     * meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser
869
     * to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as
870
     * an AJAX/PJAX response, may NOT cause browser redirection.
871
     * Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent.
872
     * @return $this the response object itself
873
     */
874 3
    public function redirect($url, $statusCode = 302, $checkAjax = true)
875
    {
876 3
        if (is_array($url) && isset($url[0])) {
877
            // ensure the route is absolute
878 2
            $url[0] = '/' . ltrim($url[0], '/');
879
        }
880 3
        $url = Url::to($url);
881 3
        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
882 3
            $url = Yii::$app->getRequest()->getHostInfo() . $url;
883
        }
884
885 3
        if ($checkAjax) {
886 3
            if (Yii::$app->getRequest()->getIsAjax()) {
887
                if (Yii::$app->getRequest()->hasHeader('X-Ie-Redirect-Compatibility') && $statusCode === 302) {
888
                    // Ajax 302 redirect in IE does not work. Change status code to 200. See https://github.com/yiisoft/yii2/issues/9670
889
                    $statusCode = 200;
890
                }
891
                if (Yii::$app->getRequest()->getIsPjax()) {
892
                    $this->setHeader('X-Pjax-Url', $url);
893
                } else {
894
                    $this->setHeader('X-Redirect', $url);
895
                }
896
            } else {
897 3
                $this->setHeader('Location', $url);
898
            }
899
        } else {
900
            $this->setHeader('Location', $url);
901
        }
902
903 3
        $this->setStatusCode($statusCode);
904
905 3
        return $this;
906
    }
907
908
    /**
909
     * Refreshes the current page.
910
     * The effect of this method call is the same as the user pressing the refresh button of his browser
911
     * (without re-posting data).
912
     *
913
     * In a controller action you may use this method like this:
914
     *
915
     * ```php
916
     * return Yii::$app->getResponse()->refresh();
917
     * ```
918
     *
919
     * @param string $anchor the anchor that should be appended to the redirection URL.
920
     * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
921
     * @return Response the response object itself
922
     */
923
    public function refresh($anchor = '')
924
    {
925
        return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor);
926
    }
927
928
    private $_cookies;
929
930
    /**
931
     * Returns the cookie collection.
932
     * Through the returned cookie collection, you add or remove cookies as follows,
933
     *
934
     * ```php
935
     * // add a cookie
936
     * $response->cookies->add(new Cookie([
937
     *     'name' => $name,
938
     *     'value' => $value,
939
     * ]);
940
     *
941
     * // remove a cookie
942
     * $response->cookies->remove('name');
943
     * // alternatively
944
     * unset($response->cookies['name']);
945
     * ```
946
     *
947
     * @return CookieCollection the cookie collection.
948
     */
949 36
    public function getCookies()
950
    {
951 36
        if ($this->_cookies === null) {
952 36
            $this->_cookies = new CookieCollection();
953
        }
954 36
        return $this->_cookies;
955
    }
956
957
    /**
958
     * @return bool whether this response has a valid [[statusCode]].
959
     */
960 30
    public function getIsInvalid()
961
    {
962 30
        return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
963
    }
964
965
    /**
966
     * @return bool whether this response is informational
967
     */
968
    public function getIsInformational()
969
    {
970
        return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200;
971
    }
972
973
    /**
974
     * @return bool whether this response is successful
975
     */
976
    public function getIsSuccessful()
977
    {
978
        return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
979
    }
980
981
    /**
982
     * @return bool whether this response is a redirection
983
     */
984 1
    public function getIsRedirection()
985
    {
986 1
        return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
987
    }
988
989
    /**
990
     * @return bool whether this response indicates a client error
991
     */
992
    public function getIsClientError()
993
    {
994
        return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
995
    }
996
997
    /**
998
     * @return bool whether this response indicates a server error
999
     */
1000
    public function getIsServerError()
1001
    {
1002
        return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
1003
    }
1004
1005
    /**
1006
     * @return bool whether this response is OK
1007
     */
1008
    public function getIsOk()
1009
    {
1010
        return $this->getStatusCode() == 200;
1011
    }
1012
1013
    /**
1014
     * @return bool whether this response indicates the current request is forbidden
1015
     */
1016
    public function getIsForbidden()
1017
    {
1018
        return $this->getStatusCode() == 403;
1019
    }
1020
1021
    /**
1022
     * @return bool whether this response indicates the currently requested resource is not found
1023
     */
1024
    public function getIsNotFound()
1025
    {
1026
        return $this->getStatusCode() == 404;
1027
    }
1028
1029
    /**
1030
     * @return bool whether this response is empty
1031
     */
1032
    public function getIsEmpty()
1033
    {
1034
        return in_array($this->getStatusCode(), [201, 204, 304]);
1035
    }
1036
1037
    /**
1038
     * @return array the formatters that are supported by default
1039
     */
1040 156
    protected function defaultFormatters()
1041
    {
1042
        return [
1043 156
            self::FORMAT_HTML => [
1044
                'class' => HtmlResponseFormatter::class,
1045
            ],
1046 156
            self::FORMAT_XML => [
1047
                'class' => XmlResponseFormatter::class,
1048
            ],
1049 156
            self::FORMAT_JSON => [
1050
                'class' => JsonResponseFormatter::class,
1051
            ],
1052 156
            self::FORMAT_JSONP => [
1053
                'class' => JsonResponseFormatter::class,
1054
                'useJsonp' => true,
1055
            ],
1056
        ];
1057
    }
1058
1059
    /**
1060
     * Prepares for sending the response.
1061
     * The default implementation will convert [[data]] into [[content]] and set headers accordingly.
1062
     * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported
1063
     */
1064 15
    protected function prepare()
1065
    {
1066 15
        if ($this->bodyRange !== null) {
1067
            return;
1068
        }
1069
1070 15
        if (isset($this->formatters[$this->format])) {
1071 14
            $formatter = $this->formatters[$this->format];
1072 14
            if (!is_object($formatter)) {
1073 14
                $this->formatters[$this->format] = $formatter = Yii::createObject($formatter);
1074
            }
1075 14
            if ($formatter instanceof ResponseFormatterInterface) {
1076 14
                $formatter->format($this);
1077 14
                return;
1078
            }
1079
            throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface.");
1080 1
        } elseif ($this->format !== self::FORMAT_RAW) {
1081
            throw new InvalidConfigException("Unsupported response format: {$this->format}");
1082
        }
1083
1084 1
        if ($this->data !== null) {
1085
            if (is_array($this->data)) {
1086
                throw new InvalidArgumentException('Response raw data must not be an array.');
1087
            } elseif (is_object($this->data)) {
1088
                if (method_exists($this->data, '__toString')) {
1089
                    $content = $this->data->__toString();
1090
                } else {
1091
                    throw new InvalidArgumentException('Response raw data must be a string or an object implementing '
1092
                        . ' __toString().');
1093
                }
1094
            } else {
1095
                $content = $this->data;
1096
            }
1097
1098
            $body = new MemoryStream();
1099
            $body->write($content);
1100
            $this->setBody($body);
1101
        }
1102 1
    }
1103
1104
    /**
1105
     * {@inheritdoc}
1106
     */
1107
    public function __clone()
1108
    {
1109
        parent::__clone();
1110
1111
        $this->cloneHttpMessageInternals();
1112
1113
        if (is_object($this->_cookies)) {
1114
            $this->_cookies = clone $this->_cookies;
1115
        }
1116
    }
1117
}
1118