Passed
Push — 2.x ( 1162d1...df5dec )
by Terry
01:55
created

SqlDriverProvider   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 200
c 4
b 0
f 0
dl 0
loc 536
rs 8.96
wmc 43

14 Methods

Rating   Name   Duplication   Size   Complexity  
B insert() 0 48 6
B update() 0 56 7
A doSave() 0 47 5
A doInitialize() 0 13 5
A remove() 0 41 5
A doFetch() 0 12 1
A doRebuild() 0 3 1
A doFetchAll() 0 14 1
A installSql() 0 48 2
A __construct() 0 6 2
A doDelete() 0 27 1
A rebuildSql() 0 19 2
A checkTableExists() 0 11 3
A checkExist() 0 35 2

How to fix   Complexity   

Complex Class

Complex classes like SqlDriverProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SqlDriverProvider, and based on these observations, apply Extract Interface, too.

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\Driver;
24
25
use Shieldon\Firewall\Driver\DriverProvider;
26
use Shieldon\Firewall\Driver\SqlDriverTrait;
27
use Exception;
28
use PDO;
29
30
use function is_bool;
31
use function is_null;
32
use function gettype;
33
use function strtolower;
34
35
/**
36
 * SQL Driver provider.
37
 */
38
class SqlDriverProvider extends DriverProvider
39
{
40
    use SqlDriverTrait;
41
42
    /**
43
     * Data engine will be used on.
44
     *
45
     * @var string
46
     */
47
    protected $tableDbEngine = 'innodb';
48
49
    /**
50
     * PDO instance.
51
     * 
52
     * @var object
53
     */
54
    protected $db;
55
56
    /**
57
     * Constructor.
58
     *
59
     * @param PDO  $pdo   The PDO instance.
60
     * @param bool $debug The option to enable debugging or not.
61
     * 
62
     * @return void
63
     */
64
    public function __construct(PDO $pdo, bool $debug = false)
65
    {
66
        $this->db = $pdo;
67
68
        if ($debug) {
69
            $this->db->setAttribute($this->db::ATTR_ERRMODE, $this->db::ERRMODE_EXCEPTION);
70
        }
71
    }
72
73
    /**
74
     * Initialize data tables.
75
     *
76
     * @param bool $dbCheck This is for creating data tables automatically
77
     *                      Turn it off, if you don't want to check data tables every pageview.
78
     *
79
     * @return void
80
     */
81
    protected function doInitialize(bool $dbCheck = true): void
82
    {
83
        if (!$this->isInitialized) {
84
            if (!empty($this->channel)) {
85
                $this->setChannel($this->channel);
86
            }
87
88
            if ($dbCheck && !$this->checkTableExists()) {
89
                $this->installSql();
90
            }
91
        }
92
93
        $this->isInitialized = true;
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     * 
99
     * @param string $ip   The data id of the entry to fetch.
100
     * @param string $type The type of the data table.
101
     * 
102
     * @return array
103
     */
104
    protected function doFetch(string $ip, string $type = 'filter'): array
105
    {
106
        $tables = [
107
            'rule' => 'doFetchFromRuleTable',
108
            'filter' => 'doFetchFromFilterTable',
109
            'session' => 'doFetchFromSessionTable',
110
        ];
111
112
        $method = $tables[$type];
113
114
        // Fetch from SqlDriverTrait.
115
        return $this->{$method}($ip);
116
    }
117
118
    /**
119
     * {@inheritDoc}
120
     * 
121
     * @param string $type The type of the data table.
122
     * 
123
     * @return bool
124
     */
125
    protected function doFetchAll(string $type = 'filter'): array
126
    {
127
        $this->assertInvalidDataTable($type);
128
129
        $tables = [
130
            'rule' => 'doFetchAllFromRuleTable',
131
            'filter' => 'doFetchAllFromFilterTable',
132
            'session' => 'doFetchAllFromSessionTable',
133
        ];
134
 
135
        $method = $tables[$type];
136
137
        // Fetch from SqlDriverTrait.
138
        return $this->{$method}();
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     * 
144
     * @param string $ip   The data id of the entry to check for.
145
     * @param string $type The type of the data table.
146
     *
147
     * @return bool
148
     */
149
    protected function checkExist(string $ip, string $type = 'filter'): bool
150
    {
151
        $tables = [
152
            'rule' => [
153
                'table' => $this->tableRuleList,
154
                'field' => 'log_ip',
155
            ],
156
            'filter' => [
157
                'table' => $this->tableFilterLogs,
158
                'field' => 'log_ip',
159
            ],
160
            'session' => [
161
                'table' => $this->tableSessions,
162
                'field' => 'id',
163
            ],
164
        ];
165
166
        $tableName = $tables[$type]['table'];
167
        $field = $tables[$type]['field'];
168
169
        $sql = 'SELECT ' . $field . ' FROM ' . $tableName . '
170
            WHERE ' . $field . ' = :' . $field . '
171
            LIMIT 1';
172
173
        $query = $this->db->prepare($sql);
174
        $query->bindValue(':' . $field, $ip);
175
176
        $query->execute();
177
        $result = $query->fetch();
178
179
        if (!empty($result[$field])) {
180
            return true; 
181
        }
182
183
        return false;
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     * 
189
     * @param string $ip     The data id.
190
     * @param array  $data   The data.
191
     * @param string $type   The type of the data table.
192
     * @param int    $expire The data will be deleted after expiring.
193
     *
194
     * @return bool
195
     */
196
    protected function doSave(string $ip, array $data, string $type = 'filter', $expire = 0): bool
197
    {
198
        $this->assertInvalidDataTable($type);
199
200
        switch ($type) {
201
202
            case 'rule':
203
                $tableName = $this->tableRuleList;
204
205
                $logWhere = [];
206
                $logWhere['log_ip'] = $ip;
207
208
                $logData = $data;
209
                $logData['log_ip'] = $ip;
210
                break;
211
212
            case 'filter':
213
                $tableName = $this->tableFilterLogs;
214
215
                $logWhere = [];
216
                $logWhere['log_ip'] = $ip;
217
218
                $logData = [];
219
                $logData['log_ip'] = $ip;
220
                $logData['log_data'] = json_encode($data);
221
                break;
222
223
            case 'session':
224
                $tableName = $this->tableSessions;
225
226
                $logWhere = [];
227
                $logWhere['id'] = $data['id'];
228
229
                $logData = $data;
230
                break;
231
232
            // @codeCoverageIgnoreStart
233
            default:
234
                return false;
235
            // @codeCoverageIgnoreEnd
236
        }
237
238
        if ($this->checkExist($ip, $type)) {
239
            return $this->update($tableName, $logData, $logWhere);
240
        }
241
242
        return (bool) $this->insert($tableName, $logData);
243
    }
244
245
    /**
246
     * {@inheritDoc}
247
     * 
248
     * @param string $ip   The key name of a redis entry.
249
     * @param string $type The type of the data table.
250
     * 
251
     * @return bool
252
     */
253
    protected function doDelete(string $ip, string $type = 'filter'): bool
254
    {
255
        $this->assertInvalidDataTable($type);
256
257
        $tables = [
258
            'rule' => [
259
                'table' => $this->tableRuleList,
260
                'field' => 'log_ip',
261
                'value' => $ip,
262
            ],
263
            'filter' => [
264
                'table' => $this->tableFilterLogs,
265
                'field' => 'log_ip',
266
                'value' => $ip,
267
            ],
268
            'session' => [
269
                'table' => $this->tableSessions,
270
                'field' => 'id',
271
                'value' => $ip,
272
            ],
273
        ];
274
275
        $tableName = $tables[$type]['table'];
276
        $field = $tables[$type]['field'];
277
        $value = $tables[$type]['value'];
278
279
        return $this->remove($tableName, [$field => $value]);
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     * 
285
     * Rebuild data tables.
286
     *
287
     * @return bool
288
     */
289
    protected function doRebuild(): bool
290
    {
291
        return $this->rebuildSql();
292
    }
293
294
    /**
295
     * Update database table.
296
     *
297
     * @param string $table The data table name.
298
     * @param array  $data  The SELECT statement.
299
     * @param array  $where The array creates WHERE clause.
300
     *
301
     * @return bool
302
     */
303
    protected function update(string $table, array $data, array $where)
304
    {
305
        $placeholder = [];
306
        foreach ($data as $k => $v) {
307
            $placeholder[] = "$k = :$k";
308
        }
309
310
        $dataPlaceholder = implode(', ', $placeholder);
311
312
        $placeholder = [];
313
        foreach ($where as $k => $v) {
314
            $placeholder[] = "$k = :$k";
315
        }
316
317
        $wherePlaceholder = implode(' AND ', $placeholder);
318
319
        try {
320
321
            $paramTypeCheck = [
322
                'integer' => $this->db::PARAM_INT,
323
                'boolean' => $this->db::PARAM_BOOL,
324
                'NULL'    => $this->db::PARAM_NULL,
325
                'string'  => $this->db::PARAM_STR,
326
            ];
327
328
329
            $sql = 'UPDATE ' . $table . ' SET ' . $dataPlaceholder . ' WHERE ' . $wherePlaceholder;
330
            $query = $this->db->prepare($sql);
331
332
            $bind = array_merge($data, $where);
333
334
            foreach ($bind as $k => $v) {
335
336
                // Default.
337
                $pdoParam = $paramTypeCheck['string'];
338
339
                $type = gettype($v);
340
341
                if (isset($paramTypeCheck[$type])) {
342
                    $pdoParam = $paramTypeCheck[$type];
343
                }
344
345
                // Solve problem with bigint.
346
                if ($v >= 2147483647) {
347
                    $pdoParam = $this->db::PARAM_STR;
348
                }
349
350
                $query->bindValue(":$k", $bind[$k], $pdoParam);
351
            }
352
353
            return $query->execute();
354
355
            // @codeCoverageIgnoreStart
356
        
357
        } catch (Exception $e) {
358
            return false;
359
        }
360
361
        // @codeCoverageIgnoreEnd 
362
    }
363
364
    /**
365
     * Insert database table.
366
     *
367
     * @param string $table The data table name.
368
     * @param array  $data  The data want to insert to the table.
369
     *
370
     * @return bool
371
     */
372
    protected function insert(string $table, array $data)
373
    {
374
        $placeholderField = [];
375
        $placeholderValue = [];
376
        foreach ($data as $k => $v) {
377
            $placeholderField[] = "`$k`";
378
            $placeholderValue[] = ":$k";
379
        }
380
381
        $dataPlaceholderField = implode(', ', $placeholderField);
382
        $dataPlaceholderValue = implode(', ', $placeholderValue);
383
384
        try {
385
386
            $paramTypeCheck = [
387
                'integer' => $this->db::PARAM_INT,
388
                'boolean' => $this->db::PARAM_BOOL,
389
                'NULL'    => $this->db::PARAM_NULL,
390
                'string'  => $this->db::PARAM_STR,
391
            ];
392
393
            $sql = 'INSERT INTO ' . $table . ' (' . $dataPlaceholderField . ') VALUES (' . $dataPlaceholderValue . ')';
394
            $query = $this->db->prepare($sql);
395
396
            foreach ($data as $k => $v) {
397
398
                // Default.
399
                $pdoParam = $paramTypeCheck['string'];
400
401
                $type = gettype($v);
402
403
                if (isset($paramTypeCheck[$type])) {
404
                    $pdoParam = $paramTypeCheck[$type];
405
                }
406
407
                // Solve problem with bigint.
408
                if ($v >= 2147483647) {
409
                    $pdoParam = $this->db::PARAM_STR;
410
                }
411
412
                $query->bindValue(":$k", $data[$k], $pdoParam);
413
            }
414
415
            return $query->execute();
416
417
            // @codeCoverageIgnoreStart
418
        } catch (Exception $e) {
419
            return false;
420
        }
421
        // @codeCoverageIgnoreEnd
422
    }
423
424
    /**
425
     * Remove a row from a table.
426
     *
427
     * @param string $table The data table name.
428
     * @param array  $where The array creates WHERE clause.
429
     *
430
     * @return bool
431
     */
432
    protected function remove(string $table, array $where): bool
433
    {
434
435
        $placeholder = [];
436
        foreach ($where as $k => $v) {
437
            $placeholder[] = "`$k` = :$k";
438
        }
439
440
        $dataPlaceholder = implode(' AND ', $placeholder);
441
442
        try {
443
444
            $paramTypeCheck = [
445
                'integer' => $this->db::PARAM_INT,
446
                'boolean' => $this->db::PARAM_BOOL,
447
                'NULL'    => $this->db::PARAM_NULL,
448
                'string'  => $this->db::PARAM_STR,
449
            ];
450
451
            $sql = 'DELETE FROM ' . $table . ' WHERE ' . $dataPlaceholder;
452
            $query = $this->db->prepare($sql);
453
454
            foreach ($where as $k => $v) {
455
456
                // Default.
457
                $pdoParam = $paramTypeCheck['string'];
458
459
                $type = gettype($v);
460
461
                if (isset($paramTypeCheck[$type])) {
462
                    $pdoParam = $paramTypeCheck[$type];
463
                }
464
465
                $query->bindValue(":$k", $v, $pdoParam);
466
            }
467
468
            return $query->execute();
469
            // @codeCoverageIgnoreStart
470
471
        } catch (Exception $e) {
472
            return false;
473
        }
474
        // @codeCoverageIgnoreEnd
475
    }
476
477
    /**
478
     * Create SQL tables that Shieldon needs.
479
     *
480
     * @return bool
481
     */
482
    protected function installSql(): bool
483
    {
484
        try {
485
486
            $sql = "
487
                CREATE TABLE IF NOT EXISTS `{$this->tableFilterLogs}` (
488
                    `log_ip` varchar(46) NOT NULL,
489
                    `log_data` blob,
490
                    PRIMARY KEY (`log_ip`)
491
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
492
            ";
493
494
            $this->db->query($sql);
495
496
            $sql = "
497
                CREATE TABLE IF NOT EXISTS `{$this->tableRuleList}` (
498
                    `log_ip` varchar(46) NOT NULL,
499
                    `ip_resolve` varchar(255) NOT NULL,
500
                    `type` tinyint(3) UNSIGNED NOT NULL,
501
                    `reason` tinyint(3) UNSIGNED NOT NULL,
502
                    `time` int(10) UNSIGNED NOT NULL,
503
                    `attempts` int(10) UNSIGNED DEFAULT 0,
504
                    PRIMARY KEY (`log_ip`)
505
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
506
            ";
507
508
            $this->db->query($sql);
509
510
            $sql = "
511
                CREATE TABLE `{$this->tableSessions}` (
512
                    `id` varchar(40) NOT NULL,
513
                    `ip` varchar(46) NOT NULL,
514
                    `time` int(10) UNSIGNED NOT NULL,
515
                    `microtimesamp` bigint(20) UNSIGNED NOT NULL,
516
                    `data` blob,
517
                    PRIMARY KEY (`id`)
518
                ) ENGINE={$this->tableDbEngine} DEFAULT CHARSET=latin1;
519
            ";
520
521
            $this->db->query($sql);
522
523
            // @codeCoverageIgnoreStart
524
        } catch (Exception $e) {
525
            return false;
526
        }
527
        // @codeCoverageIgnoreEnd
528
529
        return true;
530
    }
531
532
    /**
533
     * Clean all records in IP log and IP rule tables, and then rebuild new tables.
534
     *
535
     * @return bool
536
     */
537
    protected function rebuildSql(): bool
538
    {
539
        try {
540
            $sql = "DROP TABLE IF EXISTS `{$this->tableFilterLogs}`";
541
            $this->db->query($sql);
542
            $sql = "DROP TABLE IF EXISTS `{$this->tableRuleList}`";
543
            $this->db->query($sql);
544
            $sql = "DROP TABLE IF EXISTS `{$this->tableSessions}`";
545
            $this->db->query($sql);
546
547
            $this->installSql();
548
549
            // @codeCoverageIgnoreStart
550
        } catch (Exception $e) {
551
            return false;
552
        }
553
        // @codeCoverageIgnoreEnd
554
555
        return true;
556
    }
557
558
    /**
559
     * Check required tables exist or not.
560
     *
561
     * @return bool
562
     */
563
    protected function checkTableExists(): bool
564
    {
565
        $checkLogTable = $this->db->query("SHOW TABLES LIKE '{$this->tableFilterLogs}'");
566
567
        if ($checkLogTable) {
568
            if ($checkLogTable->rowCount() > 0) {
569
                return true;
570
            }
571
        }
572
573
        return false;
574
    }
575
}
576