Passed
Pull Request — master (#19824)
by Paweł
09:59
created

User::can()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.288

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 9
c 1
b 0
f 0
nc 8
nop 3
dl 0
loc 16
ccs 8
cts 10
cp 0.8
crap 6.288
rs 9.2222
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\base\InvalidValueException;
14
use yii\di\Instance;
15
use yii\rbac\CheckAccessInterface;
16
17
/**
18
 * User is the class for the `user` application component that manages the user authentication status.
19
 *
20
 * You may use [[isGuest]] to determine whether the current user is a guest or not.
21
 * If the user is a guest, the [[identity]] property would return `null`. Otherwise, it would
22
 * be an instance of [[IdentityInterface]].
23
 *
24
 * You may call various methods to change the user authentication status:
25
 *
26
 * - [[login()]]: sets the specified identity and remembers the authentication status in session and cookie;
27
 * - [[logout()]]: marks the user as a guest and clears the relevant information from session and cookie;
28
 * - [[setIdentity()]]: changes the user identity without touching session or cookie
29
 *   (this is best used in stateless RESTful API implementation).
30
 *
31
 * Note that User only maintains the user authentication status. It does NOT handle how to authenticate
32
 * a user. The logic of how to authenticate a user should be done in the class implementing [[IdentityInterface]].
33
 * You are also required to set [[identityClass]] with the name of this class.
34
 *
35
 * User is configured as an application component in [[\yii\web\Application]] by default.
36
 * You can access that instance via `Yii::$app->user`.
37
 *
38
 * You can modify its configuration by adding an array to your application config under `components`
39
 * as it is shown in the following example:
40
 *
41
 * ```php
42
 * 'user' => [
43
 *     'identityClass' => 'app\models\User', // User must implement the IdentityInterface
44
 *     'enableAutoLogin' => true,
45
 *     // 'loginUrl' => ['user/login'],
46
 *     // ...
47
 * ]
48
 * ```
49
 *
50
 * @property-read string|int|null $id The unique identifier for the user. If `null`, it means the user is a
51
 * guest.
52
 * @property IdentityInterface|null $identity The identity object associated with the currently logged-in
53
 * user. `null` is returned if the user is not logged in (not authenticated).
54
 * @property-read bool $isGuest Whether the current user is a guest.
55
 * @property string $returnUrl The URL that the user should be redirected to after login. Note that the type
56
 * of this property differs in getter and setter. See [[getReturnUrl()]] and [[setReturnUrl()]] for details.
57
 *
58
 * @author Qiang Xue <[email protected]>
59
 * @since 2.0
60
 */
61
class User extends Component
62
{
63
    const EVENT_BEFORE_LOGIN = 'beforeLogin';
64
    const EVENT_AFTER_LOGIN = 'afterLogin';
65
    const EVENT_BEFORE_LOGOUT = 'beforeLogout';
66
    const EVENT_AFTER_LOGOUT = 'afterLogout';
67
68
    /**
69
     * @var string the class name of the [[identity]] object.
70
     */
71
    public $identityClass;
72
    /**
73
     * @var bool whether to enable cookie-based login. Defaults to `false`.
74
     * Note that this property will be ignored if [[enableSession]] is `false`.
75
     */
76
    public $enableAutoLogin = false;
77
    /**
78
     * @var bool whether to use session to persist authentication status across multiple requests.
79
     * You set this property to be `false` if your application is stateless, which is often the case
80
     * for RESTful APIs.
81
     */
82
    public $enableSession = true;
83
    /**
84
     * @var string|array|null the URL for login when [[loginRequired()]] is called.
85
     * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL.
86
     * The first element of the array should be the route to the login action, and the rest of
87
     * the name-value pairs are GET parameters used to construct the login URL. For example,
88
     *
89
     * ```php
90
     * ['site/login', 'ref' => 1]
91
     * ```
92
     *
93
     * If this property is `null`, a 403 HTTP exception will be raised when [[loginRequired()]] is called.
94
     */
95
    public $loginUrl = ['site/login'];
96
    /**
97
     * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is `true`.
98
     * @see Cookie
99
     */
100
    public $identityCookie = ['name' => '_identity', 'httpOnly' => true];
101
    /**
102
     * @var int|null the number of seconds in which the user will be logged out automatically if the user
103
     * remains inactive. If this property is not set, the user will be logged out after
104
     * the current session expires (c.f. [[Session::timeout]]).
105
     * Note that this will not work if [[enableAutoLogin]] is `true`.
106
     */
107
    public $authTimeout;
108
    /**
109
     * @var CheckAccessInterface|string|array|null The access checker object to use for checking access or the application
110
     * component ID of the access checker.
111
     * If not set the application auth manager will be used.
112
     * @since 2.0.9
113
     */
114
    public $accessChecker;
115
    /**
116
     * @var int|null the number of seconds in which the user will be logged out automatically
117
     * regardless of activity.
118
     * Note that this will not work if [[enableAutoLogin]] is `true`.
119
     */
120
    public $absoluteAuthTimeout;
121
    /**
122
     * @var bool whether to automatically renew the identity cookie each time a page is requested.
123
     * This property is effective only when [[enableAutoLogin]] is `true`.
124
     * When this is `false`, the identity cookie will expire after the specified duration since the user
125
     * is initially logged in. When this is `true`, the identity cookie will expire after the specified duration
126
     * since the user visits the site the last time.
127
     * @see enableAutoLogin
128
     */
129
    public $autoRenewCookie = true;
130
    /**
131
     * @var string the session variable name used to store the value of [[id]].
132
     */
133
    public $idParam = '__id';
134
    /**
135
     * @var string the session variable name used to store authentication key.
136
     * @since 2.0.41
137
     */
138
    public $authKeyParam = '__authKey';
139
    /**
140
     * @var string the session variable name used to store the value of expiration timestamp of the authenticated state.
141
     * This is used when [[authTimeout]] is set.
142
     */
143
    public $authTimeoutParam = '__expire';
144
    /**
145
     * @var string the session variable name used to store the value of absolute expiration timestamp of the authenticated state.
146
     * This is used when [[absoluteAuthTimeout]] is set.
147
     */
148
    public $absoluteAuthTimeoutParam = '__absoluteExpire';
149
    /**
150
     * @var string the session variable name used to store the value of [[returnUrl]].
151
     */
152
    public $returnUrlParam = '__returnUrl';
153
    /**
154
     * @var array MIME types for which this component should redirect to the [[loginUrl]].
155
     * @since 2.0.8
156
     */
157
    public $acceptableRedirectTypes = ['text/html', 'application/xhtml+xml'];
158
159
    private $_access = [];
160
161
162
    /**
163
     * Initializes the application component.
164
     */
165 115
    public function init()
166
    {
167 115
        parent::init();
168
169 115
        if ($this->identityClass === null) {
170
            throw new InvalidConfigException('User::identityClass must be set.');
171
        }
172 115
        if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) {
173
            throw new InvalidConfigException('User::identityCookie must contain the "name" element.');
174
        }
175 115
        if ($this->accessChecker !== null) {
176 1
            $this->accessChecker = Instance::ensure($this->accessChecker, '\yii\rbac\CheckAccessInterface');
177
        }
178 115
    }
179
180
    private $_identity = false;
181
182
    /**
183
     * Returns the identity object associated with the currently logged-in user.
184
     * When [[enableSession]] is true, this method may attempt to read the user's authentication data
185
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
186
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
187
     * This is only useful when [[enableSession]] is true.
188
     * @return IdentityInterface|null the identity object associated with the currently logged-in user.
189
     * `null` is returned if the user is not logged in (not authenticated).
190
     * @see login()
191
     * @see logout()
192
     */
193 96
    public function getIdentity($autoRenew = true)
194
    {
195 96
        if ($this->_identity === false) {
196 40
            if ($this->enableSession && $autoRenew) {
197
                try {
198 38
                    $this->_identity = null;
199 38
                    $this->renewAuthStatus();
200 1
                } catch (\Exception $e) {
201 1
                    $this->_identity = false;
202 1
                    throw $e;
203
                } catch (\Throwable $e) {
204
                    $this->_identity = false;
205 37
                    throw $e;
206
                }
207
            } else {
208 2
                return null;
209
            }
210
        }
211
212 93
        return $this->_identity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_identity also could return the type true which is incompatible with the documented return type null|yii\web\IdentityInterface.
Loading history...
213
    }
214
215
    /**
216
     * Sets the user identity object.
217
     *
218
     * Note that this method does not deal with session or cookie. You should usually use [[switchIdentity()]]
219
     * to change the identity of the current user.
220
     *
221
     * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
222
     * If null, it means the current user will be a guest without any associated identity.
223
     * @throws InvalidValueException if `$identity` object does not implement [[IdentityInterface]].
224
     */
225 93
    public function setIdentity($identity)
226
    {
227 93
        if ($identity instanceof IdentityInterface) {
228 62
            $this->_identity = $identity;
229 42
        } elseif ($identity === null) {
0 ignored issues
show
introduced by
The condition $identity === null is always true.
Loading history...
230 42
            $this->_identity = null;
231
        } else {
232 1
            throw new InvalidValueException('The identity object must implement IdentityInterface.');
233
        }
234 93
        $this->_access = [];
235 93
    }
236
237
    /**
238
     * Logs in a user.
239
     *
240
     * After logging in a user:
241
     * - the user's identity information is obtainable from the [[identity]] property
242
     *
243
     * If [[enableSession]] is `true`:
244
     * - the identity information will be stored in session and be available in the next requests
245
     * - in case of `$duration == 0`: as long as the session remains active or till the user closes the browser
246
     * - in case of `$duration > 0`: as long as the session remains active or as long as the cookie
247
     *   remains valid by it's `$duration` in seconds when [[enableAutoLogin]] is set `true`.
248
     *
249
     * If [[enableSession]] is `false`:
250
     * - the `$duration` parameter will be ignored
251
     *
252
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
253
     * @param int $duration number of seconds that the user can remain in logged-in status, defaults to `0`
254
     * @return bool whether the user is logged in
255
     */
256 43
    public function login(IdentityInterface $identity, $duration = 0)
257
    {
258 43
        if ($this->beforeLogin($identity, false, $duration)) {
259 43
            $this->switchIdentity($identity, $duration);
260 43
            $id = $identity->getId();
261 43
            $ip = Yii::$app->getRequest()->getUserIP();
0 ignored issues
show
Bug introduced by
The method getUserIP() does not exist on yii\console\Request. 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

261
            $ip = Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getUserIP();
Loading history...
262 43
            if ($this->enableSession) {
263 43
                $log = "User '$id' logged in from $ip with duration $duration.";
264
            } else {
265
                $log = "User '$id' logged in from $ip. Session not enabled.";
266
            }
267
268 43
            $this->regenerateCsrfToken();
269
270 43
            Yii::info($log, __METHOD__);
271 43
            $this->afterLogin($identity, false, $duration);
272
        }
273
274 43
        return !$this->getIsGuest();
275
    }
276
277
    /**
278
     * Regenerates CSRF token
279
     *
280
     * @since 2.0.14.2
281
     */
282 43
    protected function regenerateCsrfToken()
283
    {
284 43
        $request = Yii::$app->getRequest();
285 43
        if ($request->enableCsrfCookie || $this->enableSession) {
0 ignored issues
show
Bug Best Practice introduced by
The property enableCsrfCookie does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
286 43
            $request->getCsrfToken(true);
0 ignored issues
show
Bug introduced by
The method getCsrfToken() does not exist on yii\console\Request. 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

286
            $request->/** @scrutinizer ignore-call */ 
287
                      getCsrfToken(true);
Loading history...
287
        }
288 43
    }
289
290
    /**
291
     * Logs in a user by the given access token.
292
     * This method will first authenticate the user by calling [[IdentityInterface::findIdentityByAccessToken()]]
293
     * with the provided access token. If successful, it will call [[login()]] to log in the authenticated user.
294
     * If authentication fails or [[login()]] is unsuccessful, it will return null.
295
     * @param string $token the access token
296
     * @param mixed $type the type of the token. The value of this parameter depends on the implementation.
297
     * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`.
298
     * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
299
     * the access token is invalid or [[login()]] is unsuccessful.
300
     */
301 42
    public function loginByAccessToken($token, $type = null)
302
    {
303
        /* @var $class IdentityInterface */
304 42
        $class = $this->identityClass;
305 42
        $identity = $class::findIdentityByAccessToken($token, $type);
306 42
        if ($identity && $this->login($identity)) {
307 27
            return $identity;
308
        }
309
310 15
        return null;
311
    }
312
313
    /**
314
     * Logs in a user by cookie.
315
     *
316
     * This method attempts to log in a user using the ID and authKey information
317
     * provided by the [[identityCookie|identity cookie]].
318
     */
319 2
    protected function loginByCookie()
320
    {
321 2
        $data = $this->getIdentityAndDurationFromCookie();
322 2
        if (isset($data['identity'], $data['duration'])) {
323 1
            $identity = $data['identity'];
324 1
            $duration = $data['duration'];
325 1
            if ($this->beforeLogin($identity, true, $duration)) {
326 1
                $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
327 1
                $id = $identity->getId();
328 1
                $ip = Yii::$app->getRequest()->getUserIP();
329 1
                Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__);
330 1
                $this->afterLogin($identity, true, $duration);
331
            }
332
        }
333 2
    }
334
335
    /**
336
     * Logs out the current user.
337
     * This will remove authentication-related session data.
338
     * If `$destroySession` is true, all session data will be removed.
339
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
340
     * This parameter is ignored if [[enableSession]] is false.
341
     * @return bool whether the user is logged out
342
     */
343 1
    public function logout($destroySession = true)
344
    {
345 1
        $identity = $this->getIdentity();
346 1
        if ($identity !== null && $this->beforeLogout($identity)) {
347 1
            $this->switchIdentity(null);
348 1
            $id = $identity->getId();
349 1
            $ip = Yii::$app->getRequest()->getUserIP();
350 1
            Yii::info("User '$id' logged out from $ip.", __METHOD__);
351 1
            if ($destroySession && $this->enableSession) {
352
                Yii::$app->getSession()->destroy();
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

352
                Yii::$app->/** @scrutinizer ignore-call */ 
353
                           getSession()->destroy();
Loading history...
Bug introduced by
The method getSession() does not exist on yii\base\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

352
                Yii::$app->/** @scrutinizer ignore-call */ 
353
                           getSession()->destroy();
Loading history...
353
            }
354 1
            $this->afterLogout($identity);
355
        }
356
357 1
        return $this->getIsGuest();
358
    }
359
360
    /**
361
     * Returns a value indicating whether the user is a guest (not authenticated).
362
     * @return bool whether the current user is a guest.
363
     * @see getIdentity()
364
     */
365 44
    public function getIsGuest()
366
    {
367 44
        return $this->getIdentity() === null;
368
    }
369
370
    /**
371
     * Returns a value that uniquely represents the user.
372
     * @return string|int|null the unique identifier for the user. If `null`, it means the user is a guest.
373
     * @see getIdentity()
374
     */
375 84
    public function getId()
376
    {
377 84
        $identity = $this->getIdentity();
378
379 84
        return $identity !== null ? $identity->getId() : null;
380
    }
381
382
    /**
383
     * Returns the URL that the browser should be redirected to after successful login.
384
     *
385
     * This method reads the return URL from the session. It is usually used by the login action which
386
     * may call this method to redirect the browser to where it goes after successful authentication.
387
     *
388
     * @param string|array|null $defaultUrl the default return URL in case it was not set previously.
389
     * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to.
390
     * Please refer to [[setReturnUrl()]] on accepted format of the URL.
391
     * @return string the URL that the user should be redirected to after login.
392
     * @see loginRequired()
393
     */
394 3
    public function getReturnUrl($defaultUrl = null)
395
    {
396 3
        $url = Yii::$app->getSession()->get($this->returnUrlParam, $defaultUrl);
397 3
        if (is_array($url)) {
398
            if (isset($url[0])) {
399
                return Yii::$app->getUrlManager()->createUrl($url);
400
            }
401
402
            $url = null;
403
        }
404
405 3
        return $url === null ? Yii::$app->getHomeUrl() : $url;
0 ignored issues
show
Bug introduced by
The method getHomeUrl() 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

405
        return $url === null ? Yii::$app->/** @scrutinizer ignore-call */ getHomeUrl() : $url;
Loading history...
Bug introduced by
The method getHomeUrl() does not exist on yii\base\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

405
        return $url === null ? Yii::$app->/** @scrutinizer ignore-call */ getHomeUrl() : $url;
Loading history...
406
    }
407
408
    /**
409
     * Remembers the URL in the session so that it can be retrieved back later by [[getReturnUrl()]].
410
     * @param string|array $url the URL that the user should be redirected to after login.
411
     * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL.
412
     * The first element of the array should be the route, and the rest of
413
     * the name-value pairs are GET parameters used to construct the URL. For example,
414
     *
415
     * ```php
416
     * ['admin/index', 'ref' => 1]
417
     * ```
418
     */
419 3
    public function setReturnUrl($url)
420
    {
421 3
        Yii::$app->getSession()->set($this->returnUrlParam, $url);
422 3
    }
423
424
    /**
425
     * Redirects the user browser to the login page.
426
     *
427
     * Before the redirection, the current URL (if it's not an AJAX url) will be kept as [[returnUrl]] so that
428
     * the user browser may be redirected back to the current page after successful login.
429
     *
430
     * Make sure you set [[loginUrl]] so that the user browser can be redirected to the specified login URL after
431
     * calling this method.
432
     *
433
     * Note that when [[loginUrl]] is set, calling this method will NOT terminate the application execution.
434
     *
435
     * @param bool $checkAjax whether to check if the request is an AJAX request. When this is true and the request
436
     * is an AJAX request, the current URL (for AJAX request) will NOT be set as the return URL.
437
     * @param bool $checkAcceptHeader whether to check if the request accepts HTML responses. Defaults to `true`. When this is true and
438
     * the request does not accept HTML responses the current URL will not be SET as the return URL. Also instead of
439
     * redirecting the user an ForbiddenHttpException is thrown. This parameter is available since version 2.0.8.
440
     * @return Response the redirection response if [[loginUrl]] is set
441
     * @throws ForbiddenHttpException the "Access Denied" HTTP exception if [[loginUrl]] is not set or a redirect is
442
     * not applicable.
443
     */
444 2
    public function loginRequired($checkAjax = true, $checkAcceptHeader = true)
445
    {
446 2
        $request = Yii::$app->getRequest();
447 2
        $canRedirect = !$checkAcceptHeader || $this->checkRedirectAcceptable();
448 2
        if ($this->enableSession
449 2
            && $request->getIsGet()
0 ignored issues
show
Bug introduced by
The method getIsGet() does not exist on yii\console\Request. 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

449
            && $request->/** @scrutinizer ignore-call */ getIsGet()
Loading history...
450 2
            && (!$checkAjax || !$request->getIsAjax())
0 ignored issues
show
Bug introduced by
The method getIsAjax() does not exist on yii\console\Request. 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

450
            && (!$checkAjax || !$request->/** @scrutinizer ignore-call */ getIsAjax())
Loading history...
451 2
            && $canRedirect
452
        ) {
453 1
            $this->setReturnUrl($request->getAbsoluteUrl());
0 ignored issues
show
Bug introduced by
The method getAbsoluteUrl() does not exist on yii\console\Request. 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

453
            $this->setReturnUrl($request->/** @scrutinizer ignore-call */ getAbsoluteUrl());
Loading history...
454
        }
455 2
        if ($this->loginUrl !== null && $canRedirect) {
456 1
            $loginUrl = (array) $this->loginUrl;
457 1
            if ($loginUrl[0] !== Yii::$app->requestedRoute) {
458 1
                return Yii::$app->getResponse()->redirect($this->loginUrl);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Yii::app->getResp...direct($this->loginUrl) also could return the type yii\console\Response which is incompatible with the documented return type yii\web\Response.
Loading history...
Bug introduced by
The method redirect() 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

458
                return Yii::$app->getResponse()->/** @scrutinizer ignore-call */ redirect($this->loginUrl);
Loading history...
459
            }
460
        }
461 2
        throw new ForbiddenHttpException(Yii::t('yii', 'Login Required'));
462
    }
463
464
    /**
465
     * This method is called before logging in a user.
466
     * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event.
467
     * If you override this method, make sure you call the parent implementation
468
     * so that the event is triggered.
469
     * @param IdentityInterface $identity the user identity information
470
     * @param bool $cookieBased whether the login is cookie-based
471
     * @param int $duration number of seconds that the user can remain in logged-in status.
472
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
473
     * @return bool whether the user should continue to be logged in
474
     */
475 43
    protected function beforeLogin($identity, $cookieBased, $duration)
476
    {
477 43
        $event = new UserEvent([
478 43
            'identity' => $identity,
479 43
            'cookieBased' => $cookieBased,
480 43
            'duration' => $duration,
481
        ]);
482 43
        $this->trigger(self::EVENT_BEFORE_LOGIN, $event);
483
484 43
        return $event->isValid;
485
    }
486
487
    /**
488
     * This method is called after the user is successfully logged in.
489
     * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event.
490
     * If you override this method, make sure you call the parent implementation
491
     * so that the event is triggered.
492
     * @param IdentityInterface $identity the user identity information
493
     * @param bool $cookieBased whether the login is cookie-based
494
     * @param int $duration number of seconds that the user can remain in logged-in status.
495
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
496
     */
497 43
    protected function afterLogin($identity, $cookieBased, $duration)
498
    {
499 43
        $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([
500 43
            'identity' => $identity,
501 43
            'cookieBased' => $cookieBased,
502 43
            'duration' => $duration,
503
        ]));
504 43
    }
505
506
    /**
507
     * This method is invoked when calling [[logout()]] to log out a user.
508
     * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event.
509
     * If you override this method, make sure you call the parent implementation
510
     * so that the event is triggered.
511
     * @param IdentityInterface $identity the user identity information
512
     * @return bool whether the user should continue to be logged out
513
     */
514 1
    protected function beforeLogout($identity)
515
    {
516 1
        $event = new UserEvent([
517 1
            'identity' => $identity,
518
        ]);
519 1
        $this->trigger(self::EVENT_BEFORE_LOGOUT, $event);
520
521 1
        return $event->isValid;
522
    }
523
524
    /**
525
     * This method is invoked right after a user is logged out via [[logout()]].
526
     * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event.
527
     * If you override this method, make sure you call the parent implementation
528
     * so that the event is triggered.
529
     * @param IdentityInterface $identity the user identity information
530
     */
531 1
    protected function afterLogout($identity)
532
    {
533 1
        $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent([
534 1
            'identity' => $identity,
535
        ]));
536 1
    }
537
538
    /**
539
     * Renews the identity cookie.
540
     * This method will set the expiration time of the identity cookie to be the current time
541
     * plus the originally specified cookie duration.
542
     */
543
    protected function renewIdentityCookie()
544
    {
545
        $name = $this->identityCookie['name'];
546
        $value = Yii::$app->getRequest()->getCookies()->getValue($name);
0 ignored issues
show
Bug introduced by
The method getCookies() does not exist on yii\console\Request. 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

546
        $value = Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getCookies()->getValue($name);
Loading history...
547
        if ($value !== null) {
548
            $data = json_decode($value, true);
549
            if (is_array($data) && isset($data[2])) {
550
                $cookie = Yii::createObject(array_merge($this->identityCookie, [
551
                    'class' => 'yii\web\Cookie',
552
                    'value' => $value,
553
                    'expire' => time() + (int) $data[2],
554
                ]));
555
                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

555
                Yii::$app->getResponse()->/** @scrutinizer ignore-call */ getCookies()->add($cookie);
Loading history...
556
            }
557
        }
558
    }
559
560
    /**
561
     * Sends an identity cookie.
562
     * This method is used when [[enableAutoLogin]] is true.
563
     * It saves [[id]], [[IdentityInterface::getAuthKey()|auth key]], and the duration of cookie-based login
564
     * information in the cookie.
565
     * @param IdentityInterface $identity
566
     * @param int $duration number of seconds that the user can remain in logged-in status.
567
     * @see loginByCookie()
568
     */
569 2
    protected function sendIdentityCookie($identity, $duration)
570
    {
571 2
        $cookie = Yii::createObject(array_merge($this->identityCookie, [
572 2
            'class' => 'yii\web\Cookie',
573 2
            'value' => json_encode([
574 2
                $identity->getId(),
575 2
                $identity->getAuthKey(),
576 2
                $duration,
577 2
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
578 2
            'expire' => time() + $duration,
579
        ]));
580 2
        Yii::$app->getResponse()->getCookies()->add($cookie);
581 2
    }
582
583
    /**
584
     * Determines if an identity cookie has a valid format and contains a valid auth key.
585
     * This method is used when [[enableAutoLogin]] is true.
586
     * This method attempts to authenticate a user using the information in the identity cookie.
587
     * @return array|null Returns an array of 'identity' and 'duration' if valid, otherwise null.
588
     * @see loginByCookie()
589
     * @since 2.0.9
590
     */
591 2
    protected function getIdentityAndDurationFromCookie()
592
    {
593 2
        $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
594 2
        if ($value === null) {
595
            return null;
596
        }
597 2
        $data = json_decode($value, true);
598 2
        if (is_array($data) && count($data) == 3) {
599 1
            list($id, $authKey, $duration) = $data;
600
            /* @var $class IdentityInterface */
601 1
            $class = $this->identityClass;
602 1
            $identity = $class::findIdentity($id);
603 1
            if ($identity !== null) {
604 1
                if (!$identity instanceof IdentityInterface) {
0 ignored issues
show
introduced by
$identity is always a sub-type of yii\web\IdentityInterface.
Loading history...
605
                    throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
606 1
                } elseif (!$identity->validateAuthKey($authKey)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $identity->validateAuthKey($authKey) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
607
                    $ip = Yii::$app->getRequest()->getUserIP();
608
                    Yii::warning("Invalid cookie auth key attempted for user '$id' from $ip: $authKey", __METHOD__);
609
                } else {
610 1
                    return ['identity' => $identity, 'duration' => $duration];
611
                }
612
            }
613
        }
614 2
        $this->removeIdentityCookie();
615 2
        return null;
616
    }
617
618
    /**
619
     * Removes the identity cookie.
620
     * This method is used when [[enableAutoLogin]] is true.
621
     * @since 2.0.9
622
     */
623 2
    protected function removeIdentityCookie()
624
    {
625 2
        Yii::$app->getResponse()->getCookies()->remove(Yii::createObject(array_merge($this->identityCookie, [
626 2
            'class' => 'yii\web\Cookie',
627
        ])));
628 2
    }
629
630
    /**
631
     * Switches to a new identity for the current user.
632
     *
633
     * When [[enableSession]] is true, this method may use session and/or cookie to store the user identity information,
634
     * according to the value of `$duration`. Please refer to [[login()]] for more details.
635
     *
636
     * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]]
637
     * when the current user needs to be associated with the corresponding identity information.
638
     *
639
     * @param IdentityInterface|null $identity the identity information to be associated with the current user.
640
     * If null, it means switching the current user to be a guest.
641
     * @param int $duration number of seconds that the user can remain in logged-in status.
642
     * This parameter is used only when `$identity` is not null.
643
     */
644 45
    public function switchIdentity($identity, $duration = 0)
645
    {
646 45
        $this->setIdentity($identity);
647
648 45
        if (!$this->enableSession) {
649
            return;
650
        }
651
652
        /* Ensure any existing identity cookies are removed. */
653 45
        if ($this->enableAutoLogin && ($this->autoRenewCookie || $identity === null)) {
654 1
            $this->removeIdentityCookie();
655
        }
656
657 45
        $session = Yii::$app->getSession();
658 45
        $session->regenerateID(true);
659 45
        $session->remove($this->idParam);
660 45
        $session->remove($this->authTimeoutParam);
661 45
        $session->remove($this->authKeyParam);
662
663 45
        if ($identity) {
664 43
            $session->set($this->idParam, $identity->getId());
665 43
            $session->set($this->authKeyParam, $identity->getAuthKey());
666 43
            if ($this->authTimeout !== null) {
667 2
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
668
            }
669 43
            if ($this->absoluteAuthTimeout !== null) {
670
                $session->set($this->absoluteAuthTimeoutParam, time() + $this->absoluteAuthTimeout);
671
            }
672 43
            if ($this->enableAutoLogin && $duration > 0) {
673 2
                $this->sendIdentityCookie($identity, $duration);
674
            }
675
        }
676 45
    }
677
678
    /**
679
     * Updates the authentication status using the information from session and cookie.
680
     *
681
     * This method will try to determine the user identity using the [[idParam]] session variable.
682
     *
683
     * If [[authTimeout]] is set, this method will refresh the timer.
684
     *
685
     * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]]
686
     * if [[enableAutoLogin]] is true.
687
     */
688 38
    protected function renewAuthStatus()
689
    {
690 38
        $session = Yii::$app->getSession();
691 38
        $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;
692
693 38
        if ($id === null) {
694 31
            $identity = null;
695
        } else {
696
            /* @var $class IdentityInterface */
697 8
            $class = $this->identityClass;
698 8
            $identity = $class::findIdentity($id);
699 7
            if ($identity === null) {
700 2
                $this->switchIdentity(null);
701
            }
702
        }
703
704 37
        if ($identity !== null) {
705 5
            $authKey = $session->get($this->authKeyParam);
706 5
            if ($authKey !== null && !$identity->validateAuthKey($authKey)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $identity->validateAuthKey($authKey) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
707 1
                $identity = null;
708 1
                $ip = Yii::$app->getRequest()->getUserIP();
709 1
                Yii::warning("Invalid session auth key attempted for user '$id' from $ip: $authKey", __METHOD__);
710
            }
711
        }
712
713 37
        $this->setIdentity($identity);
714
715 37
        if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
716 2
            $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null;
717 2
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null;
718 2
            if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($expire !== null && $ex...expireAbsolute < time(), Probably Intended Meaning: $expire !== null && ($ex...xpireAbsolute < time())
Loading history...
719 1
                $this->logout(false);
720 2
            } elseif ($this->authTimeout !== null) {
721 2
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
722
            }
723
        }
724
725 37
        if ($this->enableAutoLogin) {
726 2
            if ($this->getIsGuest()) {
727 2
                $this->loginByCookie();
728 1
            } elseif ($this->autoRenewCookie) {
729
                $this->renewIdentityCookie();
730
            }
731
        }
732 37
    }
733
734
    /**
735
     * Checks if the user can perform the operation as specified by the given permission.
736
     *
737
     * Note that you must configure "authManager" application component in order to use this method.
738
     * Otherwise it will always return false.
739
     *
740
     * @param string $permissionName the name of the permission (e.g. "edit post") that needs access check.
741
     * @param array $params name-value pairs that would be passed to the rules associated
742
     * with the roles and permissions assigned to the user.
743
     * @param bool $allowCaching whether to allow caching the result of access check.
744
     * When this parameter is true (default), if the access check of an operation was performed
745
     * before, its result will be directly returned when calling this method to check the same
746
     * operation. If this parameter is false, this method will always call
747
     * [[\yii\rbac\CheckAccessInterface::checkAccess()]] to obtain the up-to-date access result. Note that this
748
     * caching is effective only within the same request and only works when `$params = []`.
749
     * @return bool whether the user can perform the operation as specified by the given permission.
750
     */
751 23
    public function can($permissionName, $params = [], $allowCaching = true)
752
    {
753
        /* @var string $json to cache the $params too */
754 23
        $allowCaching && $json = json_encode($params);
755 23
        if ($allowCaching && isset($this->_access[$permissionName][$json])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $json does not seem to be defined for all execution paths leading up to this point.
Loading history...
756
            return $this->_access[$permissionName][$json];
757
        }
758 23
        if (($accessChecker = $this->getAccessChecker()) === null) {
759
            return false;
760
        }
761 23
        $access = $accessChecker->checkAccess($this->getId(), $permissionName, $params);
762 23
        if ($allowCaching) {
763 23
            $this->_access[$permissionName][$json] = $access;
764
        }
765
766 23
        return $access;
767
    }
768
769
    /**
770
     * Checks if the `Accept` header contains a content type that allows redirection to the login page.
771
     * The login page is assumed to serve `text/html` or `application/xhtml+xml` by default. You can change acceptable
772
     * content types by modifying [[acceptableRedirectTypes]] property.
773
     * @return bool whether this request may be redirected to the login page.
774
     * @see acceptableRedirectTypes
775
     * @since 2.0.8
776
     */
777 2
    public function checkRedirectAcceptable()
778
    {
779 2
        $acceptableTypes = Yii::$app->getRequest()->getAcceptableContentTypes();
0 ignored issues
show
Bug introduced by
The method getAcceptableContentTypes() does not exist on yii\console\Request. 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

779
        $acceptableTypes = Yii::$app->getRequest()->/** @scrutinizer ignore-call */ getAcceptableContentTypes();
Loading history...
780 2
        if (empty($acceptableTypes) || (count($acceptableTypes) === 1 && array_keys($acceptableTypes)[0] === '*/*')) {
781 1
            return true;
782
        }
783
784 2
        foreach ($acceptableTypes as $type => $params) {
785 2
            if (in_array($type, $this->acceptableRedirectTypes, true)) {
786 1
                return true;
787
            }
788
        }
789
790 2
        return false;
791
    }
792
793
    /**
794
     * Returns auth manager associated with the user component.
795
     *
796
     * By default this is the `authManager` application component.
797
     * You may override this method to return a different auth manager instance if needed.
798
     * @return \yii\rbac\ManagerInterface
799
     * @since 2.0.6
800
     * @deprecated since version 2.0.9, to be removed in 2.1. Use [[getAccessChecker()]] instead.
801
     */
802
    protected function getAuthManager()
803
    {
804
        return Yii::$app->getAuthManager();
805
    }
806
807
    /**
808
     * Returns the access checker used for checking access.
809
     * @return CheckAccessInterface
810
     * @since 2.0.9
811
     */
812 23
    protected function getAccessChecker()
813
    {
814 23
        return $this->accessChecker !== null ? $this->accessChecker : $this->getAuthManager();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->accessChec...$this->getAuthManager() also could return the type array|string which is incompatible with the documented return type yii\rbac\CheckAccessInterface.
Loading history...
Deprecated Code introduced by
The function yii\web\User::getAuthManager() has been deprecated: since version 2.0.9, to be removed in 2.1. Use [[getAccessChecker()]] instead. ( Ignorable by Annotation )

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

814
        return $this->accessChecker !== null ? $this->accessChecker : /** @scrutinizer ignore-deprecated */ $this->getAuthManager();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
815
    }
816
}
817