Completed
Push — master ( 7b97ec...6606fb )
by Lars
01:50
created

DB::getDebugger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
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 handles 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 = false;
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 10
   * The path name to the certificate authority file
110
   *
111 10
   * @var string
112
   */
113 10
  private $_cacert;
114
115 10
  /**
116 10
   * @var Debug
117 10
   */
118 10
  private $_debug;
119 10
120 10
  /**
121 10
   * __construct()
122 10
   *
123 10
   * @param string         $hostname
124 10
   * @param string         $username
125 10
   * @param string         $password
126
   * @param string         $database
127 10
   * @param int            $port
128
   * @param string         $charset
129 7
   * @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 4
   * @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 4
   * @param string         $logger_class_name
134
   * @param string         $logger_level  <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
135 4
   * @param array          $extra_config  <p>
136
   *                                      'session_to_db' => false|true<br>
137 4
   *                                      'socket' => 'string (path)'<br>
138
   *                                      'ssl' => 'bool'<br>
139 4
   *                                      'clientkey' => 'string (path)'<br>
140
   *                                      'clientcert' => 'string (path)'<br>
141 4
   *                                      'cacert' => 'string (path)'<br>
142
   *                                      </p>
143 4
   */
144
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config = array())
145 4
  {
146
    $this->connected = false;
147 4
148
    $this->_debug = new Debug($this);
149 4
150
    $this->_loadConfig(
151 4
        $hostname,
152
        $username,
153 4
        $password,
154
        $database,
155 4
        $port,
156
        $charset,
157 4
        $exit_on_error,
158
        $echo_on_error,
159 4
        $logger_class_name,
160
        $logger_level,
161
        $extra_config
162
    );
163
164
    $this->connect();
165
166
    $this->mysqlDefaultTimeFunctions = array(
167
      // Returns the current date.
168
      'CURDATE()',
169
      // CURRENT_DATE	| Synonyms for CURDATE()
170
      'CURRENT_DATE()',
171
      // CURRENT_TIME	| Synonyms for CURTIME()
172
      'CURRENT_TIME()',
173
      // CURRENT_TIMESTAMP | Synonyms for NOW()
174
      'CURRENT_TIMESTAMP()',
175
      // Returns the current time.
176
      'CURTIME()',
177
      // Synonym for NOW()
178 10
      'LOCALTIME()',
179
      // Synonym for NOW()
180 10
      'LOCALTIMESTAMP()',
181 10
      // Returns the current date and time.
182 10
      'NOW()',
183 10
      // Returns the time at which the function executes.
184
      'SYSDATE()',
185 10
      // Returns a UNIX timestamp.
186 4
      'UNIX_TIMESTAMP()',
187 4
      // Returns the current UTC date.
188
      'UTC_DATE()',
189 10
      // Returns the current UTC time.
190 4
      'UTC_TIME()',
191 4
      // Returns the current UTC date and time.
192
      'UTC_TIMESTAMP()',
193
    );
194 7
  }
195
196
  /**
197
   * Prevent the instance from being cloned.
198 10
   *
199
   * @return void
200
   */
201
  private function __clone()
202 10
  {
203
  }
204 10
205 10
  /**
206
   * __destruct
207 10
   *
208 10
   */
209 10
  public function __destruct()
210
  {
211 10
    // close the connection only if we don't save PHP-SESSION's in DB
212 10
    if ($this->session_to_db === false) {
213 10
      $this->close();
214
    }
215 10
  }
216 10
217
  /**
218 10
   * @param null|string $sql
219
   * @param array       $bindings
220 10
   *
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 10
  public function __invoke($sql = null, array $bindings = array())
231
  {
232
    return isset($sql) ? $this->query($sql, $bindings) : $this;
233
  }
234 10
235 10
  /**
236 9
   * __wakeup
237 9
   *
238 8
   * @return void
239 10
   */
240
  public function __wakeup()
241 3
  {
242 1
    $this->reconnect();
243
  }
244
245 2
  /**
246 1
   * Load the config from the constructor.
247
   *
248
   * @param string         $hostname
249 1
   * @param string         $username
250 1
   * @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 7
   * @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 9
   *                                      'cacert' => 'string (path)'<br>
267
   *                                      </p>
268 9
   *
269 1
   * @return bool
270
   */
271
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
272 9
  {
273
    $this->hostname = (string)$hostname;
274 9
    $this->username = (string)$username;
275
    $this->password = (string)$password;
276 9
    $this->database = (string)$database;
277 9
278 9
    if ($charset) {
279
      $this->charset = (string)$charset;
280
    }
281 9
282 9
    if ($port) {
283 9
      $this->port = (int)$port;
284 9
    } else {
285 9
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
286 9
      /** @noinspection UsageOfSilenceOperatorInspection */
287 9
      $this->port = (int)@ini_get('mysqli.default_port');
288 9
    }
289 9
290 9
    // fallback
291 3
    if (!$this->port) {
292 3
      $this->port = 3306;
293 3
    }
294
295 6
    if (!$this->socket) {
296
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
297 6
      $this->socket = @ini_get('mysqli.default_socket');
298
    }
299
300
    if ($exit_on_error === true || $exit_on_error === false) {
301
      $this->_debug->setExitOnError($exit_on_error);
302
    }
303
304 6
    if ($echo_on_error === true || $echo_on_error === false) {
305
      $this->_debug->setEchoOnError($echo_on_error);
306 6
    }
307
308
    $this->_debug->setLoggerClassName($logger_class_name);
309
    $this->_debug->setLoggerLevel($logger_level);
310
311
    if (is_array($extra_config) === true) {
312
313
      if (isset($extra_config['session_to_db'])) {
314 45
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
315
      }
316 45
317
      if (isset($extra_config['socket'])) {
318
        $this->socket = $extra_config['socket'];
319
      }
320
321
      if (isset($extra_config['ssl'])) {
322
        $this->_ssl = $extra_config['ssl'];
323
      }
324
325
      if (isset($extra_config['clientkey'])) {
326 2
        $this->_clientkey = $extra_config['clientkey'];
327
      }
328 2
329
      if (isset($extra_config['clientcert'])) {
330
        $this->_clientcert = $extra_config['clientcert'];
331
      }
332
333
      if (isset($extra_config['cacert'])) {
334
        $this->_cacert = $extra_config['cacert'];
335
      }
336
337
    } else {
338
      // only for backward compatibility
339
      $this->session_to_db = (boolean)$extra_config;
340
    }
341
342
    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
  public function _parseArrayPair($arrayPair, $glue = ',')
356
  {
357
    // init
358
    $sql = '';
359
360
    /** @noinspection IsEmptyFunctionUsageInspection */
361
    if (empty($arrayPair)) {
362
      return '';
363
    }
364
365
    $arrayPairCounter = 0;
366
    foreach ($arrayPair as $_key => $_value) {
367
      $_connector = '=';
368
      $_glueHelper = '';
369
      $_key_upper = strtoupper($_key);
370
371
      if (strpos($_key_upper, ' NOT') !== false) {
372
        $_connector = 'NOT';
373
      }
374
375
      if (strpos($_key_upper, ' IS') !== false) {
376
        $_connector = 'IS';
377
      }
378
379
      if (strpos($_key_upper, ' IS NOT') !== false) {
380
        $_connector = 'IS NOT';
381
      }
382
383 57
      if (strpos($_key_upper, ' IN') !== false) {
384
        $_connector = 'IN';
385
      }
386
387
      if (strpos($_key_upper, ' NOT IN') !== false) {
388 57
        $_connector = 'NOT IN';
389
      }
390
391
      if (strpos($_key_upper, ' BETWEEN') !== false) {
392
        $_connector = 'BETWEEN';
393 57
      }
394
395
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
396 57
        $_connector = 'NOT BETWEEN';
397 57
      }
398 11
399 57
      if (strpos($_key_upper, ' LIKE') !== false) {
400 11
        $_connector = 'LIKE';
401
      }
402
403 57
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
404 57
        $_connector = 'NOT LIKE';
405 57
      }
406
407 57 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
408 10
        $_connector = '>';
409 10
      }
410 10
411 10 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
412 10
        $_connector = '<';
413 10
      }
414 10
415 10
      if (strpos($_key_upper, ' >=') !== false) {
416 10
        $_connector = '>=';
417 10
      }
418 10
419
      if (strpos($_key_upper, ' <=') !== false) {
420 10
        $_connector = '<=';
421
      }
422 4
423 1
      if (strpos($_key_upper, ' <>') !== false) {
424 1
        $_connector = '<>';
425 4
      }
426
427 57
      if (strpos($_key_upper, ' OR') !== false) {
428
        $_glueHelper = 'OR';
429
      }
430
431
      if (strpos($_key_upper, ' AND') !== false) {
432
        $_glueHelper = 'AND';
433
      }
434
435
      if (is_array($_value) === true) {
436
        foreach ($_value as $oldKey => $oldValue) {
437
          $_value[$oldKey] = $this->secure($oldValue);
438
        }
439
440
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
441
          $_value = '(' . implode(',', $_value) . ')';
442
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
443
          $_value = '(' . implode(' AND ', $_value) . ')';
444
        }
445
446
      } else {
447
        $_value = $this->secure($_value);
448
      }
449
450 35
      $quoteString = $this->quote_string(
451
          trim(
452 35
              str_ireplace(
453
                  array(
454
                      $_connector,
455
                      $_glueHelper,
456 35
                  ),
457 4
                  '',
458
                  $_key
459 4
              )
460
          )
461
      );
462
463
      if (!is_array($_value)) {
464 33
        $_value = array($_value);
465 3
      }
466 33
467 3
      if (!$_glueHelper) {
468 33
        $_glueHelper = $glue;
469 3
      }
470 3
471
      $tmpCounter = 0;
472 33
      foreach ($_value as $valueInner) {
473 33
474 33
        $_glueHelperInner = $_glueHelper;
475
476 33
        if ($arrayPairCounter === 0) {
477
478 33
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
479 33
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
480 28
          } elseif ($tmpCounter === 0) {
481 28
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
482 24
          }
483
484
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
485 33
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
486
        }
487
488 27
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
489
        $tmpCounter++;
490
      }
491 27
492
      if ($_glueHelper === 'OR') {
493
        $sql .= ' ) ';
494
      }
495 25
496
      $arrayPairCounter++;
497
    }
498 23
499 22
    return $sql;
500 22
  }
501
502 22
  /**
503
   * _parseQueryParams
504
   *
505
   * @param string $sql
506 8
   * @param array  $params
507 8
   *
508 8
   * @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
        strpos($sql, '?') === false
515
        ||
516
        count($params) === 0
517
    ) {
518
      return array('sql' => $sql, 'params' => $params);
519
    }
520 8
521
    $parseKey = md5(uniqid((string)mt_rand(), true));
522 8
    $sql = str_replace('?', $parseKey, $sql);
523
524
    $k = 0;
525
    while (strpos($sql, $parseKey) !== false) {
526
      $sql = UTF8::str_replace_first(
527
          $parseKey,
528
          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
      );
531
532
      if (isset($params[$k])) {
533 3
        unset($params[$k]);
534
      }
535
536 3
      $k++;
537
    }
538
539
    return array('sql' => $sql, 'params' => $params);
540 3
  }
541 3
542 3
  /**
543
   * Gets the number of affected rows in a previous MySQL operation.
544 3
   *
545 3
   * @return int
546 3
   */
547 3
  public function affected_rows()
548 3
  {
549 3
    return \mysqli_affected_rows($this->link);
550 3
  }
551
552 3
  /**
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 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
    if ($this->_in_transaction === true) {
560
      $this->_debug->displayError('Error: mysql server already in transaction!', false);
561
562
      return false;
563
    }
564
565
    $this->clearErrors(); // needed for "$this->endTransaction()"
566
    $this->_in_transaction = true;
567
    $return = \mysqli_autocommit($this->link, false);
568
    if ($return === false) {
569
      $this->_in_transaction = false;
570
    }
571
572
    return $return;
573
  }
574
575
  /**
576
   * Clear the errors in "_debug->_errors".
577
   *
578
   * @return bool
579
   */
580
  public function clearErrors()
581
  {
582
    return $this->_debug->clearErrors();
583
  }
584
585
  /**
586
   * Closes a previously opened database connection.
587
   */
588
  public function close()
589
  {
590
    $this->connected = false;
591
592
    if ($this->link) {
593
      \mysqli_close($this->link);
594
    }
595
  }
596
597 26
  /**
598
   * Commits the current transaction and end the transaction.
599
   *
600
   * @return bool <p>Boolean true on success, false otherwise.</p>
601 26
   */
602 26 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 26
  {
604 1
    if ($this->_in_transaction === false) {
605
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
606
607 26
      return false;
608 1
    }
609
610
    $return = mysqli_commit($this->link);
611 26
    \mysqli_autocommit($this->link, true);
612 22
    $this->_in_transaction = false;
613 22
614
    return $return;
615 26
  }
616
617 26
  /**
618 22
   * Open a new connection to the MySQL server.
619 22
   *
620
   * @return bool
621 26
   *
622
   * @throws DBConnectException
623
   */
624
  public function connect()
625
  {
626
    if ($this->isReady()) {
627
      return true;
628
    }
629
630
    $flags = null;
631
632
    \mysqli_report(MYSQLI_REPORT_STRICT);
633
    try {
634
      $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
      if (Helper::isMysqlndIsUsed() === true) {
637
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
638
      }
639
640
      if ($this->_ssl === true) {
641 33
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 33
          throw new DBConnectException('Error connecting to mysql server: clientcert not defined');
644 2
        }
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 33
          throw new DBConnectException('Error connecting to mysql server: clientkey not defined');
648 2
        }
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 33
        }
653 33
654 33
        \mysqli_options($this->link, MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true);
655
656
        /** @noinspection PhpParamsInspection */
657
        \mysqli_ssl_set(
658 33
            $this->link,
659
            $this->_clientkey,
660 33
            $this->_clientcert,
661 33
            $this->_cacert,
662
            null,
663 33
            null
664 33
        );
665 33
666 33
        $flags = MYSQLI_CLIENT_SSL;
667
      }
668 33
669 33
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
670
      $this->connected = @\mysqli_real_connect(
671
          $this->link,
672
          $this->hostname,
673 24
          $this->username,
674
          $this->password,
675
          $this->database,
676 33
          $this->port,
677
          $this->socket,
678
          $flags
679
      );
680 5
681
    } catch (\Exception $e) {
682
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
683 33
      $this->_debug->displayError($error, true);
684
      throw new DBConnectException($error, 100, $e);
685
    }
686
    \mysqli_report(MYSQLI_REPORT_OFF);
687 3
688 3
    $errno = mysqli_connect_errno();
689
    if (!$this->connected || $errno) {
690
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
691 1
      $this->_debug->displayError($error, true);
692 1
      throw new DBConnectException($error, 101);
693
    }
694 1
695 1
    $this->set_charset($this->charset);
696
697
    return $this->isReady();
698 1
  }
699 1
700
  /**
701 1
   * Execute a "delete"-query.
702 1
   *
703
   * @param string       $table
704 1
   * @param string|array $where
705
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
706
   *
707 1
   * @return false|int <p>false on error</p>
708
   *
709
   *    * @throws QueryException
710
   */
711 33 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 3
    $table = trim($table);
715 3
716 3
    if ($table === '') {
717 3
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
718 33
719
      return false;
720
    }
721
722 33
    if (is_string($where)) {
723
      $WHERE = $this->escape($where, false);
724 33
    } elseif (is_array($where)) {
725 9
      $WHERE = $this->_parseArrayPair($where, 'AND');
726 9
    } else {
727
      $WHERE = '';
728 33
    }
729
730 1
    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 1
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
732
    }
733 33
734
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
735 33
736
    return $this->query($sql);
737 33
  }
738
739
  /**
740
   * Ends a transaction and commits if no errors, then ends autocommit.
741 3
   *
742
   * @return bool <p>This will return true or false indicating success of transactions.</p>
743
   */
744 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 3
    if ($this->_in_transaction === false) {
747
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
748
749
      return false;
750
    }
751
752 2
    if (!$this->errors()) {
753
      $return = \mysqli_commit($this->link);
754
    } else {
755
      $this->rollback();
756
      $return = false;
757
    }
758
759
    \mysqli_autocommit($this->link, true);
760
    $this->_in_transaction = false;
761 35
762
    return $return;
763 35
  }
764
765
  /**
766
   * Get all errors from "$this->_errors".
767
   *
768
   * @return array|false <p>false === on errors</p>
769
   */
770
  public function errors()
771 22
  {
772
    $errors = $this->_debug->getErrors();
773 22
774
    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 8
   * @param array $params <p>An array of key-value bindings.</p>
782
   *
783 8
   * @return array <p>with the keys -> 'sql', 'params'</p>
784
   */
785
  public function _parseQueryParamsByName($sql, array $params = array())
786
  {
787
    // is there anything to parse?
788 View Code Duplication
    if (
789
        strpos($sql, ':') === false
790
        ||
791
        count($params) === 0
792
    ) {
793
      return array('sql' => $sql, 'params' => $params);
794
    }
795
796
    $parseKey = md5(uniqid((string)mt_rand(), true));
797
798 9
    foreach ($params as $name => $value) {
799
      $parseKeyInner = $name . '-' . $parseKey;
800 9
      $sql = str_replace(':' . $name, $parseKeyInner, $sql);
801 1
    }
802
803
    foreach ($params as $name => $value) {
804 1
      $parseKeyInner = $name . '-' . $parseKey;
805
      $sqlBefore = $sql;
806
807
      while (strpos($sql, $parseKeyInner) !== false) {
808
        $sql = UTF8::str_replace_first(
809 1
            $parseKeyInner,
810
            $this->secure($params[$name]),
811
            $sql
812 1
        );
813 1
      }
814
815
      if ($sqlBefore !== $sql) {
816 1
        unset($params[$name]);
817
      }
818
    }
819 8
820
    return array('sql' => $sql, 'params' => $params);
821
  }
822 8
823
  /**
824 8
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
825
   *
826
   * @param mixed     $var           boolean: convert into "integer"<br />
827
   *                                 int: int (don't change it)<br />
828
   *                                 float: float (don't change it)<br />
829
   *                                 null: null (don't change it)<br />
830
   *                                 array: run escape() for every key => value<br />
831
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
832
   * @param bool      $stripe_non_utf8
833
   * @param bool      $html_entity_decode
834 3
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
835
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
836 3
   *                                 <strong>null</strong> => Convert the array into null, every time.
837
   *
838 3
   * @return mixed
839 2
   */
840 2
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
841
  {
842 3
    if ($var === '') {
843 3
      return '';
844 3
    }
845 3
846
    if ($var === null) {
847 3
      return null;
848
    }
849
850
    // save the current value as int (for later usage)
851
    if (!is_object($var)) {
852
      $varInt = (int)$var;
853
    }
854
855
    /** @noinspection TypeUnsafeComparisonInspection */
856 3
    if (
857
        is_int($var)
858
        ||
859 3
        is_bool($var)
860 3
        ||
861 3
        (
862 3
            isset($varInt, $var[0])
863
            &&
864
            $var[0] != '0'
865 3
            &&
866
            "$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...
867
        )
868
    ) {
869
870
      // "int" || int || bool
871
872
      return (int)$var;
873
    }
874
875
    if (is_float($var)) {
876
877
      // float
878
879
      return $var;
880
    }
881
882
    if (is_array($var)) {
883
884
      // array
885
886 3
      if ($convert_array === null) {
887
        return null;
888
      }
889 3
890 3
      $varCleaned = array();
891
      foreach ((array)$var as $key => $value) {
892 3
893 1
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
894 1
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
895
896
        /** @noinspection OffsetOperationsInspection */
897 1
        $varCleaned[$key] = $value;
898 1
      }
899 1
900 1
      if ($convert_array === true) {
901 1
        $varCleaned = implode(',', $varCleaned);
902
903
        return $varCleaned;
904 1
      }
905 3
906
      return (array)$varCleaned;
907
    }
908 3
909
    if (
910 3
        is_string($var)
911
        ||
912 1
        (
913
            is_object($var)
914
            &&
915
            method_exists($var, '__toString')
916
        )
917 1
    ) {
918
919 1
      // "string"
920
921 1
      $var = (string)$var;
922 1
923 1
      if ($stripe_non_utf8 === true) {
924 1
        $var = UTF8::cleanup($var);
925 1
      }
926
927 1
      if ($html_entity_decode === true) {
928 2
        // use no-html-entity for db
929
        $var = UTF8::html_entity_decode($var);
930
      }
931 3
932
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
933
934
      $var = \mysqli_real_escape_string($this->getLink(), $var);
935
936
      return (string)$var;
937
938
    }
939 1
940
    if ($var instanceof \DateTime) {
941 1
942
      // "DateTime"-object
943
944
      try {
945
        return $this->escape($var->format('Y-m-d H:i:s'), false);
946
      } catch (\Exception $e) {
947
        return null;
948
      }
949
950
    } else {
951
      return false;
952
    }
953
  }
954
955
  /**
956
   * Execute select/insert/update/delete sql-queries.
957
   *
958
   * @param string $query    <p>sql-query</p>
959
   * @param bool   $useCache <p>use cache?</p>
960
   * @param int    $cacheTTL <p>cache-ttl in seconds</p>
961
   * @param DB     $db       optional <p>the database connection</p>
962
   *
963
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
964
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
965
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
966
   *               "true" by e.g. "DROP"-queries<br />
967
   *               "false" on error
968
   *
969
   * @throws QueryException
970
   */
971
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600, DB $db = null)
972
  {
973
    // init
974
    $cacheKey = null;
975
    if (!$db) {
976
      $db = self::getInstance();
977
    }
978
979 View Code Duplication
    if ($useCache === true) {
980
      $cache = new Cache(null, null, false, $useCache);
981
      $cacheKey = 'sql-' . md5($query);
982
983
      if (
984
          $cache->getCacheIsReady() === true
985
          &&
986
          $cache->existsItem($cacheKey)
987
      ) {
988
        return $cache->getItem($cacheKey);
989
      }
990
991
    } else {
992
      $cache = false;
993
    }
994
995
    $result = $db->query($query);
996 7
997
    if ($result instanceof Result) {
998 7
999 7
      $return = $result->fetchAllArray();
1000 5
1001 5
      // save into the cache
1002 7 View Code Duplication
      if (
1003 5
          $cacheKey !== null
1004 5
          &&
1005
          $useCache === true
1006 7
          &&
1007
          $cache instanceof Cache
1008 7
          &&
1009
          $cache->getCacheIsReady() === true
1010
      ) {
1011 7
        $cache->setItem($cacheKey, $return, $cacheTTL);
1012
      }
1013
1014 7
    } else {
1015
      $return = $result;
1016 7
    }
1017
1018
    return $return;
1019
  }
1020
1021
  /**
1022
   * Get all table-names via "SHOW TABLES".
1023
   *
1024
   * @return array
1025
   */
1026 1
  public function getAllTables()
1027
  {
1028 1
    $query = 'SHOW TABLES';
1029 1
    $result = $this->query($query);
1030
1031
    return $result->fetchAllArray();
1032
  }
1033
1034
  /**
1035
   * @return Debug
1036 1
   */
1037
  public function getDebugger()
1038 1
  {
1039 1
    return $this->_debug;
1040
  }
1041 1
1042
  /**
1043
   * Get errors from "$this->_errors".
1044
   *
1045
   * @return array
1046
   */
1047
  public function getErrors()
1048
  {
1049
    return $this->_debug->getErrors();
1050
  }
1051
1052
  /**
1053
   * getInstance()
1054
   *
1055
   * @param string      $hostname
1056 1
   * @param string      $username
1057
   * @param string      $password
1058 1
   * @param string      $database
1059
   * @param int|string  $port            <p>default is (int)3306</p>
1060
   * @param string      $charset         <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
1061
   * @param bool|string $exit_on_error   <p>Throw a 'Exception' when a query failed, otherwise it will return 'false'.
1062 1
   *                                     Use a empty string "" or false to disable it.</p>
1063 1
   * @param bool|string $echo_on_error   <p>Echo the error if "checkForDev()" returns true.
1064
   *                                     Use a empty string "" or false to disable it.</p>
1065 1
   * @param string      $logger_class_name
1066
   * @param string      $logger_level    <p>'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'</p>
1067
   * @param array       $extra_config    <p>
1068 1
   *                                     'session_to_db' => false|true<br>
1069 1
   *                                     'socket' => 'string (path)'<br>
1070 1
   *                                     'ssl' => 'bool'<br>
1071
   *                                     'clientkey' => 'string (path)'<br>
1072 1
   *                                     'clientcert' => 'string (path)'<br>
1073
   *                                     'cacert' => 'string (path)'<br>
1074 1
   *                                     </p>
1075 1
   *
1076 1
   * @return \voku\db\DB
1077
   */
1078 1
  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())
1079
  {
1080 1
    /**
1081 1
     * @var $instance DB[]
1082 1
     */
1083 1
    static $instance = array();
1084 1
1085
    /**
1086
     * @var $firstInstance DB
1087 1
     */
1088 1
    static $firstInstance = null;
1089 1
1090
    if (
1091
        $hostname . $username . $password . $database . $port . $charset == ''
1092
        &&
1093 1
        null !== $firstInstance
1094
    ) {
1095 1
      return $firstInstance;
1096
    }
1097
1098
    $extra_config_string = '';
1099
    if (is_array($extra_config) === true) {
1100
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
1101
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
1102
      }
1103
    } else {
1104
      // only for backward compatibility
1105
      $extra_config_string = (int)$extra_config;
1106
    }
1107
1108 1
    $connection = md5(
1109 1
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
1110
    );
1111
1112 1
    if (!isset($instance[$connection])) {
1113 1
      $instance[$connection] = new self(
1114
          $hostname,
1115
          $username,
1116
          $password,
1117
          $database,
1118
          $port,
1119
          $charset,
1120
          $exit_on_error,
1121
          $echo_on_error,
1122 1
          $logger_class_name,
1123
          $logger_level,
1124 1
          $extra_config
1125 1
      );
1126
1127
      if (null === $firstInstance) {
1128
        $firstInstance = $instance[$connection];
1129
      }
1130
    }
1131
1132 4
    return $instance[$connection];
1133
  }
1134 4
1135
  /**
1136 4
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1137 1
   *
1138
   * @return \mysqli
1139 1
   */
1140
  public function getLink()
1141
  {
1142 4
    return $this->link;
1143
  }
1144
1145
  /**
1146
   * Get the current charset.
1147
   *
1148 4
   * @return string
1149 4
   */
1150
  public function get_charset()
1151 4
  {
1152
    return $this->charset;
1153
  }
1154
1155
  /**
1156
   * Check if we are in a transaction.
1157
   *
1158
   * @return bool
1159 4
   */
1160
  public function inTransaction()
1161 4
  {
1162
    return $this->_in_transaction;
1163
  }
1164
1165
  /**
1166
   * Execute a "insert"-query.
1167
   *
1168
   * @param string      $table
1169 4
   * @param array       $data
1170
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1171 4
   *
1172
   * @return false|int <p>false on error</p>
1173
   *
1174
   * @throws QueryException
1175
   */
1176
  public function insert($table, array $data = array(), $databaseName = null)
1177
  {
1178
    // init
1179 2
    $table = trim($table);
1180
1181
    if ($table === '') {
1182 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1183 1
1184 1
      return false;
1185 1
    }
1186 1
1187 1
    if (count($data) === 0) {
1188
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1189
1190 2
      return false;
1191 2
    }
1192
1193 2
    $SET = $this->_parseArrayPair($data);
1194
1195
    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...
1196
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1197
    }
1198
1199
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1200
1201 2
    return $this->query($sql);
1202
  }
1203 2
1204
  /**
1205 2
   * Returns the auto generated id used in the last query.
1206
   *
1207
   * @return int|string
1208
   */
1209
  public function insert_id()
1210
  {
1211 2
    return \mysqli_insert_id($this->link);
1212
  }
1213
1214 2
  /**
1215
   * Check if db-connection is ready.
1216 2
   *
1217 2
   * @return boolean
1218 2
   */
1219 2
  public function isReady()
1220 2
  {
1221
    return $this->connected ? true : false;
1222 2
  }
1223
1224
  /**
1225
   * Get the last sql-error.
1226
   *
1227
   * @return string|false <p>false === there was no error</p>
1228
   */
1229
  public function lastError()
1230
  {
1231
    $errors = $this->_debug->getErrors();
1232
1233
    return count($errors) > 0 ? end($errors) : false;
1234
  }
1235
1236 21
  /**
1237
   * Execute a sql-multi-query.
1238
   *
1239 21
   * @param string $sql
1240
   *
1241 21
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1242 2
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1243
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1244 2
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1245
   *
1246
   * @throws QueryException
1247 20
   */
1248 3
  public function multi_query($sql)
1249
  {
1250 3
    if (!$this->isReady()) {
1251
      return false;
1252
    }
1253 18
1254 View Code Duplication
    if (!$sql || $sql === '') {
1255 18
      $this->_debug->displayError('Can not execute an empty query.', false);
1256
1257
      return false;
1258
    }
1259 18
1260
    $query_start_time = microtime(true);
1261 18
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1262
    $query_duration = microtime(true) - $query_start_time;
1263
1264
    $this->_debug->logQuery($sql, $query_duration, 0);
1265
1266
    $returnTheResult = false;
1267
    $result = array();
1268
1269
    if ($resultTmp) {
1270
      do {
1271
1272
        $resultTmpInner = \mysqli_store_result($this->link);
1273
1274 23
        if ($resultTmpInner instanceof \mysqli_result) {
1275
1276
          $returnTheResult = true;
1277 23
          $result[] = new Result($sql, $resultTmpInner);
1278
1279
        } else {
1280 23
1281
          // is the query successful
1282
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1283
            $result[] = true;
1284 23
          } else {
1285 23
            $result[] = false;
1286 23
          }
1287 23
1288 23
        }
1289
1290 23
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1291 2
1292 2
    } else {
1293
1294 23
      // log the error query
1295 1
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1296 1
1297
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1298 23
    }
1299 1
1300 1
    // return the result only if there was a "SELECT"-query
1301
    if ($returnTheResult === true) {
1302 23
      return $result;
1303 1
    }
1304 1
1305
    if (
1306 23
        count($result) > 0
1307 1
        &&
1308 1
        in_array(false, $result, true) === false
1309
    ) {
1310 23
      return true;
1311 1
    }
1312 1
1313
    return false;
1314 23
  }
1315 1
1316 1
  /**
1317
   * Pings a server connection, or tries to reconnect
1318 23
   * if the connection has gone down.
1319 2
   *
1320 2
   * @return boolean
1321
   */
1322 23
  public function ping()
1323 2
  {
1324 2
    if (
1325
        $this->link
1326 23
        &&
1327 4
        $this->link instanceof \mysqli
1328 4
    ) {
1329
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1330 23
      /** @noinspection UsageOfSilenceOperatorInspection */
1331 1
      return (bool)@\mysqli_ping($this->link);
1332 1
    }
1333
1334 23
    return false;
1335 4
  }
1336 4
1337
  /**
1338 23
   * Get a new "Prepare"-Object for your sql-query.
1339 1
   *
1340 1
   * @param string $query
1341
   *
1342 23
   * @return Prepare
1343 1
   */
1344 1
  public function prepare($query)
1345
  {
1346 23
    return new Prepare($this, $query);
1347 2
  }
1348 2
1349
  /**
1350 23
   * Execute a sql-query and return the result-array for select-statements.
1351 1
   *
1352 1
   * @param string $query
1353
   *
1354 23
   * @return mixed
1355 2
   * @deprecated
1356 2
   * @throws \Exception
1357 2
   */
1358
  public static function qry($query)
1359 2
  {
1360 1
    $db = self::getInstance();
1361 2
1362 1
    $args = func_get_args();
1363 1
    /** @noinspection SuspiciousAssignmentsInspection */
1364
    $query = array_shift($args);
1365 2
    $query = str_replace('?', '%s', $query);
1366 23
    $args = array_map(
1367
        array(
1368
            $db,
1369 23
            'escape',
1370 23
        ),
1371 23
        $args
1372
    );
1373 23
    array_unshift($args, $query);
1374 23
    $query = call_user_func_array('sprintf', $args);
1375 23
    $result = $db->query($query);
1376 23
1377
    if ($result instanceof Result) {
1378 23
      return $result->fetchAllArray();
1379 23
    }
1380 23
1381
    return $result;
1382 23
  }
1383 23
1384 23
  /**
1385
   * Execute a sql-query.
1386 23
   *
1387 23
   * @param string        $sql            <p>The sql query-string.</p>
1388 23
   *
1389
   * @param array|boolean $params         <p>
1390 23
   *                                      "array" of sql-query-parameters<br/>
1391 23
   *                                      "false" if you don't need any parameter (default)<br/>
1392
   *                                      </p>
1393 23
   *
1394
   * @return bool|int|Result              <p>
1395 23
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1396
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1397 23
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1398 1
   *                                      "true" by e.g. "DROP"-queries<br />
1399 23
   *                                      "false" on error
1400 23
   *                                      </p>
1401 23
   *
1402
   * @throws QueryException
1403 23
   */
1404 1
  public function query($sql = '', $params = false)
1405 1
  {
1406
    if (!$this->isReady()) {
1407 23
      return false;
1408 23
    }
1409 23
1410 View Code Duplication
    if (!$sql || $sql === '') {
1411 23
      $this->_debug->displayError('Can not execute an empty query.', false);
1412 2
1413 2
      return false;
1414
    }
1415 23
1416 23
    if (
1417
        $params !== false
1418 23
        &&
1419
        is_array($params)
1420
        &&
1421
        count($params) > 0
1422
    ) {
1423
      $parseQueryParams = $this->_parseQueryParams($sql, $params);
1424
      $parseQueryParamsByName = $this->_parseQueryParamsByName($parseQueryParams['sql'], $parseQueryParams['params']);
1425
      $sql = $parseQueryParamsByName['sql'];
1426
    }
1427
1428 26
    $query_start_time = microtime(true);
1429
    $query_result = \mysqli_real_query($this->link, $sql);
1430 26
    $query_duration = microtime(true) - $query_start_time;
1431 26
1432 26
    $this->query_count++;
1433 26
1434 26
    $mysqli_field_count = \mysqli_field_count($this->link);
1435
    if ($mysqli_field_count) {
1436 26
      $result = \mysqli_store_result($this->link);
1437 26
    } else {
1438
      $result = $query_result;
1439 26
    }
1440
1441
    if ($result instanceof \mysqli_result) {
1442
1443
      // log the select query
1444
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
1445
1446
      // return query result object
1447 1
      return new Result($sql, $result);
1448
    }
1449 1
1450
    if ($query_result === true) {
1451
1452
      // "INSERT" || "REPLACE"
1453 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1454
        $insert_id = (int)$this->insert_id();
1455
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
1456
1457
        return $insert_id;
1458
      }
1459
1460
      // "UPDATE" || "DELETE"
1461 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1462
        $affected_rows = (int)$this->affected_rows();
1463 1
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
1464
1465
        return $affected_rows;
1466 1
      }
1467
1468 1
      // log the ? query
1469 1
      $this->_debug->logQuery($sql, $query_duration, 0);
1470
1471 1
      return true;
1472
    }
1473
1474 1
    // log the error query
1475 1
    $this->_debug->logQuery($sql, $query_duration, 0, true);
1476
1477 1
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
1478
  }
1479
1480
  /**
1481 1
   * Error-handling for the sql-query.
1482 1
   *
1483
   * @param string     $errorMessage
1484 1
   * @param int        $errorNumber
1485 1
   * @param string     $sql
1486
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1487 1
   * @param bool       $sqlMultiQuery
1488
   *
1489
   * @throws QueryException
1490 1
   * @throws DBGoneAwayException
1491
   *
1492 1
   * @return bool
1493 1
   */
1494 1
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1495
  {
1496 1
    $errorNumber = (int)$errorNumber;
1497
1498
    if (
1499
        $errorMessage === 'DB server has gone away'
1500 1
        ||
1501
        $errorMessage === 'MySQL server has gone away'
1502 1
        ||
1503
        $errorNumber === 2006
1504
    ) {
1505
      static $RECONNECT_COUNTER;
1506
1507
      // exit if we have more then 3 "DB server has gone away"-errors
1508
      if ($RECONNECT_COUNTER > 3) {
1509
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql, 5);
1510
        throw new DBGoneAwayException($errorMessage);
1511
      }
1512
1513
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1514
1515
      // reconnect
1516
      $RECONNECT_COUNTER++;
1517 6
      $this->reconnect(true);
1518
1519
      // re-run the current (non multi) query
1520 6
      if ($sqlMultiQuery === false) {
1521
        return $this->query($sql, $sqlParams);
1522 6
      }
1523 1
1524
      return false;
1525 1
    }
1526
1527
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber . ') ' . ":\n<br />" . $sql);
1528 6
1529 2
    $force_exception_after_error = null; // auto
1530
    if ($this->_in_transaction === true) {
1531 2
      $force_exception_after_error = false;
1532
    }
1533
    // this query returned an error, we must display it (only for dev) !!!
1534 6
1535
    $this->_debug->displayError($errorMessage . '(' . $errorNumber . ') ' . ' | ' . $sql, $force_exception_after_error);
1536 6
1537 2
    return false;
1538 6
  }
1539 4
1540 4
  /**
1541 1
   * Quote && Escape e.g. a table name string.
1542
   *
1543
   * @param string $str
1544 6
   *
1545
   * @return string
1546
   */
1547
  public function quote_string($str)
1548 6
  {
1549
    $str = str_replace(
1550 6
        '`',
1551
        '``',
1552
        trim(
1553
            $this->escape($str, false),
1554
            '`'
1555
        )
1556
    );
1557
1558
    return '`' . $str . '`';
1559
  }
1560
1561
  /**
1562
   * Reconnect to the MySQL-Server.
1563
   *
1564 2
   * @param bool $checkViaPing
1565
   *
1566
   * @return bool
1567 2
   */
1568
  public function reconnect($checkViaPing = false)
1569 2
  {
1570 1
    $ping = false;
1571
1572 1
    if ($checkViaPing === true) {
1573
      $ping = $this->ping();
1574
    }
1575 2
1576 1
    if ($ping !== true) {
1577 2
      $this->connected = false;
1578 2
      $this->connect();
1579 2
    }
1580 1
1581
    return $this->isReady();
1582
  }
1583 2
1584
  /**
1585
   * Execute a "replace"-query.
1586
   *
1587 2
   * @param string      $table
1588
   * @param array       $data
1589 2
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1590
   *
1591
   * @return false|int <p>false on error</p>
1592
   *
1593
   * @throws QueryException
1594
   */
1595
  public function replace($table, array $data = array(), $databaseName = null)
1596
  {
1597
    // init
1598
    $table = trim($table);
1599
1600
    if ($table === '') {
1601
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1602
1603 20
      return false;
1604
    }
1605
1606 20
    if (count($data) === 0) {
1607
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1608 20
1609 1
      return false;
1610
    }
1611 1
1612
    // extracting column names
1613
    $columns = array_keys($data);
1614 20
    foreach ($columns as $k => $_key) {
1615 5
      /** @noinspection AlterInForeachInspection */
1616 20
      $columns[$k] = $this->quote_string($_key);
1617 16
    }
1618 16
1619 1
    $columns = implode(',', $columns);
1620
1621
    // extracting values
1622 20
    foreach ($data as $k => $_value) {
1623
      /** @noinspection AlterInForeachInspection */
1624
      $data[$k] = $this->secure($_value);
1625
    }
1626 20
    $values = implode(',', $data);
1627
1628 20
    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...
1629
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1630
    }
1631
1632
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1633
1634
    return $this->query($sql);
1635
  }
1636 1
1637
  /**
1638 1
   * Rollback in a transaction and end the transaction.
1639
   *
1640 1
   * @return bool <p>Boolean true on success, false otherwise.</p>
1641
   */
1642 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...
1643
  {
1644
    if ($this->_in_transaction === false) {
1645
      $this->_debug->displayError('Error: mysql server is not in transaction!', false);
1646 9
1647
      return false;
1648 9
    }
1649
1650
    $return = \mysqli_rollback($this->link);
1651
    \mysqli_autocommit($this->link, true);
1652
    $this->_in_transaction = false;
1653
1654
    return $return;
1655
  }
1656
1657
  /**
1658
   * Try to secure a variable, so can you use it in sql-queries.
1659
   *
1660
   * <p>
1661
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1662
   * 1. parse into "int"
1663
   * </p><br />
1664
   *
1665
   * <p>
1666 2
   * <strong>float:</strong><br />
1667
   * 1. return "float"
1668 2
   * </p><br />
1669
   *
1670 2
   * <p>
1671 2
   * <strong>string:</strong><br />
1672 2
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1673 2
   * 2. trim whitespace<br />
1674
   * 3. trim '<br />
1675
   * 4. escape the string (and remove non utf-8 chars)<br />
1676
   * 5. trim ' again (because we maybe removed some chars)<br />
1677
   * 6. add ' around the new string<br />
1678
   * </p><br />
1679
   *
1680
   * <p>
1681
   * <strong>array:</strong><br />
1682
   * 1. return null
1683
   * </p><br />
1684
   *
1685
   * <p>
1686
   * <strong>object:</strong><br />
1687
   * 1. return false
1688
   * </p><br />
1689 2
   *
1690
   * <p>
1691 2
   * <strong>null:</strong><br />
1692 2
   * 1. return null
1693
   * </p>
1694
   *
1695
   * @param mixed $var
1696
   *
1697
   * @return mixed
1698
   */
1699
  public function secure($var)
1700
  {
1701
    if (
1702
        $var === ''
1703
        ||
1704
        ($this->_convert_null_to_empty_string === true && $var === null)
1705
    ) {
1706
      return "''";
1707
    }
1708
1709
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1710
      return $var;
1711
    }
1712
1713
    if (is_string($var)) {
1714
      $var = trim(trim($var), "'");
1715
    }
1716
1717
    $var = $this->escape($var, false, false, null);
1718
1719
    if (is_string($var)) {
1720
      $var = "'" . trim($var, "'") . "'";
1721
    }
1722
1723
    return $var;
1724
  }
1725
1726
  /**
1727
   * Execute a "select"-query.
1728
   *
1729
   * @param string       $table
1730
   * @param string|array $where
1731
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1732
   *
1733
   * @return false|Result <p>false on error</p>
1734
   *
1735
   * @throws QueryException
1736
   */
1737 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...
1738
  {
1739
    // init
1740
    $table = trim($table);
1741
1742
    if ($table === '') {
1743
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1744
1745
      return false;
1746
    }
1747
1748
    if (is_string($where)) {
1749
      $WHERE = $this->escape($where, false);
1750
    } elseif (is_array($where)) {
1751
      $WHERE = $this->_parseArrayPair($where, 'AND');
1752
    } else {
1753
      $WHERE = '';
1754
    }
1755
1756
    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...
1757
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1758
    }
1759
1760
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1761
1762
    return $this->query($sql);
1763
  }
1764
1765
  /**
1766
   * Selects a different database than the one specified on construction.
1767
   *
1768
   * @param string $database <p>Database name to switch to.</p>
1769
   *
1770
   * @return bool <p>Boolean true on success, false otherwise.</p>
1771
   */
1772
  public function select_db($database)
1773
  {
1774
    if (!$this->isReady()) {
1775
      return false;
1776
    }
1777
1778
    return mysqli_select_db($this->link, $database);
1779
  }
1780
1781
  /**
1782
   * Set the current charset.
1783
   *
1784
   * @param string $charset
1785
   *
1786
   * @return bool
1787
   */
1788
  public function set_charset($charset)
1789
  {
1790
    $charsetLower = strtolower($charset);
1791
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1792
      $charset = 'utf8';
1793
    }
1794
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1795
      $charset = 'utf8mb4';
1796
    }
1797
1798
    $this->charset = (string)$charset;
1799
1800
    $return = mysqli_set_charset($this->link, $charset);
1801
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1802
    /** @noinspection UsageOfSilenceOperatorInspection */
1803
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1804
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1805
    /** @noinspection UsageOfSilenceOperatorInspection */
1806
    @\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...
1807
1808
    return $return;
1809
  }
1810
1811
  /**
1812
   * Set the option to convert null to "''" (empty string).
1813
   *
1814
   * Used in secure() => select(), insert(), update(), delete()
1815
   *
1816
   * @param $bool
1817
   */
1818
  public function set_convert_null_to_empty_string($bool)
1819
  {
1820
    $this->_convert_null_to_empty_string = (bool)$bool;
1821
  }
1822
1823
  /**
1824
   * Enables or disables internal report functions
1825
   *
1826
   * @link http://php.net/manual/en/function.mysqli-report.php
1827
   *
1828
   * @param int $flags <p>
1829
   *                   <table>
1830
   *                   Supported flags
1831
   *                   <tr valign="top">
1832
   *                   <td>Name</td>
1833
   *                   <td>Description</td>
1834
   *                   </tr>
1835
   *                   <tr valign="top">
1836
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1837
   *                   <td>Turns reporting off</td>
1838
   *                   </tr>
1839
   *                   <tr valign="top">
1840
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1841
   *                   <td>Report errors from mysqli function calls</td>
1842
   *                   </tr>
1843
   *                   <tr valign="top">
1844
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1845
   *                   <td>
1846
   *                   Throw <b>mysqli_sql_exception</b> for errors
1847
   *                   instead of warnings
1848
   *                   </td>
1849
   *                   </tr>
1850
   *                   <tr valign="top">
1851
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1852
   *                   <td>Report if no index or bad index was used in a query</td>
1853
   *                   </tr>
1854
   *                   <tr valign="top">
1855
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1856
   *                   <td>Set all options (report all)</td>
1857
   *                   </tr>
1858
   *                   </table>
1859
   *                   </p>
1860
   *
1861
   * @return bool
1862
   */
1863
  public function set_mysqli_report($flags)
1864
  {
1865
    return \mysqli_report($flags);
1866
  }
1867
1868
  /**
1869
   * Show config errors by throw exceptions.
1870
   *
1871
   * @return bool
1872
   *
1873
   * @throws \InvalidArgumentException
1874
   */
1875
  public function showConfigError()
1876
  {
1877
1878
    if (
1879
        !$this->hostname
1880
        ||
1881
        !$this->username
1882
        ||
1883
        !$this->database
1884
    ) {
1885
1886
      if (!$this->hostname) {
1887
        throw new \InvalidArgumentException('no-sql-hostname');
1888
      }
1889
1890
      if (!$this->username) {
1891
        throw new \InvalidArgumentException('no-sql-username');
1892
      }
1893
1894
      if (!$this->database) {
1895
        throw new \InvalidArgumentException('no-sql-database');
1896
      }
1897
1898
      return false;
1899
    }
1900
1901
    return true;
1902
  }
1903
1904
  /**
1905
   * alias: "beginTransaction()"
1906
   */
1907
  public function startTransaction()
1908
  {
1909
    $this->beginTransaction();
1910
  }
1911
1912
  /**
1913
   * Execute a callback inside a transaction.
1914
   *
1915
   * @param callback $callback <p>The callback to run inside the transaction, if it's throws an "Exception" or if it's
1916
   *                           returns "false", all SQL-statements in the callback will be rollbacked.</p>
1917
   *
1918
   * @return bool <p>Boolean true on success, false otherwise.</p>
1919
   */
1920
  public function transact($callback)
1921
  {
1922
    try {
1923
1924
      $beginTransaction = $this->beginTransaction();
1925
      if ($beginTransaction === false) {
1926
        $this->_debug->displayError('Error: transact -> can not start transaction!', false);
1927
1928
        return false;
1929
      }
1930
1931
      $result = call_user_func($callback, $this);
1932
      if ($result === false) {
1933
        /** @noinspection ThrowRawExceptionInspection */
1934
        throw new \Exception('call_user_func [' . $callback . '] === false');
1935
      }
1936
1937
      return $this->commit();
1938
1939
    } catch (\Exception $e) {
1940
1941
      $this->rollback();
1942
1943
      return false;
1944
    }
1945
  }
1946
1947
  /**
1948
   * Execute a "update"-query.
1949
   *
1950
   * @param string       $table
1951
   * @param array        $data
1952
   * @param array|string $where
1953
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1954
   *
1955
   * @return false|int <p>false on error</p>
1956
   *
1957
   * @throws QueryException
1958
   */
1959
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1960
  {
1961
    // init
1962
    $table = trim($table);
1963
1964
    if ($table === '') {
1965
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1966
1967
      return false;
1968
    }
1969
1970
    if (count($data) === 0) {
1971
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1972
1973
      return false;
1974
    }
1975
1976
    $SET = $this->_parseArrayPair($data);
1977
1978
    if (is_string($where)) {
1979
      $WHERE = $this->escape($where, false);
1980
    } elseif (is_array($where)) {
1981
      $WHERE = $this->_parseArrayPair($where, 'AND');
1982
    } else {
1983
      $WHERE = '';
1984
    }
1985
1986
    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...
1987
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1988
    }
1989
1990
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1991
1992
    return $this->query($sql);
1993
  }
1994
1995
}
1996