Passed
Push — 2.x ( da3964...4ff14b )
by Terry
01:53
created

SqlDriverProvider::doInitialize()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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