Completed
Push — master ( 059279...e0d76f )
by Lars
19:42 queued 17:36
created

DB::replace()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.0702

Importance

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