Issues (1)

src/CurrentUser.php (1 issue)

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

332
            ? $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...
333 3
            : null
334 7
        ;
335
336 7
        return $expire !== null ? (int) $expire : null;
337
    }
338
339 7
    private function getExpireAbsolute(): ?int
340
    {
341
        /**
342
         * @var mixed $expire
343
         *
344
         * @psalm-suppress PossiblyNullReference
345
         */
346 7
        $expire = $this->absoluteAuthTimeout !== null
347 3
            ? $this->session->get(self::SESSION_AUTH_ABSOLUTE_EXPIRE)
348 4
            : null
349 7
        ;
350
351 7
        return $expire !== null ? (int) $expire : null;
352
    }
353
354 22
    private function saveId(?string $id): void
355
    {
356 22
        if ($this->session === null) {
357 4
            return;
358
        }
359
360 18
        $this->session->regenerateID();
361
362 18
        $this->session->remove(self::SESSION_AUTH_ID);
363 18
        $this->session->remove(self::SESSION_AUTH_EXPIRE);
364 18
        $this->session->remove(self::SESSION_AUTH_ABSOLUTE_EXPIRE);
365
366 18
        if ($id === null) {
367 5
            return;
368
        }
369
370 16
        $this->session->set(self::SESSION_AUTH_ID, $id);
371
372 16
        if ($this->authTimeout !== null) {
373 2
            $this->session->set(self::SESSION_AUTH_EXPIRE, time() + $this->authTimeout);
374
        }
375
376 16
        if ($this->absoluteAuthTimeout !== null) {
377 2
            $this->session->set(self::SESSION_AUTH_ABSOLUTE_EXPIRE, time() + $this->absoluteAuthTimeout);
378
        }
379
    }
380
}
381