Passed
Push — 2.x ( 759244...a3488e )
by Terry
01:51
created

SqlDriverProvider::checkTableExists()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 11
rs 10
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
                $results = $this->doFetchAllFromRuleTable();
115
                break;
116
117
            case 'filter':
118
                $results = $this->doFetchAllFromFilterTable();
119
                break;
120
121
            case 'session':
122
                $results = $this->doFetchAllFromSessionTable();
123
                break;
124
        }
125
126
        return $results;
127
    }
128
129
    /**
130
     * {@inheritDoc}
131
     */
132
    protected function checkExist(string $ip, string $type = 'filter'): bool
133
    {
134
        switch ($type) {
135
136
            case 'rule':
137
                $tableName = $this->tableRuleList;
138
                $field = 'log_ip';
139
                break;
140
141
            case 'filter':
142
                $tableName = $this->tableFilterLogs;
143
                $field = 'log_ip';
144
                break;
145
146
            case 'session':
147
                $tableName = $this->tableSessions;
148
                $field = 'id';
149
                break;
150
        }
151
152
        $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...
153
            WHERE ' . $field . ' = :' . $field . '
154
            LIMIT 1';
155
156
        $query = $this->db->prepare($sql);
157
        $query->bindValue(':' . $field, $ip);
158
159
        $query->execute();
160
        $result = $query->fetch();
161
162
        if (!empty($result[$field])) {
163
            return true; 
164
        }
165
166
        return false;
167
    }
168
169
    /**
170
     * {@inheritDoc}
171
     */
172
    protected function doSave(string $ip, array $data, string $type = 'filter', $expire = 0): bool
173
    {
174
        switch ($type) {
175
176
            case 'rule':
177
                $tableName = $this->tableRuleList;
178
                $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...
179
                $logData = $data;
180
                $logData['log_ip'] = $ip;
181
                break;
182
183
            case 'filter':
184
                $tableName = $this->tableFilterLogs;
185
                $logWhere['log_ip'] = $ip;
186
                $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...
187
                $logData['log_data'] = json_encode($data);
188
                break;
189
190
            case 'session':
191
                $tableName = $this->tableSessions;
192
                
193
                $logWhere['id'] = $data['id'];
194
                $logData = $data;
195
                break;
196
        }
197
198
        if ($this->checkExist($ip, $type)) {
199
            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 $logData 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...
200
        } else {
201
            return (bool) $this->insert($tableName, $logData);
202
        }
203
    }
204
205
    /**
206
     * {@inheritDoc}
207
     */
208
    protected function doDelete(string $ip, string $type = 'filter'): bool
209
    {
210
        switch ($type) {
211
            case 'rule'      : return $this->remove($this->tableRuleList,   ['log_ip' => $ip]);
212
            case 'filter': return $this->remove($this->tableFilterLogs, ['log_ip' => $ip]);
213
            case 'session'   : return $this->remove($this->tableSessions,   ['id'     => $ip]);
214
        }
215
216
        return false;
217
    }
218
219
    /**
220
     * {@inheritDoc}
221
     */
222
    protected function doRebuild(): bool
223
    {
224
        return $this->rebuildSql();
225
    }
226
227
    /**
228
     * Update database table.
229
     *
230
     * @param string $table
231
     * @param array  $data
232
     * @param array  $where
233
     *
234
     * @return bool
235
     */
236
    private function update(string $table, array $data, array $where)
237
    {
238
        $placeholder = [];
239
        foreach($data as $k => $v) {
240
            $placeholder[] = "$k = :$k";
241
        }
242
243
        $dataPlaceholder = implode(', ', $placeholder);
244
245
        $placeholder = [];
246
        foreach($where as $k => $v) {
247
            $placeholder[] = "$k = :$k";
248
        }
249
250
        $wherePlaceholder = implode(' AND ', $placeholder);
251
252
        try {
253
            $sql = 'UPDATE ' . $table . ' SET ' . $dataPlaceholder . ' WHERE ' . $wherePlaceholder;
254
            $query = $this->db->prepare($sql);
255
256
            $bind = array_merge($data, $where);
257
    
258
            foreach($bind as $k => $v) {
259
260
                // @codeCoverageIgnoreStart
261
262
                if (is_numeric($v)) {
263
                    $pdoParam = $this->db::PARAM_INT;
264
265
                    // Solve problem with bigint.
266
                    if ($v >= 2147483647) {
267
                        $pdoParam = $this->db::PARAM_STR;
268
                    } 
269
                } elseif (is_bool($v)) {
270
                    $pdoParam = $this->db::PARAM_BOOL;
271
                } elseif (is_null($v)) {
272
                    $pdoParam = $this->db::PARAM_NULL;
273
                } else {
274
                    $pdoParam = $this->db::PARAM_STR;
275
                }
276
277
                // @codeCoverageIgnoreEnd
278
279
                $query->bindValue(":$k", $bind[$k], $pdoParam);
280
            }
281
282
            return $query->execute();
283
284
        // @codeCoverageIgnoreStart
285
        
286
        } catch(Exception $e) {
287
            throw $e->getMessage();
288
            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...
289
        }
290
291
        // @codeCoverageIgnoreEnd 
292
    }
293
294
    /**
295
     * Insert database table.
296
     *
297
     * @param string $table
298
     * @param array  $data
299
     *
300
     * @return bool
301
     */
302
    private function insert(string $table, array $data)
303
    {
304
        $placeholderField = [];
305
        $placeholderValue = [];
306
        foreach($data as $k => $v) {
307
            $placeholderField[] = "`$k`";
308
            $placeholderValue[] = ":$k";
309
        }
310
311
        $dataPlaceholderField = implode(', ', $placeholderField);
312
        $dataPlaceholderValue = implode(', ', $placeholderValue);
313
314
        try {
315
            $sql = 'INSERT INTO ' . $table . ' (' . $dataPlaceholderField . ') VALUES (' . $dataPlaceholderValue . ')';
316
            $query = $this->db->prepare($sql);
317
318
            foreach($data as $k => $v) {
319
320
                // @codeCoverageIgnoreStart
321
322
                if (is_numeric($v)) {
323
                    $pdoParam = $this->db::PARAM_INT;
324
325
                    // Solve problem with bigint.
326
                    if ($v >= 2147483647) {
327
                        $pdoParam = $this->db::PARAM_STR;
328
                    }
329
                } elseif (is_bool($v)) {
330
                    $pdoParam = $this->db::PARAM_BOOL;
331
                } elseif (is_null($v)) {
332
                    $pdoParam = $this->db::PARAM_NULL;
333
                } else {
334
                    $pdoParam = $this->db::PARAM_STR;
335
                }
336
337
                // @codeCoverageIgnoreEnd
338
339
                $query->bindValue(":$k", $data[$k], $pdoParam);
340
            }
341
342
            return $query->execute();
343
344
        // @codeCoverageIgnoreStart
345
346
        } catch(Exception $e) {
347
            return false;
348
        }
349
350
        // @codeCoverageIgnoreEnd
351
    }
352
353
    /**
354
     * Remove a row from a table.
355
     *
356
     * @param string $table
357
     * @param array $where
358
     *
359
     * @return bool
360
     */
361
    private function remove(string $table, array $where): bool
362
    {
363
364
        $placeholder = [];
365
        foreach($where as $k => $v) {
366
            $placeholder[] = "`$k` = :$k";
367
        }
368
369
        $dataPlaceholder = implode(' AND ', $placeholder);
370
371
        try {
372
373
            $sql = 'DELETE FROM ' . $table . ' WHERE ' . $dataPlaceholder;
374
            $query = $this->db->prepare($sql);
375
376
            foreach($where as $k => $v) {
377
378
                // @codeCoverageIgnoreStart
379
380
                if (is_numeric($v)) {
381
                    $pdoParam = $this->db::PARAM_INT;
382
                } elseif (is_bool($v)) {
383
                    $pdoParam = $this->db::PARAM_BOOL;
384
                } elseif (is_null($v)) {
385
                    $pdoParam = $this->db::PARAM_NULL;
386
                } else {
387
                    $pdoParam = $this->db::PARAM_STR;
388
                }
389
390
                // @codeCoverageIgnoreEnd
391
392
                $query->bindValue(":$k", $v, $pdoParam);
393
            }
394
395
            return $query->execute();
396
397
        // @codeCoverageIgnoreStart
398
399
        } catch(Exception $e) {
400
            return false;
401
        }
402
403
        // @codeCoverageIgnoreEnd
404
    }
405
406
    /**
407
     * Create SQL tables that Shieldon needs.
408
     *
409
     * @return bool
410
     */
411
    protected function installSql(): bool
412
    {
413
        try {
414
415
            $sql = "
416
                CREATE TABLE IF NOT EXISTS `{$this->tableFilterLogs}` (
417
                    `log_ip` varchar(46) NOT NULL,
418
                    `log_data` blob,
419
                    PRIMARY KEY (`log_ip`)
420
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
421
            ";
422
423
            $this->db->query($sql);
424
425
            $sql = "
426
                CREATE TABLE IF NOT EXISTS `{$this->tableRuleList}` (
427
                    `log_ip` varchar(46) NOT NULL,
428
                    `ip_resolve` varchar(255) NOT NULL,
429
                    `type` tinyint(3) UNSIGNED NOT NULL,
430
                    `reason` tinyint(3) UNSIGNED NOT NULL,
431
                    `time` int(10) UNSIGNED NOT NULL,
432
                    `attempts` int(10) UNSIGNED DEFAULT 0,
433
                    PRIMARY KEY (`log_ip`)
434
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
435
            ";
436
437
            $this->db->query($sql);
438
439
            $sql = "
440
                CREATE TABLE `{$this->tableSessions}` (
441
                    `id` varchar(40) NOT NULL,
442
                    `ip` varchar(46) NOT NULL,
443
                    `time` int(10) UNSIGNED NOT NULL,
444
                    `microtimesamp` bigint(20) UNSIGNED NOT NULL,
445
                    `data` blob,
446
                    PRIMARY KEY (`id`)
447
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
448
            ";
449
450
            $this->db->query($sql);
451
452
            return true;
453
454
        // @codeCoverageIgnoreStart
455
456
        } catch (Exception $e) {
457
            return false;
458
        }
459
460
        // @codeCoverageIgnoreEnd
461
    }
462
463
    /**
464
     * Clean all records in IP log and IP rule tables, and then rebuild new tables.
465
     *
466
     * @return bool
467
     */
468
    protected function rebuildSql(): bool
469
    {
470
        try {
471
472
            $sql = "DROP TABLE IF EXISTS `{$this->tableFilterLogs}`";
473
            $this->db->query($sql);
474
475
            $sql = "DROP TABLE IF EXISTS `{$this->tableRuleList}`";
476
            $this->db->query($sql);
477
478
            $sql = "DROP TABLE IF EXISTS `{$this->tableSessions}`";
479
            $this->db->query($sql);
480
481
            $this->installSql();
482
483
            return true;
484
485
        // @codeCoverageIgnoreStart
486
487
        } catch (Exception $e) {
488
            return false;
489
        }
490
491
        // @codeCoverageIgnoreEnd
492
    }
493
494
    /**
495
     * Check required tables exist or not.
496
     *
497
     * @return bool
498
     */
499
    protected function checkTableExists(): bool
500
    {
501
        $checkLogTable = $this->db->query("SHOW TABLES LIKE '{$this->tableFilterLogs}'");
502
503
        if ($checkLogTable) {
504
            if ($checkLogTable->rowCount() > 0) {
505
                return true;
506
            }
507
        }
508
509
        return false;
510
    }
511
512
    /**
513
     * Fetch data from filter table.
514
     *
515
     * @param string $ip An IP address.
516
     *
517
     * @return array
518
     */
519
    private function doFetchFromFilterTable(string $ip): array
520
    {
521
        $results = [];
522
523
        $sql = 'SELECT log_ip, log_data FROM ' . $this->tableFilterLogs . '
524
            WHERE log_ip = :log_ip
525
            LIMIT 1';
526
527
        $query = $this->db->prepare($sql);
528
        $query->bindValue(':log_ip', $ip, $this->db::PARAM_STR);
529
        $query->execute();
530
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
531
532
        // No data found.
533
        if (is_bool($resultData) && !$resultData) {
534
            $resultData = [];
535
        }
536
537
        if (!empty($resultData['log_data'])) {
538
            $results = json_decode($resultData['log_data'], true); 
539
        }
540
541
        return $results;
542
    }
543
544
    /**
545
     * Fetch data from rule table.
546
     *
547
     * @param string $ip An IP address.
548
     *
549
     * @return array
550
     */
551
    private function doFetchFromRuleTable(string $ip): array
552
    {
553
        $results = [];
554
555
        $sql = 'SELECT * FROM ' . $this->tableRuleList . '
556
            WHERE log_ip = :log_ip
557
            LIMIT 1';
558
559
        $query = $this->db->prepare($sql);
560
        $query->bindValue(':log_ip', $ip, $this->db::PARAM_STR);
561
        $query->execute();
562
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
563
564
        // No data found.
565
        if (is_bool($resultData) && !$resultData) {
566
            $resultData = [];
567
        }
568
569
        if (is_array($resultData)) {
570
            $results = $resultData;
571
        }
572
573
        return $results;
574
    }
575
576
    /**
577
     * Fetch data from session table.
578
     *
579
     * @param string $ip An IP address.
580
     *
581
     * @return array
582
     */
583
    private function doFetchFromSessionTable(string $ip): array
584
    {
585
        $results = [];
586
587
        $sql = 'SELECT * FROM ' . $this->tableSessions . '
588
            WHERE id = :id
589
            LIMIT 1';
590
591
        $query = $this->db->prepare($sql);
592
        $query->bindValue(':id', $ip, $this->db::PARAM_STR);
593
        $query->execute();
594
        $resultData = $query->fetch($this->db::FETCH_ASSOC);
595
596
        // No data found.
597
        if (is_bool($resultData) && !$resultData) {
598
            $resultData = [];
599
        }
600
601
        if (is_array($resultData)) {
602
            $results = $resultData;
603
        }
604
605
        return $results;
606
    }
607
608
    /**
609
     * Fetch all data from filter table.
610
     *
611
     * @return array
612
     */
613
    private function doFetchAllFromFilterTable(): array
614
    {
615
        $results = [];
616
617
        $sql = 'SELECT log_ip, log_data FROM ' . $this->tableFilterLogs;
618
619
        $query = $this->db->prepare($sql);
620
        $query->execute();
621
        $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
622
623
        if (is_array($resultData)) {
624
            $results = $resultData;
625
        }
626
627
        return $results;
628
    }
629
630
    /**
631
     * Fetch all data from filter table.
632
     *
633
     * @return array
634
     */
635
    private function doFetchAllFromRuleTable(): array
636
    {
637
        $results = [];
638
639
        $sql = 'SELECT * FROM ' . $this->tableRuleList;
640
641
        $query = $this->db->prepare($sql);
642
        $query->execute();
643
        $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
644
645
        if (is_array($resultData)) {
646
            $results = $resultData;
647
        }
648
649
        return $results;
650
    }
651
652
    /**
653
     * Fetch all data from session table.
654
     * @return array
655
     */
656
    private function doFetchAllFromSessionTable(): array
657
    {
658
        $results = [];
659
660
        $sql = 'SELECT * FROM ' . $this->tableSessions . ' ORDER BY microtimesamp ASC';
661
662
        $query = $this->db->prepare($sql);
663
        $query->execute();
664
        $resultData = $query->fetchAll($this->db::FETCH_ASSOC);
665
666
        if (is_array($resultData)) {
667
            $results = $resultData;
668
        }
669
670
        return $results;
671
    }
672
}