1
0

Database.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  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') < 0 && 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. array(
  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" = ?', array($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 = array();
  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'] = array();
  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" = ?', array($pasteid)
  209. );
  210. $this->_exec(
  211. 'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
  212. '" WHERE "pasteid" = ?', array($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" = ?', array($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. array(
  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. $rows = $this->_select(
  284. 'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
  285. '" WHERE "pasteid" = ?', array($pasteid)
  286. );
  287. // create comment list
  288. $comments = array();
  289. if (count($rows)) {
  290. foreach ($rows as $row) {
  291. try {
  292. $data = Json::decode($row['data']);
  293. } catch (JsonException $e) {
  294. error_log('Error while reading a comment from the database: ' . $e->getMessage());
  295. $data = array();
  296. }
  297. $i = $this->getOpenSlot($comments, (int) $row['postdate']);
  298. $comments[$i] = $data;
  299. $comments[$i]['id'] = $row['dataid'];
  300. $comments[$i]['parentid'] = $row['parentid'];
  301. $comments[$i]['meta'] = array('created' => (int) $row['postdate']);
  302. if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) {
  303. $comments[$i]['meta']['icon'] = $row['vizhash'];
  304. }
  305. }
  306. ksort($comments);
  307. }
  308. return $comments;
  309. }
  310. /**
  311. * Test if a comment exists.
  312. *
  313. * @access public
  314. * @param string $pasteid
  315. * @param string $parentid
  316. * @param string $commentid
  317. * @return bool
  318. */
  319. public function existsComment($pasteid, $parentid, $commentid)
  320. {
  321. try {
  322. return (bool) $this->_select(
  323. 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
  324. '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
  325. array($pasteid, $parentid, $commentid), true
  326. );
  327. } catch (PDOException $e) {
  328. return false;
  329. }
  330. }
  331. /**
  332. * Save a value.
  333. *
  334. * @access public
  335. * @param string $value
  336. * @param string $namespace
  337. * @param string $key
  338. * @return bool
  339. */
  340. public function setValue($value, $namespace, $key = '')
  341. {
  342. if ($namespace === 'traffic_limiter') {
  343. $this->_last_cache[$key] = $value;
  344. try {
  345. $value = Json::encode($this->_last_cache);
  346. } catch (JsonException $e) {
  347. error_log('Error encoding JSON for table "config", row "traffic_limiter": ' . $e->getMessage());
  348. return false;
  349. }
  350. }
  351. return $this->_exec(
  352. 'UPDATE "' . $this->_sanitizeIdentifier('config') .
  353. '" SET "value" = ? WHERE "id" = ?',
  354. array($value, strtoupper($namespace))
  355. );
  356. }
  357. /**
  358. * Load a value.
  359. *
  360. * @access public
  361. * @param string $namespace
  362. * @param string $key
  363. * @return string
  364. */
  365. public function getValue($namespace, $key = '')
  366. {
  367. $configKey = strtoupper($namespace);
  368. $value = $this->_getConfig($configKey);
  369. if ($value === '') {
  370. // initialize the row, so that setValue can rely on UPDATE queries
  371. $this->_exec(
  372. 'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
  373. '" VALUES(?,?)',
  374. array($configKey, '')
  375. );
  376. // migrate filesystem based salt into database
  377. $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
  378. if ($namespace === 'salt' && is_readable($file)) {
  379. $fs = new Filesystem(array('dir' => 'data'));
  380. $value = $fs->getValue('salt');
  381. $this->setValue($value, 'salt');
  382. if (!unlink($file)) {
  383. error_log('Error deleting migrated salt: ' . $file);
  384. }
  385. return $value;
  386. }
  387. }
  388. if ($value && $namespace === 'traffic_limiter') {
  389. try {
  390. $this->_last_cache = Json::decode($value);
  391. } catch (JsonException $e) {
  392. error_log('Error decoding JSON from table "config", row "traffic_limiter": ' . $e->getMessage());
  393. $this->_last_cache = array();
  394. }
  395. if (array_key_exists($key, $this->_last_cache)) {
  396. return $this->_last_cache[$key];
  397. }
  398. }
  399. return (string) $value;
  400. }
  401. /**
  402. * Returns up to batch size number of paste ids that have expired
  403. *
  404. * @access private
  405. * @param int $batchsize
  406. * @return array
  407. */
  408. protected function _getExpiredPastes($batchsize)
  409. {
  410. try {
  411. $statement = $this->_db->prepare(
  412. 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
  413. '" WHERE "expiredate" < ? AND "expiredate" != ? ' .
  414. ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?')
  415. );
  416. $statement->execute(array(time(), 0, $batchsize));
  417. return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
  418. } catch (PDOException $e) {
  419. error_log('Error while attempting to find expired pastes in the database: ' . $e->getMessage());
  420. return array();
  421. }
  422. }
  423. /**
  424. * @inheritDoc
  425. */
  426. public function getAllPastes()
  427. {
  428. return $this->_db->query(
  429. 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . '"'
  430. )->fetchAll(PDO::FETCH_COLUMN, 0);
  431. }
  432. /**
  433. * execute a statement
  434. *
  435. * @access private
  436. * @param string $sql
  437. * @param array $params
  438. * @throws PDOException
  439. * @return bool
  440. */
  441. private function _exec($sql, array $params)
  442. {
  443. $statement = $this->_db->prepare($sql);
  444. $position = 1;
  445. foreach ($params as &$parameter) {
  446. if (is_int($parameter)) {
  447. $statement->bindParam($position, $parameter, PDO::PARAM_INT);
  448. } elseif (is_string($parameter) && strlen($parameter) >= 4000) {
  449. $statement->bindParam($position, $parameter, PDO::PARAM_STR, strlen($parameter));
  450. } else {
  451. $statement->bindParam($position, $parameter);
  452. }
  453. ++$position;
  454. }
  455. $result = $statement->execute();
  456. $statement->closeCursor();
  457. return $result;
  458. }
  459. /**
  460. * run a select statement
  461. *
  462. * @access private
  463. * @param string $sql
  464. * @param array $params
  465. * @param bool $firstOnly if only the first row should be returned
  466. * @throws PDOException
  467. * @return array
  468. */
  469. private function _select($sql, array $params, $firstOnly = false)
  470. {
  471. $statement = $this->_db->prepare($sql);
  472. $statement->execute($params);
  473. if ($firstOnly) {
  474. $result = $statement->fetch(PDO::FETCH_ASSOC);
  475. if ($this->_type === 'oci' && is_array($result)) {
  476. // returned CLOB values are streams, convert these into strings
  477. $result = array_map('PrivateBin\Data\Database::_sanitizeClob', $result);
  478. }
  479. } elseif ($this->_type === 'oci') {
  480. // workaround for https://bugs.php.net/bug.php?id=46728
  481. $result = array();
  482. while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
  483. $result[] = array_map('PrivateBin\Data\Database::_sanitizeClob', $row);
  484. }
  485. } else {
  486. $result = $statement->fetchAll(PDO::FETCH_ASSOC);
  487. }
  488. $statement->closeCursor();
  489. return $result;
  490. }
  491. /**
  492. * get table list query, depending on the database type
  493. *
  494. * @access private
  495. * @param string $type
  496. * @throws Exception
  497. * @return string
  498. */
  499. private function _getTableQuery($type)
  500. {
  501. switch ($type) {
  502. case 'ibm':
  503. $sql = 'SELECT "tabname" FROM "SYSCAT"."TABLES"';
  504. break;
  505. case 'informix':
  506. $sql = 'SELECT "tabname" FROM "systables"';
  507. break;
  508. case 'mssql':
  509. // U: tables created by the user
  510. $sql = 'SELECT "name" FROM "sysobjects" '
  511. . 'WHERE "type" = \'U\' ORDER BY "name"';
  512. break;
  513. case 'mysql':
  514. $sql = 'SHOW TABLES';
  515. break;
  516. case 'oci':
  517. $sql = 'SELECT table_name FROM all_tables';
  518. break;
  519. case 'pgsql':
  520. $sql = 'SELECT "tablename" FROM "pg_catalog"."pg_tables" '
  521. . 'WHERE "schemaname" NOT IN (\'pg_catalog\', \'information_schema\')';
  522. break;
  523. case 'sqlite':
  524. $sql = 'SELECT "name" FROM "sqlite_master" WHERE "type"=\'table\' '
  525. . 'UNION ALL SELECT "name" FROM "sqlite_temp_master" '
  526. . 'WHERE "type"=\'table\' ORDER BY "name"';
  527. break;
  528. default:
  529. throw new Exception(
  530. "PDO type $type is currently not supported.", 5
  531. );
  532. }
  533. return $sql;
  534. }
  535. /**
  536. * get a value by key from the config table
  537. *
  538. * @access private
  539. * @param string $key
  540. * @return string
  541. */
  542. private function _getConfig($key)
  543. {
  544. try {
  545. $row = $this->_select(
  546. 'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') .
  547. '" WHERE "id" = ?', array($key), true
  548. );
  549. } catch (PDOException $e) {
  550. error_log('Error while attempting to fetch configuration key "' . $key . '" in the database: ' . $e->getMessage());
  551. return '';
  552. }
  553. return $row ? $row['value'] : '';
  554. }
  555. /**
  556. * get the primary key clauses, depending on the database driver
  557. *
  558. * @access private
  559. * @param string $key
  560. * @return array
  561. */
  562. private function _getPrimaryKeyClauses($key = 'dataid')
  563. {
  564. $main_key = $after_key = '';
  565. switch ($this->_type) {
  566. case 'mysql':
  567. case 'oci':
  568. $after_key = ", PRIMARY KEY (\"$key\")";
  569. break;
  570. default:
  571. $main_key = ' PRIMARY KEY';
  572. break;
  573. }
  574. return array($main_key, $after_key);
  575. }
  576. /**
  577. * get the data type, depending on the database driver
  578. *
  579. * PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB
  580. *
  581. * @access private
  582. * @return string
  583. */
  584. private function _getDataType()
  585. {
  586. switch ($this->_type) {
  587. case 'oci':
  588. return 'CLOB';
  589. case 'pgsql':
  590. return 'TEXT';
  591. default:
  592. return 'BLOB';
  593. }
  594. }
  595. /**
  596. * get the attachment type, depending on the database driver
  597. *
  598. * PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB
  599. *
  600. * @access private
  601. * @return string
  602. */
  603. private function _getAttachmentType()
  604. {
  605. switch ($this->_type) {
  606. case 'oci':
  607. return 'CLOB';
  608. case 'pgsql':
  609. return 'TEXT';
  610. default:
  611. return 'MEDIUMBLOB';
  612. }
  613. }
  614. /**
  615. * get the meta type, depending on the database driver
  616. *
  617. * OCI doesn't accept TEXT so it has to be VARCHAR2(4000)
  618. *
  619. * @access private
  620. * @return string
  621. */
  622. private function _getMetaType()
  623. {
  624. switch ($this->_type) {
  625. case 'oci':
  626. return 'VARCHAR2(4000)';
  627. default:
  628. return 'TEXT';
  629. }
  630. }
  631. /**
  632. * create the paste table
  633. *
  634. * @access private
  635. */
  636. private function _createPasteTable()
  637. {
  638. list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
  639. $attachmentType = $this->_getAttachmentType();
  640. $metaType = $this->_getMetaType();
  641. $this->_db->exec(
  642. 'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
  643. "\"dataid\" CHAR(16) NOT NULL$main_key, " .
  644. "\"data\" $attachmentType, " .
  645. '"expiredate" INT, ' .
  646. "\"meta\" $metaType$after_key )"
  647. );
  648. }
  649. /**
  650. * create the comment table
  651. *
  652. * @access private
  653. */
  654. private function _createCommentTable()
  655. {
  656. list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
  657. $dataType = $this->_getDataType();
  658. $this->_db->exec(
  659. 'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
  660. "\"dataid\" CHAR(16) NOT NULL$main_key, " .
  661. '"pasteid" CHAR(16), ' .
  662. '"parentid" CHAR(16), ' .
  663. "\"data\" $dataType, " .
  664. "\"vizhash\" $dataType, " .
  665. "\"postdate\" INT$after_key )"
  666. );
  667. if ($this->_type === 'oci') {
  668. $this->_db->exec(
  669. 'declare
  670. already_exists exception;
  671. columns_indexed exception;
  672. pragma exception_init( already_exists, -955 );
  673. pragma exception_init(columns_indexed, -1408);
  674. begin
  675. execute immediate \'create index "comment_parent" on "' . $this->_sanitizeIdentifier('comment') . '" ("pasteid")\';
  676. exception
  677. when already_exists or columns_indexed then
  678. NULL;
  679. end;'
  680. );
  681. } else {
  682. // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
  683. $this->_db->exec(
  684. 'CREATE INDEX "' .
  685. $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
  686. $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
  687. );
  688. }
  689. }
  690. /**
  691. * create the config table
  692. *
  693. * @access private
  694. */
  695. private function _createConfigTable()
  696. {
  697. list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
  698. $charType = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
  699. $textType = $this->_getMetaType();
  700. $this->_db->exec(
  701. 'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
  702. "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
  703. );
  704. $this->_exec(
  705. 'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
  706. '" VALUES(?,?)',
  707. array('VERSION', Controller::VERSION)
  708. );
  709. }
  710. /**
  711. * sanitizes CLOB values used with OCI
  712. *
  713. * From: https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field
  714. *
  715. * @access public
  716. * @static
  717. * @param int|string|resource $value
  718. * @return int|string
  719. */
  720. public static function _sanitizeClob($value)
  721. {
  722. if (is_resource($value)) {
  723. $value = stream_get_contents($value);
  724. }
  725. return $value;
  726. }
  727. /**
  728. * sanitizes identifiers
  729. *
  730. * @access private
  731. * @param string $identifier
  732. * @return string
  733. */
  734. private function _sanitizeIdentifier($identifier)
  735. {
  736. return preg_replace('/[^A-Za-z0-9_]+/', '', $this->_prefix . $identifier);
  737. }
  738. /**
  739. * check if the current database type supports dropping columns
  740. *
  741. * @access private
  742. * @return bool
  743. */
  744. private function _supportsDropColumn()
  745. {
  746. $supportsDropColumn = true;
  747. if ($this->_type === 'sqlite') {
  748. try {
  749. $row = $this->_select('SELECT sqlite_version() AS "v"', array(), true);
  750. $supportsDropColumn = (bool) version_compare($row['v'], '3.35.0', '>=');
  751. } catch (PDOException $e) {
  752. $supportsDropColumn = false;
  753. }
  754. }
  755. return $supportsDropColumn;
  756. }
  757. /**
  758. * upgrade the database schema from an old version
  759. *
  760. * @access private
  761. * @param string $oldversion
  762. */
  763. private function _upgradeDatabase($oldversion)
  764. {
  765. $dataType = $this->_getDataType();
  766. $attachmentType = $this->_getAttachmentType();
  767. if (version_compare($oldversion, '0.21', '<=')) {
  768. // create the meta column if necessary (pre 0.21 change)
  769. try {
  770. $this->_db->exec(
  771. 'SELECT "meta" FROM "' . $this->_sanitizeIdentifier('paste') . '" ' .
  772. ($this->_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
  773. );
  774. } catch (PDOException $e) {
  775. $this->_db->exec('ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
  776. }
  777. // SQLite only allows one ALTER statement at a time...
  778. $this->_db->exec(
  779. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  780. "\" ADD COLUMN \"attachment\" $attachmentType"
  781. );
  782. $this->_db->exec(
  783. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
  784. );
  785. // SQLite doesn't support MODIFY, but it allows TEXT of similar
  786. // size as BLOB, so there is no need to change it there
  787. if ($this->_type !== 'sqlite') {
  788. $this->_db->exec(
  789. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  790. "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType"
  791. );
  792. $this->_db->exec(
  793. 'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') .
  794. "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " .
  795. "MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType"
  796. );
  797. } else {
  798. $this->_db->exec(
  799. 'CREATE UNIQUE INDEX IF NOT EXISTS "' .
  800. $this->_sanitizeIdentifier('paste_dataid') . '" ON "' .
  801. $this->_sanitizeIdentifier('paste') . '" ("dataid")'
  802. );
  803. $this->_db->exec(
  804. 'CREATE UNIQUE INDEX IF NOT EXISTS "' .
  805. $this->_sanitizeIdentifier('comment_dataid') . '" ON "' .
  806. $this->_sanitizeIdentifier('comment') . '" ("dataid")'
  807. );
  808. }
  809. // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
  810. $this->_db->exec(
  811. 'CREATE INDEX "' .
  812. $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
  813. $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
  814. );
  815. }
  816. if (version_compare($oldversion, '1.3', '<=')) {
  817. // SQLite doesn't support MODIFY, but it allows TEXT of similar
  818. // size as BLOB and PostgreSQL uses TEXT, so there is no need
  819. // to change it there
  820. if ($this->_type !== 'sqlite' && $this->_type !== 'pgsql') {
  821. $this->_db->exec(
  822. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  823. "\" MODIFY COLUMN \"data\" $attachmentType"
  824. );
  825. }
  826. }
  827. if (version_compare($oldversion, '1.7.1', '<=')) {
  828. if ($this->_supportsDropColumn()) {
  829. $this->_db->exec(
  830. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  831. '" DROP COLUMN "postdate"'
  832. );
  833. }
  834. }
  835. if (version_compare($oldversion, '1.7.8', '<=')) {
  836. if ($this->_supportsDropColumn()) {
  837. $this->_db->exec(
  838. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  839. '" DROP COLUMN "opendiscussion"'
  840. );
  841. $this->_db->exec(
  842. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  843. '" DROP COLUMN "burnafterreading"'
  844. );
  845. $this->_db->exec(
  846. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  847. '" DROP COLUMN "attachment"'
  848. );
  849. $this->_db->exec(
  850. 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
  851. '" DROP COLUMN "attachmentname"'
  852. );
  853. $this->_db->exec(
  854. 'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') .
  855. '" DROP COLUMN "nickname"'
  856. );
  857. }
  858. }
  859. $this->_exec(
  860. 'UPDATE "' . $this->_sanitizeIdentifier('config') .
  861. '" SET "value" = ? WHERE "id" = ?',
  862. array(Controller::VERSION, 'VERSION')
  863. );
  864. }
  865. }