Test Failed
Push — 2.x ( 445986...358314 )
by Terry
10:40
created

Session::log()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\Event\Event;
26
use Shieldon\Firewall\Container;
27
use Shieldon\Firewall\Driver\DriverProvider;
28
use RuntimeException;
29
use function Shieldon\Firewall\create_session_id;
30
use function Shieldon\Firewall\get_ip;
31
use function Shieldon\Firewall\get_microtimestamp;
32
use function Shieldon\Firewall\get_request;
33
use function Shieldon\Firewall\get_response;
34
use function Shieldon\Firewall\set_response;
35
use function intval;
36
use function rand;
37
use function setcookie;
38
use function time;
39
40
/*
41
 * Session for the use of Shieldon.
42
 */
43
class Session
44
{
45
    /**
46
     *   Public methods       | Desctiotion
47
     *  ----------------------|---------------------------------------------
48
     *   init                 | Initialize the session.
49
     *   getId                | Get session ID.
50
     *   setId                | Set session ID
51
     *   isInitialized        | Check if a session has been initialized or not.
52
     *   get                  | Get specific value from session by key.
53
     *   set                  | To store data in the session.
54
     *   remove               | To delete data from the session.
55
     *   has                  | To determine if an item is present in the session.
56
     *   clear                | Clear all data in the session array.
57
     *   save                 | Save session data into database.
58
     *   ::resetCookie        | Create a new session cookie for current user.
59
     *  ----------------------|---------------------------------------------
60
     */
61
62
    /**
63
     * The session data.
64
     *
65
     * @var array
66
     */
67
    protected $data = [];
68
69
    /**
70
     * The session data will be removed after expiring.
71
     * Time unit: second.
72
     *
73
     * @var int
74
     */
75
    protected $expire = 600;
76
77
    /**
78
     * The Shieldon kernel.
79
     *
80
     * @var Kernel|null
81
     */
82
    protected $kernel;
83
84
    /**
85
     * The data driver.
86
     *
87
     * @var DriverProvider|null
88
     */
89
    protected $driver;
90
91
    /**
92
     * Make sure the init() run first.
93
     *
94
     * @var bool
95
     */
96
    protected static $status = false;
97
98
    /**
99
     * A session Id.
100
     *
101
     * @var string
102
     */
103
    protected static $id = '_php_cli_';
104
105
    /**
106
     * Constructor.
107
     *
108
     * @param string $id Session ID
109
     */
110
    public function __construct(string $sessionId = '')
111
    {
112 172
        $this->setId($sessionId);
113
114 172
        /**
115
         * Store the session data back into the database table when the
116
         * Shieldon Kernel workflow is reaching the end of the process.
117
         */
118
        Event::AddListener('kernel_end', [$this, 'save'], 10);
119
120 172
        /**
121
         * Store the session data back into the database table when the
122
         * user is logged successfully.
123
         */
124
        Event::AddListener('user_login', [$this, 'save'], 10);
125
    }
126 172
127
    /**
128 172
     * Initialize.
129
     *
130
     * @param DriverProvider $driver        The data driver.
131
     * @param int            $gcExpires     The time of expiring.
132
     * @param int            $gcProbability GC setting,
133
     * @param int            $gcDivisor     GC setting,
134
     * @param bool           $psr7          Reset the cookie the PSR-7 way?
135
     *
136
     * @return void
137
     */
138
    public function init(
139
        DriverProvider $driver,
140
        int  $gcExpires = 300,
141
        int  $gcProbability = 1,
142 170
        int  $gcDivisor = 100,
143
        bool $psr7 = true
144
    ): void
145
    {
146
        $this->driver = $driver;
147
        $this->gc($gcExpires, $gcProbability, $gcDivisor);
148
149
        $this->data = [];
150 170
151 170
        $cookie = get_request()->getCookieParams();
152
153 170
        if (!empty($cookie['_shieldon'])) {
154
            self::$id = $cookie['_shieldon'];
155 170
            $this->data = $this->driver->get(self::$id, 'session');
156
        }
157 170
158 20
        if (empty($this->data)) {
159 20
            self::resetCookie($psr7);
160
            $this->create();
161
        }
162 170
163 150
        $this->parsedData();
164 150
165
        self::$status = true;
166
    }
167 170
168
    /**
169 170
     * Get the channel name from data driver.
170 170
     *
171
     * @return string
172
     */
173
    public function getChannel(): string
174
    {
175
        return $this->driver->getChannel();
176
    }
177
178 1
    /**
179
     * Check the initialization status.
180 1
     *
181
     * @return bool
182
     */
183
    public function isInitialized(): bool
184
    {
185
        return self::$status;
186
    }
187
188 101
    /**
189
     * Get session ID.
190 101
     *
191
     * @return string
192
     */
193
    public function getId(): string
194
    {
195
        return self::$id;
196
    }
197
198 63
    /**
199
     * Set session ID.
200 63
     *
201
     * @param string $id Session Id.
202
     *
203
     * @return void
204
     */
205
    public function setId(string $id): void
206
    {
207
        self::$id = $id;
208
209
        // We store this session ID into the container for the use of other functions.
210 172
        Container::set('session_id', $id, true);
211
    }
212 172
213
    /**
214
     * Get specific value from session by key.
215 172
     *
216
     * @param string $key The key of a data field.
217 172
     *
218
     * @return mixed
219
     */
220
    public function get(string $key)
221
    {
222
        $this->assertInit();
223
224
        return $this->data['parsed_data'][$key] ?? '';
225
    }
226
227 92
    /**
228
     * To store data in the session.
229 92
     *
230
     * @param string $key   The key of a data field.
231 92
     * @param mixed  $value The value of a data field.
232
     *
233
     * @return void
234
     */
235
    public function set(string $key, $value): void
236
    {
237
        $this->assertInit();
238
239
        $this->data['parsed_data'][$key] = $value;
240
    }
241
242 76
    /**
243
     * To delete data from the session.
244 76
     *
245
     * @param string $key The key of a data field.
246 75
     *
247
     * @return void
248
     */
249
    public function remove(string $key): void
250
    {
251
        $this->assertInit();
252
253
        if (isset($this->data['parsed_data'][$key])) {
254
            unset($this->data['parsed_data'][$key]);
255
        }
256 19
    }
257
258 19
    /**
259
     * To determine if an item is present in the session.
260 19
     *
261 18
     * @param string $key The key of a data field.
262
     *
263
     * @return bool
264
     */
265
    public function has($key): bool
266
    {
267
        $this->assertInit();
268
269
        return isset($this->data['parsed_data'][$key]);
270
    }
271
272 1
    /**
273
     * Clear all data in the session array.
274 1
     *
275
     * @return void
276 1
     */
277
    public function clear(): void
278
    {
279
        $this->assertInit();
280
281
        $this->data = [];
282
    }
283
284 1
    /**
285
     * Save session data into database.
286 1
     *
287
     * @return void
288 1
     */
289
    public function save(): void
290
    {
291
        $data = [];
292
293
        $data['id'] = self::$id;
294
        $data['ip'] = get_ip();
295
        $data['time'] = time();
296 151
        $data['microtimestamp'] = get_microtimestamp();
297
        $data['data'] = json_encode($this->data['parsed_data']);
298 151
299
        $this->driver->save(self::$id, $data, 'session');
300 151
    }
301 151
302 151
    /**
303 151
     * Reset cookie.
304 151
     *
305
     * @param bool $psr7 Reset the cookie the PSR-7 way, otherwise native.
306 151
     *
307
     * @return void
308 151
     */
309
    public static function resetCookie(bool $psr7 = true): void
310
    {
311
        $sessionHashId = create_session_id();
312
        $cookieName = '_shieldon';
313
        $expiredTime = time() + 3600;
314
315
        if ($psr7) {
316
            $expires = date('D, d M Y H:i:s', $expiredTime) . ' GMT';
317
            $response = get_response()->withHeader(
318 151
                'Set-Cookie',
319
                $cookieName . '=' . $sessionHashId . '; Path=/; Expires=' . $expires
320 151
            );
321 151
            set_response($response);
322 151
        } else {
323
            setcookie($cookieName, $sessionHashId, $expiredTime, '/');
324 151
        }
325 1
        self::$id = $sessionHashId;
326 1
    }
327 1
328 1
    /**
329 1
     * Perform session data garbage collection.
330 1
     *
331
     * @param int $expires     The time of expiring.
332
     * @param int $probability Numerator.
333 150
     * @param int $divisor     Denominator.
334
     *
335
     * @return bool
336 151
     */
337 151
    protected function gc(int $expires, int $probability, int $divisor): bool
338
    {
339
        $chance = intval($divisor / $probability);
340
        $hit = rand(1, $chance);
341
342
        if ($hit === 1) {
343
            $sessionData = $this->driver->getAll('session');
344
345
            if (!empty($sessionData)) {
346
                foreach ($sessionData as $v) {
347
                    $lasttime = (int) $v['time'];
348
    
349 170
                    if (time() - $lasttime > $expires) {
350
                        $this->driver->delete($v['id'], 'session');
351 170
                    }
352 170
                }
353
            }
354 170
            return true;
355
        }
356 5
        return false;
357
    }
358 5
359 5
    /**
360 5
     * Create session data structure.
361
     *
362 5
     * @return void
363 5
     */
364
    protected function create(): void
365
    {
366
        $data = [];
367 5
368
        // Initialize new session data.
369 169
        $data['id'] = self::$id;
370
        $data['ip'] = get_ip();
371
        $data['time'] = time();
372
        $data['microtimestamp'] = get_microtimestamp();
373
374
        // This field is a JSON string.
375
        $data['data'] = '{}';
376
        $data['parsed_data'] = [];
377 150
378
        $this->data = $data;
379 150
        $this->save();
380
    }
381
382 150
    /**
383 150
     * Parse JSON data and store it into parsed_data field.
384 150
     *
385 150
     * @return void
386
     */
387
    protected function parsedData()
388 150
    {
389 150
        $data = $this->data['data'] ?? '{}';
390
391 150
        $this->data['parsed_data'] = json_decode($data, true);
392 150
    }
393
394 150
    /**
395
     * Make sure init run first.
396
     *
397
     * @return void
398
     */
399
    protected function assertInit(): void
400
    {
401
        if (!$this->isInitialized()) {
402 170
            throw new RuntimeException(
403
                'The init method is supposed to run first.'
404 170
            );
405
        }
406 170
    }
407
}
408