Passed
Push — 2.x ( 4a4959...759244 )
by Terry
02:10
created

SqlDriverProvider::doFetch()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 2
dl 0
loc 20
rs 9.8666
c 0
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
11
declare(strict_types=1);
12
13
namespace Shieldon\Firewall\Driver;
14
15
use Shieldon\Firewall\Driver\DriverProvider;
16
use Exception;
17
use PDO;
18
19
use function is_array;
20
use function is_bool;
21
22
/**
23
 * SQL Driver provider.
24
 */
25
class SqlDriverProvider extends DriverProvider
26
{
27
    /**
28
     * Data engine will be used on.
29
     *
30
     * @var string
31
     */
32
    protected $tableDbEngine = 'innodb';
33
34
    /**
35
     * PDO instance.
36
     * 
37
     * @var object
38
     */
39
    protected $db;
40
41
    /**
42
     * Constructor.
43
     *
44
     * @param PDO $pdo
45
     * @param bool $debug
46
     */
47
    public function __construct(PDO $pdo, bool $debug = false)
48
    {
49
        $this->db = $pdo;
50
51
        if ($debug) {
52
            $this->db->setAttribute($this->db::ATTR_ERRMODE, $this->db::ERRMODE_EXCEPTION);
53
        }
54
    }
55
56
    /**
57
     * Initialize data tables.
58
     *
59
     * @param bool $dbCheck This is for creating data tables automatically
60
     *                      Turn it off, if you don't want to check data tables every pageview.
61
     *
62
     * @return void
63
     */
64
    protected function doInitialize(bool $dbCheck = true): void
65
    {
66
        if (!$this->isInitialized) {
67
            if (!empty($this->channel)) {
68
                $this->setChannel($this->channel);
69
            }
70
71
            if ($dbCheck && !$this->checkTableExists()) {
72
                $this->installSql();
73
            }
74
        }
75
76
        $this->isInitialized = true;
77
    }
78
79
    /**
80
     * {@inheritDoc}
81
     */
82
    protected function doFetch(string $ip, string $type = 'filter'): array
83
    {
84
        $results = [];
85
86
        switch ($type) {
87
88
            case 'rule':
89
                $results = $this->doFetchFromRuleTable($ip);
90
                break;
91
92
            case 'filter':
93
                $results = $this->doFetchFromFilterTable($ip);
94
                break;
95
96
            case 'session':
97
                $results = $this->doFetchFromSessionTable($ip);
98
                break;
99
        }
100
101
        return $results;
102
    }
103
104
   /**
105
     * {@inheritDoc}
106
     */
107
    protected function doFetchAll(string $type = 'filter'): array
108
    {
109
        $results = [];
110
111
        switch ($type) {
112
113
            case 'rule':
114
                $sql = 'SELECT * FROM ' . $this->tableRuleList;
115
116
                $query = $this->db->prepare($sql);
117
                $query->execute();
118
                $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
119
120
                if (is_array($resultData)) {
121
                    $results = $resultData;
122
                }
123
                break;
124
125
            case 'filter':
126
                $sql = 'SELECT log_ip, log_data FROM ' . $this->tableFilterLogs;
127
128
                $query = $this->db->prepare($sql);
129
                $query->execute();
130
                $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
131
132
                if (is_array($resultData)) {
133
                    $results = $resultData;
134
                }
135
                break;
136
137
            case 'session':
138
                $sql = 'SELECT * FROM ' . $this->tableSessions . ' ORDER BY microtimesamp ASC';
139
140
                $query = $this->db->prepare($sql);
141
                $query->execute();
142
                $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
143
144
                if (is_array($resultData)) {
145
                    $results = $resultData;
146
                }
147
                break;
148
        }
149
150
        return $results;
151
    }
152
153
    /**
154
     * {@inheritDoc}
155
     */
156
    protected function checkExist(string $ip, string $type = 'filter'): bool
157
    {
158
        switch ($type) {
159
160
            case 'rule':
161
                $tableName = $this->tableRuleList;
162
                $field = 'log_ip';
163
                break;
164
165
            case 'filter':
166
                $tableName = $this->tableFilterLogs;
167
                $field = 'log_ip';
168
                break;
169
170
            case 'session':
171
                $tableName = $this->tableSessions;
172
                $field = 'id';
173
                break;
174
        }
175
176
        $sql = 'SELECT ' . $field . ' FROM ' . $tableName . '
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tableName does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $field does not seem to be defined for all execution paths leading up to this point.
Loading history...
177
            WHERE ' . $field . ' = :' . $field . '
178
            LIMIT 1';
179
180
        $query = $this->db->prepare($sql);
181
        $query->bindValue(':' . $field, $ip);
182
183
        $query->execute();
184
        $result = $query->fetch();
185
186
        if (!empty($result[$field])) {
187
            return true; 
188
        }
189
190
        return false;
191
    }
192
193
    /**
194
     * {@inheritDoc}
195
     */
196
    protected function doSave(string $ip, array $data, string $type = 'filter', $expire = 0): bool
197
    {
198
        switch ($type) {
199
200
            case 'rule':
201
                $tableName = $this->tableRuleList;
202
                $logWhere['log_ip'] = $ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$logWhere was never initialized. Although not strictly required by PHP, it is generally a good practice to add $logWhere = array(); before regardless.
Loading history...
203
                $logData = $data;
204
                $logData['log_ip'] = $ip;
205
                break;
206
207
            case 'filter':
208
                $tableName = $this->tableFilterLogs;
209
                $logWhere['log_ip'] = $ip;
210
                $logData['log_ip'] = $ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$logData was never initialized. Although not strictly required by PHP, it is generally a good practice to add $logData = array(); before regardless.
Loading history...
211
                $logData['log_data'] = json_encode($data);
212
                break;
213
214
            case 'session':
215
                $tableName = $this->tableSessions;
216
                
217
                $logWhere['id'] = $data['id'];
218
                $logData = $data;
219
                break;
220
        }
221
222
        if ($this->checkExist($ip, $type)) {
223
            return $this->update($tableName, $logData, $logWhere);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tableName does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $logWhere does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $logData does not seem to be defined for all execution paths leading up to this point.
Loading history...
224
        } else {
225
            return (bool) $this->insert($tableName, $logData);
226
        }
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232
    protected function doDelete(string $ip, string $type = 'filter'): bool
233
    {
234
        switch ($type) {
235
            case 'rule'      : return $this->remove($this->tableRuleList,   ['log_ip' => $ip]);
236
            case 'filter': return $this->remove($this->tableFilterLogs, ['log_ip' => $ip]);
237
            case 'session'   : return $this->remove($this->tableSessions,   ['id'     => $ip]);
238
        }
239
240
        return false;
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246
    protected function doRebuild(): bool
247
    {
248
        return $this->rebuildSql();
249
    }
250
251
    /**
252
     * Update database table.
253
     *
254
     * @param string $table
255
     * @param array  $data
256
     * @param array  $where
257
     *
258
     * @return bool
259
     */
260
    private function update(string $table, array $data, array $where)
261
    {
262
        $placeholder = [];
263
        foreach($data as $k => $v) {
264
            $placeholder[] = "$k = :$k";
265
        }
266
267
        $dataPlaceholder = implode(', ', $placeholder);
268
269
        $placeholder = [];
270
        foreach($where as $k => $v) {
271
            $placeholder[] = "$k = :$k";
272
        }
273
274
        $wherePlaceholder = implode(' AND ', $placeholder);
275
276
        try {
277
            $sql = 'UPDATE ' . $table . ' SET ' . $dataPlaceholder . ' WHERE ' . $wherePlaceholder;
278
            $query = $this->db->prepare($sql);
279
280
            $bind = array_merge($data, $where);
281
    
282
            foreach($bind as $k => $v) {
283
284
                // @codeCoverageIgnoreStart
285
286
                if (is_numeric($v)) {
287
                    $pdoParam = $this->db::PARAM_INT;
288
289
                    // Solve problem with bigint.
290
                    if ($v >= 2147483647) {
291
                        $pdoParam = $this->db::PARAM_STR;
292
                    } 
293
                } elseif (is_bool($v)) {
294
                    $pdoParam = $this->db::PARAM_BOOL;
295
                } elseif (is_null($v)) {
296
                    $pdoParam = $this->db::PARAM_NULL;
297
                } else {
298
                    $pdoParam = $this->db::PARAM_STR;
299
                }
300
301
                // @codeCoverageIgnoreEnd
302
303
                $query->bindValue(":$k", $bind[$k], $pdoParam);
304
            }
305
306
            return $query->execute();
307
308
        // @codeCoverageIgnoreStart
309
        
310
        } catch(Exception $e) {
311
            throw $e->getMessage();
312
            return false;
0 ignored issues
show
Unused Code introduced by
return false is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
313
        }
314
315
        // @codeCoverageIgnoreEnd 
316
    }
317
318
    /**
319
     * Insert database table.
320
     *
321
     * @param string $table
322
     * @param array  $data
323
     *
324
     * @return bool
325
     */
326
    private function insert(string $table, array $data)
327
    {
328
        $placeholderField = [];
329
        $placeholderValue = [];
330
        foreach($data as $k => $v) {
331
            $placeholderField[] = "`$k`";
332
            $placeholderValue[] = ":$k";
333
        }
334
335
        $dataPlaceholderField = implode(', ', $placeholderField);
336
        $dataPlaceholderValue = implode(', ', $placeholderValue);
337
338
        try {
339
            $sql = 'INSERT INTO ' . $table . ' (' . $dataPlaceholderField . ') VALUES (' . $dataPlaceholderValue . ')';
340
            $query = $this->db->prepare($sql);
341
342
            foreach($data as $k => $v) {
343
344
                // @codeCoverageIgnoreStart
345
346
                if (is_numeric($v)) {
347
                    $pdoParam = $this->db::PARAM_INT;
348
349
                    // Solve problem with bigint.
350
                    if ($v >= 2147483647) {
351
                        $pdoParam = $this->db::PARAM_STR;
352
                    }
353
                } elseif (is_bool($v)) {
354
                    $pdoParam = $this->db::PARAM_BOOL;
355
                } elseif (is_null($v)) {
356
                    $pdoParam = $this->db::PARAM_NULL;
357
                } else {
358
                    $pdoParam = $this->db::PARAM_STR;
359
                }
360
361
                // @codeCoverageIgnoreEnd
362
363
                $query->bindValue(":$k", $data[$k], $pdoParam);
364
            }
365
366
            return $query->execute();
367
368
        // @codeCoverageIgnoreStart
369
370
        } catch(Exception $e) {
371
            return false;
372
        }
373
374
        // @codeCoverageIgnoreEnd
375
    }
376
377
    /**
378
     * Remove a row from a table.
379
     *
380
     * @param string $table
381
     * @param array $where
382
     *
383
     * @return bool
384
     */
385
    private function remove(string $table, array $where): bool
386
    {
387
388
        $placeholder = [];
389
        foreach($where as $k => $v) {
390
            $placeholder[] = "`$k` = :$k";
391
        }
392
393
        $dataPlaceholder = implode(' AND ', $placeholder);
394
395
        try {
396
397
            $sql = 'DELETE FROM ' . $table . ' WHERE ' . $dataPlaceholder;
398
            $query = $this->db->prepare($sql);
399
400
            foreach($where as $k => $v) {
401
402
                // @codeCoverageIgnoreStart
403
404
                if (is_numeric($v)) {
405
                    $pdoParam = $this->db::PARAM_INT;
406
                } elseif (is_bool($v)) {
407
                    $pdoParam = $this->db::PARAM_BOOL;
408
                } elseif (is_null($v)) {
409
                    $pdoParam = $this->db::PARAM_NULL;
410
                } else {
411
                    $pdoParam = $this->db::PARAM_STR;
412
                }
413
414
                // @codeCoverageIgnoreEnd
415
416
                $query->bindValue(":$k", $v, $pdoParam);
417
            }
418
419
            return $query->execute();
420
421
        // @codeCoverageIgnoreStart
422
423
        } catch(Exception $e) {
424
            return false;
425
        }
426
427
        // @codeCoverageIgnoreEnd
428
    }
429
430
    /**
431
     * Create SQL tables that Shieldon needs.
432
     *
433
     * @return bool
434
     */
435
    protected function installSql(): bool
436
    {
437
        try {
438
439
            $sql = "
440
                CREATE TABLE IF NOT EXISTS `{$this->tableFilterLogs}` (
441
                    `log_ip` varchar(46) NOT NULL,
442
                    `log_data` blob,
443
                    PRIMARY KEY (`log_ip`)
444
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
445
            ";
446
447
            $this->db->query($sql);
448
449
            $sql = "
450
                CREATE TABLE IF NOT EXISTS `{$this->tableRuleList}` (
451
                    `log_ip` varchar(46) NOT NULL,
452
                    `ip_resolve` varchar(255) NOT NULL,
453
                    `type` tinyint(3) UNSIGNED NOT NULL,
454
                    `reason` tinyint(3) UNSIGNED NOT NULL,
455
                    `time` int(10) UNSIGNED NOT NULL,
456
                    `attempts` int(10) UNSIGNED DEFAULT 0,
457
                    PRIMARY KEY (`log_ip`)
458
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
459
            ";
460
461
            $this->db->query($sql);
462
463
            $sql = "
464
                CREATE TABLE `{$this->tableSessions}` (
465
                    `id` varchar(40) NOT NULL,
466
                    `ip` varchar(46) NOT NULL,
467
                    `time` int(10) UNSIGNED NOT NULL,
468
                    `microtimesamp` bigint(20) UNSIGNED NOT NULL,
469
                    `data` blob,
470
                    PRIMARY KEY (`id`)
471
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
472
            ";
473
474
            $this->db->query($sql);
475
476
            return true;
477
478
        // @codeCoverageIgnoreStart
479
480
        } catch (Exception $e) {
481
            return false;
482
        }
483
484
        // @codeCoverageIgnoreEnd
485
    }
486
487
    /**
488
     * Clean all records in IP log and IP rule tables, and then rebuild new tables.
489
     *
490
     * @return bool
491
     */
492
    protected function rebuildSql(): bool
493
    {
494
        try {
495
496
            $sql = "DROP TABLE IF EXISTS `{$this->tableFilterLogs}`";
497
            $this->db->query($sql);
498
499
            $sql = "DROP TABLE IF EXISTS `{$this->tableRuleList}`";
500
            $this->db->query($sql);
501
502
            $sql = "DROP TABLE IF EXISTS `{$this->tableSessions}`";
503
            $this->db->query($sql);
504
505
            $this->installSql();
506
507
            return true;
508
509
        // @codeCoverageIgnoreStart
510
511
        } catch (Exception $e) {
512
            return false;
513
        }
514
515
        // @codeCoverageIgnoreEnd
516
    }
517
518
    /**
519
     * Check required tables exist or not.
520
     *
521
     * @return bool
522
     */
523
    protected function checkTableExists(): bool
524
    {
525
        $checkLogTable = $this->db->query("SHOW TABLES LIKE '{$this->tableFilterLogs}'");
526
527
        if ($checkLogTable) {
528
            if ($checkLogTable->rowCount() > 0) {
529
                return true;
530
            }
531
        }
532
533
        return false;
534
    }
535
536
    /**
537
     * Fetch data from filter table.
538
     *
539
     * @param string $ip An IP address.
540
     *
541
     * @return array
542
     */
543
    private function doFetchFromFilterTable(string $ip): array
544
    {
545
        $results = [];
546
547
        $sql = 'SELECT log_ip, log_data FROM ' . $this->tableFilterLogs . '
548
            WHERE log_ip = :log_ip
549
            LIMIT 1';
550
551
        $query = $this->db->prepare($sql);
552
        $query->bindValue(':log_ip', $ip, $this->db::PARAM_STR);
553
        $query->execute();
554
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
555
556
        // No data found.
557
        if (is_bool($resultData) && !$resultData) {
558
            $resultData = [];
559
        }
560
561
        if (!empty($resultData['log_data'])) {
562
            $results = json_decode($resultData['log_data'], true); 
563
        }
564
565
        return $results;
566
    }
567
568
    /**
569
     * Fetch data from rule table.
570
     *
571
     * @param string $ip An IP address.
572
     *
573
     * @return array
574
     */
575
    private function doFetchFromRuleTable(string $ip): array
576
    {
577
        $results = [];
578
579
        $sql = 'SELECT * FROM ' . $this->tableRuleList . '
580
            WHERE log_ip = :log_ip
581
            LIMIT 1';
582
583
        $query = $this->db->prepare($sql);
584
        $query->bindValue(':log_ip', $ip, $this->db::PARAM_STR);
585
        $query->execute();
586
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
587
588
        // No data found.
589
        if (is_bool($resultData) && !$resultData) {
590
            $resultData = [];
591
        }
592
593
        if (is_array($resultData)) {
594
            $results = $resultData;
595
        }
596
597
        return $results;
598
    }
599
600
    /**
601
     * Fetch data from session table.
602
     *
603
     * @param string $ip An IP address.
604
     *
605
     * @return array
606
     */
607
    private function doFetchFromSessionTable(string $ip): array
608
    {
609
        $results = [];
610
611
        $sql = 'SELECT * FROM ' . $this->tableSessions . '
612
            WHERE id = :id
613
            LIMIT 1';
614
615
        $query = $this->db->prepare($sql);
616
        $query->bindValue(':id', $ip, $this->db::PARAM_STR);
617
        $query->execute();
618
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
619
620
        // No data found.
621
        if (is_bool($resultData) && !$resultData) {
622
            $resultData = [];
623
        }
624
625
        if (is_array($resultData)) {
626
            $results = $resultData;
627
        }
628
629
        return $results;
630
    }
631
}