Passed
Pull Request — 2.2 (#20357)
by Wilmer
13:33 queued 05:55
created

Request   F

Complexity

Total Complexity 286

Size/Duplication

Total Lines 1944
Duplicated Lines 0 %

Test Coverage

Coverage 76.75%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 549
c 3
b 1
f 0
dl 0
loc 1944
ccs 439
cts 572
cp 0.7675
rs 2
wmc 286

83 Methods

Rating   Name   Duplication   Size   Complexity  
A getAuthPassword() 0 3 1
A setScriptFile() 0 3 1
A setSecurePort() 0 5 2
A setAcceptableLanguages() 0 3 1
A getSecurePort() 0 8 4
A getServerPort() 0 12 5
A getRemoteIP() 0 3 2
B getPreferredLanguage() 0 21 7
A getIsPut() 0 3 1
B getUserIpFromIpHeaders() 0 26 7
A getUserAgent() 0 3 1
A getPort() 0 8 4
A setScriptUrl() 0 3 2
A getIsOptions() 0 3 1
A getIsPost() 0 3 1
A getSecureForwardedHeaderTrustedParts() 0 24 5
C getScriptUrl() 0 21 12
A getIsFlash() 0 5 2
B getAuthCredentials() 0 31 9
A getOrigin() 0 3 1
A validateCsrfTokenInternal() 0 9 2
A utf8Encode() 0 12 4
A createCsrfCookie() 0 7 1
A getIsHead() 0 3 1
A getServerName() 0 3 2
A getAcceptableContentTypes() 0 11 3
B getIsSecureConnection() 0 21 9
C getHostInfo() 0 24 13
A getETags() 0 7 2
A generateCsrfToken() 0 11 2
A get() 0 7 2
A getQueryParams() 0 7 2
A getContentType() 0 8 3
B getSecureForwardedHeaderParts() 0 37 7
A getTrustedHeaders() 0 22 5
A resolve() 0 15 3
A getUserIP() 0 10 3
A getCsrfToken() 0 15 6
A getQueryString() 0 3 2
A setAcceptableContentTypes() 0 3 1
A loadCsrfToken() 0 7 2
A getBodyParam() 0 14 5
A getBaseUrl() 0 7 2
A filterHeaders() 0 8 3
A getIsDelete() 0 3 1
B resolvePathInfo() 0 44 9
B getHeaders() 0 32 9
B getUserIpFromIpHeader() 0 32 8
A post() 0 7 2
B resolveRequestUri() 0 19 7
A getUserHost() 0 7 2
A setPathInfo() 0 3 2
A getSecureForwardedHeaderTrustedPart() 0 11 4
A setHostInfo() 0 4 2
A setQueryParams() 0 3 1
B getBodyParams() 0 39 9
A getIsPjax() 0 3 2
A getHostName() 0 7 2
A getAcceptableLanguages() 0 11 3
A getRemoteHost() 0 3 2
A getScriptFile() 0 11 3
A setBaseUrl() 0 3 1
B validateCsrfToken() 0 22 7
A setUrl() 0 3 1
A getUrl() 0 7 2
A getIsAjax() 0 3 1
A getRawBody() 0 7 2
A getReferrer() 0 3 1
A getPathInfo() 0 7 2
A setRawBody() 0 3 1
A getIsGet() 0 3 1
A getAbsoluteUrl() 0 3 1
A getAuthUser() 0 3 1
A getQueryParam() 0 5 2
A getIpValidator() 0 3 1
A setPort() 0 5 2
A getCookies() 0 9 2
C loadCookies() 0 41 12
A getCsrfTokenFromHeader() 0 3 1
A getIsPatch() 0 3 1
A getMethod() 0 20 5
C parseAcceptHeader() 0 66 16
A setBodyParams() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Request often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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