Request::resolveRequestUri()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 11.8162

Importance

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

416
            return strtoupper(/** @scrutinizer ignore-type */ $this->headers->get('X-Http-Method-Override'));
Loading history...
417
        }
418
419 40
        if (isset($_SERVER['REQUEST_METHOD'])) {
420 10
            return strtoupper($_SERVER['REQUEST_METHOD']);
421
        }
422
423 31
        return 'GET';
424
    }
425
426
    /**
427
     * Returns whether this is a GET request.
428
     * @return bool whether this is a GET request.
429
     */
430 2
    public function getIsGet()
431
    {
432 2
        return $this->getMethod() === 'GET';
433
    }
434
435
    /**
436
     * Returns whether this is an OPTIONS request.
437
     * @return bool whether this is a OPTIONS request.
438
     */
439 3
    public function getIsOptions()
440
    {
441 3
        return $this->getMethod() === 'OPTIONS';
442
    }
443
444
    /**
445
     * Returns whether this is a HEAD request.
446
     * @return bool whether this is a HEAD request.
447
     */
448 15
    public function getIsHead()
449
    {
450 15
        return $this->getMethod() === 'HEAD';
451
    }
452
453
    /**
454
     * Returns whether this is a POST request.
455
     * @return bool whether this is a POST request.
456
     */
457
    public function getIsPost()
458
    {
459
        return $this->getMethod() === 'POST';
460
    }
461
462
    /**
463
     * Returns whether this is a DELETE request.
464
     * @return bool whether this is a DELETE request.
465
     */
466
    public function getIsDelete()
467
    {
468
        return $this->getMethod() === 'DELETE';
469
    }
470
471
    /**
472
     * Returns whether this is a PUT request.
473
     * @return bool whether this is a PUT request.
474
     */
475
    public function getIsPut()
476
    {
477
        return $this->getMethod() === 'PUT';
478
    }
479
480
    /**
481
     * Returns whether this is a PATCH request.
482
     * @return bool whether this is a PATCH request.
483
     */
484
    public function getIsPatch()
485
    {
486
        return $this->getMethod() === 'PATCH';
487
    }
488
489
    /**
490
     * Returns whether this is an AJAX (XMLHttpRequest) request.
491
     *
492
     * Note that in case of cross domain requests, browser doesn't set the X-Requested-With header by default:
493
     * https://stackoverflow.com/questions/8163703/cross-domain-ajax-doesnt-send-x-requested-with-header
494
     *
495
     * In case you are using `fetch()`, pass header manually:
496
     *
497
     * ```
498
     * fetch(url, {
499
     *    method: 'GET',
500
     *    headers: {'X-Requested-With': 'XMLHttpRequest'}
501
     * })
502
     * ```
503
     *
504
     * @return bool whether this is an AJAX (XMLHttpRequest) request.
505
     */
506 16
    public function getIsAjax()
507
    {
508 16
        return $this->headers->get('X-Requested-With') === 'XMLHttpRequest';
509
    }
510
511
    /**
512
     * Returns whether this is a PJAX request.
513
     * @return bool whether this is a PJAX request
514
     */
515 3
    public function getIsPjax()
516
    {
517 3
        return $this->getIsAjax() && $this->headers->has('X-Pjax');
518
    }
519
520
    /**
521
     * Returns whether this is an Adobe Flash or Flex request.
522
     * @return bool whether this is an Adobe Flash or Adobe Flex request.
523
     */
524
    public function getIsFlash()
525
    {
526
        $userAgent = $this->headers->get('User-Agent', '');
527
        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

527
        return stripos(/** @scrutinizer ignore-type */ $userAgent, 'Shockwave') !== false
Loading history...
528
            || stripos($userAgent, 'Flash') !== false;
529
    }
530
531
    private $_rawBody;
532
533
    /**
534
     * Returns the raw HTTP request body.
535
     * @return string the request body
536
     */
537 7
    public function getRawBody()
538
    {
539 7
        if ($this->_rawBody === null) {
540 4
            $this->_rawBody = file_get_contents('php://input');
541
        }
542
543 7
        return $this->_rawBody;
544
    }
545
546
    /**
547
     * Sets the raw HTTP request body, this method is mainly used by test scripts to simulate raw HTTP requests.
548
     * @param string $rawBody the request body
549
     */
550 3
    public function setRawBody($rawBody)
551
    {
552 3
        $this->_rawBody = $rawBody;
553
    }
554
555
    private $_bodyParams;
556
557
    /**
558
     * Returns the request parameters given in the request body.
559
     *
560
     * Request parameters are determined using the parsers configured in [[parsers]] property.
561
     * If no parsers are configured for the current [[contentType]] it uses the PHP function `mb_parse_str()`
562
     * to parse the [[rawBody|request body]].
563
     * @return array|object the request parameters given in the request body.
564
     * @throws \yii\base\InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]].
565
     * @see getMethod()
566
     * @see getBodyParam()
567
     * @see setBodyParams()
568
     */
569 16
    public function getBodyParams()
570
    {
571 16
        if ($this->_bodyParams === null) {
572 8
            if (isset($_POST[$this->methodParam])) {
573 1
                $this->_bodyParams = $_POST;
574 1
                unset($this->_bodyParams[$this->methodParam]);
575 1
                return $this->_bodyParams;
576
            }
577
578 7
            $rawContentType = $this->getContentType();
579 7
            if (($pos = strpos((string)$rawContentType, ';')) !== false) {
580
                // e.g. text/html; charset=UTF-8
581
                $contentType = substr($rawContentType, 0, $pos);
582
            } else {
583 7
                $contentType = $rawContentType;
584
            }
585
586 7
            if (isset($this->parsers[$contentType])) {
587 2
                $parser = Yii::createObject($this->parsers[$contentType]);
588 2
                if (!($parser instanceof RequestParserInterface)) {
589
                    throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface.");
590
                }
591 2
                $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
592 5
            } elseif (isset($this->parsers['*'])) {
593
                $parser = Yii::createObject($this->parsers['*']);
594
                if (!($parser instanceof RequestParserInterface)) {
595
                    throw new InvalidConfigException('The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface.');
596
                }
597
                $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
598 5
            } elseif ($this->getMethod() === 'POST') {
599
                // PHP has already parsed the body so we have all params in $_POST
600
                $this->_bodyParams = $_POST;
601
            } else {
602 5
                $this->_bodyParams = [];
603 5
                mb_parse_str($this->getRawBody(), $this->_bodyParams);
604
            }
605
        }
606
607 16
        return $this->_bodyParams;
608
    }
609
610
    /**
611
     * Sets the request body parameters.
612
     *
613
     * @param array|object $values the request body parameters (name-value pairs)
614
     * @see getBodyParams()
615
     */
616 11
    public function setBodyParams($values)
617
    {
618 11
        $this->_bodyParams = $values;
619
    }
620
621
    /**
622
     * Returns the named request body parameter value.
623
     *
624
     * If the parameter does not exist, the second parameter passed to this method will be returned.
625
     *
626
     * @param string $name the parameter name
627
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
628
     * @return mixed the parameter value
629
     * @see getBodyParams()
630
     * @see setBodyParams()
631
     */
632 7
    public function getBodyParam($name, $defaultValue = null)
633
    {
634 7
        $params = $this->getBodyParams();
635
636 7
        if (is_object($params)) {
637
            // unable to use `ArrayHelper::getValue()` due to different dots in key logic and lack of exception handling
638
            try {
639 1
                return isset($params->{$name}) ? $params->{$name} : $defaultValue;
640
            } 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...
641
                return $defaultValue;
642
            }
643
        }
644
645 7
        return isset($params[$name]) ? $params[$name] : $defaultValue;
646
    }
647
648
    /**
649
     * Returns POST parameter with a given name. If name isn't specified, returns an array of all POST parameters.
650
     *
651
     * @param string $name the parameter name
652
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
653
     * @return array|mixed
654
     */
655
    public function post($name = null, $defaultValue = null)
656
    {
657
        if ($name === null) {
658
            return $this->getBodyParams();
659
        }
660
661
        return $this->getBodyParam($name, $defaultValue);
662
    }
663
664
    private $_queryParams;
665
666
    /**
667
     * Returns the request parameters given in the [[queryString]].
668
     *
669
     * This method will return the contents of `$_GET` if params where not explicitly set.
670
     * @return array the request GET parameter values.
671
     * @see setQueryParams()
672
     */
673 65
    public function getQueryParams()
674
    {
675 65
        if ($this->_queryParams === null) {
676 58
            return $_GET;
677
        }
678
679 9
        return $this->_queryParams;
680
    }
681
682
    /**
683
     * Sets the request [[queryString]] parameters.
684
     * @param array $values the request query parameters (name-value pairs)
685
     * @see getQueryParam()
686
     * @see getQueryParams()
687
     */
688 9
    public function setQueryParams($values)
689
    {
690 9
        $this->_queryParams = $values;
691
    }
692
693
    /**
694
     * Returns GET parameter with a given name. If name isn't specified, returns an array of all GET parameters.
695
     *
696
     * @param string $name the parameter name
697
     * @param mixed $defaultValue the default parameter value if the parameter does not exist.
698
     * @return array|mixed
699
     */
700 23
    public function get($name = null, $defaultValue = null)
701
    {
702 23
        if ($name === null) {
703
            return $this->getQueryParams();
704
        }
705
706 23
        return $this->getQueryParam($name, $defaultValue);
707
    }
708
709
    /**
710
     * Returns the named GET parameter value.
711
     * If the GET parameter does not exist, the second parameter passed to this method will be returned.
712
     * @param string $name the GET parameter name.
713
     * @param mixed $defaultValue the default parameter value if the GET parameter does not exist.
714
     * @return mixed the GET parameter value
715
     * @see getBodyParam()
716
     */
717 26
    public function getQueryParam($name, $defaultValue = null)
718
    {
719 26
        $params = $this->getQueryParams();
720
721 26
        return isset($params[$name]) ? $params[$name] : $defaultValue;
722
    }
723
724
    private $_hostInfo;
725
    private $_hostName;
726
727
    /**
728
     * Returns the schema and host part of the current request URL.
729
     *
730
     * The returned URL does not have an ending slash.
731
     *
732
     * By default this value is based on the user request information. This method will
733
     * return the value of `$_SERVER['HTTP_HOST']` if it is available or `$_SERVER['SERVER_NAME']` if not.
734
     * You may want to check out the [PHP documentation](https://www.php.net/manual/en/reserved.variables.server.php)
735
     * for more information on these variables.
736
     *
737
     * You may explicitly specify it by setting the [[setHostInfo()|hostInfo]] property.
738
     *
739
     * > Warning: Dependent on the server configuration this information may not be
740
     * > reliable and [may be faked by the user sending the HTTP request](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
741
     * > If the webserver is configured to serve the same site independent of the value of
742
     * > the `Host` header, this value is not reliable. In such situations you should either
743
     * > fix your webserver configuration or explicitly set the value by setting the [[setHostInfo()|hostInfo]] property.
744
     * > If you don't have access to the server configuration, you can setup [[\yii\filters\HostControl]] filter at
745
     * > application level in order to protect against such kind of attack.
746
     *
747
     * @property string|null schema and hostname part (with port number if needed) of the request URL
748
     * (e.g. `https://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set.
749
     * See [[getHostInfo()]] for security related notes on this property.
750
     * @return string|null schema and hostname part (with port number if needed) of the request URL
751
     * (e.g. `https://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set.
752
     * @see setHostInfo()
753
     */
754 49
    public function getHostInfo()
755
    {
756 49
        if ($this->_hostInfo === null) {
757 45
            $secure = $this->getIsSecureConnection();
758 45
            $http = $secure ? 'https' : 'http';
759
760 45
            if ($this->getSecureForwardedHeaderTrustedPart('host') !== null) {
761 8
                $this->_hostInfo = $http . '://' . $this->getSecureForwardedHeaderTrustedPart('host');
762 37
            } elseif ($this->headers->has('X-Forwarded-Host')) {
763 3
                $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

763
                $this->_hostInfo = $http . '://' . trim(explode(',', /** @scrutinizer ignore-type */ $this->headers->get('X-Forwarded-Host'))[0]);
Loading history...
764 34
            } elseif ($this->headers->has('X-Original-Host')) {
765
                $this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Original-Host'))[0]);
766 34
            } elseif ($this->headers->has('Host')) {
767 11
                $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

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

1116
                    if (strcasecmp(/** @scrutinizer ignore-type */ $headerValue, $value) === 0) {
Loading history...
1117 3
                        return true;
1118
                    }
1119
                }
1120
            }
1121
        }
1122
1123 59
        return false;
1124
    }
1125
1126
    /**
1127
     * Returns the server name.
1128
     * @return string|null server name, null if not available
1129
     */
1130 1
    public function getServerName()
1131
    {
1132 1
        return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
1133
    }
1134
1135
    /**
1136
     * Returns the server port number. If a port is specified via a forwarding header (e.g. 'X-Forwarded-Port')
1137
     * and the remote host is a "trusted host" the that port will be used (see [[portHeaders]]),
1138
     * otherwise the default server port will be returned.
1139
     * @return int|null server port number, null if not available
1140
     * @see portHeaders
1141
     */
1142 8
    public function getServerPort()
1143
    {
1144 8
        foreach ($this->portHeaders as $portHeader) {
1145 8
            if ($this->headers->has($portHeader)) {
1146 2
                $port = $this->headers->get($portHeader);
1147 2
                if ($port !== null) {
1148 2
                    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...
1149
                }
1150
            }
1151
        }
1152
1153 6
        return isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null;
1154
    }
1155
1156
    /**
1157
     * Returns the URL referrer.
1158
     * @return string|null URL referrer, null if not available
1159
     */
1160
    public function getReferrer()
1161
    {
1162
        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...
1163
    }
1164
1165
    /**
1166
     * Returns the URL origin of a CORS request.
1167
     *
1168
     * The return value is taken from the `Origin` [[getHeaders()|header]] sent by the browser.
1169
     *
1170
     * Note that the origin request header indicates where a fetch originates from.
1171
     * It doesn't include any path information, but only the server name.
1172
     * It is sent with a CORS requests, as well as with POST requests.
1173
     * It is similar to the referer header, but, unlike this header, it doesn't disclose the whole path.
1174
     * Please refer to <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin> for more information.
1175
     *
1176
     * @return string|null URL origin of a CORS request, `null` if not available.
1177
     * @see getHeaders()
1178
     * @since 2.0.13
1179
     */
1180 1
    public function getOrigin()
1181
    {
1182 1
        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...
1183
    }
1184
1185
    /**
1186
     * Returns the user agent.
1187
     * @return string|null user agent, null if not available
1188
     */
1189 1
    public function getUserAgent()
1190
    {
1191 1
        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...
1192
    }
1193
1194
    /**
1195
     * Returns the user IP address from [[ipHeaders]].
1196
     * @return string|null user IP address, null if not available
1197
     * @see ipHeaders
1198
     * @since 2.0.28
1199
     */
1200 105
    protected function getUserIpFromIpHeaders()
1201
    {
1202 105
        $ip = $this->getSecureForwardedHeaderTrustedPart('for');
1203
        if (
1204 105
            $ip !== null && preg_match(
1205 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+))?$/',
1206 105
                $ip,
1207 105
                $matches
1208 105
            )
1209
        ) {
1210 14
            $ip = $this->getUserIpFromIpHeader($matches['ip']);
1211 14
            if ($ip !== null) {
1212 14
                return $ip;
1213
            }
1214
        }
1215
1216
1217 91
        foreach ($this->ipHeaders as $ipHeader) {
1218 88
            if ($this->headers->has($ipHeader)) {
1219 10
                $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

1219
                $ip = $this->getUserIpFromIpHeader(/** @scrutinizer ignore-type */ $this->headers->get($ipHeader));
Loading history...
1220 10
                if ($ip !== null) {
1221 10
                    return $ip;
1222
                }
1223
            }
1224
        }
1225 81
        return null;
1226
    }
1227
1228
    private $_ip = null;
1229
1230
    /**
1231
     * Returns the user IP address.
1232
     * The IP is determined using headers and / or `$_SERVER` variables.
1233
     * @return string|null user IP address, null if not available
1234
     */
1235 105
    public function getUserIP()
1236
    {
1237 105
        if ($this->_ip === null) {
1238 105
            $this->_ip = $this->getUserIpFromIpHeaders();
1239 105
            if ($this->_ip === null) {
1240 81
                $this->_ip = $this->getRemoteIP();
1241
            }
1242
        }
1243
1244 105
        return $this->_ip;
1245
    }
1246
1247
    /**
1248
     * Return user IP's from IP header.
1249
     *
1250
     * @param string $ips comma separated IP list
1251
     * @return string|null IP as string. Null is returned if IP can not be determined from header.
1252
     * @see getUserHost()
1253
     * @see ipHeaders
1254
     * @see getTrustedHeaders()
1255
     * @since 2.0.28
1256
     */
1257 24
    protected function getUserIpFromIpHeader($ips)
1258
    {
1259 24
        $ips = trim($ips);
1260 24
        if ($ips === '') {
1261
            return null;
1262
        }
1263 24
        $ips = preg_split('/\s*,\s*/', $ips, -1, PREG_SPLIT_NO_EMPTY);
1264 24
        krsort($ips);
1265 24
        $validator = $this->getIpValidator();
1266 24
        $resultIp = null;
1267 24
        foreach ($ips as $ip) {
1268 24
            $validator->setRanges('any');
1269 24
            if (!$validator->validate($ip) /* checking IP format */) {
1270 1
                break;
1271
            }
1272 24
            $resultIp = $ip;
1273 24
            $isTrusted = false;
1274 24
            foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) {
1275 24
                if (!is_array($trustedCidrOrHeaders)) {
1276 24
                    $trustedCidr = $trustedCidrOrHeaders;
1277
                }
1278 24
                $validator->setRanges($trustedCidr);
1279 24
                if ($validator->validate($ip) /* checking trusted range */) {
1280 7
                    $isTrusted = true;
1281 7
                    break;
1282
                }
1283
            }
1284 24
            if (!$isTrusted) {
1285 20
                break;
1286
            }
1287
        }
1288 24
        return $resultIp;
1289
    }
1290
1291
    /**
1292
     * Returns the user host name.
1293
     * The HOST is determined using headers and / or `$_SERVER` variables.
1294
     * @return string|null user host name, null if not available
1295
     */
1296
    public function getUserHost()
1297
    {
1298
        $userIp = $this->getUserIpFromIpHeaders();
1299
        if ($userIp === null) {
1300
            return $this->getRemoteHost();
1301
        }
1302
        return gethostbyaddr($userIp);
1303
    }
1304
1305
    /**
1306
     * Returns the IP on the other end of this connection.
1307
     * This is always the next hop, any headers are ignored.
1308
     * @return string|null remote IP address, `null` if not available.
1309
     * @since 2.0.13
1310
     */
1311 142
    public function getRemoteIP()
1312
    {
1313 142
        return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
1314
    }
1315
1316
    /**
1317
     * Returns the host name of the other end of this connection.
1318
     * This is always the next hop, any headers are ignored.
1319
     * @return string|null remote host name, `null` if not available
1320
     * @see getUserHost()
1321
     * @see getRemoteIP()
1322
     * @since 2.0.13
1323
     */
1324
    public function getRemoteHost()
1325
    {
1326
        return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null;
1327
    }
1328
1329
    /**
1330
     * @return string|null the username sent via HTTP authentication, `null` if the username is not given
1331
     * @see getAuthCredentials() to get both username and password in one call
1332
     */
1333 9
    public function getAuthUser()
1334
    {
1335 9
        return $this->getAuthCredentials()[0];
1336
    }
1337
1338
    /**
1339
     * @return string|null the password sent via HTTP authentication, `null` if the password is not given
1340
     * @see getAuthCredentials() to get both username and password in one call
1341
     */
1342 9
    public function getAuthPassword()
1343
    {
1344 9
        return $this->getAuthCredentials()[1];
1345
    }
1346
1347
    /**
1348
     * @return array that contains exactly two elements:
1349
     * - 0: the username sent via HTTP authentication, `null` if the username is not given
1350
     * - 1: the password sent via HTTP authentication, `null` if the password is not given
1351
     * @see getAuthUser() to get only username
1352
     * @see getAuthPassword() to get only password
1353
     * @since 2.0.13
1354
     */
1355 39
    public function getAuthCredentials()
1356
    {
1357 39
        $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
1358 39
        $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
1359 39
        if ($username !== null || $password !== null) {
1360 21
            return [$username, $password];
1361
        }
1362
1363
        /**
1364
         * Apache with php-cgi does not pass HTTP Basic authentication to PHP by default.
1365
         * To make it work, add one of the following lines to to your .htaccess file:
1366
         *
1367
         * SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
1368
         * --OR--
1369
         * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
1370
         */
1371 18
        $auth_token = $this->getHeaders()->get('Authorization');
1372
1373 18
        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

1373
        if ($auth_token !== null && strncasecmp(/** @scrutinizer ignore-type */ $auth_token, 'basic', 5) === 0) {
Loading history...
1374 18
            $parts = array_map(function ($value) {
1375 18
                return strlen($value) === 0 ? null : $value;
1376 18
            }, 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

1376
            }, explode(':', base64_decode(mb_substr(/** @scrutinizer ignore-type */ $auth_token, 6)), 2));
Loading history...
1377
1378 18
            if (count($parts) < 2) {
1379 2
                return [$parts[0], null];
1380
            }
1381
1382 16
            return $parts;
1383
        }
1384
1385
        return [null, null];
1386
    }
1387
1388
    private $_port;
1389
1390
    /**
1391
     * Returns the port to use for insecure requests.
1392
     * Defaults to 80, or the port specified by the server if the current
1393
     * request is insecure.
1394
     * @return int port number for insecure requests.
1395
     * @see setPort()
1396
     */
1397 1
    public function getPort()
1398
    {
1399 1
        if ($this->_port === null) {
1400 1
            $serverPort = $this->getServerPort();
1401 1
            $this->_port = !$this->getIsSecureConnection() && $serverPort !== null ? $serverPort : 80;
1402
        }
1403
1404 1
        return $this->_port;
1405
    }
1406
1407
    /**
1408
     * Sets the port to use for insecure requests.
1409
     * This setter is provided in case a custom port is necessary for certain
1410
     * server configurations.
1411
     * @param int $value port number.
1412
     */
1413
    public function setPort($value)
1414
    {
1415
        if ($value != $this->_port) {
1416
            $this->_port = (int) $value;
1417
            $this->_hostInfo = null;
1418
        }
1419
    }
1420
1421
    private $_securePort;
1422
1423
    /**
1424
     * Returns the port to use for secure requests.
1425
     * Defaults to 443, or the port specified by the server if the current
1426
     * request is secure.
1427
     * @return int port number for secure requests.
1428
     * @see setSecurePort()
1429
     */
1430
    public function getSecurePort()
1431
    {
1432
        if ($this->_securePort === null) {
1433
            $serverPort = $this->getServerPort();
1434
            $this->_securePort = $this->getIsSecureConnection() && $serverPort !== null ? $serverPort : 443;
1435
        }
1436
1437
        return $this->_securePort;
1438
    }
1439
1440
    /**
1441
     * Sets the port to use for secure requests.
1442
     * This setter is provided in case a custom port is necessary for certain
1443
     * server configurations.
1444
     * @param int $value port number.
1445
     */
1446
    public function setSecurePort($value)
1447
    {
1448
        if ($value != $this->_securePort) {
1449
            $this->_securePort = (int) $value;
1450
            $this->_hostInfo = null;
1451
        }
1452
    }
1453
1454
    private $_contentTypes;
1455
1456
    /**
1457
     * Returns the content types acceptable by the end user.
1458
     *
1459
     * This is determined by the `Accept` HTTP header. For example,
1460
     *
1461
     * ```php
1462
     * $_SERVER['HTTP_ACCEPT'] = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;';
1463
     * $types = $request->getAcceptableContentTypes();
1464
     * print_r($types);
1465
     * // displays:
1466
     * // [
1467
     * //     'application/json' => ['q' => 1, 'version' => '1.0'],
1468
     * //      'application/xml' => ['q' => 1, 'version' => '2.0'],
1469
     * //           'text/plain' => ['q' => 0.5],
1470
     * // ]
1471
     * ```
1472
     *
1473
     * @return array the content types ordered by the quality score. Types with the highest scores
1474
     * will be returned first. The array keys are the content types, while the array values
1475
     * are the corresponding quality score and other parameters as given in the header.
1476
     */
1477 5
    public function getAcceptableContentTypes()
1478
    {
1479 5
        if ($this->_contentTypes === null) {
1480 4
            if ($this->headers->get('Accept') !== null) {
1481 2
                $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

1481
                $this->_contentTypes = $this->parseAcceptHeader(/** @scrutinizer ignore-type */ $this->headers->get('Accept'));
Loading history...
1482
            } else {
1483 3
                $this->_contentTypes = [];
1484
            }
1485
        }
1486
1487 5
        return $this->_contentTypes;
1488
    }
1489
1490
    /**
1491
     * Sets the acceptable content types.
1492
     * Please refer to [[getAcceptableContentTypes()]] on the format of the parameter.
1493
     * @param array $value the content types that are acceptable by the end user. They should
1494
     * be ordered by the preference level.
1495
     * @see getAcceptableContentTypes()
1496
     * @see parseAcceptHeader()
1497
     */
1498 1
    public function setAcceptableContentTypes($value)
1499
    {
1500 1
        $this->_contentTypes = $value;
1501
    }
1502
1503
    /**
1504
     * Returns request content-type
1505
     * The Content-Type header field indicates the MIME type of the data
1506
     * contained in [[getRawBody()]] or, in the case of the HEAD method, the
1507
     * media type that would have been sent had the request been a GET.
1508
     * For the MIME-types the user expects in response, see [[acceptableContentTypes]].
1509
     * @return string request content-type. Empty string is returned if this information is not available.
1510
     * @link https://tools.ietf.org/html/rfc2616#section-14.17
1511
     * HTTP 1.1 header field definitions
1512
     */
1513 7
    public function getContentType()
1514
    {
1515 7
        if (isset($_SERVER['CONTENT_TYPE'])) {
1516 3
            return $_SERVER['CONTENT_TYPE'];
1517
        }
1518
1519
        //fix bug https://bugs.php.net/bug.php?id=66606
1520 4
        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...
1521
    }
1522
1523
    private $_languages;
1524
1525
    /**
1526
     * Returns the languages acceptable by the end user.
1527
     * This is determined by the `Accept-Language` HTTP header.
1528
     * @return array the languages ordered by the preference level. The first element
1529
     * represents the most preferred language.
1530
     */
1531 2
    public function getAcceptableLanguages()
1532
    {
1533 2
        if ($this->_languages === null) {
1534 1
            if ($this->headers->has('Accept-Language')) {
1535
                $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

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

1801
        return Yii::$app->/** @scrutinizer ignore-call */ getSession()->get($this->csrfParam);
Loading history...
1802
    }
1803
1804
    /**
1805
     * Generates an unmasked random token used to perform CSRF validation.
1806
     * @return string the random token for CSRF validation.
1807
     */
1808 85
    protected function generateCsrfToken()
1809
    {
1810 85
        $token = Yii::$app->getSecurity()->generateRandomString();
1811 85
        if ($this->enableCsrfCookie) {
1812 84
            $cookie = $this->createCsrfCookie($token);
1813 84
            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

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