Passed
Pull Request — master (#235)
by
unknown
02:55
created

User   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 101
c 10
b 0
f 0
dl 0
loc 405
ccs 0
cts 140
cp 0
rs 8.48
wmc 49

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getIdentity() 0 15 5
A setAccessChecker() 0 3 1
A __construct() 0 6 1
A getAuthKey() 0 3 1
A setSession() 0 3 1
A setIdentity() 0 3 1
A validateAuthKey() 0 3 1
A login() 0 8 2
A loginByAccessToken() 0 7 3
A sendIdentityCookie() 0 15 1
A logout() 0 18 5
C renewAuthStatus() 0 20 13
A afterLogout() 0 3 1
A getId() 0 3 1
A can() 0 7 2
A isGuest() 0 3 1
A switchIdentity() 0 21 5
A beforeLogin() 0 5 1
A afterLogin() 0 3 1
A beforeLogout() 0 5 1
A setIdentityCookie() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like User 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 User, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Yiisoft\Yii\Web\User;
4
5
use Psr\EventDispatcher\EventDispatcherInterface;
6
use Psr\Http\Message\ResponseInterface;
7
use Yiisoft\Access\AccessCheckerInterface;
8
use Yiisoft\Auth\IdentityInterface;
9
use Yiisoft\Auth\IdentityRepositoryInterface;
10
use Yiisoft\Yii\Web\Cookie;
11
use Yiisoft\Yii\Web\Session\SessionInterface;
12
use Yiisoft\Yii\Web\User\Event\AfterLogin;
13
use Yiisoft\Yii\Web\User\Event\AfterLogout;
14
use Yiisoft\Yii\Web\User\Event\BeforeLogin;
15
use Yiisoft\Yii\Web\User\Event\BeforeLogout;
16
17
class User implements IdentityInterface
18
{
19
    private const SESSION_AUTH_ID = '__auth_id';
20
    private const SESSION_AUTH_EXPIRE = '__auth_expire';
21
    private const SESSION_AUTH_ABSOLUTE_EXPIRE = '__auth_absolute_expire';
22
23
    private IdentityRepositoryInterface $identityRepository;
24
    private EventDispatcherInterface $eventDispatcher;
25
26
    private string $identityCookie = 'remember';
27
    private ?AccessCheckerInterface $accessChecker = null;
28
    private ?IdentityInterface $identity = null;
29
    private ?SessionInterface $session = null;
30
31
    public function __construct(
32
        IdentityRepositoryInterface $identityRepository,
33
        EventDispatcherInterface $eventDispatcher
34
    ) {
35
        $this->identityRepository = $identityRepository;
36
        $this->eventDispatcher = $eventDispatcher;
37
    }
38
39
    /**
40
     * @var int|null the number of seconds in which the user will be logged out automatically if he
41
     * remains inactive. If this property is not set, the user will be logged out after
42
     * the current session expires (c.f. [[Session::timeout]]).
43
     */
44
    public ?int $authTimeout = null;
45
46
    /**
47
     * @var int|null the number of seconds in which the user will be logged out automatically
48
     * regardless of activity.
49
     * Note that this will not work if [[enableAutoLogin]] is `true`.
50
     */
51
    public ?int $absoluteAuthTimeout = null;
52
53
    /**
54
     * @var array MIME types for which this component should redirect to the [[loginUrl]].
55
     */
56
    public array $acceptableRedirectTypes = ['text/html', 'application/xhtml+xml'];
57
58
    /**
59
     * Set session to persist authentication status across multiple requests.
60
     * If not set, authentication has to be performed on each request, which is often the case
61
     * for stateless application such as RESTful API.
62
     *
63
     * @param SessionInterface $session
64
     */
65
    public function setSession(SessionInterface $session): void
66
    {
67
        $this->session = $session;
68
    }
69
70
    public function setAccessChecker(AccessCheckerInterface $accessChecker): void
71
    {
72
        $this->accessChecker = $accessChecker;
73
    }
74
75
    /**
76
     * Returns the identity object associated with the currently logged-in user.
77
     * When [[enableSession]] is true, this method may attempt to read the user's authentication data
78
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
79
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
80
     * This is only useful when [[enableSession]] is true.
81
     * @return IdentityInterface the identity object associated with the currently logged-in user.
82
     * @throws \Throwable
83
     * @see logout()
84
     * @see login()
85
     */
86
    public function getIdentity($autoRenew = true): IdentityInterface
87
    {
88
        if ($this->identity !== null) {
89
            return $this->identity;
90
        }
91
        if ($this->session === null || !$autoRenew) {
92
            return new GuestIdentity();
93
        }
94
        try {
95
            $this->renewAuthStatus();
96
        } catch (\Throwable $e) {
97
            $this->identity = null;
98
            throw $e;
99
        }
100
        return $this->identity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->identity could return the type null which is incompatible with the type-hinted return Yiisoft\Auth\IdentityInterface. Consider adding an additional type-check to rule them out.
Loading history...
101
    }
102
103
    /**
104
     * Sets the user identity object.
105
     *
106
     * Note that this method does not deal with session or cookie. You should usually use [[switchIdentity()]]
107
     * to change the identity of the current user.
108
     *
109
     * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
110
     * Use {{@see GuestIdentity}} to indicate that the current user is a guest.
111
     */
112
    public function setIdentity(IdentityInterface $identity): void
113
    {
114
        $this->identity = $identity;
115
    }
116
117
    /**
118
     * Return the auth key value
119
     *
120
     * @return string Auth key value
121
     */
122
    public function getAuthKey(): string
123
    {
124
        return 'ABCD1234';
125
    }
126
127
    /**
128
     * Validate auth key
129
     *
130
     * @param String $authKey Auth key to validate
131
     * @return bool True if is valid
132
     */
133
    public function validateAuthKey($authKey): bool
134
    {
135
        return $authKey === 'ABCD1234';
136
    }
137
138
    /**
139
     * Sends an identity cookie.
140
     *
141
     * @param IdentityInterface $identity
142
     * @param int $duration number of seconds that the user can remain in logged-in status.
143
     * @param ResponseInterface $response Response to handle
144
     */
145
    protected function sendIdentityCookie(IdentityInterface $identity, int $duration, ResponseInterface $response): void
146
    {
147
        $data = json_encode(
148
            [
149
                $identity->getId(),
150
                $this->getAuthKey(),
151
                $duration,
152
            ],
153
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
154
        );
155
156
        $expireDateTime = new \DateTimeImmutable();
157
        $expireDateTime->setTimestamp(time() + $duration);
158
        $cookieIdentity = (new Cookie($this->identityCookie, $data))->expireAt($expireDateTime);
159
        $cookieIdentity->addToResponse($response);
160
    }
161
162
    /**
163
     * Logs in a user.
164
     *
165
     * After logging in a user:
166
     * - the user's identity information is obtainable from the [[identity]] property
167
     *
168
     * If [[enableSession]] is `true`:
169
     * - the identity information will be stored in session and be available in the next requests
170
     * - in case of `$duration == 0`: as long as the session remains active or till the user closes the browser
171
     * - in case of `$duration > 0`: as long as the session remains active or as long as the cookie
172
     *   remains valid by it's `$duration` in seconds when [[enableAutoLogin]] is set `true`.
173
     *
174
     * If [[enableSession]] is `false`:
175
     * - the `$duration` parameter will be ignored
176
     *
177
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
178
     * @param int $duration number of seconds that the user can remain in logged-in status, defaults to `0`
179
     * @param ResponseInterface $response Response to handle
180
     * @return bool whether the user is logged in
181
     */
182
    public function login(IdentityInterface $identity, int $duration = 0, ResponseInterface $response): bool
183
    {
184
        if ($this->beforeLogin($identity, $duration)) {
185
            $this->switchIdentity($identity);
186
            $this->afterLogin($identity, $duration);
187
            $this->sendIdentityCookie($identity, $duration, $response);
188
        }
189
        return !$this->isGuest();
190
    }
191
192
    /**
193
     * Logs in a user by the given access token.
194
     * This method will first authenticate the user by calling [[IdentityInterface::findIdentityByAccessToken()]]
195
     * with the provided access token. If successful, it will call [[login()]] to log in the authenticated user.
196
     * If authentication fails or [[login()]] is unsuccessful, it will return null.
197
     * @param string $token the access token
198
     * @param string $type the type of the token. The value of this parameter depends on the implementation.
199
     * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`.
200
     * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
201
     * the access token is invalid or [[login()]] is unsuccessful.
202
     */
203
    public function loginByAccessToken(string $token, string $type = null): ?IdentityInterface
204
    {
205
        $identity = $this->identityRepository->findIdentityByToken($token, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type null; however, parameter $type of Yiisoft\Auth\IdentityRep...::findIdentityByToken() 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

205
        $identity = $this->identityRepository->findIdentityByToken($token, /** @scrutinizer ignore-type */ $type);
Loading history...
206
        if ($identity && $this->login($identity)) {
0 ignored issues
show
Bug introduced by
The call to Yiisoft\Yii\Web\User\User::login() has too few arguments starting with response. ( Ignorable by Annotation )

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

206
        if ($identity && $this->/** @scrutinizer ignore-call */ login($identity)) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
207
            return $identity;
208
        }
209
        return null;
210
    }
211
212
    /**
213
     * Logs out the current user.
214
     * This will remove authentication-related session data.
215
     * If `$destroySession` is true, all session data will be removed.
216
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
217
     * This parameter is ignored if [[enableSession]] is false.
218
     * @return bool whether the user is logged out
219
     * @throws \Throwable
220
     */
221
    public function logout($destroySession = true): bool
222
    {
223
        $identity = $this->getIdentity();
224
        if ($this->isGuest()) {
225
            return false;
226
        }
227
        if ($this->beforeLogout($identity)) {
228
            $this->switchIdentity(new GuestIdentity());
229
            if ($destroySession && $this->session) {
230
                $this->session->destroy();
231
            }
232
233
            // Remove the cookie
234
            (new Cookie($this->identityCookie, ""))->expire(1);
0 ignored issues
show
Bug introduced by
The method expire() does not exist on Yiisoft\Yii\Web\Cookie. Did you maybe mean expireWhenBrowserIsClosed()? ( Ignorable by Annotation )

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

234
            (new Cookie($this->identityCookie, ""))->/** @scrutinizer ignore-call */ expire(1);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
235
236
            $this->afterLogout($identity);
237
        }
238
        return $this->isGuest();
239
    }
240
241
    /**
242
     * Returns a value indicating whether the user is a guest (not authenticated).
243
     * @return bool whether the current user is a guest.
244
     * @see getIdentity()
245
     */
246
    public function isGuest(): bool
247
    {
248
        return $this->getIdentity() instanceof GuestIdentity;
249
    }
250
251
    /**
252
     * Returns a value that uniquely represents the user.
253
     * @return string the unique identifier for the user. If `null`, it means the user is a guest.
254
     * @throws \Throwable
255
     * @see getIdentity()
256
     */
257
    public function getId(): ?string
258
    {
259
        return $this->getIdentity()->getId();
260
    }
261
262
    /**
263
     * Set the name of the cookie identity
264
     * @param string $name New name of the cookie
265
     * @return this
266
     */
267
    public function setIdentityCookie(string $name): self
268
    {
269
        $new = clone $this;
270
        $new->identityCookie = $name;
271
        return $new;
272
    }
273
274
    /**
275
     * This method is called before logging in a user.
276
     * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event.
277
     * If you override this method, make sure you call the parent implementation
278
     * so that the event is triggered.
279
     * @param IdentityInterface $identity the user identity information
280
     * @param int $duration number of seconds that the user can remain in logged-in status.
281
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
282
     * @return bool whether the user should continue to be logged in
283
     */
284
    protected function beforeLogin(IdentityInterface $identity, int $duration): bool
285
    {
286
        $event = new BeforeLogin($identity, $duration);
287
        $this->eventDispatcher->dispatch($event);
288
        return $event->isValid();
289
    }
290
291
    /**
292
     * This method is called after the user is successfully logged in.
293
     * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event.
294
     * If you override this method, make sure you call the parent implementation
295
     * so that the event is triggered.
296
     * @param IdentityInterface $identity the user identity information
297
     * @param int $duration number of seconds that the user can remain in logged-in status.
298
     * If 0, it means login till the user closes the browser or the session is manually destroyed.
299
     */
300
    protected function afterLogin(IdentityInterface $identity, int $duration): void
301
    {
302
        $this->eventDispatcher->dispatch(new AfterLogin($identity, $duration));
303
    }
304
305
    /**
306
     * This method is invoked when calling [[logout()]] to log out a user.
307
     * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event.
308
     * If you override this method, make sure you call the parent implementation
309
     * so that the event is triggered.
310
     * @param IdentityInterface $identity the user identity information
311
     * @return bool whether the user should continue to be logged out
312
     */
313
    protected function beforeLogout(IdentityInterface $identity): bool
314
    {
315
        $event = new BeforeLogout($identity);
316
        $this->eventDispatcher->dispatch($event);
317
        return $event->isValid();
318
    }
319
320
    /**
321
     * This method is invoked right after a user is logged out via [[logout()]].
322
     * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event.
323
     * If you override this method, make sure you call the parent implementation
324
     * so that the event is triggered.
325
     * @param IdentityInterface $identity the user identity information
326
     */
327
    protected function afterLogout(IdentityInterface $identity): void
328
    {
329
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
330
    }
331
332
    /**
333
     * Switches to a new identity for the current user.
334
     *
335
     * When [[enableSession]] is true, this method may use session and/or cookie to store the user identity information,
336
     * according to the value of `$duration`. Please refer to [[login()]] for more details.
337
     *
338
     * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]]
339
     * when the current user needs to be associated with the corresponding identity information.
340
     *
341
     * @param IdentityInterface $identity the identity information to be associated with the current user.
342
     * In order to indicate that the user is guest, use {{@see GuestIdentity}}.
343
     */
344
    public function switchIdentity(IdentityInterface $identity): void
345
    {
346
        $this->setIdentity($identity);
347
        if ($this->session === null) {
348
            return;
349
        }
350
351
        $this->session->regenerateID();
352
353
        $this->session->remove(self::SESSION_AUTH_ID);
354
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
355
356
        if ($identity->getId() === null) {
357
            return;
358
        }
359
        $this->session->set(self::SESSION_AUTH_ID, $identity->getId());
360
        if ($this->authTimeout !== null) {
361
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
362
        }
363
        if ($this->absoluteAuthTimeout !== null) {
364
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
365
        }
366
    }
367
368
    /**
369
     * Updates the authentication status using the information from session and cookie.
370
     *
371
     * This method will try to determine the user identity using a session variable.
372
     *
373
     * If [[authTimeout]] is set, this method will refresh the timer.
374
     *
375
     * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]]
376
     * if [[enableAutoLogin]] is true.
377
     * @throws \Throwable
378
     */
379
    protected function renewAuthStatus(): void
380
    {
381
        $id = $this->session->get(self::SESSION_AUTH_ID);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

381
        /** @scrutinizer ignore-call */ 
382
        $id = $this->session->get(self::SESSION_AUTH_ID);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
382
383
        $identity = null;
384
        if ($id !== null) {
385
            $identity = $this->identityRepository->findIdentity($id);
386
        }
387
        if ($identity === null) {
388
            $identity = new GuestIdentity();
389
        }
390
        $this->setIdentity($identity);
391
392
        if (!($identity instanceof GuestIdentity) && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
393
            $expire = $this->authTimeout !== null ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE) : null;
394
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE) : null;
395
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
396
                $this->logout(false);
397
            } elseif ($this->authTimeout !== null) {
398
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
399
            }
400
        }
401
    }
402
403
    /**
404
     * Checks if the user can perform the operation as specified by the given permission.
405
     *
406
     * Note that you must provide access checker via {{@see User::setAccessChecker()}} in order to use this method.
407
     * Otherwise it will always return false.
408
     *
409
     * @param string $permissionName the name of the permission (e.g. "edit post") that needs access check.
410
     * @param array $params name-value pairs that would be passed to the rules associated
411
     * with the roles and permissions assigned to the user.
412
     * @return bool whether the user can perform the operation as specified by the given permission.
413
     * @throws \Throwable
414
     */
415
    public function can(string $permissionName, array $params = []): bool
416
    {
417
        if ($this->accessChecker === null) {
418
            return false;
419
        }
420
421
        return $this->accessChecker->userHasPermission($this->getId(), $permissionName, $params);
422
    }
423
}
424