Passed
Push — 2.x ( 2dab0a...b10d5f )
by Terry
02:01
created

Session::resetCookie()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 13
nc 2
nop 1
dl 0
loc 20
rs 9.8333
c 1
b 0
f 0
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
     * Check the initialization status.
183
     *
184
     * @return bool
185
     */
186
    public function isInitialized(): bool
187
    {
188
        return self::$status;
189
    }
190
191
    /**
192
     * Get session ID.
193
     *
194
     * @return string
195
     */
196
    public function getId(): string
197
    {
198
        return self::$id;
199
    }
200
201
    /**
202
     * Set session ID.
203
     *
204
     * @param string $id Session Id.
205
     *
206
     * @return void
207
     */
208
    public function setId(string $id): void
209
    {
210
        self::$id = $id;
211
212
        // We store this session ID into the container for the use of other functions.
213
        Container::set('session_id', $id, true);
214
215
        self::log($id);
216
    }
217
218
    /**
219
     * Get specific value from session by key.
220
     *
221
     * @param string $key The key of a data field.
222
     *
223
     * @return mixed
224
     */
225
    public function get(string $key)
226
    {
227
        $this->assertInit();
228
229
        return $this->data['parsed_data'][$key] ?? '';
230
    }
231
232
    /**
233
     * To store data in the session.
234
     *
235
     * @param string $key   The key of a data field.
236
     * @param mixed  $value The value of a data field.
237
     *
238
     * @return void
239
     */
240
    public function set(string $key, $value): void
241
    {
242
        $this->assertInit();
243
244
        $this->data['parsed_data'][$key] = $value;
245
    }
246
247
    /**
248
     * To delete data from the session.
249
     *
250
     * @param string $key The key of a data field.
251
     *
252
     * @return void
253
     */
254
    public function remove(string $key): void
255
    {
256
        $this->assertInit();
257
258
        if (isset($this->data['parsed_data'][$key])) {
259
            unset($this->data['parsed_data'][$key]);
260
        }
261
    }
262
263
    /**
264
     * To determine if an item is present in the session.
265
     *
266
     * @param string $key The key of a data field.
267
     *
268
     * @return bool
269
     */
270
    public function has($key): bool
271
    {
272
        $this->assertInit();
273
274
        return isset($this->data['parsed_data'][$key]);
275
    }
276
277
    /**
278
     * Clear all data in the session array.
279
     *
280
     * @return void
281
     */
282
    public function clear(): void
283
    {
284
        $this->assertInit();
285
286
        $this->data = [];
287
    }
288
289
    /**
290
     * Save session data into database.
291
     *
292
     * @return void
293
     */
294
    public function save(): void
295
    {
296
        $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...
297
        $data['ip'] = get_ip();
298
        $data['time'] = time();
299
        $data['microtimesamp'] = (string) get_microtimesamp();
300
301
        $data['data'] = json_encode($this->data['parsed_data']);
302
303
        $this->driver->save(self::$id, $data, 'session');
0 ignored issues
show
Bug introduced by
The method save() 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

303
        $this->driver->/** @scrutinizer ignore-call */ 
304
                       save(self::$id, $data, 'session');

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...
304
305
        self::log(self::$id . "\n" . $this->data['data']);
306
    }
307
308
    /**
309
     * Reset cookie.
310
     * 
311
     * @param bool $psr7 Reset the cookie the PSR-7 way, otherwise native.
312
     *
313
     * @return void
314
     */
315
    public static function resetCookie(bool $psr7 = true): void
316
    {
317
        $sessionHashId = create_session_id();
318
        $cookieName = '_shieldon';
319
        $expiredTime = time() + 3600;
320
321
        if ($psr7) {
322
            $expires = date('D, d M Y H:i:s', $expiredTime) . ' GMT';
323
            $response = get_response()->withHeader(
324
                'Set-Cookie',
325
                $cookieName . '=' . $sessionHashId . '; Path=/; Expires=' . $expires
326
            );
327
            set_response($response);
328
        } else {
329
            setcookie($cookieName, $sessionHashId, $expiredTime, '/');
330
        }
331
332
        self::$id = $sessionHashId;
333
334
        self::log($sessionHashId);
335
    }
336
337
    /**
338
     * Perform session data garbage collection.
339
     *
340
     * @param int $expires     The time of expiring.
341
     * @param int $probability Numerator.
342
     * @param int $divisor     Denominator.
343
     *
344
     * @return bool
345
     */
346
    protected function gc(int $expires, int $probability, int $divisor): bool
347
    {
348
        $chance = intval($divisor / $probability);
349
        $hit = rand(1, $chance);
350
351
        if ($hit === 1) {
352
            
353
            $sessionData = $this->driver->getAll('session');
354
355
            if (!empty($sessionData)) {
356
                foreach ($sessionData as $v) {
357
                    $lasttime = (int) $v['time'];
358
    
359
                    if (time() - $lasttime > $expires) {
360
                        $this->driver->delete($v['id'], 'session');
361
                    }
362
                }
363
            }
364
            return true;
365
        }
366
        return false;
367
    }
368
369
    /**
370
     * Create session data structure.
371
     *
372
     * @return void
373
     */
374
    protected function create(): void
375
    {
376
        // Initialize new session data.
377
        $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...
378
        $data['ip'] = get_ip();
379
        $data['time'] = time();
380
        $data['microtimesamp'] = get_microtimesamp();
381
382
        // This field is a JSON string.
383
        $data['data'] = '{}';
384
        $data['parsed_data'] = [];
385
386
        $this->data = $data;
387
        $this->save();
388
389
        self::log(json_encode($this->data));
390
    }
391
392
    /**
393
     * Parse JSON data and store it into parsed_data field.
394
     *
395
     * @return void
396
     */
397
    protected function parsedData()
398
    {
399
        if (empty($this->data['data'])) {
400
            $this->data['data'] = '{}';
401
        }
402
        $this->data['parsed_data'] = json_decode($this->data['data'], true);
403
    }
404
405
    /**
406
     * Make sure init run first.
407
     *
408
     * @return void
409
     */
410
    protected function assertInit(): void
411
    {
412
        if (!$this->isInitialized()) {
413
            throw new RuntimeException(
414
                'The init method is supposed to run first.'
415
            );
416
        }
417
    }
418
419
    /**
420
     * Log.
421
     *
422
     * @return void
423
     */
424
    protected static function log($text = '')
425
    {
426
        if (php_sapi_name() === 'cli') {
427
            SessionLogger::log($text);
428
        }
429
    }
430
}
431