Passed
Push — master ( 2ca186...7f83bc )
by Alexander
05:08 queued 02:48
created

User::renewAuthStatus()   D

Complexity

Conditions 16
Paths 197

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 16.131

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 25
c 1
b 0
f 0
nc 197
nop 0
dl 0
loc 41
ccs 23
cts 25
cp 0.92
crap 16.131
rs 4.7583

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\User;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Throwable;
9
use Yiisoft\Access\AccessCheckerInterface;
10
use Yiisoft\Auth\IdentityInterface;
11
use Yiisoft\Auth\IdentityRepositoryInterface;
12
use Yiisoft\Session\SessionInterface;
13
use Yiisoft\User\Event\AfterLogin;
14
use Yiisoft\User\Event\AfterLogout;
15
use Yiisoft\User\Event\BeforeLogin;
16
use Yiisoft\User\Event\BeforeLogout;
17
18
class User
19
{
20
    private const SESSION_AUTH_ID = '__auth_id';
21
    private const SESSION_AUTH_EXPIRE = '__auth_expire';
22
    private const SESSION_AUTH_ABSOLUTE_EXPIRE = '__auth_absolute_expire';
23
24
    /**
25
     * @var int|null the number of seconds in which the user will be logged out automatically in case of
26
     * remaining inactive. If this property is not set, the user will be logged out after
27
     * the current session expires.
28
     */
29
    private ?int $authTimeout = null;
30
31
    /**
32
     * @var int|null the number of seconds in which the user will be logged out automatically
33
     * regardless of activity.
34
     */
35
    private ?int $absoluteAuthTimeout = null;
36
37
    private IdentityRepositoryInterface $identityRepository;
38
    private EventDispatcherInterface $eventDispatcher;
39
40
    private ?AccessCheckerInterface $accessChecker = null;
41
    private ?IdentityInterface $identity = null;
42
    private ?SessionInterface $session;
43
44
    /**
45
     * @param IdentityRepositoryInterface $identityRepository
46
     * @param EventDispatcherInterface $eventDispatcher
47
     * @param SessionInterface|null $session session to persist authentication status across multiple requests.
48
     * If not set, authentication has to be performed on each request, which is often the case for stateless
49
     * application such as RESTful API.
50
     */
51 22
    public function __construct(
52
        IdentityRepositoryInterface $identityRepository,
53
        EventDispatcherInterface $eventDispatcher,
54
        SessionInterface $session = null
55
    ) {
56 22
        $this->identityRepository = $identityRepository;
57 22
        $this->eventDispatcher = $eventDispatcher;
58 22
        $this->session = $session;
59 22
    }
60
61 1
    public function setAccessChecker(AccessCheckerInterface $accessChecker): void
62
    {
63 1
        $this->accessChecker = $accessChecker;
64 1
    }
65
66
    /**
67
     * Returns the identity object associated with the currently logged-in user.
68
     * This method read the user's authentication data
69
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
70
     *
71
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
72
     *
73
     * @throws Throwable
74
     *
75
     * @return IdentityInterface the identity object associated with the currently logged-in user.
76
     *
77
     * @see logout()
78
     * @see login()
79
     */
80 18
    public function getIdentity(bool $autoRenew = true): IdentityInterface
81
    {
82 18
        if ($this->identity !== null) {
83 10
            return $this->identity;
84
        }
85 10
        if ($this->session === null || !$autoRenew) {
86 3
            return new GuestIdentity();
87
        }
88
        try {
89 7
            $this->renewAuthStatus();
90 1
        } catch (Throwable $e) {
91 1
            $this->identity = null;
92 1
            throw $e;
93
        }
94
95
        /** @psalm-suppress TypeDoesNotContainType */
96 6
        return $this->identity ?? new GuestIdentity();
97
    }
98
99
    /**
100
     * Sets the user identity object.
101
     *
102
     * Note that this method does not deal with session. You should usually use {@see switchIdentity()}
103
     * to change the identity of the current user.
104
     *
105
     * @param IdentityInterface $identity the identity object associated with the currently logged user.
106
     * Use {{@see GuestIdentity}} to indicate that the current user is a guest.
107
     */
108 14
    public function setIdentity(IdentityInterface $identity): void
109
    {
110 14
        $this->identity = $identity;
111 14
    }
112
113
    /**
114
     * Logs in a user.
115
     *
116
     * After logging in a user:
117
     * - the user's identity information is obtainable from the {@see getIdentity()}
118
     * - the identity information will be stored in session and be available in the next requests as long as the session
119
     *   remains active or till the user closes the browser. Some browsers, such as Chrome, are keeping session when
120
     *   browser is re-opened.
121
     *
122
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
123
     *
124
     * @return bool whether the user is logged in
125
     */
126 1
    public function login(IdentityInterface $identity): bool
127
    {
128 1
        if ($this->beforeLogin($identity)) {
129 1
            $this->switchIdentity($identity);
130 1
            $this->afterLogin($identity);
131
        }
132 1
        return !$this->isGuest();
133
    }
134
135
    /**
136
     * Logs out the current user.
137
     * This will remove authentication-related session data.
138
     * If `$destroySession` is true, all session data will be removed.
139
     *
140
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
141
     *
142
     * @throws Throwable
143
     *
144
     * @return bool whether the user is logged out
145
     */
146 5
    public function logout(bool $destroySession = true): bool
147
    {
148 5
        $identity = $this->getIdentity();
149 5
        if ($this->isGuest()) {
150 1
            return false;
151
        }
152 4
        if ($this->beforeLogout($identity)) {
153 4
            $this->switchIdentity(new GuestIdentity());
154 4
            if ($destroySession && $this->session) {
155 1
                $this->session->destroy();
156
            }
157
158 4
            $this->afterLogout($identity);
159
        }
160 4
        return $this->isGuest();
161
    }
162
163
    /**
164
     * Returns a value indicating whether the user is a guest (not authenticated).
165
     *
166
     * @return bool whether the current user is a guest.
167
     *
168
     * @see getIdentity()
169
     */
170 8
    public function isGuest(): bool
171
    {
172 8
        return $this->getIdentity() instanceof GuestIdentity;
173
    }
174
175
    /**
176
     * Returns a value that uniquely represents the user.
177
     *
178
     * @throws Throwable
179
     *
180
     * @return string the unique identifier for the user. If `null`, it means the user is a guest.
181
     *
182
     * @see getIdentity()
183
     */
184 3
    public function getId(): ?string
185
    {
186 3
        return $this->getIdentity()->getId();
187
    }
188
189
    /**
190
     * This method is called before logging in a user.
191
     * The default implementation will trigger the {@see BeforeLogin} event.
192
     * If you override this method, make sure you call the parent implementation
193
     * so that the event is triggered.
194
     *
195
     * @param IdentityInterface $identity the user identity information
196
     *
197
     * @return bool whether the user should continue to be logged in
198
     */
199 1
    private function beforeLogin(IdentityInterface $identity): bool
200
    {
201 1
        $event = new BeforeLogin($identity);
202 1
        $this->eventDispatcher->dispatch($event);
203 1
        return $event->isValid();
204
    }
205
206
    /**
207
     * This method is called after the user is successfully logged in.
208
     *
209
     * @param IdentityInterface $identity the user identity information
210
     */
211 1
    private function afterLogin(IdentityInterface $identity): void
212
    {
213 1
        $this->eventDispatcher->dispatch(new AfterLogin($identity));
214 1
    }
215
216
    /**
217
     * This method is invoked when calling {@see logout()} to log out a user.
218
     *
219
     * @param IdentityInterface $identity the user identity information
220
     *
221
     * @return bool whether the user should continue to be logged out
222
     */
223 4
    private function beforeLogout(IdentityInterface $identity): bool
224
    {
225 4
        $event = new BeforeLogout($identity);
226 4
        $this->eventDispatcher->dispatch($event);
227 4
        return $event->isValid();
228
    }
229
230
    /**
231
     * This method is invoked right after a user is logged out via {@see logout()}.
232
     *
233
     * @param IdentityInterface $identity the user identity information
234
     */
235 4
    private function afterLogout(IdentityInterface $identity): void
236
    {
237 4
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
238 4
    }
239
240
    /**
241
     * Switches to a new identity for the current user.
242
     *
243
     * This method use session to store the user identity information.
244
     * Please refer to {@see login()} for more details.
245
     *
246
     * This method is mainly called by {@see login()} and {@see logout()}
247
     * when the current user needs to be associated with the corresponding identity information.
248
     *
249
     * @param IdentityInterface $identity the identity information to be associated with the current user.
250
     * In order to indicate that the user is guest, use {{@see GuestIdentity}}.
251
     */
252 7
    public function switchIdentity(IdentityInterface $identity): void
253
    {
254 7
        $this->setIdentity($identity);
255 7
        if ($this->session === null) {
256 2
            return;
257
        }
258
259 5
        $this->session->regenerateID();
260
261 5
        $this->session->remove(self::SESSION_AUTH_ID);
262 5
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
263
264 5
        if ($identity->getId() === null) {
265 3
            return;
266
        }
267 2
        $this->session->set(self::SESSION_AUTH_ID, $identity->getId());
268 2
        if ($this->authTimeout !== null) {
269 1
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
270
        }
271 2
        if ($this->absoluteAuthTimeout !== null) {
272 1
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
273
        }
274 2
    }
275
276
    /**
277
     * Updates the authentication status using the information from session.
278
     *
279
     * This method will try to determine the user identity using a session variable.
280
     *
281
     * If {@see authTimeout} is set, this method will refresh the timer.
282
     *
283
     * @throws Throwable
284
     */
285 7
    private function renewAuthStatus(): void
286
    {
287 7
        if ($this->session === null) {
288
            $this->setIdentity(new GuestIdentity());
289
            return;
290
        }
291
292
        /** @var mixed $id */
293 7
        $id = $this->session->get(self::SESSION_AUTH_ID);
294
295 7
        $identity = null;
296 7
        if ($id !== null) {
297 4
            $identity = $this->identityRepository->findIdentity((string)$id);
298
        }
299 6
        if ($identity === null) {
300 3
            $identity = new GuestIdentity();
301
        }
302 6
        $this->setIdentity($identity);
303
304
        if (
305 6
            !($identity instanceof GuestIdentity) &&
306 6
            ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)
307
        ) {
308
            /** @var mixed $expire */
309 3
            $expire = $this->authTimeout !== null ? $this->session->get(self::SESSION_AUTH_EXPIRE) : null;
310 3
            if ($expire !== null) {
311 1
                $expire = (int)$expire;
312
            }
313
314
            /** @var mixed $expireAbsolute */
315 3
            $expireAbsolute = $this->absoluteAuthTimeout !== null
316 1
                ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE)
317 3
                : null;
318 3
            if ($expireAbsolute !== null) {
319 1
                $expireAbsolute = (int)$expire;
320
            }
321
322 3
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
323 2
                $this->logout(false);
324 1
            } elseif ($this->authTimeout !== null) {
325 1
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
326
            }
327
        }
328 6
    }
329
330
    /**
331
     * Checks if the user can perform the operation as specified by the given permission.
332
     *
333
     * Note that you must provide access checker via {{@see User::setAccessChecker()}} in order to use this method.
334
     * Otherwise it will always return false.
335
     *
336
     * @param string $permissionName the name of the permission (e.g. "edit post") that needs access check.
337
     * @param array $params name-value pairs that would be passed to the rules associated
338
     * with the roles and permissions assigned to the user.
339
     *
340
     * @throws Throwable
341
     *
342
     * @return bool whether the user can perform the operation as specified by the given permission.
343
     */
344 2
    public function can(string $permissionName, array $params = []): bool
345
    {
346 2
        if ($this->accessChecker === null) {
347 1
            return false;
348
        }
349
350 1
        return $this->accessChecker->userHasPermission($this->getId(), $permissionName, $params);
351
    }
352
353 4
    public function setAuthTimeout(int $timeout = null): self
354
    {
355 4
        $this->authTimeout = $timeout;
356
357 4
        return $this;
358
    }
359
360 2
    public function setAbsoluteAuthTimeout(int $timeout = null): self
361
    {
362 2
        $this->absoluteAuthTimeout = $timeout;
363
364 2
        return $this;
365
    }
366
}
367