Passed
Pull Request — master (#17)
by Rustam
02:21
created

User::authTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\User;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Yiisoft\Access\AccessCheckerInterface;
9
use Yiisoft\Auth\IdentityInterface;
10
use Yiisoft\Auth\IdentityRepositoryInterface;
11
use Yiisoft\Session\SessionInterface;
12
use Yiisoft\User\Event\AfterLogin;
13
use Yiisoft\User\Event\AfterLogout;
14
use Yiisoft\User\Event\BeforeLogin;
15
use Yiisoft\User\Event\BeforeLogout;
16
17
class User
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
    /**
24
     * @var int|null the number of seconds in which the user will be logged out automatically in case of
25
     * remaining inactive. If this property is not set, the user will be logged out after
26
     * the current session expires.
27
     */
28
    private ?int $authTimeout = null;
29
30
    /**
31
     * @var int|null the number of seconds in which the user will be logged out automatically
32
     * regardless of activity.
33
     */
34
    private ?int $absoluteAuthTimeout = null;
35
36
    private IdentityRepositoryInterface $identityRepository;
37
    private EventDispatcherInterface $eventDispatcher;
38
39
    private ?AccessCheckerInterface $accessChecker = null;
40
    private ?IdentityInterface $identity = null;
41
    private ?SessionInterface $session;
42
43
    /**
44
     * @param IdentityRepositoryInterface $identityRepository
45
     * @param EventDispatcherInterface $eventDispatcher
46
     * @param SessionInterface|null $session session to persist authentication status across multiple requests.
47
     * If not set, authentication has to be performed on each request, which is often the case for stateless
48
     * application such as RESTful API.
49
     */
50 22
    public function __construct(
51
        IdentityRepositoryInterface $identityRepository,
52
        EventDispatcherInterface $eventDispatcher,
53
        SessionInterface $session = null
54
    ) {
55 22
        $this->identityRepository = $identityRepository;
56 22
        $this->eventDispatcher = $eventDispatcher;
57 22
        $this->session = $session;
58 22
    }
59
60 1
    public function setAccessChecker(AccessCheckerInterface $accessChecker): void
61
    {
62 1
        $this->accessChecker = $accessChecker;
63 1
    }
64
65
    /**
66
     * Returns the identity object associated with the currently logged-in user.
67
     * This method read the user's authentication data
68
     * stored in session and reconstruct the corresponding identity object, if it has not done so before.
69
     *
70
     * @param bool $autoRenew whether to automatically renew authentication status if it has not been done so before.
71
     *
72
     * @throws \Throwable
73
     *
74
     * @return IdentityInterface the identity object associated with the currently logged-in user.
75
     *
76
     * @see logout()
77
     * @see login()
78
     */
79 17
    public function getIdentity(bool $autoRenew = true): IdentityInterface
80
    {
81 17
        if ($this->identity !== null) {
82 9
            return $this->identity;
83
        }
84 10
        if ($this->session === null || !$autoRenew) {
85 3
            return new GuestIdentity();
86
        }
87
        try {
88 7
            $this->renewAuthStatus();
89 1
        } catch (\Throwable $e) {
90 1
            $this->identity = null;
91 1
            throw $e;
92
        }
93 6
        return $this->identity ?? new GuestIdentity();
94
    }
95
96
    /**
97
     * Sets the user identity object.
98
     *
99
     * Note that this method does not deal with session. You should usually use {@see switchIdentity()}
100
     * to change the identity of the current user.
101
     *
102
     * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
103
     * Use {{@see GuestIdentity}} to indicate that the current user is a guest.
104
     */
105 13
    public function setIdentity(IdentityInterface $identity): void
106
    {
107 13
        $this->identity = $identity;
108 13
    }
109
110
    /**
111
     * Logs in a user.
112
     *
113
     * After logging in a user:
114
     * - the user's identity information is obtainable from the {@see getIdentity()}
115
     * - the identity information will be stored in session and be available in the next requests as long as the session
116
     *   remains active or till the user closes the browser. Some browsers, such as Chrome, are keeping session when
117
     *   browser is re-opened.
118
     *
119
     * @param IdentityInterface $identity the user identity (which should already be authenticated)
120
     *
121
     * @return bool whether the user is logged in
122
     */
123 2
    public function login(IdentityInterface $identity): bool
124
    {
125 2
        if ($this->beforeLogin($identity)) {
126 2
            $this->switchIdentity($identity);
127 2
            $this->afterLogin($identity);
128
        }
129 2
        return !$this->isGuest();
130
    }
131
132
    /**
133
     * Logs in a user by the given access token.
134
     * This method will first authenticate the user by calling {@see IdentityInterface::findIdentityByToken()}
135
     * with the provided access token. If successful, it will call {@see login()} to log in the authenticated user.
136
     * If authentication fails or {@see login()} is unsuccessful, it will return null.
137
     *
138
     * @param string $token the access token
139
     * @param string $type the type of the token. The value of this parameter depends on the implementation.
140
     *
141
     * @return IdentityInterface|null the identity associated with the given access token. Null is returned if
142
     * the access token is invalid or {@see login()} is unsuccessful.
143
     */
144 2
    public function loginByAccessToken(string $token, string $type): ?IdentityInterface
145
    {
146 2
        $identity = $this->identityRepository->findIdentityByToken($token, $type);
147 2
        if ($identity && $this->login($identity)) {
148 1
            return $identity;
149
        }
150 1
        return null;
151
    }
152
153
    /**
154
     * Logs out the current user.
155
     * This will remove authentication-related session data.
156
     * If `$destroySession` is true, all session data will be removed.
157
     *
158
     * @param bool $destroySession whether to destroy the whole session. Defaults to true.
159
     *
160
     * @throws \Throwable
161
     *
162
     * @return bool whether the user is logged out
163
     */
164 5
    public function logout(bool $destroySession = true): bool
165
    {
166 5
        $identity = $this->getIdentity();
167 5
        if ($this->isGuest()) {
168 1
            return false;
169
        }
170 4
        if ($this->beforeLogout($identity)) {
171 4
            $this->switchIdentity(new GuestIdentity());
172 4
            if ($destroySession && $this->session) {
173 1
                $this->session->destroy();
174
            }
175
176 4
            $this->afterLogout($identity);
177
        }
178 4
        return $this->isGuest();
179
    }
180
181
    /**
182
     * Returns a value indicating whether the user is a guest (not authenticated).
183
     *
184
     * @return bool whether the current user is a guest.
185
     *
186
     * @see getIdentity()
187
     */
188 9
    public function isGuest(): bool
189
    {
190 9
        return $this->getIdentity() instanceof GuestIdentity;
191
    }
192
193
    /**
194
     * Returns a value that uniquely represents the user.
195
     *
196
     * @throws \Throwable
197
     *
198
     * @return string the unique identifier for the user. If `null`, it means the user is a guest.
199
     *
200
     * @see getIdentity()
201
     */
202 3
    public function getId(): ?string
203
    {
204 3
        return $this->getIdentity()->getId();
205
    }
206
207
    /**
208
     * This method is called before logging in a user.
209
     * The default implementation will trigger the {@see BeforeLogin} event.
210
     * If you override this method, make sure you call the parent implementation
211
     * so that the event is triggered.
212
     *
213
     * @param IdentityInterface $identity the user identity information
214
     *
215
     * @return bool whether the user should continue to be logged in
216
     */
217 2
    private function beforeLogin(IdentityInterface $identity): bool
218
    {
219 2
        $event = new BeforeLogin($identity);
220 2
        $this->eventDispatcher->dispatch($event);
221 2
        return $event->isValid();
222
    }
223
224
    /**
225
     * This method is called after the user is successfully logged in.
226
     *
227
     * @param IdentityInterface $identity the user identity information
228
     */
229 2
    private function afterLogin(IdentityInterface $identity): void
230
    {
231 2
        $this->eventDispatcher->dispatch(new AfterLogin($identity));
232 2
    }
233
234
    /**
235
     * This method is invoked when calling {@see logout()} to log out a user.
236
     *
237
     * @param IdentityInterface $identity the user identity information
238
     *
239
     * @return bool whether the user should continue to be logged out
240
     */
241 4
    private function beforeLogout(IdentityInterface $identity): bool
242
    {
243 4
        $event = new BeforeLogout($identity);
244 4
        $this->eventDispatcher->dispatch($event);
245 4
        return $event->isValid();
246
    }
247
248
    /**
249
     * This method is invoked right after a user is logged out via {@see logout()}.
250
     *
251
     * @param IdentityInterface $identity the user identity information
252
     */
253 4
    private function afterLogout(IdentityInterface $identity): void
254
    {
255 4
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
256 4
    }
257
258
    /**
259
     * Switches to a new identity for the current user.
260
     *
261
     * This method use session to store the user identity information.
262
     * Please refer to {@see login()} for more details.
263
     *
264
     * This method is mainly called by {@see login()} and {@see logout()}
265
     * when the current user needs to be associated with the corresponding identity information.
266
     *
267
     * @param IdentityInterface $identity the identity information to be associated with the current user.
268
     * In order to indicate that the user is guest, use {{@see GuestIdentity}}.
269
     */
270 6
    private function switchIdentity(IdentityInterface $identity): void
271
    {
272 6
        $this->setIdentity($identity);
273 6
        if ($this->session === null) {
274 1
            return;
275
        }
276
277 5
        $this->session->regenerateID();
278
279 5
        $this->session->remove(self::SESSION_AUTH_ID);
280 5
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
281
282 5
        if ($identity->getId() === null) {
283 3
            return;
284
        }
285 2
        $this->session->set(self::SESSION_AUTH_ID, $identity->getId());
286 2
        if ($this->authTimeout !== null) {
287
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
288
        }
289 2
        if ($this->absoluteAuthTimeout !== null) {
290
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
291
        }
292 2
    }
293
294
    /**
295
     * Updates the authentication status using the information from session.
296
     *
297
     * This method will try to determine the user identity using a session variable.
298
     *
299
     * If {@see authTimeout} is set, this method will refresh the timer.
300
     *
301
     * @throws \Throwable
302
     */
303 7
    private function renewAuthStatus(): void
304
    {
305 7
        $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

305
        /** @scrutinizer ignore-call */ 
306
        $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...
306
307 7
        $identity = null;
308 7
        if ($id !== null) {
309 4
            $identity = $this->identityRepository->findIdentity($id);
310
        }
311 6
        if ($identity === null) {
312 3
            $identity = new GuestIdentity();
313
        }
314 6
        $this->setIdentity($identity);
315
316 6
        if (!($identity instanceof GuestIdentity) && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
317 3
            $expire = $this->authTimeout !== null ? $this->session->get(self::SESSION_AUTH_EXPIRE) : null;
318 3
            $expireAbsolute = $this->absoluteAuthTimeout !== null
319 1
                ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE)
320 3
                : null;
321 3
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
322 2
                $this->logout(false);
323 1
            } elseif ($this->authTimeout !== null) {
324 1
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
325
            }
326
        }
327 6
    }
328
329
    /**
330
     * Checks if the user can perform the operation as specified by the given permission.
331
     *
332
     * Note that you must provide access checker via {{@see User::setAccessChecker()}} in order to use this method.
333
     * Otherwise it will always return false.
334
     *
335
     * @param string $permissionName the name of the permission (e.g. "edit post") that needs access check.
336
     * @param array $params name-value pairs that would be passed to the rules associated
337
     * with the roles and permissions assigned to the user.
338
     *
339
     * @throws \Throwable
340
     *
341
     * @return bool whether the user can perform the operation as specified by the given permission.
342
     */
343 2
    public function can(string $permissionName, array $params = []): bool
344
    {
345 2
        if ($this->accessChecker === null) {
346 1
            return false;
347
        }
348
349 1
        return $this->accessChecker->userHasPermission($this->getId(), $permissionName, $params);
350
    }
351
352 2
    public function authTimeout(int $timeout = null): self
353
    {
354 2
        $this->authTimeout = $timeout;
355
356 2
        return $this;
357
    }
358
359 1
    public function absoluteAuthTimeout(int $timeout = null): self
360
    {
361 1
        $this->absoluteAuthTimeout = $timeout;
362
363 1
        return $this;
364
    }
365
}
366