1
0

Database.php 29 KB

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