Completed
Push — master ( 4db935...7e95fd )
by Lars
07:53
created

DB::startTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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