1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <contact@terryl.in>
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 <contact@terryl.in>
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 Shieldon\Firewall\Log\SessionLogger;
29
use RuntimeException;
30
use function Shieldon\Firewall\create_session_id;
31
use function Shieldon\Firewall\get_ip;
32
use function Shieldon\Firewall\get_microtimestamp;
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 php_sapi_name;
38
use function rand;
39
use function setcookie;
40
use function time;
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 DriverProvider|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 3
    public function __construct(string $sessionId = '')
113
    {
114 3
        $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 3
        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 3
        Event::AddListener('user_login', [$this, 'save'], 10);
127

128 3
        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 3
    public function init(
143
             $driver, 
144
        int  $gcExpires     = 300, 
145
        int  $gcProbability = 1, 
146
        int  $gcDivisor     = 100, 
147
        bool $psr7          = true
148
    ): void {
149

150 3
        $this->driver = $driver;
151 3
        $this->gc($gcExpires, $gcProbability, $gcDivisor);
152

153 3
        $this->data = [];
154

155 3
        $cookie = get_request()->getCookieParams();
156

157 3
        if (!empty($cookie['_shieldon'])) {
158 3
            self::$id = $cookie['_shieldon'];
159 3
            $this->data = $this->driver->get(self::$id, 'session');
160
        }
161

162 3
        if (empty($this->data)) {
163 3
            self::resetCookie($psr7);
164 3
            $this->create();
165
        }
166

167 3
        $this->parsedData();
168

169 3
        self::$status = true;
170 3
        self::log(self::$id);
171
    }
172

173
    /**
174
     * Get the channel name from data driver.
175
     *
176
     * @return string
177
     */
178 3
    public function getChannel(): string
179
    {
180 3
        return $this->driver->getChannel();
181
    }
182

183
    /**
184
     * Check the initialization status.
185
     *
186
     * @return bool
187
     */
188 3
    public function isInitialized(): bool
189
    {
190 3
        return self::$status;
191
    }
192

193
    /**
194
     * Get session ID.
195
     *
196
     * @return string
197
     */
198 3
    public function getId(): string
199
    {
200 3
        return self::$id;
201
    }
202

203
    /**
204
     * Set session ID.
205
     *
206
     * @param string $id Session Id.
207
     *
208
     * @return void
209
     */
210 3
    public function setId(string $id): void
211
    {
212 3
        self::$id = $id;
213

214
        // We store this session ID into the container for the use of other functions.
215 3
        Container::set('session_id', $id, true);
216

217 3
        self::log($id);
218
    }
219

220
    /**
221
     * Get specific value from session by key.
222
     *
223
     * @param string $key The key of a data field.
224
     *
225
     * @return mixed
226
     */
227 3
    public function get(string $key)
228
    {
229 3
        $this->assertInit();
230

231 3
        return $this->data['parsed_data'][$key] ?? '';
232
    }
233

234
    /**
235
     * To store data in the session.
236
     *
237
     * @param string $key   The key of a data field.
238
     * @param mixed  $value The value of a data field.
239
     *
240
     * @return void
241
     */
242 3
    public function set(string $key, $value): void
243
    {
244 3
        $this->assertInit();
245

246 3
        $this->data['parsed_data'][$key] = $value;
247
    }
248

249
    /**
250
     * To delete data from the session.
251
     *
252
     * @param string $key The key of a data field.
253
     *
254
     * @return void
255
     */
256 3
    public function remove(string $key): void
257
    {
258 3
        $this->assertInit();
259

260 3
        if (isset($this->data['parsed_data'][$key])) {
261 3
            unset($this->data['parsed_data'][$key]);
262
        }
263
    }
264

265
    /**
266
     * To determine if an item is present in the session.
267
     *
268
     * @param string $key The key of a data field.
269
     *
270
     * @return bool
271
     */
272 3
    public function has($key): bool
273
    {
274 3
        $this->assertInit();
275

276 3
        return isset($this->data['parsed_data'][$key]);
277
    }
278

279
    /**
280
     * Clear all data in the session array.
281
     *
282
     * @return void
283
     */
284 3
    public function clear(): void
285
    {
286 3
        $this->assertInit();
287

288 3
        $this->data = [];
289
    }
290

291
    /**
292
     * Save session data into database.
293
     *
294
     * @return void
295
     */
296 3
    public function save(): void
297
    {
298 3
        $data = [];
299

300 3
        $data['id'] = self::$id;
301 3
        $data['ip'] = get_ip();
302 3
        $data['time'] = time();
303 3
        $data['microtimestamp'] = get_microtimestamp();
304 3
        $data['data'] = json_encode($this->data['parsed_data']);
305

306 3
        $this->driver->save(self::$id, $data, 'session');
307

308 3
        self::log(self::$id . "\n" . $this->data['data']);
309
    }
310

311
    /**
312
     * Reset cookie.
313
     * 
314
     * @param bool $psr7 Reset the cookie the PSR-7 way, otherwise native.
315
     *
316
     * @return void
317
     */
318 3
    public static function resetCookie(bool $psr7 = true): void
319
    {
320 3
        $sessionHashId = create_session_id();
321 3
        $cookieName = '_shieldon';
322 3
        $expiredTime = time() + 3600;
323

324 3
        if ($psr7) {
325 3
            $expires = date('D, d M Y H:i:s', $expiredTime) . ' GMT';
326 3
            $response = get_response()->withHeader(
327 3
                'Set-Cookie',
328 3
                $cookieName . '=' . $sessionHashId . '; Path=/; Expires=' . $expires
329
            );
330 3
            set_response($response);
331

332
        } else {
333 3
            setcookie($cookieName, $sessionHashId, $expiredTime, '/');
334
        }
335

336 3
        self::$id = $sessionHashId;
337 3
        self::log($sessionHashId);
338
    }
339

340
    /**
341
     * Perform session data garbage collection.
342
     *
343
     * @param int $expires     The time of expiring.
344
     * @param int $probability Numerator.
345
     * @param int $divisor     Denominator.
346
     *
347
     * @return bool
348
     */
349 3
    protected function gc(int $expires, int $probability, int $divisor): bool
350
    {
351 3
        $chance = intval($divisor / $probability);
352 3
        $hit = rand(1, $chance);
353

354 3
        if ($hit === 1) {
355
            
356 2
            $sessionData = $this->driver->getAll('session');
357

358 2
            if (!empty($sessionData)) {
359 2
                foreach ($sessionData as $v) {
360 2
                    $lasttime = (int) $v['time'];
361
    
362 2
                    if (time() - $lasttime > $expires) {
363 2
                        $this->driver->delete($v['id'], 'session');
364
                    }
365
                }
366
            }
367 2
            return true;
368
        }
369 3
        return false;
370
    }
371

372
    /**
373
     * Create session data structure.
374
     *
375
     * @return void
376
     */
377 3
    protected function create(): void
378
    {
379 3
        $data = [];
380

381
        // Initialize new session data.
382 3
        $data['id'] = self::$id;
383 3
        $data['ip'] = get_ip();
384 3
        $data['time'] = time();
385 3
        $data['microtimestamp'] = get_microtimestamp();
386

387
        // This field is a JSON string.
388 3
        $data['data'] = '{}';
389 3
        $data['parsed_data'] = [];
390

391 3
        $this->data = $data;
392 3
        $this->save();
393

394 3
        self::log(json_encode($this->data));
395
    }
396

397
    /**
398
     * Parse JSON data and store it into parsed_data field.
399
     *
400
     * @return void
401
     */
402 3
    protected function parsedData()
403
    {
404 3
        $data = $this->data['data'] ?? '{}';
405

406 3
        $this->data['parsed_data'] = json_decode($data, true);
407
    }
408

409
    /**
410
     * Make sure init run first.
411
     *
412
     * @return void
413
     */
414 3
    protected function assertInit(): void
415
    {
416 3
        if (!$this->isInitialized()) {
417 3
            throw new RuntimeException(
418 3
                'The init method is supposed to run first.'
419
            );
420
        }
421
    }
422

423
    /**
424
     * Log.
425
     *
426
     * @return void
427
     */
428 3
    protected static function log($text = ''): void
429
    {
430 3
        if (php_sapi_name() === 'cli') {
431 3
            SessionLogger::log($text);
432
        }
433
    }
434
}

Read our documentation on viewing source code .

Loading