Database.php 30 KB

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