Issues (48)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Db/MySqlDb.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Db;
9
10
use PDO;
11
12
/**
13
 * A {@link Db} class for connecting to MySQL.
14
 */
15
class MySqlDb extends Db {
16
    /// Properties ///
17
18
    /**
19
     * @var \PDO
20
     */
21
    protected $pdo;
22
23
    protected $config;
24
25
    /// Methods ///
26
27
    /**
28
     * Initialize an instance of the {@link MySqlDb} class.
29
     *
30
     * @param array $config The database config.
31
     */
32
    public function __construct(array $config = []) {
33
        $this->config = $config;
34
    }
35
36
    /**
37
     * {@inheritdoc}
38
     */
39 6
    public function dropTable($tablename, array $options = []) {
40
        $sql = 'drop table '.
41 6
            (val(Db::OPTION_IGNORE, $options) ? 'if exists ' : '').
42 6
            $this->backtick($this->px.$tablename);
43 6
        $result = $this->query($sql, Db::QUERY_DEFINE);
44 6
        unset($this->tables[strtolower($tablename)]);
45
46 6
        return $result;
47
    }
48
49
    /**
50
     * Surround a field with backticks.
51
     *
52
     * @param string $field The field to backtick.
53
     * @return string Returns the field properly escaped and backticked.
54
     * @link http://www.php.net/manual/en/pdo.quote.php#112169
55
     */
56 62
    protected function backtick($field) {
57 62
        return '`'.str_replace('`', '``', $field).'`';
58
    }
59
60
    /**
61
     * Execute a query on the database.
62
     *
63
     * @param string $sql The sql query to execute.
64
     * @param string $type One of the Db::QUERY_* constants.
65
     *
66
     * Db::QUERY_READ
67
     * : The query reads from the database.
68
     *
69
     * Db::QUERY_WRITE
70
     * : The query writes to the database.
71
     *
72
     * Db::QUERY_DEFINE
73
     * : The query alters the structure of the datbase.
74
     *
75
     * @param array $options Additional options for the query.
76
     *
77
     * Db::OPTION_MODE
78
     * : Override {@link Db::$mode}.
79
     *
80
     * @return array|string|PDOStatement|int Returns the result of the query.
81
     *
82
     * array
83
     * : Returns an array when reading from the database and the mode is {@link Db::MODE_EXEC}.
84
     * string
85
     * : Returns the sql query when the mode is {@link Db::MODE_SQL}.
86
     * PDOStatement
87
     * : Returns a {@link \PDOStatement} when the mode is {@link Db::MODE_PDO}.
88
     * int
89
     * : Returns the number of rows affected when performing an update or an insert.
90
     */
91 58
    public function query($sql, $type = Db::QUERY_READ, $options = []) {
92 58
        $mode = val(Db::OPTION_MODE, $options, $this->mode);
93
94 58
        if ($mode & Db::MODE_ECHO) {
95
            echo trim($sql, "\n;").";\n\n";
96
        }
97 58
        if ($mode & Db::MODE_SQL) {
98
            return $sql;
99
        }
100
101 58
        $result = null;
102 58
        if ($mode & Db::MODE_EXEC) {
103 57
            $result = $this->pdo()->query($sql);
104
105 57
            if ($type == Db::QUERY_READ) {
106 47
                $result->setFetchMode(PDO::FETCH_ASSOC);
107 47
                $result = $result->fetchAll();
108 47
                $this->rowCount = count($result);
109 57
            } elseif (is_object($result) && method_exists($result, 'rowCount')) {
110 56
                $this->rowCount = $result->rowCount();
111 56
                $result = $this->rowCount;
112 56
            }
113 58
        } elseif ($mode & Db::MODE_PDO) {
114
            /* @var \PDOStatement $result */
115 5
            $result = $this->pdo()->prepare($sql);
116 5
        }
117
118 58
        return $result;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 44
    public function getTableDef($tablename) {
125 44
        $table = parent::getTableDef($tablename);
126 44
        if ($table || $table === null) {
127 44
            return $table;
128
        }
129
130 8
        $ltablename = strtolower($tablename);
131 8
        $table = val($ltablename, $this->tables, []);
132 8
        if (!isset($table['columns'])) {
133 8
            $columns = $this->getColumns($tablename);
134 8
            if ($columns === null) {
135
                // A table with no columns does not exist.
136 6
                $this->tables[$ltablename] = ['name' => $tablename];
137 6
                return null;
138
            }
139
140 8
            $table['columns'] = $columns;
141 8
        }
142 8
        if (!isset($table['indexes'])) {
143 8
            $table['indexes'] = $this->getIndexes($tablename);
144 8
        }
145 8
        $table['name'] = $tablename;
146 8
        $this->tables[$ltablename] = $table;
147 8
        return $table;
148
    }
149
150
    /**
151
     * Get the columns for tables and put them in {MySqlDb::$tables}.
152
     *
153
     * @param string $tablename The table to get the columns for or blank for all columns.
154
     * @return array|null Returns an array of columns if {@link $tablename} is specified, or null otherwise.
155
     */
156 5
    protected function getColumns($tablename = '') {
157 5
        $ltablename = strtolower($tablename);
158
        /* @var \PDOStatement $stmt */
159 5
        $stmt = $this->get(
160 5
            'information_schema.COLUMNS',
161
            [
162 5
                'TABLE_SCHEMA' => $this->getDbName(),
163 5
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
164 5
            ],
165
            [
166
                'columns' => [
167 5
                    'TABLE_NAME',
168 5
                    'COLUMN_TYPE',
169 5
                    'IS_NULLABLE',
170 5
                    'EXTRA',
171 5
                    'COLUMN_KEY',
172 5
                    'COLUMN_DEFAULT',
173
                    'COLUMN_NAME'
174 5
                ],
175 5
                Db::OPTION_MODE => Db::MODE_PDO,
176 5
                'escapeTable' => false,
177 5
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
178 5
            ]
179 5
        );
180
181 5
        $stmt->execute();
182 5
        $tablecolumns = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
183
184 5
        foreach ($tablecolumns as $ctablename => $cdefs) {
185 5
            $ctablename = strtolower(ltrim_substr($ctablename, $this->px));
186 5
            $columns = [];
187
188 5
            foreach ($cdefs as $cdef) {
189
                $column = [
190 5
                    'type' => $this->columnTypeString($cdef['COLUMN_TYPE']),
191 5
                    'required' => !force_bool($cdef['IS_NULLABLE']),
192 5
                ];
193 5
                if ($cdef['EXTRA'] === 'auto_increment') {
194 1
                    $column['autoincrement'] = true;
195 1
                }
196 5
                if ($cdef['COLUMN_KEY'] === 'PRI') {
197 3
                    $column['primary'] = true;
198 3
                }
199
200 5
                if ($cdef['COLUMN_DEFAULT'] !== null) {
201 3
                    $column['default'] = $this->forceType($cdef['COLUMN_DEFAULT'], $column['type']);
202 3
                }
203
204 5
                $columns[$cdef['COLUMN_NAME']] = $column;
205 5
            }
206 5
            $this->tables[$ctablename]['columns'] = $columns;
207 5
        }
208 5
        if ($ltablename && isset($this->tables[$ltablename]['columns'])) {
209 4
            return $this->tables[$ltablename]['columns'];
210
        }
211 4
        return null;
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 48
    public function get($tablename, array $where, array $options = []) {
218 48
        $sql = $this->buildSelect($tablename, $where, $options);
219 48
        $result = $this->query($sql, Db::QUERY_READ, $options);
220 48
        return $result;
221
    }
222
223
    /**
224
     * Build a sql select statement.
225
     *
226
     * @param string $table The name of the main table.
227
     * @param array $where The where filter.
228
     * @param array $options An array of additional query options.
229
     * @return string Returns the select statement as a string.
230
     * @see Db::get()
231
     */
232 52
    public function buildSelect($table, array $where, array $options = []) {
233 52
        $sql = '';
234
235
        // Build the select clause.
236 52
        if (isset($options['columns'])) {
237 10
            $columns = array();
238 10
            foreach ($options['columns'] as $value) {
239 10
                $columns[] = $this->backtick($value);
240 10
            }
241 10
            $sql .= 'select '.implode(', ', $columns);
242 10
        } else {
243 42
            $sql .= "select *";
244
        }
245
246
        // Build the from clause.
247 52
        if (val('escapeTable', $options, true)) {
248 46
            $sql .= "\nfrom ".$this->backtick($this->px.$table);
249 46
        } else {
250 6
            $sql .= "\nfrom $table";
251
        }
252
253
        // Build the where clause.
254 52
        $whereString = $this->buildWhere($where, Db::OP_AND);
255 52
        if ($whereString) {
256 46
            $sql .= "\nwhere ".$whereString;
257 46
        }
258
259
        // Build the order.
260 52
        if (isset($options['order'])) {
261 35
            $order = array_quick($options['order'], Db::ORDER_ASC);
262 35
            $orders = array();
263 35
            foreach ($order as $key => $value) {
264
                switch ($value) {
265 35
                    case Db::ORDER_ASC:
266 35
                    case Db::ORDER_DESC:
267 35
                        $orders[] = $this->backtick($key)." $value";
268 35
                        break;
269
                    default:
270
                        trigger_error("Invalid sort direction '$value' for column '$key'.", E_USER_WARNING);
271
                }
272 35
            }
273
274 35
            $sql .= "\norder by ".implode(', ', $orders);
275 35
        }
276
277
        // Build the limit, offset.
278 52
        $limit = 10;
279 52
        if (isset($options['limit'])) {
280 12
            $limit = (int)$options['limit'];
281 12
            $sql .= "\nlimit $limit";
282 12
        }
283
284 52
        if (isset($options['offset'])) {
285
            $sql .= ' offset '.((int)$options['offset']);
286 52
        } elseif (isset($options['page'])) {
287
            $offset = $limit * ($options['page'] - 1);
288
            $sql .= ' offset '.$offset;
289
        }
290
291 52
        return $sql;
292
    }
293
294
    /**
295
     * Build a where clause from a where array.
296
     *
297
     * @param array $where There where string.
298
     * This is an array in the form `['column' => 'value']` with more advanced options for non-equality comparisons.
299
     * @param string $op The logical operator to join multiple field comparisons.
300
     * @param bool $quotevals Whether or not to quote the where values.
301
     * @return string The where string.
302
     */
303 54
    protected function buildWhere($where, $op = Db::OP_AND, $quotevals = true) {
304 54
        static $map = array(Db::OP_GT => '>', Db::OP_GTE => '>=', Db::OP_LT => '<', Db::OP_LTE => '<=', Db::OP_LIKE => 'like');
305
306 54
        $result = '';
307 54
        foreach ($where as $column => $value) {
308 48
            $btcolumn = $this->backtick($column);
309
310 48
            if (is_array($value)) {
311 28
                if (isset($value[0])) {
312
                    // This is a short in syntax.
313 2
                    $value = [Db::OP_IN => $value];
314 2
                }
315
316 28
                foreach ($value as $vop => $rval) {
317 28
                    if ($result) {
318 6
                        $result .= "\n  $op ";
319 6
                    }
320
321
                    switch ($vop) {
322 28
                        case Db::OP_AND:
323 28
                        case Db::OP_OR:
324 4
                            $innerWhere = [$column => $rval];
325
                            $result .= "(\n  ".
326 4
                                $this->buildWhere($innerWhere, $vop, $quotevals).
327 4
                                "\n  )";
328 4
                            break;
329 28 View Code Duplication
                        case Db::OP_EQ:
330 6
                            if ($rval === null) {
331
                                $result .= "$btcolumn is null";
332 6
                            } elseif (is_array($rval)) {
333 2
                                $result .= "$btcolumn in ".$this->bracketList($rval);
334 2
                            } else {
335 4
                                $result .= "$btcolumn = ".$this->quoteVal($rval, $quotevals);
336
                            }
337 6
                            break;
338 24
                        case Db::OP_GT:
339 24
                        case Db::OP_GTE:
340 24
                        case Db::OP_LT:
341 24
                        case Db::OP_LTE:
342 12
                            $result .= "$btcolumn {$map[$vop]} ".$this->quoteVal($rval, $quotevals);
343 12
                            break;
344 12
                        case Db::OP_LIKE:
345 2
                            $result .= $this->buildLike($btcolumn, $rval, $quotevals);
346 2
                            break;
347 10
                        case Db::OP_IN:
348
                            // Quote the in values.
349 4
                            $rval = array_map(array($this->pdo, 'quote'), (array)$rval);
350 4
                            $result .= "$btcolumn in (".implode(', ', $rval).')';
351 4
                            break;
352 6 View Code Duplication
                        case Db::OP_NE:
353 6
                            if ($rval === null) {
354 2
                                $result .= "$btcolumn is not null";
355 6
                            } elseif (is_array($rval)) {
356 2
                                $result .= "$btcolumn not in ".$this->bracketList($rval);
357 2
                            } else {
358 2
                                $result .= "$btcolumn <> ".$this->quoteVal($rval, $quotevals);
359
                            }
360 6
                            break;
361
                    }
362 28
                }
363 28
            } else {
364 22
                if ($result) {
365 6
                    $result .= "\n  $op ";
366 6
                }
367
368
                // This is just an equality operator.
369 22
                if ($value === null) {
370 2
                    $result .= "$btcolumn is null";
371 2
                } else {
372 20
                    $result .= "$btcolumn = ".$this->quoteVal($value, $quotevals);
373
                }
374
            }
375 54
        }
376 54
        return $result;
377
    }
378
379
    /**
380
     * Build a like expression.
381
     *
382
     * @param string $column The column name.
383
     * @param mixed $value The right-hand value.
384
     * @param bool $quotevals Whether or not to quote the values.
385
     * @return string Returns the like expression.
386
     */
387 1
    protected function buildLike($column, $value, $quotevals) {
388 1
        return "$column like ".$this->quoteVal($value, $quotevals);
389
    }
390
391
    /**
392
     * Convert an array into a bracketed list suitable for MySQL clauses.
393
     *
394
     * @param array $row The row to expand.
395
     * @param string $quote The quotes to surroud the items with. There are two special cases.
396
     * ' (single quote)
397
     * : The row will be passed through {@link PDO::quote()}.
398
     * ` (backticks)
399
     * : The row will be passed through {@link MySqlDb::backtick()}.
400
     * @return string Returns the bracket list.
401
     */
402 60
    public function bracketList($row, $quote = "'") {
403
        switch ($quote) {
404 60 View Code Duplication
            case "'":
405 18
                $row = array_map([$this->pdo(), 'quote'], $row);
406 18
                $quote = '';
407 18
                break;
408 60 View Code Duplication
            case '`':
409 60
                $row = array_map([$this, 'backtick'], $row);
410 60
                $quote = '';
411 60
                break;
412
        }
413
414 60
        return "($quote".implode("$quote, $quote", $row)."$quote)";
415
    }
416
417
    /**
418
     * Gets the {@link PDO} object for this connection.
419
     *
420
     * @return \PDO
421
     */
422 31
    public function pdo() {
423 31
        $dsnParts = array_translate($this->config, ['host', 'dbname', 'port']);
424 31
        $dsn = 'mysql:'.implode_assoc(';', '=', $dsnParts);
425
426 31
        if (!isset($this->pdo)) {
427
            $this->pdo = new PDO(
428
                $dsn,
429
                val('username', $this->config, ''),
430
                val('password', $this->config, ''),
431
                [
432
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
433
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
434
                    PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8'
435
                ]
436
            );
437
        }
438 31
        return $this->pdo;
439
    }
440
441
    /**
442
     * Optionally quote a where value.
443
     *
444
     * @param mixed $value The value to quote.
445
     * @param bool $quote Whether or not to quote the value.
446
     * @return string Returns the value, optionally quoted.
447
     */
448 21
    public function quoteVal($value, $quote = true) {
449 21
        if ($value instanceof Literal) {
450
            /* @var Literal $value */
451
            return $value->getValue('mysql');
452 21
        } elseif ($quote) {
453 21
            return $this->pdo()->quote($value);
454
        } else {
455
            return $value;
456
        }
457
    }
458
459
    /**
460
     * Get the current database name.
461
     *
462
     * @return mixed
463
     */
464 5
    public function getDbName() {
465 5
        return val('dbname', $this->config);
466
    }
467
468
    /**
469
     * Parse a column type string and return it in a way that is suitible for a create/alter table statement.
470
     *
471
     * @param string $typeString The string to parse.
472
     * @return string Returns a canonical typestring.
473
     */
474 18
    protected function columnTypeString($typeString) {
475 18
        $type = null;
476
477 18
        if (substr($type, 0, 4) === 'enum') {
478
            // This is an enum which will come in as an array.
479
            if (preg_match_all("`'([^']+)'`", $typeString, $matches)) {
480
                $type = $matches[1];
481
            }
482
        } else {
483 18
            if (preg_match('`([a-z]+)\s*(?:\((\d+(?:\s*,\s*\d+)*)\))?\s*(unsigned)?`', $typeString, $matches)) {
484
                //         var_dump($matches);
485 18
                $str = $matches[1];
486 18
                $length = val(2, $matches);
487 18
                $unsigned = val(3, $matches);
488
489 18
                if (substr($str, 0, 1) == 'u') {
490
                    $unsigned = true;
491
                    $str = substr($str, 1);
492
                }
493
494
                // Remove the length from types without real lengths.
495 18
                if (in_array($str, array('tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'float', 'double'))) {
496 17
                    $length = null;
497 17
                }
498
499 18
                $type = $str;
500 18
                if ($length) {
501 8
                    $length = str_replace(' ', '', $length);
502 8
                    $type .= "($length)";
503 8
                }
504 18
                if ($unsigned) {
505
                    $type .= ' unsigned';
506
                }
507 18
            }
508
        }
509
510 18
        if (!$type) {
511
            debug_print_backtrace();
512
            trigger_error("Couldn't parse type $typeString", E_USER_ERROR);
513
        }
514
515 18
        return $type;
516
    }
517
518
    /**
519
     * Get the indexes from the database.
520
     *
521
     * @param string $tablename The name of the table to get the indexes for or an empty string to get all indexes.
522
     * @return array|null
523
     */
524 5
    protected function getIndexes($tablename = '') {
525 5
        $ltablename = strtolower($tablename);
526
        /* @var \PDOStatement */
527 5
        $stmt = $this->get(
528 5
            'information_schema.STATISTICS',
529
            [
530 5
                'TABLE_SCHEMA' => $this->getDbName(),
531 5
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
532 5
            ],
533
            [
534
                'columns' => [
535 5
                    'INDEX_NAME',
536 5
                    'TABLE_NAME',
537 5
                    'NON_UNIQUE',
538
                    'COLUMN_NAME'
539 5
                ],
540 5
                'escapeTable' => false,
541 5
                'order' => ['TABLE_NAME', 'INDEX_NAME', 'SEQ_IN_INDEX'],
542 5
                Db::OPTION_MODE => Db::MODE_PDO
543 5
            ]
544 5
        );
545
546 5
        $stmt->execute();
547 5
        $indexDefs = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);
548
549 5
        foreach ($indexDefs as $indexName => $indexRows) {
550 5
            $row = reset($indexRows);
551 5
            $itablename = strtolower(ltrim_substr($row['TABLE_NAME'], $this->px));
552
            $index = [
553 5
                'name' => $indexName,
554 5
                'columns' => array_column($indexRows, 'COLUMN_NAME')
555 5
            ];
556
557 5
            if ($indexName === 'PRIMARY') {
558 3
                $index['type'] = Db::INDEX_PK;
559 3
                $this->tables[$itablename]['indexes'][Db::INDEX_PK] = $index;
560 3
            } else {
561 3
                $index['type'] = $row['NON_UNIQUE'] ? Db::INDEX_IX : Db::INDEX_UNIQUE;
562 3
                $this->tables[$itablename]['indexes'][] = $index;
563
            }
564 5
        }
565
566 5
        if ($ltablename) {
567 4
            return valr([$ltablename, 'indexes'], $this->tables, []);
568
        }
569 1
        return null;
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 2
    public function getAllTables($withDefs = false) {
576 2
        $tables = parent::getAllTables($withDefs);
577 2
        if ($tables !== null) {
578 2
            return $tables;
579
        }
580
581
        // Grab the tablenames first.
582 2
        if ($this->allTablesFetched & Db::FETCH_TABLENAMES) {
583 2
            $tablenames = array_keys($this->tables);
584 2
        } else {
585
            $tablenames = $this->getTablenames();
586
            $this->tables = [];
587
            foreach ($tablenames as $tablename) {
588
                $this->tables[strtolower($tablename)] = ['name' => $tablename];
589
            }
590
            $this->allTablesFetched = Db::FETCH_TABLENAMES;
591
        }
592
593 2
        if (!$withDefs) {
594
            return $tablenames;
595
        }
596
597 2
        $this->getColumns();
598 2
        $this->allTablesFetched |= Db::FETCH_COLUMNS;
599
600 2
        $this->getIndexes();
601 2
        $this->allTablesFetched |= Db::FETCH_INDEXES;
602
603 2
        return $this->tables;
604
    }
605
606
    /**
607
     * Get the all of tablenames in the database.
608
     *
609
     * @return array Returns an array of table names with prefixes stripped.
610
     */
611 View Code Duplication
    protected function getTablenames() {
612
        // Get the table names.
613
        $tables = (array)$this->get(
614
            'information_schema.TABLES',
615
            [
616
                'TABLE_SCHEMA' => $this->getDbName(),
617
                'TABLE_NAME' => [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
618
            ],
619
            [
620
                'columns' => ['TABLE_NAME'],
621
                'escapeTable' => false
622
            ]
623
        );
624
625
        // Strip the table prefixes.
626
        $tables = array_map(function ($name) {
627
            return ltrim_substr($name, $this->px);
628
        }, array_column($tables, 'TABLE_NAME'));
629
630
        return $tables;
631
    }
632
633
    /**
634
     * {@inheritdoc}
635
     */
636 14
    public function insert($tablename, array $rows, array $options = []) {
637 14
        $sql = $this->buildInsert($tablename, $rows, true, $options);
638 14
        $this->query($sql, Db::QUERY_WRITE);
639 14
        $id = $this->pdo()->lastInsertId();
640 14
        if (is_numeric($id)) {
641 14
            return (int)$id;
642
        } else {
643
            return $id;
644
        }
645
    }
646
647
    /**
648
     * Build an insert statement.
649
     *
650
     * @param string $tablename The name of the table to insert to.
651
     * @param array $row The row to insert.
652
     * @param bool $quotevals Whether or not to quote the values.
653
     * @param array $options An array of options for the insert. See {@link Db::insert} for the options.
654
     * @return string Returns the the sql string of the insert statement.
655
     */
656 24
    protected function buildInsert($tablename, array $row, $quotevals = true, $options = []) {
657 24
        if (val(Db::OPTION_UPSERT, $options)) {
658 2
            return $this->buildUpsert($tablename, $row, $quotevals, $options);
659 24
        } elseif (val(Db::OPTION_IGNORE, $options)) {
660 2
            $sql = 'insert ignore ';
661 24
        } elseif (val(Db::OPTION_REPLACE, $options)) {
662 2
            $sql = 'replace ';
663 2
        } else {
664 22
            $sql = 'insert ';
665
        }
666 24
        $sql .= $this->backtick($this->px.$tablename);
667
668
        // Add the list of values.
669
        $sql .=
670 24
            "\n".$this->bracketList(array_keys($row), '`').
671 24
            "\nvalues".$this->bracketList($row, $quotevals ? "'" : '');
672
673 24
        return $sql;
674
    }
675
676
    /**
677
     * Build an upsert statement.
678
     *
679
     * An upsert statement is an insert on duplicate key statement in MySQL.
680
     *
681
     * @param string $tablename The name of the table to update.
682
     * @param array $row The row to insert or update.
683
     * @param bool $quotevals Whether or not to quote the values in the row.
684
     * @param array $options An array of additional query options.
685
     * @return string Returns the upsert statement as a string.
686
     */
687 2
    protected function buildUpsert($tablename, array $row, $quotevals = true, $options = []) {
688
        // Build the initial insert statement first.
689 2
        unset($options[Db::OPTION_UPSERT]);
690 2
        $sql = $this->buildInsert($tablename, $row, $quotevals, $options);
691
692
        // Add the duplicate key stuff.
693 2
        $updates = [];
694 2
        foreach ($row as $key => $value) {
695 2
            $updates[] = $this->backtick($key).' = values('.$this->backtick($key).')';
696 2
        }
697 2
        $sql .= "\non duplicate key update ".implode(', ', $updates);
698
699 2
        return $sql;
700
    }
701
702
    /**
703
     * {@inheritdoc}
704
     */
705 34
    public function load($tablename, $rows, array $options = []) {
706 34
        $count = 0;
707 34
        $first = true;
708 34
        $spec = [];
709 34
        $stmt = null;
710
711
        // Loop over the rows and insert them with the statement.
712 34
        foreach ($rows as $row) {
713 34
            if ($first) {
714
                // Build the insert statement from the first row.
715 34
                foreach ($row as $key => $value) {
716 34
                    $spec[$key] = $this->paramName($key);
717 34
                }
718
719 34
                $sql = $this->buildInsert($tablename, $spec, false, $options);
720 34
                $stmt = $this->pdo()->prepare($sql);
721 34
                $first = false;
722 34
            }
723
724 34
            $params = array_translate($row, $spec);
725 34
            $stmt->execute($params);
726 34
            $count += $stmt->rowCount();
727 34
        }
728
729 34
        return $count;
730
    }
731
732
    /**
733
     * Make a valid pdo parameter name from a string.
734
     *
735
     * This method replaces invalid placeholder characters with underscores.
736
     *
737
     * @param string $name The name to replace.
738
     * @return string
739
     */
740 34
    protected function paramName($name) {
741 34
        $result = ':'.preg_replace('`[^a-zA-Z0-9_]`', '_', $name);
742 34
        return $result;
743
    }
744
745
    /**
746
     * {@inheritdoc}
747
     */
748 6
    public function update($tablename, array $set, array $where, array $options = []) {
749 6
        $sql = $this->buildUpdate($tablename, $set, $where, true, $options);
750 6
        $result = $this->query($sql, Db::QUERY_WRITE);
751
752 6
        if ($result instanceof \PDOStatement) {
753
            /* @var \PDOStatement $result */
754
            return $result->rowCount();
755
        }
756 6
        return $result;
757
    }
758
759
    /**
760
     * Build a sql update statement.
761
     *
762
     * @param string $tablename The name of the table to update.
763
     * @param array $set An array of columns to set.
764
     * @param array $where The where filter.
765
     * @param bool $quotevals Whether or not to quote the values.
766
     * @param array $options Additional options for the query.
767
     * @return string Returns the update statement as a string.
768
     */
769 2 View Code Duplication
    protected function buildUpdate($tablename, array $set, array $where, $quotevals = true, array $options = []) {
770
        $sql = 'update '.
771 2
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
772 2
            $this->backtick($this->px.$tablename).
773 2
            "\nset\n  ";
774
775 2
        $parts = [];
776 2
        foreach ($set as $key => $value) {
777 2
            $parts[] = $this->backtick($key).' = '.$this->quoteVal($value, $quotevals);
778 2
        }
779 2
        $sql .= implode(",\n  ", $parts);
780
781 2
        if (!empty($where)) {
782 2
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND, $quotevals);
783 2
        }
784
785 2
        return $sql;
786
    }
787
788
    /**
789
     * {@inheritdoc}
790
     */
791 30
    public function delete($tablename, array $where, array $options = []) {
792 30
        if (val(Db::OPTION_TRUNCATE, $options)) {
793
            if (!empty($where)) {
794
                throw new \InvalidArgumentException("You cannot truncate $tablename with a where filter.", 500);
795
            }
796
            $sql = 'truncate table '.$this->backtick($this->px.$tablename);
797
        } else {
798 30
            $sql = 'delete from '.$this->backtick($this->px.$tablename);
799
800 30
            if (!empty($where)) {
801
                $sql .= "\nwhere ".$this->buildWhere($where);
802
            }
803
        }
804 30
        return $this->query($sql, Db::QUERY_WRITE);
805
    }
806
807
    /**
808
     * {@inheritdoc}
809
     */
810 8
    protected function createTable($tablename, array $tabledef, array $options = []) {
811
        // The table doesn't exist so this is a create table.
812 8
        $parts = array();
813 8
        foreach ($tabledef['columns'] as $name => $def) {
814 8
            $parts[] = $this->columnDefString($name, $def);
815 8
        }
816
817 8
        foreach (val('indexes', $tabledef, []) as $index) {
818 8
            $indexDef = $this->indexDefString($tablename, $index);
819 8
            if ($indexDef) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexDef 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...
820 8
                $parts[] = $indexDef;
821 8
            }
822 8
        }
823
824 8
        $fullTablename = $this->backtick($this->px.$tablename);
825 8
        $sql = "create table $fullTablename (\n  ".
826 8
            implode(",\n  ", $parts).
827 8
            "\n)";
828
829 8
        if (val('collate', $options)) {
830
            $sql .= "\n collate {$options['collate']}";
831
        }
832
833 8
        $this->query($sql, Db::QUERY_DEFINE);
834 8
    }
835
836
    /**
837
     * Construct a column definition string from an array defintion.
838
     *
839
     * @param string $name The name of the column.
840
     * @param array $def The column definition.
841
     * @return string Returns a string representing the column definition.
842
     */
843 8
    protected function columnDefString($name, array $def) {
844 8
        $result = $this->backtick($name).' '.$this->columnTypeString($def['type']);
845
846 8
        if (val('required', $def)) {
847 7
            $result .= ' not null';
848 7
        }
849
850 8
        if (isset($def['default'])) {
851 5
            $result .= ' default '.$this->quoteVal($def['default']);
852 5
        }
853
854 8
        if (val('autoincrement', $def)) {
855 2
            $result .= ' auto_increment';
856 2
        }
857
858 8
        return $result;
859
    }
860
861
    /**
862
     * Return the SDL string that defines an index.
863
     *
864
     * @param string $tablename The name of the table that the index is on.
865
     * @param array $def The index defintion. This definition should have the following keys.
866
     *
867
     * columns
868
     * : An array of columns in the index.
869
     * type
870
     * : One of "index", "unique", or "primary".
871
     * @return null|string Returns the index string or null if the index is not correct.
872
     */
873 8
    protected function indexDefString($tablename, array $def) {
874 8
        $indexName = $this->backtick($this->buildIndexName($tablename, $def));
875 8
        switch (val('type', $def, Db::INDEX_IX)) {
876 8
            case Db::INDEX_IX:
877 5
                return "index $indexName ".$this->bracketList($def['columns'], '`');
878 5
            case Db::INDEX_UNIQUE:
879 1
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
880 5
            case Db::INDEX_PK:
881 5
                return "primary key ".$this->bracketList($def['columns'], '`');
882
        }
883
        return null;
884
    }
885
886
    /**
887
     * {@inheritdoc}
888
     */
889 4
    protected function alterTable($tablename, array $alterdef, array $options = []) {
890 4
        $columnOrders = $this->getColumnOrders($alterdef['def']['columns']);
891 4
        $parts = [];
892
893
        // Add the columns and indexes.
894 4
        foreach ($alterdef['add']['columns'] as $cname => $cdef) {
895
            // Figure out the order of the column.
896 2
            $pos = val($cname, $columnOrders, '');
897 2
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
898 4
        }
899 4
        foreach ($alterdef['add']['indexes'] as $ixdef) {
900 3
            $parts[] = 'add '.$this->indexDefString($tablename, $ixdef);
901 4
        }
902
903
        // Alter the columns.
904 4
        foreach ($alterdef['alter']['columns'] as $cname => $cdef) {
905 4
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
906 4
        }
907
908
        // Drop the columns and indexes.
909 4
        foreach ($alterdef['drop']['columns'] as $cname => $_) {
910 1
            $parts[] = 'drop '.$this->backtick($cname);
911 4
        }
912 4
        foreach ($alterdef['drop']['indexes'] as $ixdef) {
913 3
            $parts[] = 'drop index '.$this->backtick($ixdef['name']);
914 4
        }
915
916 4
        if (empty($parts)) {
917
            return false;
918
        }
919
920
        $sql = 'alter '.
921 4
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
922 4
            'table '.$this->backtick($this->px.$tablename)."\n  ".
923 4
            implode(",\n  ", $parts);
924
925 4
        $result = $this->query($sql, Db::QUERY_DEFINE);
926 4
        return $result;
927
    }
928
929
    /**
930
     * Get an array of column orders so that added columns can be slotted into their correct spot.
931
     *
932
     * @param array $cdefs An array of column definitions.
933
     * @return array Returns an array of column orders suitable for an `alter table` statement.
934
     */
935 4
    protected function getColumnOrders($cdefs) {
936 4
        $orders = array_flip(array_keys($cdefs));
937
938 4
        $prev = ' first';
939 4
        foreach ($orders as $cname => &$value) {
940 4
            $value = $prev;
941 4
            $prev = ' after '.$this->backtick($cname);
942 4
        }
943 4
        return $orders;
944
    }
945
946
    /**
947
     * Force a value into the appropriate php type based on its Sqlite type.
948
     *
949
     * @param mixed $value The value to force.
950
     * @param string $type The sqlite type name.
951
     * @return mixed Returns $value cast to the appropriate type.
952
     */
953 3 View Code Duplication
    protected function forceType($value, $type) {
954 3
        $type = strtolower($type);
955
956 3
        if ($type === 'null') {
957
            return null;
958 3
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
959 3
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
960 3
            return force_int($value);
961
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
962
            'numeric', 'decimal(10,5)'])) {
963
            return floatval($value);
964
        } else {
965
            return (string)$value;
966
        }
967
    }
968
}
969