Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.89% |
384 / 432 |
|
48.15% |
13 / 27 |
CRAP | |
0.00% |
0 / 1 |
Database | |
88.89% |
384 / 432 |
|
48.15% |
13 / 27 |
154.54 | |
0.00% |
0 / 1 |
__construct | |
97.56% |
40 / 41 |
|
0.00% |
0 / 1 |
14 | |||
create | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
9 | |||
read | |
93.33% |
28 / 30 |
|
0.00% |
0 / 1 |
14.06 | |||
delete | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
exists | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
createComment | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
5 | |||
readComments | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
9 | |||
existsComment | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
setValue | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
getValue | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
8.05 | |||
_getExpiredPastes | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getAllPastes | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
_exec | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
5.01 | |||
_select | |
60.00% |
9 / 15 |
|
0.00% |
0 / 1 |
10.14 | |||
_getVersionedKeys | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
_getTableQuery | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
9 | |||
_getConfig | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
_getPrimaryKeyClauses | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
_getDataType | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
_getAttachmentType | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
_getMetaType | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
_createPasteTable | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
_createCommentTable | |
78.26% |
18 / 23 |
|
0.00% |
0 / 1 |
2.04 | |||
_createConfigTable | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
_sanitizeClob | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
_sanitizeIdentifier | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
_upgradeDatabase | |
73.44% |
47 / 64 |
|
0.00% |
0 / 1 |
14.70 |
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 | public function __construct(array $options) |
59 | { |
60 | // set table prefix if given |
61 | if (array_key_exists('tbl', $options)) { |
62 | $this->_prefix = $options['tbl']; |
63 | } |
64 | |
65 | // initialize the db connection with new options |
66 | if ( |
67 | array_key_exists('dsn', $options) && |
68 | array_key_exists('usr', $options) && |
69 | array_key_exists('pwd', $options) && |
70 | array_key_exists('opt', $options) |
71 | ) { |
72 | // set default options |
73 | $options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; |
74 | $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false; |
75 | if (!array_key_exists(PDO::ATTR_PERSISTENT, $options['opt'])) { |
76 | $options['opt'][PDO::ATTR_PERSISTENT] = true; |
77 | } |
78 | $db_tables_exist = true; |
79 | |
80 | // setup type and dabase connection |
81 | $this->_type = strtolower( |
82 | substr($options['dsn'], 0, strpos($options['dsn'], ':')) |
83 | ); |
84 | // MySQL uses backticks to quote identifiers by default, |
85 | // tell it to expect ANSI SQL double quotes |
86 | if ($this->_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) { |
87 | $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'"; |
88 | } |
89 | $tableQuery = $this->_getTableQuery($this->_type); |
90 | $this->_db = new PDO( |
91 | $options['dsn'], |
92 | $options['usr'], |
93 | $options['pwd'], |
94 | $options['opt'] |
95 | ); |
96 | |
97 | // check if the database contains the required tables |
98 | $tables = $this->_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0); |
99 | |
100 | // create paste table if necessary |
101 | if (!in_array($this->_sanitizeIdentifier('paste'), $tables)) { |
102 | $this->_createPasteTable(); |
103 | $db_tables_exist = false; |
104 | } |
105 | |
106 | // create comment table if necessary |
107 | if (!in_array($this->_sanitizeIdentifier('comment'), $tables)) { |
108 | $this->_createCommentTable(); |
109 | $db_tables_exist = false; |
110 | } |
111 | |
112 | // create config table if necessary |
113 | $db_version = Controller::VERSION; |
114 | if (!in_array($this->_sanitizeIdentifier('config'), $tables)) { |
115 | $this->_createConfigTable(); |
116 | // if we only needed to create the config table, the DB is older then 0.22 |
117 | if ($db_tables_exist) { |
118 | $db_version = '0.21'; |
119 | } |
120 | } else { |
121 | $db_version = $this->_getConfig('VERSION'); |
122 | } |
123 | |
124 | // update database structure if necessary |
125 | if (version_compare($db_version, Controller::VERSION, '<')) { |
126 | $this->_upgradeDatabase($db_version); |
127 | } |
128 | } else { |
129 | throw new Exception( |
130 | 'Missing configuration for key dsn, usr, pwd or opt in the section model_options, please check your configuration file', 6 |
131 | ); |
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 | public function create($pasteid, array $paste) |
144 | { |
145 | $expire_date = 0; |
146 | $opendiscussion = $burnafterreading = false; |
147 | $attachment = $attachmentname = null; |
148 | $meta = $paste['meta']; |
149 | $isVersion1 = array_key_exists('data', $paste); |
150 | if (array_key_exists('expire_date', $meta)) { |
151 | $expire_date = (int) $meta['expire_date']; |
152 | unset($meta['expire_date']); |
153 | } |
154 | if (array_key_exists('opendiscussion', $meta)) { |
155 | $opendiscussion = $meta['opendiscussion']; |
156 | unset($meta['opendiscussion']); |
157 | } |
158 | if (array_key_exists('burnafterreading', $meta)) { |
159 | $burnafterreading = $meta['burnafterreading']; |
160 | unset($meta['burnafterreading']); |
161 | } |
162 | if ($isVersion1) { |
163 | if (array_key_exists('attachment', $meta)) { |
164 | $attachment = $meta['attachment']; |
165 | unset($meta['attachment']); |
166 | } |
167 | if (array_key_exists('attachmentname', $meta)) { |
168 | $attachmentname = $meta['attachmentname']; |
169 | unset($meta['attachmentname']); |
170 | } |
171 | } else { |
172 | $opendiscussion = $paste['adata'][2]; |
173 | $burnafterreading = $paste['adata'][3]; |
174 | } |
175 | try { |
176 | return $this->_exec( |
177 | 'INSERT INTO "' . $this->_sanitizeIdentifier('paste') . |
178 | '" VALUES(?,?,?,?,?,?,?,?)', |
179 | array( |
180 | $pasteid, |
181 | $isVersion1 ? $paste['data'] : Json::encode($paste), |
182 | $expire_date, |
183 | (int) $opendiscussion, |
184 | (int) $burnafterreading, |
185 | Json::encode($meta), |
186 | $attachment, |
187 | $attachmentname, |
188 | ) |
189 | ); |
190 | } catch (Exception $e) { |
191 | return false; |
192 | } |
193 | } |
194 | |
195 | /** |
196 | * Read a paste. |
197 | * |
198 | * @access public |
199 | * @param string $pasteid |
200 | * @return array|false |
201 | */ |
202 | public function read($pasteid) |
203 | { |
204 | try { |
205 | $row = $this->_select( |
206 | 'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') . |
207 | '" WHERE "dataid" = ?', array($pasteid), true |
208 | ); |
209 | } catch (Exception $e) { |
210 | $row = false; |
211 | } |
212 | if ($row === false) { |
213 | return false; |
214 | } |
215 | // create array |
216 | $data = Json::decode($row['data']); |
217 | $isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2; |
218 | $paste = $isVersion2 ? $data : array('data' => $row['data']); |
219 | |
220 | try { |
221 | $row['meta'] = Json::decode($row['meta']); |
222 | } catch (Exception $e) { |
223 | $row['meta'] = array(); |
224 | } |
225 | $row = self::upgradePreV1Format($row); |
226 | $paste['meta'] = $row['meta']; |
227 | $expire_date = (int) $row['expiredate']; |
228 | if ($expire_date > 0) { |
229 | $paste['meta']['expire_date'] = $expire_date; |
230 | } |
231 | if ($isVersion2) { |
232 | return $paste; |
233 | } |
234 | |
235 | // support v1 attachments |
236 | if (array_key_exists('attachment', $row) && !empty($row['attachment'])) { |
237 | $paste['attachment'] = $row['attachment']; |
238 | if (array_key_exists('attachmentname', $row) && !empty($row['attachmentname'])) { |
239 | $paste['attachmentname'] = $row['attachmentname']; |
240 | } |
241 | } |
242 | if ($row['opendiscussion']) { |
243 | $paste['meta']['opendiscussion'] = true; |
244 | } |
245 | if ($row['burnafterreading']) { |
246 | $paste['meta']['burnafterreading'] = true; |
247 | } |
248 | |
249 | return $paste; |
250 | } |
251 | |
252 | /** |
253 | * Delete a paste and its discussion. |
254 | * |
255 | * @access public |
256 | * @param string $pasteid |
257 | */ |
258 | public function delete($pasteid) |
259 | { |
260 | $this->_exec( |
261 | 'DELETE FROM "' . $this->_sanitizeIdentifier('paste') . |
262 | '" WHERE "dataid" = ?', array($pasteid) |
263 | ); |
264 | $this->_exec( |
265 | 'DELETE FROM "' . $this->_sanitizeIdentifier('comment') . |
266 | '" WHERE "pasteid" = ?', array($pasteid) |
267 | ); |
268 | } |
269 | |
270 | /** |
271 | * Test if a paste exists. |
272 | * |
273 | * @access public |
274 | * @param string $pasteid |
275 | * @return bool |
276 | */ |
277 | public function exists($pasteid) |
278 | { |
279 | try { |
280 | $row = $this->_select( |
281 | 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . |
282 | '" WHERE "dataid" = ?', array($pasteid), true |
283 | ); |
284 | } catch (Exception $e) { |
285 | return false; |
286 | } |
287 | 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 | public function createComment($pasteid, $parentid, $commentid, array $comment) |
301 | { |
302 | if (array_key_exists('data', $comment)) { |
303 | $version = 1; |
304 | $data = $comment['data']; |
305 | } else { |
306 | $version = 2; |
307 | $data = Json::encode($comment); |
308 | } |
309 | list($createdKey, $iconKey) = $this->_getVersionedKeys($version); |
310 | $meta = $comment['meta']; |
311 | unset($comment['meta']); |
312 | foreach (array('nickname', $iconKey) as $key) { |
313 | if (!array_key_exists($key, $meta)) { |
314 | $meta[$key] = null; |
315 | } |
316 | } |
317 | try { |
318 | return $this->_exec( |
319 | 'INSERT INTO "' . $this->_sanitizeIdentifier('comment') . |
320 | '" VALUES(?,?,?,?,?,?,?)', |
321 | array( |
322 | $commentid, |
323 | $pasteid, |
324 | $parentid, |
325 | $data, |
326 | $meta['nickname'], |
327 | $meta[$iconKey], |
328 | $meta[$createdKey], |
329 | ) |
330 | ); |
331 | } catch (Exception $e) { |
332 | 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 | public function readComments($pasteid) |
344 | { |
345 | $rows = $this->_select( |
346 | 'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') . |
347 | '" WHERE "pasteid" = ?', array($pasteid) |
348 | ); |
349 | |
350 | // create comment list |
351 | $comments = array(); |
352 | if (is_array($rows) && count($rows)) { |
353 | foreach ($rows as $row) { |
354 | $i = $this->getOpenSlot($comments, (int) $row['postdate']); |
355 | $data = Json::decode($row['data']); |
356 | if (array_key_exists('v', $data) && $data['v'] >= 2) { |
357 | $version = 2; |
358 | $comments[$i] = $data; |
359 | } else { |
360 | $version = 1; |
361 | $comments[$i] = array('data' => $row['data']); |
362 | } |
363 | list($createdKey, $iconKey) = $this->_getVersionedKeys($version); |
364 | $comments[$i]['id'] = $row['dataid']; |
365 | $comments[$i]['parentid'] = $row['parentid']; |
366 | $comments[$i]['meta'] = array($createdKey => (int) $row['postdate']); |
367 | foreach (array('nickname' => 'nickname', 'vizhash' => $iconKey) as $rowKey => $commentKey) { |
368 | if (array_key_exists($rowKey, $row) && !empty($row[$rowKey])) { |
369 | $comments[$i]['meta'][$commentKey] = $row[$rowKey]; |
370 | } |
371 | } |
372 | } |
373 | ksort($comments); |
374 | } |
375 | 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 | public function existsComment($pasteid, $parentid, $commentid) |
388 | { |
389 | try { |
390 | return (bool) $this->_select( |
391 | 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') . |
392 | '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?', |
393 | array($pasteid, $parentid, $commentid), true |
394 | ); |
395 | } catch (Exception $e) { |
396 | 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 | public function setValue($value, $namespace, $key = '') |
410 | { |
411 | if ($namespace === 'traffic_limiter') { |
412 | $this->_last_cache[$key] = $value; |
413 | try { |
414 | $value = Json::encode($this->_last_cache); |
415 | } catch (Exception $e) { |
416 | return false; |
417 | } |
418 | } |
419 | return $this->_exec( |
420 | 'UPDATE "' . $this->_sanitizeIdentifier('config') . |
421 | '" SET "value" = ? WHERE "id" = ?', |
422 | array($value, strtoupper($namespace)) |
423 | ); |
424 | } |
425 | |
426 | /** |
427 | * Load a value. |
428 | * |
429 | * @access public |
430 | * @param string $namespace |
431 | * @param string $key |
432 | * @return string |
433 | */ |
434 | public function getValue($namespace, $key = '') |
435 | { |
436 | $configKey = strtoupper($namespace); |
437 | $value = $this->_getConfig($configKey); |
438 | if ($value === '') { |
439 | // initialize the row, so that setValue can rely on UPDATE queries |
440 | $this->_exec( |
441 | 'INSERT INTO "' . $this->_sanitizeIdentifier('config') . |
442 | '" VALUES(?,?)', |
443 | array($configKey, '') |
444 | ); |
445 | |
446 | // migrate filesystem based salt into database |
447 | $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php'; |
448 | if ($namespace === 'salt' && is_readable($file)) { |
449 | $fs = new Filesystem(array('dir' => 'data')); |
450 | $value = $fs->getValue('salt'); |
451 | $this->setValue($value, 'salt'); |
452 | @unlink($file); |
453 | return $value; |
454 | } |
455 | } |
456 | if ($value && $namespace === 'traffic_limiter') { |
457 | try { |
458 | $this->_last_cache = Json::decode($value); |
459 | } catch (Exception $e) { |
460 | $this->_last_cache = array(); |
461 | } |
462 | if (array_key_exists($key, $this->_last_cache)) { |
463 | return $this->_last_cache[$key]; |
464 | } |
465 | } |
466 | 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 | protected function _getExpiredPastes($batchsize) |
477 | { |
478 | $statement = $this->_db->prepare( |
479 | 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . |
480 | '" WHERE "expiredate" < ? AND "expiredate" != ? ' . |
481 | ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?') |
482 | ); |
483 | $statement->execute(array(time(), 0, $batchsize)); |
484 | 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 | private function _exec($sql, array $params) |
507 | { |
508 | $statement = $this->_db->prepare($sql); |
509 | $position = 1; |
510 | foreach ($params as &$parameter) { |
511 | if (is_int($parameter)) { |
512 | $statement->bindParam($position, $parameter, PDO::PARAM_INT); |
513 | } elseif (is_string($parameter) && strlen($parameter) >= 4000) { |
514 | $statement->bindParam($position, $parameter, PDO::PARAM_STR, strlen($parameter)); |
515 | } else { |
516 | $statement->bindParam($position, $parameter); |
517 | } |
518 | ++$position; |
519 | } |
520 | $result = $statement->execute(); |
521 | $statement->closeCursor(); |
522 | return $result; |
523 | } |
524 | |
525 | /** |
526 | * run a select statement |
527 | * |
528 | * @access private |
529 | * @param string $sql |
530 | * @param array $params |
531 | * @param bool $firstOnly if only the first row should be returned |
532 | * @throws PDOException |
533 | * @return array|false |
534 | */ |
535 | private function _select($sql, array $params, $firstOnly = false) |
536 | { |
537 | $statement = $this->_db->prepare($sql); |
538 | $statement->execute($params); |
539 | if ($firstOnly) { |
540 | $result = $statement->fetch(PDO::FETCH_ASSOC); |
541 | } elseif ($this->_type === 'oci') { |
542 | // workaround for https://bugs.php.net/bug.php?id=46728 |
543 | $result = array(); |
544 | while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { |
545 | $result[] = array_map('PrivateBin\Data\Database::_sanitizeClob', $row); |
546 | } |
547 | } else { |
548 | $result = $statement->fetchAll(PDO::FETCH_ASSOC); |
549 | } |
550 | $statement->closeCursor(); |
551 | if ($this->_type === 'oci' && is_array($result)) { |
552 | // returned CLOB values are streams, convert these into strings |
553 | $result = $firstOnly ? |
554 | array_map('PrivateBin\Data\Database::_sanitizeClob', $result) : |
555 | $result; |
556 | } |
557 | return $result; |
558 | } |
559 | |
560 | /** |
561 | * get version dependent key names |
562 | * |
563 | * @access private |
564 | * @param int $version |
565 | * @return array |
566 | */ |
567 | private function _getVersionedKeys($version) |
568 | { |
569 | if ($version === 1) { |
570 | return array('postdate', 'vizhash'); |
571 | } |
572 | return array('created', 'icon'); |
573 | } |
574 | |
575 | /** |
576 | * get table list query, depending on the database type |
577 | * |
578 | * @access private |
579 | * @param string $type |
580 | * @throws Exception |
581 | * @return string |
582 | */ |
583 | private function _getTableQuery($type) |
584 | { |
585 | switch ($type) { |
586 | case 'ibm': |
587 | $sql = 'SELECT "tabname" FROM "SYSCAT"."TABLES"'; |
588 | break; |
589 | case 'informix': |
590 | $sql = 'SELECT "tabname" FROM "systables"'; |
591 | break; |
592 | case 'mssql': |
593 | // U: tables created by the user |
594 | $sql = 'SELECT "name" FROM "sysobjects" ' |
595 | . 'WHERE "type" = \'U\' ORDER BY "name"'; |
596 | break; |
597 | case 'mysql': |
598 | $sql = 'SHOW TABLES'; |
599 | break; |
600 | case 'oci': |
601 | $sql = 'SELECT table_name FROM all_tables'; |
602 | break; |
603 | case 'pgsql': |
604 | $sql = 'SELECT "tablename" FROM "pg_catalog"."pg_tables" ' |
605 | . 'WHERE "schemaname" NOT IN (\'pg_catalog\', \'information_schema\')'; |
606 | break; |
607 | case 'sqlite': |
608 | $sql = 'SELECT "name" FROM "sqlite_master" WHERE "type"=\'table\' ' |
609 | . 'UNION ALL SELECT "name" FROM "sqlite_temp_master" ' |
610 | . 'WHERE "type"=\'table\' ORDER BY "name"'; |
611 | break; |
612 | default: |
613 | throw new Exception( |
614 | "PDO type $type is currently not supported.", 5 |
615 | ); |
616 | } |
617 | return $sql; |
618 | } |
619 | |
620 | /** |
621 | * get a value by key from the config table |
622 | * |
623 | * @access private |
624 | * @param string $key |
625 | * @return string |
626 | */ |
627 | private function _getConfig($key) |
628 | { |
629 | try { |
630 | $row = $this->_select( |
631 | 'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') . |
632 | '" WHERE "id" = ?', array($key), true |
633 | ); |
634 | } catch (PDOException $e) { |
635 | return ''; |
636 | } |
637 | return $row ? $row['value'] : ''; |
638 | } |
639 | |
640 | /** |
641 | * get the primary key clauses, depending on the database driver |
642 | * |
643 | * @access private |
644 | * @param string $key |
645 | * @return array |
646 | */ |
647 | private function _getPrimaryKeyClauses($key = 'dataid') |
648 | { |
649 | $main_key = $after_key = ''; |
650 | switch ($this->_type) { |
651 | case 'mysql': |
652 | case 'oci': |
653 | $after_key = ", PRIMARY KEY (\"$key\")"; |
654 | break; |
655 | default: |
656 | $main_key = ' PRIMARY KEY'; |
657 | break; |
658 | } |
659 | return array($main_key, $after_key); |
660 | } |
661 | |
662 | /** |
663 | * get the data type, depending on the database driver |
664 | * |
665 | * PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB |
666 | * |
667 | * @access private |
668 | * @return string |
669 | */ |
670 | private function _getDataType() |
671 | { |
672 | switch ($this->_type) { |
673 | case 'oci': |
674 | return 'CLOB'; |
675 | case 'pgsql': |
676 | return 'TEXT'; |
677 | default: |
678 | return 'BLOB'; |
679 | } |
680 | } |
681 | |
682 | /** |
683 | * get the attachment type, depending on the database driver |
684 | * |
685 | * PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB |
686 | * |
687 | * @access private |
688 | * @return string |
689 | */ |
690 | private function _getAttachmentType() |
691 | { |
692 | switch ($this->_type) { |
693 | case 'oci': |
694 | return 'CLOB'; |
695 | case 'pgsql': |
696 | return 'TEXT'; |
697 | default: |
698 | return 'MEDIUMBLOB'; |
699 | } |
700 | } |
701 | |
702 | /** |
703 | * get the meta type, depending on the database driver |
704 | * |
705 | * OCI doesn't accept TEXT so it has to be VARCHAR2(4000) |
706 | * |
707 | * @access private |
708 | * @return string |
709 | */ |
710 | private function _getMetaType() |
711 | { |
712 | switch ($this->_type) { |
713 | case 'oci': |
714 | return 'VARCHAR2(4000)'; |
715 | default: |
716 | return 'TEXT'; |
717 | } |
718 | } |
719 | |
720 | /** |
721 | * create the paste table |
722 | * |
723 | * @access private |
724 | */ |
725 | private function _createPasteTable() |
726 | { |
727 | list($main_key, $after_key) = $this->_getPrimaryKeyClauses(); |
728 | $dataType = $this->_getDataType(); |
729 | $attachmentType = $this->_getAttachmentType(); |
730 | $metaType = $this->_getMetaType(); |
731 | $this->_db->exec( |
732 | 'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' . |
733 | "\"dataid\" CHAR(16) NOT NULL$main_key, " . |
734 | "\"data\" $attachmentType, " . |
735 | '"expiredate" INT, ' . |
736 | '"opendiscussion" INT, ' . |
737 | '"burnafterreading" INT, ' . |
738 | "\"meta\" $metaType, " . |
739 | "\"attachment\" $attachmentType, " . |
740 | "\"attachmentname\" $dataType$after_key )" |
741 | ); |
742 | } |
743 | |
744 | /** |
745 | * create the paste table |
746 | * |
747 | * @access private |
748 | */ |
749 | private function _createCommentTable() |
750 | { |
751 | list($main_key, $after_key) = $this->_getPrimaryKeyClauses(); |
752 | $dataType = $this->_getDataType(); |
753 | $this->_db->exec( |
754 | 'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' . |
755 | "\"dataid\" CHAR(16) NOT NULL$main_key, " . |
756 | '"pasteid" CHAR(16), ' . |
757 | '"parentid" CHAR(16), ' . |
758 | "\"data\" $dataType, " . |
759 | "\"nickname\" $dataType, " . |
760 | "\"vizhash\" $dataType, " . |
761 | "\"postdate\" INT$after_key )" |
762 | ); |
763 | if ($this->_type === 'oci') { |
764 | $this->_db->exec( |
765 | 'declare |
766 | already_exists exception; |
767 | columns_indexed exception; |
768 | pragma exception_init( already_exists, -955 ); |
769 | pragma exception_init(columns_indexed, -1408); |
770 | begin |
771 | execute immediate \'create index "comment_parent" on "' . $this->_sanitizeIdentifier('comment') . '" ("pasteid")\'; |
772 | exception |
773 | when already_exists or columns_indexed then |
774 | NULL; |
775 | end;' |
776 | ); |
777 | } else { |
778 | // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0 |
779 | $this->_db->exec( |
780 | 'CREATE INDEX "' . |
781 | $this->_sanitizeIdentifier('comment_parent') . '" ON "' . |
782 | $this->_sanitizeIdentifier('comment') . '" ("pasteid")' |
783 | ); |
784 | } |
785 | } |
786 | |
787 | /** |
788 | * create the paste table |
789 | * |
790 | * @access private |
791 | */ |
792 | private function _createConfigTable() |
793 | { |
794 | list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id'); |
795 | $charType = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; |
796 | $textType = $this->_getMetaType(); |
797 | $this->_db->exec( |
798 | 'CREATE TABLE "' . $this->_sanitizeIdentifier('config') . |
799 | "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )" |
800 | ); |
801 | $this->_exec( |
802 | 'INSERT INTO "' . $this->_sanitizeIdentifier('config') . |
803 | '" VALUES(?,?)', |
804 | array('VERSION', Controller::VERSION) |
805 | ); |
806 | } |
807 | |
808 | /** |
809 | * sanitizes CLOB values used with OCI |
810 | * |
811 | * From: https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field |
812 | * |
813 | * @access public |
814 | * @static |
815 | * @param int|string|resource $value |
816 | * @return int|string |
817 | */ |
818 | public static function _sanitizeClob($value) |
819 | { |
820 | if (is_resource($value)) { |
821 | $value = stream_get_contents($value); |
822 | } |
823 | return $value; |
824 | } |
825 | |
826 | /** |
827 | * sanitizes identifiers |
828 | * |
829 | * @access private |
830 | * @param string $identifier |
831 | * @return string |
832 | */ |
833 | private function _sanitizeIdentifier($identifier) |
834 | { |
835 | return preg_replace('/[^A-Za-z0-9_]+/', '', $this->_prefix . $identifier); |
836 | } |
837 | |
838 | /** |
839 | * upgrade the database schema from an old version |
840 | * |
841 | * @access private |
842 | * @param string $oldversion |
843 | */ |
844 | private function _upgradeDatabase($oldversion) |
845 | { |
846 | $dataType = $this->_getDataType(); |
847 | $attachmentType = $this->_getAttachmentType(); |
848 | if (version_compare($oldversion, '0.21', '<=')) { |
849 | // create the meta column if necessary (pre 0.21 change) |
850 | try { |
851 | $this->_db->exec( |
852 | 'SELECT "meta" FROM "' . $this->_sanitizeIdentifier('paste') . '" ' . |
853 | ($this->_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1') |
854 | ); |
855 | } catch (PDOException $e) { |
856 | $this->_db->exec('ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT'); |
857 | } |
858 | // SQLite only allows one ALTER statement at a time... |
859 | $this->_db->exec( |
860 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . |
861 | "\" ADD COLUMN \"attachment\" $attachmentType" |
862 | ); |
863 | $this->_db->exec( |
864 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType" |
865 | ); |
866 | // SQLite doesn't support MODIFY, but it allows TEXT of similar |
867 | // size as BLOB, so there is no need to change it there |
868 | if ($this->_type !== 'sqlite') { |
869 | $this->_db->exec( |
870 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . |
871 | "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType" |
872 | ); |
873 | $this->_db->exec( |
874 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') . |
875 | "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " . |
876 | "MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType" |
877 | ); |
878 | } else { |
879 | $this->_db->exec( |
880 | 'CREATE UNIQUE INDEX IF NOT EXISTS "' . |
881 | $this->_sanitizeIdentifier('paste_dataid') . '" ON "' . |
882 | $this->_sanitizeIdentifier('paste') . '" ("dataid")' |
883 | ); |
884 | $this->_db->exec( |
885 | 'CREATE UNIQUE INDEX IF NOT EXISTS "' . |
886 | $this->_sanitizeIdentifier('comment_dataid') . '" ON "' . |
887 | $this->_sanitizeIdentifier('comment') . '" ("dataid")' |
888 | ); |
889 | } |
890 | // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0 |
891 | $this->_db->exec( |
892 | 'CREATE INDEX "' . |
893 | $this->_sanitizeIdentifier('comment_parent') . '" ON "' . |
894 | $this->_sanitizeIdentifier('comment') . '" ("pasteid")' |
895 | ); |
896 | } |
897 | if (version_compare($oldversion, '1.3', '<=')) { |
898 | // SQLite doesn't support MODIFY, but it allows TEXT of similar |
899 | // size as BLOB and PostgreSQL uses TEXT, so there is no need |
900 | // to change it there |
901 | if ($this->_type !== 'sqlite' && $this->_type !== 'pgsql') { |
902 | $this->_db->exec( |
903 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . |
904 | "\" MODIFY COLUMN \"data\" $attachmentType" |
905 | ); |
906 | } |
907 | } |
908 | if (version_compare($oldversion, '1.7.1', '<=')) { |
909 | $supportsDropColumn = true; |
910 | if ($this->_type === 'sqlite') { |
911 | try { |
912 | $row = $this->_select('SELECT sqlite_version() AS "v"', array(), true); |
913 | $supportsDropColumn = version_compare($row['v'], '3.35.0', '>='); |
914 | } catch (PDOException $e) { |
915 | $supportsDropColumn = false; |
916 | } |
917 | } |
918 | if ($supportsDropColumn) { |
919 | $this->_db->exec( |
920 | 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . |
921 | '" DROP COLUMN "postdate"' |
922 | ); |
923 | } |
924 | } |
925 | $this->_exec( |
926 | 'UPDATE "' . $this->_sanitizeIdentifier('config') . |
927 | '" SET "value" = ? WHERE "id" = ?', |
928 | array(Controller::VERSION, 'VERSION') |
929 | ); |
930 | } |
931 | } |