Request::getUserIpFromIpHeader()   B
last analyzed

Complexity

Conditions 8
Paths 13

Size

Total Lines 32
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 8.0046

Importance

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

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

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

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

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

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

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

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

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

1508
                $this->_contentTypes = $this->parseAcceptHeader(/** @scrutinizer ignore-type */ $this->headers->get('Accept'));
Loading history...
1509
            } else {
1510 3
                $this->_contentTypes = [];
1511
            }
1512
        }
1513
1514 5
        return $this->_contentTypes;
1515
    }
1516
1517
    /**
1518
     * Sets the acceptable content types.
1519
     * Please refer to [[getAcceptableContentTypes()]] on the format of the parameter.
1520
     * @param array $value the content types that are acceptable by the end user. They should
1521
     * be ordered by the preference level.
1522
     * @see getAcceptableContentTypes()
1523
     * @see parseAcceptHeader()
1524
     */
1525 1
    public function setAcceptableContentTypes($value)
1526
    {
1527 1
        $this->_contentTypes = $value;
1528
    }
1529
1530
    /**
1531
     * Returns request content-type
1532
     * The Content-Type header field indicates the MIME type of the data
1533
     * contained in [[getRawBody()]] or, in the case of the HEAD method, the
1534
     * media type that would have been sent had the request been a GET.
1535
     * For the MIME-types the user expects in response, see [[acceptableContentTypes]].
1536
     * @return string request content-type. Empty string is returned if this information is not available.
1537
     * @link https://tools.ietf.org/html/rfc2616#section-14.17
1538
     * HTTP 1.1 header field definitions
1539
     */
1540 8
    public function getContentType()
1541
    {
1542 8
        if (isset($_SERVER['CONTENT_TYPE'])) {
1543 3
            return $_SERVER['CONTENT_TYPE'];
1544
        }
1545
1546
        //fix bug https://bugs.php.net/bug.php?id=66606
1547 5
        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...
1548
    }
1549
1550
    private $_languages;
1551
1552
    /**
1553
     * Returns the languages acceptable by the end user.
1554
     * This is determined by the `Accept-Language` HTTP header.
1555
     * @return array the languages ordered by the preference level. The first element
1556
     * represents the most preferred language.
1557
     */
1558 2
    public function getAcceptableLanguages()
1559
    {
1560 2
        if ($this->_languages === null) {
1561 1
            if ($this->headers->has('Accept-Language')) {
1562
                $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

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

1832
        return Yii::$app->/** @scrutinizer ignore-call */ getSession()->get($this->csrfParam);
Loading history...
1833
    }
1834
1835
    /**
1836
     * Generates an unmasked random token used to perform CSRF validation.
1837
     * @return string the random token for CSRF validation.
1838
     */
1839 85
    protected function generateCsrfToken()
1840
    {
1841 85
        $token = Yii::$app->getSecurity()->generateRandomString();
1842 85
        if ($this->enableCsrfCookie) {
1843 84
            $cookie = $this->createCsrfCookie($token);
1844 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

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