Completed
Push — master ( 3989bc...d589d2 )
by Lars
10:26 queued 59s
created

DB::set_mysqli_report()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
3
namespace voku\db;
4
5
use voku\cache\Cache;
6
use voku\db\exceptions\DBConnectException;
7
use voku\db\exceptions\DBGoneAwayException;
8
use voku\db\exceptions\QueryException;
9
use voku\helper\UTF8;
10
11
/**
12
 * DB: This class can handle DB queries via MySQLi.
13
 *
14
 * @package voku\db
15
 */
16
final class DB
17
{
18
19
  /**
20
   * @var int
21
   */
22
  public $query_count = 0;
23
24
  /**
25
   * @var \mysqli
26
   */
27
  private $link;
28
29
  /**
30
   * @var bool
31
   */
32
  private $connected = false;
33
34
  /**
35
   * @var array
36
   */
37
  private $mysqlDefaultTimeFunctions;
38
39
  /**
40
   * @var string
41
   */
42
  private $hostname = '';
43
44
  /**
45
   * @var string
46
   */
47
  private $username = '';
48
49
  /**
50
   * @var string
51
   */
52
  private $password = '';
53
54
  /**
55
   * @var string
56
   */
57
  private $database = '';
58
59
  /**
60
   * @var int
61
   */
62
  private $port = 3306;
63
64
  /**
65
   * @var string
66
   */
67
  private $charset = 'utf8';
68
69
  /**
70
   * @var string
71
   */
72
  private $socket = '';
73
74
  /**
75
   * @var bool
76
   */
77
  private $session_to_db = false;
78
79
  /**
80
   * @var bool
81
   */
82
  private $_in_transaction = false;
83
84
  /**
85
   * @var bool
86
   */
87
  private $_convert_null_to_empty_string = false;
88
89
  /**
90
   * @var bool
91
   */
92
  private $_ssl = false;
93
94
  /**
95
   * The path name to the key file
96
   *
97
   * @var string
98
   */
99
  private $_clientkey;
100
101
  /**
102
   * The path name to the certificate file
103
   *
104
   * @var string
105
   */
106
  private $_clientcert;
107
108
  /**
109
   * The path name to the certificate authority file
110
   *
111
   * @var string
112
   */
113
  private $_cacert;
114
115
  /**
116
   * @var Debug
117
   */
118
  private $_debug;
119
120
  /**
121
   * __construct()
122
   *
123
   * @param string         $hostname
124
   * @param string         $username
125
   * @param string         $password
126
   * @param string         $database
127
   * @param int            $port
128
   * @param string         $charset
129
   * @param bool|string $exit_on_error   <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
130
   *                                     Use a empty string "" or false to disable it.</p>
131
   * @param bool|string $echo_on_error   <p>Echo the error if "checkForDev()" returns true.
132
   *                                     Use a empty string "" or false to disable it.</p>
133
   * @param string         $logger_class_name
134
   * @param string         $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
135
   * @param array          $extra_config  <p>
136
   *                                      'session_to_db' => false|true<br>
137
   *                                      'socket' => 'string (path)'<br>
138
   *                                      'ssl' => 'bool'<br>
139
   *                                      'clientkey' => 'string (path)'<br>
140
   *                                      'clientcert' => 'string (path)'<br>
141
   *                                      'cacert' => 'string (path)'<br>
142
   *                                      </p>
143
   */
144 11
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config = array())
145
  {
146 11
    $this->connected = false;
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
        $extra_config
162 11
    );
163
164 8
    $this->connect();
165
166 5
    $this->mysqlDefaultTimeFunctions = array(
167
      // Returns the current date.
168 5
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170 5
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172 5
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174 5
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176 5
      'CURTIME()',
177
      // Synonym for NOW()
178 5
      'LOCALTIME()',
179
      // Synonym for NOW()
180 5
      'LOCALTIMESTAMP()',
181
      // Returns the current date and time.
182 5
      'NOW()',
183
      // Returns the time at which the function executes.
184 5
      'SYSDATE()',
185
      // Returns a UNIX timestamp.
186 5
      'UNIX_TIMESTAMP()',
187
      // Returns the current UTC date.
188 5
      'UTC_DATE()',
189
      // Returns the current UTC time.
190 5
      'UTC_TIME()',
191
      // Returns the current UTC date and time.
192 5
      '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($sql = null, array $bindings = array())
231
  {
232 2
    return isset($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|string     $port          <p>default is (int)3306</p>
253
   * @param string         $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
254
   * @param bool|string $exit_on_error   <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
255
   *                                     Use a empty string "" or false to disable it.</p>
256
   * @param bool|string $echo_on_error   <p>Echo the error if "checkForDev()" returns true.
257
   *                                     Use a empty string "" or 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($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
272
  {
273 11
    $this->hostname = (string)$hostname;
274 11
    $this->username = (string)$username;
275 11
    $this->password = (string)$password;
276 11
    $this->database = (string)$database;
277
278 11
    if ($charset) {
279 5
      $this->charset = (string)$charset;
280 5
    }
281
282 11
    if ($port) {
283 5
      $this->port = (int)$port;
284 5
    } else {
285
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
286
      /** @noinspection UsageOfSilenceOperatorInspection */
287 7
      $this->port = (int)@ini_get('mysqli.default_port');
288
    }
289
290
    // fallback
291 11
    if (!$this->port) {
292
      $this->port = 3306;
293
    }
294
295 11
    if (!$this->socket) {
296
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
297 11
      $this->socket = @ini_get('mysqli.default_socket');
298 11
    }
299
300 11
    if ($exit_on_error === true || $exit_on_error === false) {
301 11
      $this->_debug->setExitOnError($exit_on_error);
302 11
    }
303
304 11
    if ($echo_on_error === true || $echo_on_error === false) {
305 11
      $this->_debug->setEchoOnError($echo_on_error);
306 11
    }
307
308 11
    $this->_debug->setLoggerClassName($logger_class_name);
309 11
    $this->_debug->setLoggerLevel($logger_level);
310
311 11
    if (is_array($extra_config) === true) {
312
313 11
      if (isset($extra_config['session_to_db'])) {
314
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
315
      }
316
317 11
      if (isset($extra_config['socket'])) {
318
        $this->socket = $extra_config['socket'];
319
      }
320
321 11
      if (isset($extra_config['ssl'])) {
322
        $this->_ssl = $extra_config['ssl'];
323
      }
324
325 11
      if (isset($extra_config['clientkey'])) {
326
        $this->_clientkey = $extra_config['clientkey'];
327
      }
328
329 11
      if (isset($extra_config['clientcert'])) {
330
        $this->_clientcert = $extra_config['clientcert'];
331
      }
332
333 11
      if (isset($extra_config['cacert'])) {
334
        $this->_cacert = $extra_config['cacert'];
335
      }
336
337 11
    } else {
338
      // only for backward compatibility
339
      $this->session_to_db = (boolean)$extra_config;
340
    }
341
342 11
    return $this->showConfigError();
343
  }
344
345
  /**
346
   * Parses arrays with value pairs and generates SQL to use in queries.
347
   *
348
   * @param array  $arrayPair
349
   * @param string $glue <p>This is the separator.</p>
350
   *
351
   * @return string
352
   *
353
   * @internal
354
   */
355 30
  public function _parseArrayPair($arrayPair, $glue = ',')
356
  {
357
    // init
358 30
    $sql = '';
359
360
    /** @noinspection IsEmptyFunctionUsageInspection */
361 30
    if (empty($arrayPair)) {
362
      return '';
363
    }
364
365 30
    $arrayPairCounter = 0;
366 30
    foreach ($arrayPair as $_key => $_value) {
367 30
      $_connector = '=';
368 30
      $_glueHelper = '';
369 30
      $_key_upper = strtoupper($_key);
370
371 30
      if (strpos($_key_upper, ' NOT') !== false) {
372 2
        $_connector = 'NOT';
373 2
      }
374
375 30
      if (strpos($_key_upper, ' IS') !== false) {
376 1
        $_connector = 'IS';
377 1
      }
378
379 30
      if (strpos($_key_upper, ' IS NOT') !== false) {
380 1
        $_connector = 'IS NOT';
381 1
      }
382
383 30
      if (strpos($_key_upper, ' IN') !== false) {
384 1
        $_connector = 'IN';
385 1
      }
386
387 30
      if (strpos($_key_upper, ' NOT IN') !== false) {
388 1
        $_connector = 'NOT IN';
389 1
      }
390
391 30
      if (strpos($_key_upper, ' BETWEEN') !== false) {
392 1
        $_connector = 'BETWEEN';
393 1
      }
394
395 30
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
396 1
        $_connector = 'NOT BETWEEN';
397 1
      }
398
399 30
      if (strpos($_key_upper, ' LIKE') !== false) {
400 2
        $_connector = 'LIKE';
401 2
      }
402
403 30
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
404 2
        $_connector = 'NOT LIKE';
405 2
      }
406
407 30 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
408 4
        $_connector = '>';
409 4
      }
410
411 30 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
412 1
        $_connector = '<';
413 1
      }
414
415 30
      if (strpos($_key_upper, ' >=') !== false) {
416 4
        $_connector = '>=';
417 4
      }
418
419 30
      if (strpos($_key_upper, ' <=') !== false) {
420 1
        $_connector = '<=';
421 1
      }
422
423 30
      if (strpos($_key_upper, ' <>') !== false) {
424 1
        $_connector = '<>';
425 1
      }
426
427 30
      if (strpos($_key_upper, ' OR') !== false) {
428 2
        $_glueHelper = 'OR';
429 2
      }
430
431 30
      if (strpos($_key_upper, ' AND') !== false) {
432 1
        $_glueHelper = 'AND';
433 1
      }
434
435 30
      if (is_array($_value) === true) {
436 2
        foreach ($_value as $oldKey => $oldValue) {
437 2
          $_value[$oldKey] = $this->secure($oldValue);
438 2
        }
439
440 2
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
441 1
          $_value = '(' . implode(',', $_value) . ')';
442 2
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
443 1
          $_value = '(' . implode(' AND ', $_value) . ')';
444 1
        }
445
446 2
      } else {
447 30
        $_value = $this->secure($_value);
448
      }
449
450 30
      $quoteString = $this->quote_string(
451 30
          trim(
452 30
              str_ireplace(
453
                  array(
454 30
                      $_connector,
455 30
                      $_glueHelper,
456 30
                  ),
457 30
                  '',
458
                  $_key
459 30
              )
460 30
          )
461 30
      );
462
463 30
      if (!is_array($_value)) {
464 30
        $_value = array($_value);
465 30
      }
466
467 30
      if (!$_glueHelper) {
468 30
        $_glueHelper = $glue;
469 30
      }
470
471 30
      $tmpCounter = 0;
472 30
      foreach ($_value as $valueInner) {
473
474 30
        $_glueHelperInner = $_glueHelper;
475
476 30
        if ($arrayPairCounter === 0) {
477
478 30
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
479 1
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
480 30
          } elseif ($tmpCounter === 0) {
481 30
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
482 30
          }
483
484 30
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
485 1
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
486 1
        }
487
488 30
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
489 30
        $tmpCounter++;
490 30
      }
491
492 30
      if ($_glueHelper === 'OR') {
493 2
        $sql .= ' ) ';
494 2
      }
495
496 30
      $arrayPairCounter++;
497 30
    }
498
499 30
    return $sql;
500
  }
501
502
  /**
503
   * _parseQueryParams
504
   *
505
   * @param string $sql
506
   * @param array  $params
507
   *
508
   * @return array <p>with the keys -> 'sql', 'params'</p>
509
   */
510 8
  private function _parseQueryParams($sql, array $params)
511
  {
512
    // is there anything to parse?
513 View Code Duplication
    if (
514 8
        strpos($sql, '?') === false
515 8
        ||
516 3
        count($params) === 0
517 8
    ) {
518 6
      return array('sql' => $sql, 'params' => $params);
519
    }
520
521 3
    $parseKey = md5(uniqid((string)mt_rand(), true));
522 3
    $sql = str_replace('?', $parseKey, $sql);
523
524 3
    $k = 0;
525 3
    while (strpos($sql, $parseKey) !== false) {
526 3
      $sql = UTF8::str_replace_first(
527 3
          $parseKey,
528 3
          isset($params[$k]) ? $this->secure($params[$k]) : '',
529
          $sql
0 ignored issues
show
Bug introduced by
It seems like $sql defined by \voku\helper\UTF8::str_r...params[$k]) : '', $sql) on line 526 can also be of type array<integer,string>; however, voku\helper\UTF8::str_replace_first() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
530 3
      );
531
532 3
      if (isset($params[$k])) {
533 3
        unset($params[$k]);
534 3
      }
535
536 3
      $k++;
537 3
    }
538
539 3
    return array('sql' => $sql, 'params' => $params);
540
  }
541
542
  /**
543
   * Gets the number of affected rows in a previous MySQL operation.
544
   *
545
   * @return int
546
   */
547 12
  public function affected_rows()
548
  {
549 12
    return \mysqli_affected_rows($this->link);
550
  }
551
552
  /**
553
   * Begins a transaction, by turning off auto commit.
554
   *
555
   * @return bool <p>This will return true or false indicating success of transaction</p>
556
   */
557 6 View Code Duplication
  public function beginTransaction()
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...
558
  {
559 6
    if ($this->_in_transaction === true) {
560 2
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
561
562 2
      return false;
563
    }
564
565 6
    $this->clearErrors(); // needed for "$this->endTransaction()"
566 6
    $this->_in_transaction = true;
567 6
    $return = \mysqli_autocommit($this->link, false);
568 6
    if ($return === false) {
569
      $this->_in_transaction = false;
570
    }
571
572 6
    return $return;
573
  }
574
575
  /**
576
   * Clear the errors in "_debug->_errors".
577
   *
578
   * @return bool
579
   */
580 6
  public function clearErrors()
581
  {
582 6
    return $this->_debug->clearErrors();
583
  }
584
585
  /**
586
   * Closes a previously opened database connection.
587
   */
588 2
  public function close()
589
  {
590 2
    $this->connected = false;
591
592 2
    if ($this->link) {
593 2
      \mysqli_close($this->link);
594 2
    }
595 2
  }
596
597
  /**
598
   * Commits the current transaction and end the transaction.
599
   *
600
   * @return bool <p>Boolean true on success, false otherwise.</p>
601
   */
602 2 View Code Duplication
  public function commit()
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...
603
  {
604 2
    if ($this->_in_transaction === false) {
605
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
606
607
      return false;
608
    }
609
610 2
    $return = mysqli_commit($this->link);
611 2
    \mysqli_autocommit($this->link, true);
612 2
    $this->_in_transaction = false;
613
614 2
    return $return;
615
  }
616
617
  /**
618
   * Open a new connection to the MySQL server.
619
   *
620
   * @return bool
621
   *
622
   * @throws DBConnectException
623
   */
624 10
  public function connect()
625
  {
626 10
    if ($this->isReady()) {
627 1
      return true;
628
    }
629
630 10
    $flags = null;
631
632 10
    \mysqli_report(MYSQLI_REPORT_STRICT);
633
    try {
634 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> 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...
635
636 10
      if (Helper::isMysqlndIsUsed() === true) {
637 10
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
638 10
      }
639
640 10
      if ($this->_ssl === true) {
641
642
        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...
643
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
644
        }
645
646
        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...
647
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
648
        }
649
650
        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...
651
          throw new DBConnectException('Error connecting to mysql server: cacert not defined');
652
        }
653
654
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
655
656
        /** @noinspection PhpParamsInspection */
657
        \mysqli_ssl_set(
658
            $this->link,
659
            $this->_clientkey,
660
            $this->_clientcert,
661
            $this->_cacert,
662
            null,
663
            null
664
        );
665
666
        $flags = MYSQLI_CLIENT_SSL;
667
      }
668
669
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
670 10
      $this->connected = @\mysqli_real_connect(
671 10
          $this->link,
672 10
          $this->hostname,
673 10
          $this->username,
674 10
          $this->password,
675 10
          $this->database,
676 10
          $this->port,
677 10
          $this->socket,
678
          $flags
679 10
      );
680
681 10
    } catch (\Exception $e) {
682 3
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
683 3
      $this->_debug->displayError($error, true);
684
      throw new DBConnectException($error, 100, $e);
685
    }
686 7
    \mysqli_report(MYSQLI_REPORT_OFF);
687
688 7
    $errno = mysqli_connect_errno();
689 7
    if (!$this->connected || $errno) {
690
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
691
      $this->_debug->displayError($error, true);
692
      throw new DBConnectException($error, 101);
693
    }
694
695 7
    $this->set_charset($this->charset);
696
697 7
    return $this->isReady();
698
  }
699
700
  /**
701
   * Execute a "delete"-query.
702
   *
703
   * @param string       $table
704
   * @param string|array $where
705
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
706
   *
707
   * @return false|int <p>false on error</p>
708
   *
709
   *    * @throws QueryException
710
   */
711 2 View Code Duplication
  public function delete($table, $where, $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...
712
  {
713
    // init
714 2
    $table = trim($table);
715
716 2
    if ($table === '') {
717 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
718
719 1
      return false;
720
    }
721
722 2
    if (is_string($where)) {
723 1
      $WHERE = $this->escape($where, false);
724 2
    } elseif (is_array($where)) {
725 2
      $WHERE = $this->_parseArrayPair($where, 'AND');
726 2
    } else {
727 1
      $WHERE = '';
728
    }
729
730 2
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null 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...
731
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
732
    }
733
734 2
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
735
736 2
    return $this->query($sql);
737
  }
738
739
  /**
740
   * Ends a transaction and commits if no errors, then ends autocommit.
741
   *
742
   * @return bool <p>This will return true or false indicating success of transactions.</p>
743
   */
744 4 View Code Duplication
  public function endTransaction()
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...
745
  {
746 4
    if ($this->_in_transaction === false) {
747
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
748
749
      return false;
750
    }
751
752 4
    if (!$this->errors()) {
753 1
      $return = \mysqli_commit($this->link);
754 1
    } else {
755 3
      $this->rollback();
756 3
      $return = false;
757
    }
758
759 4
    \mysqli_autocommit($this->link, true);
760 4
    $this->_in_transaction = false;
761
762 4
    return $return;
763
  }
764
765
  /**
766
   * Get all errors from "$this->_errors".
767
   *
768
   * @return array|false <p>false === on errors</p>
769
   */
770 4
  public function errors()
771
  {
772 4
    $errors = $this->_debug->getErrors();
773
774 4
    return count($errors) > 0 ? $errors : false;
775
  }
776
777
  /**
778
   * Returns the SQL by replacing :placeholders with SQL-escaped values.
779
   *
780
   * @param mixed $sql    <p>The SQL string.</p>
781
   * @param array $params <p>An array of key-value bindings.</p>
782
   *
783
   * @return array <p>with the keys -> 'sql', 'params'</p>
784
   */
785 9
  public function _parseQueryParamsByName($sql, array $params = array())
786
  {
787
    // is there anything to parse?
788 View Code Duplication
    if (
789 9
        strpos($sql, ':') === false
790 9
        ||
791 7
        count($params) === 0
792 9
    ) {
793 3
      return array('sql' => $sql, 'params' => $params);
794
    }
795
796 7
    $parseKey = md5(uniqid((string)mt_rand(), true));
797
798 7
    foreach ($params as $name => $value) {
799 7
      $nameTmp = $name;
800 7
      if (strpos($name, ':') === 0) {
801 5
        $nameTmp = substr($name, 1);
802 5
      }
803
804 7
      $parseKeyInner = $nameTmp . '-' . $parseKey;
805 7
      $sql = str_replace(':' . $nameTmp, $parseKeyInner, $sql);
806 7
    }
807
808 7
    foreach ($params as $name => $value) {
809 7
      $nameTmp = $name;
810 7
      if (strpos($name, ':') === 0) {
811 5
        $nameTmp = substr($name, 1);
812 5
      }
813
814 7
      $parseKeyInner = $nameTmp . '-' . $parseKey;
815 7
      $sqlBefore = $sql;
816
817 7
      while (strpos($sql, $parseKeyInner) !== false) {
818 7
        $sql = UTF8::str_replace_first(
819 7
            $parseKeyInner,
820 7
            $this->secure($params[$name]),
821
            $sql
822 7
        );
823 7
      }
824
825 7
      if ($sqlBefore !== $sql) {
826 7
        unset($params[$name]);
827 7
      }
828 7
    }
829
830 7
    return array('sql' => $sql, 'params' => $params);
831
  }
832
833
  /**
834
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
835
   *
836
   * @param mixed     $var           boolean: convert into "integer"<br />
837
   *                                 int: int (don't change it)<br />
838
   *                                 float: float (don't change it)<br />
839
   *                                 null: null (don't change it)<br />
840
   *                                 array: run escape() for every key => value<br />
841
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
842
   * @param bool      $stripe_non_utf8
843
   * @param bool      $html_entity_decode
844
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
845
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
846
   *                                 <strong>null</strong> => Convert the array into null, every time.
847
   *
848
   * @return mixed
849
   */
850 51
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
851
  {
852 51
    if ($var === '') {
853 2
      return '';
854
    }
855
856 51
    if ($var === null) {
857 3
      return null;
858
    }
859
860
    // save the current value as int (for later usage)
861 51
    if (!is_object($var)) {
862 51
      $varInt = (int)$var;
863 51
    }
864
865
    /** @noinspection TypeUnsafeComparisonInspection */
866
    if (
867 51
        is_int($var)
868
        ||
869 50
        is_bool($var)
870 50
        ||
871
        (
872 50
            isset($varInt, $var[0])
873 50
            &&
874 50
            $var[0] != '0'
875 50
            &&
876
            "$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...
877 50
        )
878 51
    ) {
879
880
      // "int" || int || bool
881
882 29
      return (int)$var;
883
    }
884
885 50
    if (is_float($var)) {
886
887
      // float
888
889 5
      return $var;
890
    }
891
892 50
    if (is_array($var)) {
893
894
      // array
895
896 4
      if ($convert_array === null) {
897 3
        return null;
898
      }
899
900 2
      $varCleaned = array();
901 2
      foreach ((array)$var as $key => $value) {
902
903 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
904 2
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
905
906
        /** @noinspection OffsetOperationsInspection */
907 2
        $varCleaned[$key] = $value;
908 2
      }
909
910 2
      if ($convert_array === true) {
911 1
        $varCleaned = implode(',', $varCleaned);
912
913 1
        return $varCleaned;
914
      }
915
916 2
      return (array)$varCleaned;
917
    }
918
919
    if (
920 50
        is_string($var)
921
        ||
922
        (
923 3
            is_object($var)
924 3
            &&
925 3
            method_exists($var, '__toString')
926 3
        )
927 50
    ) {
928
929
      // "string"
930
931 50
      $var = (string)$var;
932
933 50
      if ($stripe_non_utf8 === true) {
934 11
        $var = UTF8::cleanup($var);
935 11
      }
936
937 50
      if ($html_entity_decode === true) {
938
        // use no-html-entity for db
939 1
        $var = UTF8::html_entity_decode($var);
940 1
      }
941
942 50
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
943
944 50
      $var = \mysqli_real_escape_string($this->getLink(), $var);
945
946 50
      return (string)$var;
947
948
    }
949
950 3
    if ($var instanceof \DateTime) {
951
952
      // "DateTime"-object
953
954
      try {
955 3
        return $this->escape($var->format('Y-m-d H:i:s'), false);
956
      } catch (\Exception $e) {
957
        return null;
958
      }
959
960
    } else {
961 2
      return false;
962
    }
963
  }
964
965
  /**
966
   * Execute select/insert/update/delete sql-queries.
967
   *
968
   * @param string $query    <p>sql-query</p>
969
   * @param bool   $useCache <p>use cache?</p>
970
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
971
   * @param DB     $db       optional <p>the database connection</p>
972
   *
973
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
974
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
975
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
976
   *               "true" by e.g. "DROP"-queries<br />
977
   *               "false" on error
978
   *
979
   * @throws QueryException
980
   */
981 3
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
982
  {
983
    // init
984 3
    $cacheKey = null;
985 3
    if (!$db) {
986 3
      $db = self::getInstance();
987 3
    }
988
989 3 View Code Duplication
    if ($useCache === true) {
990 1
      $cache = new Cache(null, null, false, $useCache);
991 1
      $cacheKey = 'sql-' . md5($query);
992
993
      if (
994 1
          $cache->getCacheIsReady() === true
995 1
          &&
996 1
          $cache->existsItem($cacheKey)
997 1
      ) {
998 1
        return $cache->getItem($cacheKey);
999
      }
1000
1001 1
    } else {
1002 3
      $cache = false;
1003
    }
1004
1005 3
    $result = $db->query($query);
1006
1007 3
    if ($result instanceof Result) {
1008
1009 1
      $return = $result->fetchAllArray();
1010
1011
      // save into the cache
1012 View Code Duplication
      if (
1013
          $cacheKey !== null
1014 1
          &&
1015
          $useCache === true
1016 1
          &&
1017
          $cache instanceof Cache
1018 1
          &&
1019 1
          $cache->getCacheIsReady() === true
1020 1
      ) {
1021 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1022 1
      }
1023
1024 1
    } else {
1025 2
      $return = $result;
1026
    }
1027
1028 3
    return $return;
1029
  }
1030
1031
  /**
1032
   * Get all table-names via "SHOW TABLES".
1033
   *
1034
   * @return array
1035
   */
1036 1
  public function getAllTables()
1037
  {
1038 1
    $query = 'SHOW TABLES';
1039 1
    $result = $this->query($query);
1040
1041 1
    return $result->fetchAllArray();
1042
  }
1043
1044
  /**
1045
   * @return Debug
1046
   */
1047 9
  public function getDebugger()
1048
  {
1049 9
    return $this->_debug;
1050
  }
1051
1052
  /**
1053
   * Get errors from "$this->_errors".
1054
   *
1055
   * @return array
1056
   */
1057 1
  public function getErrors()
1058
  {
1059 1
    return $this->_debug->getErrors();
1060
  }
1061
1062
  /**
1063
   * getInstance()
1064
   *
1065
   * @param string      $hostname
1066
   * @param string      $username
1067
   * @param string      $password
1068
   * @param string      $database
1069
   * @param int|string  $port            <p>default is (int)3306</p>
1070
   * @param string      $charset         <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1071
   * @param bool|string $exit_on_error   <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1072
   *                                     Use a empty string "" or false to disable it.</p>
1073
   * @param bool|string $echo_on_error   <p>Echo the error if "checkForDev()" returns true.
1074
   *                                     Use a empty string "" or false to disable it.</p>
1075
   * @param string      $logger_class_name
1076
   * @param string      $logger_level    <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1077
   * @param array       $extra_config    <p>
1078
   *                                     'session_to_db' => false|true<br>
1079
   *                                     'socket' => 'string (path)'<br>
1080
   *                                     'ssl' => 'bool'<br>
1081
   *                                     'clientkey' => 'string (path)'<br>
1082
   *                                     'clientcert' => 'string (path)'<br>
1083
   *                                     'cacert' => 'string (path)'<br>
1084
   *                                     </p>
1085
   *
1086
   * @return \voku\db\DB
1087
   */
1088 97
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = true, $echo_on_error = true, $logger_class_name = '', $logger_level = '', $extra_config = array())
1089
  {
1090
    /**
1091
     * @var $instance DB[]
1092
     */
1093 97
    static $instance = array();
1094
1095
    /**
1096
     * @var $firstInstance DB
1097
     */
1098 97
    static $firstInstance = null;
1099
1100
    if (
1101 97
        $hostname . $username . $password . $database . $port . $charset == ''
1102 97
        &&
1103 14
        null !== $firstInstance
1104 97
    ) {
1105 14
      return $firstInstance;
1106
    }
1107
1108 97
    $extra_config_string = '';
1109 97
    if (is_array($extra_config) === true) {
1110 97
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1111
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1112 97
      }
1113 97
    } else {
1114
      // only for backward compatibility
1115
      $extra_config_string = (int)$extra_config;
1116
    }
1117
1118 97
    $connection = md5(
1119 97
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1120 97
    );
1121
1122 97
    if (!isset($instance[$connection])) {
1123 11
      $instance[$connection] = new self(
1124 11
          $hostname,
1125 11
          $username,
1126 11
          $password,
1127 11
          $database,
1128 11
          $port,
1129 11
          $charset,
1130 11
          $exit_on_error,
1131 11
          $echo_on_error,
1132 11
          $logger_class_name,
1133 11
          $logger_level,
1134
          $extra_config
1135 11
      );
1136
1137 5
      if (null === $firstInstance) {
1138 1
        $firstInstance = $instance[$connection];
1139 1
      }
1140 5
    }
1141
1142 97
    return $instance[$connection];
1143
  }
1144
1145
  /**
1146
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1147
   *
1148
   * @return \mysqli
1149
   */
1150 54
  public function getLink()
1151
  {
1152 54
    return $this->link;
1153
  }
1154
1155
  /**
1156
   * Get the current charset.
1157
   *
1158
   * @return string
1159
   */
1160 1
  public function get_charset()
1161
  {
1162 1
    return $this->charset;
1163
  }
1164
1165
  /**
1166
   * Check if we are in a transaction.
1167
   *
1168
   * @return bool
1169
   */
1170
  public function inTransaction()
1171
  {
1172
    return $this->_in_transaction;
1173
  }
1174
1175
  /**
1176
   * Execute a "insert"-query.
1177
   *
1178
   * @param string      $table
1179
   * @param array       $data
1180
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1181
   *
1182
   * @return false|int <p>false on error</p>
1183
   *
1184
   * @throws QueryException
1185
   */
1186 28
  public function insert($table, array $data = array(), $databaseName = null)
1187
  {
1188
    // init
1189 28
    $table = trim($table);
1190
1191 28
    if ($table === '') {
1192 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1193
1194 2
      return false;
1195
    }
1196
1197 27
    if (count($data) === 0) {
1198 3
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1199
1200 3
      return false;
1201
    }
1202
1203 25
    $SET = $this->_parseArrayPair($data);
1204
1205 25
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null 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...
1206
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1207
    }
1208
1209 25
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1210
1211 25
    return $this->query($sql);
1212
  }
1213
1214
  /**
1215
   * Returns the auto generated id used in the last query.
1216
   *
1217
   * @return int|string
1218
   */
1219 56
  public function insert_id()
1220
  {
1221 56
    return \mysqli_insert_id($this->link);
1222
  }
1223
1224
  /**
1225
   * Check if db-connection is ready.
1226
   *
1227
   * @return boolean
1228
   */
1229 95
  public function isReady()
1230
  {
1231 95
    return $this->connected ? true : false;
1232
  }
1233
1234
  /**
1235
   * Get the last sql-error.
1236
   *
1237
   * @return string|false <p>false === there was no error</p>
1238
   */
1239 1
  public function lastError()
1240
  {
1241 1
    $errors = $this->_debug->getErrors();
1242
1243 1
    return count($errors) > 0 ? end($errors) : false;
1244
  }
1245
1246
  /**
1247
   * Execute a sql-multi-query.
1248
   *
1249
   * @param string $sql
1250
   *
1251
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1252
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1253
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1254
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1255
   *
1256
   * @throws QueryException
1257
   */
1258 1
  public function multi_query($sql)
1259
  {
1260 1
    if (!$this->isReady()) {
1261
      return false;
1262
    }
1263
1264 1 View Code Duplication
    if (!$sql || $sql === '') {
1265 1
      $this->_debug->displayError('Can not execute an empty query.', false);
1266
1267 1
      return false;
1268
    }
1269
1270 1
    $query_start_time = microtime(true);
1271 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1272 1
    $query_duration = microtime(true) - $query_start_time;
1273
1274 1
    $this->_debug->logQuery($sql, $query_duration, 0);
1275
1276 1
    $returnTheResult = false;
1277 1
    $result = array();
1278
1279 1
    if ($resultTmp) {
1280
      do {
1281
1282 1
        $resultTmpInner = \mysqli_store_result($this->link);
1283
1284 1
        if ($resultTmpInner instanceof \mysqli_result) {
1285
1286 1
          $returnTheResult = true;
1287 1
          $result[] = new Result($sql, $resultTmpInner);
1288
1289 1
        } else {
1290
1291
          // is the query successful
1292 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1293 1
            $result[] = true;
1294 1
          } else {
1295
            $result[] = false;
1296
          }
1297
1298
        }
1299
1300 1
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1301
1302 1
    } else {
1303
1304
      // log the error query
1305 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1306
1307 1
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1308
    }
1309
1310
    // return the result only if there was a "SELECT"-query
1311 1
    if ($returnTheResult === true) {
1312 1
      return $result;
1313
    }
1314
1315
    if (
1316 1
        count($result) > 0
1317 1
        &&
1318 1
        in_array(false, $result, true) === false
1319 1
    ) {
1320 1
      return true;
1321
    }
1322
1323
    return false;
1324
  }
1325
1326
  /**
1327
   * Pings a server connection, or tries to reconnect
1328
   * if the connection has gone down.
1329
   *
1330
   * @return boolean
1331
   */
1332 3
  public function ping()
1333
  {
1334
    if (
1335 3
        $this->link
1336 3
        &&
1337 3
        $this->link instanceof \mysqli
1338 3
    ) {
1339
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1340
      /** @noinspection UsageOfSilenceOperatorInspection */
1341 3
      return (bool)@\mysqli_ping($this->link);
1342
    }
1343
1344
    return false;
1345
  }
1346
1347
  /**
1348
   * Get a new "Prepare"-Object for your sql-query.
1349
   *
1350
   * @param string $query
1351
   *
1352
   * @return Prepare
1353
   */
1354 2
  public function prepare($query)
1355
  {
1356 2
    return new Prepare($this, $query);
1357
  }
1358
1359
  /**
1360
   * Execute a sql-query and return the result-array for select-statements.
1361
   *
1362
   * @param string $query
1363
   *
1364
   * @return mixed
1365
   * @deprecated
1366
   * @throws \Exception
1367
   */
1368
  public static function qry($query)
1369
  {
1370
    $db = self::getInstance();
1371
1372
    $args = func_get_args();
1373
    /** @noinspection SuspiciousAssignmentsInspection */
1374
    $query = array_shift($args);
1375
    $query = str_replace('?', '%s', $query);
1376
    $args = array_map(
1377
        array(
1378
            $db,
1379
            'escape',
1380
        ),
1381
        $args
1382
    );
1383
    array_unshift($args, $query);
1384
    $query = call_user_func_array('sprintf', $args);
1385
    $result = $db->query($query);
1386
1387
    if ($result instanceof Result) {
1388
      return $result->fetchAllArray();
1389
    }
1390
1391
    return $result;
1392
  }
1393
1394
  /**
1395
   * Execute a sql-query.
1396
   *
1397
   * @param string        $sql            <p>The sql query-string.</p>
1398
   *
1399
   * @param array|boolean $params         <p>
1400
   *                                      "array" of sql-query-parameters<br/>
1401
   *                                      "false" if you don't need any parameter (default)<br/>
1402
   *                                      </p>
1403
   *
1404
   * @return bool|int|Result              <p>
1405
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1406
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1407
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1408
   *                                      "true" by e.g. "DROP"-queries<br />
1409
   *                                      "false" on error
1410
   *                                      </p>
1411
   *
1412
   * @throws QueryException
1413
   */
1414 85
  public function query($sql = '', $params = false)
1415
  {
1416 85
    if (!$this->isReady()) {
1417
      return false;
1418
    }
1419
1420 85 View Code Duplication
    if (!$sql || $sql === '') {
1421 4
      $this->_debug->displayError('Can not execute an empty query.', false);
1422
1423 4
      return false;
1424
    }
1425
1426
    if (
1427
        $params !== false
1428 83
        &&
1429 18
        is_array($params)
1430 83
        &&
1431 18
        count($params) > 0
1432 83
    ) {
1433 8
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1434 8
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1435 8
      $sql = $parseQueryParamsByName['sql'];
1436 8
    }
1437
1438
    // DEBUG
1439
    // var_dump($params);
1440
    // echo $sql . "\n";
1441
1442 83
    $query_start_time = microtime(true);
1443 83
    $query_result = \mysqli_real_query($this->link, $sql);
1444 83
    $query_duration = microtime(true) - $query_start_time;
1445
1446 83
    $this->query_count++;
1447
1448 83
    $mysqli_field_count = \mysqli_field_count($this->link);
1449 83
    if ($mysqli_field_count) {
1450 53
      $result = \mysqli_store_result($this->link);
1451 53
    } else {
1452 62
      $result = $query_result;
1453
    }
1454
1455 83
    if ($result instanceof \mysqli_result) {
1456
1457
      // log the select query
1458 52
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1459
1460
      // return query result object
1461 52
      return new Result($sql, $result);
1462
    }
1463
1464 64
    if ($query_result === true) {
1465
1466
      // "INSERT" || "REPLACE"
1467 61 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1468 56
        $insert_id = (int)$this->insert_id();
1469 56
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1470
1471 56
        return $insert_id;
1472
      }
1473
1474
      // "UPDATE" || "DELETE"
1475 38 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1476 12
        $affected_rows = (int)$this->affected_rows();
1477 12
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1478
1479 12
        return $affected_rows;
1480
      }
1481
1482
      // log the ? query
1483 27
      $this->_debug->logQuery($sql, $query_duration, 0);
1484
1485 27
      return true;
1486
    }
1487
1488
    // log the error query
1489 11
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1490
1491 11
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1492
  }
1493
1494
  /**
1495
   * Error-handling for the sql-query.
1496
   *
1497
   * @param string     $errorMessage
1498
   * @param int        $errorNumber
1499
   * @param string     $sql
1500
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1501
   * @param bool       $sqlMultiQuery
1502
   *
1503
   * @throws QueryException
1504
   * @throws DBGoneAwayException
1505
   *
1506
   * @return bool
1507
   */
1508 13
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1509
  {
1510 13
    $errorNumber = (int)$errorNumber;
1511
1512
    if (
1513
        $errorMessage === 'DB server has gone away'
1514 13
        ||
1515
        $errorMessage === 'MySQL server has gone away'
1516 12
        ||
1517
        $errorNumber === 2006
1518 13
    ) {
1519 1
      static $RECONNECT_COUNTER;
1520
1521
      // exit if we have more then 3 "DB server has gone away"-errors
1522 1
      if ($RECONNECT_COUNTER > 3) {
1523
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1524
        throw new DBGoneAwayException($errorMessage);
1525
      }
1526
1527 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1528
1529
      // reconnect
1530 1
      $RECONNECT_COUNTER++;
1531 1
      $this->reconnect(true);
1532
1533
      // re-run the current (non multi) query
1534 1
      if ($sqlMultiQuery === false) {
1535 1
        return $this->query($sql, $sqlParams);
1536
      }
1537
1538
      return false;
1539
    }
1540
1541 12
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1542
1543 12
    $force_exception_after_error = null; // auto
1544 12
    if ($this->_in_transaction === true) {
1545 4
      $force_exception_after_error = false;
1546 4
    }
1547
    // this query returned an error, we must display it (only for dev) !!!
1548
1549 12
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1550
1551 12
    return false;
1552
  }
1553
1554
  /**
1555
   * Quote && Escape e.g. a table name string.
1556
   *
1557
   * @param string $str
1558
   *
1559
   * @return string
1560
   */
1561 36
  public function quote_string($str)
1562
  {
1563 36
    $str = str_replace(
1564 36
        '`',
1565 36
        '``',
1566 36
        trim(
1567 36
            $this->escape($str, false),
1568
            '`'
1569 36
        )
1570 36
    );
1571
1572 36
    return '`' . $str . '`';
1573
  }
1574
1575
  /**
1576
   * Reconnect to the MySQL-Server.
1577
   *
1578
   * @param bool $checkViaPing
1579
   *
1580
   * @return bool
1581
   */
1582 3
  public function reconnect($checkViaPing = false)
1583
  {
1584 3
    $ping = false;
1585
1586 3
    if ($checkViaPing === true) {
1587 2
      $ping = $this->ping();
1588 2
    }
1589
1590 3
    if ($ping !== true) {
1591 3
      $this->connected = false;
1592 3
      $this->connect();
1593 3
    }
1594
1595 3
    return $this->isReady();
1596
  }
1597
1598
  /**
1599
   * Execute a "replace"-query.
1600
   *
1601
   * @param string      $table
1602
   * @param array       $data
1603
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1604
   *
1605
   * @return false|int <p>false on error</p>
1606
   *
1607
   * @throws QueryException
1608
   */
1609 1
  public function replace($table, array $data = array(), $databaseName = null)
1610
  {
1611
    // init
1612 1
    $table = trim($table);
1613
1614 1
    if ($table === '') {
1615 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1616
1617 1
      return false;
1618
    }
1619
1620 1
    if (count($data) === 0) {
1621 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1622
1623 1
      return false;
1624
    }
1625
1626
    // extracting column names
1627 1
    $columns = array_keys($data);
1628 1
    foreach ($columns as $k => $_key) {
1629
      /** @noinspection AlterInForeachInspection */
1630 1
      $columns[$k] = $this->quote_string($_key);
1631 1
    }
1632
1633 1
    $columns = implode(',', $columns);
1634
1635
    // extracting values
1636 1
    foreach ($data as $k => $_value) {
1637
      /** @noinspection AlterInForeachInspection */
1638 1
      $data[$k] = $this->secure($_value);
1639 1
    }
1640 1
    $values = implode(',', $data);
1641
1642 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...
1643
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1644
    }
1645
1646 1
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1647
1648 1
    return $this->query($sql);
1649
  }
1650
1651
  /**
1652
   * Rollback in a transaction and end the transaction.
1653
   *
1654
   * @return bool <p>Boolean true on success, false otherwise.</p>
1655
   */
1656 4 View Code Duplication
  public function rollback()
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...
1657
  {
1658 4
    if ($this->_in_transaction === false) {
1659
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1660
1661
      return false;
1662
    }
1663
1664 4
    $return = \mysqli_rollback($this->link);
1665 4
    \mysqli_autocommit($this->link, true);
1666 4
    $this->_in_transaction = false;
1667
1668 4
    return $return;
1669
  }
1670
1671
  /**
1672
   * Try to secure a variable, so can you use it in sql-queries.
1673
   *
1674
   * <p>
1675
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1676
   * 1. parse into "int"
1677
   * </p><br />
1678
   *
1679
   * <p>
1680
   * <strong>float:</strong><br />
1681
   * 1. return "float"
1682
   * </p><br />
1683
   *
1684
   * <p>
1685
   * <strong>string:</strong><br />
1686
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1687
   * 2. trim whitespace<br />
1688
   * 3. trim '<br />
1689
   * 4. escape the string (and remove non utf-8 chars)<br />
1690
   * 5. trim ' again (because we maybe removed some chars)<br />
1691
   * 6. add ' around the new string<br />
1692
   * </p><br />
1693
   *
1694
   * <p>
1695
   * <strong>array:</strong><br />
1696
   * 1. return null
1697
   * </p><br />
1698
   *
1699
   * <p>
1700
   * <strong>object:</strong><br />
1701
   * 1. return false
1702
   * </p><br />
1703
   *
1704
   * <p>
1705
   * <strong>null:</strong><br />
1706
   * 1. return null
1707
   * </p>
1708
   *
1709
   * @param mixed $var
1710
   *
1711
   * @return mixed
1712
   */
1713 39
  public function secure($var)
1714
  {
1715
    if (
1716
        $var === ''
1717 39
        ||
1718 39
        ($this->_convert_null_to_empty_string === true && $var === null)
1719 39
    ) {
1720 1
      return "''";
1721
    }
1722
1723 39
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1724 1
      return $var;
1725
    }
1726
1727 39
    if (is_string($var)) {
1728 34
      $var = trim(trim($var), "'");
1729 34
    }
1730
1731 39
    $var = $this->escape($var, false, false, null);
1732
1733 39
    if (is_string($var)) {
1734 34
      $var = "'" . trim($var, "'") . "'";
1735 34
    }
1736
1737 39
    return $var;
1738
  }
1739
1740
  /**
1741
   * Execute a "select"-query.
1742
   *
1743
   * @param string       $table
1744
   * @param string|array $where
1745
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1746
   *
1747
   * @return false|Result <p>false on error</p>
1748
   *
1749
   * @throws QueryException
1750
   */
1751 24 View Code Duplication
  public function select($table, $where = '1=1', $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...
1752
  {
1753
    // init
1754 24
    $table = trim($table);
1755
1756 24
    if ($table === '') {
1757 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1758
1759 1
      return false;
1760
    }
1761
1762 24
    if (is_string($where)) {
1763 8
      $WHERE = $this->escape($where, false);
1764 24
    } elseif (is_array($where)) {
1765 17
      $WHERE = $this->_parseArrayPair($where, 'AND');
1766 17
    } else {
1767 1
      $WHERE = '';
1768
    }
1769
1770 24
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null 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...
1771
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1772
    }
1773
1774 24
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1775
1776 24
    return $this->query($sql);
1777
  }
1778
1779
  /**
1780
   * Selects a different database than the one specified on construction.
1781
   *
1782
   * @param string $database <p>Database name to switch to.</p>
1783
   *
1784
   * @return bool <p>Boolean true on success, false otherwise.</p>
1785
   */
1786
  public function select_db($database)
1787
  {
1788
    if (!$this->isReady()) {
1789
      return false;
1790
    }
1791
1792
    return mysqli_select_db($this->link, $database);
1793
  }
1794
1795
  /**
1796
   * Set the current charset.
1797
   *
1798
   * @param string $charset
1799
   *
1800
   * @return bool
1801
   */
1802 8
  public function set_charset($charset)
1803
  {
1804 8
    $charsetLower = strtolower($charset);
1805 8
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1806 6
      $charset = 'utf8';
1807 6
    }
1808 8
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1809 6
      $charset = 'utf8mb4';
1810 6
    }
1811
1812 8
    $this->charset = (string)$charset;
1813
1814 8
    $return = mysqli_set_charset($this->link, $charset);
1815
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1816
    /** @noinspection UsageOfSilenceOperatorInspection */
1817 8
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1818
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1819
    /** @noinspection UsageOfSilenceOperatorInspection */
1820 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...
1821
1822 8
    return $return;
1823
  }
1824
1825
  /**
1826
   * Set the option to convert null to "''" (empty string).
1827
   *
1828
   * Used in secure() => select(), insert(), update(), delete()
1829
   *
1830
   * @param $bool
1831
   */
1832 1
  public function set_convert_null_to_empty_string($bool)
1833
  {
1834 1
    $this->_convert_null_to_empty_string = (bool)$bool;
1835 1
  }
1836
1837
  /**
1838
   * Enables or disables internal report functions
1839
   *
1840
   * @link http://php.net/manual/en/function.mysqli-report.php
1841
   *
1842
   * @param int $flags <p>
1843
   *                   <table>
1844
   *                   Supported flags
1845
   *                   <tr valign="top">
1846
   *                   <td>Name</td>
1847
   *                   <td>Description</td>
1848
   *                   </tr>
1849
   *                   <tr valign="top">
1850
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1851
   *                   <td>Turns reporting off</td>
1852
   *                   </tr>
1853
   *                   <tr valign="top">
1854
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1855
   *                   <td>Report errors from mysqli function calls</td>
1856
   *                   </tr>
1857
   *                   <tr valign="top">
1858
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1859
   *                   <td>
1860
   *                   Throw <b>mysqli_sql_exception</b> for errors
1861
   *                   instead of warnings
1862
   *                   </td>
1863
   *                   </tr>
1864
   *                   <tr valign="top">
1865
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1866
   *                   <td>Report if no index or bad index was used in a query</td>
1867
   *                   </tr>
1868
   *                   <tr valign="top">
1869
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1870
   *                   <td>Set all options (report all)</td>
1871
   *                   </tr>
1872
   *                   </table>
1873
   *                   </p>
1874
   *
1875
   * @return bool
1876
   */
1877
  public function set_mysqli_report($flags)
1878
  {
1879
    return \mysqli_report($flags);
1880
  }
1881
1882
  /**
1883
   * Show config errors by throw exceptions.
1884
   *
1885
   * @return bool
1886
   *
1887
   * @throws \InvalidArgumentException
1888
   */
1889 11
  public function showConfigError()
1890
  {
1891
1892
    if (
1893 11
        !$this->hostname
1894 11
        ||
1895 10
        !$this->username
1896 10
        ||
1897 9
        !$this->database
1898 11
    ) {
1899
1900 3
      if (!$this->hostname) {
1901 1
        throw new \InvalidArgumentException('no-sql-hostname');
1902
      }
1903
1904 2
      if (!$this->username) {
1905 1
        throw new \InvalidArgumentException('no-sql-username');
1906
      }
1907
1908 1
      if (!$this->database) {
1909 1
        throw new \InvalidArgumentException('no-sql-database');
1910
      }
1911
1912
      return false;
1913
    }
1914
1915 8
    return true;
1916
  }
1917
1918
  /**
1919
   * alias: "beginTransaction()"
1920
   */
1921 1
  public function startTransaction()
1922
  {
1923 1
    $this->beginTransaction();
1924 1
  }
1925
1926
  /**
1927
   * Execute a callback inside a transaction.
1928
   *
1929
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1930
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1931
   *
1932
   * @return bool <p>Boolean true on success, false otherwise.</p>
1933
   */
1934 1
  public function transact($callback)
1935
  {
1936
    try {
1937
1938 1
      $beginTransaction = $this->beginTransaction();
1939 1
      if ($beginTransaction === false) {
1940 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1941
1942 1
        return false;
1943
      }
1944
1945 1
      $result = call_user_func($callback, $this);
1946 1
      if ($result === false) {
1947
        /** @noinspection ThrowRawExceptionInspection */
1948 1
        throw new \Exception('call_user_func [' . $callback . '] === false');
1949
      }
1950
1951 1
      return $this->commit();
1952
1953 1
    } catch (\Exception $e) {
1954
1955 1
      $this->rollback();
1956
1957 1
      return false;
1958
    }
1959
  }
1960
1961
  /**
1962
   * Execute a "update"-query.
1963
   *
1964
   * @param string       $table
1965
   * @param array        $data
1966
   * @param array|string $where
1967
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1968
   *
1969
   * @return false|int <p>false on error</p>
1970
   *
1971
   * @throws QueryException
1972
   */
1973 7
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1974
  {
1975
    // init
1976 7
    $table = trim($table);
1977
1978 7
    if ($table === '') {
1979 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1980
1981 1
      return false;
1982
    }
1983
1984 7
    if (count($data) === 0) {
1985 2
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1986
1987 2
      return false;
1988
    }
1989
1990 7
    $SET = $this->_parseArrayPair($data);
1991
1992 7
    if (is_string($where)) {
1993 2
      $WHERE = $this->escape($where, false);
1994 7
    } elseif (is_array($where)) {
1995 5
      $WHERE = $this->_parseArrayPair($where, 'AND');
1996 5
    } else {
1997 1
      $WHERE = '';
1998
    }
1999
2000 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...
2001
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
2002
    }
2003
2004 7
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
2005
2006 7
    return $this->query($sql);
2007
  }
2008
2009
  /**
2010
   * Determine if database table exists
2011
   *
2012
   * @param string $table
2013
   *
2014
   * @return bool
2015
   */
2016 1
  public function table_exists($table)
2017
  {
2018 1
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2019
    if (
2020
        $check !== false
2021 1
        &&
2022
        $check instanceof Result
2023 1
        &&
2024 1
        $check->num_rows > 0
2025 1
    ) {
2026 1
      return true;
2027
    }
2028
2029 1
    return false;
2030
  }
2031
2032
  /**
2033
   * Count number of rows found matching a specific query.
2034
   *
2035
   * @param string
2036
   *
2037
   * @return int
2038
   */
2039 1
  public function num_rows($query)
2040
  {
2041 1
    $check = $this->query($query);
2042
2043
    if (
2044
        $check === false
2045 1
        ||
2046
        !$check instanceof Result
2047 1
    ) {
2048
      return 0;
2049
    }
2050
2051 1
    return $check->num_rows;
2052
  }
2053
}
2054