SessionGuard   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 81
c 1
b 0
f 1
dl 0
loc 361
rs 9.2
wmc 40

20 Methods

Rating   Name   Duplication   Size   Complexity  
A check() 0 3 1
A getName() 0 3 1
A refreshRememberToken() 0 7 1
A attempt() 0 15 3
A guest() 0 3 1
A getRecallerName() 0 3 1
A login() 0 12 2
A init() 0 6 1
A createRememberTokenIfDoesntExist() 0 4 2
A updateSession() 0 3 1
A logout() 0 19 2
A fireAttemptingEvent() 0 6 1
B user() 0 40 7
A fireLoginEvent() 0 5 1
A getRecaller() 0 3 1
A validRecaller() 0 9 5
A hasValidCredentials() 0 3 2
A id() 0 7 2
A getUserByRecaller() 0 13 3
A clearUserDataFromStorage() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like SessionGuard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SessionGuard, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Framy Framework
4
 *
5
 * @copyright Copyright Framy
6
 * @Author Marco Bier <[email protected]>
7
 */
8
9
namespace app\framework\Component\Auth;
10
11
use app\framework\Component\Database\DB;
12
use app\framework\Component\Database\Model\Model;
13
use app\framework\Component\EventManager\EventManagerTrait;
14
use app\framework\Component\Hashing\Hash;
15
use app\framework\Component\Http\Session;
16
use app\framework\Component\Routing\Request;
17
use app\framework\Component\StdLib\SingletonTrait;
18
use app\framework\Component\StdLib\StdObject\StringObject\StringObject;
19
use app\framework\Component\StdLib\StdObject\StringObject\StringObjectException;
20
use Exception;
21
22
class SessionGuard
23
{
24
    use SingletonTrait,EventManagerTrait;
25
26
    /**
27
     * The currently authenticated user.
28
     */
29
    protected $user;
30
31
    /**
32
     * @var Session
33
     */
34
    protected $session;
35
36
    /**
37
     * The user provider implementation.
38
     *
39
     * @var UserProvider
40
     */
41
    protected $provider;
42
43
    /**
44
     * Indicates if the logout method has been called.
45
     *
46
     * @var bool
47
     */
48
    protected $loggedOut = false;
49
50
    /**
51
     * @var Request
52
     */
53
    protected $request;
54
55
    /**
56
     * Indicates if a token user retrieval has been attempted.
57
     *
58
     * @var bool
59
     */
60
    protected $tokenRetrievalAttempted = false;
61
62
    /**
63
     *
64
     */
65
    public function init()
66
    {
67
        $this->session  = new Session();
68
        $this->provider = new UserProvider();
69
70
        $this->request = Request::createFromGlobals();
71
    }
72
73
    /**
74
     * Determine if the current user is authenticated.
75
     *
76
     * @return bool
77
     * @throws StringObjectException
78
     */
79
    public function check()
80
    {
81
        return ! is_null($this->user());
82
    }
83
84
    /**
85
     * Determine if the current user is a guest.
86
     *
87
     * @return bool
88
     * @throws StringObjectException
89
     */
90
    public function guest()
91
    {
92
        return ! $this->check();
93
    }
94
95
    /**
96
     * Get the ID for the currently authenticated user.
97
     *
98
     * @return int|null
99
     * @throws StringObjectException
100
     */
101
    public function id()
102
    {
103
        if ($this->user()) {
104
            return $this->user()->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on app\framework\Component\Database\Model\Model. Since you implemented __get, consider adding a @property annotation.
Loading history...
105
        }
106
107
        return null;
108
    }
109
110
    /**
111
     * Update the session with the given ID.
112
     *
113
     * @param  string  $id
114
     * @return void
115
     */
116
    protected function updateSession($id)
117
    {
118
        $this->session->set($this->getName(), $id);
119
    }
120
121
    /**
122
     * Get the currently authenticated user.
123
     * @return Model|null
124
     * @throws StringObjectException
125
     */
126
    public function user()
127
    {
128
        if ($this->loggedOut) {
129
            return null;
130
        }
131
132
        // If we've already retrieved the user for the current request we can just
133
        // return it back immediately. We do not want to fetch the user data on
134
        // every call to this method because that would be tremendously slow.
135
        if (! is_null($this->user)) {
136
            return $this->user;
137
        }
138
139
        $id = $this->session->get($this->getName());
140
141
        // First we will try to load the user using the identifier in the session if
142
        // one exists. Otherwise we will check for a "remember me" cookie in this
143
        // request, and if one exists, attempt to retrieve the user using that.
144
        $user = null;
145
146
        if (! is_null($id)) {
147
            $user = $this->provider->retrieveById($id);
148
        }
149
150
        // If the user is null, but we decrypt a "recaller" cookie we can attempt to
151
        // pull the user data on that cookie which serves as a remember cookie on
152
        // the application. Once we have a user we can return it to the caller.
153
        $recaller = $this->getRecaller();
154
155
        if (is_null($user) && ! is_null($recaller)) {
156
            $user = $this->getUserByRecaller($recaller);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $user is correct as $this->getUserByRecaller($recaller) targeting app\framework\Component\...rd::getUserByRecaller() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
157
158
            if ($user) {
159
                $this->updateSession($user->id);
160
161
                $this->fireLoginEvent($user, true);
162
            }
163
        }
164
165
        return $this->user = $user;
166
    }
167
168
    /**
169
     * Attempt to authenticate a user using the given credentials.
170
     *
171
     * @param array $credentials
172
     * @param bool $remember
173
     * @param bool $login
174
     * @return bool
175
     * @throws StringObjectException
176
     */
177
    public function attempt(array $credentials = [], bool $remember = false, $login = true): bool
178
    {
179
        $this->fireAttemptingEvent($credentials, $remember, $login);
180
181
        $this->lastAttempted = $this->user = $this->provider->retrieveByCredentials($credentials);
0 ignored issues
show
Bug Best Practice introduced by
The property lastAttempted does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
182
183
        if ($this->hasValidCredentials($this->user, $credentials)) {
184
            if ($login) {
185
                $this->login($this->user, $remember);
186
            }
187
188
            return true;
189
        }
190
191
        return false;
192
    }
193
194
    /**
195
     * @throws Exception
196
     */
197
    protected function createRememberTokenIfDoesntExist()
198
    {
199
        if (! isset($this->user->remember_token)) {
200
            $this->refreshRememberToken();
201
        }
202
    }
203
204
    /**
205
     * Remove the user data from the session and cookies.
206
     *
207
     * @return void
208
     */
209
    protected function clearUserDataFromStorage()
210
    {
211
        $this->session->remove($this->getName());
212
213
        if (! is_null($this->getRecaller())) {
214
            $recaller = $this->getRecallerName();
215
216
            setcookie($recaller, null, -1, '/');
217
        }
218
    }
219
220
    /**
221
     * @throws Exception
222
     */
223
    protected function refreshRememberToken()
224
    {
225
        $this->user->rember_token = $token = StringObject::random(60);
226
227
        DB::update("UPDATE users SET remember_token=:token WHERE id=:id", [
228
            'token' => $token,
229
            'id' => $this->user->id
230
        ]);
231
    }
232
233
    /**
234
     * @param $user
235
     * @param $remember
236
     * @throws StringObjectException
237
     * @throws Exception
238
     */
239
    public function login($user, $remember)
240
    {
241
        $this->session->set($this->getName(), $user->id);
242
243
        if ($remember) {
244
            $this->createRememberTokenIfDoesntExist();
245
246
            $value = $user->id."|".$user->remember_token;
247
            setcookie("remember_session_".sha1(get_class($this)), $value);
248
        }
249
250
        $this->fireLoginEvent($user, $remember);
251
    }
252
253
    /**
254
     * Logout authenticated user
255
     *
256
     * @throws Exception
257
     */
258
    public function logout()
259
    {
260
        // If we have an event dispatcher instance, we can fire off the logout event
261
        // so any further processing can be done. This allows the developer to be
262
        // listening for anytime a user signs out of this application manually.
263
        $this->clearUserDataFromStorage();
264
265
        if (! is_null($this->user)) {
266
            $this->refreshRememberToken();
267
        }
268
269
        //TODO: fire logout event
270
271
        // Once we have fired the logout event we will clear the users out of memory
272
        // so they are no longer available as the user is no longer considered as
273
        // being signed into this application and should not be available here.
274
        $this->user = null;
275
276
        $this->loggedOut = true;
277
    }
278
279
    /**
280
     * @param array $credentials
281
     * @param bool $remember
282
     * @param bool $login
283
     * @throws StringObjectException
284
     */
285
    protected function fireAttemptingEvent(array $credentials = [], bool $remember = false, $login = true)
286
    {
287
        $this->eventManager()->fire("auth.attempting", [
288
            'credentials' => $credentials,
289
            'remember' => $remember,
290
            'login' => $login
291
        ]);
292
    }
293
294
    /**
295
     * @param $user
296
     * @param $remember
297
     * @throws StringObjectException
298
     */
299
    protected function fireLoginEvent($user, $remember)
300
    {
301
        $this->eventManager()->fire("auth.login", [
302
            'user' => $user,
303
            'remember' => $remember,
304
        ]);
305
    }
306
307
    /**
308
     * @param $recaller
309
     * @return Model|null
310
     * @throws StringObjectException
311
     */
312
    public function getUserByRecaller($recaller)
313
    {
314
        if ($this->validRecaller($recaller) && ! $this->tokenRetrievalAttempted) {
315
            $this->tokenRetrievalAttempted = true;
316
317
            list($id, $token) = explode('|', $recaller, 2);
318
319
            $this->viaRemember = ! is_null($user = $this->provider->retrieveByToken($id, $token));
0 ignored issues
show
introduced by
The condition is_null($user = $this->p...veByToken($id, $token)) is always true.
Loading history...
Bug Best Practice introduced by
The property viaRemember does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug introduced by
Are you sure the assignment to $user is correct as $this->provider->retrieveByToken($id, $token) targeting app\framework\Component\...ider::retrieveByToken() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
320
321
            return $user;
322
        }
323
324
        return null;
325
    }
326
327
    /**
328
     * Get a unique identifier for the auth session value.
329
     *
330
     * @return string
331
     */
332
    public function getName()
333
    {
334
        return 'login_session_'.sha1(get_class($this));
335
    }
336
337
    /**
338
     * @return mixed
339
     */
340
    public function getRecaller()
341
    {
342
        return $this->request->cookies()->get($this->getRecallerName());
343
    }
344
345
    /**
346
     * Get the name of the cookie used to store the "recaller".
347
     *
348
     * @return string
349
     */
350
    public function getRecallerName()
351
    {
352
        return 'remember_session_'.sha1(get_class($this));
353
    }
354
355
    /**
356
     * Check if user credentials are valid
357
     *
358
     * @param $user
359
     * @param $credentials
360
     * @return bool
361
     */
362
    protected function hasValidCredentials($user, $credentials)
363
    {
364
        return !is_null($user) && Hash::check($credentials['password'], $user->password);
365
    }
366
367
    /**
368
     * Determine if the recaller cookie is in a valid format.
369
     *
370
     * @param  mixed  $recaller
371
     * @throws StringObjectException
372
     * @return bool
373
     */
374
    protected function validRecaller($recaller)
375
    {
376
        if (! is_string($recaller) || ! (new StringObject($recaller))->contains('|')) {
377
            return false;
378
        }
379
380
        $segments = explode('|', $recaller);
381
382
        return count($segments) == 2 && trim($segments[0]) !== '' && trim($segments[1]) !== '';
383
    }
384
}
385