Completed
Push — master ( c0748e...3e48cd )
by Lars
02:36
created

DB::quote_string()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace voku\db;
4
5
use voku\cache\Cache;
6
use voku\helper\UTF8;
7
8
/**
9
 * DB: this handles DB queries via MySQLi
10
 *
11
 * @package   voku\db
12
 */
13
final class DB
14
{
15
16
  /**
17
   * @var int
18
   */
19
  public $query_count = 0;
20
21
  /**
22
   * @var \mysqli
23
   */
24
  private $link = false;
25
26
  /**
27
   * @var bool
28
   */
29
  private $connected = false;
30
31
  /**
32
   * @var array
33
   */
34
  private $mysqlDefaultTimeFunctions;
35
36
  /**
37
   * @var string
38
   */
39
  private $hostname = '';
40
41
  /**
42
   * @var string
43
   */
44
  private $username = '';
45
46
  /**
47
   * @var string
48
   */
49
  private $password = '';
50
51
  /**
52
   * @var string
53
   */
54
  private $database = '';
55
56
  /**
57
   * @var int
58
   */
59
  private $port = 3306;
60
61
  /**
62
   * @var string
63
   */
64
  private $charset = 'utf8';
65
66
  /**
67
   * @var string
68
   */
69
  private $socket = '';
70
71
  /**
72
   * @var bool
73
   */
74
  private $session_to_db = false;
75
76
  /**
77
   * @var bool
78
   */
79
  private $_in_transaction = false;
80
81
  /**
82
   * @var Debug
83
   */
84
  private $_debug;
85
86
  /**
87
   * __construct()
88
   *
89
   * @param string         $hostname
90
   * @param string         $username
91
   * @param string         $password
92
   * @param string         $database
93
   * @param int            $port
94
   * @param string         $charset
95
   * @param boolean|string $exit_on_error use a empty string "" or false to disable it
96
   * @param boolean|string $echo_on_error use a empty string "" or false to disable it
97
   * @param string         $logger_class_name
98
   * @param string         $logger_level  'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'
99
   * @param boolean|string $session_to_db use a empty string "" or false to disable it
100
   */
101 10
  protected function __construct($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $session_to_db)
102
  {
103 10
    $this->connected = false;
104
105 10
    $this->_debug = new Debug($this);
106
107 10
    $this->_loadConfig(
108 10
        $hostname,
109 10
        $username,
110 10
        $password,
111 10
        $database,
112 10
        $port,
113 10
        $charset,
114 10
        $exit_on_error,
115 10
        $echo_on_error,
116 10
        $logger_class_name,
117 10
        $logger_level,
118
        $session_to_db
119 10
    );
120
121 7
    $this->connect();
122
123 4
    $this->mysqlDefaultTimeFunctions = array(
124
      // Returns the current date.
125 4
      'CURDATE()',
126
      // CURRENT_DATE	| Synonyms for CURDATE()
127 4
      'CURRENT_DATE()',
128
      // CURRENT_TIME	| Synonyms for CURTIME()
129 4
      'CURRENT_TIME()',
130
      // CURRENT_TIMESTAMP | Synonyms for NOW()
131 4
      'CURRENT_TIMESTAMP()',
132
      // Returns the current time.
133 4
      'CURTIME()',
134
      // Synonym for NOW()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
135 4
      'LOCALTIME()',
136
      // Synonym for NOW()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
137 4
      'LOCALTIMESTAMP()',
138
      // Returns the current date and time.
139 4
      'NOW()',
140
      // Returns the time at which the function executes.
141 4
      'SYSDATE()',
142
      // Returns a UNIX timestamp.
143 4
      'UNIX_TIMESTAMP()',
144
      // Returns the current UTC date.
145 4
      'UTC_DATE()',
146
      // Returns the current UTC time.
147 4
      'UTC_TIME()',
148
      // Returns the current UTC date and time.
149 4
      'UTC_TIMESTAMP()',
150
    );
151 4
  }
152
153
  /**
154
   * Load the config from the constructor.
155
   *
156
   * @param string         $hostname
157
   * @param string         $username
158
   * @param string         $password
159
   * @param string         $database
160
   * @param int            $port
161
   * @param string         $charset
162
   * @param boolean|string $exit_on_error use a empty string "" or false to disable it
163
   * @param boolean|string $echo_on_error use a empty string "" or false to disable it
164
   * @param string         $logger_class_name
165
   * @param string         $logger_level
166
   * @param boolean|string $session_to_db use a empty string "" or false to disable it
167
   *
168
   * @return bool
169
   */
170 10
  private function _loadConfig($hostname, $username, $password, $database, $port, $charset, $exit_on_error, $echo_on_error, $logger_class_name, $logger_level, $session_to_db)
171
  {
172 10
    $this->hostname = (string)$hostname;
173 10
    $this->username = (string)$username;
174 10
    $this->password = (string)$password;
175 10
    $this->database = (string)$database;
176
177 10
    if ($charset) {
178 4
      $this->charset = (string)$charset;
179 4
    }
180
181 10
    if ($port) {
182 4
      $this->port = (int)$port;
183 4
    } else {
184
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
185 7
      $this->port = @ini_get('mysqli.default_port');
186
    }
187
188 10
    if (!$this->socket) {
189
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
190 10
      $this->socket = @ini_get('mysqli.default_socket');
191 10
    }
192
193 10
    if ($exit_on_error === true || $exit_on_error === false) {
194 10
      $this->_debug->setExitOnError($exit_on_error);
195 10
    }
196
197 10
    if ($echo_on_error === true || $echo_on_error === false) {
198 10
      $this->_debug->setEchoOnError($echo_on_error);
199 10
    }
200
201 10
    $this->_debug->setLoggerClassName($logger_class_name);
202 10
    $this->_debug->setLoggerLevel($logger_level);
203
204 10
    $this->session_to_db = (boolean)$session_to_db;
205
206 10
    return $this->showConfigError();
207
  }
208
209
  /**
210
   * Show config errors by throw exceptions.
211
   *
212
   * @return bool
213
   *
214
   * @throws \Exception
215
   */
216 10
  public function showConfigError()
217
  {
218
219
    if (
220 10
        !$this->hostname
221 10
        ||
222 9
        !$this->username
223 9
        ||
224 8
        !$this->database
225 10
    ) {
226
227 3
      if (!$this->hostname) {
228 1
        throw new \Exception('no-sql-hostname');
229
      }
230
231 2
      if (!$this->username) {
232 1
        throw new \Exception('no-sql-username');
233
      }
234
235 1
      if (!$this->database) {
236 1
        throw new \Exception('no-sql-database');
237
      }
238
239
      return false;
240
    }
241
242 7
    return true;
243
  }
244
245
  /**
246
   * Open a new connection to the MySQL server.
247
   *
248
   * @return boolean
249
   */
250 8
  public function connect()
251
  {
252 8
    if ($this->isReady()) {
253 1
      return true;
254
    }
255
256 8
    mysqli_report(MYSQLI_REPORT_STRICT);
257
    try {
258 8
      $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...
259
260 8
      mysqli_options($this->link, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
261
262
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
263 8
      $this->connected = @mysqli_real_connect(
264 8
          $this->link,
265 8
          $this->hostname,
266 8
          $this->username,
267 8
          $this->password,
268 8
          $this->database,
269 8
          $this->port,
270 8
          $this->socket
271 8
      );
272 8
    } catch (\Exception $e) {
273 3
      $this->_debug->displayError('Error connecting to mysql server: ' . $e->getMessage(), true);
274
    }
275 5
    mysqli_report(MYSQLI_REPORT_OFF);
276
277 5
    if (!$this->connected) {
278
      $this->_debug->displayError('Error connecting to mysql server: ' . mysqli_connect_error(), true);
279
    } else {
280 5
      $this->set_charset($this->charset);
281
    }
282
283 5
    return $this->isReady();
284
  }
285
286
  /**
287
   * Check if db-connection is ready.
288
   *
289
   * @return boolean
290
   */
291 36
  public function isReady()
292
  {
293 36
    return $this->connected ? true : false;
294
  }
295
296
  /**
297
   * Get a new "Prepare"-Object for your sql-query.
298
   *
299
   * @param string $query
300
   *
301
   * @return Prepare
302
   */
303
  public function prepare($query)
304
  {
305
    return new Prepare($this, $query);
306
  }
307
308
  /**
309
   * Execute a sql-query and return the result-array for select-statements.
310
   *
311
   * @param $query
312
   *
313
   * @return mixed
314
   * @deprecated
315
   * @throws \Exception
316
   */
317
  public static function qry($query)
318
  {
319
    $db = self::getInstance();
320
321
    $args = func_get_args();
322
    $query = array_shift($args);
323
    $query = str_replace('?', '%s', $query);
324
    $args = array_map(
325
        array(
326
            $db,
327
            'escape',
328
        ),
329
        $args
330
    );
331
    array_unshift($args, $query);
332
    $query = call_user_func_array('sprintf', $args);
333
    $result = $db->query($query);
334
335
    if ($result instanceof Result) {
336
      $return = $result->fetchAllArray();
337
    } else {
338
      $return = $result;
339
    }
340
341
    if ($return || is_array($return)) {
342
      return $return;
343
    } else {
344
      return false;
345
    }
346
  }
347
348
  /**
349
   * getInstance()
350
   *
351
   * @param string      $hostname
352
   * @param string      $username
353
   * @param string      $password
354
   * @param string      $database
355
   * @param string      $port          default is (int)3306
356
   * @param string      $charset       default is 'utf8', but if you need 4-byte chars, then your tables need
357
   *                                   the 'utf8mb4'-charset
358
   * @param bool|string $exit_on_error use a empty string "" or false to disable it
359
   * @param bool|string $echo_on_error use a empty string "" or false to disable it
360
   * @param string      $logger_class_name
361
   * @param string      $logger_level
362
   * @param bool|string $session_to_db use a empty string "" or false to disable it
363
   *
364
   * @return \voku\db\DB
365
   */
366 46
  public static function getInstance($hostname = '', $username = '', $password = '', $database = '', $port = '', $charset = '', $exit_on_error = '', $echo_on_error = '', $logger_class_name = '', $logger_level = '', $session_to_db = '')
367
  {
368
    /**
369
     * @var $instance DB[]
370
     */
371 46
    static $instance = array();
372
373
    /**
374
     * @var $firstInstance DB
375
     */
376 46
    static $firstInstance = null;
377
378
    if (
379 46
        $hostname . $username . $password . $database . $port . $charset == ''
380 46
        &&
381 9
        null !== $firstInstance
382 46
    ) {
383 9
      return $firstInstance;
384
    }
385
386 46
    $connection = md5(
387 46
        $hostname . $username . $password . $database . $port . $charset . (int)$exit_on_error . (int)$echo_on_error . $logger_class_name . $logger_level . (int)$session_to_db
388 46
    );
389
390 46
    if (!isset($instance[$connection])) {
391 10
      $instance[$connection] = new self(
392 10
          $hostname,
393 10
          $username,
394 10
          $password,
395 10
          $database,
396 10
          $port,
397 10
          $charset,
398 10
          $exit_on_error,
399 10
          $echo_on_error,
400 10
          $logger_class_name,
401 10
          $logger_level,
402
          $session_to_db
403 10
      );
404
405 4
      if (null === $firstInstance) {
406 1
        $firstInstance = $instance[$connection];
407 1
      }
408 4
    }
409
410 46
    return $instance[$connection];
411
  }
412
413
  /**
414
   * Execute a sql-query.
415
   *
416
   * @param string        $sql            sql-query
417
   *
418
   * @param array|boolean $params         "array" of sql-query-parameters
419
   *                                      "false" if you don't need any parameter (default)
420
   *
421
   * @return bool|int|Result              "Result" by "<b>SELECT</b>"-queries<br />
422
   *                                      "int" (insert_id) by "<b>INSERT / REPLACE</b>"-queries<br />
423
   *                                      "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
424
   *                                      "true" by e.g. "DROP"-queries<br />
425
   *                                      "false" on error
426
   *
427
   * @throws \Exception
428
   */
429 27
  public function query($sql = '', $params = false)
430
  {
431 27
    if (!$this->isReady()) {
432
      return false;
433
    }
434
435 27 View Code Duplication
    if (!$sql || $sql === '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
436 4
      $this->_debug->displayError('Can\'t execute an empty Query', false);
437
438 4
      return false;
439
    }
440
441
    if (
442
        $params !== false
443 25
        &&
444 2
        is_array($params)
445 25
        &&
446 2
        count($params) > 0
447 25
    ) {
448 2
      $sql = $this->_parseQueryParams($sql, $params);
449 2
    }
450
451 25
    $query_start_time = microtime(true);
452 25
    $result = mysqli_query($this->link, $sql);
453 25
    $query_duration = microtime(true) - $query_start_time;
454 25
    $this->query_count++;
455
456 25
    $resultCount = 0;
457 25
    if ($result instanceof \mysqli_result) {
458 22
      $resultCount = (int)$result->num_rows;
459 22
    }
460 25
    $this->_debug->logQuery($sql, $query_duration, $resultCount);
461
462 25
    if ($result instanceof \mysqli_result) {
463
464
      // return query result object
465 22
      return new Result($sql, $result);
466
467 18
    } else if ($result === true) {
468
469 16
      if (preg_match('/^\s*"?(INSERT|UPDATE|DELETE|REPLACE)\s+/i', $sql)) {
470
471
        // it is an "INSERT" || "REPLACE"
472 16
        if ($this->insert_id() > 0) {
473 15
          return (int)$this->insert_id();
474
        }
475
476
        // it is an "UPDATE" || "DELETE"
477 7
        if ($this->affected_rows() > 0) {
478 7
          return (int)$this->affected_rows();
479
        }
480
      }
481
482
      return true;
483
    }
484
485 8
    return $this->queryErrorHandling(mysqli_error($this->link), $sql, $params);
486
  }
487
488
  /**
489
   * _parseQueryParams
490
   *
491
   * @param string $sql
492
   * @param array  $params
493
   *
494
   * @return string
495
   */
496 2
  private function _parseQueryParams($sql, array $params)
497
  {
498
    // is there anything to parse?
499 2
    if (strpos($sql, '?') === false) {
500
      return $sql;
501
    }
502
503 2
    if (count($params) > 0) {
504 2
      $parseKey = md5(uniqid(mt_rand(), true));
505 2
      $sql = str_replace('?', $parseKey, $sql);
506
507 2
      $k = 0;
508 2
      while (strpos($sql, $parseKey) !== false) {
509 2
        $value = $this->secure($params[$k]);
510 2
        $sql = preg_replace("/$parseKey/", $value, $sql, 1);
511 2
        $k++;
512 2
      }
513 2
    }
514
515 2
    return $sql;
516
  }
517
518
  /**
519
   * Try to secure a variable, so can you use it in sql-queries.
520
   *
521
   * int: (also strings that contains only an int-value)
522
   * 1. parse into (int)
523
   *
524
   * strings:
525
   * 1. check if the string isn't a default mysql-time-function e.g. 'CURDATE()'
526
   * 2. trim whitespace
527
   * 3. trim '
528
   * 4. escape the string (and remove non utf-8 chars)
529
   * 5. trim ' again (because we maybe removed some chars)
530
   * 6. add ' around the new string
531
   *
532
   * @param mixed $var
533
   *
534
   * @return string | null
535
   */
536 18
  public function secure($var)
537
  {
538
    // save the current value as int (for later usage)
539 18
    if (!is_object($var)) {
540 18
      $varInt = (int)$var;
541 18
    }
542
543
    /** @noinspection TypeUnsafeComparisonInspection */
544
    if (
545 18
        is_int($var)
546
        ||
547 15
        is_bool($var)
548 15
        ||
549 15
        (isset($varInt, $var[0]) && $var[0] != '0' && "$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...
550 18
    ) {
551
552
      // "int" || int || bool
553
554 16
      $var = (int)$var;
555
556 18
    } elseif (is_string($var)) {
557
558
      // "string"
559
560 15
      if (!in_array($var, $this->mysqlDefaultTimeFunctions, true)) {
561 15
        $var = "'" . trim($this->escape(trim(trim((string)$var), "'")), "'") . "'";
562 15
      }
563
564 15 View Code Duplication
    } elseif (is_float($var)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
565
566
      // float
567
568 2
      $var = number_format((float)str_replace(',', '.', $var), 8, '.', '');
569
570 2
    } elseif (is_array($var)) {
571
572
      // array
573
574 1
      $var = null;
575
576 1
    } elseif ($var instanceof \DateTime) {
577
578
      // "DateTime"-object
579
580
      try {
581 1
        $var = "'" . $this->escape($var->format('Y-m-d H:i:s'), false) . "'";
582 1
      } catch (\Exception $e) {
583
        $var = null;
584
      }
585
586 1
    } else {
587
588
      // fallback ...
589
590
      $var = "'" . trim($this->escape(trim(trim((string)$var), "'")), "'") . "'";
591
592
    }
593
594 18
    return $var;
595
  }
596
597
  /**
598
   * Escape
599
   *
600
   * @param mixed $var boolean: convert into "integer"<br />
601
   *                   int: convert into "integer"<br />
602
   *                   float: convert into "float" and replace "," with "."<br />
603
   *                   array: run escape() for every key => value<br />
604
   *                   string: run UTF8::cleanup() and mysqli_real_escape_string()<br />
605
   * @param bool  $stripe_non_utf8
606
   * @param bool  $html_entity_decode
607
   * @param bool  $array_to_string
608
   *
609
   * @return array|bool|float|int|string
610
   */
611 23
  public function escape($var = '', $stripe_non_utf8 = true, $html_entity_decode = false, $array_to_string = false)
612
  {
613
    // save the current value as int (for later usage)
614 23
    if (!is_object($var)) {
615 23
      $varInt = (int)$var;
616 23
    }
617
618
    /** @noinspection TypeUnsafeComparisonInspection */
619
    if (
620 23
        is_int($var)
621
        ||
622 23
        is_bool($var)
623 23
        ||
624 23
        (isset($varInt, $var[0]) && $var[0] != '0' && "$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...
625 23
    ) {
626
627
      // "int" || int || bool
628
629 5
      return (int)$var;
630
631 23 View Code Duplication
    } elseif (is_float($var)) {
632
633
      // float
634
635 2
      return number_format((float)str_replace(',', '.', $var), 8, '.', '');
636
637 23
    } elseif (is_array($var)) {
638
639
      // array
640
641 1
      $varCleaned = array();
642 1
      foreach ($var as $key => $value) {
643
644 1
        $key = (string)$this->escape($key, $stripe_non_utf8, $html_entity_decode);
645 1
        $value = (string)$this->escape($value, $stripe_non_utf8, $html_entity_decode);
646
647 1
        $varCleaned[$key] = $value;
648 1
      }
649
650 1
      if ($array_to_string === true) {
651 1
        $varCleaned = implode(',', $varCleaned);
652
653 1
        return $varCleaned;
654
      } else {
655 1
        return (array)$varCleaned;
656
      }
657
    }
658
659 23
    if (is_string($var)) {
660
661
      // "string"
662
663 23
      if ($stripe_non_utf8 === true) {
664 21
        $var = UTF8::cleanup($var);
665 21
      }
666
667 23
      if ($html_entity_decode === true) {
668
        // use no-html-entity for db
669 1
        $var = UTF8::html_entity_decode($var);
670 1
      }
671
672 23
      $var = get_magic_quotes_gpc() ? stripslashes($var) : $var;
673
674 23
      $var = mysqli_real_escape_string($this->getLink(), $var);
675
676 23
      return (string)$var;
677
    } else {
678
      return false;
679
    }
680
  }
681
682
  /**
683
   * Get the mysqli-link (link identifier returned by mysqli-connect).
684
   *
685
   * @return \mysqli
686
   */
687 24
  public function getLink()
688
  {
689 24
    return $this->link;
690
  }
691
692
  /**
693
   * Returns the auto generated id used in the last query.
694
   *
695
   * @return int|string
696
   */
697 16
  public function insert_id()
698
  {
699 16
    return mysqli_insert_id($this->link);
700
  }
701
702
  /**
703
   * Gets the number of affected rows in a previous MySQL operation.
704
   *
705
   * @return int
706
   */
707 7
  public function affected_rows()
708
  {
709 7
    return mysqli_affected_rows($this->link);
710
  }
711
712
  /**
713
   * Error-handling for the sql-query.
714
   *
715
   * @param string     $errorMsg
716
   * @param string     $sql
717
   * @param array|bool $sqlParams false if there wasn't any parameter
718
   *
719
   * @throws \Exception
720
   *
721
   * @return bool
722
   */
723 9 View Code Duplication
  protected function queryErrorHandling($errorMsg, $sql, $sqlParams = false)
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...
724
  {
725 9
    if ($errorMsg === 'DB server has gone away' || $errorMsg === 'MySQL server has gone away') {
726 1
      static $reconnectCounter;
727
728
      // exit if we have more then 3 "DB server has gone away"-errors
729 1
      if ($reconnectCounter > 3) {
730
        $this->_debug->mailToAdmin('SQL-Fatal-Error', $errorMsg . ":\n<br />" . $sql, 5);
731
        throw new \Exception($errorMsg);
732
      } else {
733 1
        $this->_debug->mailToAdmin('SQL-Error', $errorMsg . ":\n<br />" . $sql);
734
735
        // reconnect
736 1
        $reconnectCounter++;
737 1
        $this->reconnect(true);
738
739
        // re-run the current query
740 1
        return $this->query($sql, $sqlParams);
741
      }
742
    } else {
743 8
      $this->_debug->mailToAdmin('SQL-Warning', $errorMsg . ":\n<br />" . $sql);
744
745
      // this query returned an error, we must display it (only for dev) !!!
746 8
      $this->_debug->displayError($errorMsg . ' | ' . $sql);
747
    }
748
749 8
    return false;
750
  }
751
752
  /**
753
   * Reconnect to the MySQL-Server.
754
   *
755
   * @param bool $checkViaPing
756
   *
757
   * @return bool
758
   */
759 2
  public function reconnect($checkViaPing = false)
760
  {
761 2
    $ping = false;
762
763 2
    if ($checkViaPing === true) {
764 2
      $ping = $this->ping();
765 2
    }
766
767 2
    if ($ping !== true) {
768 2
      $this->connected = false;
769 2
      $this->connect();
770 2
    }
771
772 2
    return $this->isReady();
773
  }
774
775
  /**
776
   * Pings a server connection, or tries to reconnect
777
   * if the connection has gone down.
778
   *
779
   * @return boolean
780
   */
781 3
  public function ping()
782
  {
783
    if (
784 3
        $this->link
785 3
        &&
786 3
        $this->link instanceof \mysqli
787 3
    ) {
788
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
789 3
      return @mysqli_ping($this->link);
790
    } else {
791
      return false;
792
    }
793
  }
794
795
  /**
796
   * Execute select/insert/update/delete sql-queries.
797
   *
798
   * @param string $query    sql-query
799
   * @param bool   $useCache use cache?
800
   * @param int    $cacheTTL cache-ttl in seconds
801
   *
802
   * @return mixed "array" by "<b>SELECT</b>"-queries<br />
803
   *               "int" (insert_id) by "<b>INSERT</b>"-queries<br />
804
   *               "int" (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
805
   *               "true" by e.g. "DROP"-queries<br />
806
   *               "false" on error
807
   */
808 3
  public static function execSQL($query, $useCache = false, $cacheTTL = 3600)
809
  {
810 3
    $db = self::getInstance();
811
812 3
    if ($useCache === true) {
813 1
      $cache = new Cache(null, null, false, $useCache);
814 1
      $cacheKey = 'sql-' . md5($query);
815
816
      if (
817 1
          $cache->getCacheIsReady() === true
818 1
          &&
819 1
          $cache->existsItem($cacheKey)
820 1
      ) {
821 1
        return $cache->getItem($cacheKey);
822
      }
823
824 1
    } else {
825 3
      $cache = false;
826
    }
827
828 3
    $result = $db->query($query);
829
830 3
    if ($result instanceof Result) {
831
832 1
      $return = $result->fetchAllArray();
833
834
      if (
835 1
          isset($cacheKey)
836 1
          &&
837
          $useCache === true
838 1
          &&
839
          $cache instanceof Cache
840 1
          &&
841 1
          $cache->getCacheIsReady() === true
842 1
      ) {
843 1
        $cache->setItem($cacheKey, $return, $cacheTTL);
844 1
      }
845
846 1
    } else {
847 2
      $return = $result;
848
    }
849
850 3
    return $return;
851
  }
852
853
  /**
854
   * Get the current charset.
855
   *
856
   * @return string
857
   */
858 1
  public function get_charset()
859
  {
860 1
    return $this->charset;
861
  }
862
863
  /**
864
   * Set the current charset.
865
   *
866
   * @param string $charset
867
   *
868
   * @return bool
869
   */
870 6
  public function set_charset($charset)
871
  {
872 6
    $this->charset = (string)$charset;
873
874 6
    $return = mysqli_set_charset($this->link, $charset);
875
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
876 6
    @mysqli_query($this->link, 'SET CHARACTER SET ' . $charset);
877
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
878 6
    @mysqli_query($this->link, "SET NAMES '" . ($charset === 'utf8' ? 'utf8mb4' : $charset) . "'");
879
880 6
    return $return;
881
  }
882
883
  /**
884
   * __wakeup
885
   *
886
   * @return void
887
   */
888 1
  public function __wakeup()
889
  {
890 1
    $this->reconnect();
891 1
  }
892
893
  /**
894
   * Get all table-names via "SHOW TABLES".
895
   *
896
   * @return array
897
   */
898 1
  public function getAllTables()
899
  {
900 1
    $query = 'SHOW TABLES';
901 1
    $result = $this->query($query);
902
903 1
    return $result->fetchAllArray();
904
  }
905
906
  /**
907
   * Execute a sql-multi-query.
908
   *
909
   * @param string $sql
910
   *
911
   * @return false|Result[] "Result"-Array by "<b>SELECT</b>"-queries<br />
912
   *                        "boolean" by only "<b>INSERT</b>"-queries<br />
913
   *                        "boolean" by only (affected_rows) by "<b>UPDATE / DELETE</b>"-queries<br />
914
   *                        "boolean" by only by e.g. "DROP"-queries<br />
915
   *
916
   * @throws \Exception
917
   */
918 1
  public function multi_query($sql)
919
  {
920 1
    if (!$this->isReady()) {
921
      return false;
922
    }
923
924 1 View Code Duplication
    if (!$sql || $sql === '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
925 1
      $this->_debug->displayError('Can\'t execute an empty Query', false);
926
927 1
      return false;
928
    }
929
930 1
    $query_start_time = microtime(true);
931 1
    $resultTmp = mysqli_multi_query($this->link, $sql);
932 1
    $query_duration = microtime(true) - $query_start_time;
933
934 1
    $this->_debug->logQuery($sql, $query_duration, 0);
935
936 1
    $returnTheResult = false;
937 1
    $result = array();
938 1
    if ($resultTmp) {
939
      do {
940 1
        $resultTmpInner = mysqli_store_result($this->link);
941
942 1
        if ($resultTmpInner instanceof \mysqli_result) {
943 1
          $returnTheResult = true;
944 1
          $result[] = new Result($sql, $resultTmpInner);
945 1
        } else {
946 1
          $errorMsg = mysqli_error($this->link);
947
948
          // is the query successful
949 1
          if ($resultTmpInner === true || !$errorMsg) {
950 1
            $result[] = true;
951 1
          } else {
952
            $result[] = $this->queryErrorHandling($errorMsg, $sql);
953
          }
954
        }
955 1
      } while (mysqli_more_results($this->link) === true ? mysqli_next_result($this->link) : false);
956
957 1
    } else {
958
959
      $errorMsg = mysqli_error($this->link);
960
961
      if ($this->_debug->checkForDev() === true) {
962
        echo "Info: maybe you have to increase your 'max_allowed_packet = 30M' in the config: 'my.conf' \n<br />";
963
        echo 'Error:' . $errorMsg;
964
      }
965
966
      $this->_debug->mailToAdmin('SQL-Error in mysqli_multi_query', $errorMsg . ":\n<br />" . $sql);
967
    }
968
969
    // return the result only if there was a "SELECT"-query
970 1
    if ($returnTheResult === true) {
971 1
      return $result;
972
    }
973
974 1
    if (!in_array(false, $result, true)) {
975 1
      return true;
976
    } else {
977
      return false;
978
    }
979
  }
980
981
  /**
982
   * alias: "beginTransaction()"
983
   */
984 1
  public function startTransaction()
985
  {
986 1
    $this->beginTransaction();
987 1
  }
988
989
  /**
990
   * Begins a transaction, by turning off auto commit.
991
   *
992
   * @return boolean this will return true or false indicating success of transaction
993
   */
994 4
  public function beginTransaction()
995
  {
996 4
    $this->clearErrors();
997
998 4
    if ($this->inTransaction() === true) {
999 1
      $this->_debug->displayError('Error mysql server already in transaction!', true);
1000
1001
      return false;
1002 4
    } elseif (mysqli_connect_errno()) {
1003
      $this->_debug->displayError('Error connecting to mysql server: ' . mysqli_connect_error(), true);
1004
1005
      return false;
1006
    } else {
1007 4
      $this->_in_transaction = true;
1008 4
      mysqli_autocommit($this->link, false);
1009
1010 4
      return true;
1011
1012
    }
1013
  }
1014
1015
  /**
1016
   * Clear the errors in "_debug->_errors".
1017
   *
1018
   * @return bool
1019
   */
1020 4
  public function clearErrors()
1021
  {
1022 4
    return $this->_debug->clearErrors();
1023
  }
1024
1025
  /**
1026
   * Check if we are in a transaction.
1027
   *
1028
   * @return boolean
1029
   */
1030 4
  public function inTransaction()
1031
  {
1032 4
    return $this->_in_transaction;
1033
  }
1034
1035
  /**
1036
   * Ends a transaction and commits if no errors, then ends autocommit.
1037
   *
1038
   * @return boolean this will return true or false indicating success of transactions
1039
   */
1040 2
  public function endTransaction()
1041
  {
1042
1043 2
    if (!$this->errors()) {
1044 1
      mysqli_commit($this->link);
1045 1
      $return = true;
1046 1
    } else {
1047 1
      $this->rollback();
1048 1
      $return = false;
1049
    }
1050
1051 2
    mysqli_autocommit($this->link, true);
1052 2
    $this->_in_transaction = false;
1053
1054 2
    return $return;
1055
  }
1056
1057
  /**
1058
   * Get all errors from "$this->_errors".
1059
   *
1060
   * @return array|false false === on errors
1061
   */
1062 2
  public function errors()
1063
  {
1064 2
    $errors = $this->_debug->getErrors();
1065
1066 2
    return count($errors) > 0 ? $errors : false;
1067
  }
1068
1069
  /**
1070
   * Rollback in a transaction.
1071
   */
1072 2
  public function rollback()
1073
  {
1074
    // init
1075 2
    $return = false;
1076
1077 2
    if ($this->_in_transaction === true) {
1078 2
      $return = mysqli_rollback($this->link);
1079 2
      mysqli_autocommit($this->link, true);
1080 2
      $this->_in_transaction = false;
1081 2
    }
1082
1083 2
    return $return;
1084
  }
1085
1086
  /**
1087
   * Execute a "insert"-query.
1088
   *
1089
   * @param string $table
1090
   * @param array  $data
1091
   *
1092
   * @return false|int false on error
1093
   */
1094 15
  public function insert($table, $data = array())
1095
  {
1096 15
    $table = trim($table);
1097
1098 15
    if ($table === '') {
1099 2
      $this->_debug->displayError('invalid table name');
1100
1101 1
      return false;
1102
    }
1103
1104 14
    if (count($data) === 0) {
1105 3
      $this->_debug->displayError('empty data for INSERT');
1106
1107 2
      return false;
1108
    }
1109
1110 12
    $SET = $this->_parseArrayPair($data);
1111
1112 12
    $sql = 'INSERT INTO ' . $this->quote_string($table) . " SET $SET;";
1113
1114 12
    return $this->query($sql);
1115
  }
1116
1117
  /**
1118
   * Parses arrays with value pairs and generates SQL to use in queries.
1119
   *
1120
   * @param array  $arrayPair
1121
   * @param string $glue this is the separator
1122
   *
1123
   * @return string
1124
   */
1125 16
  private function _parseArrayPair($arrayPair, $glue = ',')
1126
  {
1127
    // init
1128 16
    $sql = '';
1129 16
    $pairs = array();
1130
1131
    /** @noinspection IsEmptyFunctionUsageInspection */
1132 16
    if (!empty($arrayPair)) {
1133
1134 16
      foreach ($arrayPair as $_key => $_value) {
1135 16
        $_connector = '=';
1136 16
        $_key_upper = strtoupper($_key);
1137
1138 16
        if (strpos($_key_upper, ' NOT') !== false) {
1139 2
          $_connector = 'NOT';
1140 2
        }
1141
1142 16
        if (strpos($_key_upper, ' IS') !== false) {
1143 1
          $_connector = 'IS';
1144 1
        }
1145
1146 16
        if (strpos($_key_upper, ' IS NOT') !== false) {
1147 1
          $_connector = 'IS NOT';
1148 1
        }
1149
1150 16
        if (strpos($_key_upper, ' IN') !== false) {
1151 1
          $_connector = 'IN';
1152 1
        }
1153
1154 16
        if (strpos($_key_upper, ' NOT IN') !== false) {
1155 1
          $_connector = 'NOT IN';
1156 1
        }
1157
1158 16
        if (strpos($_key_upper, ' BETWEEN') !== false) {
1159 1
          $_connector = 'BETWEEN';
1160 1
        }
1161
1162 16
        if (strpos($_key_upper, ' NOT BETWEEN') !== false) {
1163 1
          $_connector = 'NOT BETWEEN';
1164 1
        }
1165
1166 16
        if (strpos($_key_upper, ' LIKE') !== false) {
1167 2
          $_connector = 'LIKE';
1168 2
        }
1169
1170 16
        if (strpos($_key_upper, ' NOT LIKE') !== false) {
1171 2
          $_connector = 'NOT LIKE';
1172 2
        }
1173
1174 16 View Code Duplication
        if (strpos($_key_upper, ' >') !== false && strpos($_key_upper, ' =') === false) {
1175 2
          $_connector = '>';
1176 2
        }
1177
1178 16 View Code Duplication
        if (strpos($_key_upper, ' <') !== false && strpos($_key_upper, ' =') === false) {
1179 1
          $_connector = '<';
1180 1
        }
1181
1182 16
        if (strpos($_key_upper, ' >=') !== false) {
1183 2
          $_connector = '>=';
1184 2
        }
1185
1186 16
        if (strpos($_key_upper, ' <=') !== false) {
1187 1
          $_connector = '<=';
1188 1
        }
1189
1190 16
        if (strpos($_key_upper, ' <>') !== false) {
1191 1
          $_connector = '<>';
1192 1
        }
1193
1194 16
        if (is_array($_value) === true) {
1195 1
          foreach ($_value as $oldKey => $oldValue) {
1196 1
            $_value[$oldKey] = $this->secure($oldValue);
1197 1
          }
1198
1199 1
          if ($_connector === 'NOT IN' || $_connector === 'IN') {
1200 1
            $_value = '(' . implode(',', $_value) . ')';
1201 1
          } elseif ($_connector === 'NOT BETWEEN' || $_connector === 'BETWEEN') {
1202 1
            $_value = '(' . implode(' AND ', $_value) . ')';
1203 1
          }
1204
1205 1
        } else {
1206 16
          $_value = $this->secure($_value);
1207
        }
1208
1209 16
        $quoteString = $this->quote_string(trim(str_ireplace($_connector, '', $_key)));
1210 16
        $pairs[] = ' ' . $quoteString . ' ' . $_connector . ' ' . $_value . " \n";
1211 16
      }
1212
1213 16
      $sql = implode($glue, $pairs);
1214 16
    }
1215
1216 16
    return $sql;
1217
  }
1218
1219
  /**
1220
   * Quote && Escape e.g. a table name string.
1221
   *
1222
   * @param string $str
1223
   *
1224
   * @return string
1225
   */
1226 18
  public function quote_string($str)
1227
  {
1228 18
    return '`' . $this->escape($str, false) . '`';
1229
  }
1230
1231
  /**
1232
   * Get errors from "$this->_errors".
1233
   *
1234
   * @return array
1235
   */
1236 1
  public function getErrors()
1237
  {
1238 1
    return $this->_debug->getErrors();
1239
  }
1240
1241
  /**
1242
   * Execute a "replace"-query.
1243
   *
1244
   * @param string $table
1245
   * @param array  $data
1246
   *
1247
   * @return false|int false on error
1248
   */
1249 1
  public function replace($table, $data = array())
1250
  {
1251
1252 1
    $table = trim($table);
1253
1254 1
    if ($table === '') {
1255 1
      $this->_debug->displayError('invalid table name');
1256
1257 1
      return false;
1258
    }
1259
1260 1
    if (count($data) === 0) {
1261 1
      $this->_debug->displayError('empty data for REPLACE');
1262
1263 1
      return false;
1264
    }
1265
1266
    // extracting column names
1267 1
    $columns = array_keys($data);
1268 1
    foreach ($columns as $k => $_key) {
1269
      /** @noinspection AlterInForeachInspection */
1270 1
      $columns[$k] = $this->quote_string($_key);
1271 1
    }
1272
1273 1
    $columns = implode(',', $columns);
1274
1275
    // extracting values
1276 1
    foreach ($data as $k => $_value) {
1277
      /** @noinspection AlterInForeachInspection */
1278 1
      $data[$k] = $this->secure($_value);
1279 1
    }
1280 1
    $values = implode(',', $data);
1281
1282 1
    $sql = 'REPLACE INTO ' . $this->quote_string($table) . " ($columns) VALUES ($values);";
1283
1284 1
    return $this->query($sql);
1285
  }
1286
1287
  /**
1288
   * Execute a "update"-query.
1289
   *
1290
   * @param string       $table
1291
   * @param array        $data
1292
   * @param array|string $where
1293
   *
1294
   * @return false|int false on error
1295
   */
1296 6
  public function update($table, $data = array(), $where = '1=1')
1297
  {
1298 6
    $table = trim($table);
1299
1300 6
    if ($table === '') {
1301 1
      $this->_debug->displayError('invalid table name');
1302
1303 1
      return false;
1304
    }
1305
1306 6
    if (count($data) === 0) {
1307 2
      $this->_debug->displayError('empty data for UPDATE');
1308
1309 2
      return false;
1310
    }
1311
1312 6
    $SET = $this->_parseArrayPair($data);
1313
1314 6
    if (is_string($where)) {
1315 2
      $WHERE = $this->escape($where, false);
1316 6
    } elseif (is_array($where)) {
1317 4
      $WHERE = $this->_parseArrayPair($where, 'AND');
1318 4
    } else {
1319 1
      $WHERE = '';
1320
    }
1321
1322 6
    $sql = 'UPDATE ' . $this->quote_string($table) . " SET $SET WHERE ($WHERE);";
1323
1324 6
    return $this->query($sql);
1325
  }
1326
1327
  /**
1328
   * Execute a "delete"-query.
1329
   *
1330
   * @param string       $table
1331
   * @param string|array $where
1332
   *
1333
   * @return false|int false on error
1334
   */
1335 1 View Code Duplication
  public function delete($table, $where)
1336
  {
1337
1338 1
    $table = trim($table);
1339
1340 1
    if ($table === '') {
1341 1
      $this->_debug->displayError('invalid table name');
1342
1343 1
      return false;
1344
    }
1345
1346 1
    if (is_string($where)) {
1347 1
      $WHERE = $this->escape($where, false);
1348 1
    } elseif (is_array($where)) {
1349 1
      $WHERE = $this->_parseArrayPair($where, 'AND');
1350 1
    } else {
1351 1
      $WHERE = '';
1352
    }
1353
1354 1
    $sql = 'DELETE FROM ' . $this->quote_string($table) . " WHERE ($WHERE);";
1355
1356 1
    return $this->query($sql);
1357
  }
1358
1359
  /**
1360
   * Execute a "select"-query.
1361
   *
1362
   * @param string       $table
1363
   * @param string|array $where
1364
   *
1365
   * @return false|Result false on error
1366
   */
1367 17 View Code Duplication
  public function select($table, $where = '1=1')
1368
  {
1369
1370 17
    if ($table === '') {
1371 1
      $this->_debug->displayError('invalid table name');
1372
1373 1
      return false;
1374
    }
1375
1376 17
    if (is_string($where)) {
1377 3
      $WHERE = $this->escape($where, false);
1378 17
    } elseif (is_array($where)) {
1379 15
      $WHERE = $this->_parseArrayPair($where, 'AND');
1380 15
    } else {
1381 1
      $WHERE = '';
1382
    }
1383
1384 17
    $sql = 'SELECT * FROM ' . $this->quote_string($table) . " WHERE ($WHERE);";
1385
1386 17
    return $this->query($sql);
1387
  }
1388
1389
  /**
1390
   * Get the last sql-error.
1391
   *
1392
   * @return string false on error
1393
   */
1394 1
  public function lastError()
1395
  {
1396 1
    $errors = $this->_debug->getErrors();
1397
1398 1
    return count($errors) > 0 ? end($errors) : false;
1399
  }
1400
1401
  /**
1402
   * @return Debug
1403
   */
1404 5
  public function getDebugger()
1405
  {
1406 5
    return $this->_debug;
1407
  }
1408
1409
  /**
1410
   * __destruct
1411
   *
1412
   */
1413
  public function __destruct()
1414
  {
1415
    // close the connection only if we don't save PHP-SESSION's in DB
1416
    if ($this->session_to_db === false) {
1417
      $this->close();
1418
    }
1419
  }
1420
1421
  /**
1422
   * Closes a previously opened database connection.
1423
   */
1424 2
  public function close()
1425
  {
1426 2
    $this->connected = false;
1427
1428 2
    if ($this->link) {
1429 2
      mysqli_close($this->link);
1430 2
    }
1431 2
  }
1432
1433
  /**
1434
   * prevent the instance from being cloned
1435
   *
1436
   * @return void
1437
   */
1438
  private function __clone()
1439
  {
1440
  }
1441
1442
}
1443