Completed
Push — master ( e0d76f...7b97ec )
by Lars
01:54
created

DB::isReady()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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