Completed
Push — master ( 39d9a4...0a738c )
by Lars
03:51
created

DB::commit()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 28

Duplication

Lines 28
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 10.5

Importance

Changes 0
Metric Value
dl 28
loc 28
ccs 6
cts 12
cp 0.5
rs 8.8497
c 0
b 0
f 0
cc 6
nc 5
nop 0
crap 10.5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Doctrine\DBAL\Connection;
8
use voku\cache\Cache;
9
use voku\db\exceptions\DBConnectException;
10
use voku\db\exceptions\DBGoneAwayException;
11
use voku\db\exceptions\QueryException;
12
use voku\helper\UTF8;
13
14
/**
15
 * DB: This class can handle DB queries via MySQLi.
16
 */
17
final class DB
18
{
19
    /**
20
     * @var int
21
     */
22
    public $query_count = 0;
23
24
    /**
25
     * @var \mysqli|null
26
     */
27
    private $mysqli_link;
28
29
    /**
30
     * @var bool
31
     */
32
    private $connected = false;
33
34
    /**
35
     * @var array
36
     */
37
    private $mysqlDefaultTimeFunctions;
38
39
    /**
40
     * @var string
41
     */
42
    private $hostname = '';
43
44
    /**
45
     * @var string
46
     */
47
    private $username = '';
48
49
    /**
50
     * @var string
51
     */
52
    private $password = '';
53
54
    /**
55
     * @var string
56
     */
57
    private $database = '';
58
59
    /**
60
     * @var int
61
     */
62
    private $port = 3306;
63
64
    /**
65
     * @var string
66
     */
67
    private $charset = 'utf8';
68
69
    /**
70
     * @var string
71
     */
72
    private $socket = '';
73
74
    /**
75
     * @var bool
76
     */
77
    private $session_to_db = false;
78
79
    /**
80
     * @var bool
81
     */
82
    private $in_transaction = false;
83
84
    /**
85
     * @var bool
86
     */
87
    private $convert_null_to_empty_string = false;
88
89
    /**
90
     * @var bool
91
     */
92
    private $ssl = false;
93
94
    /**
95
     * The path name to the key file
96
     *
97
     * @var string
98
     */
99
    private $clientkey;
100
101
    /**
102
     * The path name to the certificate file
103
     *
104
     * @var string
105
     */
106
    private $clientcert;
107
108
    /**
109
     * The path name to the certificate authority file
110
     *
111
     * @var string
112
     */
113
    private $cacert;
114
115
    /**
116
     * @var Debug
117
     */
118
    private $debug;
119
120
    /**
121
     * @var \Doctrine\DBAL\Connection|null
122
     */
123
    private $doctrine_connection;
124
125
    /**
126
     * @var int
127
     */
128
    private $affected_rows = 0;
129
130
    /**
131
     * __construct()
132
     *
133
     * @param string $hostname
134
     * @param string $username
135
     * @param string $password
136
     * @param string $database
137
     * @param int    $port
138
     * @param string $charset
139
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
140
     *                                      'false'. Use false to disable it.</p>
141
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
142
     *                                      Use false to disable it.</p>
143
     * @param string $logger_class_name
144
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
145
     * @param array  $extra_config          <p>
146
     *                                      'session_to_db' => bool<br>
147
     *                                      'socket'        => 'string (path)'<br>
148
     *                                      'ssl'           => bool<br>
149
     *                                      'clientkey'     => 'string (path)'<br>
150
     *                                      'clientcert'    => 'string (path)'<br>
151
     *                                      'cacert'        => 'string (path)'<br>
152
     *                                      </p>
153 23
     */
154
    private function __construct(string $hostname, string $username, string $password, string $database, $port, string $charset, bool $exit_on_error, bool $echo_on_error, string $logger_class_name, string $logger_level, array $extra_config = [])
155 23
    {
156
        $this->debug = new Debug($this);
157 23
158 23
        $this->_loadConfig(
159 23
            $hostname,
160 23
            $username,
161 23
            $password,
162 23
            $database,
163 23
            $port,
164 23
            $charset,
165 23
            $exit_on_error,
166 23
            $echo_on_error,
167 23
            $logger_class_name,
168 23
            $logger_level,
169
            $extra_config
170
        );
171 14
172
        $this->connect();
173 5
174
        $this->mysqlDefaultTimeFunctions = [
175
            // Returns the current date.
176
            'CURDATE()',
177
            // CURRENT_DATE	| Synonyms for CURDATE()
178
            'CURRENT_DATE()',
179
            // CURRENT_TIME	| Synonyms for CURTIME()
180
            'CURRENT_TIME()',
181
            // CURRENT_TIMESTAMP | Synonyms for NOW()
182
            'CURRENT_TIMESTAMP()',
183
            // Returns the current time.
184
            'CURTIME()',
185
            // Synonym for NOW()
186
            'LOCALTIME()',
187
            // Synonym for NOW()
188
            'LOCALTIMESTAMP()',
189
            // Returns the current date and time.
190
            'NOW()',
191
            // Returns the time at which the function executes.
192
            'SYSDATE()',
193
            // Returns a UNIX timestamp.
194
            'UNIX_TIMESTAMP()',
195
            // Returns the current UTC date.
196
            'UTC_DATE()',
197
            // Returns the current UTC time.
198
            'UTC_TIME()',
199
            // Returns the current UTC date and time.
200
            'UTC_TIMESTAMP()',
201 5
        ];
202
    }
203
204
    /**
205
     * Prevent the instance from being cloned.
206
     *
207
     * @return void
208
     */
209
    private function __clone()
210
    {
211
    }
212
213
    /**
214
     * __destruct
215
     */
216
    public function __destruct()
217
    {
218
        // close the connection only if we don't save PHP-SESSION's in DB
219
        if (!$this->session_to_db) {
220
            $this->close();
221
        }
222
    }
223
224
    /**
225
     * @param string|null $sql
226
     * @param array       $bindings
227
     *
228
     * @return bool|DB|int|Result|string
229
     *                                      <p>
230
     *                                      "DB" by "$sql" === null<br />
231
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
232
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
233
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
234
     *                                      "true" by e.g. "DROP"-queries<br />
235
     *                                      "false" on error
236 4
     *                                      </p>
237
     */
238 4
    public function __invoke(string $sql = null, array $bindings = [])
239
    {
240
        return $sql !== null ? $this->query($sql, $bindings) : $this;
241
    }
242
243
    /**
244
     * __wakeup
245
     *
246 4
     * @return void
247
     */
248 4
    public function __wakeup()
249 4
    {
250
        $this->reconnect();
251
    }
252
253
    /**
254
     * Load the config from the constructor.
255
     *
256
     * @param string $hostname
257
     * @param string $username
258
     * @param string $password
259
     * @param string $database
260
     * @param int    $port                  <p>default is (int)3306</p>
261
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
262
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
263
     *                                      'false'. Use false to disable it.</p>
264
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
265
     *                                      Use false to disable it.</p>
266
     * @param string $logger_class_name
267
     * @param string $logger_level
268
     * @param array  $extra_config          <p>
269
     *                                      'session_to_db' => false|true<br>
270
     *                                      'socket' => 'string (path)'<br>
271
     *                                      'ssl' => 'bool'<br>
272
     *                                      'clientkey' => 'string (path)'<br>
273
     *                                      'clientcert' => 'string (path)'<br>
274
     *                                      'cacert' => 'string (path)'<br>
275
     *                                      </p>
276
     *
277 23
     * @return bool
278
     */
279
    private function _loadConfig(
280
        string $hostname,
281
        string $username,
282
        string $password,
283
        string $database,
284
        $port,
285
        string $charset,
286
        bool $exit_on_error,
287
        bool $echo_on_error,
288
        string $logger_class_name,
289
        string $logger_level,
290 23
        array $extra_config = []
291 23
    ): bool {
292 23
        $this->hostname = $hostname;
293 23
        $this->username = $username;
294
        $this->password = $password;
295 23
        $this->database = $database;
296 23
297
        if ($charset) {
298
            $this->charset = $charset;
299 23
        }
300 11
301
        if ($port) {
302
            $this->port = (int) $port;
303 13
        } else {
304
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
305
            $this->port = (int) @\ini_get('mysqli.default_port');
306
        }
307 23
308
        // fallback
309
        if (!$this->port) {
310
            $this->port = 3306;
311 23
        }
312
313 23
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
314
        if (!$this->socket && $defaultSocket = @\ini_get('mysqli.default_socket')) {
315
            $this->socket = $defaultSocket;
316 23
        }
317 23
318
        $this->debug->setExitOnError($exit_on_error);
319 23
        $this->debug->setEchoOnError($echo_on_error);
320 23
321
        $this->debug->setLoggerClassName($logger_class_name);
322 23
        $this->debug->setLoggerLevel($logger_level);
323
324 23
        $this->setConfigExtra($extra_config);
325
326
        return $this->showConfigError();
327
    }
328
329
    /**
330
     * Parses arrays with value pairs and generates SQL to use in queries.
331
     *
332
     * @param array  $arrayPair
333
     * @param string $glue <p>This is the separator.</p>
334
     *
335
     * @return string
336
     *
337 72
     * @internal
338
     */
339
    public function _parseArrayPair(array $arrayPair, string $glue = ','): string
340 72
    {
341
        // init
342 72
        $sql = '';
343
344
        if (\count($arrayPair) === 0) {
345
            return '';
346 72
        }
347 72
348 72
        $arrayPairCounter = 0;
349 72
        foreach ($arrayPair as $_key => $_value) {
350 72
            $_connector = '=';
351
            $_glueHelper = '';
352 72
            $_key_upper = \strtoupper((string) $_key);
353 6
354
            if (\strpos($_key_upper, ' NOT') !== false) {
355
                $_connector = 'NOT';
356 72
            }
357 3
358
            if (\strpos($_key_upper, ' IS') !== false) {
359
                $_connector = 'IS';
360 72
            }
361 3
362
            if (\strpos($_key_upper, ' IS NOT') !== false) {
363
                $_connector = 'IS NOT';
364 72
            }
365 3
366
            if (\strpos($_key_upper, ' IN') !== false) {
367
                $_connector = 'IN';
368 72
            }
369 3
370
            if (\strpos($_key_upper, ' NOT IN') !== false) {
371
                $_connector = 'NOT IN';
372 72
            }
373 3
374
            if (\strpos($_key_upper, ' BETWEEN') !== false) {
375
                $_connector = 'BETWEEN';
376 72
            }
377 3
378
            if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
379
                $_connector = 'NOT BETWEEN';
380 72
            }
381 6
382
            if (\strpos($_key_upper, ' LIKE') !== false) {
383
                $_connector = 'LIKE';
384 72
            }
385 6
386
            if (\strpos($_key_upper, ' NOT LIKE') !== false) {
387
                $_connector = 'NOT LIKE';
388 72
            }
389 8
390 View Code Duplication
            if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
391
                $_connector = '>';
392 72
            }
393 3
394 View Code Duplication
            if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
395
                $_connector = '<';
396 72
            }
397 8
398
            if (\strpos($_key_upper, ' >=') !== false) {
399
                $_connector = '>=';
400 72
            }
401 3
402
            if (\strpos($_key_upper, ' <=') !== false) {
403
                $_connector = '<=';
404 72
            }
405 3
406
            if (\strpos($_key_upper, ' <>') !== false) {
407
                $_connector = '<>';
408 72
            }
409 6
410
            if (\strpos($_key_upper, ' OR') !== false) {
411
                $_glueHelper = 'OR';
412 72
            }
413 3
414
            if (\strpos($_key_upper, ' AND') !== false) {
415
                $_glueHelper = 'AND';
416 72
            }
417 7
418 7
            if (\is_array($_value)) {
419 7
                $firstKey = null;
420 7
                $firstValue = null;
421
                foreach ($_value as $oldKey => $oldValue) {
422 7
                    $_value[$oldKey] = $this->secure($oldValue);
423 7
424
                    if ($firstKey === null) {
425
                        $firstKey = $oldKey;
426 7
                    }
427 7
428
                    if ($firstValue === null) {
429
                        $firstValue = $_value[$oldKey];
430
                    }
431 7
                }
432 3
433 7
                if ($_connector === 'NOT IN' || $_connector === 'IN') {
434 3
                    $_value = '(' . \implode(',', $_value) . ')';
435 7
                } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
436 1
                    $_value = '(' . \implode(' AND ', $_value) . ')';
437 1
                } elseif ($firstKey && $firstValue) {
438 1 View Code Duplication
                    if (\strpos((string) $firstKey, ' +') !== false) {
439
                        $firstKey = \str_replace(' +', '', (string) $firstKey);
440
                        $_value = $firstKey . ' + ' . $firstValue;
441 1
                    }
442 1
443 7 View Code Duplication
                    if (\strpos((string) $firstKey, ' -') !== false) {
444
                        $firstKey = \str_replace(' -', '', (string) $firstKey);
445
                        $_value = $firstKey . ' - ' . $firstValue;
446
                    }
447 72
                }
448
            } else {
449
                $_value = $this->secure($_value);
450 72
            }
451 72
452 72
            $quoteString = $this->quote_string(
453
                \trim(
454 72
                    (string) \str_ireplace(
455 72
                        [
456
                            $_connector,
457 72
                            $_glueHelper,
458 72
                        ],
459
                        '',
460
                        (string) $_key
461
                    )
462
                )
463 72
            );
464
465 72
            $_value = (array) $_value;
466 72
467
            if (!$_glueHelper) {
468
                $_glueHelper = $glue;
469 72
            }
470 72
471 72
            $tmpCounter = 0;
472
            foreach ($_value as $valueInner) {
473 72
                $_glueHelperInner = $_glueHelper;
474 72
475 3
                if ($arrayPairCounter === 0) {
476 72
                    if ($tmpCounter === 0 && $_glueHelper === 'OR') {
477 72
                        $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
478
                    } elseif ($tmpCounter === 0) {
479 68
                        $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
480 3
                    }
481
                } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
482
                    $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
483 72
                }
484
485
                if (\is_string($valueInner) && $valueInner === '') {
486
                    $valueInner = "''";
487 72
                }
488 72
489
                $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
490
                $tmpCounter++;
491 72
            }
492 6
493
            if ($_glueHelper === 'OR') {
494
                $sql .= ' ) ';
495 72
            }
496
497
            $arrayPairCounter++;
498 72
        }
499
500
        return $sql;
501
    }
502
503
    /**
504
     * _parseQueryParams
505
     *
506
     * @param string $sql
507
     * @param array  $params
508
     *
509 7
     * @return array
510
     *               <p>with the keys -> 'sql', 'params'</p>
511 7
     */
512
    private function _parseQueryParams(string $sql, array $params = []): array
513
    {
514
        $offset = \strpos($sql, '?');
515 7
516
        // is there anything to parse?
517 7
        if (
518
            $offset === false
519 3
            ||
520
            \count($params) === 0
521
        ) {
522 7
            return ['sql' => $sql, 'params' => $params];
523
        }
524
525 7
        foreach ($params as $key => $param) {
526 3
527
            // use this only for not named parameters
528
            if (!\is_int($key)) {
529 7
                continue;
530
            }
531
532
            if ($offset === false) {
533 7
                continue;
534
            }
535 7
536
            $replacement = $this->secure($param);
537 7
538 7
            unset($params[$key]);
539
540
            $sql = \substr_replace($sql, $replacement, $offset, 1);
541 7
            $offset = \strpos($sql, '?', $offset + \strlen((string) $replacement));
542
        }
543
544
        return ['sql' => $sql, 'params' => $params];
545
    }
546
547
    /**
548
     * Returns the SQL by replacing :placeholders with SQL-escaped values.
549
     *
550
     * @param string $sql    <p>The SQL string.</p>
551
     * @param array  $params <p>An array of key-value bindings.</p>
552 10
     *
553
     * @return array
554
     *               <p>with the keys -> 'sql', 'params'</p>
555
     */
556 10
    private function _parseQueryParamsByName(string $sql, array $params = []): array
557
    {
558 10
        // is there anything to parse?
559
        if (
560 7
            \strpos($sql, ':') === false
561
            ||
562
            \count($params) === 0
563 6
        ) {
564 6
            return ['sql' => $sql, 'params' => $params];
565 6
        }
566
567
        $offset = null;
568 6
        $replacement = null;
569
        foreach ($params as $name => $param) {
570
571
            // use this only for named parameters
572
            if (\is_int($name)) {
573 6
                continue;
574 6
            }
575
576
            // add ":" if needed
577
            if (\strpos($name, ':') !== 0) {
578
                $nameTmp = ':' . $name;
579 6
            } else {
580 6
                $nameTmp = $name;
581
            }
582 6
583
            if ($offset === null) {
584
                $offset = \strpos($sql, $nameTmp);
585 6
            } else {
586 3
                $offset = \strpos($sql, $nameTmp, $offset + \strlen((string) $replacement));
587
            }
588
589 6
            if ($offset === false) {
590
                continue;
591 6
            }
592
593 6
            $replacement = $this->secure($param);
594
595
            unset($params[$name]);
596 6
597
            $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
598
        }
599
600
        return ['sql' => $sql, 'params' => $params];
601
    }
602
603
    /**
604 25
     * Gets the number of affected rows in a previous MySQL operation.
605
     *
606
     * @return int
607 25
     */
608
    public function affected_rows(): int
609 25
    {
610
        if (
611 25
            $this->mysqli_link
612
            &&
613
            $this->mysqli_link instanceof \mysqli
614
        ) {
615
            return \mysqli_affected_rows($this->mysqli_link);
616
        }
617
618
        return (int) $this->affected_rows;
619
    }
620
621
    /**
622 18
     * Begins a transaction, by turning off auto commit.
623
     *
624 18
     * @return bool
625 6
     *              <p>This will return true or false indicating success of transaction</p>
626
     */
627 6 View Code Duplication
    public function beginTransaction(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
628
    {
629
        if ($this->in_transaction) {
630 18
            $this->debug->displayError('Error: mysql server already in transaction!', false);
631 18
632
            return false;
633 18
        }
634 18
635
        $this->clearErrors(); // needed for "$this->endTransaction()"
636
        $this->in_transaction = true;
637
638
        if ($this->mysqli_link) {
639
            $return = \mysqli_autocommit($this->mysqli_link, false);
640
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
641
            $this->doctrine_connection->setAutoCommit(false);
642
            $this->doctrine_connection->beginTransaction();
643
644
            if ($this->doctrine_connection->isTransactionActive()) {
645
                $return = true;
646 18
            } else {
647
                $return = false;
648
            }
649
        } else {
650 18
            $return = false;
651
        }
652
653
        if (!$return) {
654
            $this->in_transaction = false;
655
        }
656
657
        return $return;
658 18
    }
659
660 18
    /**
661
     * Clear the errors in "_debug->_errors".
662
     *
663
     * @return bool
664
     */
665
    public function clearErrors(): bool
666
    {
667
        return $this->debug->clearErrors();
668
    }
669
670 6
    /**
671
     * Closes a previously opened database connection.
672 6
     *
673
     * @return bool
674
     *              Will return "true", if the connection was closed,
675 6
     *              otherwise (e.g. if the connection was already closed) "false".
676
     */
677 6
    public function close(): bool
678
    {
679
        $this->connected = false;
680
681
        if (
682
            $this->doctrine_connection
683
            &&
684
            $this->doctrine_connection instanceof \Doctrine\DBAL\Connection
685
        ) {
686
            $connectedBefore = $this->doctrine_connection->isConnected();
687
688
            $this->doctrine_connection->close();
689
690
            $this->mysqli_link = null;
691
692
            if ($connectedBefore) {
693 6
                return !$this->doctrine_connection->isConnected();
694
            }
695 6
696
            return false;
697 6
        }
698 6
699
        if (
700 6
            $this->mysqli_link
701
            &&
702
            $this->mysqli_link instanceof \mysqli
703 3
        ) {
704
            $result = \mysqli_close($this->mysqli_link);
705 3
            $this->mysqli_link = null;
706
707
            return $result;
708
        }
709
710
        $this->mysqli_link = null;
711
712
        return false;
713 9
    }
714
715 9
    /**
716
     * Commits the current transaction and end the transaction.
717
     *
718
     * @return bool
719
     *              <p>bool true on success, false otherwise.</p>
720
     */
721 9 View Code Duplication
    public function commit(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
722 9
    {
723 9
        if (!$this->in_transaction) {
724
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
725
726
            return false;
727
        }
728
729
        if ($this->mysqli_link) {
730
            $return = \mysqli_commit($this->mysqli_link);
731
            \mysqli_autocommit($this->mysqli_link, true);
732
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
733
            $this->doctrine_connection->commit();
734
            $this->doctrine_connection->setAutoCommit(true);
735 9
736
            if ($this->doctrine_connection->isAutoCommit()) {
737 9
                $return = true;
738
            } else {
739
                $return = false;
740
            }
741
        } else {
742
            $return = false;
743
        }
744
745
        $this->in_transaction = false;
746
747 20
        return $return;
748
    }
749 20
750 3
    /**
751
     * Open a new connection to the MySQL server.
752
     *
753 20
     * @throws DBConnectException
754
     *
755
     * @return bool
756
     */
757
    public function connect(): bool
758
    {
759
        if ($this->isReady()) {
760
            return true;
761
        }
762
763
        if ($this->doctrine_connection) {
764
            $this->doctrine_connection->connect();
765
766
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
767
768 View Code Duplication
            if ($this->isDoctrineMySQLiConnection()) {
769
                \assert($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection);
770
771
                $this->mysqli_link = $doctrineWrappedConnection->getWrappedResourceHandle();
772
773
                $this->connected = $this->doctrine_connection->isConnected();
774
775
                if (!$this->connected) {
776
                    $error = 'Error connecting to mysql server: ' . \print_r($this->doctrine_connection->errorInfo(), false);
777
                    $this->debug->displayError($error, false);
778
779
                    throw new DBConnectException($error, 101);
780
                }
781
782
                $this->set_charset($this->charset);
783
784
                return $this->isReady();
785
            }
786
787 View Code Duplication
            if ($this->isDoctrinePDOConnection()) {
788
                $this->mysqli_link = null;
789
790
                $this->connected = $this->doctrine_connection->isConnected();
791
792
                if (!$this->connected) {
793
                    $error = 'Error connecting to mysql server: ' . \print_r($this->doctrine_connection->errorInfo(), false);
794
                    $this->debug->displayError($error, false);
795 20
796
                    throw new DBConnectException($error, 101);
797 20
                }
798
799
                $this->set_charset($this->charset);
800 20
801
                return $this->isReady();
802 20
            }
803 20
        }
804
805
        $flags = null;
806 20
807
        \mysqli_report(\MYSQLI_REPORT_STRICT);
808
809
        try {
810
            $this->mysqli_link = \mysqli_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like \mysqli_init() of type object<mysql> is incompatible with the declared type object<mysqli>|null of property $mysqli_link.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
811
812
            if (Helper::isMysqlndIsUsed()) {
813
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
814
            }
815
816
            if ($this->ssl) {
817
                if (empty($this->clientcert)) {
818
                    throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
819
                }
820
821
                if (empty($this->clientkey)) {
822
                    throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
823
                }
824
825
                if (empty($this->cacert)) {
826
                    throw new DBConnectException('Error connecting to mysql server: cacert not defined');
827
                }
828
829
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
830
831
                /** @noinspection PhpParamsInspection */
832
                \mysqli_ssl_set(
833
                    $this->mysqli_link,
834
                    $this->clientkey,
835 20
                    $this->clientcert,
836 20
                    $this->cacert,
837 20
                    '',
838 20
                    ''
839 20
                );
840 20
841 20
                $flags = \MYSQLI_CLIENT_SSL;
842 20
            }
843 20
844
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
845 9
            $this->connected = @\mysqli_real_connect(
846 9
                $this->mysqli_link,
847 9
                $this->hostname,
848
                $this->username,
849 9
                $this->password,
850
                $this->database,
851 11
                $this->port,
852
                $this->socket,
853 11
                (int) $flags
854 11
            );
855
        } catch (\Exception $e) {
856
            $error = 'Error connecting to mysql server: ' . $e->getMessage();
857
            $this->debug->displayError($error, false);
858
859
            throw new DBConnectException($error, 100, $e);
860
        }
861 11
        \mysqli_report(\MYSQLI_REPORT_OFF);
862
863 11
        $errno = \mysqli_connect_errno();
864
        if (!$this->connected || $errno) {
865
            $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
866
            $this->debug->displayError($error, false);
867
868
            throw new DBConnectException($error, 101);
869
        }
870
871
        $this->set_charset($this->charset);
872
873
        return $this->isReady();
874
    }
875
876
    /**
877 4
     * Execute a "delete"-query.
878
     *
879
     * @param string       $table
880 4
     * @param array|string $where
881
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
882 4
     *
883 3
     * @throws QueryException
884
     *
885 3
     * @return false|int
886
     *                   <p>false on error</p>
887
     */
888 4 View Code Duplication
    public function delete(string $table, $where, string $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
889 3
    {
890 4
        // init
891 4
        $table = \trim($table);
892
893 3
        if ($table === '') {
894
            $this->debug->displayError('Invalid table name, table name in empty.', false);
895
896 4
            return false;
897
        }
898
899
        if (\is_string($where)) {
900 4
            $WHERE = $this->escape($where, false);
901
        } elseif (\is_array($where)) {
902 4
            $WHERE = $this->_parseArrayPair($where, 'AND');
903
        } else {
904
            $WHERE = '';
905
        }
906
907
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
908
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
909
        }
910 12
911
        $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
912 12
913
        $return = $this->query($sql);
914
915
        \assert(\is_int($return) || $return === false);
916
917
        return $return;
918 12
    }
919 3
920
    /**
921 9
     * Ends a transaction and commits if no errors, then ends autocommit.
922 9
     *
923
     * @return bool
924
     *              <p>This will return true or false indicating success of transactions.</p>
925 12
     */
926 12
    public function endTransaction(): bool
927
    {
928
        if (!$this->in_transaction) {
929
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
930
931
            return false;
932
        }
933
934
        if (!$this->errors()) {
935
            $return = $this->commit();
936
        } else {
937 12
            $this->rollback();
938
            $return = false;
939 12
        }
940
941
        if ($this->mysqli_link) {
942
            \mysqli_autocommit($this->mysqli_link, true);
943
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
944
            $this->doctrine_connection->setAutoCommit(true);
945
946
            if ($this->doctrine_connection->isAutoCommit()) {
947 12
                $return = true;
948
            } else {
949 12
                $return = false;
950
            }
951 12
        }
952
953
        $this->in_transaction = false;
954
955
        return $return;
956
    }
957
958
    /**
959
     * Get all errors from "$this->errors".
960
     *
961
     * @return array|false
962
     *                     <p>false === on errors</p>
963
     */
964
    public function errors()
965
    {
966
        $errors = $this->debug->getErrors();
967
968
        return \count($errors) > 0 ? $errors : false;
969
    }
970
971 110
    /**
972
     * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
973
     *
974 110
     * @param mixed     $var           bool: convert into "integer"<br />
975 6
     *                                 int: int (don't change it)<br />
976
     *                                 float: float (don't change it)<br />
977
     *                                 null: null (don't change it)<br />
978
     *                                 array: run escape() for every key => value<br />
979 110
     *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
980
     * @param bool      $stripe_non_utf8
981
     * @param bool      $html_entity_decode
982
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
983
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
984 110
     *                                 <strong>null</strong> => Convert the array into null, every time.
985
     *
986 110
     * @return mixed
987 9
     */
988 9
    public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
989 9
    {
990 6
        // [empty]
991 6
        if ($var === '') {
992 6
            return '';
993
        }
994
995
        // ''
996
        if ($var === "''") {
997 110
            return "''";
998 9
        }
999
1000 9
        // check the type
1001
        $type = \gettype($var);
1002 110
1003 110
        if ($type === 'object') {
1004 65
            if ($var instanceof \DateTimeInterface) {
1005
                $var = $var->format('Y-m-d H:i:s');
1006 107
                $type = 'string';
1007 107
            } elseif (\method_exists($var, '__toString')) {
1008 23
                $var = (string) $var;
1009
                $type = 'string';
1010
            }
1011 107
        }
1012 3
1013
        switch ($type) {
1014
            case 'boolean':
1015 107
                $var = (int) $var;
1016
1017
                break;
1018 107
1019
            case 'double':
1020 107
            case 'integer':
1021
                break;
1022 107
1023
            case 'string':
1024
                if ($stripe_non_utf8) {
1025
                    $var = UTF8::cleanup($var);
1026
                }
1027
1028 107
                if ($html_entity_decode) {
1029
                    $var = UTF8::html_entity_decode($var);
1030 9
                }
1031 6
1032 3
                $var = \get_magic_quotes_gpc() ? \stripslashes($var) : $var;
1033
1034
                if (
1035 3
                    $this->mysqli_link
1036
                    &&
1037
                    $this->mysqli_link instanceof \mysqli
1038 6
                ) {
1039 6
                    $var = \mysqli_real_escape_string($this->mysqli_link, $var);
1040 6
                } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1041 6
                    $pdoConnection = $this->getDoctrinePDOConnection();
1042
                    \assert($pdoConnection !== false);
1043
                    $var = $pdoConnection->quote($var);
1044 6
                    $var = \substr($var, 1, -1);
1045
                }
1046
1047 6
                break;
1048 3
1049
            case 'array':
1050 3
                if ($convert_array === null) {
1051
                    if ($this->convert_null_to_empty_string) {
1052 6
                        $var = "''";
1053
                    } else {
1054
                        $var = 'NULL';
1055
                    }
1056 6
                } else {
1057
                    $varCleaned = [];
1058 9
                    foreach ((array) $var as $key => $value) {
1059 6
                        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
1060
                        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
1061
1062 6
                        /** @noinspection OffsetOperationsInspection */
1063
                        $varCleaned[$key] = $value;
1064
                    }
1065 6
1066 View Code Duplication
                    if ($convert_array === true) {
1067
                        $varCleaned = \implode(',', $varCleaned);
1068 6
1069
                        $var = $varCleaned;
1070
                    } else {
1071
                        $var = $varCleaned;
1072
                    }
1073 110
                }
1074
1075
                break;
1076
1077
            case 'NULL':
1078
                if ($this->convert_null_to_empty_string) {
1079
                    $var = "''";
1080
                } else {
1081
                    $var = 'NULL';
1082
                }
1083
1084
                break;
1085
1086
            default:
1087
                throw new \InvalidArgumentException(\sprintf('Not supported value "%s" of type %s.', \print_r($var, true), $type));
1088
1089
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be 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...
1090
        }
1091
1092 9
        return $var;
1093
    }
1094
1095 9
    /**
1096 9
     * Execute select/insert/update/delete sql-queries.
1097 9
     *
1098
     * @param string  $query    <p>sql-query</p>
1099
     * @param bool    $useCache optional <p>use cache?</p>
1100 9
     * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
1101 3
     * @param DB|null $db       optional <p>the database connection</p>
1102 3
     *
1103
     *@throws QueryException
1104
     *
1105 3
     * @return mixed
1106
     *               <ul>
1107 3
     *               <li>"array" by "<b>SELECT</b>"-queries</li>
1108
     *               <li>"int|string" (insert_id) by "<b>INSERT</b>"-queries</li>
1109 3
     *               <li>"int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1110
     *               <li>"true" by e.g. "DROP"-queries</li>
1111
     *               <li>"false" on error</li>
1112 9
     *               </ul>
1113
     */
1114
    public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1115 9
    {
1116
        // init
1117 9
        $cacheKey = null;
1118 3
        if (!$db) {
1119
            $db = self::getInstance();
1120
        }
1121
1122 3 View Code Duplication
        if ($useCache) {
1123
            $cache = new Cache(null, null, false, $useCache);
1124 3
            $cacheKey = 'sql-' . \md5($query);
1125
1126 3
            if (
1127
                $cache->getCacheIsReady()
1128 3
                &&
1129
                $cache->existsItem($cacheKey)
1130 3
            ) {
1131
                return $cache->getItem($cacheKey);
1132
            }
1133 6
        } else {
1134
            $cache = false;
1135
        }
1136 9
1137
        $result = $db->query($query);
1138
1139
        if ($result instanceof Result) {
1140
            $return = $result->fetchAllArray();
1141
1142
            // save into the cache
1143 View Code Duplication
            if (
1144 3
                $cacheKey !== null
1145
                &&
1146 3
                $useCache
1147 3
                &&
1148
                $cache instanceof Cache
1149 3
                &&
1150
                $cache->getCacheIsReady()
1151
            ) {
1152
                $cache->setItem($cacheKey, $return, $cacheTTL);
1153
            }
1154
        } else {
1155 8
            $return = $result;
1156
        }
1157
1158 8
        return $return;
1159 8
    }
1160 8
1161 8
    /**
1162 8
     * Get all table-names via "SHOW TABLES".
1163 8
     *
1164 8
     * @return array
1165 8
     */
1166 8
    public function getAllTables(): array
1167 8
    {
1168
        $query = 'SHOW TABLES';
1169
        $result = $this->query($query);
1170 8
1171
        \assert($result instanceof Result);
1172
1173
        return $result->fetchAllArray();
1174 8
    }
1175
1176
    /**
1177
     * @return array
1178
     */
1179
    public function getConfig(): array
1180 9
    {
1181
        $config = [
1182 9
            'hostname'   => $this->hostname,
1183
            'username'   => $this->username,
1184
            'password'   => $this->password,
1185
            'port'       => $this->port,
1186
            'database'   => $this->database,
1187
            'socket'     => $this->socket,
1188 2
            'charset'    => $this->charset,
1189
            'cacert'     => $this->cacert,
1190 2
            'clientcert' => $this->clientcert,
1191
            'clientkey'  => $this->clientkey,
1192
        ];
1193
1194
        if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1195
            $config += $this->doctrine_connection->getParams();
1196
        }
1197
1198
        return $config;
1199
    }
1200
1201
    /**
1202
     * @return Debug
1203
     */
1204
    public function getDebugger(): Debug
1205
    {
1206
        return $this->debug;
1207
    }
1208
1209
    /**
1210
     * @return \Doctrine\DBAL\Connection|null|null
1211
     */
1212
    public function getDoctrineConnection()
1213 3
    {
1214
        return $this->doctrine_connection;
1215 3
    }
1216
1217
    /**
1218
     * @return \Doctrine\DBAL\Driver\Connection|false
1219
     */
1220 View Code Duplication
    private function getDoctrinePDOConnection()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1221
    {
1222
        if ($this->doctrine_connection) {
1223
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1224
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1225
                return $doctrineWrappedConnection;
1226
            }
1227
        }
1228
1229
        return false;
1230
    }
1231
1232
    /**
1233
     * Get errors from "$this->errors".
1234
     *
1235
     * @return array
1236
     */
1237
    public function getErrors(): array
1238
    {
1239
        return $this->debug->getErrors();
1240
    }
1241
1242
    /**
1243
     * @param string $hostname             <p>Hostname of the mysql server</p>
1244 206
     * @param string $username             <p>Username for the mysql connection</p>
1245
     * @param string $password             <p>Password for the mysql connection</p>
1246
     * @param string $database             <p>Database for the mysql connection</p>
1247
     * @param int    $port                 <p>default is (int)3306</p>
1248
     * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1249
     * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1250
     *                                     Use false to disable it.</p>
1251
     * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1252
     *                                     Use false to disable it.</p>
1253
     * @param string $logger_class_name
1254
     * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1255
     * @param array  $extra_config         <p>
1256
     *                                     're_connect'    => bool<br>
1257
     *                                     'session_to_db' => bool<br>
1258
     *                                     'doctrine'      => \Doctrine\DBAL\Connection<br>
1259
     *                                     'socket'        => 'string (path)'<br>
1260 206
     *                                     'ssl'           => bool<br>
1261
     *                                     'clientkey'     => 'string (path)'<br>
1262
     *                                     'clientcert'    => 'string (path)'<br>
1263
     *                                     'cacert'        => 'string (path)'<br>
1264
     *                                     </p>
1265 206
     *
1266
     * @return self
1267
     */
1268 206
    public static function getInstance(
1269 121
        string $hostname = '',
1270
        string $username = '',
1271
        string $password = '',
1272
        string $database = '',
1273 206
        $port = 3306,
1274
        string $charset = 'utf8',
1275 206
        bool $exit_on_error = true,
1276
        bool $echo_on_error = true,
1277 123
        string $logger_class_name = '',
1278
        string $logger_level = '',
1279
        array $extra_config = []
1280
    ): self {
1281 123
        /**
1282
         * @var self[]
1283
         */
1284 125
        static $instance = [];
1285 125
1286
        /**
1287
         * @var self|null
1288
         */
1289
        static $firstInstance = null;
1290
1291
        // fallback
1292
        if (!$charset) {
1293
            $charset = 'utf8';
1294 125
        }
1295 125
1296
        if (
1297
            '' . $hostname . $username . $password . $database . $port . $charset === '' . $port . $charset
1298 125
            &&
1299 23
            $firstInstance instanceof self
1300 23
        ) {
1301 23
            if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1302 23
                $firstInstance->reconnect(true);
1303 23
            }
1304 23
1305 23
            return $firstInstance;
1306 23
        }
1307 23
1308 23
        $extra_config_string = '';
1309 23
        foreach ($extra_config as $extra_config_key => $extra_config_value) {
1310 23
            if (\is_object($extra_config_value)) {
1311
                $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1312
            } else {
1313 5
                $extra_config_value_tmp = (string) $extra_config_value;
1314 1
            }
1315
            $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1316
        }
1317
1318 113
        $connection = \md5(
1319
            $hostname . $username . $password . $database . $port . $charset . (int) $exit_on_error . (int) $echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1320
        );
1321
1322 113
        if (!isset($instance[$connection])) {
1323
            $instance[$connection] = new self(
1324
                $hostname,
1325
                $username,
1326
                $password,
1327
                $database,
1328
                $port,
1329
                $charset,
1330
                $exit_on_error,
1331
                $echo_on_error,
1332
                $logger_class_name,
1333
                $logger_level,
1334
                $extra_config
1335
            );
1336
1337
            if ($firstInstance === null) {
1338
                $firstInstance = $instance[$connection];
1339
            }
1340
        }
1341
1342
        if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1343
            $instance[$connection]->reconnect(true);
1344
        }
1345
1346
        return $instance[$connection];
1347 55
    }
1348
1349
    /**
1350
     * @param \Doctrine\DBAL\Connection $doctrine
1351
     * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1352
     * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1353
     *                                                 return 'false'. Use false to disable it.</p>
1354
     * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1355
     *                                                 Use false to disable it.</p>
1356 55
     * @param string                    $logger_class_name
1357
     * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1358 55
     * @param array                     $extra_config  <p>
1359 55
     *                                                 're_connect'    => bool<br>
1360 55
     *                                                 'session_to_db' => bool<br>
1361 55
     *                                                 'doctrine'      => \Doctrine\DBAL\Connection<br>
1362 55
     *                                                 'socket'        => 'string (path)'<br>
1363 55
     *                                                 'ssl'           => bool<br>
1364 55
     *                                                 'clientkey'     => 'string (path)'<br>
1365 55
     *                                                 'clientcert'    => 'string (path)'<br>
1366 55
     *                                                 'cacert'        => 'string (path)'<br>
1367 55
     *                                                 </p>
1368 55
     *
1369 55
     * @return self
1370
     */
1371
    public static function getInstanceDoctrineHelper(
1372
        \Doctrine\DBAL\Connection $doctrine,
1373
        string $charset = 'utf8',
1374
        bool $exit_on_error = true,
1375
        bool $echo_on_error = true,
1376
        string $logger_class_name = '',
1377
        string $logger_level = '',
1378 52
        array $extra_config = []
1379
    ): self {
1380 52
        $extra_config['doctrine'] = $doctrine;
1381
1382
        return self::getInstance(
1383
            '',
1384
            '',
1385
            '',
1386
            '',
1387
            3306,
1388 3
            $charset,
1389
            $exit_on_error,
1390 3
            $echo_on_error,
1391
            $logger_class_name,
1392
            $logger_level,
1393
            $extra_config
1394
        );
1395
    }
1396
1397
    /**
1398
     * Get the mysqli-link (link identifier returned by mysqli-connect).
1399
     *
1400
     * @return \mysqli|null
1401
     */
1402
    public function getLink()
1403
    {
1404
        return $this->mysqli_link;
1405
    }
1406
1407
    /**
1408
     * Get the current charset.
1409
     *
1410
     * @return string
1411
     */
1412
    public function get_charset(): string
1413
    {
1414 74
        return $this->charset;
1415
    }
1416
1417 74
    /**
1418
     * Check if we are in a transaction.
1419 74
     *
1420 6
     * @return bool
1421
     */
1422 6
    public function inTransaction(): bool
1423
    {
1424
        return $this->in_transaction;
1425 71
    }
1426 9
1427
    /**
1428 9
     * Execute a "insert"-query.
1429
     *
1430
     * @param string      $table
1431 65
     * @param array       $data
1432
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1433 65
     *
1434
     * @throws QueryException
1435
     *
1436
     * @return false|int|string
1437 65
     *                   <p>false on error</p>
1438
     */
1439 65
    public function insert(string $table, array $data = [], string $databaseName = null)
1440
    {
1441
        // init
1442
        $table = \trim($table);
1443
1444
        if ($table === '') {
1445
            $this->debug->displayError('Invalid table name, table name in empty.', false);
1446
1447 100
            return false;
1448
        }
1449 100
1450 100
        if (\count($data) === 0) {
1451
            $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1452
1453
            return false;
1454
        }
1455
1456
        $SET = $this->_parseArrayPair($data);
1457
1458
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1459
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1460
        }
1461
1462
        $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET ${SET}";
1463
1464
        $return = $this->query($sql);
1465
        if ($return === false) {
1466
            return false;
1467
        }
1468
1469
        \assert(\is_int($return) || \is_string($return));
1470
1471
        return $return;
1472
    }
1473
1474
    /**
1475
     * Returns the auto generated id used in the last query.
1476 9
     *
1477
     * @return int|string|false
1478 9
     */
1479
    public function insert_id()
1480
    {
1481
        if ($this->mysqli_link) {
1482
            return \mysqli_insert_id($this->mysqli_link);
1483
        }
1484
1485 9
        $doctrinePDOConnection = $this->getDoctrinePDOConnection();
1486
        if ($doctrinePDOConnection) {
1487
            return $doctrinePDOConnection->lastInsertId();
1488
        }
1489
1490
        return false;
1491
    }
1492
1493 160
    /**
1494
     * @return bool
1495 160
     */
1496
    public function isDoctrineMySQLiConnection(): bool
1497
    {
1498
        if ($this->doctrine_connection) {
1499
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1500
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1501
                return true;
1502
            }
1503 3
        }
1504
1505 3
        return false;
1506
    }
1507 3
1508
    /**
1509
     * @return bool
1510
     */
1511 View Code Duplication
    public function isDoctrinePDOConnection(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1512
    {
1513
        if ($this->doctrine_connection) {
1514
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1515
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1516
                return true;
1517
            }
1518
        }
1519
1520
        return false;
1521
    }
1522 3
1523
    /**
1524 3
     * Check if db-connection is ready.
1525
     *
1526
     * @return bool
1527
     */
1528 3
    public function isReady(): bool
1529 3
    {
1530
        return $this->connected ? true : false;
1531 3
    }
1532
1533
    /**
1534 3
     * Get the last sql-error.
1535
     *
1536
     * @return false|string
1537
     *                      <p>false === there was no error</p>
1538
     */
1539
    public function lastError()
1540
    {
1541
        $errors = $this->debug->getErrors();
1542
1543
        return \count($errors) > 0 ? \end($errors) : false;
1544
    }
1545
1546
    /**
1547
     * Execute a sql-multi-query.
1548
     *
1549
     * @param string $sql
1550
     *
1551
     *@throws QueryException
1552
     *
1553
     * @return bool|Result[]
1554
     *                        <ul>
1555
     *                        <li>"Result"-Array by "<b>SELECT</b>"-queries</li>
1556
     *                        <li>"bool" by only "<b>INSERT</b>"-queries</li>
1557
     *                        <li>"bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1558
     *                        <li>"bool" by only by e.g. "DROP"-queries</li>
1559
     *                        </ul>
1560
     */
1561
    public function multi_query(string $sql)
1562
    {
1563
        if (!$this->isReady()) {
1564
            return false;
1565
        }
1566
1567
        if (!$sql || $sql === '') {
1568
            $this->debug->displayError('Can not execute an empty query.', false);
1569
1570
            return false;
1571
        }
1572
1573
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1574
            $query_start_time = \microtime(true);
1575
            $queryException = null;
1576
            $query_result_doctrine = false;
1577
1578
            try {
1579
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1580
                $resultTmp = $query_result_doctrine->execute();
1581
                $mysqli_field_count = $query_result_doctrine->columnCount();
1582
            } catch (\Exception $e) {
1583
                $resultTmp = false;
1584
                $mysqli_field_count = null;
1585
1586
                $queryException = $e;
1587
            }
1588
1589
            $query_duration = \microtime(true) - $query_start_time;
1590
1591
            $this->debug->logQuery($sql, $query_duration, 0);
1592
1593
            $returnTheResult = false;
1594
            $result = [];
1595
1596 3
            if ($resultTmp) {
1597 3
                if ($mysqli_field_count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mysqli_field_count of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1598 3
                    if (
1599
                        $query_result_doctrine
1600 3
                        &&
1601
                        $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1602 3
                    ) {
1603 3
                        $result = $query_result_doctrine;
1604
                    }
1605 3
                } else {
1606
                    $result = $resultTmp;
1607 3
                }
1608
1609 3
                if (
1610 3
                    $result instanceof \Doctrine\DBAL\Statement
1611 3
                    &&
1612
                    $result->columnCount() > 0
1613 3
                ) {
1614
                    $returnTheResult = true;
1615 3
1616
                    // return query result object
1617 3
                    $result = [new Result($sql, $result)];
1618
                } else {
1619
                    $result = [$result];
1620
                }
1621 3
            } else {
1622
1623
                // log the error query
1624
                $this->debug->logQuery($sql, $query_duration, 0, true);
1625 3
1626
                if (
1627 3
                    isset($queryException)
1628
                    &&
1629
                    $queryException instanceof \Doctrine\DBAL\Query\QueryException
1630
                ) {
1631
                    return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1632 3
                }
1633 3
            }
1634
        } elseif ($this->mysqli_link) {
1635
            $query_start_time = \microtime(true);
1636
            $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1637 3
            $query_duration = \microtime(true) - $query_start_time;
1638
1639 3
            $this->debug->logQuery($sql, $query_duration, 0);
1640
1641 3
            $returnTheResult = false;
1642
            $result = [];
1643
1644
            if ($resultTmp) {
1645
                do {
1646
                    $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1647
1648
                    if ($resultTmpInner instanceof \mysqli_result) {
1649
                        $returnTheResult = true;
1650
                        $result[] = new Result($sql, $resultTmpInner);
1651
                    } elseif (\mysqli_errno($this->mysqli_link)) {
1652
                        $result[] = false;
1653
                    } else {
1654 3
                        $result[] = true;
1655
                    }
1656 3
                } while (\mysqli_more_results($this->mysqli_link) ? \mysqli_next_result($this->mysqli_link) : false);
1657
            } else {
1658
1659 3
                // log the error query
1660
                $this->debug->logQuery($sql, $query_duration, 0, true);
1661 3
1662
                return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1663
            }
1664
        } else {
1665
1666 3
            // log the error query
1667
            $this->debug->logQuery($sql, 0, 0, true);
1668
1669
            return $this->queryErrorHandling('no database connection', 1, $sql, false, true);
1670
        }
1671
1672
        // return the result only if there was a "SELECT"-query
1673
        if ($returnTheResult) {
1674
            return $result;
1675 9
        }
1676
1677 9
        if (
1678 3
            \count($result) > 0
1679
            &&
1680
            !\in_array(false, $result, true)
1681 6
        ) {
1682
            return true;
1683
        }
1684
1685
        return false;
1686 6
    }
1687
1688 6
    /**
1689
     * Count number of rows found matching a specific query.
1690 6
     *
1691
     * @param string $query
1692
     *
1693
     * @return int
1694
     */
1695
    public function num_rows(string $query): int
1696
    {
1697
        $check = $this->query($query);
1698
1699
        if (
1700
            $check === false
1701
            ||
1702
            !$check instanceof Result
1703 2
        ) {
1704
            return 0;
1705 2
        }
1706
1707
        return $check->num_rows;
1708
    }
1709
1710
    /**
1711
     * Pings a server connection, or tries to reconnect
1712
     * if the connection has gone down.
1713
     *
1714
     * @return bool
1715
     */
1716
    public function ping(): bool
1717
    {
1718
        if (!$this->connected) {
1719 3
            return false;
1720
        }
1721 3
1722
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1723 3
            return $this->doctrine_connection->ping();
1724
        }
1725 3
1726 3
        if (
1727 3
            $this->mysqli_link
1728
            &&
1729 3
            $this->mysqli_link instanceof \mysqli
1730 3
        ) {
1731
            return \mysqli_ping($this->mysqli_link);
1732 3
        }
1733
1734 3
        return false;
1735 3
    }
1736 3
1737
    /**
1738 3
     * Get a new "Prepare"-Object for your sql-query.
1739 3
     *
1740
     * @param string $query
1741
     *
1742 3
     * @return Prepare
1743
     */
1744
    public function prepare(string $query): Prepare
1745
    {
1746
        return new Prepare($this, $query);
1747
    }
1748
1749
    /**
1750
     * Execute a sql-query and return the result-array for select-statements.
1751
     *
1752
     * @param string $query
1753
     *
1754
     * @throws \Exception
1755
     *
1756
     * @return mixed
1757
     *
1758
     * @deprecated
1759
     */
1760
    public static function qry(string $query)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1761
    {
1762
        $db = self::getInstance();
1763
1764
        $args = \func_get_args();
1765
        /** @noinspection SuspiciousAssignmentsInspection */
1766
        $query = \array_shift($args);
1767
        $query = \str_replace('?', '%s', $query);
1768
        $args = \array_map(
1769
            [
1770
                $db,
1771
                'escape',
1772
            ],
1773
            $args
1774
        );
1775
        \array_unshift($args, $query);
1776
        $query = \sprintf(...$args);
1777
        $result = $db->query($query);
1778
1779
        if ($result instanceof Result) {
1780 140
            return $result->fetchAllArray();
1781
        }
1782 140
1783
        return $result;
1784
    }
1785
1786 140
    /**
1787 12
     * Execute a sql-query.
1788
     *
1789 12
     * example:
1790
     * <code>
1791
     * $sql = "INSERT INTO TABLE_NAME_HERE
1792
     *   SET
1793 134
     *     foo = :foo,
1794
     *     bar = :bar
1795 134
     * ";
1796
     * $insert_id = $db->query(
1797 134
     *   $sql,
1798
     *   [
1799 7
     *     'foo' => 1.1,
1800 7
     *     'bar' => 1,
1801 7
     *   ]
1802
     * );
1803
     * </code>
1804
     *
1805
     * @param string     $sql               <p>The sql query-string.</p>
1806
     * @param array|bool $params            <p>
1807
     *                                      "array" of sql-query-parameters<br/>
1808 134
     *                                      "false" if you don't need any parameter (default)<br/>
1809 134
     *                                      </p>
1810 134
     *
1811
     *@throws QueryException
1812 134
     *
1813
     * @return bool|int|Result|string
1814
     *                                      <p>
1815
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
1816
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1817
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1818
     *                                      "true" by e.g. "DROP"-queries<br />
1819
     *                                      "false" on error
1820
     *                                      </p>
1821
     */
1822
    public function query(string $sql = '', $params = false)
1823
    {
1824 134
        if (!$this->isReady()) {
1825 134
            return false;
1826
        }
1827
1828 134
        if ($sql === '') {
1829
            $this->debug->displayError('Can not execute an empty query.', false);
1830 134
1831
            return false;
1832 134
        }
1833 99
1834
        if (
1835
            $params !== false
1836
            &&
1837
            \is_array($params)
1838
            &&
1839
            \count($params) > 0
1840
        ) {
1841
            $parseQueryParams = $this->_parseQueryParams($sql, $params);
1842
            $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1843 99
            $sql = $parseQueryParamsByName['sql'];
1844
        }
1845
1846 106
        // DEBUG
1847
        // var_dump($params);
1848
        // echo $sql . "\n";
1849
1850 134
        $query_start_time = \microtime(true);
1851
        $queryException = null;
1852 134
        $query_result_doctrine = false;
1853
1854
        if ($this->doctrine_connection) {
1855
            try {
1856
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1857
                $query_result = $query_result_doctrine->execute();
1858
                $mysqli_field_count = $query_result_doctrine->columnCount();
1859
            } catch (\Exception $e) {
1860
                $query_result = false;
1861
                $mysqli_field_count = null;
1862 134
1863
                $queryException = $e;
1864
            }
1865 96
        } elseif ($this->mysqli_link) {
1866
            $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1867
            $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1868 96
        } else {
1869
            $query_result = false;
1870
            $mysqli_field_count = null;
1871 112
1872
            $queryException = new DBConnectException('no mysql connection');
1873
        }
1874 103
1875 100
        $query_duration = \microtime(true) - $query_start_time;
1876
1877 100
        $this->query_count++;
1878
1879 100
        if ($mysqli_field_count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mysqli_field_count of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1880
            if ($this->doctrine_connection) {
1881
                $result = false;
1882
                if (
1883 50
                    $query_result_doctrine
1884 25
                    &&
1885 25
                    $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1886
                ) {
1887
                    $result = $query_result_doctrine;
1888
                }
1889
            } elseif ($this->mysqli_link) {
1890 25
                $result = \mysqli_store_result($this->mysqli_link);
1891
            } else {
1892 25
                $result = false;
1893
            }
1894
        } else {
1895
            $result = $query_result;
1896 25
        }
1897
1898 25
        if (
1899
            $result instanceof \Doctrine\DBAL\Statement
1900
            &&
1901
            $result->columnCount() > 0
1902 33
        ) {
1903
1904 33
            // log the select query
1905
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1906
1907
            // return query result object
1908 33
            return new Result($sql, $result);
1909 33
        }
1910
1911
        if ($result instanceof \mysqli_result) {
1912
1913
            // log the select query
1914
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1915
1916
            // return query result object
1917
            return new Result($sql, $result);
1918
        }
1919
1920
        if ($query_result) {
1921
1922
            // "INSERT" || "REPLACE"
1923
            if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1924
                $insert_id = $this->insert_id();
1925
1926
                $this->debug->logQuery($sql, $query_duration, $insert_id);
1927
1928
                return $insert_id;
1929 39
            }
1930
1931
            // "UPDATE" || "DELETE"
1932 39
            if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1933
                if ($this->mysqli_link) {
1934 36
                    $this->affected_rows = $this->affected_rows();
1935
                } elseif ($query_result_doctrine) {
1936 39
                    $this->affected_rows = $query_result_doctrine->rowCount();
1937
                }
1938 3
1939
                $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1940
1941 3
                return $this->affected_rows;
1942
            }
1943
1944
            // log the ? query
1945
            $this->debug->logQuery($sql, $query_duration, 0);
1946
1947 3
            return true;
1948
        }
1949
1950 3
        // log the error query
1951 3
        $this->debug->logQuery($sql, $query_duration, 0, true);
1952
1953
        if ($queryException) {
1954 3
            return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1955 3
        }
1956
1957
        if ($this->mysqli_link) {
1958
            return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
1959
        }
1960
1961 36
        return false;
1962
    }
1963 36
1964 36
    /**
1965 12
     * Error-handling for the sql-query.
1966
     *
1967
     * @param string     $errorMessage
1968
     * @param int        $errorNumber
1969 36
     * @param string     $sql
1970
     * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1971 36
     * @param bool       $sqlMultiQuery
1972
     *
1973
     * @throws DBGoneAwayException
1974
     * @throws QueryException
1975
     *
1976
     * @return false|mixed
1977
     */
1978
    private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1979
    {
1980
        if (
1981 86
            $errorMessage === 'DB server has gone away'
1982
            ||
1983 86
            $errorMessage === 'MySQL server has gone away'
1984 86
            ||
1985 86
            $errorNumber === 2006
1986 86
        ) {
1987 86
            static $RECONNECT_COUNTER;
1988 86
1989
            // exit if we have more then 3 "DB server has gone away"-errors
1990
            if ($RECONNECT_COUNTER > 3) {
1991
                $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1992 86
1993
                throw new DBGoneAwayException($errorMessage);
1994
            }
1995
1996
            $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1997
1998
            // reconnect
1999
            $RECONNECT_COUNTER++;
2000
            $this->reconnect(true);
2001
2002 7
            // re-run the current (non multi) query
2003
            if (!$sqlMultiQuery) {
2004 7
                return $this->query($sql, $sqlParams);
2005 7
            }
2006 6
2007
            return false;
2008
        }
2009 7
2010 7
        $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2011 7
2012
        $force_exception_after_error = null; // auto
2013
        if ($this->in_transaction) {
2014 7
            $force_exception_after_error = false;
2015
        }
2016
        // this query returned an error, we must display it (only for dev) !!!
2017
2018
        $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2019
2020
        return false;
2021
    }
2022
2023
    /**
2024
     * Quote && Escape e.g. a table name string.
2025
     *
2026
     * @param mixed $str
2027
     *
2028 3
     * @return string
2029
     */
2030
    public function quote_string($str): string
2031 3
    {
2032
        $str = \str_replace(
2033 3
            '`',
2034 3
            '``',
2035
            \trim(
2036 3
                (string) $this->escape($str, false),
2037
                '`'
2038
            )
2039 3
        );
2040 3
2041
        return '`' . $str . '`';
2042 3
    }
2043
2044
    /**
2045
     * Reconnect to the MySQL-Server.
2046 3
     *
2047 3
     * @param bool $checkViaPing
2048
     *
2049 3
     * @return bool
2050
     */
2051
    public function reconnect(bool $checkViaPing = false): bool
2052 3
    {
2053
        $ping = false;
2054
        if ($checkViaPing) {
2055 3
            $ping = $this->ping();
2056
        }
2057 3
2058
        if (!$ping) {
2059 3
            $this->connected = false;
2060
            $this->connect();
2061 3
        }
2062
2063
        return $this->isReady();
2064
    }
2065 3
2066
    /**
2067 3
     * Execute a "replace"-query.
2068
     *
2069
     * @param string      $table
2070
     * @param array       $data
2071
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2072
     *
2073
     * @throws QueryException
2074
     *
2075 12
     * @return false|int
2076
     *                   <p>false on error</p>
2077 12
     */
2078
    public function replace(string $table, array $data = [], string $databaseName = null)
2079
    {
2080
        // init
2081
        $table = \trim($table);
2082
2083
        if ($table === '') {
2084 12
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2085
2086 12
            return false;
2087 12
        }
2088 12
2089
        if (\count($data) === 0) {
2090
            $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2091
2092
            return false;
2093
        }
2094
2095
        // extracting column names
2096
        $columns = \array_keys($data);
2097
        foreach ($columns as $k => $_key) {
2098
            /** @noinspection AlterInForeachInspection */
2099
            $columns[$k] = $this->quote_string($_key);
2100 12
        }
2101
2102 12
        $columns = \implode(',', $columns);
2103
2104
        // extracting values
2105
        foreach ($data as $k => $_value) {
2106
            /** @noinspection AlterInForeachInspection */
2107
            $data[$k] = $this->secure($_value);
2108
        }
2109
        $values = \implode(',', $data);
2110
2111
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2112
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2113
        }
2114
2115
        $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " (${columns}) VALUES (${values})";
2116
2117
        $return = $this->query($sql);
2118
        \assert(\is_int($return) || $return === false);
2119
2120
        return $return;
2121
    }
2122
2123
    /**
2124
     * Rollback in a transaction and end the transaction.
2125
     *
2126
     * @return bool
2127
     *              <p>bool true on success, false otherwise.</p>
2128
     */
2129 View Code Duplication
    public function rollback(): bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2130
    {
2131
        if (!$this->in_transaction) {
2132
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
2133
2134
            return false;
2135
        }
2136
2137
        // init
2138
        $return = false;
2139
2140
        if ($this->mysqli_link) {
2141
            $return = \mysqli_rollback($this->mysqli_link);
2142
            \mysqli_autocommit($this->mysqli_link, true);
2143
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2144
            $this->doctrine_connection->rollBack();
2145
            $this->doctrine_connection->setAutoCommit(true);
2146
2147
            if ($this->doctrine_connection->isAutoCommit()) {
2148
                $return = true;
2149 87
            } else {
2150
                $return = false;
2151 87
            }
2152 6
        }
2153
2154
        $this->in_transaction = false;
2155
2156
        return $return;
2157
    }
2158
2159 6
    /**
2160 6
     * Try to secure a variable, so can you use it in sql-queries.
2161 6
     *
2162 6
     * <p>
2163
     * <strong>int:</strong> (also strings that contains only an int-value)<br />
2164
     * 1. parse into "int"
2165 6
     * </p><br />
2166
     *
2167
     * <p>
2168 6
     * <strong>float:</strong><br />
2169 6
     * 1. return "float"
2170
     * </p><br />
2171 6
     *
2172
     * <p>
2173
     * <strong>string:</strong><br />
2174
     * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2175
     * 2. trim '<br />
2176
     * 3. escape the string (and remove non utf-8 chars)<br />
2177 6
     * 4. trim ' again (because we maybe removed some chars)<br />
2178
     * 5. add ' around the new string<br />
2179
     * </p><br />
2180 87
     *
2181 6
     * <p>
2182
     * <strong>array:</strong><br />
2183
     * 1. return null
2184 87
     * </p><br />
2185 3
     *
2186
     * <p>
2187
     * <strong>object:</strong><br />
2188 87
     * 1. return false
2189 3
     * </p><br />
2190 3
     *
2191
     * <p>
2192
     * <strong>null:</strong><br />
2193 3
     * 1. return null
2194
     * </p>
2195
     *
2196 87
     * @param mixed     $var
2197 3
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2198
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2199
     *                                 <strong>null</strong> => Convert the array into null, every time.
2200 87
     *
2201 77
     * @return mixed
2202
     */
2203
    public function secure($var, $convert_array = true)
2204 87
    {
2205
        if (\is_array($var)) {
2206 84
            if ($convert_array === null) {
2207 77
                if ($this->convert_null_to_empty_string) {
2208
                    $var = "''";
2209
                } else {
2210 84
                    $var = 'NULL';
2211
                }
2212
            } else {
2213
                $varCleaned = [];
2214
                foreach ((array) $var as $key => $value) {
2215
                    $key = $this->escape($key, false, false, $convert_array);
2216
                    $value = $this->secure($value);
2217
2218
                    /** @noinspection OffsetOperationsInspection */
2219
                    $varCleaned[$key] = $value;
2220
                }
2221
2222 View Code Duplication
                if ($convert_array === true) {
2223
                    $varCleaned = \implode(',', $varCleaned);
2224 62
2225
                    $var = $varCleaned;
2226
                } else {
2227 62
                    $var = $varCleaned;
2228
                }
2229 62
            }
2230 3
2231
            return $var;
2232 3
        }
2233
2234
        if ($var === '') {
2235 62
            return "''";
2236 25
        }
2237 41
2238 41
        if ($var === "''") {
2239
            return "''";
2240 3
        }
2241
2242
        if ($var === null) {
2243 62
            if ($this->convert_null_to_empty_string) {
2244
                return "''";
2245
            }
2246
2247 62
            return 'NULL';
2248
        }
2249 62
2250
        if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2251
            return $var;
2252
        }
2253
2254
        if (\is_string($var)) {
2255
            $var = \trim($var, "'");
2256
        }
2257
2258
        $var = $this->escape($var, false, false, null);
2259
2260
        if (\is_string($var)) {
2261
            $var = "'" . \trim($var, "'") . "'";
2262
        }
2263
2264
        return $var;
2265
    }
2266
2267
    /**
2268
     * Execute a "select"-query.
2269
     *
2270
     * @param string       $table
2271
     * @param array|string $where
2272
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2273
     *
2274
     * @throws QueryException
2275
     *
2276
     * @return false|Result
2277
     *                      <p>false on error</p>
2278
     */
2279 View Code Duplication
    public function select(string $table, $where = '1=1', string $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2280
    {
2281
        // init
2282
        $table = \trim($table);
2283
2284
        if ($table === '') {
2285
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2286 23
2287
            return false;
2288 23
        }
2289
2290
        if (\is_string($where)) {
2291
            $WHERE = $this->escape($where, false);
2292 23
        } elseif (\is_array($where)) {
2293
            $WHERE = $this->_parseArrayPair($where, 'AND');
2294
        } else {
2295
            $WHERE = '';
2296
        }
2297
2298
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2299
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2300 23
        }
2301
2302
        $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
2303
2304 23
        $return = $this->query($sql);
2305
        \assert($return instanceof Result || $return === false);
2306
2307
        return $return;
2308 23
    }
2309
2310
    /**
2311
     * Selects a different database than the one specified on construction.
2312 23
     *
2313
     * @param string $database <p>Database name to switch to.</p>
2314
     *
2315
     * @return bool
2316 23
     *              <p>bool true on success, false otherwise.</p>
2317
     */
2318
    public function select_db(string $database): bool
2319 23
    {
2320
        if (!$this->isReady()) {
2321
            return false;
2322
        }
2323
2324
        if ($this->mysqli_link) {
2325
            return \mysqli_select_db($this->mysqli_link, $database);
2326
        }
2327
2328 14
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2329
            $return = $this->query('use :database', ['database' => $database]);
2330 14
            \assert(\is_bool($return));
2331 14
2332 8
            return $return;
2333
        }
2334 14
2335 8
        return false;
2336
    }
2337
2338 14
    /**
2339
     * @param array $extra_config   <p>
2340
     *                              'session_to_db' => false|true<br>
2341 14
     *                              'socket' => 'string (path)'<br>
2342
     *                              'ssl' => 'bool'<br>
2343 14
     *                              'clientkey' => 'string (path)'<br>
2344
     *                              'clientcert' => 'string (path)'<br>
2345 14
     *                              'cacert' => 'string (path)'<br>
2346
     *                              </p>
2347
     */
2348 14
    public function setConfigExtra(array $extra_config)
2349
    {
2350 14
        if (isset($extra_config['session_to_db'])) {
2351
            $this->session_to_db = (bool) $extra_config['session_to_db'];
2352
        }
2353
2354
        if (isset($extra_config['doctrine'])) {
2355
            if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2356
                $this->doctrine_connection = $extra_config['doctrine'];
2357
            } else {
2358
                throw new DBConnectException('Error "doctrine"-connection is not valid');
2359
            }
2360
        }
2361
2362 14
        if (isset($extra_config['socket'])) {
2363
            $this->socket = $extra_config['socket'];
2364
        }
2365
2366
        if (isset($extra_config['ssl'])) {
2367
            $this->ssl = $extra_config['ssl'];
2368
        }
2369
2370
        if (isset($extra_config['clientkey'])) {
2371
            $this->clientkey = $extra_config['clientkey'];
2372
        }
2373
2374
        if (isset($extra_config['clientcert'])) {
2375
            $this->clientcert = $extra_config['clientcert'];
2376 3
        }
2377
2378 3
        if (isset($extra_config['cacert'])) {
2379
            $this->cacert = $extra_config['cacert'];
2380 3
        }
2381
    }
2382
2383
    /**
2384
     * Set the current charset.
2385
     *
2386
     * @param string $charset
2387
     *
2388
     * @return bool
2389
     */
2390
    public function set_charset(string $charset): bool
2391
    {
2392
        $charsetLower = \strtolower($charset);
2393
        if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2394
            $charset = 'utf8';
2395
        }
2396
        if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this)) {
2397
            $charset = 'utf8mb4';
2398
        }
2399
2400
        $this->charset = $charset;
2401
2402
        if (
2403
            $this->mysqli_link
2404
            &&
2405
            $this->mysqli_link instanceof \mysqli
2406
        ) {
2407
            $return = \mysqli_set_charset($this->mysqli_link, $charset);
2408
2409
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2410
            @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2411
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2412
            @\mysqli_query($this->mysqli_link, "SET NAMES '" . $charset . "'");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2413
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2414
            $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2415
            if (!$doctrineWrappedConnection instanceof Connection) {
2416
                return false;
2417
            }
2418
2419
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2420
            @$doctrineWrappedConnection->exec('SET CHARACTER SET ' . $charset);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2421
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2422
            @$doctrineWrappedConnection->exec("SET NAMES '" . $charset . "'");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2423
2424
            $return = true;
2425
        } else {
2426
            $return = false;
2427
        }
2428
2429
        return $return;
2430
    }
2431
2432
    /**
2433
     * Set the option to convert null to "''" (empty string).
2434
     *
2435
     * Used in secure() => select(), insert(), update(), delete()
2436
     *
2437
     * @deprecated It's not recommended to convert NULL into an empty string!
2438
     *
2439
     * @param bool $bool
2440
     *
2441
     * @return self
2442
     */
2443 23
    public function set_convert_null_to_empty_string(bool $bool): self
2444
    {
2445
        $this->convert_null_to_empty_string = $bool;
2446
2447 23
        return $this;
2448
    }
2449 23
2450
    /**
2451
     * Enables or disables internal report functions
2452
     *
2453
     * @see http://php.net/manual/en/function.mysqli-report.php
2454
     *
2455 23
     * @param int $flags <p>
2456
     *                   <table>
2457 20
     *                   Supported flags
2458
     *                   <tr valign="top">
2459 23
     *                   <td>Name</td>
2460
     *                   <td>Description</td>
2461 9
     *                   </tr>
2462 3
     *                   <tr valign="top">
2463
     *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2464
     *                   <td>Turns reporting off</td>
2465 6
     *                   </tr>
2466 3
     *                   <tr valign="top">
2467
     *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2468
     *                   <td>Report errors from mysqli function calls</td>
2469 3
     *                   </tr>
2470 3
     *                   <tr valign="top">
2471
     *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2472
     *                   <td>
2473
     *                   Throw <b>mysqli_sql_exception</b> for errors
2474
     *                   instead of warnings
2475
     *                   </td>
2476 14
     *                   </tr>
2477
     *                   <tr valign="top">
2478
     *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2479
     *                   <td>Report if no index or bad index was used in a query</td>
2480
     *                   </tr>
2481
     *                   <tr valign="top">
2482 3
     *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2483
     *                   <td>Set all options (report all)</td>
2484 3
     *                   </tr>
2485
     *                   </table>
2486
     *                   </p>
2487
     *
2488
     * @return bool
2489
     */
2490
    public function set_mysqli_report(int $flags): bool
2491
    {
2492
        if (
2493
            $this->mysqli_link
2494 3
            &&
2495
            $this->mysqli_link instanceof \mysqli
2496 3
        ) {
2497
            return \mysqli_report($flags);
2498 3
        }
2499
2500 3
        return false;
2501
    }
2502 3
2503
    /**
2504
     * Show config errors by throw exceptions.
2505
     *
2506
     * @throws \InvalidArgumentException
2507
     *
2508
     * @return bool
2509
     */
2510
    public function showConfigError(): bool
2511
    {
2512
        // check if a doctrine connection is already open, first
2513 3
        if (
2514
            $this->doctrine_connection
2515
            &&
2516 3
            $this->doctrine_connection->isConnected()
2517 3
        ) {
2518 3
            return true;
2519
        }
2520 3
2521
        if (
2522
            !$this->hostname
2523 3
            ||
2524 3
            !$this->username
2525
            ||
2526 3
            !$this->database
2527
        ) {
2528
            if (!$this->hostname) {
2529 3
                throw new \InvalidArgumentException('no-sql-hostname');
2530 3
            }
2531 3
2532
            if (!$this->username) {
2533 3
                throw new \InvalidArgumentException('no-sql-username');
2534
            }
2535
2536
            if (!$this->database) {
2537
                throw new \InvalidArgumentException('no-sql-database');
2538
            }
2539
2540
            return false;
2541
        }
2542
2543
        return true;
2544
    }
2545
2546
    /**
2547
     * alias: "beginTransaction()"
2548
     */
2549 21
    public function startTransaction(): bool
2550
    {
2551
        return $this->beginTransaction();
2552 21
    }
2553
2554 21
    /**
2555 3
     * Determine if database table exists
2556
     *
2557 3
     * @param string $table
2558
     *
2559
     * @return bool
2560 21
     */
2561 6
    public function table_exists(string $table): bool
2562
    {
2563 6
        $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2564
2565
        return $check !== false
2566
               &&
2567
               $check instanceof Result
2568
               &&
2569 21
               $check->num_rows > 0;
2570
    }
2571
2572
    /**
2573
     * Execute a callback inside a transaction.
2574 21
     *
2575 6
     * @param \Closure $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2576 18
     *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2577 15
     *
2578
     * @return bool
2579 3
     *              <p>bool true on success, false otherwise.</p>
2580
     */
2581
    public function transact($callback): bool
2582 21
    {
2583
        try {
2584
            $beginTransaction = $this->beginTransaction();
2585
            if (!$beginTransaction) {
2586 21
                $this->debug->displayError('Error: transact -> can not start transaction!', false);
2587
2588 21
                return false;
2589
            }
2590
2591
            $result = $callback($this);
2592
            if ($result === false) {
2593
                /** @noinspection ThrowRawExceptionInspection */
2594
                throw new \Exception('call_user_func [' . \print_r($callback, true) . '] === false');
2595
            }
2596
2597
            return $this->commit();
2598
        } catch (\Exception $e) {
2599
            $this->rollback();
2600
2601
            return false;
2602
        }
2603
    }
2604
2605
    /**
2606
     * Execute a "update"-query.
2607
     *
2608
     * @param string       $table
2609
     * @param array        $data
2610
     * @param array|string $where
2611
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2612
     *
2613
     * @throws QueryException
2614
     *
2615
     * @return false|int
2616
     *                   <p>false on error</p>
2617
     */
2618
    public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2619
    {
2620
        // init
2621
        $table = \trim($table);
2622
2623
        if ($table === '') {
2624
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2625
2626
            return false;
2627
        }
2628
2629
        if (\count($data) === 0) {
2630
            $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2631
2632
            return false;
2633
        }
2634
2635
        // DEBUG
2636
        //var_dump($data);
2637
2638
        $SET = $this->_parseArrayPair($data);
2639
2640
        // DEBUG
2641
        //var_dump($SET);
2642
2643
        if (\is_string($where)) {
2644
            $WHERE = $this->escape($where, false);
2645
        } elseif (\is_array($where)) {
2646
            $WHERE = $this->_parseArrayPair($where, 'AND');
2647
        } else {
2648
            $WHERE = '';
2649
        }
2650
2651
        if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2652
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2653
        }
2654
2655
        $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET ${SET} WHERE (${WHERE})";
2656
2657
        $return = $this->query($sql);
2658
        \assert(\is_int($return) || $return === false);
2659
2660
        return $return;
2661
    }
2662
}
2663