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