Issues (12)

lib/Data/Database.php (1 issue)

errors are not suppressed.

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

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