Issues (14)

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 81
    public function __construct(array $options)
59
    {
60
        // set table prefix if given
61 81
        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 81
            array_key_exists('dsn', $options) &&
68 81
            array_key_exists('usr', $options) &&
69 81
            array_key_exists('pwd', $options) &&
70 81
            array_key_exists('opt', $options)
71
        ) {
72
            // set default options
73 81
            $options['opt'][PDO::ATTR_ERRMODE]          = PDO::ERRMODE_EXCEPTION;
74 81
            $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
75 81
            if (!array_key_exists(PDO::ATTR_PERSISTENT, $options['opt'])) {
76 43
                $options['opt'][PDO::ATTR_PERSISTENT] = true;
77
            }
78 81
            $db_tables_exist                            = true;
79
80
            // setup type and dabase connection
81 81
            $this->_type = strtolower(
82 81
                substr($options['dsn'], 0, strpos($options['dsn'], ':'))
83 81
            );
84
            // MySQL uses backticks to quote identifiers by default,
85
            // tell it to expect ANSI SQL double quotes
86 81
            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 81
            $tableQuery = $this->_getTableQuery($this->_type);
90 81
            $this->_db  = new PDO(
91 81
                $options['dsn'],
92 81
                $options['usr'],
93 81
                $options['pwd'],
94 81
                $options['opt']
95 81
            );
96
97
            // check if the database contains the required tables
98 81
            $tables = $this->_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
99
100
            // create paste table if necessary
101 81
            if (!in_array($this->_sanitizeIdentifier('paste'), $tables)) {
102 45
                $this->_createPasteTable();
103 45
                $db_tables_exist = false;
104
            }
105
106
            // create comment table if necessary
107 81
            if (!in_array($this->_sanitizeIdentifier('comment'), $tables)) {
108 45
                $this->_createCommentTable();
109 45
                $db_tables_exist = false;
110
            }
111
112
            // create config table if necessary
113 81
            $db_version = Controller::VERSION;
114 81
            if (!in_array($this->_sanitizeIdentifier('config'), $tables)) {
115 46
                $this->_createConfigTable();
116
                // if we only needed to create the config table, the DB is older then 0.22
117 46
                if ($db_tables_exist) {
118 46
                    $db_version = '0.21';
119
                }
120
            } else {
121 73
                $db_version = $this->_getConfig('VERSION');
122
            }
123
124
            // update database structure if necessary
125 81
            if (version_compare($db_version, Controller::VERSION, '<')) {
126 81
                $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 41
    public function create($pasteid, array &$paste)
144
    {
145 41
        $expire_date      = 0;
146 41
        $opendiscussion   = $burnafterreading = false;
147 41
        $attachment       = $attachmentname   = null;
148 41
        $meta             = $paste['meta'];
149 41
        $isVersion1       = array_key_exists('data', $paste);
150 41
        if (array_key_exists('expire_date', $meta)) {
151 24
            $expire_date = (int) $meta['expire_date'];
152 24
            unset($meta['expire_date']);
153
        }
154 41
        if (array_key_exists('opendiscussion', $meta)) {
155 2
            $opendiscussion = $meta['opendiscussion'];
156 2
            unset($meta['opendiscussion']);
157
        }
158 41
        if (array_key_exists('burnafterreading', $meta)) {
159 1
            $burnafterreading = $meta['burnafterreading'];
160 1
            unset($meta['burnafterreading']);
161
        }
162 41
        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 39
            $opendiscussion   = $paste['adata'][2];
173 39
            $burnafterreading = $paste['adata'][3];
174
        }
175
        try {
176 41
            return $this->_exec(
177 41
                'INSERT INTO "' . $this->_sanitizeIdentifier('paste') .
178 41
                '" VALUES(?,?,?,?,?,?,?,?)',
179 41
                array(
180 41
                    $pasteid,
181 41
                    $isVersion1 ? $paste['data'] : Json::encode($paste),
182 41
                    $expire_date,
183 41
                    (int) $opendiscussion,
184 41
                    (int) $burnafterreading,
185 41
                    Json::encode($meta),
186 41
                    $attachment,
187 41
                    $attachmentname,
188 41
                )
189 41
            );
190 4
        } catch (Exception $e) {
191 4
            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 28
    public function delete($pasteid)
259
    {
260 28
        $this->_exec(
261 28
            'DELETE FROM "' . $this->_sanitizeIdentifier('paste') .
262 28
            '" WHERE "dataid" = ?', array($pasteid)
263 28
        );
264 28
        $this->_exec(
265 28
            'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
266 28
            '" WHERE "pasteid" = ?', array($pasteid)
267 28
        );
268
    }
269
270
    /**
271
     * Test if a paste exists.
272
     *
273
     * @access public
274
     * @param  string $pasteid
275
     * @return bool
276
     */
277 62
    public function exists($pasteid)
278
    {
279
        try {
280 62
            $row = $this->_select(
281 62
                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
282 62
                '" WHERE "dataid" = ?', array($pasteid), true
283 62
            );
284 1
        } catch (Exception $e) {
285 1
            return false;
286
        }
287 62
        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 10
    public function createComment($pasteid, $parentid, $commentid, array &$comment)
301
    {
302 10
        if (array_key_exists('data', $comment)) {
303 1
            $version = 1;
304 1
            $data    = $comment['data'];
305
        } else {
306
            try {
307 10
                $version = 2;
308 10
                $data    = Json::encode($comment);
309 1
            } catch (Exception $e) {
310 1
                return false;
311
            }
312
        }
313 9
        list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
314 9
        $meta                       = $comment['meta'];
315 9
        unset($comment['meta']);
316 9
        foreach (array('nickname', $iconKey) as $key) {
317 9
            if (!array_key_exists($key, $meta)) {
318 9
                $meta[$key] = null;
319
            }
320
        }
321
        try {
322 9
            return $this->_exec(
323 9
                'INSERT INTO "' . $this->_sanitizeIdentifier('comment') .
324 9
                '" VALUES(?,?,?,?,?,?,?)',
325 9
                array(
326 9
                    $commentid,
327 9
                    $pasteid,
328 9
                    $parentid,
329 9
                    $data,
330 9
                    $meta['nickname'],
331 9
                    $meta[$iconKey],
332 9
                    $meta[$createdKey],
333 9
                )
334 9
            );
335 1
        } catch (Exception $e) {
336 1
            return false;
337
        }
338
    }
339
340
    /**
341
     * Read all comments of paste.
342
     *
343
     * @access public
344
     * @param  string $pasteid
345
     * @return array
346
     */
347 18
    public function readComments($pasteid)
348
    {
349 18
        $rows = $this->_select(
350 18
            'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
351 18
            '" WHERE "pasteid" = ?', array($pasteid)
352 18
        );
353
354
        // create comment list
355 18
        $comments = array();
356 18
        if (is_array($rows) && count($rows)) {
357 6
            foreach ($rows as $row) {
358 6
                $i    = $this->getOpenSlot($comments, (int) $row['postdate']);
359 6
                $data = Json::decode($row['data']);
360 6
                if (array_key_exists('v', $data) && $data['v'] >= 2) {
361 6
                    $version      = 2;
362 6
                    $comments[$i] = $data;
363
                } else {
364 1
                    $version      = 1;
365 1
                    $comments[$i] = array('data' => $row['data']);
366
                }
367 6
                list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
368 6
                $comments[$i]['id']         = $row['dataid'];
369 6
                $comments[$i]['parentid']   = $row['parentid'];
370 6
                $comments[$i]['meta']       = array($createdKey => (int) $row['postdate']);
371 6
                foreach (array('nickname' => 'nickname', 'vizhash' => $iconKey) as $rowKey => $commentKey) {
372 6
                    if (array_key_exists($rowKey, $row) && !empty($row[$rowKey])) {
373 5
                        $comments[$i]['meta'][$commentKey] = $row[$rowKey];
374
                    }
375
                }
376
            }
377 6
            ksort($comments);
378
        }
379 18
        return $comments;
380
    }
381
382
    /**
383
     * Test if a comment exists.
384
     *
385
     * @access public
386
     * @param  string $pasteid
387
     * @param  string $parentid
388
     * @param  string $commentid
389
     * @return bool
390
     */
391 14
    public function existsComment($pasteid, $parentid, $commentid)
392
    {
393
        try {
394 14
            return (bool) $this->_select(
395 14
                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
396 14
                '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
397 14
                array($pasteid, $parentid, $commentid), true
398 14
            );
399 1
        } catch (Exception $e) {
400 1
            return false;
401
        }
402
    }
403
404
    /**
405
     * Save a value.
406
     *
407
     * @access public
408
     * @param  string $value
409
     * @param  string $namespace
410
     * @param  string $key
411
     * @return bool
412
     */
413 17
    public function setValue($value, $namespace, $key = '')
414
    {
415 17
        if ($namespace === 'traffic_limiter') {
416 4
            $this->_last_cache[$key] = $value;
417
            try {
418 4
                $value = Json::encode($this->_last_cache);
419
            } catch (Exception $e) {
420
                return false;
421
            }
422
        }
423 17
        return $this->_exec(
424 17
            'UPDATE "' . $this->_sanitizeIdentifier('config') .
425 17
            '" SET "value" = ? WHERE "id" = ?',
426 17
            array($value, strtoupper($namespace))
427 17
        );
428
    }
429
430
    /**
431
     * Load a value.
432
     *
433
     * @access public
434
     * @param  string $namespace
435
     * @param  string $key
436
     * @return string
437
     */
438 22
    public function getValue($namespace, $key = '')
439
    {
440 22
        $configKey = strtoupper($namespace);
441 22
        $value     = $this->_getConfig($configKey);
442 22
        if ($value === '') {
443
            // initialize the row, so that setValue can rely on UPDATE queries
444 17
            $this->_exec(
445 17
                'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
446 17
                '" VALUES(?,?)',
447 17
                array($configKey, '')
448 17
            );
449
450
            // migrate filesystem based salt into database
451 17
            $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
452 17
            if ($namespace === 'salt' && is_readable($file)) {
453 1
                $fs    = new Filesystem(array('dir' => 'data'));
454 1
                $value = $fs->getValue('salt');
455 1
                $this->setValue($value, 'salt');
456 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

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