Completed
Push — master ( d589d2...129dc0 )
by Lars
01:42
created

DB::_parseQueryParams()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 31
Code Lines 18

Duplication

Lines 7
Ratio 22.58 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 0
Metric Value
dl 7
loc 31
ccs 20
cts 20
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 18
nc 4
nop 2
crap 6
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
      $secureParamValue = $this->secure($params[$name]);
817 7
818 7
      while (strpos($sql, $parseKeyInner) !== false) {
819 7
        $sql = UTF8::str_replace_first(
820 7
            $parseKeyInner,
821
            $secureParamValue,
822 7
            $sql
823 7
        );
824
      }
825 7
826 7
      if ($sqlBefore !== $sql) {
827 7
        unset($params[$name]);
828 7
      }
829
    }
830 7
831
    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 51
   */
851
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
852 51
  {
853 2
    if ($var === '') {
854
      return '';
855
    }
856 51
857 3
    if ($var === null) {
858
      return null;
859
    }
860
861 51
    // save the current value as int (for later usage)
862 51
    if (!is_object($var)) {
863 51
      $varInt = (int)$var;
864
    }
865
866
    /** @noinspection TypeUnsafeComparisonInspection */
867 51
    if (
868
        is_int($var)
869 50
        ||
870 50
        is_bool($var)
871
        ||
872 50
        (
873 50
            isset($varInt, $var[0])
874 50
            &&
875 50
            $var[0] != '0'
876
            &&
877 50
            "$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
    ) {
880
881
      // "int" || int || bool
882 29
883
      return (int)$var;
884
    }
885 50
886
    if (is_float($var)) {
887
888
      // float
889 5
890
      return $var;
891
    }
892 50
893
    if (is_array($var)) {
894
895
      // array
896 4
897 3
      if ($convert_array === null) {
898
        return null;
899
      }
900 2
901 2
      $varCleaned = array();
902
      foreach ((array)$var as $key => $value) {
903 2
904 2
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
905
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
906
907 2
        /** @noinspection OffsetOperationsInspection */
908 2
        $varCleaned[$key] = $value;
909
      }
910 2
911 1
      if ($convert_array === true) {
912
        $varCleaned = implode(',', $varCleaned);
913 1
914
        return $varCleaned;
915
      }
916 2
917
      return (array)$varCleaned;
918
    }
919
920 50
    if (
921
        is_string($var)
922
        ||
923 3
        (
924 3
            is_object($var)
925 3
            &&
926 3
            method_exists($var, '__toString')
927 50
        )
928
    ) {
929
930
      // "string"
931 50
932
      $var = (string)$var;
933 50
934 11
      if ($stripe_non_utf8 === true) {
935 11
        $var = UTF8::cleanup($var);
936
      }
937 50
938
      if ($html_entity_decode === true) {
939 1
        // use no-html-entity for db
940 1
        $var = UTF8::html_entity_decode($var);
941
      }
942 50
943
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
944 50
945
      $var = \mysqli_real_escape_string($this->getLink(), $var);
946 50
947
      return (string)$var;
948
949
    }
950 3
951
    if ($var instanceof \DateTime) {
952
953
      // "DateTime"-object
954
955 3
      try {
956
        return $this->escape($var->format('Y-m-d H:i:s'), false);
957
      } catch (\Exception $e) {
958
        return null;
959
      }
960
961 2
    } else {
962
      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 3
   */
982
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
983
  {
984 3
    // init
985 3
    $cacheKey = null;
986 3
    if (!$db) {
987 3
      $db = self::getInstance();
988
    }
989 3
990 1 View Code Duplication
    if ($useCache === true) {
991 1
      $cache = new Cache(null, null, false, $useCache);
992
      $cacheKey = 'sql-' . md5($query);
993
994 1
      if (
995 1
          $cache->getCacheIsReady() === true
996 1
          &&
997 1
          $cache->existsItem($cacheKey)
998 1
      ) {
999
        return $cache->getItem($cacheKey);
1000
      }
1001 1
1002 3
    } else {
1003
      $cache = false;
1004
    }
1005 3
1006
    $result = $db->query($query);
1007 3
1008
    if ($result instanceof Result) {
1009 1
1010
      $return = $result->fetchAllArray();
1011
1012
      // save into the cache
1013 View Code Duplication
      if (
1014 1
          $cacheKey !== null
1015
          &&
1016 1
          $useCache === true
1017
          &&
1018 1
          $cache instanceof Cache
1019 1
          &&
1020 1
          $cache->getCacheIsReady() === true
1021 1
      ) {
1022 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
1023
      }
1024 1
1025 2
    } else {
1026
      $return = $result;
1027
    }
1028 3
1029
    return $return;
1030
  }
1031
1032
  /**
1033
   * Get all table-names via "SHOW TABLES".
1034
   *
1035
   * @return array
1036 1
   */
1037
  public function getAllTables()
1038 1
  {
1039 1
    $query = 'SHOW TABLES';
1040
    $result = $this->query($query);
1041 1
1042
    return $result->fetchAllArray();
1043
  }
1044
1045
  /**
1046
   * @return Debug
1047 9
   */
1048
  public function getDebugger()
1049 9
  {
1050
    return $this->_debug;
1051
  }
1052
1053
  /**
1054
   * Get errors from "$this->_errors".
1055
   *
1056
   * @return array
1057 1
   */
1058
  public function getErrors()
1059 1
  {
1060
    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 97
   */
1089
  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 97
     */
1094
    static $instance = array();
1095
1096
    /**
1097
     * @var $firstInstance DB
1098 97
     */
1099
    static $firstInstance = null;
1100
1101 97
    if (
1102 97
        $hostname . $username . $password . $database . $port . $charset == ''
1103 14
        &&
1104 97
        null !== $firstInstance
1105 14
    ) {
1106
      return $firstInstance;
1107
    }
1108 97
1109 97
    $extra_config_string = '';
1110 97
    if (is_array($extra_config) === true) {
1111
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1112 97
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1113 97
      }
1114
    } else {
1115
      // only for backward compatibility
1116
      $extra_config_string = (int)$extra_config;
1117
    }
1118 97
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
    );
1122 97
1123 11
    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
          $logger_level,
1135 11
          $extra_config
1136
      );
1137 5
1138 1
      if (null === $firstInstance) {
1139 1
        $firstInstance = $instance[$connection];
1140 5
      }
1141
    }
1142 97
1143
    return $instance[$connection];
1144
  }
1145
1146
  /**
1147
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1148
   *
1149
   * @return \mysqli
1150 54
   */
1151
  public function getLink()
1152 54
  {
1153
    return $this->link;
1154
  }
1155
1156
  /**
1157
   * Get the current charset.
1158
   *
1159
   * @return string
1160 1
   */
1161
  public function get_charset()
1162 1
  {
1163
    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 28
   */
1187
  public function insert($table, array $data = array(), $databaseName = null)
1188
  {
1189 28
    // init
1190
    $table = trim($table);
1191 28
1192 2
    if ($table === '') {
1193
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1194 2
1195
      return false;
1196
    }
1197 27
1198 3
    if (count($data) === 0) {
1199
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1200 3
1201
      return false;
1202
    }
1203 25
1204
    $SET = $this->_parseArrayPair($data);
1205 25
1206
    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 25
1210
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1211 25
1212
    return $this->query($sql);
1213
  }
1214
1215
  /**
1216
   * Returns the auto generated id used in the last query.
1217
   *
1218
   * @return int|string
1219 56
   */
1220
  public function insert_id()
1221 56
  {
1222
    return \mysqli_insert_id($this->link);
1223
  }
1224
1225
  /**
1226
   * Check if db-connection is ready.
1227
   *
1228
   * @return boolean
1229 95
   */
1230
  public function isReady()
1231 95
  {
1232
    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 1
   */
1240
  public function lastError()
1241 1
  {
1242
    $errors = $this->_debug->getErrors();
1243 1
1244
    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 1
   */
1259
  public function multi_query($sql)
1260 1
  {
1261
    if (!$this->isReady()) {
1262
      return false;
1263
    }
1264 1
1265 1 View Code Duplication
    if (!$sql || $sql === '') {
1266
      $this->_debug->displayError('Can not execute an empty query.', false);
1267 1
1268
      return false;
1269
    }
1270 1
1271 1
    $query_start_time = microtime(true);
1272 1
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1273
    $query_duration = microtime(true) - $query_start_time;
1274 1
1275
    $this->_debug->logQuery($sql, $query_duration, 0);
1276 1
1277 1
    $returnTheResult = false;
1278
    $result = array();
1279 1
1280
    if ($resultTmp) {
1281
      do {
1282 1
1283
        $resultTmpInner = \mysqli_store_result($this->link);
1284 1
1285
        if ($resultTmpInner instanceof \mysqli_result) {
1286 1
1287 1
          $returnTheResult = true;
1288
          $result[] = new Result($sql, $resultTmpInner);
1289 1
1290
        } else {
1291
1292 1
          // is the query successful
1293 1
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1294 1
            $result[] = true;
1295
          } else {
1296
            $result[] = false;
1297
          }
1298
1299
        }
1300 1
1301
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1302 1
1303
    } else {
1304
1305 1
      // log the error query
1306
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1307 1
1308
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1309
    }
1310
1311 1
    // return the result only if there was a "SELECT"-query
1312 1
    if ($returnTheResult === true) {
1313
      return $result;
1314
    }
1315
1316 1
    if (
1317 1
        count($result) > 0
1318 1
        &&
1319 1
        in_array(false, $result, true) === false
1320 1
    ) {
1321
      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 3
   */
1333
  public function ping()
1334
  {
1335 3
    if (
1336 3
        $this->link
1337 3
        &&
1338 3
        $this->link instanceof \mysqli
1339
    ) {
1340
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1341 3
      /** @noinspection UsageOfSilenceOperatorInspection */
1342
      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 2
   */
1355
  public function prepare($query)
1356 2
  {
1357
    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 85
   */
1415
  public function query($sql = '', $params = false)
1416 85
  {
1417
    if (!$this->isReady()) {
1418
      return false;
1419
    }
1420 85
1421 4 View Code Duplication
    if (!$sql || $sql === '') {
1422
      $this->_debug->displayError('Can not execute an empty query.', false);
1423 4
1424
      return false;
1425
    }
1426
1427
    if (
1428 83
        $params !== false
1429 18
        &&
1430 83
        is_array($params)
1431 18
        &&
1432 83
        count($params) > 0
1433 8
    ) {
1434 8
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1435 8
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1436 8
      $sql = $parseQueryParamsByName['sql'];
1437
    }
1438
1439
    // DEBUG
1440
    // var_dump($params);
1441
    // echo $sql . "\n";
1442 83
1443 83
    $query_start_time = microtime(true);
1444 83
    $query_result = \mysqli_real_query($this->link, $sql);
1445
    $query_duration = microtime(true) - $query_start_time;
1446 83
1447
    $this->query_count++;
1448 83
1449 83
    $mysqli_field_count = \mysqli_field_count($this->link);
1450 53
    if ($mysqli_field_count) {
1451 53
      $result = \mysqli_store_result($this->link);
1452 62
    } else {
1453
      $result = $query_result;
1454
    }
1455 83
1456
    if ($result instanceof \mysqli_result) {
1457
1458 52
      // log the select query
1459
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1460
1461 52
      // return query result object
1462
      return new Result($sql, $result);
1463
    }
1464 64
1465
    if ($query_result === true) {
1466
1467 61
      // "INSERT" || "REPLACE"
1468 56 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1469 56
        $insert_id = (int)$this->insert_id();
1470
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1471 56
1472
        return $insert_id;
1473
      }
1474
1475 38
      // "UPDATE" || "DELETE"
1476 12 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1477 12
        $affected_rows = (int)$this->affected_rows();
1478
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1479 12
1480
        return $affected_rows;
1481
      }
1482
1483 27
      // log the ? query
1484
      $this->_debug->logQuery($sql, $query_duration, 0);
1485 27
1486
      return true;
1487
    }
1488
1489 11
    // log the error query
1490
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1491 11
1492
    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 13
   */
1509
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1510 13
  {
1511
    $errorNumber = (int)$errorNumber;
1512
1513
    if (
1514 13
        $errorMessage === 'DB server has gone away'
1515
        ||
1516 12
        $errorMessage === 'MySQL server has gone away'
1517
        ||
1518 13
        $errorNumber === 2006
1519 1
    ) {
1520
      static $RECONNECT_COUNTER;
1521
1522 1
      // exit if we have more then 3 "DB server has gone away"-errors
1523
      if ($RECONNECT_COUNTER > 3) {
1524
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1525
        throw new DBGoneAwayException($errorMessage);
1526
      }
1527 1
1528
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1529
1530 1
      // reconnect
1531 1
      $RECONNECT_COUNTER++;
1532
      $this->reconnect(true);
1533
1534 1
      // re-run the current (non multi) query
1535 1
      if ($sqlMultiQuery === false) {
1536
        return $this->query($sql, $sqlParams);
1537
      }
1538
1539
      return false;
1540
    }
1541 12
1542
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1543 12
1544 12
    $force_exception_after_error = null; // auto
1545 4
    if ($this->_in_transaction === true) {
1546 4
      $force_exception_after_error = false;
1547
    }
1548
    // this query returned an error, we must display it (only for dev) !!!
1549 12
1550
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1551 12
1552
    return false;
1553
  }
1554
1555
  /**
1556
   * Quote && Escape e.g. a table name string.
1557
   *
1558
   * @param string $str
1559
   *
1560
   * @return string
1561 36
   */
1562
  public function quote_string($str)
1563 36
  {
1564 36
    $str = str_replace(
1565 36
        '`',
1566 36
        '``',
1567 36
        trim(
1568
            $this->escape($str, false),
1569 36
            '`'
1570 36
        )
1571
    );
1572 36
1573
    return '`' . $str . '`';
1574
  }
1575
1576
  /**
1577
   * Reconnect to the MySQL-Server.
1578
   *
1579
   * @param bool $checkViaPing
1580
   *
1581
   * @return bool
1582 3
   */
1583
  public function reconnect($checkViaPing = false)
1584 3
  {
1585
    $ping = false;
1586 3
1587 2
    if ($checkViaPing === true) {
1588 2
      $ping = $this->ping();
1589
    }
1590 3
1591 3
    if ($ping !== true) {
1592 3
      $this->connected = false;
1593 3
      $this->connect();
1594
    }
1595 3
1596
    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 1
   */
1610
  public function replace($table, array $data = array(), $databaseName = null)
1611
  {
1612 1
    // init
1613
    $table = trim($table);
1614 1
1615 1
    if ($table === '') {
1616
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1617 1
1618
      return false;
1619
    }
1620 1
1621 1
    if (count($data) === 0) {
1622
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1623 1
1624
      return false;
1625
    }
1626
1627 1
    // extracting column names
1628 1
    $columns = array_keys($data);
1629
    foreach ($columns as $k => $_key) {
1630 1
      /** @noinspection AlterInForeachInspection */
1631 1
      $columns[$k] = $this->quote_string($_key);
1632
    }
1633 1
1634
    $columns = implode(',', $columns);
1635
1636 1
    // extracting values
1637
    foreach ($data as $k => $_value) {
1638 1
      /** @noinspection AlterInForeachInspection */
1639 1
      $data[$k] = $this->secure($_value);
1640 1
    }
1641
    $values = implode(',', $data);
1642 1
1643
    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 1
1647
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1648 1
1649
    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 4
   */
1657 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 4
  {
1659
    if ($this->_in_transaction === false) {
1660
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1661
1662
      return false;
1663
    }
1664 4
1665 4
    $return = \mysqli_rollback($this->link);
1666 4
    \mysqli_autocommit($this->link, true);
1667
    $this->_in_transaction = false;
1668 4
1669
    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 39
   */
1714
  public function secure($var)
1715
  {
1716
    if (
1717 39
        $var === ''
1718 39
        ||
1719 39
        (
1720 1
            $this->_convert_null_to_empty_string === true
1721
            &&
1722
            $var === null
1723 39
        )
1724 1
    ) {
1725
      return "''";
1726
    }
1727 39
1728 34
    if (
1729 34
        $this->_convert_null_to_empty_string === FALSE
1730
        &&
1731 39
        $var === null
1732
    ) {
1733 39
      return "NULL";
1734 34
    }
1735 34
1736
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1737 39
      return $var;
1738
    }
1739
1740
    if (is_string($var)) {
1741
      $var = trim(trim($var), "'");
1742
    }
1743
1744
    $var = $this->escape($var, false, false, null);
1745
1746
    if (is_string($var)) {
1747
      $var = "'" . trim($var, "'") . "'";
1748
    }
1749
1750
    return $var;
1751 24
  }
1752
1753
  /**
1754 24
   * Execute a "select"-query.
1755
   *
1756 24
   * @param string       $table
1757 1
   * @param string|array $where
1758
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1759 1
   *
1760
   * @return false|Result <p>false on error</p>
1761
   *
1762 24
   * @throws QueryException
1763 8
   */
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 17
  {
1766 17
    // init
1767 1
    $table = trim($table);
1768
1769
    if ($table === '') {
1770 24
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1771
1772
      return false;
1773
    }
1774 24
1775
    if (is_string($where)) {
1776 24
      $WHERE = $this->escape($where, false);
1777
    } elseif (is_array($where)) {
1778
      $WHERE = $this->_parseArrayPair($where, 'AND');
1779
    } else {
1780
      $WHERE = '';
1781
    }
1782
1783
    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
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1788
1789
    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 8
      return false;
1803
    }
1804 8
1805 8
    return mysqli_select_db($this->link, $database);
1806 6
  }
1807 6
1808 8
  /**
1809 6
   * Set the current charset.
1810 6
   *
1811
   * @param string $charset
1812 8
   *
1813
   * @return bool
1814 8
   */
1815
  public function set_charset($charset)
1816
  {
1817 8
    $charsetLower = strtolower($charset);
1818
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1819
      $charset = 'utf8';
1820 8
    }
1821
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1822 8
      $charset = 'utf8mb4';
1823
    }
1824
1825
    $this->charset = (string)$charset;
1826
1827
    $return = mysqli_set_charset($this->link, $charset);
1828
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1829
    /** @noinspection UsageOfSilenceOperatorInspection */
1830
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1831
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1832 1
    /** @noinspection UsageOfSilenceOperatorInspection */
1833
    @\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 1
1835 1
    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
  public function set_convert_null_to_empty_string($bool)
1846
  {
1847
    $this->_convert_null_to_empty_string = (bool)$bool;
1848
  }
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 11
   */
1890
  public function set_mysqli_report($flags)
1891
  {
1892
    return \mysqli_report($flags);
1893 11
  }
1894 11
1895 10
  /**
1896 10
   * Show config errors by throw exceptions.
1897 9
   *
1898 11
   * @return bool
1899
   *
1900 3
   * @throws \InvalidArgumentException
1901 1
   */
1902
  public function showConfigError()
1903
  {
1904 2
1905 1
    if (
1906
        !$this->hostname
1907
        ||
1908 1
        !$this->username
1909 1
        ||
1910
        !$this->database
1911
    ) {
1912
1913
      if (!$this->hostname) {
1914
        throw new \InvalidArgumentException('no-sql-hostname');
1915 8
      }
1916
1917
      if (!$this->username) {
1918
        throw new \InvalidArgumentException('no-sql-username');
1919
      }
1920
1921 1
      if (!$this->database) {
1922
        throw new \InvalidArgumentException('no-sql-database');
1923 1
      }
1924 1
1925
      return false;
1926
    }
1927
1928
    return true;
1929
  }
1930
1931
  /**
1932
   * alias: "beginTransaction()"
1933
   */
1934 1
  public function startTransaction()
1935
  {
1936
    $this->beginTransaction();
1937
  }
1938 1
1939 1
  /**
1940 1
   * Execute a callback inside a transaction.
1941
   *
1942 1
   * @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 1
   * @return bool <p>Boolean true on success, false otherwise.</p>
1946 1
   */
1947
  public function transact($callback)
1948 1
  {
1949
    try {
1950
1951 1
      $beginTransaction = $this->beginTransaction();
1952
      if ($beginTransaction === false) {
1953 1
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1954
1955 1
        return false;
1956
      }
1957 1
1958
      $result = call_user_func($callback, $this);
1959
      if ($result === false) {
1960
        /** @noinspection ThrowRawExceptionInspection */
1961
        throw new \Exception('call_user_func [' . $callback . '] === false');
1962
      }
1963
1964
      return $this->commit();
1965
1966
    } catch (\Exception $e) {
1967
1968
      $this->rollback();
1969
1970
      return false;
1971
    }
1972
  }
1973 7
1974
  /**
1975
   * Execute a "update"-query.
1976 7
   *
1977
   * @param string       $table
1978 7
   * @param array        $data
1979 1
   * @param array|string $where
1980
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1981 1
   *
1982
   * @return false|int <p>false on error</p>
1983
   *
1984 7
   * @throws QueryException
1985 2
   */
1986
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1987 2
  {
1988
    // init
1989
    $table = trim($table);
1990 7
1991
    if ($table === '') {
1992 7
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1993 2
1994 7
      return false;
1995 5
    }
1996 5
1997 1
    if (count($data) === 0) {
1998
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1999
2000 7
      return false;
2001
    }
2002
2003
    $SET = $this->_parseArrayPair($data);
2004 7
2005
    if (is_string($where)) {
2006 7
      $WHERE = $this->escape($where, false);
2007
    } elseif (is_array($where)) {
2008
      $WHERE = $this->_parseArrayPair($where, 'AND');
2009
    } else {
2010
      $WHERE = '';
2011
    }
2012
2013
    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 1
2017
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
2018 1
2019
    return $this->query($sql);
2020
  }
2021 1
2022
  /**
2023 1
   * Determine if database table exists
2024 1
   *
2025 1
   * @param string $table
2026 1
   *
2027
   * @return bool
2028
   */
2029 1
  public function table_exists($table)
2030
  {
2031
    $check = $this->query('SELECT 1 FROM ' . $this->quote_string($table));
2032
    if (
2033
        $check !== false
2034
        &&
2035
        $check instanceof Result
2036
        &&
2037
        $check->num_rows > 0
2038
    ) {
2039 1
      return true;
2040
    }
2041 1
2042
    return false;
2043
  }
2044
2045 1
  /**
2046
   * Count number of rows found matching a specific query.
2047 1
   *
2048
   * @param string
2049
   *
2050
   * @return int
2051 1
   */
2052
  public function num_rows($query)
2053
  {
2054
    $check = $this->query($query);
2055
2056
    if (
2057
        $check === false
2058
        ||
2059
        !$check instanceof Result
2060
    ) {
2061
      return 0;
2062
    }
2063
2064
    return $check->num_rows;
2065
  }
2066
}
2067