Completed
Push — master ( 1303d9...d9f018 )
by Lars
03:28 queued 01:34
created

DB::escape()   F

Complexity

Conditions 24
Paths 138

Size

Total Lines 111

Duplication

Lines 7
Ratio 6.31 %

Code Coverage

Tests 48
CRAP Score 25.6808

Importance

Changes 0
Metric Value
dl 7
loc 111
ccs 48
cts 56
cp 0.8571
rs 3.08
c 0
b 0
f 0
cc 24
nc 138
nop 4
crap 25.6808

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Arrayy\Arrayy;
8
use Doctrine\DBAL\Connection;
9
use voku\cache\Cache;
10
use voku\db\exceptions\DBConnectException;
11
use voku\db\exceptions\DBGoneAwayException;
12
use voku\db\exceptions\QueryException;
13
use voku\helper\UTF8;
14
15
/**
16
 * DB: This class can handle DB queries via MySQLi.
17
 */
18
final class DB
19
{
20
    /**
21
     * @var int
22
     */
23
    public $query_count = 0;
24
25
    /**
26
     * @var \mysqli|null
27
     */
28
    private $mysqli_link;
29
30
    /**
31
     * @var bool
32
     */
33
    private $connected = false;
34
35
    /**
36
     * @var array
37
     */
38
    private $mysqlDefaultTimeFunctions;
39
40
    /**
41
     * @var string
42
     */
43
    private $hostname = '';
44
45
    /**
46
     * @var string
47
     */
48
    private $username = '';
49
50
    /**
51
     * @var string
52
     */
53
    private $password = '';
54
55
    /**
56
     * @var string
57
     */
58
    private $database = '';
59
60
    /**
61
     * @var int
62
     */
63
    private $port = 3306;
64
65
    /**
66
     * @var string
67
     */
68
    private $charset = 'utf8';
69
70
    /**
71
     * @var string
72
     */
73
    private $socket = '';
74
75
    /**
76
     * @var int|null
77
     */
78
    private $flags;
79
80
    /**
81
     * @var bool
82
     */
83
    private $session_to_db = false;
84
85
    /**
86
     * @var bool
87
     */
88
    private $in_transaction = false;
89
90
    /**
91
     * @var bool
92
     */
93
    private $convert_null_to_empty_string = false;
94
95
    /**
96
     * @var bool
97
     */
98
    private $ssl = false;
99
100
    /**
101
     * The path name to the key file
102
     *
103
     * @var string
104
     */
105
    private $clientkey;
106
107
    /**
108
     * The path name to the certificate file
109
     *
110
     * @var string
111
     */
112
    private $clientcert;
113
114
    /**
115
     * The path name to the certificate authority file
116
     *
117
     * @var string
118
     */
119
    private $cacert;
120
121
    /**
122
     * @var Debug
123
     */
124
    private $debug;
125
126
    /**
127
     * @var \Doctrine\DBAL\Connection|null
128
     */
129
    private $doctrine_connection;
130
131
    /**
132
     * @var int
133
     */
134
    private $affected_rows = 0;
135
136
    /**
137
     * __construct()
138
     *
139
     * @param string $hostname
140
     * @param string $username
141
     * @param string $password
142
     * @param string $database
143
     * @param int    $port
144
     * @param string $charset
145
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
146
     *                                      'false'. Use false to disable it.</p>
147
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
148
     *                                      Use false to disable it.</p>
149
     * @param string $logger_class_name
150
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
151
     * @param array  $extra_config          <p>
152
     *                                      'session_to_db' => bool<br>
153
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
154
     *                                      'socket'        => string (path)<br>
155
     *                                      'flags'         => null|int<br>
156
     *                                      'ssl'           => bool<br>
157
     *                                      'clientkey'     => string (path)<br>
158
     *                                      'clientcert'    => string (path)<br>
159
     *                                      'cacert'        => string (path)<br>
160
     *                                      </p>
161
     */
162 24
    private function __construct(
163
        string $hostname,
164
        string $username,
165
        string $password,
166
        string $database,
167
        $port,
168
        string $charset,
169
        bool $exit_on_error,
170
        bool $echo_on_error,
171
        string $logger_class_name,
172
        string $logger_level,
173
        array $extra_config = []
174
    ) {
175 24
        $this->debug = new Debug($this);
176
177 24
        $this->_loadConfig(
178 24
            $hostname,
179 24
            $username,
180 24
            $password,
181 24
            $database,
182 24
            $port,
183 24
            $charset,
184 24
            $exit_on_error,
185 24
            $echo_on_error,
186 24
            $logger_class_name,
187 24
            $logger_level,
188 24
            $extra_config
189
        );
190
191 15
        $this->connect();
192
193 6
        $this->mysqlDefaultTimeFunctions = [
194
            // Returns the current date.
195
            'CURDATE()',
196
            // CURRENT_DATE	| Synonyms for CURDATE()
197
            'CURRENT_DATE()',
198
            // CURRENT_TIME	| Synonyms for CURTIME()
199
            'CURRENT_TIME()',
200
            // CURRENT_TIMESTAMP | Synonyms for NOW()
201
            'CURRENT_TIMESTAMP()',
202
            // Returns the current time.
203
            'CURTIME()',
204
            // Synonym for NOW()
205
            'LOCALTIME()',
206
            // Synonym for NOW()
207
            'LOCALTIMESTAMP()',
208
            // Returns the current date and time.
209
            'NOW()',
210
            // Returns the time at which the function executes.
211
            'SYSDATE()',
212
            // Returns a UNIX timestamp.
213
            'UNIX_TIMESTAMP()',
214
            // Returns the current UTC date.
215
            'UTC_DATE()',
216
            // Returns the current UTC time.
217
            'UTC_TIME()',
218
            // Returns the current UTC date and time.
219
            'UTC_TIMESTAMP()',
220
        ];
221 6
    }
222
223
    /**
224
     * Prevent the instance from being cloned.
225
     *
226
     * @return void
227
     */
228
    private function __clone()
229
    {
230
    }
231
232
    /**
233
     * __destruct
234
     */
235
    public function __destruct()
236
    {
237
        // close the connection only if we don't save PHP-SESSION's in DB
238
        if (!$this->session_to_db) {
239
            $this->close();
240
        }
241
    }
242
243
    /**
244
     * @param string|null $sql
245
     * @param array       $bindings
246
     *
247
     * @return bool|DB|int|Result|string
248
     *                                      <p>
249
     *                                      "DB" by "$sql" === null<br />
250
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
251
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
252
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
253
     *                                      "true" by e.g. "DROP"-queries<br />
254
     *                                      "false" on error
255
     *                                      </p>
256
     */
257 4
    public function __invoke(string $sql = null, array $bindings = [])
258
    {
259 4
        return $sql !== null ? $this->query($sql, $bindings) : $this;
260
    }
261
262
    /**
263
     * __wakeup
264
     *
265
     * @return void
266
     */
267 4
    public function __wakeup()
268
    {
269 4
        $this->reconnect();
270 4
    }
271
272
    /**
273
     * Load the config from the constructor.
274
     *
275
     * @param string $hostname
276
     * @param string $username
277
     * @param string $password
278
     * @param string $database
279
     * @param int    $port                  <p>default is (int)3306</p>
280
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
281
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
282
     *                                      'false'. Use false to disable it.</p>
283
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
284
     *                                      Use false to disable it.</p>
285
     * @param string $logger_class_name
286
     * @param string $logger_level
287
     * @param array  $extra_config          <p>
288
     *                                      'session_to_db' => bool<br>
289
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
290
     *                                      'socket'        => string (path)<br>
291
     *                                      'flags'         => null|int<br>
292
     *                                      'ssl'           => bool<br>
293
     *                                      'clientkey'     => string (path)<br>
294
     *                                      'clientcert'    => string (path)<br>
295
     *                                      'cacert'        => string (path)<br>
296
     *                                      </p>
297
     *
298
     * @return bool
299
     */
300 24
    private function _loadConfig(
301
        string $hostname,
302
        string $username,
303
        string $password,
304
        string $database,
305
        $port,
306
        string $charset,
307
        bool $exit_on_error,
308
        bool $echo_on_error,
309
        string $logger_class_name,
310
        string $logger_level,
311
        array $extra_config = []
312
    ): bool {
313 24
        $this->hostname = $hostname;
314 24
        $this->username = $username;
315 24
        $this->password = $password;
316 24
        $this->database = $database;
317
318 24
        if ($charset) {
319 24
            $this->charset = $charset;
320
        }
321
322 24
        if ($port) {
323 12
            $this->port = (int) $port;
324
        } else {
325
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
326 13
            $this->port = (int) @\ini_get('mysqli.default_port');
327
        }
328
329
        // fallback
330 24
        if (!$this->port) {
331
            $this->port = 3306;
332
        }
333
334
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
335
        if (
336 24
            !$this->socket
337
            &&
338 24
            ($defaultSocket = @\ini_get('mysqli.default_socket'))
339
            &&
340 24
            \is_readable($defaultSocket)
341
        ) {
342 24
            $this->socket = $defaultSocket;
343
        }
344
345 24
        $this->debug->setExitOnError($exit_on_error);
346 24
        $this->debug->setEchoOnError($echo_on_error);
347
348 24
        $this->debug->setLoggerClassName($logger_class_name);
349 24
        $this->debug->setLoggerLevel($logger_level);
350
351 24
        $this->setConfigExtra($extra_config);
352
353 24
        return $this->showConfigError();
354
    }
355
356
    /**
357
     * Parses arrays with value pairs and generates SQL to use in queries.
358
     *
359
     * @param array  $arrayPair
360
     * @param string $glue <p>This is the separator.</p>
361
     *
362
     * @return string
363
     *
364
     * @internal
365
     */
366 72
    public function _parseArrayPair(array $arrayPair, string $glue = ','): string
367
    {
368
        // init
369 72
        $sql = '';
370
371 72
        if (\count($arrayPair) === 0) {
372
            return '';
373
        }
374
375 72
        $arrayPairCounter = 0;
376 72
        foreach ($arrayPair as $_key => $_value) {
377 72
            $_connector = '=';
378 72
            $_glueHelper = '';
379 72
            $_key_upper = \strtoupper((string) $_key);
380
381 72
            if (\strpos($_key_upper, ' NOT') !== false) {
382 6
                $_connector = 'NOT';
383
            }
384
385 72
            if (\strpos($_key_upper, ' IS') !== false) {
386 3
                $_connector = 'IS';
387
            }
388
389 72
            if (\strpos($_key_upper, ' IS NOT') !== false) {
390 3
                $_connector = 'IS NOT';
391
            }
392
393 72
            if (\strpos($_key_upper, ' IN') !== false) {
394 3
                $_connector = 'IN';
395
            }
396
397 72
            if (\strpos($_key_upper, ' NOT IN') !== false) {
398 3
                $_connector = 'NOT IN';
399
            }
400
401 72
            if (\strpos($_key_upper, ' BETWEEN') !== false) {
402 3
                $_connector = 'BETWEEN';
403
            }
404
405 72
            if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
406 3
                $_connector = 'NOT BETWEEN';
407
            }
408
409 72
            if (\strpos($_key_upper, ' LIKE') !== false) {
410 6
                $_connector = 'LIKE';
411
            }
412
413 72
            if (\strpos($_key_upper, ' NOT LIKE') !== false) {
414 6
                $_connector = 'NOT LIKE';
415
            }
416
417 72 View Code Duplication
            if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
418 8
                $_connector = '>';
419
            }
420
421 72 View Code Duplication
            if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
422 3
                $_connector = '<';
423
            }
424
425 72
            if (\strpos($_key_upper, ' >=') !== false) {
426 8
                $_connector = '>=';
427
            }
428
429 72
            if (\strpos($_key_upper, ' <=') !== false) {
430 3
                $_connector = '<=';
431
            }
432
433 72
            if (\strpos($_key_upper, ' <>') !== false) {
434 3
                $_connector = '<>';
435
            }
436
437 72
            if (\strpos($_key_upper, ' OR') !== false) {
438 6
                $_glueHelper = 'OR';
439
            }
440
441 72
            if (\strpos($_key_upper, ' AND') !== false) {
442 3
                $_glueHelper = 'AND';
443
            }
444
445 72
            if (\is_array($_value)) {
446 7
                $firstKey = null;
447 7
                $firstValue = null;
448 7
                foreach ($_value as $oldKey => $oldValue) {
449 7
                    $_value[$oldKey] = $this->secure($oldValue);
450
451 7
                    if ($firstKey === null) {
452 7
                        $firstKey = $oldKey;
453
                    }
454
455 7
                    if ($firstValue === null) {
456 7
                        $firstValue = $_value[$oldKey];
457
                    }
458
                }
459
460 7
                if ($_connector === 'NOT IN' || $_connector === 'IN') {
461 3
                    $_value = '(' . \implode(',', $_value) . ')';
462 7
                } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
463 3
                    $_value = '(' . \implode(' AND ', $_value) . ')';
464 7
                } elseif ($firstKey && $firstValue) {
465 1 View Code Duplication
                    if (\strpos((string) $firstKey, ' +') !== false) {
466 1
                        $firstKey = \str_replace(' +', '', (string) $firstKey);
467 1
                        $_value = $firstKey . ' + ' . $firstValue;
468
                    }
469
470 1 View Code Duplication
                    if (\strpos((string) $firstKey, ' -') !== false) {
471 1
                        $firstKey = \str_replace(' -', '', (string) $firstKey);
472 7
                        $_value = $firstKey . ' - ' . $firstValue;
473
                    }
474
                }
475
            } else {
476 72
                $_value = $this->secure($_value);
477
            }
478
479 72
            $quoteString = $this->quote_string(
480 72
                \trim(
481 72
                    (string) \str_ireplace(
482
                        [
483 72
                            $_connector,
484 72
                            $_glueHelper,
485
                        ],
486 72
                        '',
487 72
                        (string) $_key
488
                    )
489
                )
490
            );
491
492 72
            $_value = (array) $_value;
493
494 72
            if (!$_glueHelper) {
495 72
                $_glueHelper = $glue;
496
            }
497
498 72
            $tmpCounter = 0;
499 72
            foreach ($_value as $valueInner) {
500 72
                $_glueHelperInner = $_glueHelper;
501
502 72
                if ($arrayPairCounter === 0) {
503 72
                    if ($tmpCounter === 0 && $_glueHelper === 'OR') {
504 3
                        $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
505 72
                    } elseif ($tmpCounter === 0) {
506 72
                        $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
507
                    }
508 68
                } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
509 3
                    $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
510
                }
511
512 72
                if (\is_string($valueInner) && $valueInner === '') {
513
                    $valueInner = "''";
514
                }
515
516 72
                $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
517 72
                $tmpCounter++;
518
            }
519
520 72
            if ($_glueHelper === 'OR') {
521 6
                $sql .= ' ) ';
522
            }
523
524 72
            $arrayPairCounter++;
525
        }
526
527 72
        return $sql;
528
    }
529
530
    /**
531
     * _parseQueryParams
532
     *
533
     * @param string $sql
534
     * @param array  $params
535
     *
536
     * @return array
537
     *               <p>with the keys -> 'sql', 'params'</p>
538
     */
539 7
    private function _parseQueryParams(string $sql, array $params = []): array
540
    {
541 7
        $offset = \strpos($sql, '?');
542
543
        // is there anything to parse?
544
        if (
545 7
            $offset === false
546
            ||
547 7
            \count($params) === 0
548
        ) {
549 3
            return ['sql' => $sql, 'params' => $params];
550
        }
551
552 7
        foreach ($params as $key => $param) {
553
554
            // use this only for not named parameters
555 7
            if (!\is_int($key)) {
556 3
                continue;
557
            }
558
559 7
            if ($offset === false) {
560
                continue;
561
            }
562
563 7
            $replacement = $this->secure($param);
564
565 7
            unset($params[$key]);
566
567 7
            $sql = \substr_replace($sql, $replacement, $offset, 1);
568 7
            $offset = \strpos($sql, '?', $offset + \strlen((string) $replacement));
569
        }
570
571 7
        return ['sql' => $sql, 'params' => $params];
572
    }
573
574
    /**
575
     * Returns the SQL by replacing :placeholders with SQL-escaped values.
576
     *
577
     * @param string $sql    <p>The SQL string.</p>
578
     * @param array  $params <p>An array of key-value bindings.</p>
579
     *
580
     * @return array
581
     *               <p>with the keys -> 'sql', 'params'</p>
582
     */
583 10
    private function _parseQueryParamsByName(string $sql, array $params = []): array
584
    {
585
        // is there anything to parse?
586
        if (
587 10
            \strpos($sql, ':') === false
588
            ||
589 10
            \count($params) === 0
590
        ) {
591 7
            return ['sql' => $sql, 'params' => $params];
592
        }
593
594 6
        $offset = null;
595 6
        $replacement = null;
596 6
        foreach ($params as $name => $param) {
597
598
            // use this only for named parameters
599 6
            if (\is_int($name)) {
600
                continue;
601
            }
602
603
            // add ":" if needed
604 6
            if (\strpos($name, ':') !== 0) {
605 6
                $nameTmp = ':' . $name;
606
            } else {
607
                $nameTmp = $name;
608
            }
609
610 6
            if ($offset === null) {
611 6
                $offset = \strpos($sql, $nameTmp);
612
            } else {
613 6
                $offset = \strpos($sql, $nameTmp, $offset + \strlen((string) $replacement));
614
            }
615
616 6
            if ($offset === false) {
617 3
                continue;
618
            }
619
620 6
            $replacement = $this->secure($param);
621
622 6
            unset($params[$name]);
623
624 6
            $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
625
        }
626
627 6
        return ['sql' => $sql, 'params' => $params];
628
    }
629
630
    /**
631
     * Gets the number of affected rows in a previous MySQL operation.
632
     *
633
     * @return int
634
     */
635 25
    public function affected_rows(): int
636
    {
637
        if (
638 25
            $this->mysqli_link
639
            &&
640 25
            $this->mysqli_link instanceof \mysqli
641
        ) {
642 25
            return \mysqli_affected_rows($this->mysqli_link);
643
        }
644
645
        return (int) $this->affected_rows;
646
    }
647
648
    /**
649
     * Begins a transaction, by turning off auto commit.
650
     *
651
     * @return bool
652
     *              <p>This will return true or false indicating success of transaction</p>
653
     */
654 18 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...
655
    {
656 18
        if ($this->in_transaction) {
657 6
            $this->debug->displayError('Error: mysql server already in transaction!', false);
658
659 6
            return false;
660
        }
661
662 18
        $this->clearErrors(); // needed for "$this->endTransaction()"
663 18
        $this->in_transaction = true;
664
665 18
        if ($this->mysqli_link) {
666 18
            $return = \mysqli_autocommit($this->mysqli_link, false);
667
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
668
            $this->doctrine_connection->setAutoCommit(false);
669
            $this->doctrine_connection->beginTransaction();
670
671
            if ($this->doctrine_connection->isTransactionActive()) {
672
                $return = true;
673
            } else {
674
                $return = false;
675
            }
676
        } else {
677
            $return = false;
678
        }
679
680 18
        if (!$return) {
681
            $this->in_transaction = false;
682
        }
683
684 18
        return $return;
685
    }
686
687
    /**
688
     * Clear the errors in "_debug->_errors".
689
     *
690
     * @return bool
691
     */
692 18
    public function clearErrors(): bool
693
    {
694 18
        return $this->debug->clearErrors();
695
    }
696
697
    /**
698
     * Closes a previously opened database connection.
699
     *
700
     * @return bool
701
     *              Will return "true", if the connection was closed,
702
     *              otherwise (e.g. if the connection was already closed) "false".
703
     */
704 6
    public function close(): bool
705
    {
706 6
        $this->connected = false;
707
708
        if (
709 6
            $this->doctrine_connection
710
            &&
711 6
            $this->doctrine_connection instanceof \Doctrine\DBAL\Connection
712
        ) {
713
            $connectedBefore = $this->doctrine_connection->isConnected();
714
715
            $this->doctrine_connection->close();
716
717
            $this->mysqli_link = null;
718
719
            if ($connectedBefore) {
720
                return !$this->doctrine_connection->isConnected();
721
            }
722
723
            return false;
724
        }
725
726
        if (
727 6
            $this->mysqli_link
728
            &&
729 6
            $this->mysqli_link instanceof \mysqli
730
        ) {
731 6
            $result = \mysqli_close($this->mysqli_link);
732 6
            $this->mysqli_link = null;
733
734 6
            return $result;
735
        }
736
737 3
        $this->mysqli_link = null;
738
739 3
        return false;
740
    }
741
742
    /**
743
     * Commits the current transaction and end the transaction.
744
     *
745
     * @return bool
746
     *              <p>bool true on success, false otherwise.</p>
747
     */
748 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...
749
    {
750 9
        if (!$this->in_transaction) {
751
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
752
753
            return false;
754
        }
755
756 9
        if ($this->mysqli_link) {
757 9
            $return = \mysqli_commit($this->mysqli_link);
758 9
            \mysqli_autocommit($this->mysqli_link, true);
759
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
760
            $this->doctrine_connection->commit();
761
            $this->doctrine_connection->setAutoCommit(true);
762
763
            if ($this->doctrine_connection->isAutoCommit()) {
764
                $return = true;
765
            } else {
766
                $return = false;
767
            }
768
        } else {
769
            $return = false;
770
        }
771
772 9
        $this->in_transaction = false;
773
774 9
        return $return;
775
    }
776
777
    /**
778
     * Open a new connection to the MySQL server.
779
     *
780
     * @throws DBConnectException
781
     *
782
     * @return bool
783
     */
784 21
    public function connect(): bool
785
    {
786 21
        if ($this->isReady()) {
787 3
            return true;
788
        }
789
790 21
        if ($this->doctrine_connection) {
791
            $this->doctrine_connection->connect();
792
793
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
794
795
            if ($this->isDoctrineMySQLiConnection()) {
796
                \assert($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection);
797
798
                $this->mysqli_link = $doctrineWrappedConnection->getWrappedResourceHandle();
799
800
                return $this->connect_helper();
801
            }
802
803
            if ($this->isDoctrinePDOConnection()) {
804
                $this->mysqli_link = null;
805
806
                return $this->connect_helper();
807
            }
808
        }
809
810 21
        $flags = $this->flags;
811
812 21
        \mysqli_report(\MYSQLI_REPORT_STRICT);
813
814
        try {
815 21
            $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...
816
817 21
            if (Helper::isMysqlndIsUsed()) {
818 21
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
819
            }
820
821 21
            if ($this->ssl) {
822
                if (empty($this->clientcert)) {
823
                    throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
824
                }
825
826
                if (empty($this->clientkey)) {
827
                    throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
828
                }
829
830
                if (empty($this->cacert)) {
831
                    throw new DBConnectException('Error connecting to mysql server: cacert not defined');
832
                }
833
834
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
835
836
                \mysqli_ssl_set(
837
                    $this->mysqli_link,
838
                    $this->clientkey,
839
                    $this->clientcert,
840
                    $this->cacert,
841
                    '',
842
                    ''
843
                );
844
845
                $flags |= \MYSQLI_CLIENT_SSL;
846
            }
847
848
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
849 21
            $this->connected = @\mysqli_real_connect(
850 21
                $this->mysqli_link,
851 21
                $this->hostname,
852 21
                $this->username,
853 21
                $this->password,
854 21
                $this->database,
855 21
                $this->port,
856 21
                $this->socket,
857 21
                (int) $flags
858
            );
859 9
        } catch (\Exception $e) {
860 9
            $error = 'Error connecting to mysql server: ' . $e->getMessage();
861 9
            $this->debug->displayError($error, false);
862
863 9
            throw new DBConnectException($error, 100, $e);
864
        }
865 12
        \mysqli_report(\MYSQLI_REPORT_OFF);
866
867 12
        $errno = \mysqli_connect_errno();
868 12
        if (!$this->connected || $errno) {
869
            $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
870
            $this->debug->displayError($error, false);
871
872
            throw new DBConnectException($error, 101);
873
        }
874
875 12
        $this->set_charset($this->charset);
876
877 12
        return $this->isReady();
878
    }
879
880
    /**
881
     * @return bool
882
     */
883
    private function connect_helper(): bool
884
    {
885
        if (!$this->doctrine_connection) {
886
            $this->connected = false;
887
        } else {
888
            $this->connected = $this->doctrine_connection->isConnected();
889
        }
890
891
        if (!$this->connected) {
892
            $error = 'Error connecting to mysql server: ' . \print_r($this->doctrine_connection ? $this->doctrine_connection->errorInfo() : [], false);
893
            $this->debug->displayError($error, false);
894
895
            throw new DBConnectException($error, 101);
896
        }
897
898
        $this->set_charset($this->charset);
899
900
        return $this->isReady();
901
    }
902
903
    /**
904
     * Execute a "delete"-query.
905
     *
906
     * @param string       $table
907
     * @param array|string $where
908
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
909
     *
910
     * @throws QueryException
911
     *
912
     * @return false|int
913
     *                   <p>false on error</p>
914
     */
915 4 View Code Duplication
    public function delete(
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...
916
        string $table,
917
        $where,
918
        string $databaseName = null
919
    ) {
920
        // init
921 4
        $table = \trim($table);
922
923 4
        if ($table === '') {
924 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
925
926 3
            return false;
927
        }
928
929 4
        if (\is_string($where)) {
930 3
            $WHERE = $this->escape($where, false);
931 4
        } elseif (\is_array($where)) {
932 4
            $WHERE = $this->_parseArrayPair($where, 'AND');
933
        } else {
934 3
            $WHERE = '';
935
        }
936
937 4
        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...
938
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
939
        }
940
941 4
        $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
942
943 4
        $return = $this->query($sql);
944
945 4
        \assert(\is_int($return) || $return === false);
946
947 4
        return $return;
948
    }
949
950
    /**
951
     * Ends a transaction and commits if no errors, then ends autocommit.
952
     *
953
     * @return bool
954
     *              <p>This will return true or false indicating success of transactions.</p>
955
     */
956 12
    public function endTransaction(): bool
957
    {
958 12
        if (!$this->in_transaction) {
959
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
960
961
            return false;
962
        }
963
964 12
        if (!$this->errors()) {
965 3
            $return = $this->commit();
966
        } else {
967 9
            $this->rollback();
968 9
            $return = false;
969
        }
970
971 12
        if ($this->mysqli_link) {
972 12
            \mysqli_autocommit($this->mysqli_link, true);
973
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
974
            $this->doctrine_connection->setAutoCommit(true);
975
976
            if ($this->doctrine_connection->isAutoCommit()) {
977
                $return = true;
978
            } else {
979
                $return = false;
980
            }
981
        }
982
983 12
        $this->in_transaction = false;
984
985 12
        return $return;
986
    }
987
988
    /**
989
     * Get all errors from "$this->errors".
990
     *
991
     * @return array|false
992
     *                     <p>false === on errors</p>
993
     */
994 12
    public function errors()
995
    {
996 12
        $errors = $this->debug->getErrors();
997
998 12
        return \count($errors) > 0 ? $errors : false;
999
    }
1000
1001
    /**
1002
     * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
1003
     *
1004
     * @param mixed     $var           bool: convert into "integer"<br />
1005
     *                                 int: int (don't change it)<br />
1006
     *                                 float: float (don't change it)<br />
1007
     *                                 null: null (don't change it)<br />
1008
     *                                 array: run escape() for every key => value<br />
1009
     *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
1010
     * @param bool      $stripe_non_utf8
1011
     * @param bool      $html_entity_decode
1012
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
1013
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
1014
     *                                 <strong>null</strong> => Convert the array into null, every time.
1015
     *
1016
     * @return mixed
1017
     */
1018 110
    public function escape(
1019
        $var = '',
1020
        bool $stripe_non_utf8 = true,
1021
        bool $html_entity_decode = false,
1022
        $convert_array = false
1023
    ) {
1024
        // [empty]
1025 110
        if ($var === '') {
1026 6
            return '';
1027
        }
1028
1029
        // ''
1030 110
        if ($var === "''") {
1031
            return "''";
1032
        }
1033
1034
        // check the type
1035 110
        $type = \gettype($var);
1036
1037 110
        if ($type === 'object') {
1038 9
            if ($var instanceof \DateTimeInterface) {
1039 9
                $var = $var->format('Y-m-d H:i:s');
1040 9
                $type = 'string';
1041 6
            } elseif (\method_exists($var, '__toString')) {
1042 6
                $var = (string) $var;
1043 6
                $type = 'string';
1044
            }
1045
        }
1046
1047
        switch ($type) {
1048 110
            case 'boolean':
1049 9
                $var = (int) $var;
1050
1051 9
                break;
1052
1053 110
            case 'double':
1054 110
            case 'integer':
1055 65
                break;
1056
1057 107
            case 'string':
1058 107
                if ($stripe_non_utf8) {
1059 23
                    $var = UTF8::cleanup($var);
1060
                }
1061
1062 107
                if ($html_entity_decode) {
1063 3
                    $var = UTF8::html_entity_decode($var);
1064
                }
1065
1066
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
1067 107
                $var = @\get_magic_quotes_gpc() ? \stripslashes($var) : $var;
1068
1069
                if (
1070 107
                    $this->mysqli_link
1071
                    &&
1072 107
                    $this->mysqli_link instanceof \mysqli
1073
                ) {
1074 107
                    $var = \mysqli_real_escape_string($this->mysqli_link, $var);
1075
                } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1076
                    $pdoConnection = $this->getDoctrinePDOConnection();
1077
                    \assert($pdoConnection !== false);
1078
                    $var = $pdoConnection->quote($var);
1079
                    $var = \substr($var, 1, -1);
1080
                }
1081
1082 107
                break;
1083
1084 9
            case 'array':
1085 6
                if ($convert_array === null) {
1086 3
                    if ($this->convert_null_to_empty_string) {
1087
                        $var = "''";
1088
                    } else {
1089 3
                        $var = 'NULL';
1090
                    }
1091
                } else {
1092 6
                    $varCleaned = [];
1093 6
                    foreach ((array) $var as $key => $value) {
1094 6
                        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
1095 6
                        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
1096
1097
                        /** @noinspection OffsetOperationsInspection */
1098 6
                        $varCleaned[$key] = $value;
1099
                    }
1100
1101 6 View Code Duplication
                    if ($convert_array === true) {
1102 3
                        $varCleaned = \implode(',', $varCleaned);
1103
1104 3
                        $var = $varCleaned;
1105
                    } else {
1106 6
                        $var = $varCleaned;
1107
                    }
1108
                }
1109
1110 6
                break;
1111
1112 9
            case 'NULL':
1113 6
                if ($this->convert_null_to_empty_string) {
1114
                    $var = "''";
1115
                } else {
1116 6
                    $var = 'NULL';
1117
                }
1118
1119 6
                break;
1120
1121
            default:
1122 6
                throw new \InvalidArgumentException(\sprintf('Not supported value "%s" of type %s.', \print_r($var, true), $type));
1123
1124
                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...
1125
        }
1126
1127 110
        return $var;
1128
    }
1129
1130
    /**
1131
     * Execute select/insert/update/delete sql-queries.
1132
     *
1133
     * @param string  $query    <p>sql-query</p>
1134
     * @param bool    $useCache optional <p>use cache?</p>
1135
     * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
1136
     * @param DB|null $db       optional <p>the database connection</p>
1137
     *
1138
     * @throws QueryException
1139
     *
1140
     * @return mixed
1141
     *               <ul>
1142
     *               <li>"array" by "<b>SELECT</b>"-queries</li>
1143
     *               <li>"int|string" (insert_id) by "<b>INSERT</b>"-queries</li>
1144
     *               <li>"int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1145
     *               <li>"true" by e.g. "DROP"-queries</li>
1146
     *               <li>"false" on error</li>
1147
     *               </ul>
1148
     */
1149 9
    public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1150
    {
1151
        // init
1152 9
        $cacheKey = null;
1153 9
        if (!$db) {
1154 9
            $db = self::getInstance();
1155
        }
1156
1157 9 View Code Duplication
        if ($useCache) {
1158 3
            $cache = new Cache(null, null, false, $useCache);
1159 3
            $cacheKey = 'sql-' . \md5($query);
1160
1161
            if (
1162 3
                $cache->getCacheIsReady()
1163
                &&
1164 3
                $cache->existsItem($cacheKey)
1165
            ) {
1166 3
                return $cache->getItem($cacheKey);
1167
            }
1168
        } else {
1169 9
            $cache = false;
1170
        }
1171
1172 9
        $result = $db->query($query);
1173
1174 9
        if ($result instanceof Result) {
1175
            // save into the cache
1176
            if (
1177 3
                $cacheKey !== null
1178
                &&
1179 3
                $useCache
1180
                &&
1181 3
                $cache instanceof Cache
1182
                &&
1183 3
                $cache->getCacheIsReady()
1184
            ) {
1185 1
                $return = $result->fetchAllArrayy();
1186
1187 1
                $cache->setItem($cacheKey, $return, $cacheTTL);
1188
            } else {
1189 3
                $return = $result->fetchAllArrayyYield();
1190
            }
1191
        } else {
1192 6
            $return = $result;
1193
        }
1194
1195 9
        return $return;
1196
    }
1197
1198
    /**
1199
     * Get all table-names via "SHOW TABLES".
1200
     *
1201
     * @return Arrayy
1202
     */
1203 3
    public function getAllTables(): Arrayy
1204
    {
1205 3
        $query = 'SHOW TABLES';
1206 3
        $result = $this->query($query);
1207
1208 3
        \assert($result instanceof Result);
1209
1210 3
        return $result->fetchAllArrayyYield();
1211
    }
1212
1213
    /**
1214
     * @return array
1215
     */
1216 49
    public function getConfig(): array
1217
    {
1218
        $config = [
1219 49
            'hostname'   => $this->hostname,
1220 49
            'username'   => $this->username,
1221 49
            'password'   => $this->password,
1222 49
            'port'       => $this->port,
1223 49
            'database'   => $this->database,
1224 49
            'socket'     => $this->socket,
1225 49
            'charset'    => $this->charset,
1226 49
            'cacert'     => $this->cacert,
1227 49
            'clientcert' => $this->clientcert,
1228 49
            'clientkey'  => $this->clientkey,
1229
        ];
1230
1231 49
        if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1232
            $config += $this->doctrine_connection->getParams();
1233
        }
1234
1235 49
        return $config;
1236
    }
1237
1238
    /**
1239
     * @return Debug
1240
     */
1241 10
    public function getDebugger(): Debug
1242
    {
1243 10
        return $this->debug;
1244
    }
1245
1246
    /**
1247
     * @return \Doctrine\DBAL\Connection|null|null
1248
     */
1249 2
    public function getDoctrineConnection()
1250
    {
1251 2
        return $this->doctrine_connection;
1252
    }
1253
1254
    /**
1255
     * @return \Doctrine\DBAL\Driver\Connection|false
1256
     */
1257 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...
1258
    {
1259
        if ($this->doctrine_connection) {
1260
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1261
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1262
                return $doctrineWrappedConnection;
1263
            }
1264
        }
1265
1266
        return false;
1267
    }
1268
1269
    /**
1270
     * Get errors from "$this->errors".
1271
     *
1272
     * @return array
1273
     */
1274 3
    public function getErrors(): array
1275
    {
1276 3
        return $this->debug->getErrors();
1277
    }
1278
1279
    /**
1280
     * @param string $hostname              <p>Hostname of the mysql server</p>
1281
     * @param string $username              <p>Username for the mysql connection</p>
1282
     * @param string $password              <p>Password for the mysql connection</p>
1283
     * @param string $database              <p>Database for the mysql connection</p>
1284
     * @param int    $port                  <p>default is (int)3306</p>
1285
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1286
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
1287
     *                                      'false'. Use false to disable it.</p>
1288
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
1289
     *                                      Use false to disable it.</p>
1290
     * @param string $logger_class_name
1291
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1292
     * @param array  $extra_config          <p>
1293
     *                                      're_connect'    => bool<br>
1294
     *                                      'session_to_db' => bool<br>
1295
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
1296
     *                                      'socket'        => string (path)<br>
1297
     *                                      'flags'         => null|int<br>
1298
     *                                      'ssl'           => bool<br>
1299
     *                                      'clientkey'     => string (path)<br>
1300
     *                                      'clientcert'    => string (path)<br>
1301
     *                                      'cacert'        => string (path)<br>
1302
     *                                      </p>
1303
     *
1304
     * @return self
1305
     */
1306 207
    public static function getInstance(
1307
        string $hostname = '',
1308
        string $username = '',
1309
        string $password = '',
1310
        string $database = '',
1311
        $port = 3306,
1312
        string $charset = 'utf8',
1313
        bool $exit_on_error = true,
1314
        bool $echo_on_error = true,
1315
        string $logger_class_name = '',
1316
        string $logger_level = '',
1317
        array $extra_config = []
1318
    ): self {
1319
        /**
1320
         * @var self[]
1321
         */
1322 207
        static $instance = [];
1323
1324
        /**
1325
         * @var self|null
1326
         */
1327 207
        static $firstInstance = null;
1328
1329
        // fallback
1330 207
        if (!$charset) {
1331 121
            $charset = 'utf8';
1332
        }
1333
1334
        if (
1335 207
            '' . $hostname . $username . $password . $database . $port . $charset === '' . $port . $charset
1336
            &&
1337 207
            $firstInstance instanceof self
1338
        ) {
1339 123
            if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1340
                $firstInstance->reconnect(true);
1341
            }
1342
1343 123
            return $firstInstance;
1344
        }
1345
1346 126
        $extra_config_string = '';
1347 126
        foreach ($extra_config as $extra_config_key => $extra_config_value) {
1348 56
            if (\is_object($extra_config_value)) {
1349
                $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1350
            } else {
1351 56
                $extra_config_value_tmp = (string) $extra_config_value;
1352
            }
1353 56
            $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1354
        }
1355
1356 126
        $connection = \md5(
1357 126
            $hostname . $username . $password . $database . $port . $charset . (int) $exit_on_error . (int) $echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1358
        );
1359
1360 126
        if (!isset($instance[$connection])) {
1361 24
            $instance[$connection] = new self(
1362 24
                $hostname,
1363 24
                $username,
1364 24
                $password,
1365 24
                $database,
1366 24
                $port,
1367 24
                $charset,
1368 24
                $exit_on_error,
1369 24
                $echo_on_error,
1370 24
                $logger_class_name,
1371 24
                $logger_level,
1372 24
                $extra_config
1373
            );
1374
1375 6
            if ($firstInstance === null) {
1376 1
                $firstInstance = $instance[$connection];
1377
            }
1378
        }
1379
1380 114
        if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1381
            $instance[$connection]->reconnect(true);
1382
        }
1383
1384 114
        return $instance[$connection];
1385
    }
1386
1387
    /**
1388
     * @param \Doctrine\DBAL\Connection $doctrine
1389
     * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1390
     * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1391
     *                                                 return 'false'. Use false to disable it.</p>
1392
     * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1393
     *                                                 Use false to disable it.</p>
1394
     * @param string                    $logger_class_name
1395
     * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1396
     * @param array                     $extra_config  <p>
1397
     *                                                 're_connect'    => bool<br>
1398
     *                                                 'session_to_db' => bool<br>
1399
     *                                                 'socket'        => string (path)<br>
1400
     *                                                 'flags'         => null|int<br>
1401
     *                                                 'ssl'           => bool<br>
1402
     *                                                 'clientkey'     => string (path)<br>
1403
     *                                                 'clientcert'    => string (path)<br>
1404
     *                                                 'cacert'        => string (path)<br>
1405
     *                                                 </p>
1406
     *
1407
     * @return self
1408
     */
1409 55
    public static function getInstanceDoctrineHelper(
1410
        \Doctrine\DBAL\Connection $doctrine,
1411
        string $charset = 'utf8',
1412
        bool $exit_on_error = true,
1413
        bool $echo_on_error = true,
1414
        string $logger_class_name = '',
1415
        string $logger_level = '',
1416
        array $extra_config = []
1417
    ): self {
1418 55
        $extra_config['doctrine'] = $doctrine;
1419
1420 55
        return self::getInstance(
1421 55
            '',
1422 55
            '',
1423 55
            '',
1424 55
            '',
1425 55
            3306,
1426 55
            $charset,
1427 55
            $exit_on_error,
1428 55
            $echo_on_error,
1429 55
            $logger_class_name,
1430 55
            $logger_level,
1431 55
            $extra_config
1432
        );
1433
    }
1434
1435
    /**
1436
     * Get the mysqli-link (link identifier returned by mysqli-connect).
1437
     *
1438
     * @return \mysqli|null
1439
     */
1440 15
    public function getLink()
1441
    {
1442 15
        return $this->mysqli_link;
1443
    }
1444
1445
    /**
1446
     * Get the current charset.
1447
     *
1448
     * @return string
1449
     */
1450 3
    public function get_charset(): string
1451
    {
1452 3
        return $this->charset;
1453
    }
1454
1455
    /**
1456
     * Check if we are in a transaction.
1457
     *
1458
     * @return bool
1459
     */
1460
    public function inTransaction(): bool
1461
    {
1462
        return $this->in_transaction;
1463
    }
1464
1465
    /**
1466
     * Execute a "insert"-query.
1467
     *
1468
     * @param string      $table
1469
     * @param array       $data
1470
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1471
     *
1472
     * @throws QueryException
1473
     *
1474
     * @return false|int|string
1475
     *                   <p>false on error</p>
1476
     */
1477 74
    public function insert(
1478
        string $table,
1479
        array $data = [],
1480
        string $databaseName = null
1481
    ) {
1482
        // init
1483 74
        $table = \trim($table);
1484
1485 74
        if ($table === '') {
1486 6
            $this->debug->displayError('Invalid table name, table name in empty.', false);
1487
1488 6
            return false;
1489
        }
1490
1491 71
        if (\count($data) === 0) {
1492 9
            $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1493
1494 9
            return false;
1495
        }
1496
1497 65
        $SET = $this->_parseArrayPair($data);
1498
1499 65
        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...
1500
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1501
        }
1502
1503 65
        $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET ${SET}";
1504
1505 65
        $return = $this->query($sql);
1506 65
        if ($return === false) {
1507 3
            return false;
1508
        }
1509
1510 65
        \assert(\is_int($return) || \is_string($return));
1511
1512 65
        return $return;
1513
    }
1514
1515
    /**
1516
     * Returns the auto generated id used in the last query.
1517
     *
1518
     * @return false|int|string
1519
     */
1520 100
    public function insert_id()
1521
    {
1522 100
        if ($this->mysqli_link) {
1523 100
            return \mysqli_insert_id($this->mysqli_link);
1524
        }
1525
1526
        $doctrinePDOConnection = $this->getDoctrinePDOConnection();
1527
        if ($doctrinePDOConnection) {
1528
            return $doctrinePDOConnection->lastInsertId();
1529
        }
1530
1531
        return false;
1532
    }
1533
1534
    /**
1535
     * @return bool
1536
     */
1537
    public function isDoctrineMySQLiConnection(): bool
1538
    {
1539
        if ($this->doctrine_connection) {
1540
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1541
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1542
                return true;
1543
            }
1544
        }
1545
1546
        return false;
1547
    }
1548
1549
    /**
1550
     * @return bool
1551
     */
1552 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...
1553
    {
1554
        if ($this->doctrine_connection) {
1555
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1556
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1557
                return true;
1558
            }
1559
        }
1560
1561
        return false;
1562
    }
1563
1564
    /**
1565
     * Check if db-connection is ready.
1566
     *
1567
     * @return bool
1568
     */
1569 162
    public function isReady(): bool
1570
    {
1571 162
        return $this->connected ? true : false;
1572
    }
1573
1574
    /**
1575
     * Get the last sql-error.
1576
     *
1577
     * @return false|string
1578
     *                      <p>false === there was no error</p>
1579
     */
1580 3
    public function lastError()
1581
    {
1582 3
        $errors = $this->debug->getErrors();
1583
1584 3
        return \count($errors) > 0 ? \end($errors) : false;
1585
    }
1586
1587
    /**
1588
     * Execute a sql-multi-query.
1589
     *
1590
     * @param string $sql
1591
     *
1592
     * @throws QueryException
1593
     *
1594
     * @return bool|Result[]
1595
     *                        <ul>
1596
     *                        <li>"Result"-Array by "<b>SELECT</b>"-queries</li>
1597
     *                        <li>"bool" by only "<b>INSERT</b>"-queries</li>
1598
     *                        <li>"bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1599
     *                        <li>"bool" by only by e.g. "DROP"-queries</li>
1600
     *                        </ul>
1601
     */
1602 3
    public function multi_query(string $sql)
1603
    {
1604 3
        if (!$this->isReady()) {
1605
            return false;
1606
        }
1607
1608 3
        if (!$sql || $sql === '') {
1609 3
            $this->debug->displayError('Can not execute an empty query.', false);
1610
1611 3
            return false;
1612
        }
1613
1614 3
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1615
            $query_start_time = \microtime(true);
1616
            $queryException = null;
1617
            $query_result_doctrine = false;
1618
1619
            try {
1620
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1621
                $resultTmp = $query_result_doctrine->execute();
1622
                $mysqli_field_count = $query_result_doctrine->columnCount();
1623
            } catch (\Exception $e) {
1624
                $resultTmp = false;
1625
                $mysqli_field_count = null;
1626
1627
                $queryException = $e;
1628
            }
1629
1630
            $query_duration = \microtime(true) - $query_start_time;
1631
1632
            $this->debug->logQuery($sql, $query_duration, 0);
1633
1634
            $returnTheResult = false;
1635
            $result = [];
1636
1637
            if ($resultTmp) {
1638
                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...
1639
                    if (
1640
                        $query_result_doctrine
1641
                        &&
1642
                        $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1643
                    ) {
1644
                        $result = $query_result_doctrine;
1645
                    }
1646
                } else {
1647
                    $result = $resultTmp;
1648
                }
1649
1650
                if (
1651
                    $result instanceof \Doctrine\DBAL\Statement
1652
                    &&
1653
                    $result->columnCount() > 0
1654
                ) {
1655
                    $returnTheResult = true;
1656
1657
                    // return query result object
1658
                    $result = [new Result($sql, $result)];
1659
                } else {
1660
                    $result = [$result];
1661
                }
1662
            } else {
1663
1664
                // log the error query
1665
                $this->debug->logQuery($sql, $query_duration, 0, true);
1666
1667
                if (
1668
                    isset($queryException)
1669
                    &&
1670
                    $queryException instanceof \Doctrine\DBAL\Query\QueryException
1671
                ) {
1672
                    return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1673
                }
1674
            }
1675 3
        } elseif ($this->mysqli_link) {
1676 3
            $query_start_time = \microtime(true);
1677 3
            $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1678 3
            $query_duration = \microtime(true) - $query_start_time;
1679
1680 3
            $this->debug->logQuery($sql, $query_duration, 0);
1681
1682 3
            $returnTheResult = false;
1683 3
            $result = [];
1684
1685 3
            if ($resultTmp) {
1686
                do {
1687 3
                    $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1688
1689 3
                    if ($resultTmpInner instanceof \mysqli_result) {
1690 3
                        $returnTheResult = true;
1691 3
                        $result[] = new Result($sql, $resultTmpInner);
1692 3
                    } elseif (\mysqli_errno($this->mysqli_link)) {
1693
                        $result[] = false;
1694
                    } else {
1695 3
                        $result[] = true;
1696
                    }
1697 3
                } while (\mysqli_more_results($this->mysqli_link) ? \mysqli_next_result($this->mysqli_link) : false);
1698
            } else {
1699
1700
                // log the error query
1701 3
                $this->debug->logQuery($sql, $query_duration, 0, true);
1702
1703 3
                return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1704
            }
1705
        } else {
1706
1707
            // log the error query
1708
            $this->debug->logQuery($sql, 0, 0, true);
1709
1710
            return $this->queryErrorHandling('no database connection', 1, $sql, false, true);
1711
        }
1712
1713
        // return the result only if there was a "SELECT"-query
1714 3
        if ($returnTheResult) {
1715 3
            return $result;
1716
        }
1717
1718
        if (
1719 3
            \count($result) > 0
1720
            &&
1721 3
            !\in_array(false, $result, true)
1722
        ) {
1723 3
            return true;
1724
        }
1725
1726
        return false;
1727
    }
1728
1729
    /**
1730
     * Count number of rows found matching a specific query.
1731
     *
1732
     * @param string $query
1733
     *
1734
     * @return int
1735
     */
1736 3
    public function num_rows(string $query): int
1737
    {
1738 3
        $check = $this->query($query);
1739
1740
        if (
1741 3
            $check === false
1742
            ||
1743 3
            !$check instanceof Result
1744
        ) {
1745
            return 0;
1746
        }
1747
1748 3
        return $check->num_rows;
1749
    }
1750
1751
    /**
1752
     * Pings a server connection, or tries to reconnect
1753
     * if the connection has gone down.
1754
     *
1755
     * @return bool
1756
     */
1757 9
    public function ping(): bool
1758
    {
1759 9
        if (!$this->connected) {
1760 3
            return false;
1761
        }
1762
1763 6
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1764
            return $this->doctrine_connection->ping();
1765
        }
1766
1767
        if (
1768 6
            $this->mysqli_link
1769
            &&
1770 6
            $this->mysqli_link instanceof \mysqli
1771
        ) {
1772 6
            return \mysqli_ping($this->mysqli_link);
1773
        }
1774
1775
        return false;
1776
    }
1777
1778
    /**
1779
     * Get a new "Prepare"-Object for your sql-query.
1780
     *
1781
     * @param string $query
1782
     *
1783
     * @return Prepare
1784
     */
1785 2
    public function prepare(string $query): Prepare
1786
    {
1787 2
        return new Prepare($this, $query);
1788
    }
1789
1790
    /**
1791
     * Execute a sql-query and return the result-array for select-statements.
1792
     *
1793
     * @param string $query
1794
     *
1795
     * @throws \Exception
1796
     *
1797
     * @return mixed
1798
     *
1799
     * @deprecated
1800
     */
1801 3
    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...
1802
    {
1803 3
        $db = self::getInstance();
1804
1805 3
        $args = \func_get_args();
1806
        /** @noinspection SuspiciousAssignmentsInspection */
1807 3
        $query = \array_shift($args);
1808 3
        $query = \str_replace('?', '%s', $query);
1809 3
        $args = \array_map(
1810
            [
1811 3
                $db,
1812 3
                'escape',
1813
            ],
1814 3
            $args
1815
        );
1816 3
        \array_unshift($args, $query);
1817 3
        $query = \sprintf(...$args);
1818 3
        $result = $db->query($query);
1819
1820 3
        if ($result instanceof Result) {
1821 3
            return $result->fetchAllArrayyYield();
1822
        }
1823
1824 3
        return $result;
1825
    }
1826
1827
    /**
1828
     * Execute a sql-query.
1829
     *
1830
     * example:
1831
     * <code>
1832
     * $sql = "INSERT INTO TABLE_NAME_HERE
1833
     *   SET
1834
     *     foo = :foo,
1835
     *     bar = :bar
1836
     * ";
1837
     * $insert_id = $db->query(
1838
     *   $sql,
1839
     *   [
1840
     *     'foo' => 1.1,
1841
     *     'bar' => 1,
1842
     *   ]
1843
     * );
1844
     * </code>
1845
     *
1846
     * @param string     $sql               <p>The sql query-string.</p>
1847
     * @param array|bool $params            <p>
1848
     *                                      "array" of sql-query-parameters<br/>
1849
     *                                      "false" if you don't need any parameter (default)<br/>
1850
     *                                      </p>
1851
     *
1852
     * @throws QueryException
1853
     *
1854
     * @return bool|int|Result|string
1855
     *                                      <p>
1856
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
1857
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1858
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1859
     *                                      "true" by e.g. "DROP"-queries<br />
1860
     *                                      "false" on error
1861
     *                                      </p>
1862
     */
1863 140
    public function query(string $sql = '', $params = false)
1864
    {
1865 140
        if (!$this->isReady()) {
1866
            return false;
1867
        }
1868
1869 140
        if ($sql === '') {
1870 12
            $this->debug->displayError('Can not execute an empty query.', false);
1871
1872 12
            return false;
1873
        }
1874
1875
        if (
1876 134
            $params !== false
1877
            &&
1878 134
            \is_array($params)
1879
            &&
1880 134
            \count($params) > 0
1881
        ) {
1882 7
            $parseQueryParams = $this->_parseQueryParams($sql, $params);
1883 7
            $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1884 7
            $sql = $parseQueryParamsByName['sql'];
1885
        }
1886
1887
        // DEBUG
1888
        // var_dump($params);
1889
        // echo $sql . "\n";
1890
1891 134
        $query_start_time = \microtime(true);
1892 134
        $queryException = null;
1893 134
        $query_result_doctrine = false;
1894
1895 134
        if ($this->doctrine_connection) {
1896
            try {
1897
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1898
                $query_result = $query_result_doctrine->execute();
1899
                $mysqli_field_count = $query_result_doctrine->columnCount();
1900
            } catch (\Exception $e) {
1901
                $query_result = false;
1902
                $mysqli_field_count = null;
1903
1904
                $queryException = $e;
1905
            }
1906 134
        } elseif ($this->mysqli_link) {
1907 134
            $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1908 134
            $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1909
        } else {
1910
            $query_result = false;
1911
            $mysqli_field_count = null;
1912
1913
            $queryException = new DBConnectException('no mysql connection');
1914
        }
1915
1916 134
        $query_duration = \microtime(true) - $query_start_time;
1917
1918 134
        $this->query_count++;
1919
1920 134
        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...
1921 99
            if ($this->doctrine_connection) {
1922
                $result = false;
1923
                if (
1924
                    $query_result_doctrine
1925
                    &&
1926
                    $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1927
                ) {
1928
                    $result = $query_result_doctrine;
1929
                }
1930 99
            } elseif ($this->mysqli_link) {
1931 99
                $result = \mysqli_store_result($this->mysqli_link);
1932
            } else {
1933 99
                $result = false;
1934
            }
1935
        } else {
1936 106
            $result = $query_result;
1937
        }
1938
1939
        if (
1940 134
            $result instanceof \Doctrine\DBAL\Statement
1941
            &&
1942 134
            $result->columnCount() > 0
1943
        ) {
1944
1945
            // log the select query
1946
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1947
1948
            // return query result object
1949
            return new Result($sql, $result);
1950
        }
1951
1952 134
        if ($result instanceof \mysqli_result) {
1953
1954
            // log the select query
1955 96
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1956
1957
            // return query result object
1958 96
            return new Result($sql, $result);
1959
        }
1960
1961 112
        if ($query_result) {
1962
1963
            // "INSERT" || "REPLACE"
1964 103
            if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1965 100
                $insert_id = $this->insert_id();
1966
1967 100
                $this->debug->logQuery($sql, $query_duration, $insert_id);
1968
1969 100
                return $insert_id;
1970
            }
1971
1972
            // "UPDATE" || "DELETE"
1973 50
            if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1974 25
                if ($this->mysqli_link) {
1975 25
                    $this->affected_rows = $this->affected_rows();
1976
                } elseif ($query_result_doctrine) {
1977
                    $this->affected_rows = $query_result_doctrine->rowCount();
1978
                }
1979
1980 25
                $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1981
1982 25
                return $this->affected_rows;
1983
            }
1984
1985
            // log the ? query
1986 25
            $this->debug->logQuery($sql, $query_duration, 0);
1987
1988 25
            return true;
1989
        }
1990
1991
        // log the error query
1992 33
        $this->debug->logQuery($sql, $query_duration, 0, true);
1993
1994 33
        if ($queryException) {
1995
            return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1996
        }
1997
1998 33
        if ($this->mysqli_link) {
1999 33
            return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
2000
        }
2001
2002
        return false;
2003
    }
2004
2005
    /**
2006
     * Error-handling for the sql-query.
2007
     *
2008
     * @param string     $errorMessage
2009
     * @param int        $errorNumber
2010
     * @param string     $sql
2011
     * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
2012
     * @param bool       $sqlMultiQuery
2013
     *
2014
     * @throws QueryException
2015
     * @throws DBGoneAwayException
2016
     *
2017
     * @return false|mixed
2018
     */
2019 39
    private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
2020
    {
2021
        if (
2022 39
            $errorMessage === 'DB server has gone away'
2023
            ||
2024 36
            $errorMessage === 'MySQL server has gone away'
2025
            ||
2026 39
            $errorNumber === 2006
2027
        ) {
2028 3
            static $RECONNECT_COUNTER;
2029
2030
            // exit if we have more then 3 "DB server has gone away"-errors
2031 3
            if ($RECONNECT_COUNTER > 3) {
2032
                $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
2033
2034
                throw new DBGoneAwayException($errorMessage);
2035
            }
2036
2037 3
            $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2038
2039
            // reconnect
2040 3
            $RECONNECT_COUNTER++;
2041 3
            $this->reconnect(true);
2042
2043
            // re-run the current (non multi) query
2044 3
            if (!$sqlMultiQuery) {
2045 3
                return $this->query($sql, $sqlParams);
2046
            }
2047
2048
            return false;
2049
        }
2050
2051 36
        $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2052
2053 36
        $force_exception_after_error = null; // auto
2054 36
        if ($this->in_transaction) {
2055 12
            $force_exception_after_error = false;
2056
        }
2057
        // this query returned an error, we must display it (only for dev) !!!
2058
2059 36
        $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2060
2061 36
        return false;
2062
    }
2063
2064
    /**
2065
     * Quote && Escape e.g. a table name string.
2066
     *
2067
     * @param mixed $str
2068
     *
2069
     * @return string
2070
     */
2071 86
    public function quote_string($str): string
2072
    {
2073 86
        $str = \str_replace(
2074 86
            '`',
2075 86
            '``',
2076 86
            \trim(
2077 86
                (string) $this->escape($str, false),
2078 86
                '`'
2079
            )
2080
        );
2081
2082 86
        return '`' . $str . '`';
2083
    }
2084
2085
    /**
2086
     * Reconnect to the MySQL-Server.
2087
     *
2088
     * @param bool $checkViaPing
2089
     *
2090
     * @return bool
2091
     */
2092 7
    public function reconnect(bool $checkViaPing = false): bool
2093
    {
2094 7
        $ping = false;
2095 7
        if ($checkViaPing) {
2096 6
            $ping = $this->ping();
2097
        }
2098
2099 7
        if (!$ping) {
2100 7
            $this->connected = false;
2101 7
            $this->connect();
2102
        }
2103
2104 7
        return $this->isReady();
2105
    }
2106
2107
    /**
2108
     * Execute a "replace"-query.
2109
     *
2110
     * @param string      $table
2111
     * @param array       $data
2112
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2113
     *
2114
     * @throws QueryException
2115
     *
2116
     * @return false|int
2117
     *                   <p>false on error</p>
2118
     */
2119 3
    public function replace(string $table, array $data = [], string $databaseName = null)
2120
    {
2121
        // init
2122 3
        $table = \trim($table);
2123
2124 3
        if ($table === '') {
2125 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2126
2127 3
            return false;
2128
        }
2129
2130 3
        if (\count($data) === 0) {
2131 3
            $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2132
2133 3
            return false;
2134
        }
2135
2136
        // extracting column names
2137 3
        $columns = \array_keys($data);
2138 3
        foreach ($columns as $k => $_key) {
2139 3
            $columns[$k] = $this->quote_string($_key);
2140
        }
2141
2142 3
        $columns = \implode(',', $columns);
2143
2144
        // extracting values
2145 3
        foreach ($data as $k => $_value) {
2146 3
            $data[$k] = $this->secure($_value);
2147
        }
2148 3
        $values = \implode(',', $data);
2149
2150 3
        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...
2151
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2152
        }
2153
2154 3
        $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " (${columns}) VALUES (${values})";
2155
2156 3
        $return = $this->query($sql);
2157 3
        \assert(\is_int($return) || $return === false);
2158
2159 3
        return $return;
2160
    }
2161
2162
    /**
2163
     * Rollback in a transaction and end the transaction.
2164
     *
2165
     * @return bool
2166
     *              <p>bool true on success, false otherwise.</p>
2167
     */
2168 12 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...
2169
    {
2170 12
        if (!$this->in_transaction) {
2171
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
2172
2173
            return false;
2174
        }
2175
2176
        // init
2177 12
        $return = false;
2178
2179 12
        if ($this->mysqli_link) {
2180 12
            $return = \mysqli_rollback($this->mysqli_link);
2181 12
            \mysqli_autocommit($this->mysqli_link, true);
2182
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2183
            $this->doctrine_connection->rollBack();
2184
            $this->doctrine_connection->setAutoCommit(true);
2185
2186
            if ($this->doctrine_connection->isAutoCommit()) {
2187
                $return = true;
2188
            } else {
2189
                $return = false;
2190
            }
2191
        }
2192
2193 12
        $this->in_transaction = false;
2194
2195 12
        return $return;
2196
    }
2197
2198
    /**
2199
     * Try to secure a variable, so can you use it in sql-queries.
2200
     *
2201
     * <p>
2202
     * <strong>int:</strong> (also strings that contains only an int-value)<br />
2203
     * 1. parse into "int"
2204
     * </p><br />
2205
     *
2206
     * <p>
2207
     * <strong>float:</strong><br />
2208
     * 1. return "float"
2209
     * </p><br />
2210
     *
2211
     * <p>
2212
     * <strong>string:</strong><br />
2213
     * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2214
     * 2. trim '<br />
2215
     * 3. escape the string (and remove non utf-8 chars)<br />
2216
     * 4. trim ' again (because we maybe removed some chars)<br />
2217
     * 5. add ' around the new string<br />
2218
     * </p><br />
2219
     *
2220
     * <p>
2221
     * <strong>array:</strong><br />
2222
     * 1. return null
2223
     * </p><br />
2224
     *
2225
     * <p>
2226
     * <strong>object:</strong><br />
2227
     * 1. return false
2228
     * </p><br />
2229
     *
2230
     * <p>
2231
     * <strong>null:</strong><br />
2232
     * 1. return null
2233
     * </p>
2234
     *
2235
     * @param mixed     $var
2236
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2237
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2238
     *                                 <strong>null</strong> => Convert the array into null, every time.
2239
     *
2240
     * @return mixed
2241
     */
2242 87
    public function secure($var, $convert_array = true)
2243
    {
2244 87
        if (\is_array($var)) {
2245 6
            if ($convert_array === null) {
2246
                if ($this->convert_null_to_empty_string) {
2247
                    $var = "''";
2248
                } else {
2249
                    $var = 'NULL';
2250
                }
2251
            } else {
2252 6
                $varCleaned = [];
2253 6
                foreach ((array) $var as $key => $value) {
2254 6
                    $key = $this->escape($key, false, false, $convert_array);
2255 6
                    $value = $this->secure($value);
2256
2257
                    /** @noinspection OffsetOperationsInspection */
2258 6
                    $varCleaned[$key] = $value;
2259
                }
2260
2261 6 View Code Duplication
                if ($convert_array === true) {
2262 6
                    $varCleaned = \implode(',', $varCleaned);
2263
2264 6
                    $var = $varCleaned;
2265
                } else {
2266
                    $var = $varCleaned;
2267
                }
2268
            }
2269
2270 6
            return $var;
2271
        }
2272
2273 87
        if ($var === '') {
2274 6
            return "''";
2275
        }
2276
2277 87
        if ($var === "''") {
2278 3
            return "''";
2279
        }
2280
2281 87
        if ($var === null) {
2282 3
            if ($this->convert_null_to_empty_string) {
2283 3
                return "''";
2284
            }
2285
2286 3
            return 'NULL';
2287
        }
2288
2289 87
        if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2290 3
            return $var;
2291
        }
2292
2293 87
        if (\is_string($var)) {
2294 77
            $var = \trim($var, "'");
2295
        }
2296
2297 87
        $var = $this->escape($var, false, false, null);
2298
2299 84
        if (\is_string($var)) {
2300 77
            $var = "'" . \trim($var, "'") . "'";
2301
        }
2302
2303 84
        return $var;
2304
    }
2305
2306
    /**
2307
     * Execute a "select"-query.
2308
     *
2309
     * @param string       $table
2310
     * @param array|string $where
2311
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2312
     *
2313
     * @throws QueryException
2314
     *
2315
     * @return false|Result
2316
     *                      <p>false on error</p>
2317
     */
2318 62 View Code Duplication
    public function select(
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...
2319
        string $table,
2320
        $where = '1=1',
2321
        string $databaseName = null
2322
    ) {
2323
        // init
2324 62
        $table = \trim($table);
2325
2326 62
        if ($table === '') {
2327 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2328
2329 3
            return false;
2330
        }
2331
2332 62
        if (\is_string($where)) {
2333 25
            $WHERE = $this->escape($where, false);
2334 41
        } elseif (\is_array($where)) {
2335 41
            $WHERE = $this->_parseArrayPair($where, 'AND');
2336
        } else {
2337 3
            $WHERE = '';
2338
        }
2339
2340 62
        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...
2341
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2342
        }
2343
2344 62
        $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
2345
2346 62
        $return = $this->query($sql);
2347 62
        \assert($return instanceof Result || $return === false);
2348
2349 62
        return $return;
2350
    }
2351
2352
    /**
2353
     * Selects a different database than the one specified on construction.
2354
     *
2355
     * @param string $database <p>Database name to switch to.</p>
2356
     *
2357
     * @return bool
2358
     *              <p>bool true on success, false otherwise.</p>
2359
     */
2360
    public function select_db(string $database): bool
2361
    {
2362
        if (!$this->isReady()) {
2363
            return false;
2364
        }
2365
2366
        if ($this->mysqli_link) {
2367
            return \mysqli_select_db($this->mysqli_link, $database);
2368
        }
2369
2370
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2371
            $return = $this->query('use :database', ['database' => $database]);
2372
            \assert(\is_bool($return));
2373
2374
            return $return;
2375
        }
2376
2377
        return false;
2378
    }
2379
2380
    /**
2381
     * @param array  $extra_config          <p>
2382
     *                                      'session_to_db' => bool<br>
2383
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
2384
     *                                      'socket'        => string (path)<br>
2385
     *                                      'flags'         => null|int<br>
2386
     *                                      'ssl'           => bool<br>
2387
     *                                      'clientkey'     => string (path)<br>
2388
     *                                      'clientcert'    => string (path)<br>
2389
     *                                      'cacert'        => string (path)<br>
2390
     *                                      </p>
2391
     */
2392 24
    public function setConfigExtra(array $extra_config)
2393
    {
2394 24
        if (isset($extra_config['session_to_db'])) {
2395
            $this->session_to_db = (bool) $extra_config['session_to_db'];
2396
        }
2397
2398 24
        if (isset($extra_config['doctrine'])) {
2399
            if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2400
                $this->doctrine_connection = $extra_config['doctrine'];
2401
            } else {
2402
                throw new DBConnectException('Error "doctrine"-connection is not valid');
2403
            }
2404
        }
2405
2406 24
        if (isset($extra_config['socket'])) {
2407
            $this->socket = $extra_config['socket'];
2408
        }
2409
2410 24
        if (isset($extra_config['flags'])) {
2411 1
            $this->flags = $extra_config['flags'];
2412
        }
2413
2414 24
        if (isset($extra_config['ssl'])) {
2415
            $this->ssl = $extra_config['ssl'];
2416
        }
2417
2418 24
        if (isset($extra_config['clientkey'])) {
2419
            $this->clientkey = $extra_config['clientkey'];
2420
        }
2421
2422 24
        if (isset($extra_config['clientcert'])) {
2423
            $this->clientcert = $extra_config['clientcert'];
2424
        }
2425
2426 24
        if (isset($extra_config['cacert'])) {
2427
            $this->cacert = $extra_config['cacert'];
2428
        }
2429 24
    }
2430
2431
    /**
2432
     * Set the current charset.
2433
     *
2434
     * @param string $charset
2435
     *
2436
     * @return bool
2437
     */
2438 15
    public function set_charset(string $charset): bool
2439
    {
2440 15
        $charsetLower = \strtolower($charset);
2441 15
        if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2442 9
            $charset = 'utf8';
2443
        }
2444 15
        if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this)) {
2445 9
            $charset = 'utf8mb4';
2446
        }
2447
2448 15
        $this->charset = $charset;
2449
2450
        if (
2451 15
            $this->mysqli_link
2452
            &&
2453 15
            $this->mysqli_link instanceof \mysqli
2454
        ) {
2455 15
            $return = \mysqli_set_charset($this->mysqli_link, $charset);
2456
2457
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2458 15
            @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2459
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2460 15
            @\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...
2461
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2462
            $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2463
            if (!$doctrineWrappedConnection instanceof Connection) {
2464
                return false;
2465
            }
2466
2467
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2468
            @$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...
2469
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2470
            @$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...
2471
2472
            $return = true;
2473
        } else {
2474
            $return = false;
2475
        }
2476
2477 15
        return $return;
2478
    }
2479
2480
    /**
2481
     * Set the option to convert null to "''" (empty string).
2482
     *
2483
     * Used in secure() => select(), insert(), update(), delete()
2484
     *
2485
     * @deprecated It's not recommended to convert NULL into an empty string!
2486
     *
2487
     * @param bool $bool
2488
     *
2489
     * @return self
2490
     */
2491 3
    public function set_convert_null_to_empty_string(bool $bool): self
2492
    {
2493 3
        $this->convert_null_to_empty_string = $bool;
2494
2495 3
        return $this;
2496
    }
2497
2498
    /**
2499
     * Enables or disables internal report functions
2500
     *
2501
     * @see http://php.net/manual/en/function.mysqli-report.php
2502
     *
2503
     * @param int $flags <p>
2504
     *                   <table>
2505
     *                   Supported flags
2506
     *                   <tr valign="top">
2507
     *                   <td>Name</td>
2508
     *                   <td>Description</td>
2509
     *                   </tr>
2510
     *                   <tr valign="top">
2511
     *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2512
     *                   <td>Turns reporting off</td>
2513
     *                   </tr>
2514
     *                   <tr valign="top">
2515
     *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2516
     *                   <td>Report errors from mysqli function calls</td>
2517
     *                   </tr>
2518
     *                   <tr valign="top">
2519
     *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2520
     *                   <td>
2521
     *                   Throw <b>mysqli_sql_exception</b> for errors
2522
     *                   instead of warnings
2523
     *                   </td>
2524
     *                   </tr>
2525
     *                   <tr valign="top">
2526
     *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2527
     *                   <td>Report if no index or bad index was used in a query</td>
2528
     *                   </tr>
2529
     *                   <tr valign="top">
2530
     *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2531
     *                   <td>Set all options (report all)</td>
2532
     *                   </tr>
2533
     *                   </table>
2534
     *                   </p>
2535
     *
2536
     * @return bool
2537
     */
2538
    public function set_mysqli_report(int $flags): bool
2539
    {
2540
        if (
2541
            $this->mysqli_link
2542
            &&
2543
            $this->mysqli_link instanceof \mysqli
2544
        ) {
2545
            return \mysqli_report($flags);
2546
        }
2547
2548
        return false;
2549
    }
2550
2551
    /**
2552
     * Show config errors by throw exceptions.
2553
     *
2554
     * @throws \InvalidArgumentException
2555
     *
2556
     * @return bool
2557
     */
2558 24
    public function showConfigError(): bool
2559
    {
2560
        // check if a doctrine connection is already open, first
2561
        if (
2562 24
            $this->doctrine_connection
2563
            &&
2564 24
            $this->doctrine_connection->isConnected()
2565
        ) {
2566
            return true;
2567
        }
2568
2569
        if (
2570 24
            !$this->hostname
2571
            ||
2572 21
            !$this->username
2573
            ||
2574 24
            !$this->database
2575
        ) {
2576 9
            if (!$this->hostname) {
2577 3
                throw new \InvalidArgumentException('no-sql-hostname');
2578
            }
2579
2580 6
            if (!$this->username) {
2581 3
                throw new \InvalidArgumentException('no-sql-username');
2582
            }
2583
2584 3
            if (!$this->database) {
2585 3
                throw new \InvalidArgumentException('no-sql-database');
2586
            }
2587
2588
            return false;
2589
        }
2590
2591 15
        return true;
2592
    }
2593
2594
    /**
2595
     * alias: "beginTransaction()"
2596
     */
2597 3
    public function startTransaction(): bool
2598
    {
2599 3
        return $this->beginTransaction();
2600
    }
2601
2602
    /**
2603
     * Determine if database table exists
2604
     *
2605
     * @param string $table
2606
     *
2607
     * @return bool
2608
     */
2609 3
    public function table_exists(string $table): bool
2610
    {
2611 3
        $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2612
2613 3
        return $check !== false
2614
               &&
2615 3
               $check instanceof Result
2616
               &&
2617 3
               $check->num_rows > 0;
2618
    }
2619
2620
    /**
2621
     * Execute a callback inside a transaction.
2622
     *
2623
     * @param \Closure $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2624
     *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2625
     *
2626
     * @return bool
2627
     *              <p>bool true on success, false otherwise.</p>
2628
     */
2629 3
    public function transact($callback): bool
2630
    {
2631
        try {
2632 3
            $beginTransaction = $this->beginTransaction();
2633 3
            if (!$beginTransaction) {
2634 3
                $this->debug->displayError('Error: transact -> can not start transaction!', false);
2635
2636 3
                return false;
2637
            }
2638
2639 3
            $result = $callback($this);
2640 3
            if ($result === false) {
2641
                /** @noinspection ThrowRawExceptionInspection */
2642 3
                throw new \Exception('call_user_func [' . \print_r($callback, true) . '] === false');
2643
            }
2644
2645 3
            return $this->commit();
2646 3
        } catch (\Exception $e) {
2647 3
            $this->rollback();
2648
2649 3
            return false;
2650
        }
2651
    }
2652
2653
    /**
2654
     * Execute a "update"-query.
2655
     *
2656
     * @param string       $table
2657
     * @param array        $data
2658
     * @param array|string $where
2659
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2660
     *
2661
     * @throws QueryException
2662
     *
2663
     * @return false|int
2664
     *                   <p>false on error</p>
2665
     */
2666 21
    public function update(
2667
        string $table,
2668
        array $data = [],
2669
        $where = '1=1',
2670
        string $databaseName = null
2671
    ) {
2672
        // init
2673 21
        $table = \trim($table);
2674
2675 21
        if ($table === '') {
2676 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2677
2678 3
            return false;
2679
        }
2680
2681 21
        if (\count($data) === 0) {
2682 6
            $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2683
2684 6
            return false;
2685
        }
2686
2687
        // DEBUG
2688
        //var_dump($data);
2689
2690 21
        $SET = $this->_parseArrayPair($data);
2691
2692
        // DEBUG
2693
        //var_dump($SET);
2694
2695 21
        if (\is_string($where)) {
2696 6
            $WHERE = $this->escape($where, false);
2697 18
        } elseif (\is_array($where)) {
2698 15
            $WHERE = $this->_parseArrayPair($where, 'AND');
2699
        } else {
2700 3
            $WHERE = '';
2701
        }
2702
2703 21
        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...
2704
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2705
        }
2706
2707 21
        $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET ${SET} WHERE (${WHERE})";
2708
2709 21
        $return = $this->query($sql);
2710 21
        \assert(\is_int($return) || $return === false);
2711
2712 21
        return $return;
2713
    }
2714
}
2715