Issues (13)

lib/Data/Database.php (1 issue)

1
<?php declare(strict_types=1);
2
/**
3
 * PrivateBin
4
 *
5
 * a zero-knowledge paste bin
6
 *
7
 * @link      https://github.com/PrivateBin/PrivateBin
8
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10
 */
11
12
namespace PrivateBin\Data;
13
14
use Exception;
15
use PDO;
16
use PDOException;
17
use PrivateBin\Controller;
18
use PrivateBin\Json;
19
20
/**
21
 * Database
22
 *
23
 * Model for database access, implemented as a singleton.
24
 */
25
class Database extends AbstractData
26
{
27
    /**
28
     * instance of database connection
29
     *
30
     * @access private
31
     * @var PDO
32
     */
33
    private $_db;
34
35
    /**
36
     * table prefix
37
     *
38
     * @access private
39
     * @var string
40
     */
41
    private $_prefix = '';
42
43
    /**
44
     * database type
45
     *
46
     * @access private
47
     * @var string
48
     */
49
    private $_type = '';
50
51
    /**
52
     * instantiates a new Database data backend
53
     *
54
     * @access public
55
     * @param  array $options
56
     * @throws Exception
57
     */
58 78
    public function __construct(array $options)
59
    {
60
        // set table prefix if given
61 78
        if (array_key_exists('tbl', $options)) {
62 55
            $this->_prefix = $options['tbl'];
63
        }
64
65
        // initialize the db connection with new options
66
        if (
67 78
            array_key_exists('dsn', $options) &&
68 78
            array_key_exists('usr', $options) &&
69 78
            array_key_exists('pwd', $options) &&
70 78
            array_key_exists('opt', $options)
71
        ) {
72
            // set default options
73 78
            $options['opt'][PDO::ATTR_ERRMODE]          = PDO::ERRMODE_EXCEPTION;
74 78
            $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
75 78
            if (!array_key_exists(PDO::ATTR_PERSISTENT, $options['opt'])) {
76 41
                $options['opt'][PDO::ATTR_PERSISTENT] = true;
77
            }
78 78
            $db_tables_exist                            = true;
79
80
            // setup type and dabase connection
81 78
            $this->_type = strtolower(
82 78
                substr($options['dsn'], 0, strpos($options['dsn'], ':'))
83 78
            );
84
            // MySQL uses backticks to quote identifiers by default,
85
            // tell it to expect ANSI SQL double quotes
86 78
            if ($this->_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
87 1
                $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
88
            }
89 78
            $tableQuery = $this->_getTableQuery($this->_type);
90 78
            $this->_db  = new PDO(
91 78
                $options['dsn'],
92 78
                $options['usr'],
93 78
                $options['pwd'],
94 78
                $options['opt']
95 78
            );
96
97
            // check if the database contains the required tables
98 78
            $tables = $this->_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
99
100
            // create paste table if necessary
101 78
            if (!in_array($this->_sanitizeIdentifier('paste'), $tables)) {
102 44
                $this->_createPasteTable();
103 44
                $db_tables_exist = false;
104
            }
105
106
            // create comment table if necessary
107 78
            if (!in_array($this->_sanitizeIdentifier('comment'), $tables)) {
108 44
                $this->_createCommentTable();
109 44
                $db_tables_exist = false;
110
            }
111
112
            // create config table if necessary
113 78
            $db_version = Controller::VERSION;
114 78
            if (!in_array($this->_sanitizeIdentifier('config'), $tables)) {
115 45
                $this->_createConfigTable();
116
                // if we only needed to create the config table, the DB is older then 0.22
117 45
                if ($db_tables_exist) {
118 45
                    $db_version = '0.21';
119
                }
120
            } else {
121 71
                $db_version = $this->_getConfig('VERSION');
122
            }
123
124
            // update database structure if necessary
125 78
            if (version_compare($db_version, Controller::VERSION, '<')) {
126 78
                $this->_upgradeDatabase($db_version);
127
            }
128
        } else {
129 4
            throw new Exception(
130 4
                'Missing configuration for key dsn, usr, pwd or opt in the section model_options, please check your configuration file', 6
131 4
            );
132
        }
133
    }
134
135
    /**
136
     * Create a paste.
137
     *
138
     * @access public
139
     * @param  string $pasteid
140
     * @param  array  $paste
141
     * @return bool
142
     */
143 39
    public function create($pasteid, array $paste)
144
    {
145 39
        $expire_date      = 0;
146 39
        $opendiscussion   = $burnafterreading = false;
147 39
        $attachment       = $attachmentname   = null;
148 39
        $meta             = $paste['meta'];
149 39
        $isVersion1       = array_key_exists('data', $paste);
150 39
        if (array_key_exists('expire_date', $meta)) {
151 24
            $expire_date = (int) $meta['expire_date'];
152 24
            unset($meta['expire_date']);
153
        }
154 39
        if (array_key_exists('opendiscussion', $meta)) {
155 2
            $opendiscussion = $meta['opendiscussion'];
156 2
            unset($meta['opendiscussion']);
157
        }
158 39
        if (array_key_exists('burnafterreading', $meta)) {
159 1
            $burnafterreading = $meta['burnafterreading'];
160 1
            unset($meta['burnafterreading']);
161
        }
162 39
        if ($isVersion1) {
163 2
            if (array_key_exists('attachment', $meta)) {
164 1
                $attachment = $meta['attachment'];
165 1
                unset($meta['attachment']);
166
            }
167 2
            if (array_key_exists('attachmentname', $meta)) {
168 1
                $attachmentname = $meta['attachmentname'];
169 2
                unset($meta['attachmentname']);
170
            }
171
        } else {
172 37
            $opendiscussion   = $paste['adata'][2];
173 37
            $burnafterreading = $paste['adata'][3];
174
        }
175
        try {
176 39
            return $this->_exec(
177 39
                'INSERT INTO "' . $this->_sanitizeIdentifier('paste') .
178 39
                '" VALUES(?,?,?,?,?,?,?,?)',
179 39
                array(
180 39
                    $pasteid,
181 39
                    $isVersion1 ? $paste['data'] : Json::encode($paste),
182 39
                    $expire_date,
183 39
                    (int) $opendiscussion,
184 39
                    (int) $burnafterreading,
185 39
                    Json::encode($meta),
186 39
                    $attachment,
187 39
                    $attachmentname,
188 39
                )
189 39
            );
190 3
        } catch (Exception $e) {
191 3
            return false;
192
        }
193
    }
194
195
    /**
196
     * Read a paste.
197
     *
198
     * @access public
199
     * @param  string $pasteid
200
     * @return array|false
201
     */
202 30
    public function read($pasteid)
203
    {
204
        try {
205 30
            $row = $this->_select(
206 30
                'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') .
207 30
                '" WHERE "dataid" = ?', array($pasteid), true
208 30
            );
209
        } catch (Exception $e) {
210
            $row = false;
211
        }
212 30
        if ($row === false) {
213 2
            return false;
214
        }
215
        // create array
216 29
        $data       = Json::decode($row['data']);
217 29
        $isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2;
218 29
        $paste      = $isVersion2 ? $data : array('data' => $row['data']);
219
220
        try {
221 29
            $row['meta'] = Json::decode($row['meta']);
222 1
        } catch (Exception $e) {
223 1
            $row['meta'] = array();
224
        }
225 29
        $row                        = self::upgradePreV1Format($row);
226 29
        $paste['meta']              = $row['meta'];
227 29
        $expire_date                = (int) $row['expiredate'];
228 29
        if ($expire_date > 0) {
229 16
            $paste['meta']['expire_date'] = $expire_date;
230
        }
231 29
        if ($isVersion2) {
232 24
            return $paste;
233
        }
234
235
        // support v1 attachments
236 5
        if (array_key_exists('attachment', $row) && !empty($row['attachment'])) {
237 2
            $paste['attachment'] = $row['attachment'];
238 2
            if (array_key_exists('attachmentname', $row) && !empty($row['attachmentname'])) {
239 2
                $paste['attachmentname'] = $row['attachmentname'];
240
            }
241
        }
242 5
        if ($row['opendiscussion']) {
243 2
            $paste['meta']['opendiscussion'] = true;
244
        }
245 5
        if ($row['burnafterreading']) {
246 1
            $paste['meta']['burnafterreading'] = true;
247
        }
248
249 5
        return $paste;
250
    }
251
252
    /**
253
     * Delete a paste and its discussion.
254
     *
255
     * @access public
256
     * @param  string $pasteid
257
     */
258 26
    public function delete($pasteid)
259
    {
260 26
        $this->_exec(
261 26
            'DELETE FROM "' . $this->_sanitizeIdentifier('paste') .
262 26
            '" WHERE "dataid" = ?', array($pasteid)
263 26
        );
264 26
        $this->_exec(
265 26
            'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
266 26
            '" WHERE "pasteid" = ?', array($pasteid)
267 26
        );
268
    }
269
270
    /**
271
     * Test if a paste exists.
272
     *
273
     * @access public
274
     * @param  string $pasteid
275
     * @return bool
276
     */
277 59
    public function exists($pasteid)
278
    {
279
        try {
280 59
            $row = $this->_select(
281 59
                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
282 59
                '" WHERE "dataid" = ?', array($pasteid), true
283 59
            );
284 1
        } catch (Exception $e) {
285 1
            return false;
286
        }
287 59
        return (bool) $row;
288
    }
289
290
    /**
291
     * Create a comment in a paste.
292
     *
293
     * @access public
294
     * @param  string $pasteid
295
     * @param  string $parentid
296
     * @param  string $commentid
297
     * @param  array  $comment
298
     * @return bool
299
     */
300 9
    public function createComment($pasteid, $parentid, $commentid, array $comment)
301
    {
302 9
        if (array_key_exists('data', $comment)) {
303 1
            $version = 1;
304 1
            $data    = $comment['data'];
305
        } else {
306 9
            $version = 2;
307 9
            $data    = Json::encode($comment);
308
        }
309 9
        list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
310 9
        $meta                       = $comment['meta'];
311 9
        unset($comment['meta']);
312 9
        foreach (array('nickname', $iconKey) as $key) {
313 9
            if (!array_key_exists($key, $meta)) {
314 9
                $meta[$key] = null;
315
            }
316
        }
317
        try {
318 9
            return $this->_exec(
319 9
                'INSERT INTO "' . $this->_sanitizeIdentifier('comment') .
320 9
                '" VALUES(?,?,?,?,?,?,?)',
321 9
                array(
322 9
                    $commentid,
323 9
                    $pasteid,
324 9
                    $parentid,
325 9
                    $data,
326 9
                    $meta['nickname'],
327 9
                    $meta[$iconKey],
328 9
                    $meta[$createdKey],
329 9
                )
330 9
            );
331 1
        } catch (Exception $e) {
332 1
            return false;
333
        }
334
    }
335
336
    /**
337
     * Read all comments of paste.
338
     *
339
     * @access public
340
     * @param  string $pasteid
341
     * @return array
342
     */
343 18
    public function readComments($pasteid)
344
    {
345 18
        $rows = $this->_select(
346 18
            'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
347 18
            '" WHERE "pasteid" = ?', array($pasteid)
348 18
        );
349
350
        // create comment list
351 18
        $comments = array();
352 18
        if (is_array($rows) && count($rows)) {
353 6
            foreach ($rows as $row) {
354 6
                $i    = $this->getOpenSlot($comments, (int) $row['postdate']);
355 6
                $data = Json::decode($row['data']);
356 6
                if (array_key_exists('v', $data) && $data['v'] >= 2) {
357 6
                    $version      = 2;
358 6
                    $comments[$i] = $data;
359
                } else {
360 1
                    $version      = 1;
361 1
                    $comments[$i] = array('data' => $row['data']);
362
                }
363 6
                list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
364 6
                $comments[$i]['id']         = $row['dataid'];
365 6
                $comments[$i]['parentid']   = $row['parentid'];
366 6
                $comments[$i]['meta']       = array($createdKey => (int) $row['postdate']);
367 6
                foreach (array('nickname' => 'nickname', 'vizhash' => $iconKey) as $rowKey => $commentKey) {
368 6
                    if (array_key_exists($rowKey, $row) && !empty($row[$rowKey])) {
369 5
                        $comments[$i]['meta'][$commentKey] = $row[$rowKey];
370
                    }
371
                }
372
            }
373 6
            ksort($comments);
374
        }
375 18
        return $comments;
376
    }
377
378
    /**
379
     * Test if a comment exists.
380
     *
381
     * @access public
382
     * @param  string $pasteid
383
     * @param  string $parentid
384
     * @param  string $commentid
385
     * @return bool
386
     */
387 13
    public function existsComment($pasteid, $parentid, $commentid)
388
    {
389
        try {
390 13
            return (bool) $this->_select(
391 13
                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
392 13
                '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
393 13
                array($pasteid, $parentid, $commentid), true
394 13
            );
395 1
        } catch (Exception $e) {
396 1
            return false;
397
        }
398
    }
399
400
    /**
401
     * Save a value.
402
     *
403
     * @access public
404
     * @param  string $value
405
     * @param  string $namespace
406
     * @param  string $key
407
     * @return bool
408
     */
409 17
    public function setValue($value, $namespace, $key = '')
410
    {
411 17
        if ($namespace === 'traffic_limiter') {
412 4
            $this->_last_cache[$key] = $value;
413
            try {
414 4
                $value = Json::encode($this->_last_cache);
415
            } catch (Exception $e) {
416
                return false;
417
            }
418
        }
419 17
        return $this->_exec(
420 17
            'UPDATE "' . $this->_sanitizeIdentifier('config') .
421 17
            '" SET "value" = ? WHERE "id" = ?',
422 17
            array($value, strtoupper($namespace))
423 17
        );
424
    }
425
426
    /**
427
     * Load a value.
428
     *
429
     * @access public
430
     * @param  string $namespace
431
     * @param  string $key
432
     * @return string
433
     */
434 22
    public function getValue($namespace, $key = '')
435
    {
436 22
        $configKey = strtoupper($namespace);
437 22
        $value     = $this->_getConfig($configKey);
438 22
        if ($value === '') {
439
            // initialize the row, so that setValue can rely on UPDATE queries
440 17
            $this->_exec(
441 17
                'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
442 17
                '" VALUES(?,?)',
443 17
                array($configKey, '')
444 17
            );
445
446
            // migrate filesystem based salt into database
447 17
            $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
448 17
            if ($namespace === 'salt' && is_readable($file)) {
449 1
                $fs    = new Filesystem(array('dir' => 'data'));
450 1
                $value = $fs->getValue('salt');
451 1
                $this->setValue($value, 'salt');
452 1
                @unlink($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

452
                /** @scrutinizer ignore-unhandled */ @unlink($file);

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...
453 1
                return $value;
454
            }
455
        }
456 21
        if ($value && $namespace === 'traffic_limiter') {
457
            try {
458 1
                $this->_last_cache = Json::decode($value);
459
            } catch (Exception $e) {
460
                $this->_last_cache = array();
461
            }
462 1
            if (array_key_exists($key, $this->_last_cache)) {
463 1
                return $this->_last_cache[$key];
464
            }
465
        }
466 21
        return (string) $value;
467
    }
468
469
    /**
470
     * Returns up to batch size number of paste ids that have expired
471
     *
472
     * @access private
473
     * @param  int $batchsize
474
     * @return array
475
     */
476 12
    protected function _getExpiredPastes($batchsize)
477
    {
478 12
        $statement = $this->_db->prepare(
479 12
            'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
480 12
            '" WHERE "expiredate" < ? AND "expiredate" != ? ' .
481 12
            ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?')
482 12
        );
483 12
        $statement->execute(array(time(), 0, $batchsize));
484 12
        return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
485
    }
486
487
    /**
488
     * @inheritDoc
489
     */
490
    public function getAllPastes()
491
    {
492
        return $this->_db->query(
493
            'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . '"'
494
        )->fetchAll(PDO::FETCH_COLUMN, 0);
495
    }
496
497
    /**
498
     * execute a statement
499
     *
500
     * @access private
501
     * @param  string $sql
502
     * @param  array $params
503
     * @throws PDOException
504
     * @return bool
505
     */
506 60
    private function _exec($sql, array $params)
507
    {
508 60
        $statement = $this->_db->prepare($sql);
509 60
        foreach ($params as $key => &$parameter) {
510 60
            $position = $key + 1;
511 60
            if (is_int($parameter)) {
512 38
                $statement->bindParam($position, $parameter, PDO::PARAM_INT);
513 60
            } elseif (is_string($parameter) && strlen($parameter) >= 4000) {
514
                $statement->bindParam($position, $parameter, PDO::PARAM_STR, strlen($parameter));
515
            } else {
516 60
                $statement->bindParam($position, $parameter);
517
            }
518
        }
519 60
        $result = $statement->execute();
520 60
        $statement->closeCursor();
521 60
        return $result;
522
    }
523
524
    /**
525
     * run a select statement
526
     *
527
     * @access private
528
     * @param  string $sql
529
     * @param  array $params
530
     * @param  bool $firstOnly if only the first row should be returned
531
     * @throws PDOException
532
     * @return array|false
533
     */
534 78
    private function _select($sql, array $params, $firstOnly = false)
535
    {
536 78
        $statement = $this->_db->prepare($sql);
537 78
        $statement->execute($params);
538 78
        if ($firstOnly) {
539 78
            $result = $statement->fetch(PDO::FETCH_ASSOC);
540 18
        } elseif ($this->_type === 'oci') {
541
            // workaround for https://bugs.php.net/bug.php?id=46728
542
            $result = array();
543
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
544
                $result[] = array_map('PrivateBin\Data\Database::_sanitizeClob', $row);
545
            }
546
        } else {
547 18
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
548
        }
549 78
        $statement->closeCursor();
550 78
        if ($this->_type === 'oci' && is_array($result)) {
551
            // returned CLOB values are streams, convert these into strings
552
            $result = $firstOnly ?
553
                array_map('PrivateBin\Data\Database::_sanitizeClob', $result) :
554
                $result;
555
        }
556 78
        return $result;
557
    }
558
559
    /**
560
     * get version dependent key names
561
     *
562
     * @access private
563
     * @param  int $version
564
     * @return array
565
     */
566 9
    private function _getVersionedKeys($version)
567
    {
568 9
        if ($version === 1) {
569 1
            return array('postdate', 'vizhash');
570
        }
571 9
        return array('created', 'icon');
572
    }
573
574
    /**
575
     * get table list query, depending on the database type
576
     *
577
     * @access private
578
     * @param  string $type
579
     * @throws Exception
580
     * @return string
581
     */
582 78
    private function _getTableQuery($type)
583
    {
584
        switch ($type) {
585 78
            case 'ibm':
586 1
                $sql = 'SELECT "tabname" FROM "SYSCAT"."TABLES"';
587 1
                break;
588 78
            case 'informix':
589 1
                $sql = 'SELECT "tabname" FROM "systables"';
590 1
                break;
591 78
            case 'mssql':
592
                // U: tables created by the user
593 1
                $sql = 'SELECT "name" FROM "sysobjects" '
594 1
                     . 'WHERE "type" = \'U\' ORDER BY "name"';
595 1
                break;
596 78
            case 'mysql':
597 1
                $sql = 'SHOW TABLES';
598 1
                break;
599 78
            case 'oci':
600 1
                $sql = 'SELECT table_name FROM all_tables';
601 1
                break;
602 78
            case 'pgsql':
603 1
                $sql = 'SELECT "tablename" FROM "pg_catalog"."pg_tables" '
604 1
                     . 'WHERE "schemaname" NOT IN (\'pg_catalog\', \'information_schema\')';
605 1
                break;
606 78
            case 'sqlite':
607 78
                $sql = 'SELECT "name" FROM "sqlite_master" WHERE "type"=\'table\' '
608 78
                     . 'UNION ALL SELECT "name" FROM "sqlite_temp_master" '
609 78
                     . 'WHERE "type"=\'table\' ORDER BY "name"';
610 78
                break;
611
            default:
612 1
                throw new Exception(
613 1
                    "PDO type $type is currently not supported.", 5
614 1
                );
615
        }
616 78
        return $sql;
617
    }
618
619
    /**
620
     * get a value by key from the config table
621
     *
622
     * @access private
623
     * @param  string $key
624
     * @return string
625
     */
626 72
    private function _getConfig($key)
627
    {
628
        try {
629 72
            $row = $this->_select(
630 72
                'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') .
631 72
                '" WHERE "id" = ?', array($key), true
632 72
            );
633
        } catch (PDOException $e) {
634
            return '';
635
        }
636 72
        return $row ? $row['value'] : '';
637
    }
638
639
    /**
640
     * get the primary key clauses, depending on the database driver
641
     *
642
     * @access private
643
     * @param  string $key
644
     * @return array
645
     */
646 45
    private function _getPrimaryKeyClauses($key = 'dataid')
647
    {
648 45
        $main_key = $after_key = '';
649 45
        switch ($this->_type) {
650 45
            case 'mysql':
651 45
            case 'oci':
652
                $after_key = ", PRIMARY KEY (\"$key\")";
653
                break;
654
            default:
655 45
                $main_key = ' PRIMARY KEY';
656 45
                break;
657
        }
658 45
        return array($main_key, $after_key);
659
    }
660
661
    /**
662
     * get the data type, depending on the database driver
663
     *
664
     * PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB
665
     *
666
     * @access private
667
     * @return string
668
     */
669 45
    private function _getDataType()
670
    {
671 45
        switch ($this->_type) {
672 45
            case 'oci':
673
                return 'CLOB';
674 45
            case 'pgsql':
675
                return 'TEXT';
676
            default:
677 45
                return 'BLOB';
678
        }
679
    }
680
681
    /**
682
     * get the attachment type, depending on the database driver
683
     *
684
     * PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB
685
     *
686
     * @access private
687
     * @return string
688
     */
689 45
    private function _getAttachmentType()
690
    {
691 45
        switch ($this->_type) {
692 45
            case 'oci':
693
                return 'CLOB';
694 45
            case 'pgsql':
695
                return 'TEXT';
696
            default:
697 45
                return 'MEDIUMBLOB';
698
        }
699
    }
700
701
    /**
702
     * get the meta type, depending on the database driver
703
     *
704
     * OCI doesn't accept TEXT so it has to be VARCHAR2(4000)
705
     *
706
     * @access private
707
     * @return string
708
     */
709 45
    private function _getMetaType()
710
    {
711 45
        switch ($this->_type) {
712 45
            case 'oci':
713
                return 'VARCHAR2(4000)';
714
            default:
715 45
                return 'TEXT';
716
        }
717
    }
718
719
    /**
720
     * create the paste table
721
     *
722
     * @access private
723
     */
724 44
    private function _createPasteTable()
725
    {
726 44
        list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
727 44
        $dataType                   = $this->_getDataType();
728 44
        $attachmentType             = $this->_getAttachmentType();
729 44
        $metaType                   = $this->_getMetaType();
730 44
        $this->_db->exec(
731 44
            'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
732 44
            "\"dataid\" CHAR(16) NOT NULL$main_key, " .
733 44
            "\"data\" $attachmentType, " .
734 44
            '"expiredate" INT, ' .
735 44
            '"opendiscussion" INT, ' .
736 44
            '"burnafterreading" INT, ' .
737 44
            "\"meta\" $metaType, " .
738 44
            "\"attachment\" $attachmentType, " .
739 44
            "\"attachmentname\" $dataType$after_key )"
740 44
        );
741
    }
742
743
    /**
744
     * create the paste table
745
     *
746
     * @access private
747
     */
748 44
    private function _createCommentTable()
749
    {
750 44
        list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
751 44
        $dataType                   = $this->_getDataType();
752 44
        $this->_db->exec(
753 44
            'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
754 44
            "\"dataid\" CHAR(16) NOT NULL$main_key, " .
755 44
            '"pasteid" CHAR(16), ' .
756 44
            '"parentid" CHAR(16), ' .
757 44
            "\"data\" $dataType, " .
758 44
            "\"nickname\" $dataType, " .
759 44
            "\"vizhash\" $dataType, " .
760 44
            "\"postdate\" INT$after_key )"
761 44
        );
762 44
        if ($this->_type === 'oci') {
763
            $this->_db->exec(
764
                'declare
765
                    already_exists  exception;
766
                    columns_indexed exception;
767
                    pragma exception_init( already_exists, -955 );
768
                    pragma exception_init(columns_indexed, -1408);
769
                begin
770
                    execute immediate \'create index "comment_parent" on "' . $this->_sanitizeIdentifier('comment') . '" ("pasteid")\';
771
                exception
772
                    when already_exists or columns_indexed then
773
                    NULL;
774
                end;'
775
            );
776
        } else {
777
            // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
778 44
            $this->_db->exec(
779 44
                'CREATE INDEX "' .
780 44
                $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
781 44
                $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
782 44
            );
783
        }
784
    }
785
786
    /**
787
     * create the paste table
788
     *
789
     * @access private
790
     */
791 45
    private function _createConfigTable()
792
    {
793 45
        list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
794 45
        $charType                   = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
795 45
        $textType                   = $this->_getMetaType();
796 45
        $this->_db->exec(
797 45
            'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
798 45
            "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
799 45
        );
800 45
        $this->_exec(
801 45
            'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
802 45
            '" VALUES(?,?)',
803 45
            array('VERSION', Controller::VERSION)
804 45
        );
805
    }
806
807
    /**
808
     * sanitizes CLOB values used with OCI
809
     *
810
     * From: https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field
811
     *
812
     * @access public
813
     * @static
814
     * @param  int|string|resource $value
815
     * @return int|string
816
     */
817 1
    public static function _sanitizeClob($value)
818
    {
819 1
        if (is_resource($value)) {
820 1
            $value = stream_get_contents($value);
821
        }
822 1
        return $value;
823
    }
824
825
    /**
826
     * sanitizes identifiers
827
     *
828
     * @access private
829
     * @param  string $identifier
830
     * @return string
831
     */
832 78
    private function _sanitizeIdentifier($identifier)
833
    {
834 78
        return preg_replace('/[^A-Za-z0-9_]+/', '', $this->_prefix . $identifier);
835
    }
836
837
    /**
838
     * upgrade the database schema from an old version
839
     *
840
     * @access private
841
     * @param  string $oldversion
842
     */
843 1
    private function _upgradeDatabase($oldversion)
844
    {
845 1
        $dataType       = $this->_getDataType();
846 1
        $attachmentType = $this->_getAttachmentType();
847 1
        if (version_compare($oldversion, '0.21', '<=')) {
848
            // create the meta column if necessary (pre 0.21 change)
849
            try {
850 1
                $this->_db->exec(
851 1
                    'SELECT "meta" FROM "' . $this->_sanitizeIdentifier('paste') . '" ' .
852 1
                    ($this->_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
853 1
                );
854
            } catch (PDOException $e) {
855
                $this->_db->exec('ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
856
            }
857
            // SQLite only allows one ALTER statement at a time...
858 1
            $this->_db->exec(
859 1
                'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
860 1
                "\" ADD COLUMN \"attachment\" $attachmentType"
861 1
            );
862 1
            $this->_db->exec(
863 1
                'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
864 1
            );
865
            // SQLite doesn't support MODIFY, but it allows TEXT of similar
866
            // size as BLOB, so there is no need to change it there
867 1
            if ($this->_type !== 'sqlite') {
868
                $this->_db->exec(
869
                    'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
870
                    "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType"
871
                );
872
                $this->_db->exec(
873
                    'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') .
874
                    "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " .
875
                    "MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType"
876
                );
877
            } else {
878 1
                $this->_db->exec(
879 1
                    'CREATE UNIQUE INDEX IF NOT EXISTS "' .
880 1
                    $this->_sanitizeIdentifier('paste_dataid') . '" ON "' .
881 1
                    $this->_sanitizeIdentifier('paste') . '" ("dataid")'
882 1
                );
883 1
                $this->_db->exec(
884 1
                    'CREATE UNIQUE INDEX IF NOT EXISTS "' .
885 1
                    $this->_sanitizeIdentifier('comment_dataid') . '" ON "' .
886 1
                    $this->_sanitizeIdentifier('comment') . '" ("dataid")'
887 1
                );
888
            }
889
            // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
890 1
            $this->_db->exec(
891 1
                'CREATE INDEX "' .
892 1
                $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
893 1
                $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
894 1
            );
895
        }
896 1
        if (version_compare($oldversion, '1.3', '<=')) {
897
            // SQLite doesn't support MODIFY, but it allows TEXT of similar
898
            // size as BLOB and PostgreSQL uses TEXT, so there is no need
899
            // to change it there
900 1
            if ($this->_type !== 'sqlite' && $this->_type !== 'pgsql') {
901
                $this->_db->exec(
902
                    'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
903
                    "\" MODIFY COLUMN \"data\" $attachmentType"
904
                );
905
            }
906
        }
907 1
        if (version_compare($oldversion, '1.7.1', '<=')) {
908 1
            $supportsDropColumn = true;
909 1
            if ($this->_type === 'sqlite') {
910
                try {
911 1
                    $row                = $this->_select('SELECT sqlite_version() AS "v"', array(), true);
912 1
                    $supportsDropColumn = version_compare($row['v'], '3.35.0', '>=');
913
                } catch (PDOException $e) {
914
                    $supportsDropColumn = false;
915
                }
916
            }
917 1
            if ($supportsDropColumn) {
918
                $this->_db->exec(
919
                    'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
920
                    '" DROP COLUMN "postdate"'
921
                );
922
            }
923
        }
924 1
        $this->_exec(
925 1
            'UPDATE "' . $this->_sanitizeIdentifier('config') .
926 1
            '" SET "value" = ? WHERE "id" = ?',
927 1
            array(Controller::VERSION, 'VERSION')
928 1
        );
929
    }
930
}
931