Completed
Push — master ( 9c074b...c3ae3f )
by Lars
30:57 queued 21:37
created

DB::secure()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.1782

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 11
cts 13
cp 0.8462
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 14
nc 6
nop 1
crap 7.1782
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
   * __wakeup
217
   *
218 10
   * @return void
219
   */
220 10
  public function __wakeup()
221
  {
222
    $this->reconnect();
223
  }
224
225
  /**
226
   * Load the config from the constructor.
227
   *
228
   * @param string         $hostname
229
   * @param string         $username
230 10
   * @param string         $password
231
   * @param string         $database
232
   * @param int|string     $port          <p>default is (int)3306</p>
233
   * @param string         $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
234 10
   * @param boolean|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
235 10
   * @param boolean|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
236 9
   * @param string         $logger_class_name
237 9
   * @param string         $logger_level
238 8
   * @param array          $extra_config <p>
239 10
   *                                     'session_to_db' => false|true<br>
240
   *                                     'socket' => 'string (path)'<br>
241 3
   *                                     'ssl' => 'bool'<br>
242 1
   *                                     'clientkey' => 'string (path)'<br>
243
   *                                     'clientcert' => 'string (path)'<br>
244
   *                                     'cacert' => 'string (path)'<br>
245 2
   *                                     </p>
246 1
   *
247
   * @return bool
248
   */
249 1
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $extra_config)
250 1
  {
251
    $this->hostname = (string)$hostname;
252
    $this->username = (string)$username;
253
    $this->password = (string)$password;
254
    $this->database = (string)$database;
255
256 7
    if ($charset) {
257
      $this->charset = (string)$charset;
258
    }
259
260
    if ($port) {
261
      $this->port = (int)$port;
262
    } else {
263
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
264
      /** @noinspection UsageOfSilenceOperatorInspection */
265
      $this->port = (int)@ini_get('mysqli.default_port');
266 9
    }
267
268 9
    // fallback
269 1
    if (!$this->port) {
270
      $this->port = 3306;
271
    }
272 9
273
    if (!$this->socket) {
274 9
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
275
      $this->socket = @ini_get('mysqli.default_socket');
276 9
    }
277 9
278 9
    if ($exit_on_error === true || $exit_on_error === false) {
279
      $this->_debug->setExitOnError($exit_on_error);
280
    }
281 9
282 9
    if ($echo_on_error === true || $echo_on_error === false) {
283 9
      $this->_debug->setEchoOnError($echo_on_error);
284 9
    }
285 9
286 9
    $this->_debug->setLoggerClassName($logger_class_name);
287 9
    $this->_debug->setLoggerLevel($logger_level);
288 9
289 9
    if (is_array($extra_config) === true) {
290 9
291 3
      if (isset($extra_config['session_to_db'])) {
292 3
        $this->session_to_db = (boolean)$extra_config['session_to_db'];
293 3
      }
294
295 6
      if (isset($extra_config['socket'])) {
296
        $this->socket = $extra_config['socket'];
297 6
      }
298
299
      if (isset($extra_config['ssl'])) {
300
        $this->_ssl = $extra_config['ssl'];
301
      }
302
303
      if (isset($extra_config['clientkey'])) {
304 6
        $this->_clientkey = $extra_config['clientkey'];
305
      }
306 6
307
      if (isset($extra_config['clientcert'])) {
308
        $this->_clientcert = $extra_config['clientcert'];
309
      }
310
311
      if (isset($extra_config['cacert'])) {
312
        $this->_cacert = $extra_config['cacert'];
313
      }
314 45
315
    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
316 45
      // only for backward compatibility
317
      //$this->session_to_db = (boolean)$extra_config;
318
    }
319
320
    return $this->showConfigError();
321
  }
322
323
  /**
324
   * Parses arrays with value pairs and generates SQL to use in queries.
325
   *
326 2
   * @param array  $arrayPair
327
   * @param string $glue <p>This is the separator.</p>
328 2
   *
329
   * @return string
330
   *
331
   * @internal
332
   */
333
  public function _parseArrayPair($arrayPair, $glue = ',')
334
  {
335
    // init
336
    $sql = '';
337
338
    /** @noinspection IsEmptyFunctionUsageInspection */
339
    if (empty($arrayPair)) {
340
      return '';
341
    }
342
343
    $arrayPairCounter = 0;
344
    foreach ($arrayPair as $_key => $_value) {
345
      $_connector = '=';
346
      $_glueHelper = '';
347
      $_key_upper = strtoupper($_key);
348
349
      if (strpos($_key_upper, ' NOT') !== false) {
350
        $_connector = 'NOT';
351
      }
352
353
      if (strpos($_key_upper, ' IS') !== false) {
354
        $_connector = 'IS';
355
      }
356
357
      if (strpos($_key_upper, ' IS NOT') !== false) {
358
        $_connector = 'IS NOT';
359
      }
360
361
      if (strpos($_key_upper, ' IN') !== false) {
362
        $_connector = 'IN';
363
      }
364
365
      if (strpos($_key_upper, ' NOT IN') !== false) {
366
        $_connector = 'NOT IN';
367
      }
368
369
      if (strpos($_key_upper, ' BETWEEN') !== false) {
370
        $_connector = 'BETWEEN';
371
      }
372
373
      if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
374
        $_connector = 'NOT BETWEEN';
375
      }
376
377
      if (strpos($_key_upper, ' LIKE') !== false) {
378
        $_connector = 'LIKE';
379
      }
380
381
      if (strpos($_key_upper, ' NOT LIKE') !== false) {
382
        $_connector = 'NOT LIKE';
383 57
      }
384
385 View Code Duplication
      if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
386
        $_connector = '>';
387
      }
388 57
389 View Code Duplication
      if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
390
        $_connector = '<';
391
      }
392
393 57
      if (strpos($_key_upper, ' >=') !== false) {
394
        $_connector = '>=';
395
      }
396 57
397 57
      if (strpos($_key_upper, ' <=') !== false) {
398 11
        $_connector = '<=';
399 57
      }
400 11
401
      if (strpos($_key_upper, ' <>') !== false) {
402
        $_connector = '<>';
403 57
      }
404 57
405 57
      if (strpos($_key_upper, ' OR') !== false) {
406
        $_glueHelper = 'OR';
407 57
      }
408 10
409 10
      if (strpos($_key_upper, ' AND') !== false) {
410 10
        $_glueHelper = 'AND';
411 10
      }
412 10
413 10
      if (is_array($_value) === true) {
414 10
        foreach ($_value as $oldKey => $oldValue) {
415 10
          $_value[$oldKey] = $this->secure($oldValue);
416 10
        }
417 10
418 10
        if ($_connector === 'NOT IN' || $_connector === 'IN') {
419
          $_value = '(' . implode(',', $_value) . ')';
420 10
        } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
421
          $_value = '(' . implode(' AND ', $_value) . ')';
422 4
        }
423 1
424 1
      } else {
425 4
        $_value = $this->secure($_value);
426
      }
427 57
428
      $quoteString = $this->quote_string(
429
          trim(
430
              str_ireplace(
431
                  array(
432
                      $_connector,
433
                      $_glueHelper,
434
                  ),
435
                  '',
436
                  $_key
437
              )
438
          )
439
      );
440
441
      if (!is_array($_value)) {
442
        $_value = array($_value);
443
      }
444
445
      if (!$_glueHelper) {
446
        $_glueHelper = $glue;
447
      }
448
449
      $tmpCounter = 0;
450 35
      foreach ($_value as $valueInner) {
451
452 35
        $_glueHelperInner = $_glueHelper;
453
454
        if ($arrayPairCounter === 0) {
455
456 35
          if ($tmpCounter === 0 && $_glueHelper === 'OR') {
457 4
            $_glueHelperInner = '1 = 1 AND ('; // first "OR"-query glue
458
          } elseif ($tmpCounter === 0) {
459 4
            $_glueHelperInner = ''; // first query glue e.g. for "INSERT"-query -> skip the first ","
460
          }
461
462
        } elseif ($tmpCounter === 0 && $_glueHelper === 'OR') {
463
          $_glueHelperInner = 'AND ('; // inner-loop "OR"-query glue
464 33
        }
465 3
466 33
        $sql .= ' ' . $_glueHelperInner . ' ' . $quoteString . ' ' . $_connector . ' ' . $valueInner . " \n";
467 3
        $tmpCounter++;
468 33
      }
469 3
470 3
      if ($_glueHelper === 'OR') {
471
        $sql .= ' ) ';
472 33
      }
473 33
474 33
      $arrayPairCounter++;
475
    }
476 33
477
    return $sql;
478 33
  }
479 33
480 28
  /**
481 28
   * _parseQueryParams
482 24
   *
483
   * @param string $sql
484
   * @param array  $params
485 33
   *
486
   * @return string
487
   */
488 27
  private function _parseQueryParams($sql, array $params)
489
  {
490
    // is there anything to parse?
491 27
    if (strpos($sql, '?') === false) {
492
      return $sql;
493
    }
494
495 25
    if (count($params) > 0) {
496
      $parseKey = md5(uniqid((string)mt_rand(), true));
497
      $sql = str_replace('?', $parseKey, $sql);
498 23
499 22
      $k = 0;
500 22
      while (strpos($sql, $parseKey) !== false) {
501
        $value = $this->secure($params[$k]);
502 22
        $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 502 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...
503
        $k++;
504
      }
505
    }
506 8
507 8
    return $sql;
508 8
  }
509
510 8
  /**
511
   * Gets the number of affected rows in a previous MySQL operation.
512
   *
513
   * @return int
514
   */
515
  public function affected_rows()
516
  {
517
    return \mysqli_affected_rows($this->link);
518
  }
519
520 8
  /**
521
   * Begins a transaction, by turning off auto commit.
522 8
   *
523
   * @return bool <p>This will return true or false indicating success of transaction</p>
524
   */
525
  public function beginTransaction()
526
  {
527
    if ($this->_in_transaction === true) {
528
      $this->_debug->displayError('Error mysql server already in transaction!', false);
529
530
      return false;
531
    }
532
533 3
    $this->clearErrors(); // needed for "$this->endTransaction()"
534
535
    $this->_in_transaction = true;
536 3
    $return = \mysqli_autocommit($this->link, false);
537
    if ($return === false) {
538
      $this->_in_transaction = false;
539
    }
540 3
541 3
    return $return;
542 3
  }
543
544 3
  /**
545 3
   * Clear the errors in "_debug->_errors".
546 3
   *
547 3
   * @return bool
548 3
   */
549 3
  public function clearErrors()
550 3
  {
551
    return $this->_debug->clearErrors();
552 3
  }
553
554
  /**
555
   * Closes a previously opened database connection.
556
   *
557
   * @return bool
558
   */
559
  public function close()
560
  {
561
    $this->connected = false;
562
563
    if (!$this->link) {
564
      return false;
565
    }
566
567
    if (\mysqli_close($this->link)) {
568
      $this->link = null;
569
      return true;
570
    }
571
572
    return false;
573
  }
574
575
  /**
576
   * Open a new connection to the MySQL server.
577
   *
578
   * @return bool
579
   *
580
   * @throws DBConnectException
581
   */
582
  public function connect()
583
  {
584
    if ($this->isReady()) {
585
      return true;
586
    }
587
588
    $flags = null;
0 ignored issues
show
Unused Code introduced by
$flags is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
589
590
    \mysqli_report(MYSQLI_REPORT_STRICT);
591
    try {
592
593
      $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...
594
595
      if (Helper::isMysqlndIsUsed() === true) {
596
        \mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
597 26
      }
598
599
600
601 26
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
602 26
      $this->connected = @\mysqli_real_connect(
603 26
          $this->link,
604 1
          $this->hostname,
605
          $this->username,
606
          $this->password,
607 26
          $this->database,
608 1
          $this->port,
609
          $this->socket
610
      );
611 26
612 22
    } catch (\Exception $e) {
613 22
      $error = 'Error connecting to mysql server: ' . $e->getMessage();
614
      $this->_debug->displayError($error, false);
615 26
      throw new DBConnectException($error, 100, $e);
616
    }
617 26
    \mysqli_report(MYSQLI_REPORT_OFF);
618 22
619 22
    $errno = mysqli_connect_errno();
620
    if (!$this->connected || $errno) {
621 26
      $error = 'Error connecting to mysql server: ' . \mysqli_connect_error() . ' (' . $errno . ')';
622
      $this->_debug->displayError($error, false);
623
      throw new DBConnectException($error, 101);
624
    }
625
626
    $this->set_charset($this->charset);
627
628
    return $this->isReady();
629
  }
630
631
  /**
632
   * Execute a "delete"-query.
633
   *
634
   * @param string       $table
635
   * @param string|array $where
636
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
637
   *
638
   * @return false|int <p>false on error</p>
639
   *
640
   *    * @throws QueryException
641 33
   */
642 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...
643 33
  {
644 2
    // init
645
    $table = trim($table);
646
647 33
    if ($table === '') {
648 2
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
649
650
      return false;
651
    }
652 33
653 33
    if (is_string($where)) {
654 33
      $WHERE = $this->escape($where, false);
655
    } elseif (is_array($where)) {
656
      $WHERE = $this->_parseArrayPair($where, 'AND');
657
    } else {
658 33
      $WHERE = '';
659
    }
660 33
661 33
    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...
662
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
663 33
    }
664 33
665 33
    $sql = 'DELETE FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
666 33
667
    return $this->query($sql);
668 33
  }
669 33
670
  /**
671
   * Ends a transaction and commits if no errors, then ends autocommit.
672
   *
673 24
   * @return bool <p>This will return true or false indicating success of transactions.</p>
674
   */
675
  public function endTransaction()
676 33
  {
677
    if (!$this->errors()) {
678
      $return = \mysqli_commit($this->link);
679
    } else {
680 5
      $this->rollback();
681
      $return = false;
682
    }
683 33
684
    \mysqli_autocommit($this->link, true);
685
    $this->_in_transaction = false;
686
687 3
    return $return;
688 3
  }
689
690
  /**
691 1
   * Get all errors from "$this->_errors".
692 1
   *
693
   * @return array|false <p>false === on errors</p>
694 1
   */
695 1
  public function errors()
696
  {
697
    $errors = $this->_debug->getErrors();
698 1
699 1
    return count($errors) > 0 ? $errors : false;
700
  }
701 1
702 1
  /**
703
   * Escape: Use "mysqli_real_escape_string" and clean non UTF-8 chars + some extra optional stuff.
704 1
   *
705
   * @param mixed     $var           boolean: convert into "integer"<br />
706
   *                                 int: int (don't change it)<br />
707 1
   *                                 float: float (don't change it)<br />
708
   *                                 null: null (don't change it)<br />
709
   *                                 array: run escape() for every key => value<br />
710
   *                                 string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
711 33
   * @param bool      $stripe_non_utf8
712
   * @param bool      $html_entity_decode
713
   * @param bool|null $convert_array <strong>false</strong> => Keep the array.<br />
714 3
   *                                 <strong>true</strong> => Convert to string var1,var2,var3...<br />
715 3
   *                                 <strong>null</strong> => Convert the array into null, every time.
716 3
   *
717 3
   * @return mixed
718 33
   */
719
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $convert_array = false)
720
  {
721
    if ($var === '') {
722 33
      return '';
723
    }
724 33
725 9
    if ($var === null) {
726 9
      return null;
727
    }
728 33
729
    // save the current value as int (for later usage)
730 1
    if (!is_object($var)) {
731 1
      $varInt = (int)$var;
732
    }
733 33
734
    /** @noinspection TypeUnsafeComparisonInspection */
735 33
    if (
736
        is_int($var)
737 33
        ||
738
        is_bool($var)
739
        ||
740
        (
741 3
            isset($varInt, $var[0])
742
            &&
743
            $var[0] != '0'
744
            &&
745
            "$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...
746 3
        )
747
    ) {
748
749
      // "int" || int || bool
750
751
      return (int)$var;
752 2
    }
753
754
    if (is_float($var)) {
755
756
      // float
757
758
      return $var;
759
    }
760
761 35
    if (is_array($var)) {
762
763 35
      // array
764
765
      if ($convert_array === null) {
766
        return null;
767
      }
768
769
      $varCleaned = array();
770
      foreach ((array)$var as $key => $value) {
771 22
772
        $key = $this->escape($key, $stripe_non_utf8, $html_entity_decode);
773 22
        $value = $this->escape($value, $stripe_non_utf8, $html_entity_decode);
774
775
        /** @noinspection OffsetOperationsInspection */
776
        $varCleaned[$key] = $value;
777
      }
778
779
      if ($convert_array === true) {
780
        $varCleaned = implode(',', $varCleaned);
781 8
782
        return $varCleaned;
783 8
      }
784
785
      return (array)$varCleaned;
786
    }
787
788
    if (
789
        is_string($var)
790
        ||
791
        (
792
            is_object($var)
793
            &&
794
            method_exists($var, '__toString')
795
        )
796
    ) {
797
798 9
      // "string"
799
800 9
      $var = (string)$var;
801 1
802
      if ($stripe_non_utf8 === true) {
803
        $var = UTF8::cleanup($var);
804 1
      }
805
806
      if ($html_entity_decode === true) {
807
        // use no-html-entity for db
808
        $var = UTF8::html_entity_decode($var);
809 1
      }
810
811
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
812 1
813 1
      $var = \mysqli_real_escape_string($this->getLink(), $var);
814
815
      return (string)$var;
816 1
817
    }
818
819 8
    if ($var instanceof \DateTime) {
820
821
      // "DateTime"-object
822 8
823
      try {
824 8
        return $this->escape($var->format('Y-m-d H:i:s'), false);
825
      } catch (\Exception $e) {
826
        return null;
827
      }
828
829
    } else {
830
      return false;
831
    }
832
  }
833
834 3
  /**
835
   * Execute select/insert/update/delete sql-queries.
836 3
   *
837
   * @param string $query    sql-query
838 3
   * @param bool   $useCache use cache?
839 2
   * @param int    $cacheTTL cache-ttl in seconds
840 2
   *
841
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
842 3
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
843 3
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
844 3
   *               "true" by e.g. "DROP"-queries<br />
845 3
   *               "false" on error
846
   *
847 3
   * @throws QueryException
848
   */
849
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600)
850
  {
851
    // init
852
    $cacheKey = null;
853
    $db = self::getInstance();
854
855 View Code Duplication
    if ($useCache === true) {
856 3
      $cache = new Cache(null, null, false, $useCache);
857
      $cacheKey = 'sql-' . md5($query);
858
859 3
      if (
860 3
          $cache->getCacheIsReady() === true
861 3
          &&
862 3
          $cache->existsItem($cacheKey)
863
      ) {
864
        return $cache->getItem($cacheKey);
865 3
      }
866
867
    } else {
868
      $cache = false;
869
    }
870
871
    $result = $db->query($query);
872
873
    if ($result instanceof Result) {
874
875
      $return = $result->fetchAllArray();
876
877
      // save into the cache
878 View Code Duplication
      if (
879
          $cacheKey !== null
880
          &&
881
          $useCache === true
882
          &&
883
          $cache instanceof Cache
884
          &&
885
          $cache->getCacheIsReady() === true
886 3
      ) {
887
        $cache->setItem($cacheKey, $return, $cacheTTL);
888
      }
889 3
890 3
    } else {
891
      $return = $result;
892 3
    }
893 1
894 1
    return $return;
895
  }
896
897 1
  /**
898 1
   * Get all table-names via "SHOW TABLES".
899 1
   *
900 1
   * @return array
901 1
   */
902
  public function getAllTables()
903
  {
904 1
    $query = 'SHOW TABLES';
905 3
    $result = $this->query($query);
906
907
    return $result->fetchAllArray();
908 3
  }
909
910 3
  /**
911
   * @return Debug
912 1
   */
913
  public function getDebugger()
914
  {
915
    return $this->_debug;
916
  }
917 1
918
  /**
919 1
   * Get errors from "$this->_errors".
920
   *
921 1
   * @return array
922 1
   */
923 1
  public function getErrors()
924 1
  {
925 1
    return $this->_debug->getErrors();
926
  }
927 1
928 2
  /**
929
   * getInstance()
930
   *
931 3
   * @param string      $hostname
932
   * @param string      $username
933
   * @param string      $password
934
   * @param string      $database
935
   * @param int|string  $port          <p>default is (int)3306</p>
936
   * @param string      $charset       <p>default is 'utf8' or 'utf8mb4' (if supported)</p>
937
   * @param bool|string $exit_on_error <p>Use a empty string "" or false to disable it.</p>
938
   * @param bool|string $echo_on_error <p>Use a empty string "" or false to disable it.</p>
939 1
   * @param string      $logger_class_name
940
   * @param string      $logger_level
941 1
   * @param array       $extra_config    <p>
942
   *                                     'session_to_db' => false|true<br>
943
   *                                     'socket' => 'string (path)'<br>
944
   *                                     'ssl' => 'bool'<br>
945
   *                                     'clientkey' => 'string (path)'<br>
946
   *                                     'clientcert' => 'string (path)'<br>
947
   *                                     'cacert' => 'string (path)'<br>
948
   *                                     </p>
949
   *
950
   * @return \voku\db\DB
951
   */
952
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $extra_config = array())
953
  {
954
    /**
955
     * @var $instance DB[]
956
     */
957
    static $instance = array();
958
959
    /**
960
     * @var $firstInstance DB
961
     */
962
    static $firstInstance = null;
963
964
    if (
965
        $hostname . $username . $password . $database . $port . $charset == ''
966
        &&
967
        null !== $firstInstance
968
    ) {
969
      return $firstInstance;
970
    }
971
972
    $extra_config_string = '';
973
    if (is_array($extra_config) === true) {
974
      foreach ($extra_config as $extra_config_key => $extra_config_value) {
975
        $extra_config_string .= $extra_config_key . (string)$extra_config_value;
976
      }
977
    } else {
978
      // only for backward compatibility
979
      $extra_config_string = (int)$extra_config;
980
    }
981
982
    $connection = md5(
983
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . $extra_config_string
984
    );
985
986
    if (!isset($instance[$connection])) {
987
      $instance[$connection] = new self(
988
          $hostname,
989
          $username,
990
          $password,
991
          $database,
992
          $port,
993
          $charset,
994
          $exit_on_error,
995
          $echo_on_error,
996 7
          $logger_class_name,
997
          $logger_level,
998 7
          $extra_config
999 7
      );
1000 5
1001 5
      if (null === $firstInstance) {
1002 7
        $firstInstance = $instance[$connection];
1003 5
      }
1004 5
    }
1005
1006 7
    return $instance[$connection];
1007
  }
1008 7
1009
  /**
1010
   * Get the mysqli-link (link identifier returned by mysqli-connect).
1011 7
   *
1012
   * @return \mysqli
1013
   */
1014 7
  public function getLink()
1015
  {
1016 7
    return $this->link;
1017
  }
1018
1019
  /**
1020
   * Get the current charset.
1021
   *
1022
   * @return string
1023
   */
1024
  public function get_charset()
1025
  {
1026 1
    return $this->charset;
1027
  }
1028 1
1029 1
  /**
1030
   * Check if we are in a transaction.
1031
   *
1032
   * @return bool
1033
   */
1034
  public function inTransaction()
1035
  {
1036 1
    return $this->_in_transaction;
1037
  }
1038 1
1039 1
  /**
1040
   * Execute a "insert"-query.
1041 1
   *
1042
   * @param string      $table
1043
   * @param array       $data
1044
   * @param string|null $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1045
   *
1046
   * @return false|int <p>false on error</p>
1047
   *
1048
   * @throws QueryException
1049
   */
1050
  public function insert($table, array $data = array(), $databaseName = null)
1051
  {
1052
    // init
1053
    $table = trim($table);
1054
1055
    if ($table === '') {
1056 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1057
1058 1
      return false;
1059
    }
1060
1061
    if (count($data) === 0) {
1062 1
      $this->_debug->displayError('Invalid data for INSERT, data is empty.', false);
1063 1
1064
      return false;
1065 1
    }
1066
1067
    $SET = $this->_parseArrayPair($data);
1068 1
1069 1
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1070 1
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1071
    }
1072 1
1073
    $sql = 'INSERT INTO ' . $databaseName . $this->quote_string($table) . " SET $SET;";
1074 1
1075 1
    return $this->query($sql);
1076 1
  }
1077
1078 1
  /**
1079
   * Returns the auto generated id used in the last query.
1080 1
   *
1081 1
   * @return int|string
1082 1
   */
1083 1
  public function insert_id()
1084 1
  {
1085
    return \mysqli_insert_id($this->link);
1086
  }
1087 1
1088 1
  /**
1089 1
   * Check if db-connection is ready.
1090
   *
1091
   * @return boolean
1092
   */
1093 1
  public function isReady()
1094
  {
1095 1
    return $this->connected ? true : false;
1096
  }
1097
1098
  /**
1099
   * Get the last sql-error.
1100
   *
1101
   * @return string|false <p>false === there was no error</p>
1102
   */
1103
  public function lastError()
1104
  {
1105
    $errors = $this->_debug->getErrors();
1106
1107
    return count($errors) > 0 ? end($errors) : false;
1108 1
  }
1109 1
1110
  /**
1111
   * Execute a sql-multi-query.
1112 1
   *
1113 1
   * @param string $sql
1114
   *
1115
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
1116
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
1117
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1118
   *                        "boolean" by only by e.g. "DROP"-queries<br />
1119
   *
1120
   * @throws QueryException
1121
   */
1122 1
  public function multi_query($sql)
1123
  {
1124 1
    if (!$this->isReady()) {
1125 1
      return false;
1126
    }
1127
1128 View Code Duplication
    if (!$sql || $sql === '') {
1129
      $this->_debug->displayError('Can not execute an empty query.', false);
1130
1131
      return false;
1132 4
    }
1133
1134 4
    $query_start_time = microtime(true);
1135
    $resultTmp = \mysqli_multi_query($this->link, $sql);
1136 4
    $query_duration = microtime(true) - $query_start_time;
1137 1
1138
    $this->_debug->logQuery($sql, $query_duration, 0);
1139 1
1140
    $returnTheResult = false;
1141
    $result = array();
1142 4
    if ($resultTmp) {
1143
      do {
1144
        $resultTmpInner = \mysqli_store_result($this->link);
1145
1146
        if ($resultTmpInner instanceof \mysqli_result) {
1147
          $returnTheResult = true;
1148 4
1149 4
          $result[] = new Result($sql, $resultTmpInner);
1150
1151 4
        } else {
1152
1153
          // is the query successful
1154
          if ($resultTmpInner === true || !\mysqli_errno($this->link)) {
1155
            $result[] = true;
1156
          } else {
1157
            $result[] = false;
1158
          }
1159 4
        }
1160
1161 4
      } while (\mysqli_more_results($this->link) === true ? \mysqli_next_result($this->link) : false);
1162
1163
    } else {
1164
1165
      // log the error query
1166
      $this->_debug->logQuery($sql, $query_duration, 0, true);
1167
1168
      return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, false, true);
1169 4
    }
1170
1171 4
    // return the result only if there was a "SELECT"-query
1172
    if ($returnTheResult === true) {
1173
      return $result;
1174
    }
1175
1176
    if (
1177
        count($result) > 0
1178
        &&
1179 2
        in_array(false, $result, true) === false
1180
    ) {
1181
      return true;
1182 2
    }
1183 1
1184 1
    return false;
1185 1
  }
1186 1
1187 1
  /**
1188
   * Pings a server connection, or tries to reconnect
1189
   * if the connection has gone down.
1190 2
   *
1191 2
   * @return boolean
1192
   */
1193 2
  public function ping()
1194
  {
1195
    if (
1196
        $this->link
1197
        &&
1198
        $this->link instanceof \mysqli
1199
    ) {
1200
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1201 2
      /** @noinspection UsageOfSilenceOperatorInspection */
1202
      return (bool)@\mysqli_ping($this->link);
1203 2
    }
1204
1205 2
    return false;
1206
  }
1207
1208
  /**
1209
   * Selects a different database than the one specified on construction.
1210
   *
1211 2
   * @param string $database <p>Database name to switch to.</p>
1212
   *
1213
   * @return bool <p>Boolean true on success, false otherwise.</p>
1214 2
   */
1215
  public function select_db($database)
1216 2
  {
1217 2
    if (!$this->isReady()) {
1218 2
      return false;
1219 2
    }
1220 2
1221
    return mysqli_select_db($this->link, $database);
1222 2
  }
1223
1224
  /**
1225
   * Get a new "Prepare"-Object for your sql-query.
1226
   *
1227
   * @param string $query
1228
   *
1229
   * @return Prepare
1230
   */
1231
  public function prepare($query)
1232
  {
1233
    return new Prepare($this, $query);
1234
  }
1235
1236 21
  /**
1237
   * Execute a sql-query and return the result-array for select-statements.
1238
   *
1239 21
   * @param $query
1240
   *
1241 21
   * @return mixed
1242 2
   * @deprecated
1243
   * @throws \Exception
1244 2
   */
1245
  public static function qry($query)
1246
  {
1247 20
    $db = self::getInstance();
1248 3
1249
    $args = func_get_args();
1250 3
    /** @noinspection SuspiciousAssignmentsInspection */
1251
    $query = array_shift($args);
1252
    $query = str_replace('?', '%s', $query);
1253 18
    $args = array_map(
1254
        array(
1255 18
            $db,
1256
            'escape',
1257
        ),
1258
        $args
1259 18
    );
1260
    array_unshift($args, $query);
1261 18
    $query = call_user_func_array('sprintf', $args);
1262
    $result = $db->query($query);
1263
1264
    if ($result instanceof Result) {
1265
      return $result->fetchAllArray();
1266
    }
1267
1268
    return $result;
1269
  }
1270
1271
  /**
1272
   * Execute a sql-query.
1273
   *
1274 23
   * @param string        $sql            <p>The sql query-string.</p>
1275
   *
1276
   * @param array|boolean $params         <p>
1277 23
   *                                      "array" of sql-query-parameters<br/>
1278
   *                                      "false" if you don't need any parameter (default)<br/>
1279
   *                                      </p>
1280 23
   *
1281
   * @return bool|int|Result              <p>
1282
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1283
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1284 23
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1285 23
   *                                      "true" by e.g. "DROP"-queries<br />
1286 23
   *                                      "false" on error
1287 23
   *                                      </p>
1288 23
   *
1289
   * @throws QueryException
1290 23
   */
1291 2
  public function query($sql = '', $params = false)
1292 2
  {
1293
    if (!$this->isReady()) {
1294 23
      return false;
1295 1
    }
1296 1
1297 View Code Duplication
    if (!$sql || $sql === '') {
1298 23
      $this->_debug->displayError('Can not execute an empty query.', false);
1299 1
1300 1
      return false;
1301
    }
1302 23
1303 1
    if (
1304 1
        $params !== false
1305
        &&
1306 23
        is_array($params)
1307 1
        &&
1308 1
        count($params) > 0
1309
    ) {
1310 23
      $sql = $this->_parseQueryParams($sql, $params);
1311 1
    }
1312 1
1313
    $query_start_time = microtime(true);
1314 23
    $query_result = \mysqli_real_query($this->link, $sql);
1315 1
    $query_duration = microtime(true) - $query_start_time;
1316 1
1317
    $this->query_count++;
1318 23
1319 2
    $mysqli_field_count = \mysqli_field_count($this->link);
1320 2
    if ($mysqli_field_count) {
1321
      $result = \mysqli_store_result($this->link);
1322 23
    } else {
1323 2
      $result = $query_result;
1324 2
    }
1325
1326 23
    if ($result instanceof \mysqli_result) {
1327 4
1328 4
      // log the select query
1329
      $this->_debug->logQuery($sql, $query_duration, $mysqli_field_count);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() 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...
1330 23
1331 1
      // return query result object
1332 1
      return new Result($sql, $result);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Result::__construct() 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...
1333
    }
1334 23
1335 4
    if ($query_result === true) {
1336 4
1337
      // "INSERT" || "REPLACE"
1338 23 View Code Duplication
      if (preg_match('/^\s*?(?:INSERT|REPLACE)\s+/i', $sql)) {
1339 1
        $insert_id = (int)$this->insert_id();
1340 1
        $this->_debug->logQuery($sql, $query_duration, $insert_id);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() 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...
1341
1342 23
        return $insert_id;
1343 1
      }
1344 1
1345
      // "UPDATE" || "DELETE"
1346 23 View Code Duplication
      if (preg_match('/^\s*?(?:UPDATE|DELETE)\s+/i', $sql)) {
1347 2
        $affected_rows = (int)$this->affected_rows();
1348 2
        $this->_debug->logQuery($sql, $query_duration, $affected_rows);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() 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...
1349
1350 23
        return $affected_rows;
1351 1
      }
1352 1
1353
      // log the ? query
1354 23
      $this->_debug->logQuery($sql, $query_duration, 0);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() 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...
1355 2
1356 2
      return true;
1357 2
    }
1358
1359 2
    // log the error query
1360 1
    $this->_debug->logQuery($sql, $query_duration, 0, true);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\Debug::logQuery() 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...
1361 2
1362 1
    return $this->queryErrorHandling(\mysqli_error($this->link), \mysqli_errno($this->link), $sql, $params);
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->_parseQueryParams($sql, $params) on line 1310 can also be of type array<integer,string>; however, voku\db\DB::queryErrorHandling() 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...
1363 1
  }
1364
1365 2
  /**
1366 23
   * Error-handling for the sql-query.
1367
   *
1368
   * @param string     $errorMessage
1369 23
   * @param int        $errorNumber
1370 23
   * @param string     $sql
1371 23
   * @param array|bool $sqlParams <p>false if there wasn't any parameter</p>
1372
   * @param bool       $sqlMultiQuery
1373 23
   *
1374 23
   * @throws QueryException
1375 23
   * @throws DBGoneAwayException
1376 23
   *
1377
   * @return bool
1378 23
   */
1379 23
  private function queryErrorHandling($errorMessage, $errorNumber, $sql, $sqlParams = false, $sqlMultiQuery = false)
1380 23
  {
1381
    $errorNumber = (int)$errorNumber;
1382 23
1383 23
    if (
1384 23
        $errorMessage === 'DB server has gone away'
1385
        ||
1386 23
        $errorMessage === 'MySQL server has gone away'
1387 23
        ||
1388 23
        $errorNumber === 2006
1389
    ) {
1390 23
      static $RECONNECT_COUNTER;
1391 23
1392
      // exit if we have more then 3 "DB server has gone away"-errors
1393 23
      if ($RECONNECT_COUNTER > 3) {
1394
        $this->_debug->mailToAdmin('DB-Fatal-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql, 5);
1395 23
        throw new DBGoneAwayException($errorMessage);
1396
      }
1397 23
1398 1
      $this->_debug->mailToAdmin('DB-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1399 23
1400 23
      // reconnect
1401 23
      $RECONNECT_COUNTER++;
1402
      $this->reconnect(true);
1403 23
1404 1
      // re-run the current (non multi) query
1405 1
      if ($sqlMultiQuery === false) {
1406
        return $this->query($sql, $sqlParams);
1407 23
      }
1408 23
1409 23
      return false;
1410
    }
1411 23
1412 2
    $this->_debug->mailToAdmin('SQL-Error', $errorMessage . '(' . $errorNumber. ') ' . ":\n<br />" . $sql);
1413 2
1414
    // this query returned an error, we must display it (only for dev) !!!
1415 23
    $this->_debug->displayError($errorMessage . '(' . $errorNumber. ') ' . ' | ' . $sql);
1416 23
1417
    return false;
1418 23
  }
1419
1420
  /**
1421
   * Quote && Escape e.g. a table name string.
1422
   *
1423
   * @param string $str
1424
   *
1425
   * @return string
1426
   */
1427
  public function quote_string($str)
1428 26
  {
1429
    $str = str_replace(
1430 26
        '`',
1431 26
        '``',
1432 26
        trim(
1433 26
            $this->escape($str, false),
1434 26
            '`'
1435
        )
1436 26
    );
1437 26
1438
    return '`' . $str . '`';
1439 26
  }
1440
1441
  /**
1442
   * Reconnect to the MySQL-Server.
1443
   *
1444
   * @param bool $checkViaPing
1445
   *
1446
   * @return bool
1447 1
   */
1448
  public function reconnect($checkViaPing = false)
1449 1
  {
1450
    $ping = false;
1451
1452
    if ($checkViaPing === true) {
1453
      $ping = $this->ping();
1454
    }
1455
1456
    if ($ping !== true) {
1457
      $this->connected = false;
1458
      $this->connect();
1459
    }
1460
1461
    return $this->isReady();
1462
  }
1463 1
1464
  /**
1465
   * Execute a "replace"-query.
1466 1
   *
1467
   * @param string      $table
1468 1
   * @param array       $data
1469 1
   * @param null|string $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1470
   *
1471 1
   * @return false|int <p>false on error</p>
1472
   *
1473
   * @throws QueryException
1474 1
   */
1475 1
  public function replace($table, array $data = array(), $databaseName = null)
1476
  {
1477 1
    // init
1478
    $table = trim($table);
1479
1480
    if ($table === '') {
1481 1
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1482 1
1483
      return false;
1484 1
    }
1485 1
1486
    if (count($data) === 0) {
1487 1
      $this->_debug->displayError('Invalid data for REPLACE, data is empty.', false);
1488
1489
      return false;
1490 1
    }
1491
1492 1
    // extracting column names
1493 1
    $columns = array_keys($data);
1494 1
    foreach ($columns as $k => $_key) {
1495
      /** @noinspection AlterInForeachInspection */
1496 1
      $columns[$k] = $this->quote_string($_key);
1497
    }
1498
1499
    $columns = implode(',', $columns);
1500 1
1501
    // extracting values
1502 1
    foreach ($data as $k => $_value) {
1503
      /** @noinspection AlterInForeachInspection */
1504
      $data[$k] = $this->secure($_value);
1505
    }
1506
    $values = implode(',', $data);
1507
1508
    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...
1509
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1510
    }
1511
1512
    $sql = 'REPLACE INTO ' . $databaseName . $this->quote_string($table) . " ($columns) VALUES ($values);";
1513
1514
    return $this->query($sql);
1515
  }
1516
1517 6
  /**
1518
   * Rollback in a transaction and end the transaction.
1519
   *
1520 6
   * @return bool <p>Boolean true on success, false otherwise.</p>
1521
   */
1522 6
  public function rollback()
1523 1
  {
1524
    if ($this->_in_transaction === false) {
1525 1
      return false;
1526
    }
1527
1528 6
    $return = \mysqli_rollback($this->link);
1529 2
    \mysqli_autocommit($this->link, true);
1530
    $this->_in_transaction = false;
1531 2
1532
    return $return;
1533
  }
1534 6
1535
  /**
1536 6
   * Commits the current transaction and end the transaction.
1537 2
   *
1538 6
   * @return bool <p>Boolean true on success, false otherwise.</p>
1539 4
   */
1540 4
  public function commit()
1541 1
  {
1542
    if ($this->_in_transaction === false) {
1543
      return false;
1544 6
    }
1545
1546
    if (mysqli_commit($this->link)) {
1547
      $this->_in_transaction = false;
1548 6
      return true;
1549
    }
1550 6
1551
    return false;
1552
  }
1553
1554
  /**
1555
   * Execute a callback inside a transaction.
1556
   *
1557
   * @param callback $callback The callback to run inside the transaction
1558
   *
1559
   * @return bool Boolean true on success, false otherwise
1560
   */
1561
  public function transact($callback)
1562
  {
1563
    try {
1564 2
      $this->beginTransaction();
1565
      call_user_func($callback, $this);
1566
      return $this->commit();
1567 2
    } catch (\Exception $e) {
1568
      $this->rollback();
1569 2
      return false;
1570 1
    }
1571
  }
1572 1
1573
  /**
1574
   * Try to secure a variable, so can you use it in sql-queries.
1575 2
   *
1576 1
   * <p>
1577 2
   * <strong>int:</strong> (also strings that contains only an int-value)<br />
1578 2
   * 1. parse into "int"
1579 2
   * </p><br />
1580 1
   *
1581
   * <p>
1582
   * <strong>float:</strong><br />
1583 2
   * 1. return "float"
1584
   * </p><br />
1585
   *
1586
   * <p>
1587 2
   * <strong>string:</strong><br />
1588
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'<br />
1589 2
   * 2. trim whitespace<br />
1590
   * 3. trim '<br />
1591
   * 4. escape the string (and remove non utf-8 chars)<br />
1592
   * 5. trim ' again (because we maybe removed some chars)<br />
1593
   * 6. add ' around the new string<br />
1594
   * </p><br />
1595
   *
1596
   * <p>
1597
   * <strong>array:</strong><br />
1598
   * 1. return null
1599
   * </p><br />
1600
   *
1601
   * <p>
1602
   * <strong>object:</strong><br />
1603 20
   * 1. return false
1604
   * </p><br />
1605
   *
1606 20
   * <p>
1607
   * <strong>null:</strong><br />
1608 20
   * 1. return null
1609 1
   * </p>
1610
   *
1611 1
   * @param mixed $var
1612
   *
1613
   * @return mixed
1614 20
   */
1615 5
  public function secure($var)
1616 20
  {
1617 16
    if (
1618 16
        $var === ''
1619 1
        ||
1620
        ($this->_convert_null_to_empty_string === true && $var === null)
1621
    ) {
1622 20
      return "''";
1623
    }
1624
1625
    if (in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
1626 20
      return $var;
1627
    }
1628 20
1629
    if (is_string($var)) {
1630
      $var = trim(trim($var), "'");
1631
    }
1632
1633
    $var = $this->escape($var, false, false, null);
1634
1635
    if (is_string($var)) {
1636 1
      $var = "'" . trim($var, "'") . "'";
1637
    }
1638 1
1639
    return $var;
1640 1
  }
1641
1642
  /**
1643
   * Execute a "select"-query.
1644
   *
1645
   * @param string       $table
1646 9
   * @param string|array $where
1647
   * @param string|null  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1648 9
   *
1649
   * @return false|Result <p>false on error</p>
1650
   *
1651
   * @throws QueryException
1652
   */
1653 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...
1654
  {
1655
    // init
1656
    $table = trim($table);
1657
1658
    if ($table === '') {
1659
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1660
1661
      return false;
1662
    }
1663
1664
    if (is_string($where)) {
1665
      $WHERE = $this->escape($where, false);
1666 2
    } elseif (is_array($where)) {
1667
      $WHERE = $this->_parseArrayPair($where, 'AND');
1668 2
    } else {
1669
      $WHERE = '';
1670 2
    }
1671 2
1672 2
    if ($databaseName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1673 2
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1674
    }
1675
1676
    $sql = 'SELECT * FROM ' . $databaseName . $this->quote_string($table) . " WHERE ($WHERE);";
1677
1678
    return $this->query($sql);
1679
  }
1680
1681
  /**
1682
   * Set the current charset.
1683
   *
1684
   * @param string $charset
1685
   *
1686
   * @return bool
1687
   */
1688
  public function set_charset($charset)
1689 2
  {
1690
    $charsetLower = strtolower($charset);
1691 2
    if ($charsetLower === 'utf8' || $charsetLower === 'utf-8') {
1692 2
      $charset = 'utf8';
1693
    }
1694
    if ($charset === 'utf8' && Helper::isUtf8mb4Supported($this) === true) {
1695
      $charset = 'utf8mb4';
1696
    }
1697
1698
    $this->charset = (string)$charset;
1699
1700
    $return = mysqli_set_charset($this->link, $charset);
1701
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1702
    /** @noinspection UsageOfSilenceOperatorInspection */
1703
    @\mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
1704
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1705
    /** @noinspection UsageOfSilenceOperatorInspection */
1706
    @\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...
1707
1708
    return $return;
1709
  }
1710
1711
  /**
1712
   * Set the option to convert null to "''" (empty string).
1713
   *
1714
   * Used in secure() => select(), insert(), update(), delete()
1715
   *
1716
   * @param $bool
1717
   */
1718
  public function set_convert_null_to_empty_string($bool)
1719
  {
1720
    $this->_convert_null_to_empty_string = (bool)$bool;
1721
  }
1722
1723
  /**
1724
   * Enables or disables internal report functions
1725
   *
1726
   * @link http://php.net/manual/en/function.mysqli-report.php
1727
   *
1728
   * @param int $flags <p>
1729
   *                   <table>
1730
   *                   Supported flags
1731
   *                   <tr valign="top">
1732
   *                   <td>Name</td>
1733
   *                   <td>Description</td>
1734
   *                   </tr>
1735
   *                   <tr valign="top">
1736
   *                   <td><b>MYSQLI_REPORT_OFF</b></td>
1737
   *                   <td>Turns reporting off</td>
1738
   *                   </tr>
1739
   *                   <tr valign="top">
1740
   *                   <td><b>MYSQLI_REPORT_ERROR</b></td>
1741
   *                   <td>Report errors from mysqli function calls</td>
1742
   *                   </tr>
1743
   *                   <tr valign="top">
1744
   *                   <td><b>MYSQLI_REPORT_STRICT</b></td>
1745
   *                   <td>
1746
   *                   Throw <b>mysqli_sql_exception</b> for errors
1747
   *                   instead of warnings
1748
   *                   </td>
1749
   *                   </tr>
1750
   *                   <tr valign="top">
1751
   *                   <td><b>MYSQLI_REPORT_INDEX</b></td>
1752
   *                   <td>Report if no index or bad index was used in a query</td>
1753
   *                   </tr>
1754
   *                   <tr valign="top">
1755
   *                   <td><b>MYSQLI_REPORT_ALL</b></td>
1756
   *                   <td>Set all options (report all)</td>
1757
   *                   </tr>
1758
   *                   </table>
1759
   *                   </p>
1760
   *
1761
   * @return bool
1762
   */
1763
  public function set_mysqli_report($flags)
1764
  {
1765
    return \mysqli_report($flags);
1766
  }
1767
1768
  /**
1769
   * Show config errors by throw exceptions.
1770
   *
1771
   * @return bool
1772
   *
1773
   * @throws \InvalidArgumentException
1774
   */
1775
  public function showConfigError()
1776
  {
1777
1778
    if (
1779
        !$this->hostname
1780
        ||
1781
        !$this->username
1782
        ||
1783
        !$this->database
1784
    ) {
1785
1786
      if (!$this->hostname) {
1787
        throw new \InvalidArgumentException('no-sql-hostname');
1788
      }
1789
1790
      if (!$this->username) {
1791
        throw new \InvalidArgumentException('no-sql-username');
1792
      }
1793
1794
      if (!$this->database) {
1795
        throw new \InvalidArgumentException('no-sql-database');
1796
      }
1797
1798
      return false;
1799
    }
1800
1801
    return true;
1802
  }
1803
1804
  /**
1805
   * alias: "beginTransaction()"
1806
   */
1807
  public function startTransaction()
1808
  {
1809
    $this->beginTransaction();
1810
  }
1811
1812
  /**
1813
   * Execute a "update"-query.
1814
   *
1815
   * @param string       $table
1816
   * @param array        $data
1817
   * @param array|string $where
1818
   * @param null|string  $databaseName <p>Use <strong>null</strong> if you will use the current database.</p>
1819
   *
1820
   * @return false|int <p>false on error</p>
1821
   *
1822
   * @throws QueryException
1823
   */
1824
  public function update($table, array $data = array(), $where = '1=1', $databaseName = null)
1825
  {
1826
    // init
1827
    $table = trim($table);
1828
1829
    if ($table === '') {
1830
      $this->_debug->displayError('Invalid table name, table name in empty.', false);
1831
1832
      return false;
1833
    }
1834
1835
    if (count($data) === 0) {
1836
      $this->_debug->displayError('Invalid data for UPDATE, data is empty.', false);
1837
1838
      return false;
1839
    }
1840
1841
    $SET = $this->_parseArrayPair($data);
1842
1843
    if (is_string($where)) {
1844
      $WHERE = $this->escape($where, false);
1845
    } elseif (is_array($where)) {
1846
      $WHERE = $this->_parseArrayPair($where, 'AND');
1847
    } else {
1848
      $WHERE = '';
1849
    }
1850
1851
    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...
1852
      $databaseName = $this->quote_string(trim($databaseName)) . '.';
1853
    }
1854
1855
    $sql = 'UPDATE ' . $databaseName . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1856
1857
    return $this->query($sql);
1858
  }
1859
1860
  /**
1861
   * @param null|string $sql
1862
   * @param array $bindings
1863
   *
1864
   * @return bool|int|Result|DB           <p>
1865
   *                                      "DB" by "$sql" === null<br />
1866
   *                                      "Result" by "<b>SELECT</b>"-queries<br />
1867
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
1868
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
1869
   *                                      "true" by e.g. "DROP"-queries<br />
1870
   *                                      "false" on error
1871
   *                                      </p>
1872
   */
1873
  public function __invoke($sql = null, array $bindings = array())
1874
  {
1875
    return isset($sql) ? $this->query($sql, $bindings) : $this;
1876
  }
1877
1878
}
1879