Passed
Push — master ( 2f2218...fc54fa )
by Alexander
02:12
created

CurrentUser::switchIdentity()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 2
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\AfterLogout;
13
use Yiisoft\User\Event\AfterLogin;
14
use Yiisoft\User\Event\BeforeLogout;
15
use Yiisoft\User\Event\BeforeLogin;
16
use Yiisoft\User\Guest\GuestIdentity;
17
use Yiisoft\User\Guest\GuestIdentityFactory;
18
use Yiisoft\User\Guest\GuestIdentityFactoryInterface;
19
use Yiisoft\User\Guest\GuestIdentityInterface;
20
21
use function time;
22
23
/**
24
 * Maintains current identity and allows logging in and out using it.
25
 */
26
final class CurrentUser
27
{
28
    private const SESSION_AUTH_ID = '__auth_id';
29
    private const SESSION_AUTH_EXPIRE = '__auth_expire';
30
    private const SESSION_AUTH_ABSOLUTE_EXPIRE = '__auth_absolute_expire';
31
32
    private IdentityRepositoryInterface $identityRepository;
33
    private EventDispatcherInterface $eventDispatcher;
34
    private GuestIdentityFactoryInterface $guestIdentityFactory;
35
    private ?AccessCheckerInterface $accessChecker = null;
36
    private ?SessionInterface $session = null;
37
38
    private ?IdentityInterface $identity = null;
39
    private ?IdentityInterface $identityOverride = null;
40
41
    private ?int $authTimeout = null;
42
    private ?int $absoluteAuthTimeout = null;
43
44 42
    public function __construct(
45
        IdentityRepositoryInterface $identityRepository,
46
        EventDispatcherInterface $eventDispatcher,
47
        GuestIdentityFactoryInterface $guestIdentityFactory = null
48
    ) {
49 42
        $this->identityRepository = $identityRepository;
50 42
        $this->eventDispatcher = $eventDispatcher;
51 42
        $this->guestIdentityFactory = $guestIdentityFactory ?? new GuestIdentityFactory();
52 42
    }
53
54
    /**
55
     * Returns a new instance with the specified session to store current user ID and auth timeouts.
56
     *
57
     * @param SessionInterface $session The session instance.
58
     *
59
     * @return self
60
     */
61 39
    public function withSession(SessionInterface $session): self
62
    {
63 39
        $new = clone $this;
64 39
        $new->session = $session;
65 39
        return $new;
66
    }
67
68
    /**
69
     * Returns a new instance with the specified access checker to check user permissions {@see can()}.
70
     *
71
     * @param AccessCheckerInterface $accessChecker The access checker instance.
72
     *
73
     * @return self
74
     */
75 2
    public function withAccessChecker(AccessCheckerInterface $accessChecker): self
76
    {
77 2
        $new = clone $this;
78 2
        $new->accessChecker = $accessChecker;
79 2
        return $new;
80
    }
81
82
    /**
83
     * Returns a new instance with the specified number of seconds in which
84
     * the user will be logged out automatically in case of remaining inactive.
85
     *
86
     * @param int $timeout The number of seconds in which the user will be logged out automatically in case of
87
     * remaining inactive. Default is `null`, the user will be logged out after the current session expires.
88
     *
89
     * @return self
90
     */
91 7
    public function withAuthTimeout(int $timeout): self
92
    {
93 7
        $new = clone $this;
94 7
        $new->authTimeout = $timeout;
95 7
        return $new;
96
    }
97
98
    /**
99
     * Returns a new instance with the specified number of seconds in which
100
     * the user will be logged out automatically regardless of activity.
101
     *
102
     * @param int $timeout The number of seconds in which the user will be
103
     * logged out automatically regardless of activity.
104
     *
105
     * @return self
106
     */
107 5
    public function withAbsoluteAuthTimeout(int $timeout): self
108
    {
109 5
        $new = clone $this;
110 5
        $new->absoluteAuthTimeout = $timeout;
111 5
        return $new;
112
    }
113
114
    /**
115
     * Returns the identity object associated with the currently logged-in user.
116
     */
117 37
    public function getIdentity(): IdentityInterface
118
    {
119 37
        $identity = $this->identityOverride ?? $this->identity;
120
121 37
        if ($identity === null) {
122 30
            $id = $this->getSavedId();
123
124 30
            if ($id !== null) {
125 8
                $identity = $this->identityRepository->findIdentity($id);
126
            }
127
128 30
            $identity ??= $this->guestIdentityFactory->create();
129 30
            $this->identity = $identity;
130
        }
131
132 37
        return $identity;
133
    }
134
135
    /**
136
     * Returns a value that uniquely represents the user.
137
     *
138
     * @return string|null The unique identifier for the user. If `null`, it means the user is a guest.
139
     *
140
     * @see getIdentity()
141
     */
142 3
    public function getId(): ?string
143
    {
144 3
        return $this->getIdentity()->getId();
145
    }
146
147
    /**
148
     * Returns a value indicating whether the user is a guest (not authenticated).
149
     *
150
     * @return bool Whether the current user is a guest.
151
     *
152
     * @see getIdentity()
153
     */
154 24
    public function isGuest(): bool
155
    {
156 24
        return $this->getIdentity() instanceof GuestIdentityInterface;
157
    }
158
159
    /**
160
     * Checks if the user can perform the operation as specified by the given permission.
161
     *
162
     * Note that you must provide access checker via {@see withAccessChecker()} in order to use this
163
     * method. Otherwise, it will always return `false`.
164
     *
165
     * @param string $permissionName The name of the permission (e.g. "edit post") that needs access check.
166
     * @param array $params Name-value pairs that would be passed to the rules associated with the roles and
167
     * permissions assigned to the user.
168
     *
169
     * @return bool Whether the user can perform the operation as specified by the given permission.
170
     */
171 2
    public function can(string $permissionName, array $params = []): bool
172
    {
173 2
        if ($this->accessChecker === null) {
174 1
            return false;
175
        }
176
177 1
        return $this->accessChecker->userHasPermission($this->getId(), $permissionName, $params);
178
    }
179
180
    /**
181
     * Logs in a user.
182
     *
183
     * @param IdentityInterface $identity The user identity (which should already be authenticated).
184
     *
185
     * @return bool Whether the user is logged in.
186
     */
187 15
    public function login(IdentityInterface $identity): bool
188
    {
189 15
        if ($this->beforeLogin($identity)) {
190 15
            $this->switchIdentity($identity);
191 15
            $this->afterLogin($identity);
192
        }
193
194 15
        return !$this->isGuest();
195
    }
196
197
    /**
198
     * Logs out the current user.
199
     *
200
     * @return bool Whether the user is logged out.
201
     */
202 4
    public function logout(): bool
203
    {
204 4
        if ($this->isGuest()) {
205 1
            return false;
206
        }
207
208 3
        $identity = $this->getIdentity();
209
210 3
        if ($this->beforeLogout($identity)) {
211 3
            $this->switchIdentity($this->guestIdentityFactory->create());
212 3
            $this->afterLogout($identity);
213
        }
214
215 3
        return $this->isGuest();
216
    }
217
218
    /**
219
     * Overrides identity.
220
     *
221
     * @param IdentityInterface $identity The identity instance to overriding.
222
     */
223 3
    public function overrideIdentity(IdentityInterface $identity): void
224
    {
225 3
        $this->identityOverride = $identity;
226 3
    }
227
228
    /**
229
     * Clears the identity override.
230
     */
231 1
    public function clearIdentityOverride(): void
232
    {
233 1
        $this->identityOverride = null;
234 1
    }
235
236
    /**
237
     * Clears the data for working with the event loop.
238
     */
239 1
    public function clear(): void
240
    {
241 1
        $this->identity = null;
242 1
        $this->identityOverride = null;
243 1
    }
244
245
    /**
246
     * This method is called before logging in a user.
247
     * The default implementation will trigger the {@see BeforeLogin} event.
248
     *
249
     * @param IdentityInterface $identity The user identity information.
250
     *
251
     * @return bool Whether the user should continue to be logged in.
252
     */
253 15
    private function beforeLogin(IdentityInterface $identity): bool
254
    {
255 15
        $event = new BeforeLogin($identity);
256 15
        $this->eventDispatcher->dispatch($event);
257 15
        return $event->isValid();
258
    }
259
260
    /**
261
     * This method is called after the user is successfully logged in.
262
     *
263
     * @param IdentityInterface $identity The user identity information.
264
     */
265 15
    private function afterLogin(IdentityInterface $identity): void
266
    {
267 15
        $this->eventDispatcher->dispatch(new AfterLogin($identity));
268 15
    }
269
270
    /**
271
     * This method is invoked when calling {@see logout()} to log out a user.
272
     *
273
     * @param IdentityInterface $identity The user identity information.
274
     *
275
     * @return bool Whether the user should continue to be logged out.
276
     */
277 3
    private function beforeLogout(IdentityInterface $identity): bool
278
    {
279 3
        $event = new BeforeLogout($identity);
280 3
        $this->eventDispatcher->dispatch($event);
281 3
        return $event->isValid();
282
    }
283
284
    /**
285
     * This method is invoked right after a user is logged out via {@see logout()}.
286
     *
287
     * @param IdentityInterface $identity The user identity information.
288
     */
289 3
    private function afterLogout(IdentityInterface $identity): void
290
    {
291 3
        $this->eventDispatcher->dispatch(new AfterLogout($identity));
292 3
    }
293
294
    /**
295
     * Switches to a new identity for the current user.
296
     *
297
     * This method is called by {@see login()} and {@see logout()} when the current
298
     * user needs to be associated with the corresponding identity information.
299
     *
300
     * @param IdentityInterface $identity The identity information to be associated with the current user.
301
     * In order to indicate that the user is guest, use {@see GuestIdentityInterface, GuestIdentity}.
302
     */
303 15
    private function switchIdentity(IdentityInterface $identity): void
304
    {
305 15
        $this->identity = $identity;
306 15
        $this->saveId($identity instanceof GuestIdentityInterface ? null : $identity->getId());
307 15
    }
308
309 30
    private function getSavedId(): ?string
310
    {
311 30
        if ($this->session === null) {
312 2
            return null;
313
        }
314
315
        /** @var mixed $id */
316 28
        $id = $this->session->get(self::SESSION_AUTH_ID);
317
318 28
        if ($id !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
319 7
            $expire = $this->getExpire();
320 7
            $expireAbsolute = $this->getExpireAbsolute();
321
322 7
            if (($expire !== null && $expire < time()) || ($expireAbsolute !== null && $expireAbsolute < time())) {
323 2
                $this->saveId(null);
324 2
                return null;
325
            }
326
327 5
            if ($this->authTimeout !== null) {
328 3
                $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
329
            }
330
        }
331
332 26
        return $id === null ? null : (string) $id;
333
    }
334
335 7
    private function getExpire(): ?int
336
    {
337
        /**
338
         * @var mixed $expire
339
         * @psalm-suppress PossiblyNullReference
340
         */
341 7
        $expire = $this->authTimeout !== null
342 4
            ? $this->session->get(self::SESSION_AUTH_EXPIRE)
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

342
            ? $this->session->/** @scrutinizer ignore-call */ get(self::SESSION_AUTH_EXPIRE)

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...
343 3
            : null
344
        ;
345
346 7
        return $expire !== null ? (int) $expire : null;
347
    }
348
349 7
    private function getExpireAbsolute(): ?int
350
    {
351
        /**
352
         * @var mixed $expire
353
         * @psalm-suppress PossiblyNullReference
354
         */
355 7
        $expire = $this->absoluteAuthTimeout !== null
356 3
            ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE)
357 4
            : null
358
        ;
359
360 7
        return $expire !== null ? (int) $expire : null;
361
    }
362
363 17
    private function saveId(?string $id): void
364
    {
365 17
        if ($this->session === null) {
366 2
            return;
367
        }
368
369 15
        $this->session->regenerateID();
370
371 15
        $this->session->remove(self::SESSION_AUTH_ID);
372 15
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
373 15
        $this->session->remove(self::SESSION_AUTH_ABSOLUTE_EXPIRE);
374
375 15
        if ($id === null) {
376 5
            return;
377
        }
378
379 13
        $this->session->set(self::SESSION_AUTH_ID, $id);
380
381 13
        if ($this->authTimeout !== null) {
382 2
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
383
        }
384
385 13
        if ($this->absoluteAuthTimeout !== null) {
386 2
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
387
        }
388 13
    }
389
}
390