Completed
Push — master ( 578b2d...d7dae2 )
by Lars
13:43 queued 10s
created

DB::prepare()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Doctrine\DBAL\Connection;
8
use voku\cache\Cache;
9
use voku\db\exceptions\DBConnectException;
10
use voku\db\exceptions\DBGoneAwayException;
11
use voku\db\exceptions\QueryException;
12
use voku\helper\UTF8;
13
14
/**
15
 * DB: This class can handle DB queries via MySQLi.
16
 */
17
final class DB
18
{
19
    /**
20
     * @var int
21
     */
22
    public $query_count = 0;
23
24
    /**
25
     * @var \mysqli|null
26
     */
27
    private $mysqli_link;
28
29
    /**
30
     * @var bool
31
     */
32
    private $connected = false;
33
34
    /**
35
     * @var array
36
     */
37
    private $mysqlDefaultTimeFunctions;
38
39
    /**
40
     * @var string
41
     */
42
    private $hostname = '';
43
44
    /**
45
     * @var string
46
     */
47
    private $username = '';
48
49
    /**
50
     * @var string
51
     */
52
    private $password = '';
53
54
    /**
55
     * @var string
56
     */
57
    private $database = '';
58
59
    /**
60
     * @var int
61
     */
62
    private $port = 3306;
63
64
    /**
65
     * @var string
66
     */
67
    private $charset = 'utf8';
68
69
    /**
70
     * @var string
71
     */
72
    private $socket = '';
73
74
    /**
75
     * @var int|null
76
     */
77
    private $flags;
78
79
    /**
80
     * @var bool
81
     */
82
    private $session_to_db = false;
83
84
    /**
85
     * @var bool
86
     */
87
    private $in_transaction = false;
88
89
    /**
90
     * @var bool
91
     */
92
    private $convert_null_to_empty_string = false;
93
94
    /**
95
     * @var bool
96
     */
97
    private $ssl = false;
98
99
    /**
100
     * The path name to the key file
101
     *
102
     * @var string
103
     */
104
    private $clientkey;
105
106
    /**
107
     * The path name to the certificate file
108
     *
109
     * @var string
110
     */
111
    private $clientcert;
112
113
    /**
114
     * The path name to the certificate authority file
115
     *
116
     * @var string
117
     */
118
    private $cacert;
119
120
    /**
121
     * @var Debug
122
     */
123
    private $debug;
124
125
    /**
126
     * @var \Doctrine\DBAL\Connection|null
127
     */
128
    private $doctrine_connection;
129
130
    /**
131
     * @var int
132
     */
133
    private $affected_rows = 0;
134
135
    /**
136
     * __construct()
137
     *
138
     * @param string $hostname
139
     * @param string $username
140
     * @param string $password
141
     * @param string $database
142
     * @param int    $port
143
     * @param string $charset
144
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
145
     *                                      'false'. Use false to disable it.</p>
146
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
147
     *                                      Use false to disable it.</p>
148
     * @param string $logger_class_name
149
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
150
     * @param array  $extra_config          <p>
151
     *                                      'session_to_db' => bool<br>
152
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
153
     *                                      'socket'        => string (path)<br>
154
     *                                      'flags'         => null|int<br>
155
     *                                      'ssl'           => bool<br>
156
     *                                      'clientkey'     => string (path)<br>
157
     *                                      'clientcert'    => string (path)<br>
158
     *                                      'cacert'        => string (path)<br>
159
     *                                      </p>
160
     */
161 24
    private function __construct(
162
        string $hostname,
163
        string $username,
164
        string $password,
165
        string $database,
166
        $port,
167
        string $charset,
168
        bool $exit_on_error,
169
        bool $echo_on_error,
170
        string $logger_class_name,
171
        string $logger_level,
172
        array $extra_config = []
173
    ) {
174 24
        $this->debug = new Debug($this);
175
176 24
        $this->_loadConfig(
177 24
            $hostname,
178 24
            $username,
179 24
            $password,
180 24
            $database,
181 24
            $port,
182 24
            $charset,
183 24
            $exit_on_error,
184 24
            $echo_on_error,
185 24
            $logger_class_name,
186 24
            $logger_level,
187 24
            $extra_config
188
        );
189
190 15
        $this->connect();
191
192 6
        $this->mysqlDefaultTimeFunctions = [
193
            // Returns the current date.
194
            'CURDATE()',
195
            // CURRENT_DATE	| Synonyms for CURDATE()
196
            'CURRENT_DATE()',
197
            // CURRENT_TIME	| Synonyms for CURTIME()
198
            'CURRENT_TIME()',
199
            // CURRENT_TIMESTAMP | Synonyms for NOW()
200
            'CURRENT_TIMESTAMP()',
201
            // Returns the current time.
202
            'CURTIME()',
203
            // Synonym for NOW()
204
            'LOCALTIME()',
205
            // Synonym for NOW()
206
            'LOCALTIMESTAMP()',
207
            // Returns the current date and time.
208
            'NOW()',
209
            // Returns the time at which the function executes.
210
            'SYSDATE()',
211
            // Returns a UNIX timestamp.
212
            'UNIX_TIMESTAMP()',
213
            // Returns the current UTC date.
214
            'UTC_DATE()',
215
            // Returns the current UTC time.
216
            'UTC_TIME()',
217
            // Returns the current UTC date and time.
218
            'UTC_TIMESTAMP()',
219
        ];
220 6
    }
221
222
    /**
223
     * Prevent the instance from being cloned.
224
     *
225
     * @return void
226
     */
227
    private function __clone()
228
    {
229
    }
230
231
    /**
232
     * __destruct
233
     */
234
    public function __destruct()
235
    {
236
        // close the connection only if we don't save PHP-SESSION's in DB
237
        if (!$this->session_to_db) {
238
            $this->close();
239
        }
240
    }
241
242
    /**
243
     * @param string|null $sql
244
     * @param array       $bindings
245
     *
246
     * @return bool|DB|int|Result|string
247
     *                                      <p>
248
     *                                      "DB" by "$sql" === null<br />
249
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
250
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
251
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
252
     *                                      "true" by e.g. "DROP"-queries<br />
253
     *                                      "false" on error
254
     *                                      </p>
255
     */
256 4
    public function __invoke(string $sql = null, array $bindings = [])
257
    {
258 4
        return $sql !== null ? $this->query($sql, $bindings) : $this;
259
    }
260
261
    /**
262
     * __wakeup
263
     *
264
     * @return void
265
     */
266 4
    public function __wakeup()
267
    {
268 4
        $this->reconnect();
269 4
    }
270
271
    /**
272
     * Load the config from the constructor.
273
     *
274
     * @param string $hostname
275
     * @param string $username
276
     * @param string $password
277
     * @param string $database
278
     * @param int    $port                  <p>default is (int)3306</p>
279
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
280
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
281
     *                                      'false'. Use false to disable it.</p>
282
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
283
     *                                      Use false to disable it.</p>
284
     * @param string $logger_class_name
285
     * @param string $logger_level
286
     * @param array  $extra_config          <p>
287
     *                                      'session_to_db' => bool<br>
288
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
289
     *                                      'socket'        => string (path)<br>
290
     *                                      'flags'         => null|int<br>
291
     *                                      'ssl'           => bool<br>
292
     *                                      'clientkey'     => string (path)<br>
293
     *                                      'clientcert'    => string (path)<br>
294
     *                                      'cacert'        => string (path)<br>
295
     *                                      </p>
296
     *
297
     * @return bool
298
     */
299 24
    private function _loadConfig(
300
        string $hostname,
301
        string $username,
302
        string $password,
303
        string $database,
304
        $port,
305
        string $charset,
306
        bool $exit_on_error,
307
        bool $echo_on_error,
308
        string $logger_class_name,
309
        string $logger_level,
310
        array $extra_config = []
311
    ): bool {
312 24
        $this->hostname = $hostname;
313 24
        $this->username = $username;
314 24
        $this->password = $password;
315 24
        $this->database = $database;
316
317 24
        if ($charset) {
318 24
            $this->charset = $charset;
319
        }
320
321 24
        if ($port) {
322 12
            $this->port = (int) $port;
323
        } else {
324
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
325 13
            $this->port = (int) @\ini_get('mysqli.default_port');
326
        }
327
328
        // fallback
329 24
        if (!$this->port) {
330
            $this->port = 3306;
331
        }
332
333
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
334
        if (
335 24
            !$this->socket
336
            &&
337 24
            ($defaultSocket = @\ini_get('mysqli.default_socket'))
338
            &&
339 24
            \is_readable($defaultSocket)
340
        ) {
341 24
            $this->socket = $defaultSocket;
342
        }
343
344 24
        $this->debug->setExitOnError($exit_on_error);
345 24
        $this->debug->setEchoOnError($echo_on_error);
346
347 24
        $this->debug->setLoggerClassName($logger_class_name);
348 24
        $this->debug->setLoggerLevel($logger_level);
349
350 24
        $this->setConfigExtra($extra_config);
351
352 24
        return $this->showConfigError();
353
    }
354
355
    /**
356
     * Parses arrays with value pairs and generates SQL to use in queries.
357
     *
358
     * @param array  $arrayPair
359
     * @param string $glue <p>This is the separator.</p>
360
     *
361
     * @return string
362
     *
363
     * @internal
364
     */
365 72
    public function _parseArrayPair(array $arrayPair, string $glue = ','): string
366
    {
367
        // init
368 72
        $sql = '';
369
370 72
        if (\count($arrayPair) === 0) {
371
            return '';
372
        }
373
374 72
        $arrayPairCounter = 0;
375 72
        foreach ($arrayPair as $_key => $_value) {
376 72
            $_connector = '=';
377 72
            $_glueHelper = '';
378 72
            $_key_upper = \strtoupper((string) $_key);
379
380 72
            if (\strpos($_key_upper, ' NOT') !== false) {
381 6
                $_connector = 'NOT';
382
            }
383
384 72
            if (\strpos($_key_upper, ' IS') !== false) {
385 3
                $_connector = 'IS';
386
            }
387
388 72
            if (\strpos($_key_upper, ' IS NOT') !== false) {
389 3
                $_connector = 'IS NOT';
390
            }
391
392 72
            if (\strpos($_key_upper, ' IN') !== false) {
393 3
                $_connector = 'IN';
394
            }
395
396 72
            if (\strpos($_key_upper, ' NOT IN') !== false) {
397 3
                $_connector = 'NOT IN';
398
            }
399
400 72
            if (\strpos($_key_upper, ' BETWEEN') !== false) {
401 3
                $_connector = 'BETWEEN';
402
            }
403
404 72
            if (\strpos($_key_upper, ' NOT BETWEEN') !== false) {
405 3
                $_connector = 'NOT BETWEEN';
406
            }
407
408 72
            if (\strpos($_key_upper, ' LIKE') !== false) {
409 6
                $_connector = 'LIKE';
410
            }
411
412 72
            if (\strpos($_key_upper, ' NOT LIKE') !== false) {
413 6
                $_connector = 'NOT LIKE';
414
            }
415
416 72 View Code Duplication
            if (\strpos($_key_upper, ' >') !== false && \strpos($_key_upper, ' =') === false) {
417 8
                $_connector = '>';
418
            }
419
420 72 View Code Duplication
            if (\strpos($_key_upper, ' <') !== false && \strpos($_key_upper, ' =') === false) {
421 3
                $_connector = '<';
422
            }
423
424 72
            if (\strpos($_key_upper, ' >=') !== false) {
425 8
                $_connector = '>=';
426
            }
427
428 72
            if (\strpos($_key_upper, ' <=') !== false) {
429 3
                $_connector = '<=';
430
            }
431
432 72
            if (\strpos($_key_upper, ' <>') !== false) {
433 3
                $_connector = '<>';
434
            }
435
436 72
            if (\strpos($_key_upper, ' OR') !== false) {
437 6
                $_glueHelper = 'OR';
438
            }
439
440 72
            if (\strpos($_key_upper, ' AND') !== false) {
441 3
                $_glueHelper = 'AND';
442
            }
443
444 72
            if (\is_array($_value)) {
445 7
                $firstKey = null;
446 7
                $firstValue = null;
447 7
                foreach ($_value as $oldKey => $oldValue) {
448 7
                    $_value[$oldKey] = $this->secure($oldValue);
449
450 7
                    if ($firstKey === null) {
451 7
                        $firstKey = $oldKey;
452
                    }
453
454 7
                    if ($firstValue === null) {
455 7
                        $firstValue = $_value[$oldKey];
456
                    }
457
                }
458
459 7
                if ($_connector === 'NOT IN' || $_connector === 'IN') {
460 3
                    $_value = '(' . \implode(',', $_value) . ')';
461 7
                } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
462 3
                    $_value = '(' . \implode(' AND ', $_value) . ')';
463 7
                } elseif ($firstKey && $firstValue) {
464 1 View Code Duplication
                    if (\strpos((string) $firstKey, ' +') !== false) {
465 1
                        $firstKey = \str_replace(' +', '', (string) $firstKey);
466 1
                        $_value = $firstKey . ' + ' . $firstValue;
467
                    }
468
469 1 View Code Duplication
                    if (\strpos((string) $firstKey, ' -') !== false) {
470 1
                        $firstKey = \str_replace(' -', '', (string) $firstKey);
471 7
                        $_value = $firstKey . ' - ' . $firstValue;
472
                    }
473
                }
474
            } else {
475 72
                $_value = $this->secure($_value);
476
            }
477
478 72
            $quoteString = $this->quote_string(
479 72
                \trim(
480 72
                    (string) \str_ireplace(
481
                        [
482 72
                            $_connector,
483 72
                            $_glueHelper,
484
                        ],
485 72
                        '',
486 72
                        (string) $_key
487
                    )
488
                )
489
            );
490
491 72
            $_value = (array) $_value;
492
493 72
            if (!$_glueHelper) {
494 72
                $_glueHelper = $glue;
495
            }
496
497 72
            $tmpCounter = 0;
498 72
            foreach ($_value as $valueInner) {
499 72
                $_glueHelperInner = $_glueHelper;
500
501 72
                if ($arrayPairCounter === 0) {
502 72
                    if ($tmpCounter === 0 && $_glueHelper === 'OR') {
503 3
                        $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
504 72
                    } elseif ($tmpCounter === 0) {
505 72
                        $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
506
                    }
507 68
                } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
508 3
                    $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
509
                }
510
511 72
                if (\is_string($valueInner) && $valueInner === '') {
512
                    $valueInner = "''";
513
                }
514
515 72
                $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
516 72
                $tmpCounter++;
517
            }
518
519 72
            if ($_glueHelper === 'OR') {
520 6
                $sql .= ' ) ';
521
            }
522
523 72
            $arrayPairCounter++;
524
        }
525
526 72
        return $sql;
527
    }
528
529
    /**
530
     * _parseQueryParams
531
     *
532
     * @param string $sql
533
     * @param array  $params
534
     *
535
     * @return array
536
     *               <p>with the keys -> 'sql', 'params'</p>
537
     */
538 7
    private function _parseQueryParams(string $sql, array $params = []): array
539
    {
540 7
        $offset = \strpos($sql, '?');
541
542
        // is there anything to parse?
543
        if (
544 7
            $offset === false
545
            ||
546 7
            \count($params) === 0
547
        ) {
548 3
            return ['sql' => $sql, 'params' => $params];
549
        }
550
551 7
        foreach ($params as $key => $param) {
552
553
            // use this only for not named parameters
554 7
            if (!\is_int($key)) {
555 3
                continue;
556
            }
557
558 7
            if ($offset === false) {
559
                continue;
560
            }
561
562 7
            $replacement = $this->secure($param);
563
564 7
            unset($params[$key]);
565
566 7
            $sql = \substr_replace($sql, $replacement, $offset, 1);
567 7
            $offset = \strpos($sql, '?', $offset + \strlen((string) $replacement));
568
        }
569
570 7
        return ['sql' => $sql, 'params' => $params];
571
    }
572
573
    /**
574
     * Returns the SQL by replacing :placeholders with SQL-escaped values.
575
     *
576
     * @param string $sql    <p>The SQL string.</p>
577
     * @param array  $params <p>An array of key-value bindings.</p>
578
     *
579
     * @return array
580
     *               <p>with the keys -> 'sql', 'params'</p>
581
     */
582 10
    private function _parseQueryParamsByName(string $sql, array $params = []): array
583
    {
584
        // is there anything to parse?
585
        if (
586 10
            \strpos($sql, ':') === false
587
            ||
588 10
            \count($params) === 0
589
        ) {
590 7
            return ['sql' => $sql, 'params' => $params];
591
        }
592
593 6
        $offset = null;
594 6
        $replacement = null;
595 6
        foreach ($params as $name => $param) {
596
597
            // use this only for named parameters
598 6
            if (\is_int($name)) {
599
                continue;
600
            }
601
602
            // add ":" if needed
603 6
            if (\strpos($name, ':') !== 0) {
604 6
                $nameTmp = ':' . $name;
605
            } else {
606
                $nameTmp = $name;
607
            }
608
609 6
            if ($offset === null) {
610 6
                $offset = \strpos($sql, $nameTmp);
611
            } else {
612 6
                $offset = \strpos($sql, $nameTmp, $offset + \strlen((string) $replacement));
613
            }
614
615 6
            if ($offset === false) {
616 3
                continue;
617
            }
618
619 6
            $replacement = $this->secure($param);
620
621 6
            unset($params[$name]);
622
623 6
            $sql = \substr_replace($sql, $replacement, $offset, \strlen($nameTmp));
624
        }
625
626 6
        return ['sql' => $sql, 'params' => $params];
627
    }
628
629
    /**
630
     * Gets the number of affected rows in a previous MySQL operation.
631
     *
632
     * @return int
633
     */
634 25
    public function affected_rows(): int
635
    {
636
        if (
637 25
            $this->mysqli_link
638
            &&
639 25
            $this->mysqli_link instanceof \mysqli
640
        ) {
641 25
            return \mysqli_affected_rows($this->mysqli_link);
642
        }
643
644
        return (int) $this->affected_rows;
645
    }
646
647
    /**
648
     * Begins a transaction, by turning off auto commit.
649
     *
650
     * @return bool
651
     *              <p>This will return true or false indicating success of transaction</p>
652
     */
653 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...
654
    {
655 18
        if ($this->in_transaction) {
656 6
            $this->debug->displayError('Error: mysql server already in transaction!', false);
657
658 6
            return false;
659
        }
660
661 18
        $this->clearErrors(); // needed for "$this->endTransaction()"
662 18
        $this->in_transaction = true;
663
664 18
        if ($this->mysqli_link) {
665 18
            $return = \mysqli_autocommit($this->mysqli_link, false);
666
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
667
            $this->doctrine_connection->setAutoCommit(false);
668
            $this->doctrine_connection->beginTransaction();
669
670
            if ($this->doctrine_connection->isTransactionActive()) {
671
                $return = true;
672
            } else {
673
                $return = false;
674
            }
675
        } else {
676
            $return = false;
677
        }
678
679 18
        if (!$return) {
680
            $this->in_transaction = false;
681
        }
682
683 18
        return $return;
684
    }
685
686
    /**
687
     * Clear the errors in "_debug->_errors".
688
     *
689
     * @return bool
690
     */
691 18
    public function clearErrors(): bool
692
    {
693 18
        return $this->debug->clearErrors();
694
    }
695
696
    /**
697
     * Closes a previously opened database connection.
698
     *
699
     * @return bool
700
     *              Will return "true", if the connection was closed,
701
     *              otherwise (e.g. if the connection was already closed) "false".
702
     */
703 6
    public function close(): bool
704
    {
705 6
        $this->connected = false;
706
707
        if (
708 6
            $this->doctrine_connection
709
            &&
710 6
            $this->doctrine_connection instanceof \Doctrine\DBAL\Connection
711
        ) {
712
            $connectedBefore = $this->doctrine_connection->isConnected();
713
714
            $this->doctrine_connection->close();
715
716
            $this->mysqli_link = null;
717
718
            if ($connectedBefore) {
719
                return !$this->doctrine_connection->isConnected();
720
            }
721
722
            return false;
723
        }
724
725
        if (
726 6
            $this->mysqli_link
727
            &&
728 6
            $this->mysqli_link instanceof \mysqli
729
        ) {
730 6
            $result = \mysqli_close($this->mysqli_link);
731 6
            $this->mysqli_link = null;
732
733 6
            return $result;
734
        }
735
736 3
        $this->mysqli_link = null;
737
738 3
        return false;
739
    }
740
741
    /**
742
     * Commits the current transaction and end the transaction.
743
     *
744
     * @return bool
745
     *              <p>bool true on success, false otherwise.</p>
746
     */
747 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...
748
    {
749 9
        if (!$this->in_transaction) {
750
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
751
752
            return false;
753
        }
754
755 9
        if ($this->mysqli_link) {
756 9
            $return = \mysqli_commit($this->mysqli_link);
757 9
            \mysqli_autocommit($this->mysqli_link, true);
758
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
759
            $this->doctrine_connection->commit();
760
            $this->doctrine_connection->setAutoCommit(true);
761
762
            if ($this->doctrine_connection->isAutoCommit()) {
763
                $return = true;
764
            } else {
765
                $return = false;
766
            }
767
        } else {
768
            $return = false;
769
        }
770
771 9
        $this->in_transaction = false;
772
773 9
        return $return;
774
    }
775
776
    /**
777
     * Open a new connection to the MySQL server.
778
     *
779
     * @throws DBConnectException
780
     *
781
     * @return bool
782
     */
783 21
    public function connect(): bool
784
    {
785 21
        if ($this->isReady()) {
786 3
            return true;
787
        }
788
789 21
        if ($this->doctrine_connection) {
790
            $this->doctrine_connection->connect();
791
792
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
793
794
            if ($this->isDoctrineMySQLiConnection()) {
795
                \assert($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection);
796
797
                $this->mysqli_link = $doctrineWrappedConnection->getWrappedResourceHandle();
798
799
                return $this->connect_helper();
800
            }
801
802
            if ($this->isDoctrinePDOConnection()) {
803
                $this->mysqli_link = null;
804
805
                return $this->connect_helper();
806
            }
807
        }
808
809 21
        $flags = $this->flags;
810
811 21
        \mysqli_report(\MYSQLI_REPORT_STRICT);
812
813
        try {
814 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...
815
816 21
            if (Helper::isMysqlndIsUsed()) {
817 21
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
818
            }
819
820 21
            if ($this->ssl) {
821
                if (empty($this->clientcert)) {
822
                    throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
823
                }
824
825
                if (empty($this->clientkey)) {
826
                    throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
827
                }
828
829
                if (empty($this->cacert)) {
830
                    throw new DBConnectException('Error connecting to mysql server: cacert not defined');
831
                }
832
833
                \mysqli_options($this->mysqli_link, \MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
834
835
                \mysqli_ssl_set(
836
                    $this->mysqli_link,
837
                    $this->clientkey,
838
                    $this->clientcert,
839
                    $this->cacert,
840
                    '',
841
                    ''
842
                );
843
844
                $flags |= \MYSQLI_CLIENT_SSL;
845
            }
846
847
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
848 21
            $this->connected = @\mysqli_real_connect(
849 21
                $this->mysqli_link,
850 21
                $this->hostname,
851 21
                $this->username,
852 21
                $this->password,
853 21
                $this->database,
854 21
                $this->port,
855 21
                $this->socket,
856 21
                (int) $flags
857
            );
858 9
        } catch (\Exception $e) {
859 9
            $error = 'Error connecting to mysql server: ' . $e->getMessage();
860 9
            $this->debug->displayError($error, false);
861
862 9
            throw new DBConnectException($error, 100, $e);
863
        }
864 12
        \mysqli_report(\MYSQLI_REPORT_OFF);
865
866 12
        $errno = \mysqli_connect_errno();
867 12
        if (!$this->connected || $errno) {
868
            $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
869
            $this->debug->displayError($error, false);
870
871
            throw new DBConnectException($error, 101);
872
        }
873
874 12
        $this->set_charset($this->charset);
875
876 12
        return $this->isReady();
877
    }
878
879
    /**
880
     * @return bool
881
     */
882
    private function connect_helper(): bool
883
    {
884
        if (!$this->doctrine_connection) {
885
            $this->connected = false;
886
        } else {
887
            $this->connected = $this->doctrine_connection->isConnected();
888
        }
889
890
        if (!$this->connected) {
891
            $error = 'Error connecting to mysql server: ' . \print_r($this->doctrine_connection ? $this->doctrine_connection->errorInfo() : [], false);
892
            $this->debug->displayError($error, false);
893
894
            throw new DBConnectException($error, 101);
895
        }
896
897
        $this->set_charset($this->charset);
898
899
        return $this->isReady();
900
    }
901
902
    /**
903
     * Execute a "delete"-query.
904
     *
905
     * @param string       $table
906
     * @param array|string $where
907
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
908
     *
909
     * @throws QueryException
910
     *
911
     * @return false|int
912
     *                   <p>false on error</p>
913
     */
914 4 View Code Duplication
    public function delete(string $table, $where, string $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
915
    {
916
        // init
917 4
        $table = \trim($table);
918
919 4
        if ($table === '') {
920 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
921
922 3
            return false;
923
        }
924
925 4
        if (\is_string($where)) {
926 3
            $WHERE = $this->escape($where, false);
927 4
        } elseif (\is_array($where)) {
928 4
            $WHERE = $this->_parseArrayPair($where, 'AND');
929
        } else {
930 3
            $WHERE = '';
931
        }
932
933 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...
934
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
935
        }
936
937 4
        $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
938
939 4
        $return = $this->query($sql);
940
941 4
        \assert(\is_int($return) || $return === false);
942
943 4
        return $return;
944
    }
945
946
    /**
947
     * Ends a transaction and commits if no errors, then ends autocommit.
948
     *
949
     * @return bool
950
     *              <p>This will return true or false indicating success of transactions.</p>
951
     */
952 12
    public function endTransaction(): bool
953
    {
954 12
        if (!$this->in_transaction) {
955
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
956
957
            return false;
958
        }
959
960 12
        if (!$this->errors()) {
961 3
            $return = $this->commit();
962
        } else {
963 9
            $this->rollback();
964 9
            $return = false;
965
        }
966
967 12
        if ($this->mysqli_link) {
968 12
            \mysqli_autocommit($this->mysqli_link, true);
969
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
970
            $this->doctrine_connection->setAutoCommit(true);
971
972
            if ($this->doctrine_connection->isAutoCommit()) {
973
                $return = true;
974
            } else {
975
                $return = false;
976
            }
977
        }
978
979 12
        $this->in_transaction = false;
980
981 12
        return $return;
982
    }
983
984
    /**
985
     * Get all errors from "$this->errors".
986
     *
987
     * @return array|false
988
     *                     <p>false === on errors</p>
989
     */
990 12
    public function errors()
991
    {
992 12
        $errors = $this->debug->getErrors();
993
994 12
        return \count($errors) > 0 ? $errors : false;
995
    }
996
997
    /**
998
     * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
999
     *
1000
     * @param mixed     $var           bool: convert into "integer"<br />
1001
     *                                 int: int (don't change it)<br />
1002
     *                                 float: float (don't change it)<br />
1003
     *                                 null: null (don't change it)<br />
1004
     *                                 array: run escape() for every key => value<br />
1005
     *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
1006
     * @param bool      $stripe_non_utf8
1007
     * @param bool      $html_entity_decode
1008
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
1009
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
1010
     *                                 <strong>null</strong> => Convert the array into null, every time.
1011
     *
1012
     * @return mixed
1013
     */
1014 110
    public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
1015
    {
1016
        // [empty]
1017 110
        if ($var === '') {
1018 6
            return '';
1019
        }
1020
1021
        // ''
1022 110
        if ($var === "''") {
1023
            return "''";
1024
        }
1025
1026
        // check the type
1027 110
        $type = \gettype($var);
1028
1029 110
        if ($type === 'object') {
1030 9
            if ($var instanceof \DateTimeInterface) {
1031 9
                $var = $var->format('Y-m-d H:i:s');
1032 9
                $type = 'string';
1033 6
            } elseif (\method_exists($var, '__toString')) {
1034 6
                $var = (string) $var;
1035 6
                $type = 'string';
1036
            }
1037
        }
1038
1039
        switch ($type) {
1040 110
            case 'boolean':
1041 9
                $var = (int) $var;
1042
1043 9
                break;
1044
1045 110
            case 'double':
1046 110
            case 'integer':
1047 65
                break;
1048
1049 107
            case 'string':
1050 107
                if ($stripe_non_utf8) {
1051 23
                    $var = UTF8::cleanup($var);
1052
                }
1053
1054 107
                if ($html_entity_decode) {
1055 3
                    $var = UTF8::html_entity_decode($var);
1056
                }
1057
1058
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
1059 107
                $var = @\get_magic_quotes_gpc() ? \stripslashes($var) : $var;
1060
1061
                if (
1062 107
                    $this->mysqli_link
1063
                    &&
1064 107
                    $this->mysqli_link instanceof \mysqli
1065
                ) {
1066 107
                    $var = \mysqli_real_escape_string($this->mysqli_link, $var);
1067
                } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1068
                    $pdoConnection = $this->getDoctrinePDOConnection();
1069
                    \assert($pdoConnection !== false);
1070
                    $var = $pdoConnection->quote($var);
1071
                    $var = \substr($var, 1, -1);
1072
                }
1073
1074 107
                break;
1075
1076 9
            case 'array':
1077 6
                if ($convert_array === null) {
1078 3
                    if ($this->convert_null_to_empty_string) {
1079
                        $var = "''";
1080
                    } else {
1081 3
                        $var = 'NULL';
1082
                    }
1083
                } else {
1084 6
                    $varCleaned = [];
1085 6
                    foreach ((array) $var as $key => $value) {
1086 6
                        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
1087 6
                        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
1088
1089
                        /** @noinspection OffsetOperationsInspection */
1090 6
                        $varCleaned[$key] = $value;
1091
                    }
1092
1093 6 View Code Duplication
                    if ($convert_array === true) {
1094 3
                        $varCleaned = \implode(',', $varCleaned);
1095
1096 3
                        $var = $varCleaned;
1097
                    } else {
1098 6
                        $var = $varCleaned;
1099
                    }
1100
                }
1101
1102 6
                break;
1103
1104 9
            case 'NULL':
1105 6
                if ($this->convert_null_to_empty_string) {
1106
                    $var = "''";
1107
                } else {
1108 6
                    $var = 'NULL';
1109
                }
1110
1111 6
                break;
1112
1113
            default:
1114 6
                throw new \InvalidArgumentException(\sprintf('Not supported value "%s" of type %s.', \print_r($var, true), $type));
1115
1116
                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...
1117
        }
1118
1119 110
        return $var;
1120
    }
1121
1122
    /**
1123
     * Execute select/insert/update/delete sql-queries.
1124
     *
1125
     * @param string  $query    <p>sql-query</p>
1126
     * @param bool    $useCache optional <p>use cache?</p>
1127
     * @param int     $cacheTTL optional <p>cache-ttl in seconds</p>
1128
     * @param DB|null $db       optional <p>the database connection</p>
1129
     *
1130
     * @throws QueryException
1131
     *
1132
     * @return mixed
1133
     *               <ul>
1134
     *               <li>"array" by "<b>SELECT</b>"-queries</li>
1135
     *               <li>"int|string" (insert_id) by "<b>INSERT</b>"-queries</li>
1136
     *               <li>"int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1137
     *               <li>"true" by e.g. "DROP"-queries</li>
1138
     *               <li>"false" on error</li>
1139
     *               </ul>
1140
     */
1141 9
    public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
1142
    {
1143
        // init
1144 9
        $cacheKey = null;
1145 9
        if (!$db) {
1146 9
            $db = self::getInstance();
1147
        }
1148
1149 9 View Code Duplication
        if ($useCache) {
1150 3
            $cache = new Cache(null, null, false, $useCache);
1151 3
            $cacheKey = 'sql-' . \md5($query);
1152
1153
            if (
1154 3
                $cache->getCacheIsReady()
1155
                &&
1156 3
                $cache->existsItem($cacheKey)
1157
            ) {
1158 3
                return $cache->getItem($cacheKey);
1159
            }
1160
        } else {
1161 9
            $cache = false;
1162
        }
1163
1164 9
        $result = $db->query($query);
1165
1166 9
        if ($result instanceof Result) {
1167 3
            $return = $result->fetchAllArray();
1168
1169
            // save into the cache
1170 View Code Duplication
            if (
1171 3
                $cacheKey !== null
1172
                &&
1173 3
                $useCache
1174
                &&
1175 3
                $cache instanceof Cache
1176
                &&
1177 3
                $cache->getCacheIsReady()
1178
            ) {
1179 3
                $cache->setItem($cacheKey, $return, $cacheTTL);
1180
            }
1181
        } else {
1182 6
            $return = $result;
1183
        }
1184
1185 9
        return $return;
1186
    }
1187
1188
    /**
1189
     * Get all table-names via "SHOW TABLES".
1190
     *
1191
     * @return array
1192
     */
1193 3
    public function getAllTables(): array
1194
    {
1195 3
        $query = 'SHOW TABLES';
1196 3
        $result = $this->query($query);
1197
1198 3
        \assert($result instanceof Result);
1199
1200 3
        return $result->fetchAllArray();
1201
    }
1202
1203
    /**
1204
     * @return array
1205
     */
1206 49
    public function getConfig(): array
1207
    {
1208
        $config = [
1209 49
            'hostname'   => $this->hostname,
1210 49
            'username'   => $this->username,
1211 49
            'password'   => $this->password,
1212 49
            'port'       => $this->port,
1213 49
            'database'   => $this->database,
1214 49
            'socket'     => $this->socket,
1215 49
            'charset'    => $this->charset,
1216 49
            'cacert'     => $this->cacert,
1217 49
            'clientcert' => $this->clientcert,
1218 49
            'clientkey'  => $this->clientkey,
1219
        ];
1220
1221 49
        if ($this->doctrine_connection instanceof \Doctrine\DBAL\Connection) {
1222
            $config += $this->doctrine_connection->getParams();
1223
        }
1224
1225 49
        return $config;
1226
    }
1227
1228
    /**
1229
     * @return Debug
1230
     */
1231 10
    public function getDebugger(): Debug
1232
    {
1233 10
        return $this->debug;
1234
    }
1235
1236
    /**
1237
     * @return \Doctrine\DBAL\Connection|null|null
1238
     */
1239 2
    public function getDoctrineConnection()
1240
    {
1241 2
        return $this->doctrine_connection;
1242
    }
1243
1244
    /**
1245
     * @return \Doctrine\DBAL\Driver\Connection|false
1246
     */
1247 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...
1248
    {
1249
        if ($this->doctrine_connection) {
1250
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1251
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1252
                return $doctrineWrappedConnection;
1253
            }
1254
        }
1255
1256
        return false;
1257
    }
1258
1259
    /**
1260
     * Get errors from "$this->errors".
1261
     *
1262
     * @return array
1263
     */
1264 3
    public function getErrors(): array
1265
    {
1266 3
        return $this->debug->getErrors();
1267
    }
1268
1269
    /**
1270
     * @param string $hostname              <p>Hostname of the mysql server</p>
1271
     * @param string $username              <p>Username for the mysql connection</p>
1272
     * @param string $password              <p>Password for the mysql connection</p>
1273
     * @param string $database              <p>Database for the mysql connection</p>
1274
     * @param int    $port                  <p>default is (int)3306</p>
1275
     * @param string $charset               <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1276
     * @param bool   $exit_on_error         <p>Throw a 'Exception' when a query failed, otherwise it will return
1277
     *                                      'false'. Use false to disable it.</p>
1278
     * @param bool   $echo_on_error         <p>Echo the error if "checkForDev()" returns true.
1279
     *                                      Use false to disable it.</p>
1280
     * @param string $logger_class_name
1281
     * @param string $logger_level          <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1282
     * @param array  $extra_config          <p>
1283
     *                                      're_connect'    => bool<br>
1284
     *                                      'session_to_db' => bool<br>
1285
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
1286
     *                                      'socket'        => string (path)<br>
1287
     *                                      'flags'         => null|int<br>
1288
     *                                      'ssl'           => bool<br>
1289
     *                                      'clientkey'     => string (path)<br>
1290
     *                                      'clientcert'    => string (path)<br>
1291
     *                                      'cacert'        => string (path)<br>
1292
     *                                      </p>
1293
     *
1294
     * @return self
1295
     */
1296 207
    public static function getInstance(
1297
        string $hostname = '',
1298
        string $username = '',
1299
        string $password = '',
1300
        string $database = '',
1301
        $port = 3306,
1302
        string $charset = 'utf8',
1303
        bool $exit_on_error = true,
1304
        bool $echo_on_error = true,
1305
        string $logger_class_name = '',
1306
        string $logger_level = '',
1307
        array $extra_config = []
1308
    ): self {
1309
        /**
1310
         * @var self[]
1311
         */
1312 207
        static $instance = [];
1313
1314
        /**
1315
         * @var self|null
1316
         */
1317 207
        static $firstInstance = null;
1318
1319
        // fallback
1320 207
        if (!$charset) {
1321 121
            $charset = 'utf8';
1322
        }
1323
1324
        if (
1325 207
            '' . $hostname . $username . $password . $database . $port . $charset === '' . $port . $charset
1326
            &&
1327 207
            $firstInstance instanceof self
1328
        ) {
1329 123
            if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1330
                $firstInstance->reconnect(true);
1331
            }
1332
1333 123
            return $firstInstance;
1334
        }
1335
1336 126
        $extra_config_string = '';
1337 126
        foreach ($extra_config as $extra_config_key => $extra_config_value) {
1338 56
            if (\is_object($extra_config_value)) {
1339
                $extra_config_value_tmp = \spl_object_hash($extra_config_value);
1340
            } else {
1341 56
                $extra_config_value_tmp = (string) $extra_config_value;
1342
            }
1343 56
            $extra_config_string .= $extra_config_key . $extra_config_value_tmp;
1344
        }
1345
1346 126
        $connection = \md5(
1347 126
            $hostname . $username . $password . $database . $port . $charset . (int) $exit_on_error . (int) $echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1348
        );
1349
1350 126
        if (!isset($instance[$connection])) {
1351 24
            $instance[$connection] = new self(
1352 24
                $hostname,
1353 24
                $username,
1354 24
                $password,
1355 24
                $database,
1356 24
                $port,
1357 24
                $charset,
1358 24
                $exit_on_error,
1359 24
                $echo_on_error,
1360 24
                $logger_class_name,
1361 24
                $logger_level,
1362 24
                $extra_config
1363
            );
1364
1365 6
            if ($firstInstance === null) {
1366 1
                $firstInstance = $instance[$connection];
1367
            }
1368
        }
1369
1370 114
        if (isset($extra_config['re_connect']) && $extra_config['re_connect'] === true) {
1371
            $instance[$connection]->reconnect(true);
1372
        }
1373
1374 114
        return $instance[$connection];
1375
    }
1376
1377
    /**
1378
     * @param \Doctrine\DBAL\Connection $doctrine
1379
     * @param string                    $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1380
     * @param bool                      $exit_on_error <p>Throw a 'Exception' when a query failed, otherwise it will
1381
     *                                                 return 'false'. Use false to disable it.</p>
1382
     * @param bool                      $echo_on_error <p>Echo the error if "checkForDev()" returns true.
1383
     *                                                 Use false to disable it.</p>
1384
     * @param string                    $logger_class_name
1385
     * @param string                    $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1386
     * @param array                     $extra_config  <p>
1387
     *                                                 're_connect'    => bool<br>
1388
     *                                                 'session_to_db' => bool<br>
1389
     *                                                 'socket'        => string (path)<br>
1390
     *                                                 'flags'         => null|int<br>
1391
     *                                                 'ssl'           => bool<br>
1392
     *                                                 'clientkey'     => string (path)<br>
1393
     *                                                 'clientcert'    => string (path)<br>
1394
     *                                                 'cacert'        => string (path)<br>
1395
     *                                                 </p>
1396
     *
1397
     * @return self
1398
     */
1399 55
    public static function getInstanceDoctrineHelper(
1400
        \Doctrine\DBAL\Connection $doctrine,
1401
        string $charset = 'utf8',
1402
        bool $exit_on_error = true,
1403
        bool $echo_on_error = true,
1404
        string $logger_class_name = '',
1405
        string $logger_level = '',
1406
        array $extra_config = []
1407
    ): self {
1408 55
        $extra_config['doctrine'] = $doctrine;
1409
1410 55
        return self::getInstance(
1411 55
            '',
1412 55
            '',
1413 55
            '',
1414 55
            '',
1415 55
            3306,
1416 55
            $charset,
1417 55
            $exit_on_error,
1418 55
            $echo_on_error,
1419 55
            $logger_class_name,
1420 55
            $logger_level,
1421 55
            $extra_config
1422
        );
1423
    }
1424
1425
    /**
1426
     * Get the mysqli-link (link identifier returned by mysqli-connect).
1427
     *
1428
     * @return \mysqli|null
1429
     */
1430 15
    public function getLink()
1431
    {
1432 15
        return $this->mysqli_link;
1433
    }
1434
1435
    /**
1436
     * Get the current charset.
1437
     *
1438
     * @return string
1439
     */
1440 3
    public function get_charset(): string
1441
    {
1442 3
        return $this->charset;
1443
    }
1444
1445
    /**
1446
     * Check if we are in a transaction.
1447
     *
1448
     * @return bool
1449
     */
1450
    public function inTransaction(): bool
1451
    {
1452
        return $this->in_transaction;
1453
    }
1454
1455
    /**
1456
     * Execute a "insert"-query.
1457
     *
1458
     * @param string      $table
1459
     * @param array       $data
1460
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1461
     *
1462
     * @throws QueryException
1463
     *
1464
     * @return false|int|string
1465
     *                   <p>false on error</p>
1466
     */
1467 74
    public function insert(string $table, array $data = [], string $databaseName = null)
1468
    {
1469
        // init
1470 74
        $table = \trim($table);
1471
1472 74
        if ($table === '') {
1473 6
            $this->debug->displayError('Invalid table name, table name in empty.', false);
1474
1475 6
            return false;
1476
        }
1477
1478 71
        if (\count($data) === 0) {
1479 9
            $this->debug->displayError('Invalid data for INSERT, data is empty.', false);
1480
1481 9
            return false;
1482
        }
1483
1484 65
        $SET = $this->_parseArrayPair($data);
1485
1486 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...
1487
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1488
        }
1489
1490 65
        $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET ${SET}";
1491
1492 65
        $return = $this->query($sql);
1493 65
        if ($return === false) {
1494 3
            return false;
1495
        }
1496
1497 65
        \assert(\is_int($return) || \is_string($return));
1498
1499 65
        return $return;
1500
    }
1501
1502
    /**
1503
     * Returns the auto generated id used in the last query.
1504
     *
1505
     * @return false|int|string
1506
     */
1507 100
    public function insert_id()
1508
    {
1509 100
        if ($this->mysqli_link) {
1510 100
            return \mysqli_insert_id($this->mysqli_link);
1511
        }
1512
1513
        $doctrinePDOConnection = $this->getDoctrinePDOConnection();
1514
        if ($doctrinePDOConnection) {
1515
            return $doctrinePDOConnection->lastInsertId();
1516
        }
1517
1518
        return false;
1519
    }
1520
1521
    /**
1522
     * @return bool
1523
     */
1524
    public function isDoctrineMySQLiConnection(): bool
1525
    {
1526
        if ($this->doctrine_connection) {
1527
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1528
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliConnection) {
1529
                return true;
1530
            }
1531
        }
1532
1533
        return false;
1534
    }
1535
1536
    /**
1537
     * @return bool
1538
     */
1539 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...
1540
    {
1541
        if ($this->doctrine_connection) {
1542
            $doctrineWrappedConnection = $this->doctrine_connection->getWrappedConnection();
1543
            if ($doctrineWrappedConnection instanceof \Doctrine\DBAL\Driver\PDOConnection) {
1544
                return true;
1545
            }
1546
        }
1547
1548
        return false;
1549
    }
1550
1551
    /**
1552
     * Check if db-connection is ready.
1553
     *
1554
     * @return bool
1555
     */
1556 162
    public function isReady(): bool
1557
    {
1558 162
        return $this->connected ? true : false;
1559
    }
1560
1561
    /**
1562
     * Get the last sql-error.
1563
     *
1564
     * @return false|string
1565
     *                      <p>false === there was no error</p>
1566
     */
1567 3
    public function lastError()
1568
    {
1569 3
        $errors = $this->debug->getErrors();
1570
1571 3
        return \count($errors) > 0 ? \end($errors) : false;
1572
    }
1573
1574
    /**
1575
     * Execute a sql-multi-query.
1576
     *
1577
     * @param string $sql
1578
     *
1579
     * @throws QueryException
1580
     *
1581
     * @return bool|Result[]
1582
     *                        <ul>
1583
     *                        <li>"Result"-Array by "<b>SELECT</b>"-queries</li>
1584
     *                        <li>"bool" by only "<b>INSERT</b>"-queries</li>
1585
     *                        <li>"bool" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries</li>
1586
     *                        <li>"bool" by only by e.g. "DROP"-queries</li>
1587
     *                        </ul>
1588
     */
1589 3
    public function multi_query(string $sql)
1590
    {
1591 3
        if (!$this->isReady()) {
1592
            return false;
1593
        }
1594
1595 3
        if (!$sql || $sql === '') {
1596 3
            $this->debug->displayError('Can not execute an empty query.', false);
1597
1598 3
            return false;
1599
        }
1600
1601 3
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1602
            $query_start_time = \microtime(true);
1603
            $queryException = null;
1604
            $query_result_doctrine = false;
1605
1606
            try {
1607
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1608
                $resultTmp = $query_result_doctrine->execute();
1609
                $mysqli_field_count = $query_result_doctrine->columnCount();
1610
            } catch (\Exception $e) {
1611
                $resultTmp = false;
1612
                $mysqli_field_count = null;
1613
1614
                $queryException = $e;
1615
            }
1616
1617
            $query_duration = \microtime(true) - $query_start_time;
1618
1619
            $this->debug->logQuery($sql, $query_duration, 0);
1620
1621
            $returnTheResult = false;
1622
            $result = [];
1623
1624
            if ($resultTmp) {
1625
                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...
1626
                    if (
1627
                        $query_result_doctrine
1628
                        &&
1629
                        $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1630
                    ) {
1631
                        $result = $query_result_doctrine;
1632
                    }
1633
                } else {
1634
                    $result = $resultTmp;
1635
                }
1636
1637
                if (
1638
                    $result instanceof \Doctrine\DBAL\Statement
1639
                    &&
1640
                    $result->columnCount() > 0
1641
                ) {
1642
                    $returnTheResult = true;
1643
1644
                    // return query result object
1645
                    $result = [new Result($sql, $result)];
1646
                } else {
1647
                    $result = [$result];
1648
                }
1649
            } else {
1650
1651
                // log the error query
1652
                $this->debug->logQuery($sql, $query_duration, 0, true);
1653
1654
                if (
1655
                    isset($queryException)
1656
                    &&
1657
                    $queryException instanceof \Doctrine\DBAL\Query\QueryException
1658
                ) {
1659
                    return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, false, true);
1660
                }
1661
            }
1662 3
        } elseif ($this->mysqli_link) {
1663 3
            $query_start_time = \microtime(true);
1664 3
            $resultTmp = \mysqli_multi_query($this->mysqli_link, $sql);
1665 3
            $query_duration = \microtime(true) - $query_start_time;
1666
1667 3
            $this->debug->logQuery($sql, $query_duration, 0);
1668
1669 3
            $returnTheResult = false;
1670 3
            $result = [];
1671
1672 3
            if ($resultTmp) {
1673
                do {
1674 3
                    $resultTmpInner = \mysqli_store_result($this->mysqli_link);
1675
1676 3
                    if ($resultTmpInner instanceof \mysqli_result) {
1677 3
                        $returnTheResult = true;
1678 3
                        $result[] = new Result($sql, $resultTmpInner);
1679 3
                    } elseif (\mysqli_errno($this->mysqli_link)) {
1680
                        $result[] = false;
1681
                    } else {
1682 3
                        $result[] = true;
1683
                    }
1684 3
                } while (\mysqli_more_results($this->mysqli_link) ? \mysqli_next_result($this->mysqli_link) : false);
1685
            } else {
1686
1687
                // log the error query
1688 3
                $this->debug->logQuery($sql, $query_duration, 0, true);
1689
1690 3
                return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, false, true);
1691
            }
1692
        } else {
1693
1694
            // log the error query
1695
            $this->debug->logQuery($sql, 0, 0, true);
1696
1697
            return $this->queryErrorHandling('no database connection', 1, $sql, false, true);
1698
        }
1699
1700
        // return the result only if there was a "SELECT"-query
1701 3
        if ($returnTheResult) {
1702 3
            return $result;
1703
        }
1704
1705
        if (
1706 3
            \count($result) > 0
1707
            &&
1708 3
            !\in_array(false, $result, true)
1709
        ) {
1710 3
            return true;
1711
        }
1712
1713
        return false;
1714
    }
1715
1716
    /**
1717
     * Count number of rows found matching a specific query.
1718
     *
1719
     * @param string $query
1720
     *
1721
     * @return int
1722
     */
1723 3
    public function num_rows(string $query): int
1724
    {
1725 3
        $check = $this->query($query);
1726
1727
        if (
1728 3
            $check === false
1729
            ||
1730 3
            !$check instanceof Result
1731
        ) {
1732
            return 0;
1733
        }
1734
1735 3
        return $check->num_rows;
1736
    }
1737
1738
    /**
1739
     * Pings a server connection, or tries to reconnect
1740
     * if the connection has gone down.
1741
     *
1742
     * @return bool
1743
     */
1744 9
    public function ping(): bool
1745
    {
1746 9
        if (!$this->connected) {
1747 3
            return false;
1748
        }
1749
1750 6
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
1751
            return $this->doctrine_connection->ping();
1752
        }
1753
1754
        if (
1755 6
            $this->mysqli_link
1756
            &&
1757 6
            $this->mysqli_link instanceof \mysqli
1758
        ) {
1759 6
            return \mysqli_ping($this->mysqli_link);
1760
        }
1761
1762
        return false;
1763
    }
1764
1765
    /**
1766
     * Get a new "Prepare"-Object for your sql-query.
1767
     *
1768
     * @param string $query
1769
     *
1770
     * @return Prepare
1771
     */
1772 2
    public function prepare(string $query): Prepare
1773
    {
1774 2
        return new Prepare($this, $query);
1775
    }
1776
1777
    /**
1778
     * Execute a sql-query and return the result-array for select-statements.
1779
     *
1780
     * @param string $query
1781
     *
1782
     * @throws \Exception
1783
     *
1784
     * @return mixed
1785
     *
1786
     * @deprecated
1787
     */
1788 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...
1789
    {
1790 3
        $db = self::getInstance();
1791
1792 3
        $args = \func_get_args();
1793
        /** @noinspection SuspiciousAssignmentsInspection */
1794 3
        $query = \array_shift($args);
1795 3
        $query = \str_replace('?', '%s', $query);
1796 3
        $args = \array_map(
1797
            [
1798 3
                $db,
1799 3
                'escape',
1800
            ],
1801 3
            $args
1802
        );
1803 3
        \array_unshift($args, $query);
1804 3
        $query = \sprintf(...$args);
1805 3
        $result = $db->query($query);
1806
1807 3
        if ($result instanceof Result) {
1808 3
            return $result->fetchAllArray();
1809
        }
1810
1811 3
        return $result;
1812
    }
1813
1814
    /**
1815
     * Execute a sql-query.
1816
     *
1817
     * example:
1818
     * <code>
1819
     * $sql = "INSERT INTO TABLE_NAME_HERE
1820
     *   SET
1821
     *     foo = :foo,
1822
     *     bar = :bar
1823
     * ";
1824
     * $insert_id = $db->query(
1825
     *   $sql,
1826
     *   [
1827
     *     'foo' => 1.1,
1828
     *     'bar' => 1,
1829
     *   ]
1830
     * );
1831
     * </code>
1832
     *
1833
     * @param string     $sql               <p>The sql query-string.</p>
1834
     * @param array|bool $params            <p>
1835
     *                                      "array" of sql-query-parameters<br/>
1836
     *                                      "false" if you don't need any parameter (default)<br/>
1837
     *                                      </p>
1838
     *
1839
     * @throws QueryException
1840
     *
1841
     * @return bool|int|Result|string
1842
     *                                      <p>
1843
     *                                      "Result" by "<b>SELECT</b>"-queries<br />
1844
     *                                      "int|string" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1845
     *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1846
     *                                      "true" by e.g. "DROP"-queries<br />
1847
     *                                      "false" on error
1848
     *                                      </p>
1849
     */
1850 140
    public function query(string $sql = '', $params = false)
1851
    {
1852 140
        if (!$this->isReady()) {
1853
            return false;
1854
        }
1855
1856 140
        if ($sql === '') {
1857 12
            $this->debug->displayError('Can not execute an empty query.', false);
1858
1859 12
            return false;
1860
        }
1861
1862
        if (
1863 134
            $params !== false
1864
            &&
1865 134
            \is_array($params)
1866
            &&
1867 134
            \count($params) > 0
1868
        ) {
1869 7
            $parseQueryParams = $this->_parseQueryParams($sql, $params);
1870 7
            $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1871 7
            $sql = $parseQueryParamsByName['sql'];
1872
        }
1873
1874
        // DEBUG
1875
        // var_dump($params);
1876
        // echo $sql . "\n";
1877
1878 134
        $query_start_time = \microtime(true);
1879 134
        $queryException = null;
1880 134
        $query_result_doctrine = false;
1881
1882 134
        if ($this->doctrine_connection) {
1883
            try {
1884
                $query_result_doctrine = $this->doctrine_connection->prepare($sql);
1885
                $query_result = $query_result_doctrine->execute();
1886
                $mysqli_field_count = $query_result_doctrine->columnCount();
1887
            } catch (\Exception $e) {
1888
                $query_result = false;
1889
                $mysqli_field_count = null;
1890
1891
                $queryException = $e;
1892
            }
1893 134
        } elseif ($this->mysqli_link) {
1894 134
            $query_result = \mysqli_real_query($this->mysqli_link, $sql);
1895 134
            $mysqli_field_count = \mysqli_field_count($this->mysqli_link);
1896
        } else {
1897
            $query_result = false;
1898
            $mysqli_field_count = null;
1899
1900
            $queryException = new DBConnectException('no mysql connection');
1901
        }
1902
1903 134
        $query_duration = \microtime(true) - $query_start_time;
1904
1905 134
        $this->query_count++;
1906
1907 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...
1908 99
            if ($this->doctrine_connection) {
1909
                $result = false;
1910
                if (
1911
                    $query_result_doctrine
1912
                    &&
1913
                    $query_result_doctrine instanceof \Doctrine\DBAL\Statement
1914
                ) {
1915
                    $result = $query_result_doctrine;
1916
                }
1917 99
            } elseif ($this->mysqli_link) {
1918 99
                $result = \mysqli_store_result($this->mysqli_link);
1919
            } else {
1920 99
                $result = false;
1921
            }
1922
        } else {
1923 106
            $result = $query_result;
1924
        }
1925
1926
        if (
1927 134
            $result instanceof \Doctrine\DBAL\Statement
1928
            &&
1929 134
            $result->columnCount() > 0
1930
        ) {
1931
1932
            // log the select query
1933
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1934
1935
            // return query result object
1936
            return new Result($sql, $result);
1937
        }
1938
1939 134
        if ($result instanceof \mysqli_result) {
1940
1941
            // log the select query
1942 96
            $this->debug->logQuery($sql, $query_duration, $mysqli_field_count);
1943
1944
            // return query result object
1945 96
            return new Result($sql, $result);
1946
        }
1947
1948 112
        if ($query_result) {
1949
1950
            // "INSERT" || "REPLACE"
1951 103
            if (\preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1952 100
                $insert_id = $this->insert_id();
1953
1954 100
                $this->debug->logQuery($sql, $query_duration, $insert_id);
1955
1956 100
                return $insert_id;
1957
            }
1958
1959
            // "UPDATE" || "DELETE"
1960 50
            if (\preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1961 25
                if ($this->mysqli_link) {
1962 25
                    $this->affected_rows = $this->affected_rows();
1963
                } elseif ($query_result_doctrine) {
1964
                    $this->affected_rows = $query_result_doctrine->rowCount();
1965
                }
1966
1967 25
                $this->debug->logQuery($sql, $query_duration, $this->affected_rows);
1968
1969 25
                return $this->affected_rows;
1970
            }
1971
1972
            // log the ? query
1973 25
            $this->debug->logQuery($sql, $query_duration, 0);
1974
1975 25
            return true;
1976
        }
1977
1978
        // log the error query
1979 33
        $this->debug->logQuery($sql, $query_duration, 0, true);
1980
1981 33
        if ($queryException) {
1982
            return $this->queryErrorHandling($queryException->getMessage(), $queryException->getCode(), $sql, $params);
1983
        }
1984
1985 33
        if ($this->mysqli_link) {
1986 33
            return $this->queryErrorHandling(\mysqli_error($this->mysqli_link), \mysqli_errno($this->mysqli_link), $sql, $params);
1987
        }
1988
1989
        return false;
1990
    }
1991
1992
    /**
1993
     * Error-handling for the sql-query.
1994
     *
1995
     * @param string     $errorMessage
1996
     * @param int        $errorNumber
1997
     * @param string     $sql
1998
     * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1999
     * @param bool       $sqlMultiQuery
2000
     *
2001
     * @throws QueryException
2002
     * @throws DBGoneAwayException
2003
     *
2004
     * @return false|mixed
2005
     */
2006 39
    private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
2007
    {
2008
        if (
2009 39
            $errorMessage === 'DB server has gone away'
2010
            ||
2011 36
            $errorMessage === 'MySQL server has gone away'
2012
            ||
2013 39
            $errorNumber === 2006
2014
        ) {
2015 3
            static $RECONNECT_COUNTER;
2016
2017
            // exit if we have more then 3 "DB server has gone away"-errors
2018 3
            if ($RECONNECT_COUNTER > 3) {
2019
                $this->debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
2020
2021
                throw new DBGoneAwayException($errorMessage);
2022
            }
2023
2024 3
            $this->debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2025
2026
            // reconnect
2027 3
            $RECONNECT_COUNTER++;
2028 3
            $this->reconnect(true);
2029
2030
            // re-run the current (non multi) query
2031 3
            if (!$sqlMultiQuery) {
2032 3
                return $this->query($sql, $sqlParams);
2033
            }
2034
2035
            return false;
2036
        }
2037
2038 36
        $this->debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
2039
2040 36
        $force_exception_after_error = null; // auto
2041 36
        if ($this->in_transaction) {
2042 12
            $force_exception_after_error = false;
2043
        }
2044
        // this query returned an error, we must display it (only for dev) !!!
2045
2046 36
        $this->debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
2047
2048 36
        return false;
2049
    }
2050
2051
    /**
2052
     * Quote && Escape e.g. a table name string.
2053
     *
2054
     * @param mixed $str
2055
     *
2056
     * @return string
2057
     */
2058 86
    public function quote_string($str): string
2059
    {
2060 86
        $str = \str_replace(
2061 86
            '`',
2062 86
            '``',
2063 86
            \trim(
2064 86
                (string) $this->escape($str, false),
2065 86
                '`'
2066
            )
2067
        );
2068
2069 86
        return '`' . $str . '`';
2070
    }
2071
2072
    /**
2073
     * Reconnect to the MySQL-Server.
2074
     *
2075
     * @param bool $checkViaPing
2076
     *
2077
     * @return bool
2078
     */
2079 7
    public function reconnect(bool $checkViaPing = false): bool
2080
    {
2081 7
        $ping = false;
2082 7
        if ($checkViaPing) {
2083 6
            $ping = $this->ping();
2084
        }
2085
2086 7
        if (!$ping) {
2087 7
            $this->connected = false;
2088 7
            $this->connect();
2089
        }
2090
2091 7
        return $this->isReady();
2092
    }
2093
2094
    /**
2095
     * Execute a "replace"-query.
2096
     *
2097
     * @param string      $table
2098
     * @param array       $data
2099
     * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2100
     *
2101
     * @throws QueryException
2102
     *
2103
     * @return false|int
2104
     *                   <p>false on error</p>
2105
     */
2106 3
    public function replace(string $table, array $data = [], string $databaseName = null)
2107
    {
2108
        // init
2109 3
        $table = \trim($table);
2110
2111 3
        if ($table === '') {
2112 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2113
2114 3
            return false;
2115
        }
2116
2117 3
        if (\count($data) === 0) {
2118 3
            $this->debug->displayError('Invalid data for REPLACE, data is empty.', false);
2119
2120 3
            return false;
2121
        }
2122
2123
        // extracting column names
2124 3
        $columns = \array_keys($data);
2125 3
        foreach ($columns as $k => $_key) {
2126 3
            $columns[$k] = $this->quote_string($_key);
2127
        }
2128
2129 3
        $columns = \implode(',', $columns);
2130
2131
        // extracting values
2132 3
        foreach ($data as $k => $_value) {
2133 3
            $data[$k] = $this->secure($_value);
2134
        }
2135 3
        $values = \implode(',', $data);
2136
2137 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...
2138
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2139
        }
2140
2141 3
        $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " (${columns}) VALUES (${values})";
2142
2143 3
        $return = $this->query($sql);
2144 3
        \assert(\is_int($return) || $return === false);
2145
2146 3
        return $return;
2147
    }
2148
2149
    /**
2150
     * Rollback in a transaction and end the transaction.
2151
     *
2152
     * @return bool
2153
     *              <p>bool true on success, false otherwise.</p>
2154
     */
2155 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...
2156
    {
2157 12
        if (!$this->in_transaction) {
2158
            $this->debug->displayError('Error: mysql server is not in transaction!', false);
2159
2160
            return false;
2161
        }
2162
2163
        // init
2164 12
        $return = false;
2165
2166 12
        if ($this->mysqli_link) {
2167 12
            $return = \mysqli_rollback($this->mysqli_link);
2168 12
            \mysqli_autocommit($this->mysqli_link, true);
2169
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2170
            $this->doctrine_connection->rollBack();
2171
            $this->doctrine_connection->setAutoCommit(true);
2172
2173
            if ($this->doctrine_connection->isAutoCommit()) {
2174
                $return = true;
2175
            } else {
2176
                $return = false;
2177
            }
2178
        }
2179
2180 12
        $this->in_transaction = false;
2181
2182 12
        return $return;
2183
    }
2184
2185
    /**
2186
     * Try to secure a variable, so can you use it in sql-queries.
2187
     *
2188
     * <p>
2189
     * <strong>int:</strong> (also strings that contains only an int-value)<br />
2190
     * 1. parse into "int"
2191
     * </p><br />
2192
     *
2193
     * <p>
2194
     * <strong>float:</strong><br />
2195
     * 1. return "float"
2196
     * </p><br />
2197
     *
2198
     * <p>
2199
     * <strong>string:</strong><br />
2200
     * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
2201
     * 2. trim '<br />
2202
     * 3. escape the string (and remove non utf-8 chars)<br />
2203
     * 4. trim ' again (because we maybe removed some chars)<br />
2204
     * 5. add ' around the new string<br />
2205
     * </p><br />
2206
     *
2207
     * <p>
2208
     * <strong>array:</strong><br />
2209
     * 1. return null
2210
     * </p><br />
2211
     *
2212
     * <p>
2213
     * <strong>object:</strong><br />
2214
     * 1. return false
2215
     * </p><br />
2216
     *
2217
     * <p>
2218
     * <strong>null:</strong><br />
2219
     * 1. return null
2220
     * </p>
2221
     *
2222
     * @param mixed     $var
2223
     * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
2224
     *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
2225
     *                                 <strong>null</strong> => Convert the array into null, every time.
2226
     *
2227
     * @return mixed
2228
     */
2229 87
    public function secure($var, $convert_array = true)
2230
    {
2231 87
        if (\is_array($var)) {
2232 6
            if ($convert_array === null) {
2233
                if ($this->convert_null_to_empty_string) {
2234
                    $var = "''";
2235
                } else {
2236
                    $var = 'NULL';
2237
                }
2238
            } else {
2239 6
                $varCleaned = [];
2240 6
                foreach ((array) $var as $key => $value) {
2241 6
                    $key = $this->escape($key, false, false, $convert_array);
2242 6
                    $value = $this->secure($value);
2243
2244
                    /** @noinspection OffsetOperationsInspection */
2245 6
                    $varCleaned[$key] = $value;
2246
                }
2247
2248 6 View Code Duplication
                if ($convert_array === true) {
2249 6
                    $varCleaned = \implode(',', $varCleaned);
2250
2251 6
                    $var = $varCleaned;
2252
                } else {
2253
                    $var = $varCleaned;
2254
                }
2255
            }
2256
2257 6
            return $var;
2258
        }
2259
2260 87
        if ($var === '') {
2261 6
            return "''";
2262
        }
2263
2264 87
        if ($var === "''") {
2265 3
            return "''";
2266
        }
2267
2268 87
        if ($var === null) {
2269 3
            if ($this->convert_null_to_empty_string) {
2270 3
                return "''";
2271
            }
2272
2273 3
            return 'NULL';
2274
        }
2275
2276 87
        if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
2277 3
            return $var;
2278
        }
2279
2280 87
        if (\is_string($var)) {
2281 77
            $var = \trim($var, "'");
2282
        }
2283
2284 87
        $var = $this->escape($var, false, false, null);
2285
2286 84
        if (\is_string($var)) {
2287 77
            $var = "'" . \trim($var, "'") . "'";
2288
        }
2289
2290 84
        return $var;
2291
    }
2292
2293
    /**
2294
     * Execute a "select"-query.
2295
     *
2296
     * @param string       $table
2297
     * @param array|string $where
2298
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2299
     *
2300
     * @throws QueryException
2301
     *
2302
     * @return false|Result
2303
     *                      <p>false on error</p>
2304
     */
2305 62 View Code Duplication
    public function select(string $table, $where = '1=1', string $databaseName = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2306
    {
2307
        // init
2308 62
        $table = \trim($table);
2309
2310 62
        if ($table === '') {
2311 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2312
2313 3
            return false;
2314
        }
2315
2316 62
        if (\is_string($where)) {
2317 25
            $WHERE = $this->escape($where, false);
2318 41
        } elseif (\is_array($where)) {
2319 41
            $WHERE = $this->_parseArrayPair($where, 'AND');
2320
        } else {
2321 3
            $WHERE = '';
2322
        }
2323
2324 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...
2325
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2326
        }
2327
2328 62
        $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE (${WHERE})";
2329
2330 62
        $return = $this->query($sql);
2331 62
        \assert($return instanceof Result || $return === false);
2332
2333 62
        return $return;
2334
    }
2335
2336
    /**
2337
     * Selects a different database than the one specified on construction.
2338
     *
2339
     * @param string $database <p>Database name to switch to.</p>
2340
     *
2341
     * @return bool
2342
     *              <p>bool true on success, false otherwise.</p>
2343
     */
2344
    public function select_db(string $database): bool
2345
    {
2346
        if (!$this->isReady()) {
2347
            return false;
2348
        }
2349
2350
        if ($this->mysqli_link) {
2351
            return \mysqli_select_db($this->mysqli_link, $database);
2352
        }
2353
2354
        if ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2355
            $return = $this->query('use :database', ['database' => $database]);
2356
            \assert(\is_bool($return));
2357
2358
            return $return;
2359
        }
2360
2361
        return false;
2362
    }
2363
2364
    /**
2365
     * @param array  $extra_config          <p>
2366
     *                                      'session_to_db' => bool<br>
2367
     *                                      'doctrine'      => \Doctrine\DBAL\Connection<br>
2368
     *                                      'socket'        => string (path)<br>
2369
     *                                      'flags'         => null|int<br>
2370
     *                                      'ssl'           => bool<br>
2371
     *                                      'clientkey'     => string (path)<br>
2372
     *                                      'clientcert'    => string (path)<br>
2373
     *                                      'cacert'        => string (path)<br>
2374
     *                                      </p>
2375
     */
2376 24
    public function setConfigExtra(array $extra_config)
2377
    {
2378 24
        if (isset($extra_config['session_to_db'])) {
2379
            $this->session_to_db = (bool) $extra_config['session_to_db'];
2380
        }
2381
2382 24
        if (isset($extra_config['doctrine'])) {
2383
            if ($extra_config['doctrine'] instanceof \Doctrine\DBAL\Connection) {
2384
                $this->doctrine_connection = $extra_config['doctrine'];
2385
            } else {
2386
                throw new DBConnectException('Error "doctrine"-connection is not valid');
2387
            }
2388
        }
2389
2390 24
        if (isset($extra_config['socket'])) {
2391
            $this->socket = $extra_config['socket'];
2392
        }
2393
2394 24
        if (isset($extra_config['flags'])) {
2395 1
            $this->flags = $extra_config['flags'];
2396
        }
2397
2398 24
        if (isset($extra_config['ssl'])) {
2399
            $this->ssl = $extra_config['ssl'];
2400
        }
2401
2402 24
        if (isset($extra_config['clientkey'])) {
2403
            $this->clientkey = $extra_config['clientkey'];
2404
        }
2405
2406 24
        if (isset($extra_config['clientcert'])) {
2407
            $this->clientcert = $extra_config['clientcert'];
2408
        }
2409
2410 24
        if (isset($extra_config['cacert'])) {
2411
            $this->cacert = $extra_config['cacert'];
2412
        }
2413 24
    }
2414
2415
    /**
2416
     * Set the current charset.
2417
     *
2418
     * @param string $charset
2419
     *
2420
     * @return bool
2421
     */
2422 15
    public function set_charset(string $charset): bool
2423
    {
2424 15
        $charsetLower = \strtolower($charset);
2425 15
        if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
2426 9
            $charset = 'utf8';
2427
        }
2428 15
        if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this)) {
2429 9
            $charset = 'utf8mb4';
2430
        }
2431
2432 15
        $this->charset = $charset;
2433
2434
        if (
2435 15
            $this->mysqli_link
2436
            &&
2437 15
            $this->mysqli_link instanceof \mysqli
2438
        ) {
2439 15
            $return = \mysqli_set_charset($this->mysqli_link, $charset);
2440
2441
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2442 15
            @\mysqli_query($this->mysqli_link, 'SET CHARACTER SET ' . $charset);
2443
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2444 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...
2445
        } elseif ($this->doctrine_connection && $this->isDoctrinePDOConnection()) {
2446
            $doctrineWrappedConnection = $this->getDoctrinePDOConnection();
2447
            if (!$doctrineWrappedConnection instanceof Connection) {
2448
                return false;
2449
            }
2450
2451
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2452
            @$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...
2453
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
2454
            @$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...
2455
2456
            $return = true;
2457
        } else {
2458
            $return = false;
2459
        }
2460
2461 15
        return $return;
2462
    }
2463
2464
    /**
2465
     * Set the option to convert null to "''" (empty string).
2466
     *
2467
     * Used in secure() => select(), insert(), update(), delete()
2468
     *
2469
     * @deprecated It's not recommended to convert NULL into an empty string!
2470
     *
2471
     * @param bool $bool
2472
     *
2473
     * @return self
2474
     */
2475 3
    public function set_convert_null_to_empty_string(bool $bool): self
2476
    {
2477 3
        $this->convert_null_to_empty_string = $bool;
2478
2479 3
        return $this;
2480
    }
2481
2482
    /**
2483
     * Enables or disables internal report functions
2484
     *
2485
     * @see http://php.net/manual/en/function.mysqli-report.php
2486
     *
2487
     * @param int $flags <p>
2488
     *                   <table>
2489
     *                   Supported flags
2490
     *                   <tr valign="top">
2491
     *                   <td>Name</td>
2492
     *                   <td>Description</td>
2493
     *                   </tr>
2494
     *                   <tr valign="top">
2495
     *                   <td><b>MYSQLI_REPORT_OFF</b></td>
2496
     *                   <td>Turns reporting off</td>
2497
     *                   </tr>
2498
     *                   <tr valign="top">
2499
     *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
2500
     *                   <td>Report errors from mysqli function calls</td>
2501
     *                   </tr>
2502
     *                   <tr valign="top">
2503
     *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
2504
     *                   <td>
2505
     *                   Throw <b>mysqli_sql_exception</b> for errors
2506
     *                   instead of warnings
2507
     *                   </td>
2508
     *                   </tr>
2509
     *                   <tr valign="top">
2510
     *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
2511
     *                   <td>Report if no index or bad index was used in a query</td>
2512
     *                   </tr>
2513
     *                   <tr valign="top">
2514
     *                   <td><b>MYSQLI_REPORT_ALL</b></td>
2515
     *                   <td>Set all options (report all)</td>
2516
     *                   </tr>
2517
     *                   </table>
2518
     *                   </p>
2519
     *
2520
     * @return bool
2521
     */
2522
    public function set_mysqli_report(int $flags): bool
2523
    {
2524
        if (
2525
            $this->mysqli_link
2526
            &&
2527
            $this->mysqli_link instanceof \mysqli
2528
        ) {
2529
            return \mysqli_report($flags);
2530
        }
2531
2532
        return false;
2533
    }
2534
2535
    /**
2536
     * Show config errors by throw exceptions.
2537
     *
2538
     * @throws \InvalidArgumentException
2539
     *
2540
     * @return bool
2541
     */
2542 24
    public function showConfigError(): bool
2543
    {
2544
        // check if a doctrine connection is already open, first
2545
        if (
2546 24
            $this->doctrine_connection
2547
            &&
2548 24
            $this->doctrine_connection->isConnected()
2549
        ) {
2550
            return true;
2551
        }
2552
2553
        if (
2554 24
            !$this->hostname
2555
            ||
2556 21
            !$this->username
2557
            ||
2558 24
            !$this->database
2559
        ) {
2560 9
            if (!$this->hostname) {
2561 3
                throw new \InvalidArgumentException('no-sql-hostname');
2562
            }
2563
2564 6
            if (!$this->username) {
2565 3
                throw new \InvalidArgumentException('no-sql-username');
2566
            }
2567
2568 3
            if (!$this->database) {
2569 3
                throw new \InvalidArgumentException('no-sql-database');
2570
            }
2571
2572
            return false;
2573
        }
2574
2575 15
        return true;
2576
    }
2577
2578
    /**
2579
     * alias: "beginTransaction()"
2580
     */
2581 3
    public function startTransaction(): bool
2582
    {
2583 3
        return $this->beginTransaction();
2584
    }
2585
2586
    /**
2587
     * Determine if database table exists
2588
     *
2589
     * @param string $table
2590
     *
2591
     * @return bool
2592
     */
2593 3
    public function table_exists(string $table): bool
2594
    {
2595 3
        $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2596
2597 3
        return $check !== false
2598
               &&
2599 3
               $check instanceof Result
2600
               &&
2601 3
               $check->num_rows > 0;
2602
    }
2603
2604
    /**
2605
     * Execute a callback inside a transaction.
2606
     *
2607
     * @param \Closure $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
2608
     *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
2609
     *
2610
     * @return bool
2611
     *              <p>bool true on success, false otherwise.</p>
2612
     */
2613 3
    public function transact($callback): bool
2614
    {
2615
        try {
2616 3
            $beginTransaction = $this->beginTransaction();
2617 3
            if (!$beginTransaction) {
2618 3
                $this->debug->displayError('Error: transact -> can not start transaction!', false);
2619
2620 3
                return false;
2621
            }
2622
2623 3
            $result = $callback($this);
2624 3
            if ($result === false) {
2625
                /** @noinspection ThrowRawExceptionInspection */
2626 3
                throw new \Exception('call_user_func [' . \print_r($callback, true) . '] === false');
2627
            }
2628
2629 3
            return $this->commit();
2630 3
        } catch (\Exception $e) {
2631 3
            $this->rollback();
2632
2633 3
            return false;
2634
        }
2635
    }
2636
2637
    /**
2638
     * Execute a "update"-query.
2639
     *
2640
     * @param string       $table
2641
     * @param array        $data
2642
     * @param array|string $where
2643
     * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
2644
     *
2645
     * @throws QueryException
2646
     *
2647
     * @return false|int
2648
     *                   <p>false on error</p>
2649
     */
2650 21
    public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
2651
    {
2652
        // init
2653 21
        $table = \trim($table);
2654
2655 21
        if ($table === '') {
2656 3
            $this->debug->displayError('Invalid table name, table name in empty.', false);
2657
2658 3
            return false;
2659
        }
2660
2661 21
        if (\count($data) === 0) {
2662 6
            $this->debug->displayError('Invalid data for UPDATE, data is empty.', false);
2663
2664 6
            return false;
2665
        }
2666
2667
        // DEBUG
2668
        //var_dump($data);
2669
2670 21
        $SET = $this->_parseArrayPair($data);
2671
2672
        // DEBUG
2673
        //var_dump($SET);
2674
2675 21
        if (\is_string($where)) {
2676 6
            $WHERE = $this->escape($where, false);
2677 18
        } elseif (\is_array($where)) {
2678 15
            $WHERE = $this->_parseArrayPair($where, 'AND');
2679
        } else {
2680 3
            $WHERE = '';
2681
        }
2682
2683 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...
2684
            $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2685
        }
2686
2687 21
        $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET ${SET} WHERE (${WHERE})";
2688
2689 21
        $return = $this->query($sql);
2690 21
        \assert(\is_int($return) || $return === false);
2691
2692 21
        return $return;
2693
    }
2694
}
2695