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

User::can()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
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 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