Passed
Push — 2.x ( a1962c...4a3192 )
by Terry
02:33
created

Session::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
rs 10
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\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...
26
use Shieldon\Firewall\Container;
27
use RuntimeException;
28
use function Shieldon\Firewall\get_request;
29
use function Shieldon\Firewall\get_response;
30
use function Shieldon\Firewall\set_response;
31
use function Shieldon\Firewall\get_microtimesamp;
32
use function Shieldon\Firewall\create_session_id;
33
use function Shieldon\Firewall\get_ip;
34
use function time;
35
use function rand;
36
use function intval;
37
use function setcookie;
38
39
/*
40
 * Session for the use of Shieldon.
41
 */
42
class Session
43
{
44
    /**
45
     * The session data.
46
     *
47
     * @var array
48
     */
49
    protected $data = [];
50
51
    /**
52
     * The session data will be removed after expiring.
53
     * Time unit: second.
54
     *
55
     * @var int
56
     */
57
    protected $expire = 600;
58
59
    /**
60
     * The Shieldon kernel.
61
     *
62
     * @var Kernel|null
63
     */
64
    protected $kernel;
65
66
    /**
67
     * The data driver.
68
     *
69
     * @var DirverProvider|null
70
     */
71
    protected $driver;
72
73
    /**
74
     * Make sure the init() run first.
75
     *
76
     * @var bool
77
     */
78
    protected static $status = false;
79
80
    /**
81
     * A session Id.
82
     *
83
     * @var string
84
     */
85
    protected static $id = '_php_cli_';
86
87
    /**
88
     * Constructor.
89
     * 
90
     * @param string $id Session ID
91
     */
92
    public function __construct(string $sessionId = '')
93
    {
94
        $this->setId($sessionId);
95
96
        /**
97
         * Store the session data back into the database table when the 
98
         * Shieldon Kernel workflow is reaching the end of the process.
99
         */
100
        add_listener('kernel_end', [$this, 'save'], 10);
101
102
        /**
103
         * Store the session data back into the database table when the 
104
         * user is logged successfully.
105
         */
106
        add_listener('user_login', [$this, 'save'], 10);
107
108
        self::log();
109
    }
110
    
111
    /**
112
     * Log.
113
     *
114
     * @return void
115
     */
116
    protected static function log($text = '')
117
    {
118
        if (php_sapi_name() !== 'cli') {
119
            return;
120
        }
121
122
        $dir = BOOTSTRAP_DIR . '/../tmp/shieldon/session_logs';
123
        $file = $dir . '/' . date('Y-m-d') . '.json';
124
    
125
        $originalUmask = umask(0);
126
127
        if (!is_dir($dir)) {
128
            mkdir($dir, 0777, true);
129
        }
130
    
131
        umask($originalUmask);
132
133
        $method = debug_backtrace()[1]['function'];
134
    
135
        $content = date('Y-m-d H:i:s') . ' - [' . $method . '] ' . $text;
136
        file_put_contents($file, $content . PHP_EOL, FILE_APPEND);
137
    }
138
139
    /**
140
     * Get session ID.
141
     *
142
     * @return string
143
     */
144
    public function getId(): string
145
    {
146
        return self::$id;
147
    }
148
149
    /**
150
     * Set session ID.
151
     *
152
     * @param string $id Session Id.
153
     *
154
     * @return void
155
     */
156
    public function setId(string $id): void
157
    {
158
        self::$id = $id;
159
160
        // We store this session ID into the container for the use of other functions.
161
        Container::set('session_id', $id, true);
162
163
        self::log($id);
164
    }
165
166
    /**
167
     * Initialize.
168
     *
169
     * @param object $driver        The data driver.
170
     * @param int    $gcExpires     The time of expiring.
171
     * @param int    $gcProbability GC setting,
172
     * @param int    $gcDivisor     GC setting,
173
     * @param bool   $psr7          Reset the cookie the PSR-7 way?
174
     *
175
     * @return void
176
     */
177
    public function init(
178
             $driver, 
179
        int  $gcExpires     = 300, 
180
        int  $gcProbability = 1, 
181
        int  $gcDivisor     = 100, 
182
        bool $psr7          = false
183
    ): void {
184
        $this->driver = $driver;
185
186
        $cookie = get_request()->getCookieParams();
187
188
        $this->gc($gcExpires, $gcProbability, $gcDivisor);
189
 
190
        // New visitor? Create a new session.
191
        if (php_sapi_name() !== 'cli' && empty($cookie['_shieldon'])) {
192
            self::resetCookie($psr7);
193
            $this->create();
194
            self::$status = true;
195
            return;
196
        }
197
198
        $this->data = $this->driver->get(self::$id, 'session');
199
200
        if (empty($this->data)) {
201
            self::resetCookie($psr7);
202
            $this->create();
203
        }
204
205
        $this->parsedData();
206
207
        self::$status = true;
208
209
        self::log(self::$id);
210
    }
211
212
    /**
213
     * Check the initialization status.
214
     *
215
     * @return bool
216
     */
217
    public function IsInitialized(): bool
218
    {
219
        return $this->status;
220
    }
221
222
    /**
223
     * Get specific value from session by key.
224
     *
225
     * @param string $key The key of a data field.
226
     *
227
     * @return mixed
228
     */
229
    public function get(string $key)
230
    {
231
        $this->assertInit();
232
233
        return $this->data['parsed_data'][$key] ?? '';
234
    }
235
236
    /**
237
     * Parse JSON data and store it into parsed_data field.
238
     *
239
     * @return void
240
     */
241
    protected function parsedData()
242
    {
243
        if (empty($this->data['data'])) {
244
            $this->data['data'] = '{}';
245
        }
246
        $this->data['parsed_data'] = json_decode($this->data['data'], true);
247
    }
248
249
    /**
250
     * To store data in the session.
251
     *
252
     * @param string $key   The key of a data field.
253
     * @param mixed  $value The value of a data field.
254
     *
255
     * @return void
256
     */
257
    public function set(string $key, $value): void
258
    {
259
        $this->assertInit();
260
261
        $this->data['parsed_data'][$key] = $value;
262
    }
263
264
    /**
265
     * To delete data from the session.
266
     *
267
     * @param string $key The key of a data field.
268
     *
269
     * @return void
270
     */
271
    public function remove(string $key): void
272
    {
273
        $this->assertInit();
274
275
        if (isset($this->data['parsed_data'][$key])) {
276
            unset($this->data['parsed_data'][$key]);
277
        }
278
    }
279
280
    /**
281
     * To determine if an item is present in the session.
282
     *
283
     * @param string $key The key of a data field.
284
     *
285
     * @return bool
286
     */
287
    public function has($key): bool
288
    {
289
        $this->assertInit();
290
291
        return isset($this->data['parsed_data'][$key]);
292
    }
293
294
    /**
295
     * Clear all data in the session array.
296
     *
297
     * @return void
298
     */
299
    public function clear(): void
300
    {
301
        $this->assertInit();
302
303
        $this->data = [];
304
    }
305
306
    /**
307
     * Perform session data garbage collection.
308
     *
309
     * @param int $expires     The time of expiring.
310
     * @param int $probability Numerator.
311
     * @param int $divisor     Denominator.
312
     *
313
     * @return bool
314
     */
315
    protected function gc(int $expires, int $probability, int $divisor): bool
316
    {
317
        $chance = intval($divisor / $probability);
318
        $hit = rand(1, $chance);
319
320
        if ($hit === 1) {
321
            
322
            $sessionData = $this->driver->getAll('session');
0 ignored issues
show
Bug introduced by
The method getAll() 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

322
            /** @scrutinizer ignore-call */ 
323
            $sessionData = $this->driver->getAll('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...
323
324
            if (!empty($sessionData)) {
325
                foreach ($sessionData as $v) {
326
                    $lasttime = (int) $v['time'];
327
    
328
                    if (time() - $lasttime > $expires) {
329
                        $this->driver->delete($v['id'], 'session');
330
                    }
331
                }
332
            }
333
            return true;
334
        }
335
        return false;
336
    }
337
338
    /**
339
     * Reset cookie.
340
     * 
341
     * @param bool $psr7 Reset the cookie the PSR-7 way, otherwise native.
342
     *
343
     * @return void
344
     */
345
    public static function resetCookie(bool $psr7 = true): void
346
    {
347
        $sessionHashId = create_session_id();
348
        $cookieName = '_shieldon';
349
        $expiredTime = time() + 3600;
350
351
        if ($psr7) {
352
            $expires = date('D, d M Y H:i:s', $expiredTime) . ' GMT';
353
            $response = get_response()->withHeader(
354
                'Set-Cookie',
355
                $cookieName . '=' . $sessionHashId . '; Path=/; Expires=' . $expires
356
            );
357
            set_response($response);
358
        } else {
359
            setcookie($cookieName, $sessionHashId, $expiredTime, '/');
360
        }
361
362
        self::$id = $sessionHashId;
363
364
        self::log($sessionHashId);
365
    }
366
367
    /**
368
     * Create session data structure.
369
     *
370
     * @return void
371
     */
372
    protected function create(): void
373
    {
374
        // Initialize new session data.
375
        $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...
376
        $data['ip'] = get_ip();
377
        $data['time'] = time();
378
        $data['microtimesamp'] = get_microtimesamp();
379
380
        // This field is a JSON string.
381
        $data['data'] = '{}';
382
        $data['parsed_data'] = [];
383
384
        $this->data = $data;
385
        $this->save();
386
387
        self::log(json_encode($this->data));
388
    }
389
390
    /**
391
     * Save session data into database.
392
     *
393
     * @return void
394
     */
395
    public function save(): void
396
    {
397
        $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...
398
        $data['ip'] = get_ip();
399
        $data['time'] = (string) time();
400
        $data['microtimesamp'] = (string) get_microtimesamp();
401
402
        $data['data'] = json_encode($this->data['parsed_data']);
403
404
        $this->driver->save(self::$id, $data, 'session');
405
406
        self::log(self::$id . "\n" . $this->data['data']);
407
    }
408
409
    /**
410
     * Make sure init run first.
411
     *
412
     * @return void
413
     */
414
    protected function assertInit(): void
415
    {
416
        if (!self::$status) {
417
            throw new RuntimeException(
418
                'The init method is supposed to run first.'
419
            );
420
        }
421
    }
422
}
423