Test Failed
Pull Request — master (#20371)
by Alexander
18:51 queued 10:32
created

Request::getAcceptableLanguages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 0
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 12
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\validators\IpValidator;
13
14
/**
15
 * The web Request class represents an HTTP request.
16
 *
17
 * It encapsulates the $_SERVER variable and resolves its inconsistency among different Web servers.
18
 * Also it provides an interface to retrieve request parameters from $_POST, $_GET, $_COOKIES and REST
19
 * parameters sent via other HTTP methods like PUT or DELETE.
20
 *
21
 * Request is configured as an application component in [[\yii\web\Application]] by default.
22
 * You can access that instance via `Yii::$app->request`.
23
 *
24
 * For more details and usage information on Request, see the [guide article on requests](guide:runtime-requests).
25
 *
26
 * @property-read string $absoluteUrl The currently requested absolute URL.
27
 * @property array $acceptableContentTypes The content types ordered by the quality score. Types with the
28
 * highest scores will be returned first. The array keys are the content types, while the array values are the
29
 * corresponding quality score and other parameters as given in the header.
30
 * @property array $acceptableLanguages The languages ordered by the preference level. The first element
31
 * represents the most preferred language.
32
 * @property-read array $authCredentials That contains exactly two elements: - 0: the username sent via HTTP
33
 * authentication, `null` if the username is not given - 1: the password sent via HTTP authentication, `null` if
34
 * the password is not given.
35
 * @property-read string|null $authPassword The password sent via HTTP authentication, `null` if the password
36
 * is not given.
37
 * @property-read string|null $authUser The username sent via HTTP authentication, `null` if the username is
38
 * not given.
39
 * @property string $baseUrl The relative URL for the application.
40
 * @property array|object $bodyParams The request parameters given in the request body.
41
 * @property-read string $contentType Request content-type. Empty string is returned if this information is
42
 * not available.
43
 * @property-read CookieCollection $cookies The cookie collection.
44
 * @property-read null|string $csrfToken The token used to perform CSRF validation. Null is returned if the
45
 * [[validateCsrfHeaderOnly]] is true.
46
 * @property-read string|null $csrfTokenFromHeader The CSRF token sent via [[csrfHeader]] by browser. Null is
47
 * returned if no such header is sent.
48
 * @property-read array $eTags The entity tags.
49
 * @property-read HeaderCollection $headers The header collection.
50
 * @property string|null $hostInfo Schema and hostname part (with port number if needed) of the request URL
51
 * (e.g. `https://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set. See
52
 * [[getHostInfo()]] for security related notes on this property.
53
 * @property-read string|null $hostName Hostname part of the request URL (e.g. `www.yiiframework.com`).
54
 * @property-read bool $isAjax Whether this is an AJAX (XMLHttpRequest) request.
55
 * @property-read bool $isDelete Whether this is a DELETE request.
56
 * @property-read bool $isFlash Whether this is an Adobe Flash or Adobe Flex request.
57
 * @property-read bool $isGet Whether this is a GET request.
58
 * @property-read bool $isHead Whether this is a HEAD request.
59
 * @property-read bool $isOptions Whether this is a OPTIONS request.
60
 * @property-read bool $isPatch Whether this is a PATCH request.
61
 * @property-read bool $isPjax Whether this is a PJAX request.
62
 * @property-read bool $isPost Whether this is a POST request.
63
 * @property-read bool $isPut Whether this is a PUT request.
64
 * @property-read bool $isSecureConnection If the request is sent via secure channel (https).
65
 * @property-read string $method Request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. The value
66
 * returned is turned into upper case.
67
 * @property-read string|null $origin URL origin of a CORS request, `null` if not available.
68
 * @property string $pathInfo Part of the request URL that is after the entry script and before the question
69
 * mark. Note, the returned path info is already URL-decoded.
70
 * @property int $port Port number for insecure requests.
71
 * @property array $queryParams The request GET parameter values.
72
 * @property-read string $queryString Part of the request URL that is after the question mark.
73
 * @property string $rawBody The request body.
74
 * @property-read string|null $referrer URL referrer, null if not available.
75
 * @property-read string|null $remoteHost Remote host name, `null` if not available.
76
 * @property-read string|null $remoteIP Remote IP address, `null` if not available.
77
 * @property string $scriptFile The entry script file path.
78
 * @property string $scriptUrl The relative URL of the entry script.
79
 * @property int $securePort Port number for secure requests.
80
 * @property-read string|null $serverName Server name, null if not available.
81
 * @property-read int|null $serverPort Server port number, null if not available.
82
 * @property string $url The currently requested relative URL. Note that the URI returned may be URL-encoded
83
 * depending on the client.
84
 * @property-read string|null $userAgent User agent, null if not available.
85
 * @property-read string|null $userHost User host name, null if not available.
86
 * @property-read string|null $userIP User IP address, null if not available.
87
 *
88
 * @author Qiang Xue <[email protected]>
89
 * @since 2.0
90
 * @SuppressWarnings(PHPMD.SuperGlobals)
91
 */
92
class Request extends \yii\base\Request
93
{
94
    /**
95
     * Default name of the HTTP header for sending CSRF token.
96
     */
97
    const CSRF_HEADER = 'X-CSRF-Token';
98
    /**
99
     * The length of the CSRF token mask.
100
     * @deprecated since 2.0.12. The mask length is now equal to the token length.
101
     */
102
    const CSRF_MASK_LENGTH = 8;
103
104
    /**
105
     * @var bool whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to true.
106
     * When CSRF validation is enabled, forms submitted to an Yii Web application must be originated
107
     * from the same application. If not, a 400 HTTP exception will be raised.
108
     *
109
     * Note, this feature requires that the user client accepts cookie. Also, to use this feature,
110
     * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfParam]].
111
     * You may use [[\yii\helpers\Html::beginForm()]] to generate his hidden input.
112
     *
113
     * In JavaScript, you may get the values of [[csrfParam]] and [[csrfToken]] via `yii.getCsrfParam()` and
114
     * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered.
115
     * You also need to include CSRF meta tags in your pages by using [[\yii\helpers\Html::csrfMetaTags()]].
116
     *
117
     * For SPA, you can use CSRF validation by custom header with a random or an empty value.
118
     * Include a header with the name specified by [[csrfHeader]] to requests that must be validated.
119
     * Warning! CSRF validation by custom header can be used only for same-origin requests or
120
     * with CORS configured to allow requests from the list of specific origins only.
121
     *
122
     * @see Controller::enableCsrfValidation
123
     * @see https://en.wikipedia.org/wiki/Cross-site_request_forgery
124
     */
125
    public $enableCsrfValidation = true;
126
    /**
127
     * @var string the name of the HTTP header for sending CSRF token. Defaults to [[CSRF_HEADER]].
128
     * This property may be changed for Yii API applications only.
129
     * Don't change this property for Yii Web application.
130
     */
131
    public $csrfHeader = self::CSRF_HEADER;
132
    /**
133
     * @var array the name of the HTTP header for sending CSRF token.
134
     * by default validate CSRF token on non-"safe" methods only
135
     * This property is used only when [[enableCsrfValidation]] is true.
136
     * @see https://datatracker.ietf.org/doc/html/rfc9110#name-safe-methods
137
     */
138
    public $csrfTokenSafeMethods = ['GET', 'HEAD', 'OPTIONS'];
139
    /**
140
     * @var array "unsafe" methods not triggered a CORS-preflight request
141
     * This property is used only when both [[enableCsrfValidation]] and [[validateCsrfHeaderOnly]] are true.
142
     * @see https://fetch.spec.whatwg.org/#http-cors-protocol
143
     */
144
    public $csrfHeaderUnsafeMethods = ['GET', 'HEAD', 'POST'];
145
    /**
146
     * @var bool whether to use custom header only to CSRF validation of SPA. Defaults to false.
147
     * If false and [[enableCsrfValidation]] is true, CSRF validation by token will used.
148
     * Warning! CSRF validation by custom header can be used for Yii API applications only.
149
     * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi
150
     */
151
    public $validateCsrfHeaderOnly = false;
152
    /**
153
     * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'.
154
     * This property is used only when [[enableCsrfValidation]] is true.
155
     */
156
    public $csrfParam = '_csrf';
157
    /**
158
     * @var array the configuration for creating the CSRF [[Cookie|cookie]]. This property is used only when
159
     * both [[enableCsrfValidation]] and [[enableCsrfCookie]] are true.
160
     */
161
    public $csrfCookie = ['httpOnly' => true];
162
    /**
163
     * @var bool whether to use cookie to persist CSRF token. If false, CSRF token will be stored
164
     * in session under the name of [[csrfParam]]. Note that while storing CSRF tokens in session increases
165
     * security, it requires starting a session for every page, which will degrade your site performance.
166
     */
167
    public $enableCsrfCookie = true;
168
    /**
169
     * @var bool whether cookies should be validated to ensure they are not tampered. Defaults to true.
170
     */
171
    public $enableCookieValidation = true;
172
    /**
173
     * @var string a secret key used for cookie validation. This property must be set if [[enableCookieValidation]] is true.
174
     */
175
    public $cookieValidationKey;
176
    /**
177
     * @var string the name of the POST parameter that is used to indicate if a request is a PUT, PATCH or DELETE
178
     * request tunneled through POST. Defaults to '_method'.
179
     * @see getMethod()
180
     * @see getBodyParams()
181
     */
182
    public $methodParam = '_method';
183
    /**
184
     * @var array the parsers for converting the raw HTTP request body into [[bodyParams]].
185
     * The array keys are the request `Content-Types`, and the array values are the
186
     * corresponding configurations for [[Yii::createObject|creating the parser objects]].
187
     * A parser must implement the [[RequestParserInterface]].
188
     *
189
     * To enable parsing for JSON requests you can use the [[JsonParser]] class like in the following example:
190
     *
191
     * ```
192
     * [
193
     *     'application/json' => 'yii\web\JsonParser',
194
     * ]
195
     * ```
196
     *
197
     * To register a parser for parsing all request types you can use `'*'` as the array key.
198
     * This one will be used as a fallback in case no other types match.
199
     *
200
     * @see getBodyParams()
201
     */
202
    public $parsers = [];
203
    /**
204
     * @var array the configuration for trusted security related headers.
205
     *
206
     * An array key is an IPv4 or IPv6 IP address in CIDR notation for matching a client.
207
     *
208
     * An array value is a list of headers to trust. These will be matched against
209
     * [[secureHeaders]] to determine which headers are allowed to be sent by a specified host.
210
     * The case of the header names must be the same as specified in [[secureHeaders]].
211
     *
212
     * For example, to trust all headers listed in [[secureHeaders]] for IP addresses
213
     * in range `192.168.0.0-192.168.0.254` write the following:
214
     *
215
     * ```php
216
     * [
217
     *     '192.168.0.0/24',
218
     * ]
219
     * ```
220
     *
221
     * To trust just the `X-Forwarded-For` header from `10.0.0.1`, use:
222
     *
223
     * ```
224
     * [
225
     *     '10.0.0.1' => ['X-Forwarded-For']
226
     * ]
227
     * ```
228
     *
229
     * Default is to trust all headers except those listed in [[secureHeaders]] from all hosts.
230
     * Matches are tried in order and searching is stopped when IP matches.
231
     *
232
     * > Info: Matching is performed using [[IpValidator]].
233
     * See [[IpValidator::::setRanges()|IpValidator::setRanges()]]
234
     * and [[IpValidator::networks]] for advanced matching.
235
     *
236
     * @see secureHeaders
237
     * @since 2.0.13
238
     */
239
    public $trustedHosts = [];
240
    /**
241
     * @var array lists of headers that are, by default, subject to the trusted host configuration.
242
     * These headers will be filtered unless explicitly allowed in [[trustedHosts]].
243
     * If the list contains the `Forwarded` header, processing will be done according to RFC 7239.
244
     * The match of header names is case-insensitive.
245
     * @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
246
     * @see https://datatracker.ietf.org/doc/html/rfc7239
247
     * @see trustedHosts
248
     * @since 2.0.13
249
     */
250
    public $secureHeaders = [
251
        // Common:
252
        'X-Forwarded-For',
253
        'X-Forwarded-Host',
254
        'X-Forwarded-Proto',
255
        'X-Forwarded-Port',
256
257
        // Microsoft:
258
        'Front-End-Https',
259
        'X-Rewrite-Url',
260
261
        // ngrok:
262
        'X-Original-Host',
263
    ];
264
    /**
265
     * @var string[] List of headers where proxies store the real client IP.
266
     * It's not advisable to put insecure headers here.
267
     * To use the `Forwarded` header according to RFC 7239, the header must be added to [[secureHeaders]] list.
268
     * The match of header names is case-insensitive.
269
     * @see trustedHosts
270
     * @see secureHeaders
271
     * @since 2.0.13
272
     */
273
    public $ipHeaders = [
274
        'X-Forwarded-For', // Common
275
    ];
276
    /**
277
     * @var string[] List of headers where proxies store the real request port.
278
     * It's not advisable to put insecure headers here.
279
     * To use the `Forwarded Port`, the header must be added to [[secureHeaders]] list.
280
     * The match of header names is case-insensitive.
281
     * @see trustedHosts
282
     * @see secureHeaders
283
     * @since 2.0.46
284
     */
285 1
    public $portHeaders = [
286
        'X-Forwarded-Port', // Common
287 1
    ];
288 1
    /**
289 1
     * @var array list of headers to check for determining whether the connection is made via HTTPS.
290 1
     * The array keys are header names and the array value is a list of header values that indicate a secure connection.
291 1
     * The match of header names and values is case-insensitive.
292
     * It's not advisable to put insecure headers here.
293 1
     * @see trustedHosts
294
     * @see secureHeaders
295
     * @since 2.0.13
296 1
     */
297
    public $secureProtocolHeaders = [
298
        'X-Forwarded-Proto' => ['https'], // Common
299
        'Front-End-Https' => ['on'], // Microsoft
300
    ];
301
302
    /**
303
     * @var CookieCollection Collection of request cookies.
304
     */
305
    private $_cookies;
306
    /**
307 265
     * @var HeaderCollection Collection of request headers.
308
     */
309 265
    private $_headers;
310
311
312 265
    /**
313 265
     * Resolves the current request into a route and the associated parameters.
314 245
     * @return array the first element is the route, and the second is the associated parameters.
315
     * @throws NotFoundHttpException if the request cannot be resolved.
316
     */
317
    public function resolve()
318
    {
319
        $result = Yii::$app->getUrlManager()->parseRequest($this);
320
        if ($result !== false) {
321
            list($route, $params) = $result;
322
            if ($this->_queryParams === null) {
323
                $_GET = $params + $_GET; // preserve numeric keys
324 265
            } else {
325
                $this->_queryParams = $params + $this->_queryParams;
326
            }
327 265
328
            return [$route, $this->getQueryParams()];
329
        }
330 265
331 77
        throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'));
332 77
    }
333 77
334 77
    /**
335 75
     * Filters headers according to the [[trustedHosts]].
336 75
     * @param HeaderCollection $headerCollection
337
     * @since 2.0.13
338 77
     */
339 77
    protected function filterHeaders(HeaderCollection $headerCollection)
340 40
    {
341 40
        $trustedHeaders = $this->getTrustedHeaders();
342
343
        // remove all secure headers unless they are trusted
344
        foreach ($this->secureHeaders as $secureHeader) {
345 265
            if (!in_array($secureHeader, $trustedHeaders)) {
346
                $headerCollection->remove($secureHeader);
347
            }
348
        }
349
    }
350
351
    /**
352
     * Trusted headers according to the [[trustedHosts]].
353
     * @return array
354
     * @since 2.0.28
355 170
     */
356
    protected function getTrustedHeaders()
357 170
    {
358
        // do not trust any of the [[secureHeaders]] by default
359
        $trustedHeaders = [];
360
361
        // check if the client is a trusted host
362
        if (!empty($this->trustedHosts)) {
363
            $validator = $this->getIpValidator();
364
            $ip = $this->getRemoteIP();
365 265
            foreach ($this->trustedHosts as $cidr => $headers) {
366
                if (!is_array($headers)) {
367 265
                    $cidr = $headers;
368 265
                    $headers = $this->secureHeaders;
369 265
                }
370
                $validator->setRanges($cidr);
371
                if ($validator->validate($ip)) {
372
                    $trustedHeaders = $headers;
373
                    break;
374 265
                }
375
            }
376
        }
377
        return $trustedHeaders;
378
    }
379
380
    /**
381 265
     * Creates instance of [[IpValidator]].
382
     * You can override this method to adjust validator or implement different matching strategy.
383 265
     *
384 261
     * @return IpValidator
385 261
     * @since 2.0.13
386 115
     */
387 115
    protected function getIpValidator()
388 115
    {
389
        return new IpValidator();
390
    }
391
392
    /**
393 265
     * Returns the header collection.
394
     * The header collection contains incoming HTTP headers.
395
     * @return HeaderCollection the header collection
396 265
     */
397
    public function getHeaders()
398
    {
399
        if ($this->_headers === null) {
400
            $this->_headers = new HeaderCollection();
401
            if (function_exists('getallheaders')) {
402
                $headers = getallheaders();
403
                foreach ($headers as $name => $value) {
404 43
                    $this->_headers->add($name, $value);
405
                }
406
            } elseif (function_exists('http_get_request_headers')) {
407 43
                $headers = http_get_request_headers();
408
                foreach ($headers as $name => $value) {
409
                    $this->_headers->add($name, $value);
410 43
                }
411
            } else {
412 6
                // ['prefix' => length]
413
                $headerPrefixes = ['HTTP_' => 5, 'REDIRECT_HTTP_' => 14];
414
415 41
                foreach ($_SERVER as $name => $value) {
416 1
                    foreach ($headerPrefixes as $prefix => $length) {
417
                        if (strncmp($name, $prefix, $length) === 0) {
418
                            $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, $length)))));
419 40
                            $this->_headers->add($name, $value);
420 10
                            continue 2;
421
                        }
422
                    }
423 31
                }
424
            }
425
            $this->filterHeaders($this->_headers);
426
        }
427
428
        return $this->_headers;
429
    }
430 2
431
    /**
432 2
     * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, PATCH, DELETE).
433
     * @return string request method, such as GET, POST, HEAD, PUT, PATCH, DELETE.
434
     * The value returned is turned into upper case.
435
     */
436
    public function getMethod()
437
    {
438
        if (
439 3
            isset($_POST[$this->methodParam])
440
            // Never allow to downgrade request from WRITE methods (POST, PATCH, DELETE, etc)
441 3
            // to read methods (GET, HEAD, OPTIONS) for security reasons.
442
            && !in_array(strtoupper($_POST[$this->methodParam]), ['GET', 'HEAD', 'OPTIONS'], true)
443
        ) {
444
            return strtoupper($_POST[$this->methodParam]);
445
        }
446
447
        if ($this->headers->has('X-Http-Method-Override')) {
448 15
            return strtoupper($this->headers->get('X-Http-Method-Override'));
0 ignored issues
show
Bug introduced by
It seems like $this->headers->get('X-Http-Method-Override') can also be of type array and null; however, parameter $string of strtoupper() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

448
            return strtoupper(/** @scrutinizer ignore-type */ $this->headers->get('X-Http-Method-Override'));
Loading history...
449
        }
450 15
451
        if (isset($_SERVER['REQUEST_METHOD'])) {
452
            return strtoupper($_SERVER['REQUEST_METHOD']);
453
        }
454
455
        return 'GET';
456
    }
457
458
    /**
459
     * Returns whether this is a GET request.
460
     * @return bool whether this is a GET request.
461
     */
462
    public function getIsGet()
463
    {
464
        return $this->getMethod() === 'GET';
465
    }
466
467
    /**
468
     * Returns whether this is an OPTIONS request.
469
     * @return bool whether this is a OPTIONS request.
470
     */
471
    public function getIsOptions()
472
    {
473
        return $this->getMethod() === 'OPTIONS';
474
    }
475
476
    /**
477
     * Returns whether this is a HEAD request.
478
     * @return bool whether this is a HEAD request.
479
     */
480
    public function getIsHead()
481
    {
482
        return $this->getMethod() === 'HEAD';
483
    }
484
485
    /**
486
     * Returns whether this is a POST request.
487
     * @return bool whether this is a POST request.
488
     */
489
    public function getIsPost()
490
    {
491
        return $this->getMethod() === 'POST';
492
    }
493
494
    /**
495
     * Returns whether this is a DELETE request.
496
     * @return bool whether this is a DELETE request.
497
     */
498
    public function getIsDelete()
499
    {
500
        return $this->getMethod() === 'DELETE';
501
    }
502
503
    /**
504
     * Returns whether this is a PUT request.
505
     * @return bool whether this is a PUT request.
506 16
     */
507
    public function getIsPut()
508 16
    {
509
        return $this->getMethod() === 'PUT';
510
    }
511
512
    /**
513
     * Returns whether this is a PATCH request.
514
     * @return bool whether this is a PATCH request.
515 3
     */
516
    public function getIsPatch()
517 3
    {
518
        return $this->getMethod() === 'PATCH';
519
    }
520
521
    /**
522
     * Returns whether this is an AJAX (XMLHttpRequest) request.
523
     *
524
     * Note that in case of cross domain requests, browser doesn't set the X-Requested-With header by default:
525
     * https://stackoverflow.com/questions/8163703/cross-domain-ajax-doesnt-send-x-requested-with-header
526
     *
527
     * In case you are using `fetch()`, pass header manually:
528
     *
529
     * ```
530
     * fetch(url, {
531
     *    method: 'GET',
532
     *    headers: {'X-Requested-With': 'XMLHttpRequest'}
533
     * })
534
     * ```
535
     *
536
     * @return bool whether this is an AJAX (XMLHttpRequest) request.
537 7
     */
538
    public function getIsAjax()
539 7
    {
540 4
        return $this->headers->get('X-Requested-With') === 'XMLHttpRequest';
541
    }
542
543 7
    /**
544
     * Returns whether this is a PJAX request.
545
     * @return bool whether this is a PJAX request
546
     */
547
    public function getIsPjax()
548
    {
549
        return $this->getIsAjax() && $this->headers->has('X-Pjax');
550 3
    }
551
552 3
    /**
553
     * Returns whether this is an Adobe Flash or Flex request.
554
     * @return bool whether this is an Adobe Flash or Adobe Flex request.
555
     */
556
    public function getIsFlash()
557
    {
558
        $userAgent = $this->headers->get('User-Agent', '');
559
        return stripos($userAgent, 'Shockwave') !== false
0 ignored issues
show
Bug introduced by
It seems like $userAgent can also be of type array and null; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

559
        return stripos(/** @scrutinizer ignore-type */ $userAgent, 'Shockwave') !== false
Loading history...
560
            || stripos($userAgent, 'Flash') !== false;
561
    }
562
563
    private $_rawBody;
564
565
    /**
566
     * Returns the raw HTTP request body.
567
     * @return string the request body
568
     */
569 16
    public function getRawBody()
570
    {
571 16
        if ($this->_rawBody === null) {
572 8
            $this->_rawBody = file_get_contents('php://input');
573 1
        }
574 1
575 1
        return $this->_rawBody;
576
    }
577
578 7
    /**
579 7
     * Sets the raw HTTP request body, this method is mainly used by test scripts to simulate raw HTTP requests.
580
     * @param string $rawBody the request body
581
     */
582
    public function setRawBody($rawBody)
583 7
    {
584
        $this->_rawBody = $rawBody;
585
    }
586 7
587 2
    private $_bodyParams;
588 2
589
    /**
590
     * Returns the request parameters given in the request body.
591 2
     *
592 5
     * Request parameters are determined using the parsers configured in [[parsers]] property.
593
     * If no parsers are configured for the current [[contentType]] it uses the PHP function `mb_parse_str()`
594
     * to parse the [[rawBody|request body]].
595
     * @return array|object the request parameters given in the request body.
596
     * @throws \yii\base\InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]].
597
     * @see getMethod()
598 5
     * @see getBodyParam()
599
     * @see setBodyParams()
600
     */
601
    public function getBodyParams()
602 5
    {
603 5
        if ($this->_bodyParams === null) {
604
            if (isset($_POST[$this->methodParam])) {
605
                $this->_bodyParams = $_POST;
606
                unset($this->_bodyParams[$this->methodParam]);
607 16
                return $this->_bodyParams;
608
            }
609
610
            $rawContentType = $this->getContentType();
611
            if (($pos = strpos((string)$rawContentType, ';')) !== false) {
612
                // e.g. text/html; charset=UTF-8
613
                $contentType = substr($rawContentType, 0, $pos);
614
            } else {
615
                $contentType = $rawContentType;
616 11
            }
617
618 11
            if (isset($this->parsers[$contentType])) {
619
                $parser = Yii::createObject($this->parsers[$contentType]);
620
                if (!($parser instanceof RequestParserInterface)) {
621
                    throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface.");
622
                }
623
                $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
624
            } elseif (isset($this->parsers['*'])) {
625
                $parser = Yii::createObject($this->parsers['*']);
626
                if (!($parser instanceof RequestParserInterface)) {
627
                    throw new InvalidConfigException('The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface.');
628
                }
629
                $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
630
            } elseif ($this->getMethod() === 'POST') {
631
                // PHP has already parsed the body so we have all params in $_POST
632 7
                $this->_bodyParams = $_POST;
633
            } else {
634 7
                $this->_bodyParams = [];
635
                mb_parse_str($this->getRawBody(), $this->_bodyParams);
636 7
            }
637
        }
638
639 1
        return $this->_bodyParams;
640
    }
641
642
    /**
643
     * Sets the request body parameters.
644
     *
645 7
     * @param array|object $values the request body parameters (name-value pairs)
646
     * @see getBodyParams()
647
     */
648
    public function setBodyParams($values)
649
    {
650
        $this->_bodyParams = $values;
651
    }
652
653
    /**
654
     * Returns the named request body parameter value.
655
     *
656
     * If the parameter does not exist, the second parameter passed to this method will be returned.
657
     *
658
     * @param string $name the parameter name
659
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
660
     * @return mixed the parameter value
661
     * @see getBodyParams()
662
     * @see setBodyParams()
663
     */
664
    public function getBodyParam($name, $defaultValue = null)
665
    {
666
        $params = $this->getBodyParams();
667
668
        if (is_object($params)) {
669
            // unable to use `ArrayHelper::getValue()` due to different dots in key logic and lack of exception handling
670
            try {
671
                return isset($params->{$name}) ? $params->{$name} : $defaultValue;
672
            } catch (\Exception $e) {
0 ignored issues
show
Unused Code introduced by
catch (\Exception $e) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
673 65
                return $defaultValue;
674
            }
675 65
        }
676 58
677
        return isset($params[$name]) ? $params[$name] : $defaultValue;
678
    }
679 9
680
    /**
681
     * Returns POST parameter with a given name. If name isn't specified, returns an array of all POST parameters.
682
     *
683
     * @param string $name the parameter name
684
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
685
     * @return array|mixed
686
     */
687
    public function post($name = null, $defaultValue = null)
688 9
    {
689
        if ($name === null) {
690 9
            return $this->getBodyParams();
691
        }
692
693
        return $this->getBodyParam($name, $defaultValue);
694
    }
695
696
    private $_queryParams;
697
698
    /**
699
     * Returns the request parameters given in the [[queryString]].
700 23
     *
701
     * This method will return the contents of `$_GET` if params where not explicitly set.
702 23
     * @return array the request GET parameter values.
703
     * @see setQueryParams()
704
     */
705
    public function getQueryParams()
706 23
    {
707
        if ($this->_queryParams === null) {
708
            return $_GET;
709
        }
710
711
        return $this->_queryParams;
712
    }
713
714
    /**
715
     * Sets the request [[queryString]] parameters.
716
     * @param array $values the request query parameters (name-value pairs)
717 26
     * @see getQueryParam()
718
     * @see getQueryParams()
719 26
     */
720
    public function setQueryParams($values)
721 26
    {
722
        $this->_queryParams = $values;
723
    }
724
725
    /**
726
     * Returns GET parameter with a given name. If name isn't specified, returns an array of all GET parameters.
727
     *
728
     * @param string $name the parameter name
729
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
730
     * @return array|mixed
731
     */
732
    public function get($name = null, $defaultValue = null)
733
    {
734
        if ($name === null) {
735
            return $this->getQueryParams();
736
        }
737
738
        return $this->getQueryParam($name, $defaultValue);
739
    }
740
741
    /**
742
     * Returns the named GET parameter value.
743
     * If the GET parameter does not exist, the second parameter passed to this method will be returned.
744
     * @param string $name the GET parameter name.
745
     * @param mixed $defaultValue the default parameter value if the GET parameter does not exist.
746
     * @return mixed the GET parameter value
747
     * @see getBodyParam()
748
     */
749
    public function getQueryParam($name, $defaultValue = null)
750
    {
751
        $params = $this->getQueryParams();
752
753
        return isset($params[$name]) ? $params[$name] : $defaultValue;
754 49
    }
755
756 49
    private $_hostInfo;
757 45
    private $_hostName;
758 45
759
    /**
760 45
     * Returns the schema and host part of the current request URL.
761 8
     *
762 37
     * The returned URL does not have an ending slash.
763 3
     *
764 34
     * By default this value is based on the user request information. This method will
765
     * return the value of `$_SERVER['HTTP_HOST']` if it is available or `$_SERVER['SERVER_NAME']` if not.
766 34
     * You may want to check out the [PHP documentation](https://www.php.net/manual/en/reserved.variables.server.php)
767 11
     * for more information on these variables.
768 23
     *
769 1
     * You may explicitly specify it by setting the [[setHostInfo()|hostInfo]] property.
770 1
     *
771 1
     * > Warning: Dependent on the server configuration this information may not be
772
     * > reliable and [may be faked by the user sending the HTTP request](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
773
     * > If the webserver is configured to serve the same site independent of the value of
774
     * > the `Host` header, this value is not reliable. In such situations you should either
775
     * > fix your webserver configuration or explicitly set the value by setting the [[setHostInfo()|hostInfo]] property.
776
     * > If you don't have access to the server configuration, you can setup [[\yii\filters\HostControl]] filter at
777 49
     * > application level in order to protect against such kind of attack.
778
     *
779
     * @property string|null schema and hostname part (with port number if needed) of the request URL
780
     * (e.g. `https://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set.
781
     * See [[getHostInfo()]] for security related notes on this property.
782
     * @return string|null schema and hostname part (with port number if needed) of the request URL
783
     * (e.g. `https://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set.
784
     * @see setHostInfo()
785
     */
786
    public function getHostInfo()
787 59
    {
788
        if ($this->_hostInfo === null) {
789 59
            $secure = $this->getIsSecureConnection();
790 59
            $http = $secure ? 'https' : 'http';
791
792
            if ($this->getSecureForwardedHeaderTrustedPart('host') !== null) {
793
                $this->_hostInfo = $http . '://' . $this->getSecureForwardedHeaderTrustedPart('host');
794
            } elseif ($this->headers->has('X-Forwarded-Host')) {
795
                $this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Forwarded-Host'))[0]);
0 ignored issues
show
Bug introduced by
It seems like $this->headers->get('X-Forwarded-Host') can also be of type array and null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

795
                $this->_hostInfo = $http . '://' . trim(explode(',', /** @scrutinizer ignore-type */ $this->headers->get('X-Forwarded-Host'))[0]);
Loading history...
796
            } elseif ($this->headers->has('X-Original-Host')) {
797
                $this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Original-Host'))[0]);
798
            } elseif ($this->headers->has('Host')) {
799
                $this->_hostInfo = $http . '://' . $this->headers->get('Host');
0 ignored issues
show
Bug introduced by
Are you sure $this->headers->get('Host') of type array|null|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

799
                $this->_hostInfo = $http . '://' . /** @scrutinizer ignore-type */ $this->headers->get('Host');
Loading history...
800
            } elseif (isset($_SERVER['SERVER_NAME'])) {
801
                $this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME'];
802
                $port = $secure ? $this->getSecurePort() : $this->getPort();
803
                if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) {
804 20
                    $this->_hostInfo .= ':' . $port;
805
                }
806 20
            }
807 20
        }
808
809
        return $this->_hostInfo;
810 20
    }
811
812
    /**
813
     * Sets the schema and host part of the application URL.
814
     * This setter is provided in case the schema and hostname cannot be determined
815
     * on certain Web servers.
816
     * @param string|null $value the schema and host part of the application URL. The trailing slashes will be removed.
817
     * @see getHostInfo() for security related notes on this property.
818
     */
819
    public function setHostInfo($value)
820
    {
821
        $this->_hostName = null;
822 475
        $this->_hostInfo = $value === null ? null : rtrim($value, '/');
823
    }
824 475
825 473
    /**
826
     * Returns the host part of the current request URL.
827
     * Value is calculated from current [[getHostInfo()|hostInfo]] property.
828 475
     *
829
     * > Warning: The content of this value may not be reliable, dependent on the server
830
     * > configuration. Please refer to [[getHostInfo()]] for more information.
831
     *
832
     * @return string|null hostname part of the request URL (e.g. `www.yiiframework.com`)
833
     * @see getHostInfo()
834
     * @since 2.0.10
835
     */
836
    public function getHostName()
837 2
    {
838
        if ($this->_hostName === null) {
839 2
            $this->_hostName = parse_url((string)$this->getHostInfo(), PHP_URL_HOST);
840
        }
841
842
        return $this->_hostName;
843
    }
844
845
    private $_baseUrl;
846
847
    /**
848
     * Returns the relative URL for the application.
849
     * This is similar to [[scriptUrl]] except that it does not include the script file name,
850 476
     * and the ending slashes are removed.
851
     * @return string the relative URL for the application
852 476
     * @see setScriptUrl()
853 1
     */
854
    public function getBaseUrl()
855
    {
856
        if ($this->_baseUrl === null) {
857
            $this->_baseUrl = rtrim(dirname($this->getScriptUrl()), '\\/');
858
        }
859
860
        return $this->_baseUrl;
861
    }
862
863
    /**
864
     * Sets the relative URL for the application.
865
     * By default the URL is determined based on the entry script URL.
866
     * This setter is provided in case you want to change this behavior.
867
     * @param string $value the relative URL for the application
868
     */
869
    public function setBaseUrl($value)
870 475
    {
871
        $this->_baseUrl = $value;
872
    }
873
874
    private $_scriptUrl;
875
876
    /**
877
     * Returns the relative URL of the entry script.
878
     * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework.
879 486
     * @return string the relative URL of the entry script.
880
     * @throws InvalidConfigException if unable to determine the entry script URL
881 486
     */
882
    public function getScriptUrl()
883
    {
884
        if ($this->_scriptUrl === null) {
885
            $scriptFile = $this->getScriptFile();
886
            $scriptName = basename($scriptFile);
887
            if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $scriptName) {
888
                $this->_scriptUrl = $_SERVER['SCRIPT_NAME'];
889
            } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $scriptName) {
890
                $this->_scriptUrl = $_SERVER['PHP_SELF'];
891
            } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) {
892 477
                $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME'];
893
            } elseif (isset($_SERVER['PHP_SELF']) && ($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) {
894 477
                $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName;
895 448
            } elseif (!empty($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) {
896
                $this->_scriptUrl = str_replace([$_SERVER['DOCUMENT_ROOT'], '\\'], ['', '/'], $scriptFile);
897
            } else {
898 29
                throw new InvalidConfigException('Unable to determine the entry script URL.');
899 27
            }
900
        }
901
902 2
        return $this->_scriptUrl;
903
    }
904
905
    /**
906
     * Sets the relative URL for the application entry script.
907
     * This setter is provided in case the entry script URL cannot be determined
908
     * on certain Web servers.
909
     * @param string $value the relative URL for the application entry script.
910
     */
911
    public function setScriptUrl($value)
912 448
    {
913
        $this->_scriptUrl = $value === null ? null : '/' . trim($value, '/');
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
914 448
    }
915
916
    private $_scriptFile;
917
918
    /**
919
     * Returns the entry script file path.
920
     * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`.
921
     * @return string the entry script file path
922
     * @throws InvalidConfigException
923
     */
924
    public function getScriptFile()
925
    {
926
        if (isset($this->_scriptFile)) {
927 19
            return $this->_scriptFile;
928
        }
929 19
930
        if (isset($_SERVER['SCRIPT_FILENAME'])) {
931
            return $_SERVER['SCRIPT_FILENAME'];
932
        }
933 19
934
        throw new InvalidConfigException('Unable to determine the entry script file path.');
935
    }
936
937
    /**
938
     * Sets the entry script file path.
939
     * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`.
940
     * If your server configuration does not return the correct value, you may configure
941 20
     * this property to make it right.
942
     * @param string $value the entry script file path.
943 20
     */
944
    public function setScriptFile($value)
945
    {
946
        $this->_scriptFile = $value;
947
    }
948
949
    private $_pathInfo;
950
951
    /**
952
     * Returns the path info of the currently requested URL.
953
     * A path info refers to the part that is after the entry script and before the question mark (query string).
954
     * The starting and ending slashes are both removed.
955
     * @return string part of the request URL that is after the entry script and before the question mark.
956
     * Note, the returned path info is already URL-decoded.
957
     * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration
958
     */
959
    public function getPathInfo()
960
    {
961
        if ($this->_pathInfo === null) {
962
            $this->_pathInfo = $this->resolvePathInfo();
963
        }
964
965
        return $this->_pathInfo;
966
    }
967
968
    /**
969
     * Sets the path info of the current request.
970
     * This method is mainly provided for testing purpose.
971
     * @param string $value the path info of the current request
972
     */
973
    public function setPathInfo($value)
974
    {
975
        $this->_pathInfo = $value === null ? null : ltrim($value, '/');
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
976
    }
977
978
    /**
979
     * Resolves the path info part of the currently requested URL.
980
     * A path info refers to the part that is after the entry script and before the question mark (query string).
981
     * The starting slashes are both removed (ending slashes will be kept).
982
     * @return string part of the request URL that is after the entry script and before the question mark.
983
     * Note, the returned path info is decoded.
984
     * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration
985
     */
986
    protected function resolvePathInfo()
987
    {
988
        $pathInfo = $this->getUrl();
989
990
        if (($pos = strpos($pathInfo, '?')) !== false) {
991
            $pathInfo = substr($pathInfo, 0, $pos);
992
        }
993
994
        $pathInfo = urldecode($pathInfo);
995
996
        // try to encode in UTF8 if not so
997
        // https://www.w3.org/International/questions/qa-forms-utf-8.en.html
998
        if (
999
            !preg_match('%^(?:
1000
            [\x09\x0A\x0D\x20-\x7E]              # ASCII
1001
            | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
1002
            | \xE0[\xA0-\xBF][\x80-\xBF]         # excluding overlongs
1003
            | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
1004
            | \xED[\x80-\x9F][\x80-\xBF]         # excluding surrogates
1005
            | \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
1006
            | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
1007
            | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
1008
            )*$%xs', $pathInfo)
1009
        ) {
1010
            $pathInfo = $this->utf8Encode($pathInfo);
1011
        }
1012
1013
        $scriptUrl = $this->getScriptUrl();
1014
        $baseUrl = $this->getBaseUrl();
1015
        if (strpos($pathInfo, $scriptUrl) === 0) {
1016
            $pathInfo = substr($pathInfo, strlen($scriptUrl));
1017
        } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) {
1018
            $pathInfo = substr($pathInfo, strlen($baseUrl));
1019
        } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) {
1020
            $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl));
1021
        } else {
1022
            throw new InvalidConfigException('Unable to determine the path info of the current request.');
1023
        }
1024
1025
        if (strncmp($pathInfo, '/', 1) === 0) {
1026
            $pathInfo = substr($pathInfo, 1);
1027 1
        }
1028
1029 1
        return (string) $pathInfo;
1030
    }
1031
1032
    /**
1033
     * Encodes an ISO-8859-1 string to UTF-8
1034
     * @param string $s
1035
     * @return string the UTF-8 translation of `s`.
1036
     * @see https://github.com/symfony/polyfill-php72/blob/master/Php72.php#L24
1037
     * @phpcs:disable Generic.Formatting.DisallowMultipleStatements.SameLine
1038
     * @phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore
1039
     */
1040
    private function utf8Encode($s)
1041 11
    {
1042
        $s .= $s;
1043 11
        $len = \strlen($s);
1044 3
        for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
1045
            switch (true) {
1046
                case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
1047 11
                case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
1048
                default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break;
1049
            }
1050
        }
1051
        return substr($s, 0, $j);
1052
    }
1053
1054
    /**
1055
     * Returns the currently requested absolute URL.
1056 25
     * This is a shortcut to the concatenation of [[hostInfo]] and [[url]].
1057
     * @return string the currently requested absolute URL.
1058 25
     */
1059
    public function getAbsoluteUrl()
1060
    {
1061
        return $this->getHostInfo() . $this->getUrl();
1062
    }
1063
1064
    private $_url;
1065
1066
    /**
1067
     * Returns the currently requested relative URL.
1068
     * This refers to the portion of the URL that is after the [[hostInfo]] part.
1069 3
     * It includes the [[queryString]] part if any.
1070
     * @return string the currently requested relative URL. Note that the URI returned may be URL-encoded depending on the client.
1071 3
     * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration
1072
     */
1073 3
    public function getUrl()
1074 3
    {
1075 3
        if ($this->_url === null) {
1076 3
            $this->_url = $this->resolveRequestUri();
1077
        }
1078
1079
        return $this->_url;
1080
    }
1081
1082
    /**
1083
     * Sets the currently requested relative URL.
1084
     * The URI must refer to the portion that is after [[hostInfo]].
1085
     * Note that the URI should be URL-encoded.
1086
     * @param string $value the request URI to be set
1087 3
     */
1088
    public function setUrl($value)
1089
    {
1090
        $this->_url = $value;
1091
    }
1092
1093
    /**
1094
     * Resolves the request URI portion for the currently requested URL.
1095
     * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any.
1096
     * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework.
1097
     * @return string|bool the request URI portion for the currently requested URL.
1098
     * Note that the URI returned may be URL-encoded depending on the client.
1099
     * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration
1100
     */
1101
    protected function resolveRequestUri()
1102
    {
1103 73
        if ($this->headers->has('X-Rewrite-Url')) { // IIS
1104
            $requestUri = $this->headers->get('X-Rewrite-Url');
1105 73
        } elseif (isset($_SERVER['REQUEST_URI'])) {
1106 2
            $requestUri = $_SERVER['REQUEST_URI'];
1107
            if ($requestUri !== '' && $requestUri[0] !== '/') {
1108
                $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri);
1109 71
            }
1110 9
        } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI
1111
            $requestUri = $_SERVER['ORIG_PATH_INFO'];
1112
            if (!empty($_SERVER['QUERY_STRING'])) {
1113 62
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
1114 62
            }
1115 5
        } else {
1116 5
            throw new InvalidConfigException('Unable to determine the request URI.');
1117 3
        }
1118
1119
        return $requestUri;
1120
    }
1121
1122
    /**
1123 59
     * Returns part of the request URL that is after the question mark.
1124
     * @return string part of the request URL that is after the question mark
1125
     */
1126
    public function getQueryString()
1127
    {
1128
        return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
1129
    }
1130 1
1131
    /**
1132 1
     * Return if the request is sent via secure channel (https).
1133
     * @return bool if the request is sent via secure channel (https)
1134
     */
1135
    public function getIsSecureConnection()
1136
    {
1137
        if (isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)) {
1138
            return true;
1139
        }
1140
1141
        if (($proto = $this->getSecureForwardedHeaderTrustedPart('proto')) !== null) {
1142 8
            return strcasecmp($proto, 'https') === 0;
1143
        }
1144 8
1145 8
        foreach ($this->secureProtocolHeaders as $header => $values) {
1146 2
            if (($headerValue = $this->headers->get($header, null)) !== null) {
1147 2
                foreach ($values as $value) {
1148 2
                    if (strcasecmp($headerValue, $value) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $headerValue can also be of type array; however, parameter $string1 of strcasecmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1148
                    if (strcasecmp(/** @scrutinizer ignore-type */ $headerValue, $value) === 0) {
Loading history...
1149
                        return true;
1150
                    }
1151
                }
1152
            }
1153 6
        }
1154
1155
        return false;
1156
    }
1157
1158
    /**
1159
     * Returns the server name.
1160
     * @return string|null server name, null if not available
1161
     */
1162
    public function getServerName()
1163
    {
1164
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
1165
    }
1166
1167
    /**
1168
     * Returns the server port number. If a port is specified via a forwarding header (e.g. 'X-Forwarded-Port')
1169
     * and the remote host is a "trusted host" the that port will be used (see [[portHeaders]]),
1170
     * otherwise the default server port will be returned.
1171
     * @return int|null server port number, null if not available
1172
     * @see portHeaders
1173
     */
1174
    public function getServerPort()
1175
    {
1176
        foreach ($this->portHeaders as $portHeader) {
1177
            if ($this->headers->has($portHeader)) {
1178
                $port = $this->headers->get($portHeader);
1179
                if ($port !== null) {
1180 1
                    return $port;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $port returns the type array|string which is incompatible with the documented return type integer|null.
Loading history...
1181
                }
1182 1
            }
1183
        }
1184
1185
        return isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null;
1186
    }
1187
1188
    /**
1189 1
     * Returns the URL referrer.
1190
     * @return string|null URL referrer, null if not available
1191 1
     */
1192
    public function getReferrer()
1193
    {
1194
        return $this->headers->get('Referer');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->headers->get('Referer') also could return the type array which is incompatible with the documented return type null|string.
Loading history...
1195
    }
1196
1197
    /**
1198
     * Returns the URL origin of a CORS request.
1199
     *
1200 105
     * The return value is taken from the `Origin` [[getHeaders()|header]] sent by the browser.
1201
     *
1202 105
     * Note that the origin request header indicates where a fetch originates from.
1203
     * It doesn't include any path information, but only the server name.
1204 105
     * It is sent with a CORS requests, as well as with POST requests.
1205 105
     * It is similar to the referer header, but, unlike this header, it doesn't disclose the whole path.
1206 105
     * Please refer to <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin> for more information.
1207 105
     *
1208 105
     * @return string|null URL origin of a CORS request, `null` if not available.
1209
     * @see getHeaders()
1210 14
     * @since 2.0.13
1211 14
     */
1212 14
    public function getOrigin()
1213
    {
1214
        return $this->getHeaders()->get('origin');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getHeaders()->get('origin') also could return the type array which is incompatible with the documented return type null|string.
Loading history...
1215
    }
1216
1217 91
    /**
1218 88
     * Returns the user agent.
1219 10
     * @return string|null user agent, null if not available
1220 10
     */
1221 10
    public function getUserAgent()
1222
    {
1223
        return $this->headers->get('User-Agent');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->headers->get('User-Agent') also could return the type array which is incompatible with the documented return type null|string.
Loading history...
1224
    }
1225 81
1226
    /**
1227
     * Returns the user IP address from [[ipHeaders]].
1228
     * @return string|null user IP address, null if not available
1229
     * @see ipHeaders
1230
     * @since 2.0.28
1231
     */
1232
    protected function getUserIpFromIpHeaders()
1233
    {
1234
        $ip = $this->getSecureForwardedHeaderTrustedPart('for');
1235 105
        if (
1236
            $ip !== null && preg_match(
1237 105
                '/^\[?(?P<ip>(?:(?:(?:[0-9a-f]{1,4}:){1,6}(?:[0-9a-f]{1,4})?(?:(?::[0-9a-f]{1,4}){1,6}))|(?:\d{1,3}\.){3}\d{1,3}))\]?(?::(?P<port>\d+))?$/',
1238 105
                $ip,
1239 105
                $matches
1240 81
            )
1241
        ) {
1242
            $ip = $this->getUserIpFromIpHeader($matches['ip']);
1243
            if ($ip !== null) {
1244 105
                return $ip;
1245
            }
1246
        }
1247
1248
1249
        foreach ($this->ipHeaders as $ipHeader) {
1250
            if ($this->headers->has($ipHeader)) {
1251
                $ip = $this->getUserIpFromIpHeader($this->headers->get($ipHeader));
0 ignored issues
show
Bug introduced by
It seems like $this->headers->get($ipHeader) can also be of type array; however, parameter $ips of yii\web\Request::getUserIpFromIpHeader() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1251
                $ip = $this->getUserIpFromIpHeader(/** @scrutinizer ignore-type */ $this->headers->get($ipHeader));
Loading history...
1252
                if ($ip !== null) {
1253
                    return $ip;
1254
                }
1255
            }
1256
        }
1257 24
        return null;
1258
    }
1259 24
1260 24
    private $_ip = null;
1261
1262
    /**
1263 24
     * Returns the user IP address.
1264 24
     * The IP is determined using headers and / or `$_SERVER` variables.
1265 24
     * @return string|null user IP address, null if not available
1266 24
     */
1267 24
    public function getUserIP()
1268 24
    {
1269 24
        if ($this->_ip === null) {
1270 1
            $this->_ip = $this->getUserIpFromIpHeaders();
1271
            if ($this->_ip === null) {
1272 24
                $this->_ip = $this->getRemoteIP();
1273 24
            }
1274 24
        }
1275 24
1276 24
        return $this->_ip;
1277
    }
1278 24
1279 24
    /**
1280 7
     * Return user IP's from IP header.
1281 7
     *
1282
     * @param string $ips comma separated IP list
1283
     * @return string|null IP as string. Null is returned if IP can not be determined from header.
1284 24
     * @see getUserHost()
1285 20
     * @see ipHeaders
1286
     * @see getTrustedHeaders()
1287
     * @since 2.0.28
1288 24
     */
1289
    protected function getUserIpFromIpHeader($ips)
1290
    {
1291
        $ips = trim($ips);
1292
        if ($ips === '') {
1293
            return null;
1294
        }
1295
        $ips = preg_split('/\s*,\s*/', $ips, -1, PREG_SPLIT_NO_EMPTY);
1296
        krsort($ips);
1297
        $validator = $this->getIpValidator();
1298
        $resultIp = null;
1299
        foreach ($ips as $ip) {
1300
            $validator->setRanges('any');
1301
            if (!$validator->validate($ip) /* checking IP format */) {
1302
                break;
1303
            }
1304
            $resultIp = $ip;
1305
            $isTrusted = false;
1306
            foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) {
1307
                if (!is_array($trustedCidrOrHeaders)) {
1308
                    $trustedCidr = $trustedCidrOrHeaders;
1309
                }
1310
                $validator->setRanges($trustedCidr);
1311 142
                if ($validator->validate($ip) /* checking trusted range */) {
1312
                    $isTrusted = true;
1313 142
                    break;
1314
                }
1315
            }
1316
            if (!$isTrusted) {
1317
                break;
1318
            }
1319
        }
1320
        return $resultIp;
1321
    }
1322
1323
    /**
1324
     * Returns the user host name.
1325
     * The HOST is determined using headers and / or `$_SERVER` variables.
1326
     * @return string|null user host name, null if not available
1327
     */
1328
    public function getUserHost()
1329
    {
1330
        $userIp = $this->getUserIpFromIpHeaders();
1331
        if ($userIp === null) {
1332
            return $this->getRemoteHost();
1333 9
        }
1334
        return gethostbyaddr($userIp);
1335 9
    }
1336
1337
    /**
1338
     * Returns the IP on the other end of this connection.
1339
     * This is always the next hop, any headers are ignored.
1340
     * @return string|null remote IP address, `null` if not available.
1341
     * @since 2.0.13
1342 9
     */
1343
    public function getRemoteIP()
1344 9
    {
1345
        return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
1346
    }
1347
1348
    /**
1349
     * Returns the host name of the other end of this connection.
1350
     * This is always the next hop, any headers are ignored.
1351
     * @return string|null remote host name, `null` if not available
1352
     * @see getUserHost()
1353
     * @see getRemoteIP()
1354
     * @since 2.0.13
1355 39
     */
1356
    public function getRemoteHost()
1357 39
    {
1358 39
        return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null;
1359 39
    }
1360 21
1361
    /**
1362
     * @return string|null the username sent via HTTP authentication, `null` if the username is not given
1363
     * @see getAuthCredentials() to get both username and password in one call
1364
     */
1365
    public function getAuthUser()
1366
    {
1367
        return $this->getAuthCredentials()[0];
1368
    }
1369
1370
    /**
1371 18
     * @return string|null the password sent via HTTP authentication, `null` if the password is not given
1372
     * @see getAuthCredentials() to get both username and password in one call
1373 18
     */
1374 18
    public function getAuthPassword()
1375 18
    {
1376 18
        return $this->getAuthCredentials()[1];
1377
    }
1378 18
1379 2
    /**
1380
     * @return array that contains exactly two elements:
1381
     * - 0: the username sent via HTTP authentication, `null` if the username is not given
1382 16
     * - 1: the password sent via HTTP authentication, `null` if the password is not given
1383
     * @see getAuthUser() to get only username
1384
     * @see getAuthPassword() to get only password
1385
     * @since 2.0.13
1386
     */
1387
    public function getAuthCredentials()
1388
    {
1389
        $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
1390
        $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
1391
        if ($username !== null || $password !== null) {
1392
            return [$username, $password];
1393
        }
1394
1395
        /**
1396
         * Apache with php-cgi does not pass HTTP Basic authentication to PHP by default.
1397 1
         * To make it work, add one of the following lines to to your .htaccess file:
1398
         *
1399 1
         * SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
1400 1
         * --OR--
1401 1
         * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
1402
         */
1403
        $auth_token = $this->getHeaders()->get('Authorization');
1404 1
1405
        if ($auth_token !== null && strncasecmp($auth_token, 'basic', 5) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $auth_token can also be of type array; however, parameter $string1 of strncasecmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1405
        if ($auth_token !== null && strncasecmp(/** @scrutinizer ignore-type */ $auth_token, 'basic', 5) === 0) {
Loading history...
1406
            $parts = array_map(function ($value) {
1407
                return strlen($value) === 0 ? null : $value;
1408
            }, explode(':', base64_decode(mb_substr($auth_token, 6)), 2));
0 ignored issues
show
Bug introduced by
It seems like $auth_token can also be of type array; however, parameter $string of mb_substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1408
            }, explode(':', base64_decode(mb_substr(/** @scrutinizer ignore-type */ $auth_token, 6)), 2));
Loading history...
1409
1410
            if (count($parts) < 2) {
1411
                return [$parts[0], null];
1412
            }
1413
1414
            return $parts;
1415
        }
1416
1417
        return [null, null];
1418
    }
1419
1420
    private $_port;
1421
1422
    /**
1423
     * Returns the port to use for insecure requests.
1424
     * Defaults to 80, or the port specified by the server if the current
1425
     * request is insecure.
1426
     * @return int port number for insecure requests.
1427
     * @see setPort()
1428
     */
1429
    public function getPort()
1430
    {
1431
        if ($this->_port === null) {
1432
            $serverPort = $this->getServerPort();
1433
            $this->_port = !$this->getIsSecureConnection() && $serverPort !== null ? $serverPort : 80;
1434
        }
1435
1436
        return $this->_port;
1437
    }
1438
1439
    /**
1440
     * Sets the port to use for insecure requests.
1441
     * This setter is provided in case a custom port is necessary for certain
1442
     * server configurations.
1443
     * @param int $value port number.
1444
     */
1445
    public function setPort($value)
1446
    {
1447
        if ($value != $this->_port) {
1448
            $this->_port = (int) $value;
1449
            $this->_hostInfo = null;
1450
        }
1451
    }
1452
1453
    private $_securePort;
1454
1455
    /**
1456
     * Returns the port to use for secure requests.
1457
     * Defaults to 443, or the port specified by the server if the current
1458
     * request is secure.
1459
     * @return int port number for secure requests.
1460
     * @see setSecurePort()
1461
     */
1462
    public function getSecurePort()
1463
    {
1464
        if ($this->_securePort === null) {
1465
            $serverPort = $this->getServerPort();
1466
            $this->_securePort = $this->getIsSecureConnection() && $serverPort !== null ? $serverPort : 443;
1467
        }
1468
1469
        return $this->_securePort;
1470
    }
1471
1472
    /**
1473
     * Sets the port to use for secure requests.
1474
     * This setter is provided in case a custom port is necessary for certain
1475
     * server configurations.
1476
     * @param int $value port number.
1477 5
     */
1478
    public function setSecurePort($value)
1479 5
    {
1480 4
        if ($value != $this->_securePort) {
1481 2
            $this->_securePort = (int) $value;
1482
            $this->_hostInfo = null;
1483 3
        }
1484
    }
1485
1486
    private $_contentTypes;
1487 5
1488
    /**
1489
     * Returns the content types acceptable by the end user.
1490
     *
1491
     * This is determined by the `Accept` HTTP header. For example,
1492
     *
1493
     * ```php
1494
     * $_SERVER['HTTP_ACCEPT'] = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;';
1495
     * $types = $request->getAcceptableContentTypes();
1496
     * print_r($types);
1497
     * // displays:
1498 1
     * // [
1499
     * //     'application/json' => ['q' => 1, 'version' => '1.0'],
1500 1
     * //      'application/xml' => ['q' => 1, 'version' => '2.0'],
1501
     * //           'text/plain' => ['q' => 0.5],
1502
     * // ]
1503
     * ```
1504
     *
1505
     * @return array the content types ordered by the quality score. Types with the highest scores
1506
     * will be returned first. The array keys are the content types, while the array values
1507
     * are the corresponding quality score and other parameters as given in the header.
1508
     */
1509
    public function getAcceptableContentTypes()
1510
    {
1511
        if ($this->_contentTypes === null) {
1512
            if ($this->headers->get('Accept') !== null) {
1513 7
                $this->_contentTypes = $this->parseAcceptHeader($this->headers->get('Accept'));
0 ignored issues
show
Bug introduced by
It seems like $this->headers->get('Accept') can also be of type array; however, parameter $header of yii\web\Request::parseAcceptHeader() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1513
                $this->_contentTypes = $this->parseAcceptHeader(/** @scrutinizer ignore-type */ $this->headers->get('Accept'));
Loading history...
1514
            } else {
1515 7
                $this->_contentTypes = [];
1516 3
            }
1517
        }
1518
1519
        return $this->_contentTypes;
1520 4
    }
1521
1522
    /**
1523
     * Sets the acceptable content types.
1524
     * Please refer to [[getAcceptableContentTypes()]] on the format of the parameter.
1525
     * @param array $value the content types that are acceptable by the end user. They should
1526
     * be ordered by the preference level.
1527
     * @see getAcceptableContentTypes()
1528
     * @see parseAcceptHeader()
1529
     */
1530
    public function setAcceptableContentTypes($value)
1531 2
    {
1532
        $this->_contentTypes = $value;
1533 2
    }
1534 1
1535
    /**
1536
     * Returns request content-type
1537 1
     * The Content-Type header field indicates the MIME type of the data
1538
     * contained in [[getRawBody()]] or, in the case of the HEAD method, the
1539
     * media type that would have been sent had the request been a GET.
1540
     * For the MIME-types the user expects in response, see [[acceptableContentTypes]].
1541 2
     * @return string request content-type. Empty string is returned if this information is not available.
1542
     * @link https://tools.ietf.org/html/rfc2616#section-14.17
1543
     * HTTP 1.1 header field definitions
1544
     */
1545
    public function getContentType()
1546
    {
1547
        if (isset($_SERVER['CONTENT_TYPE'])) {
1548 1
            return $_SERVER['CONTENT_TYPE'];
1549
        }
1550 1
1551
        //fix bug https://bugs.php.net/bug.php?id=66606
1552
        return $this->headers->get('Content-Type') ?: '';
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->headers->get('Content-Type') ?: '' also could return the type array which is incompatible with the documented return type string.
Loading history...
1553
    }
1554
1555
    private $_languages;
1556
1557
    /**
1558
     * Returns the languages acceptable by the end user.
1559
     * This is determined by the `Accept-Language` HTTP header.
1560
     * @return array the languages ordered by the preference level. The first element
1561
     * represents the most preferred language.
1562
     */
1563
    public function getAcceptableLanguages()
1564
    {
1565
        if ($this->_languages === null) {
1566
            if ($this->headers->has('Accept-Language')) {
1567
                $this->_languages = array_keys($this->parseAcceptHeader($this->headers->get('Accept-Language')));
0 ignored issues
show
Bug introduced by
It seems like $this->headers->get('Accept-Language') can also be of type array; however, parameter $header of yii\web\Request::parseAcceptHeader() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1567
                $this->_languages = array_keys($this->parseAcceptHeader(/** @scrutinizer ignore-type */ $this->headers->get('Accept-Language')));
Loading history...
1568
            } else {
1569
                $this->_languages = [];
1570
            }
1571
        }
1572
1573
        return $this->_languages;
1574
    }
1575
1576
    /**
1577 3
     * @param array $value the languages that are acceptable by the end user. They should
1578
     * be ordered by the preference level.
1579 3
     */
1580 3
    public function setAcceptableLanguages($value)
1581 3
    {
1582 3
        $this->_languages = $value;
1583 1
    }
1584
1585 3
    /**
1586 3
     * Parses the given `Accept` (or `Accept-Language`) header.
1587 3
     *
1588 3
     * This method will return the acceptable values with their quality scores and the corresponding parameters
1589 2
     * as specified in the given `Accept` header. The array keys of the return value are the acceptable values,
1590 2
     * while the array values consisting of the corresponding quality scores and parameters. The acceptable
1591 2
     * values with the highest quality scores will be returned first. For example,
1592 2
     *
1593
     * ```php
1594 2
     * $header = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;';
1595
     * $accepts = $request->parseAcceptHeader($header);
1596
     * print_r($accepts);
1597 1
     * // displays:
1598
     * // [
1599
     * //     'application/json' => ['q' => 1, 'version' => '1.0'],
1600 3
     * //      'application/xml' => ['q' => 1, 'version' => '2.0'],
1601
     * //           'text/plain' => ['q' => 0.5],
1602
     * // ]
1603 3
     * ```
1604 3
     *
1605 3
     * @param string $header the header to be parsed
1606 3
     * @return array the acceptable values ordered by their quality score. The values with the highest scores
1607 2
     * will be returned first.
1608
     */
1609
    public function parseAcceptHeader($header)
1610 2
    {
1611 1
        $accepts = [];
1612
        foreach (explode(',', $header) as $i => $part) {
1613
            $params = preg_split('/\s*;\s*/', trim($part), -1, PREG_SPLIT_NO_EMPTY);
1614 2
            if (empty($params)) {
1615
                continue;
1616
            }
1617
            $values = [
1618 2
                'q' => [$i, array_shift($params), 1],
1619
            ];
1620
            foreach ($params as $param) {
1621
                if (strpos($param, '=') !== false) {
1622 2
                    list($key, $value) = explode('=', $param, 2);
1623
                    if ($key === 'q') {
1624
                        $values['q'][2] = (float) $value;
1625
                    } else {
1626 2
                        $values[$key] = $value;
1627 2
                    }
1628 2
                } else {
1629
                    $values[] = $param;
1630
                }
1631
            }
1632 2
            $accepts[] = $values;
1633 3
        }
1634
1635 3
        usort($accepts, function ($a, $b) {
1636 3
            $a = $a['q']; // index, name, q
1637 3
            $b = $b['q'];
1638 3
            if ($a[2] > $b[2]) {
1639 3
                return -1;
1640
            }
1641
1642 3
            if ($a[2] < $b[2]) {
1643
                return 1;
1644
            }
1645
1646
            if ($a[1] === $b[1]) {
1647
                return $a[0] > $b[0] ? 1 : -1;
1648
            }
1649
1650
            if ($a[1] === '*/*') {
1651
                return 1;
1652
            }
1653 1
1654
            if ($b[1] === '*/*') {
1655 1
                return -1;
1656 1
            }
1657
1658 1
            $wa = $a[1][strlen($a[1]) - 1] === '*';
1659 1
            $wb = $b[1][strlen($b[1]) - 1] === '*';
1660 1
            if ($wa xor $wb) {
1661 1
                return $wa ? 1 : -1;
1662
            }
1663
1664 1
            return $a[0] > $b[0] ? 1 : -1;
1665 1
        });
1666 1
1667
        $result = [];
1668 1
        foreach ($accepts as $accept) {
1669
            $name = $accept['q'][1];
1670
            $accept['q'] = $accept['q'][2];
1671
            $result[$name] = $accept;
1672
        }
1673 1
1674
        return $result;
1675
    }
1676
1677
    /**
1678
     * Returns the user-preferred language that should be used by this application.
1679
     * The language resolution is based on the user preferred languages and the languages
1680
     * supported by the application. The method will try to find the best match.
1681
     * @param array $languages a list of the languages supported by the application. If this is empty, the current
1682
     * application language will be returned without further processing.
1683
     * @return string the language that the application should use.
1684
     */
1685
    public function getPreferredLanguage(array $languages = [])
1686
    {
1687
        if (empty($languages)) {
1688
            return Yii::$app->language;
1689
        }
1690
        foreach ($this->getAcceptableLanguages() as $acceptableLanguage) {
1691
            $acceptableLanguage = str_replace('_', '-', strtolower($acceptableLanguage));
1692
            foreach ($languages as $language) {
1693
                $normalizedLanguage = str_replace('_', '-', strtolower($language));
1694
1695
                if (
1696
                    $normalizedLanguage === $acceptableLanguage // en-us==en-us
1697
                    || strpos($acceptableLanguage, $normalizedLanguage . '-') === 0 // en==en-us
1698
                    || strpos($normalizedLanguage, $acceptableLanguage . '-') === 0 // en-us==en
1699
                ) {
1700
                    return $language;
1701
                }
1702
            }
1703
        }
1704
1705
        return reset($languages);
1706
    }
1707 83
1708
    /**
1709 83
     * Gets the Etags.
1710 83
     *
1711 83
     * @return array The entity tags
1712 83
     */
1713
    public function getETags()
1714
    {
1715 82
        if ($this->headers->has('If-None-Match')) {
1716
            return preg_split('/[\s,]+/', str_replace('-gzip', '', $this->headers->get('If-None-Match')), -1, PREG_SPLIT_NO_EMPTY);
1717
        }
1718
1719
        return [];
1720
    }
1721
1722
    /**
1723 83
     * Returns the cookie collection.
1724
     *
1725 83
     * Through the returned cookie collection, you may access a cookie using the following syntax:
1726 83
     *
1727 82
     * ```php
1728 1
     * $cookie = $request->cookies['name']
1729
     * if ($cookie !== null) {
1730 81
     *     $value = $cookie->value;
1731
     * }
1732
     *
1733
     * // alternatively
1734
     * $value = $request->cookies->getValue('name');
1735
     * ```
1736
     *
1737
     * @return CookieCollection the cookie collection.
1738
     */
1739
    public function getCookies()
1740
    {
1741
        if ($this->_cookies === null) {
1742
            $this->_cookies = new CookieCollection($this->loadCookies(), [
1743
                'readOnly' => true,
1744
            ]);
1745
        }
1746
1747
        return $this->_cookies;
1748
    }
1749
1750
    /**
1751
     * Converts `$_COOKIE` into an array of [[Cookie]].
1752
     * @return array the cookies obtained from request
1753 1
     * @throws InvalidConfigException if [[cookieValidationKey]] is not set when [[enableCookieValidation]] is true
1754 1
     */
1755 1
    protected function loadCookies()
1756 1
    {
1757 1
        $cookies = [];
1758 1
        if ($this->enableCookieValidation) {
1759 1
            if ($this->cookieValidationKey == '') {
1760
                throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
1761
            }
1762
            foreach ($_COOKIE as $name => $value) {
1763 82
                if (!is_string($value)) {
1764
                    continue;
1765
                }
1766
                $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
1767
                if ($data === false) {
1768
                    continue;
1769
                }
1770
                if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {
1771
                    $data = @unserialize($data, ['allowed_classes' => false]);
1772
                } else {
1773
                    $data = @unserialize($data);
1774
                }
1775
                if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) {
1776
                    $cookies[$name] = Yii::createObject([
1777 89
                        'class' => 'yii\web\Cookie',
1778
                        'name' => $name,
1779 89
                        'value' => $data[1],
1780 89
                        'expire' => null,
1781 88
                    ]);
1782 85
                }
1783
            }
1784 88
        } else {
1785
            foreach ($_COOKIE as $name => $value) {
1786
                $cookies[$name] = Yii::createObject([
1787 88
                    'class' => 'yii\web\Cookie',
1788
                    'name' => $name,
1789
                    'value' => $value,
1790
                    'expire' => null,
1791
                ]);
1792
            }
1793
        }
1794
1795 89
        return $cookies;
1796
    }
1797 89
1798 85
    private $_csrfToken;
1799
1800
    /**
1801 4
     * Returns the token used to perform CSRF validation.
1802
     *
1803
     * This token is generated in a way to prevent [BREACH attacks](https://en.wikipedia.org/wiki/BREACH). It may be passed
1804
     * along via a hidden field of an HTML form or an HTTP header value to support CSRF validation.
1805
     * @param bool $regenerate whether to regenerate CSRF token. When this parameter is true, each time
1806
     * this method is called, a new CSRF token will be generated and persisted (in session or cookie).
1807
     * @return null|string the token used to perform CSRF validation. Null is returned if the [[validateCsrfHeaderOnly]] is true.
1808 85
     */
1809
    public function getCsrfToken($regenerate = false)
1810 85
    {
1811 85
        if ($this->validateCsrfHeaderOnly) {
1812 84
            return null;
1813 84
        }
1814
1815 1
        if ($this->_csrfToken === null || $regenerate) {
1816
            $token = $this->loadCsrfToken();
1817
            if ($regenerate || empty($token)) {
1818 85
                $token = $this->generateCsrfToken();
1819
            }
1820
            $this->_csrfToken = Yii::$app->security->maskToken($token);
1821
        }
1822
1823
        return $this->_csrfToken;
1824 3
    }
1825
1826 3
    /**
1827
     * Loads the CSRF token from cookie or session.
1828
     * @return string|null the CSRF token loaded from cookie or session. Null is returned if the cookie or session
1829
     * does not have CSRF token.
1830
     */
1831
    protected function loadCsrfToken()
1832
    {
1833
        if ($this->enableCsrfCookie) {
1834
            return $this->getCookies()->getValue($this->csrfParam);
1835
        }
1836 84
1837
        return Yii::$app->getSession()->get($this->csrfParam);
0 ignored issues
show
Bug introduced by
The method getSession() does not exist on yii\console\Application. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1837
        return Yii::$app->/** @scrutinizer ignore-call */ getSession()->get($this->csrfParam);
Loading history...
1838 84
    }
1839 84
1840 84
    /**
1841 84
     * Generates an unmasked random token used to perform CSRF validation.
1842 84
     * @return string the random token for CSRF validation.
1843 84
     */
1844
    protected function generateCsrfToken()
1845
    {
1846
        $token = Yii::$app->getSecurity()->generateRandomString();
1847
        if ($this->enableCsrfCookie) {
1848
            $cookie = $this->createCsrfCookie($token);
1849
            Yii::$app->getResponse()->getCookies()->add($cookie);
0 ignored issues
show
Bug introduced by
The method getCookies() does not exist on yii\console\Response. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1849
            Yii::$app->getResponse()->/** @scrutinizer ignore-call */ getCookies()->add($cookie);
Loading history...
1850
        } else {
1851
            Yii::$app->getSession()->set($this->csrfParam, $token);
1852
        }
1853
1854
        return $token;
1855
    }
1856
1857
    /**
1858
     * @return string|null the CSRF token sent via [[csrfHeader]] by browser. Null is returned if no such header is sent.
1859
     */
1860 8
    public function getCsrfTokenFromHeader()
1861
    {
1862 8
        return $this->headers->get($this->csrfHeader);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->headers->get($this->csrfHeader) also could return the type array which is incompatible with the documented return type null|string.
Loading history...
1863
    }
1864 8
1865 7
    /**
1866
     * Creates a cookie with a randomly generated CSRF token.
1867
     * Initial values specified in [[csrfCookie]] will be applied to the generated cookie.
1868 4
     * @param string $token the CSRF token
1869
     * @return Cookie the generated cookie
1870 4
     * @see enableCsrfValidation
1871 2
     */
1872
    protected function createCsrfCookie($token)
1873
    {
1874 3
        $options = $this->csrfCookie;
1875 3
        return Yii::createObject(array_merge($options, [
1876
            'class' => 'yii\web\Cookie',
1877
            'name' => $this->csrfParam,
1878
            'value' => $token,
1879
        ]));
1880
    }
1881
1882
    /**
1883
     * Performs the CSRF validation.
1884
     *
1885 4
     * This method will validate the user-provided CSRF token by comparing it with the one stored in cookie or session.
1886
     * This method is mainly called in [[Controller::beforeAction()]].
1887 4
     *
1888 3
     * Note that the method will NOT perform CSRF validation if [[enableCsrfValidation]] is false or the HTTP method
1889
     * is among GET, HEAD or OPTIONS.
1890
     *
1891 4
     * @param string|null $clientSuppliedToken the user-provided CSRF token to be validated. If null, the token will be retrieved from
1892
     * the [[csrfParam]] POST field or HTTP header.
1893 4
     * This parameter is available since version 2.0.4.
1894
     * @return bool whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true.
1895
     */
1896
    public function validateCsrfToken($clientSuppliedToken = null)
1897
    {
1898
        $method = $this->getMethod();
1899
1900
        if ($this->validateCsrfHeaderOnly) {
1901
            return in_array($method, $this->csrfHeaderUnsafeMethods, true)
1902
                ? $this->headers->has($this->csrfHeader)
1903
                : true;
1904
        }
1905 166
1906
        if (!$this->enableCsrfValidation || in_array($method, $this->csrfTokenSafeMethods, true)) {
1907 166
            return true;
1908
        }
1909 166
1910 20
        $trueToken = $this->getCsrfToken();
1911 20
1912 20
        if ($clientSuppliedToken !== null) {
1913
            return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken);
1914
        }
1915 151
1916
        return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
1917
            || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
1918
    }
1919
1920
    /**
1921
     * Validates CSRF token.
1922
     *
1923
     * @param string $clientSuppliedToken The masked client-supplied token.
1924
     * @param string $trueToken The masked true token.
1925
     * @return bool
1926
     */
1927 166
    private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
1928
    {
1929 166
        if (!is_string($clientSuppliedToken)) {
0 ignored issues
show
introduced by
The condition is_string($clientSuppliedToken) is always true.
Loading history...
1930 81
            return false;
1931
        }
1932
1933 166
        $security = Yii::$app->security;
1934 166
1935 166
        return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
1936 74
    }
1937 72
1938
    /**
1939 74
     * Gets first `Forwarded` header value for token
1940
     *
1941 166
     * @param string $token Header token
1942
     *
1943 166
     * @return string|null
1944 166
     *
1945 166
     * @since 2.0.31
1946 20
     */
1947 166
    protected function getSecureForwardedHeaderTrustedPart($token)
1948 166
    {
1949
        $token = strtolower($token);
1950 166
1951
        if ($parts = $this->getSecureForwardedHeaderTrustedParts()) {
1952
            $lastElement = array_pop($parts);
1953
            if ($lastElement && isset($lastElement[$token])) {
1954
                return $lastElement[$token];
1955
            }
1956
        }
1957
        return null;
1958
    }
1959
1960
    private $_secureForwardedHeaderTrustedParts;
1961
1962 166
    /**
1963
     * Gets only trusted `Forwarded` header parts
1964 166
     *
1965
     * @return array
1966
     *
1967 166
     * @since 2.0.31
1968 109
     */
1969
    protected function getSecureForwardedHeaderTrustedParts()
1970
    {
1971
        if ($this->_secureForwardedHeaderTrustedParts !== null) {
1972
            return $this->_secureForwardedHeaderTrustedParts;
1973
        }
1974
1975
        $validator = $this->getIpValidator();
1976
        $trustedHosts = [];
1977
        foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) {
1978
            if (!is_array($trustedCidrOrHeaders)) {
1979
                $trustedCidr = $trustedCidrOrHeaders;
1980 57
            }
1981 57
            $trustedHosts[] = $trustedCidr;
1982 37
        }
1983
        $validator->setRanges($trustedHosts);
1984
1985 20
        $this->_secureForwardedHeaderTrustedParts = array_filter(
1986
            $this->getSecureForwardedHeaderParts(),
1987 20
            function ($headerPart) use ($validator) {
1988 20
                return isset($headerPart['for']) ? !$validator->validate($headerPart['for']) : true;
1989 20
            }
1990 20
        );
1991 20
1992 4
        return $this->_secureForwardedHeaderTrustedParts;
1993
    }
1994 20
1995 20
    private $_secureForwardedHeaderParts;
1996 20
1997
    /**
1998 20
     * Returns decoded forwarded header
1999
     *
2000
     * @return array
2001
     *
2002
     * @since 2.0.31
2003
     */
2004
    protected function getSecureForwardedHeaderParts()
2005
    {
2006
        if ($this->_secureForwardedHeaderParts !== null) {
2007
            return $this->_secureForwardedHeaderParts;
2008
        }
2009
        if (count(preg_grep('/^forwarded$/i', $this->secureHeaders)) === 0) {
2010
            return $this->_secureForwardedHeaderParts = [];
2011
        }
2012
        /*
2013
         * First header is always correct, because proxy CAN add headers
2014
         * after last one is found.
2015
         * Keep in mind that it is NOT enforced, therefore we cannot be
2016
         * sure, that this is really a first one.
2017
         *
2018
         * FPM keeps last header sent which is a bug. You need to merge
2019
         * headers together on your web server before letting FPM handle it
2020
         * @see https://bugs.php.net/bug.php?id=78844
2021
         */
2022
        $forwarded = $this->headers->get('Forwarded', '');
2023
        if ($forwarded === '') {
2024
            return $this->_secureForwardedHeaderParts = [];
2025
        }
2026
2027
        preg_match_all('/(?:[^",]++|"[^"]++")+/', $forwarded, $forwardedElements);
2028
2029
        foreach ($forwardedElements[0] as $forwardedPairs) {
2030
            preg_match_all('/(?P<key>\w+)\s*=\s*(?:(?P<value>[^",;]*[^",;\s])|"(?P<value2>[^"]+)")/', $forwardedPairs, $matches, PREG_SET_ORDER);
2031
            $this->_secureForwardedHeaderParts[] = array_reduce($matches, function ($carry, $item) {
2032
                $value = $item['value'];
2033
                if (isset($item['value2']) && $item['value2'] !== '') {
2034
                    $value = $item['value2'];
2035
                }
2036
                $carry[strtolower($item['key'])] = $value;
2037
                return $carry;
2038
            }, []);
2039
        }
2040
        return $this->_secureForwardedHeaderParts;
2041
    }
2042
}
2043