Database.php 31 KB

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