Completed
Push — master ( dbe3f7...286249 )
by Lars
03:20
created

DB::getLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

Loading history...
560
  {
561 6
    if ($this->_in_transaction === true) {
562 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
563
564 2
      return false;
565
    }
566
567 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
568 6
    $this->_in_transaction = true;
569 6
    $return = \mysqli_autocommit($this->link, false);
570 6
    if ($return === false) {
571
      $this->_in_transaction = false;
572
    }
573
574 6
    return $return;
575
  }
576
577
  /**
578
   * Clear the errors in "_debug->_errors".
579
   *
580
   * @return bool
581
   */
582 6
  public function clearErrors(): bool
583
  {
584 6
    return $this->_debug->clearErrors();
585
  }
586
587
  /**
588
   * Closes a previously opened database connection.
589
   */
590 2
  public function close(): bool
591
  {
592 2
    $this->connected = false;
593
594
    if (
595 2
        $this->link
596
        &&
597 2
        $this->link instanceof \mysqli
598
    ) {
599 2
      $result = \mysqli_close($this->link);
600 2
      $this->link = null;
601
602 2
      return $result;
603
    }
604
605 1
    return false;
606
  }
607
608
  /**
609
   * Commits the current transaction and end the transaction.
610
   *
611
   * @return bool <p>Boolean true on success, false otherwise.</p>
612
   */
613 2 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...
614
  {
615 2
    if ($this->_in_transaction === false) {
616
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
617
618
      return false;
619
    }
620
621 2
    $return = mysqli_commit($this->link);
622 2
    \mysqli_autocommit($this->link, true);
623 2
    $this->_in_transaction = false;
624
625 2
    return $return;
626
  }
627
628
  /**
629
   * Open a new connection to the MySQL server.
630
   *
631
   * @return bool
632
   *
633
   * @throws DBConnectException
634
   */
635 10
  public function connect(): bool
636
  {
637 10
    if ($this->isReady()) {
638 1
      return true;
639
    }
640
641 10
    $flags = null;
642
643 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
644
    try {
645 10
      $this->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 $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...
646
647 10
      if (Helper::isMysqlndIsUsed() === true) {
648 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
649
      }
650
651 10
      if ($this->_ssl === true) {
652
653
        if (empty($this->clientcert)) {
0 ignored issues
show
Bug introduced by
The property clientcert does not seem to exist. Did you mean _clientcert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
654
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
655
        }
656
657
        if (empty($this->clientkey)) {
0 ignored issues
show
Bug introduced by
The property clientkey does not seem to exist. Did you mean _clientkey?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
658
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
659
        }
660
661
        if (empty($this->cacert)) {
0 ignored issues
show
Bug introduced by
The property cacert does not seem to exist. Did you mean _cacert?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
662
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
663
        }
664
665
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
666
667
        /** @noinspection PhpParamsInspection */
668
        \mysqli_ssl_set(
669
            $this->link,
670
            $this->_clientkey,
671
            $this->_clientcert,
672
            $this->_cacert,
673
            null,
674
            null
675
        );
676
677
        $flags = MYSQLI_CLIENT_SSL;
678
      }
679
680
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
681 10
      $this->connected = @\mysqli_real_connect(
682 10
          $this->link,
683 10
          $this->hostname,
684 10
          $this->username,
685 10
          $this->password,
686 10
          $this->database,
687 10
          $this->port,
688 10
          $this->socket,
689 10
          (int)$flags
690
      );
691
692 3
    } catch (\Exception $e) {
693 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
694 3
      $this->_debug->displayError($error, true);
695
      throw new DBConnectException($error, 100, $e);
696
    }
697 7
    \mysqli_report(MYSQLI_REPORT_OFF);
698
699 7
    $errno = mysqli_connect_errno();
700 7
    if (!$this->connected || $errno) {
701
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
702
      $this->_debug->displayError($error, true);
703
      throw new DBConnectException($error, 101);
704
    }
705
706 7
    $this->set_charset($this->charset);
707
708 7
    return $this->isReady();
709
  }
710
711
  /**
712
   * Execute a "delete"-query.
713
   *
714
   * @param string       $table
715
   * @param string|array $where
716
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
717
   *
718
   * @return false|int <p>false on error</p>
719
   *
720
   *    * @throws QueryException
721
   */
722 2 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...
723
  {
724
    // init
725 2
    $table = \trim($table);
726
727 2
    if ($table === '') {
728 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
729
730 1
      return false;
731
    }
732
733 2
    if (\is_string($where)) {
734 1
      $WHERE = $this->escape($where, false);
735 2
    } elseif (\is_array($where)) {
736 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
737
    } else {
738 1
      $WHERE = '';
739
    }
740
741 2
    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...
742
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
743
    }
744
745 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
746
747 2
    return $this->query($sql);
748
  }
749
750
  /**
751
   * Ends a transaction and commits if no errors, then ends autocommit.
752
   *
753
   * @return bool <p>This will return true or false indicating success of transactions.</p>
754
   */
755 4 View Code Duplication
  public function endTransaction(): 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...
756
  {
757 4
    if ($this->_in_transaction === false) {
758
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
759
760
      return false;
761
    }
762
763 4
    if (!$this->errors()) {
764 1
      $return = \mysqli_commit($this->link);
765
    } else {
766 3
      $this->rollback();
767 3
      $return = false;
768
    }
769
770 4
    \mysqli_autocommit($this->link, true);
771 4
    $this->_in_transaction = false;
772
773 4
    return $return;
774
  }
775
776
  /**
777
   * Get all errors from "$this->_errors".
778
   *
779
   * @return array|false <p>false === on errors</p>
780
   */
781 4
  public function errors()
782
  {
783 4
    $errors = $this->_debug->getErrors();
784
785 4
    return \count($errors) > 0 ? $errors : false;
786
  }
787
788
  /**
789
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
790
   *
791
   * @param mixed $sql    <p>The SQL string.</p>
792
   * @param array $params <p>An array of key-value bindings.</p>
793
   *
794
   * @return array <p>with the keys -> 'sql', 'params'</p>
795
   */
796 14
  public function _parseQueryParamsByName(string $sql, array $params = []): array
797
  {
798
    // is there anything to parse?
799 View Code Duplication
    if (
800 14
        \strpos($sql, ':') === false
801
        ||
802 14
        \count($params) === 0
803
    ) {
804 3
      return ['sql' => $sql, 'params' => $params];
805
    }
806
807 12
    $parseKey = \md5(\uniqid((string)\mt_rand(), true));
808
809 12
    foreach ($params as $name => $value) {
810 12
      $nameTmp = $name;
811 12
      if (\strpos($name, ':') === 0) {
812 10
        $nameTmp = \substr($name, 1);
813
      }
814
815 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
816 12
      $sql = \str_replace(':' . $nameTmp, $parseKeyInner, $sql);
817
    }
818
819 12
    foreach ($params as $name => $value) {
820 12
      $nameTmp = $name;
821 12
      if (\strpos($name, ':') === 0) {
822 10
        $nameTmp = \substr($name, 1);
823
      }
824
825 12
      $parseKeyInner = $nameTmp . '-' . $parseKey;
826 12
      $sqlBefore = $sql;
827 12
      $secureParamValue = $this->secure($params[$name]);
828
829 12
      while (\strpos($sql, $parseKeyInner) !== false) {
830 12
        $sql = UTF8::str_replace_first(
831 12
            $parseKeyInner,
832 12
            (string)$secureParamValue,
833 12
            $sql
834
        );
835
      }
836
837 12
      if ($sqlBefore !== $sql) {
838 12
        unset($params[$name]);
839
      }
840
    }
841
842 12
    return ['sql' => $sql, 'params' => $params];
843
  }
844
845
  /**
846
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
847
   *
848
   * @param mixed     $var           boolean: convert into "integer"<br />
849
   *                                 int: int (don't change it)<br />
850
   *                                 float: float (don't change it)<br />
851
   *                                 null: null (don't change it)<br />
852
   *                                 array: run escape() for every key => value<br />
853
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
854
   * @param bool      $stripe_non_utf8
855
   * @param bool      $html_entity_decode
856
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
857
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
858
   *                                 <strong>null</strong> => Convert the array into null, every time.
859
   *
860
   * @return mixed
861
   */
862 55
  public function escape($var = '', bool $stripe_non_utf8 = true, bool $html_entity_decode = false, $convert_array = false)
863
  {
864
    // [empty]
865 55
    if ($var === '') {
866 2
      return '';
867
    }
868
869
    // ''
870 55
    if ($var === "''") {
871
      return "''";
872
    }
873
874
    // NULL
875 55
    if ($var === null) {
876
      if (
877 2
          $this->_convert_null_to_empty_string === true
878
      ) {
879
        return "''";
880
      }
881
882 2
      return 'NULL';
883
    }
884
885
    // save the current value as int (for later usage)
886 55
    if (!\is_object($var)) {
887 55
      $varInt = (int)$var;
888
    }
889
890
    // "int" || int || bool
891
    if (
892 55
        \is_int($var)
893
        ||
894 52
        \is_bool($var)
895
        ||
896
        (
897 52
            isset($varInt, $var[0])
898
            &&
899 52
            $var[0] != '0'
900
            &&
901 55
            "$varInt" == $var
0 ignored issues
show
Bug introduced by
The variable $varInt does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
902
        )
903
    ) {
904 32
      return (int)$var;
905
    }
906
907
    // float
908 52
    if (\is_float($var)) {
909 5
      return $var;
910
    }
911
912
    // array
913 52
    if (\is_array($var)) {
914
915 4
      if ($convert_array === null) {
916 3
        return null;
917
      }
918
919 2
      $varCleaned = [];
920 2
      foreach ((array)$var as $key => $value) {
921
922 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
923 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
924
925
        /** @noinspection OffsetOperationsInspection */
926 2
        $varCleaned[$key] = $value;
927
      }
928
929 2
      if ($convert_array === true) {
930 1
        $varCleaned = \implode(',', $varCleaned);
931
932 1
        return $varCleaned;
933
      }
934
935 2
      return $varCleaned;
936
    }
937
938
    // "string"
939
    if (
940 52
        \is_string($var)
941
        ||
942
        (
943 3
            \is_object($var)
944
            &&
945 52
            method_exists($var, '__toString')
946
        )
947
    ) {
948 52
      $var = (string)$var;
949
950 52
      if ($stripe_non_utf8 === true) {
951 11
        $var = UTF8::cleanup($var);
952
      }
953
954 52
      if ($html_entity_decode === true) {
955
        // use no-html-entity for db
956 1
        $var = UTF8::html_entity_decode($var);
957
      }
958
959 52
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
960
961 52
      $var = \mysqli_real_escape_string($this->getLink(), $var);
962
963 52
      return (string)$var;
964
965
    }
966
967
    // "DateTime"-object
968 3
    if ($var instanceof \DateTime) {
969 3
      return $this->escape($var->format('Y-m-d H:i:s'), false);
970
    }
971
972 2
    return false;
973
  }
974
975
  /**
976
   * Execute select/insert/update/delete sql-queries.
977
   *
978
   * @param string $query    <p>sql-query</p>
979
   * @param bool   $useCache <p>use cache?</p>
980
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
981
   * @param DB     $db       optional <p>the database connection</p>
982
   *
983
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
984
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
985
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
986
   *               "true" by e.g. "DROP"-queries<br />
987
   *               "false" on error
988
   *
989
   * @throws QueryException
990
   */
991 3
  public static function execSQL(string $query, bool $useCache = false, int $cacheTTL = 3600, self $db = null)
992
  {
993
    // init
994 3
    $cacheKey = null;
995 3
    if (!$db) {
996 3
      $db = self::getInstance();
997
    }
998
999 3 View Code Duplication
    if ($useCache === true) {
1000 1
      $cache = new Cache(null, null, false, $useCache);
1001 1
      $cacheKey = 'sql-' . \md5($query);
1002
1003
      if (
1004 1
          $cache->getCacheIsReady() === true
1005
          &&
1006 1
          $cache->existsItem($cacheKey)
1007
      ) {
1008 1
        return $cache->getItem($cacheKey);
1009
      }
1010
1011
    } else {
1012 3
      $cache = false;
1013
    }
1014
1015 3
    $result = $db->query($query);
1016
1017 3
    if ($result instanceof Result) {
1018
1019 1
      $return = $result->fetchAllArray();
1020
1021
      // save into the cache
1022 View Code Duplication
      if (
1023 1
          $cacheKey !== null
1024
          &&
1025 1
          $useCache === true
1026
          &&
1027 1
          $cache instanceof Cache
1028
          &&
1029 1
          $cache->getCacheIsReady() === true
1030
      ) {
1031 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1032
      }
1033
1034
    } else {
1035 2
      $return = $result;
1036
    }
1037
1038 3
    return $return;
1039
  }
1040
1041
  /**
1042
   * Get all table-names via "SHOW TABLES".
1043
   *
1044
   * @return array
1045
   */
1046 1
  public function getAllTables(): array
1047
  {
1048 1
    $query = 'SHOW TABLES';
1049 1
    $result = $this->query($query);
1050
1051 1
    return $result->fetchAllArray();
1052
  }
1053
1054
  /**
1055
   * @return Debug
1056
   */
1057 9
  public function getDebugger(): Debug
1058
  {
1059 9
    return $this->_debug;
1060
  }
1061
1062
  /**
1063
   * Get errors from "$this->_errors".
1064
   *
1065
   * @return array
1066
   */
1067 1
  public function getErrors(): array
1068
  {
1069 1
    return $this->_debug->getErrors();
1070
  }
1071
1072
  /**
1073
   * getInstance()
1074
   *
1075
   * @param string $hostname
1076
   * @param string $username
1077
   * @param string $password
1078
   * @param string $database
1079
   * @param int    $port                 <p>default is (int)3306</p>
1080
   * @param string $charset              <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1081
   * @param bool   $exit_on_error        <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1082
   *                                     Use false to disable it.</p>
1083
   * @param bool   $echo_on_error        <p>Echo the error if "checkForDev()" returns true.
1084
   *                                     Use false to disable it.</p>
1085
   * @param string $logger_class_name
1086
   * @param string $logger_level         <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1087
   * @param array  $extra_config         <p>
1088
   *                                     'session_to_db' => false|true<br>
1089
   *                                     'socket' => 'string (path)'<br>
1090
   *                                     'ssl' => 'bool'<br>
1091
   *                                     'clientkey' => 'string (path)'<br>
1092
   *                                     'clientcert' => 'string (path)'<br>
1093
   *                                     'cacert' => 'string (path)'<br>
1094
   *                                     </p>
1095
   *
1096
   * @return \voku\db\DB
1097
   */
1098 97
  public static function getInstance(string $hostname = '', string $username = '', string $password = '', string $database = '', $port = 3306, string $charset = 'utf8', bool $exit_on_error = true, bool $echo_on_error = true, string $logger_class_name = '', string $logger_level = '', array $extra_config = []): self
1099
  {
1100
    /**
1101
     * @var $instance DB[]
1102
     */
1103 97
    static $instance = [];
1104
1105
    /**
1106
     * @var $firstInstance DB
1107
     */
1108 97
    static $firstInstance = null;
1109
1110
    if (
1111 97
        $hostname . $username . $password . $database . $port . $charset == '3306utf8'
1112
        &&
1113 97
        null !== $firstInstance
1114
    ) {
1115 14
      return $firstInstance;
1116
    }
1117
1118 97
    $extra_config_string = '';
1119 97
    if (\is_array($extra_config) === true) {
1120 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1121 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1122
      }
1123
    } else {
1124
      // only for backward compatibility
1125
      $extra_config_string = (int)$extra_config;
1126
    }
1127
1128 97
    $connection = \md5(
1129 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1130
    );
1131
1132 97
    if (!isset($instance[$connection])) {
1133 11
      $instance[$connection] = new self(
1134 11
          $hostname,
1135 11
          $username,
1136 11
          $password,
1137 11
          $database,
1138 11
          $port,
1139 11
          $charset,
1140 11
          $exit_on_error,
1141 11
          $echo_on_error,
1142 11
          $logger_class_name,
1143 11
          $logger_level,
1144 11
          $extra_config
1145
      );
1146
1147 5
      if (null === $firstInstance) {
1148 1
        $firstInstance = $instance[$connection];
1149
      }
1150
    }
1151
1152 97
    return $instance[$connection];
1153
  }
1154
1155
  /**
1156
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1157
   *
1158
   * @return \mysqli
1159
   */
1160 56
  public function getLink(): \mysqli
1161
  {
1162 56
    return $this->link;
1163
  }
1164
1165
  /**
1166
   * Get the current charset.
1167
   *
1168
   * @return string
1169
   */
1170 1
  public function get_charset(): string
1171
  {
1172 1
    return $this->charset;
1173
  }
1174
1175
  /**
1176
   * Check if we are in a transaction.
1177
   *
1178
   * @return bool
1179
   */
1180
  public function inTransaction(): bool
1181
  {
1182
    return $this->_in_transaction;
1183
  }
1184
1185
  /**
1186
   * Execute a "insert"-query.
1187
   *
1188
   * @param string      $table
1189
   * @param array       $data
1190
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1191
   *
1192
   * @return false|int <p>false on error</p>
1193
   *
1194
   * @throws QueryException
1195
   */
1196 28
  public function insert(string $table, array $data = [], string $databaseName = null)
1197
  {
1198
    // init
1199 28
    $table = \trim($table);
1200
1201 28
    if ($table === '') {
1202 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1203
1204 2
      return false;
1205
    }
1206
1207 27
    if (\count($data) === 0) {
1208 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1209
1210 3
      return false;
1211
    }
1212
1213 25
    $SET = $this->_parseArrayPair($data);
1214
1215 25
    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...
1216
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1217
    }
1218
1219 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1220
1221 25
    return $this->query($sql);
1222
  }
1223
1224
  /**
1225
   * Returns the auto generated id used in the last query.
1226
   *
1227
   * @return int|string
1228
   */
1229 58
  public function insert_id()
1230
  {
1231 58
    return \mysqli_insert_id($this->link);
1232
  }
1233
1234
  /**
1235
   * Check if db-connection is ready.
1236
   *
1237
   * @return boolean
1238
   */
1239 106
  public function isReady(): bool
1240
  {
1241 106
    return $this->connected ? true : false;
1242
  }
1243
1244
  /**
1245
   * Get the last sql-error.
1246
   *
1247
   * @return string|false <p>false === there was no error</p>
1248
   */
1249 1
  public function lastError()
1250
  {
1251 1
    $errors = $this->_debug->getErrors();
1252
1253 1
    return \count($errors) > 0 ? end($errors) : false;
1254
  }
1255
1256
  /**
1257
   * Execute a sql-multi-query.
1258
   *
1259
   * @param string $sql
1260
   *
1261
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1262
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1263
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1264
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1265
   *
1266
   * @throws QueryException
1267
   */
1268 1
  public function multi_query(string $sql)
1269
  {
1270 1
    if (!$this->isReady()) {
1271
      return false;
1272
    }
1273
1274 1 View Code Duplication
    if (!$sql || $sql === '') {
1275 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1276
1277 1
      return false;
1278
    }
1279
1280 1
    $query_start_time = microtime(true);
1281 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1282 1
    $query_duration = microtime(true) - $query_start_time;
1283
1284 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1285
1286 1
    $returnTheResult = false;
1287 1
    $result = [];
1288
1289 1
    if ($resultTmp) {
1290
      do {
1291
1292 1
        $resultTmpInner = \mysqli_store_result($this->link);
1293
1294 1
        if ($resultTmpInner instanceof \mysqli_result) {
1295
1296 1
          $returnTheResult = true;
1297 1
          $result[] = new Result($sql, $resultTmpInner);
1298
1299
        } else {
1300
1301
          // is the query successful
1302 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1303 1
            $result[] = true;
1304
          } else {
1305
            $result[] = false;
1306
          }
1307
1308
        }
1309
1310 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1311
1312
    } else {
1313
1314
      // log the error query
1315 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1316
1317 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1318
    }
1319
1320
    // return the result only if there was a "SELECT"-query
1321 1
    if ($returnTheResult === true) {
1322 1
      return $result;
1323
    }
1324
1325
    if (
1326 1
        \count($result) > 0
1327
        &&
1328 1
        \in_array(false, $result, true) === false
1329
    ) {
1330 1
      return true;
1331
    }
1332
1333
    return false;
1334
  }
1335
1336
  /**
1337
   * Pings a server connection, or tries to reconnect
1338
   * if the connection has gone down.
1339
   *
1340
   * @return boolean
1341
   */
1342 3
  public function ping(): bool
1343
  {
1344
    if (
1345 3
        $this->link
1346
        &&
1347 3
        $this->link instanceof \mysqli
1348
    ) {
1349
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1350 2
      return (bool)@\mysqli_ping($this->link);
1351
    }
1352
1353 1
    return false;
1354
  }
1355
1356
  /**
1357
   * Get a new "Prepare"-Object for your sql-query.
1358
   *
1359
   * @param string $query
1360
   *
1361
   * @return Prepare
1362
   */
1363 2
  public function prepare(string $query): Prepare
1364
  {
1365 2
    return new Prepare($this, $query);
1366
  }
1367
1368
  /**
1369
   * Execute a sql-query and return the result-array for select-statements.
1370
   *
1371
   * @param string $query
1372
   *
1373
   * @return mixed
1374
   * @deprecated
1375
   * @throws \Exception
1376
   */
1377 1
  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...
1378
  {
1379 1
    $db = self::getInstance();
1380
1381 1
    $args = \func_get_args();
1382
    /** @noinspection SuspiciousAssignmentsInspection */
1383 1
    $query = \array_shift($args);
1384 1
    $query = \str_replace('?', '%s', $query);
1385 1
    $args = \array_map(
1386
        [
1387 1
            $db,
1388 1
            'escape',
1389
        ],
1390 1
        $args
1391
    );
1392 1
    \array_unshift($args, $query);
1393 1
    $query = \sprintf(...$args);
1394 1
    $result = $db->query($query);
1395
1396 1
    if ($result instanceof Result) {
1397 1
      return $result->fetchAllArray();
1398
    }
1399
1400 1
    return $result;
1401
  }
1402
1403
  /**
1404
   * Execute a sql-query.
1405
   *
1406
   * @param string        $sql            <p>The sql query-string.</p>
1407
   *
1408
   * @param array|boolean $params         <p>
1409
   *                                      "array" of sql-query-parameters<br/>
1410
   *                                      "false" if you don't need any parameter (default)<br/>
1411
   *                                      </p>
1412
   *
1413
   * @return bool|int|Result              <p>
1414
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1415
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1416
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1417
   *                                      "true" by e.g. "DROP"-queries<br />
1418
   *                                      "false" on error
1419
   *                                      </p>
1420
   *
1421
   * @throws QueryException
1422
   */
1423 96
  public function query(string $sql = '', $params = false)
1424
  {
1425 96
    if (!$this->isReady()) {
1426
      return false;
1427
    }
1428
1429 96 View Code Duplication
    if (!$sql || $sql === '') {
1430 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1431
1432 4
      return false;
1433
    }
1434
1435
    if (
1436 94
        $params !== false
1437
        &&
1438 94
        \is_array($params)
1439
        &&
1440 94
        \count($params) > 0
1441
    ) {
1442 13
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1443 13
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1444 13
      $sql = $parseQueryParamsByName['sql'];
1445
    }
1446
1447
    // DEBUG
1448
    // var_dump($params);
1449
    // echo $sql . "\n";
1450
1451 94
    $query_start_time = microtime(true);
1452 94
    $query_result = \mysqli_real_query($this->link, $sql);
1453 94
    $query_duration = microtime(true) - $query_start_time;
1454
1455 94
    $this->query_count++;
1456
1457 94
    $mysqli_field_count = \mysqli_field_count($this->link);
1458 94
    if ($mysqli_field_count) {
1459 62
      $result = \mysqli_store_result($this->link);
1460
    } else {
1461 64
      $result = $query_result;
1462
    }
1463
1464 94
    if ($result instanceof \mysqli_result) {
1465
1466
      // log the select query
1467 61
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1468
1469
      // return query result object
1470 61
      return new Result($sql, $result);
1471
    }
1472
1473 66
    if ($query_result === true) {
1474
1475
      // "INSERT" || "REPLACE"
1476 63 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1477 58
        $insert_id = (int)$this->insert_id();
1478 58
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1479
1480 58
        return $insert_id;
1481
      }
1482
1483
      // "UPDATE" || "DELETE"
1484 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1485 12
        $affected_rows = $this->affected_rows();
1486 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1487
1488 12
        return $affected_rows;
1489
      }
1490
1491
      // log the ? query
1492 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1493
1494 27
      return true;
1495
    }
1496
1497
    // log the error query
1498 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1499
1500 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1501
  }
1502
1503
  /**
1504
   * Error-handling for the sql-query.
1505
   *
1506
   * @param string     $errorMessage
1507
   * @param int        $errorNumber
1508
   * @param string     $sql
1509
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1510
   * @param bool       $sqlMultiQuery
1511
   *
1512
   * @throws QueryException
1513
   * @throws DBGoneAwayException
1514
   *
1515
   * @return mixed|false
1516
   */
1517 13
  private function queryErrorHandling(string $errorMessage, int $errorNumber, string $sql, $sqlParams = false, bool $sqlMultiQuery = false)
1518
  {
1519
    if (
1520 13
        $errorMessage === 'DB server has gone away'
1521
        ||
1522 12
        $errorMessage === 'MySQL server has gone away'
1523
        ||
1524 13
        $errorNumber === 2006
1525
    ) {
1526 1
      static $RECONNECT_COUNTER;
1527
1528
      // exit if we have more then 3 "DB server has gone away"-errors
1529 1
      if ($RECONNECT_COUNTER > 3) {
1530
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1531
        throw new DBGoneAwayException($errorMessage);
1532
      }
1533
1534 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1535
1536
      // reconnect
1537 1
      $RECONNECT_COUNTER++;
1538 1
      $this->reconnect(true);
1539
1540
      // re-run the current (non multi) query
1541 1
      if ($sqlMultiQuery === false) {
1542 1
        return $this->query($sql, $sqlParams);
1543
      }
1544
1545
      return false;
1546
    }
1547
1548 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1549
1550 12
    $force_exception_after_error = null; // auto
1551 12
    if ($this->_in_transaction === true) {
1552 4
      $force_exception_after_error = false;
1553
    }
1554
    // this query returned an error, we must display it (only for dev) !!!
1555
1556 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1557
1558 12
    return false;
1559
  }
1560
1561
  /**
1562
   * Quote && Escape e.g. a table name string.
1563
   *
1564
   * @param mixed $str
1565
   *
1566
   * @return string
1567
   */
1568 36
  public function quote_string($str): string
1569
  {
1570 36
    $str = \str_replace(
1571 36
        '`',
1572 36
        '``',
1573 36
        \trim(
1574 36
            (string)$this->escape($str, false),
1575 36
            '`'
1576
        )
1577
    );
1578
1579 36
    return '`' . $str . '`';
1580
  }
1581
1582
  /**
1583
   * Reconnect to the MySQL-Server.
1584
   *
1585
   * @param bool $checkViaPing
1586
   *
1587
   * @return bool
1588
   */
1589 3
  public function reconnect(bool $checkViaPing = false): bool
1590
  {
1591 3
    $ping = false;
1592
1593 3
    if ($checkViaPing === true) {
1594 2
      $ping = $this->ping();
1595
    }
1596
1597 3
    if ($ping !== true) {
1598 3
      $this->connected = false;
1599 3
      $this->connect();
1600
    }
1601
1602 3
    return $this->isReady();
1603
  }
1604
1605
  /**
1606
   * Execute a "replace"-query.
1607
   *
1608
   * @param string      $table
1609
   * @param array       $data
1610
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1611
   *
1612
   * @return false|int <p>false on error</p>
1613
   *
1614
   * @throws QueryException
1615
   */
1616 1
  public function replace(string $table, array $data = [], string $databaseName = null)
1617
  {
1618
    // init
1619 1
    $table = \trim($table);
1620
1621 1
    if ($table === '') {
1622 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1623
1624 1
      return false;
1625
    }
1626
1627 1
    if (\count($data) === 0) {
1628 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1629
1630 1
      return false;
1631
    }
1632
1633
    // extracting column names
1634 1
    $columns = \array_keys($data);
1635 1
    foreach ($columns as $k => $_key) {
1636
      /** @noinspection AlterInForeachInspection */
1637 1
      $columns[$k] = $this->quote_string($_key);
1638
    }
1639
1640 1
    $columns = \implode(',', $columns);
1641
1642
    // extracting values
1643 1
    foreach ($data as $k => $_value) {
1644
      /** @noinspection AlterInForeachInspection */
1645 1
      $data[$k] = $this->secure($_value);
1646
    }
1647 1
    $values = \implode(',', $data);
1648
1649 1
    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...
1650
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1651
    }
1652
1653 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1654
1655 1
    return $this->query($sql);
1656
  }
1657
1658
  /**
1659
   * Rollback in a transaction and end the transaction.
1660
   *
1661
   * @return bool <p>Boolean true on success, false otherwise.</p>
1662
   */
1663 4 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...
1664
  {
1665 4
    if ($this->_in_transaction === false) {
1666
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1667
1668
      return false;
1669
    }
1670
1671 4
    $return = \mysqli_rollback($this->link);
1672 4
    \mysqli_autocommit($this->link, true);
1673 4
    $this->_in_transaction = false;
1674
1675 4
    return $return;
1676
  }
1677
1678
  /**
1679
   * Try to secure a variable, so can you use it in sql-queries.
1680
   *
1681
   * <p>
1682
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1683
   * 1. parse into "int"
1684
   * </p><br />
1685
   *
1686
   * <p>
1687
   * <strong>float:</strong><br />
1688
   * 1. return "float"
1689
   * </p><br />
1690
   *
1691
   * <p>
1692
   * <strong>string:</strong><br />
1693
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1694
   * 2. \trim whitespace<br />
1695
   * 3. \trim '<br />
1696
   * 4. escape the string (and remove non utf-8 chars)<br />
1697
   * 5. \trim ' again (because we maybe removed some chars)<br />
1698
   * 6. add ' around the new string<br />
1699
   * </p><br />
1700
   *
1701
   * <p>
1702
   * <strong>array:</strong><br />
1703
   * 1. return null
1704
   * </p><br />
1705
   *
1706
   * <p>
1707
   * <strong>object:</strong><br />
1708
   * 1. return false
1709
   * </p><br />
1710
   *
1711
   * <p>
1712
   * <strong>null:</strong><br />
1713
   * 1. return null
1714
   * </p>
1715
   *
1716
   * @param mixed $var
1717
   *
1718
   * @return mixed
1719
   */
1720 44
  public function secure($var)
1721
  {
1722 44
    if ($var === '') {
1723 2
      return '';
1724
    }
1725
1726 44
    if ($var === "''") {
1727 1
      return "''";
1728
    }
1729
1730 44
    if ($var === null) {
1731
      if (
1732 2
          $this->_convert_null_to_empty_string === true
1733
      ) {
1734 1
        return "''";
1735
      }
1736
1737 2
      return 'NULL';
1738
    }
1739
1740 43
    if (\in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1741 1
      return $var;
1742
    }
1743
1744 43
    if (\is_string($var)) {
1745 36
      $var = \trim(\trim($var), "'");
1746
    }
1747
1748 43
    $var = $this->escape($var, false, false, null);
1749
1750 43
    if (\is_string($var)) {
1751 36
      $var = "'" . \trim($var, "'") . "'";
1752
    }
1753
1754 43
    return $var;
1755
  }
1756
1757
  /**
1758
   * Execute a "select"-query.
1759
   *
1760
   * @param string       $table
1761
   * @param string|array $where
1762
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1763
   *
1764
   * @return false|Result <p>false on error</p>
1765
   *
1766
   * @throws QueryException
1767
   */
1768 24 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...
1769
  {
1770
    // init
1771 24
    $table = \trim($table);
1772
1773 24
    if ($table === '') {
1774 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1775
1776 1
      return false;
1777
    }
1778
1779 24
    if (\is_string($where)) {
1780 8
      $WHERE = $this->escape($where, false);
1781 17
    } elseif (\is_array($where)) {
1782 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1783
    } else {
1784 1
      $WHERE = '';
1785
    }
1786
1787 24
    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...
1788
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
1789
    }
1790
1791 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1792
1793 24
    return $this->query($sql);
1794
  }
1795
1796
  /**
1797
   * Selects a different database than the one specified on construction.
1798
   *
1799
   * @param string $database <p>Database name to switch to.</p>
1800
   *
1801
   * @return bool <p>Boolean true on success, false otherwise.</p>
1802
   */
1803
  public function select_db(string $database): bool
1804
  {
1805
    if (!$this->isReady()) {
1806
      return false;
1807
    }
1808
1809
    return mysqli_select_db($this->link, $database);
1810
  }
1811
1812
  /**
1813
   * Set the current charset.
1814
   *
1815
   * @param string $charset
1816
   *
1817
   * @return bool
1818
   */
1819 8
  public function set_charset(string $charset): bool
1820
  {
1821 8
    $charsetLower = strtolower($charset);
1822 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1823 6
      $charset = 'utf8';
1824
    }
1825 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1826 6
      $charset = 'utf8mb4';
1827
    }
1828
1829 8
    $this->charset = $charset;
1830
1831 8
    $return = mysqli_set_charset($this->link, $charset);
1832
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1833 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1834
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1835 8
    @\mysqli_query($this->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...
1836
1837 8
    return $return;
1838
  }
1839
1840
  /**
1841
   * Set the option to convert null to "''" (empty string).
1842
   *
1843
   * Used in secure() => select(), insert(), update(), delete()
1844
   *
1845
   * @deprecated It's not recommended to convert NULL into an empty string!
1846
   *
1847
   * @param $bool
1848
   *
1849
   * @return $this
1850
   */
1851 1
  public function set_convert_null_to_empty_string(bool $bool)
1852
  {
1853 1
    $this->_convert_null_to_empty_string = $bool;
1854
1855 1
    return $this;
1856
  }
1857
1858
  /**
1859
   * Enables or disables internal report functions
1860
   *
1861
   * @link http://php.net/manual/en/function.mysqli-report.php
1862
   *
1863
   * @param int $flags <p>
1864
   *                   <table>
1865
   *                   Supported flags
1866
   *                   <tr valign="top">
1867
   *                   <td>Name</td>
1868
   *                   <td>Description</td>
1869
   *                   </tr>
1870
   *                   <tr valign="top">
1871
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1872
   *                   <td>Turns reporting off</td>
1873
   *                   </tr>
1874
   *                   <tr valign="top">
1875
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1876
   *                   <td>Report errors from mysqli function calls</td>
1877
   *                   </tr>
1878
   *                   <tr valign="top">
1879
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1880
   *                   <td>
1881
   *                   Throw <b>mysqli_sql_exception</b> for errors
1882
   *                   instead of warnings
1883
   *                   </td>
1884
   *                   </tr>
1885
   *                   <tr valign="top">
1886
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1887
   *                   <td>Report if no index or bad index was used in a query</td>
1888
   *                   </tr>
1889
   *                   <tr valign="top">
1890
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1891
   *                   <td>Set all options (report all)</td>
1892
   *                   </tr>
1893
   *                   </table>
1894
   *                   </p>
1895
   *
1896
   * @return bool
1897
   */
1898
  public function set_mysqli_report(int $flags): bool
1899
  {
1900
    return \mysqli_report($flags);
1901
  }
1902
1903
  /**
1904
   * Show config errors by throw exceptions.
1905
   *
1906
   * @return bool
1907
   *
1908
   * @throws \InvalidArgumentException
1909
   */
1910 11
  public function showConfigError(): bool
1911
  {
1912
1913
    if (
1914 11
        !$this->hostname
1915
        ||
1916 10
        !$this->username
1917
        ||
1918 11
        !$this->database
1919
    ) {
1920
1921 3
      if (!$this->hostname) {
1922 1
        throw new \InvalidArgumentException('no-sql-hostname');
1923
      }
1924
1925 2
      if (!$this->username) {
1926 1
        throw new \InvalidArgumentException('no-sql-username');
1927
      }
1928
1929 1
      if (!$this->database) {
1930 1
        throw new \InvalidArgumentException('no-sql-database');
1931
      }
1932
1933
      return false;
1934
    }
1935
1936 8
    return true;
1937
  }
1938
1939
  /**
1940
   * alias: "beginTransaction()"
1941
   */
1942 1
  public function startTransaction(): bool
1943
  {
1944 1
    return $this->beginTransaction();
1945
  }
1946
1947
  /**
1948
   * Execute a callback inside a transaction.
1949
   *
1950
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1951
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1952
   *
1953
   * @return bool <p>Boolean true on success, false otherwise.</p>
1954
   */
1955 1
  public function transact($callback): bool
1956
  {
1957
    try {
1958
1959 1
      $beginTransaction = $this->beginTransaction();
1960 1
      if ($beginTransaction === false) {
1961 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1962
1963 1
        return false;
1964
      }
1965
1966 1
      $result = $callback($this);
1967 1
      if ($result === false) {
1968
        /** @noinspection ThrowRawExceptionInspection */
1969 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1970
      }
1971
1972 1
      return $this->commit();
1973
1974 1
    } catch (\Exception $e) {
1975
1976 1
      $this->rollback();
1977
1978 1
      return false;
1979
    }
1980
  }
1981
1982
  /**
1983
   * Execute a "update"-query.
1984
   *
1985
   * @param string       $table
1986
   * @param array        $data
1987
   * @param array|string $where
1988
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1989
   *
1990
   * @return false|int <p>false on error</p>
1991
   *
1992
   * @throws QueryException
1993
   */
1994 7
  public function update(string $table, array $data = [], $where = '1=1', string $databaseName = null)
1995
  {
1996
    // init
1997 7
    $table = \trim($table);
1998
1999 7
    if ($table === '') {
2000 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
2001
2002 1
      return false;
2003
    }
2004
2005 7
    if (\count($data) === 0) {
2006 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
2007
2008 2
      return false;
2009
    }
2010
2011 7
    $SET = $this->_parseArrayPair($data);
2012
2013 7
    if (\is_string($where)) {
2014 2
      $WHERE = $this->escape($where, false);
2015 6
    } elseif (\is_array($where)) {
2016 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
2017
    } else {
2018 1
      $WHERE = '';
2019
    }
2020
2021 7
    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...
2022
      $databaseName = $this->quote_string(\trim($databaseName)) . '.';
2023
    }
2024
2025 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
2026
2027 7
    return $this->query($sql);
2028
  }
2029
2030
  /**
2031
   * Determine if database table exists
2032
   *
2033
   * @param string $table
2034
   *
2035
   * @return bool
2036
   */
2037 1
  public function table_exists(string $table): bool
2038
  {
2039 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2040
2041 1
    return $check !== false
2042
           &&
2043 1
           $check instanceof Result
2044
           &&
2045 1
           $check->num_rows > 0;
2046
  }
2047
2048
  /**
2049
   * Count number of rows found matching a specific query.
2050
   *
2051
   * @param string $query
2052
   *
2053
   * @return int
2054
   */
2055 1
  public function num_rows(string $query): int
2056
  {
2057 1
    $check = $this->query($query);
2058
2059
    if (
2060 1
        $check === false
2061
        ||
2062 1
        !$check instanceof Result
2063
    ) {
2064
      return 0;
2065
    }
2066
2067 1
    return $check->num_rows;
2068
  }
2069
}
2070