Passed
Push — 2.x ( 3c49c1...b272d3 )
by Terry
02:18
created

Session   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 30
eloc 92
c 5
b 0
f 0
dl 0
loc 393
rs 10

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
A remove() 0 6 2
A log() 0 4 2
A init() 0 37 4
A getChannel() 0 3 1
A has() 0 5 1
A isInitialized() 0 3 1
A create() 0 16 1
A assertInit() 0 5 2
A set() 0 5 1
A parsedData() 0 6 2
A resetCookie() 0 20 2
A setId() 0 8 1
A get() 0 5 1
A clear() 0 5 1
A save() 0 12 1
A getId() 0 3 1
A gc() 0 21 5
1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 * 
10
 * php version 7.1.0
11
 * 
12
 * @category  Web-security
13
 * @package   Shieldon
14
 * @author    Terry Lin <[email protected]>
15
 * @copyright 2019 terrylinooo
16
 * @license   https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT
17
 * @link      https://github.com/terrylinooo/shieldon
18
 * @see       https://shieldon.io
19
 */
20
21
declare(strict_types=1);
22
23
namespace Shieldon\Firewall;
24
25
use Shieldon\Firewall\Container;
26
use Shieldon\Firewall\Driver\DirverProvider;
0 ignored issues
show
Bug introduced by
The type Shieldon\Firewall\Driver\DirverProvider was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Shieldon\Firewall\Log\SessionLogger;
28
use Shieldon\Event\Event;
29
use RuntimeException;
30
use function Shieldon\Firewall\create_session_id;
31
use function Shieldon\Firewall\get_ip;
32
use function Shieldon\Firewall\get_microtimesamp;
33
use function Shieldon\Firewall\get_request;
34
use function Shieldon\Firewall\get_response;
35
use function Shieldon\Firewall\set_response;
36
use function intval;
37
use function rand;
38
use function setcookie;
39
use function time;
40
use function php_sapi_name;
41
42
/*
43
 * Session for the use of Shieldon.
44
 */
45
class Session
46
{
47
    /**
48
     *   Public methods       | Desctiotion
49
     *  ----------------------|---------------------------------------------
50
     *   init                 | Initialize the session.
51
     *   getId                | Get session ID.
52
     *   setId                | Set session ID
53
     *   isInitialized        | Check if a session has been initialized or not.
54
     *   get                  | Get specific value from session by key.
55
     *   set                  | To store data in the session.
56
     *   remove               | To delete data from the session.
57
     *   has                  | To determine if an item is present in the session.
58
     *   clear                | Clear all data in the session array.
59
     *   save                 | Save session data into database.
60
     *   ::resetCookie        | Create a new session cookie for current user.
61
     *  ----------------------|---------------------------------------------
62
     */
63
64
    /**
65
     * The session data.
66
     *
67
     * @var array
68
     */
69
    protected $data = [];
70
71
    /**
72
     * The session data will be removed after expiring.
73
     * Time unit: second.
74
     *
75
     * @var int
76
     */
77
    protected $expire = 600;
78
79
    /**
80
     * The Shieldon kernel.
81
     *
82
     * @var Kernel|null
83
     */
84
    protected $kernel;
85
86
    /**
87
     * The data driver.
88
     *
89
     * @var DirverProvider|null
90
     */
91
    protected $driver;
92
93
    /**
94
     * Make sure the init() run first.
95
     *
96
     * @var bool
97
     */
98
    protected static $status = false;
99
100
    /**
101
     * A session Id.
102
     *
103
     * @var string
104
     */
105
    protected static $id = '_php_cli_';
106
107
    /**
108
     * Constructor.
109
     * 
110
     * @param string $id Session ID
111
     */
112
    public function __construct(string $sessionId = '')
113
    {
114
        $this->setId($sessionId);
115
116
        /**
117
         * Store the session data back into the database table when the 
118
         * Shieldon Kernel workflow is reaching the end of the process.
119
         */
120
        Event::AddListener('kernel_end', [$this, 'save'], 10);
121
122
        /**
123
         * Store the session data back into the database table when the 
124
         * user is logged successfully.
125
         */
126
        Event::AddListener('user_login', [$this, 'save'], 10);
127
128
        self::log();
129
    }
130
131
    /**
132
     * Initialize.
133
     *
134
     * @param object $driver        The data driver.
135
     * @param int    $gcExpires     The time of expiring.
136
     * @param int    $gcProbability GC setting,
137
     * @param int    $gcDivisor     GC setting,
138
     * @param bool   $psr7          Reset the cookie the PSR-7 way?
139
     *
140
     * @return void
141
     */
142
    public function init(
143
             $driver, 
144
        int  $gcExpires     = 300, 
145
        int  $gcProbability = 1, 
146
        int  $gcDivisor     = 100, 
147
        bool $psr7          = true
148
    ): void {
149
        $this->driver = $driver;
150
151
        $cookie = get_request()->getCookieParams();
152
153
        $this->gc($gcExpires, $gcProbability, $gcDivisor);
154
 
155
        // New visitor? Create a new session.
156
        if (
157
            php_sapi_name() !== 'cli' && 
158
            empty($cookie['_shieldon'])
159
        ) {
160
            self::resetCookie($psr7);
161
            $this->create();
162
            self::$status = true;
163
164
            return;
165
        }
166
167
        $this->data = $this->driver->get(self::$id, 'session');
168
169
        if (empty($this->data)) {
170
            self::resetCookie($psr7);
171
            $this->create();
172
        }
173
174
        $this->parsedData();
175
176
        self::$status = true;
177
178
        self::log(self::$id);
179
    }
180
181
    /**
182
     * Get the channel name from data driver.
183
     *
184
     * @return string
185
     */
186
    public function getChannel(): string
187
    {
188
        return $this->driver->getChannel();
1 ignored issue
show
Bug introduced by
The method getChannel() 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

188
        return $this->driver->/** @scrutinizer ignore-call */ getChannel();

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...
189
    }
190
191
    /**
192
     * Check the initialization status.
193
     *
194
     * @return bool
195
     */
196
    public function isInitialized(): bool
197
    {
198
        return self::$status;
199
    }
200
201
    /**
202
     * Get session ID.
203
     *
204
     * @return string
205
     */
206
    public function getId(): string
207
    {
208
        return self::$id;
209
    }
210
211
    /**
212
     * Set session ID.
213
     *
214
     * @param string $id Session Id.
215
     *
216
     * @return void
217
     */
218
    public function setId(string $id): void
219
    {
220
        self::$id = $id;
221
222
        // We store this session ID into the container for the use of other functions.
223
        Container::set('session_id', $id, true);
224
225
        self::log($id);
226
    }
227
228
    /**
229
     * Get specific value from session by key.
230
     *
231
     * @param string $key The key of a data field.
232
     *
233
     * @return mixed
234
     */
235
    public function get(string $key)
236
    {
237
        $this->assertInit();
238
239
        return $this->data['parsed_data'][$key] ?? '';
240
    }
241
242
    /**
243
     * To store data in the session.
244
     *
245
     * @param string $key   The key of a data field.
246
     * @param mixed  $value The value of a data field.
247
     *
248
     * @return void
249
     */
250
    public function set(string $key, $value): void
251
    {
252
        $this->assertInit();
253
254
        $this->data['parsed_data'][$key] = $value;
255
    }
256
257
    /**
258
     * To delete data from the session.
259
     *
260
     * @param string $key The key of a data field.
261
     *
262
     * @return void
263
     */
264
    public function remove(string $key): void
265
    {
266
        $this->assertInit();
267
268
        if (isset($this->data['parsed_data'][$key])) {
269
            unset($this->data['parsed_data'][$key]);
270
        }
271
    }
272
273
    /**
274
     * To determine if an item is present in the session.
275
     *
276
     * @param string $key The key of a data field.
277
     *
278
     * @return bool
279
     */
280
    public function has($key): bool
281
    {
282
        $this->assertInit();
283
284
        return isset($this->data['parsed_data'][$key]);
285
    }
286
287
    /**
288
     * Clear all data in the session array.
289
     *
290
     * @return void
291
     */
292
    public function clear(): void
293
    {
294
        $this->assertInit();
295
296
        $this->data = [];
297
    }
298
299
    /**
300
     * Save session data into database.
301
     *
302
     * @return void
303
     */
304
    public function save(): void
305
    {
306
        $data['id'] = self::$id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
307
        $data['ip'] = get_ip();
308
        $data['time'] = time();
309
        $data['microtimesamp'] = (string) get_microtimesamp();
310
311
        $data['data'] = json_encode($this->data['parsed_data']);
312
313
        $this->driver->save(self::$id, $data, 'session');
314
315
        self::log(self::$id . "\n" . $this->data['data']);
316
    }
317
318
    /**
319
     * Reset cookie.
320
     * 
321
     * @param bool $psr7 Reset the cookie the PSR-7 way, otherwise native.
322
     *
323
     * @return void
324
     */
325
    public static function resetCookie(bool $psr7 = true): void
326
    {
327
        $sessionHashId = create_session_id();
328
        $cookieName = '_shieldon';
329
        $expiredTime = time() + 3600;
330
331
        if ($psr7) {
332
            $expires = date('D, d M Y H:i:s', $expiredTime) . ' GMT';
333
            $response = get_response()->withHeader(
334
                'Set-Cookie',
335
                $cookieName . '=' . $sessionHashId . '; Path=/; Expires=' . $expires
336
            );
337
            set_response($response);
338
        } else {
339
            setcookie($cookieName, $sessionHashId, $expiredTime, '/');
340
        }
341
342
        self::$id = $sessionHashId;
343
344
        self::log($sessionHashId);
345
    }
346
347
    /**
348
     * Perform session data garbage collection.
349
     *
350
     * @param int $expires     The time of expiring.
351
     * @param int $probability Numerator.
352
     * @param int $divisor     Denominator.
353
     *
354
     * @return bool
355
     */
356
    protected function gc(int $expires, int $probability, int $divisor): bool
357
    {
358
        $chance = intval($divisor / $probability);
359
        $hit = rand(1, $chance);
360
361
        if ($hit === 1) {
362
            
363
            $sessionData = $this->driver->getAll('session');
364
365
            if (!empty($sessionData)) {
366
                foreach ($sessionData as $v) {
367
                    $lasttime = (int) $v['time'];
368
    
369
                    if (time() - $lasttime > $expires) {
370
                        $this->driver->delete($v['id'], 'session');
371
                    }
372
                }
373
            }
374
            return true;
375
        }
376
        return false;
377
    }
378
379
    /**
380
     * Create session data structure.
381
     *
382
     * @return void
383
     */
384
    protected function create(): void
385
    {
386
        // Initialize new session data.
387
        $data['id'] = self::$id;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
388
        $data['ip'] = get_ip();
389
        $data['time'] = time();
390
        $data['microtimesamp'] = get_microtimesamp();
391
392
        // This field is a JSON string.
393
        $data['data'] = '{}';
394
        $data['parsed_data'] = [];
395
396
        $this->data = $data;
397
        $this->save();
398
399
        self::log(json_encode($this->data));
400
    }
401
402
    /**
403
     * Parse JSON data and store it into parsed_data field.
404
     *
405
     * @return void
406
     */
407
    protected function parsedData()
408
    {
409
        if (empty($this->data['data'])) {
410
            $this->data['data'] = '{}';
411
        }
412
        $this->data['parsed_data'] = json_decode($this->data['data'], true);
413
    }
414
415
    /**
416
     * Make sure init run first.
417
     *
418
     * @return void
419
     */
420
    protected function assertInit(): void
421
    {
422
        if (!$this->isInitialized()) {
423
            throw new RuntimeException(
424
                'The init method is supposed to run first.'
425
            );
426
        }
427
    }
428
429
    /**
430
     * Log.
431
     *
432
     * @return void
433
     */
434
    protected static function log($text = '')
435
    {
436
        if (php_sapi_name() === 'cli') {
437
            SessionLogger::log($text);
438
        }
439
    }
440
}