1
0

data.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 0.22
  11. */
  12. namespace PrivateBin\data;
  13. /**
  14. * privatebin_data
  15. *
  16. * Model for data access, implemented as a singleton.
  17. */
  18. class data extends AbstractData
  19. {
  20. /**
  21. * directory where data is stored
  22. *
  23. * @access private
  24. * @static
  25. * @var string
  26. */
  27. private static $_dir = 'data/';
  28. /**
  29. * get instance of singleton
  30. *
  31. * @access public
  32. * @static
  33. * @param array $options
  34. * @return privatebin_data
  35. */
  36. public static function getInstance($options = null)
  37. {
  38. // if given update the data directory
  39. if (
  40. is_array($options) &&
  41. array_key_exists('dir', $options)
  42. ) {
  43. self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
  44. }
  45. // if needed initialize the singleton
  46. if (!(self::$_instance instanceof privatebin_data)) {
  47. self::$_instance = new self;
  48. self::_init();
  49. }
  50. return self::$_instance;
  51. }
  52. /**
  53. * Create a paste.
  54. *
  55. * @access public
  56. * @param string $pasteid
  57. * @param array $paste
  58. * @return bool
  59. */
  60. public function create($pasteid, $paste)
  61. {
  62. $storagedir = self::_dataid2path($pasteid);
  63. if (is_file($storagedir . $pasteid)) {
  64. return false;
  65. }
  66. if (!is_dir($storagedir)) {
  67. mkdir($storagedir, 0705, true);
  68. }
  69. return (bool) @file_put_contents($storagedir . $pasteid, json_encode($paste));
  70. }
  71. /**
  72. * Read a paste.
  73. *
  74. * @access public
  75. * @param string $pasteid
  76. * @return stdClass|false
  77. */
  78. public function read($pasteid)
  79. {
  80. if (!$this->exists($pasteid)) {
  81. return false;
  82. }
  83. $paste = json_decode(
  84. file_get_contents(self::_dataid2path($pasteid) . $pasteid)
  85. );
  86. if (property_exists($paste->meta, 'attachment')) {
  87. $paste->attachment = $paste->meta->attachment;
  88. unset($paste->meta->attachment);
  89. if (property_exists($paste->meta, 'attachmentname')) {
  90. $paste->attachmentname = $paste->meta->attachmentname;
  91. unset($paste->meta->attachmentname);
  92. }
  93. }
  94. return $paste;
  95. }
  96. /**
  97. * Delete a paste and its discussion.
  98. *
  99. * @access public
  100. * @param string $pasteid
  101. * @return void
  102. */
  103. public function delete($pasteid)
  104. {
  105. // Delete the paste itself.
  106. @unlink(self::_dataid2path($pasteid) . $pasteid);
  107. // Delete discussion if it exists.
  108. $discdir = self::_dataid2discussionpath($pasteid);
  109. if (is_dir($discdir)) {
  110. // Delete all files in discussion directory
  111. $dir = dir($discdir);
  112. while (false !== ($filename = $dir->read())) {
  113. if (is_file($discdir . $filename)) {
  114. @unlink($discdir . $filename);
  115. }
  116. }
  117. $dir->close();
  118. // Delete the discussion directory.
  119. @rmdir($discdir);
  120. }
  121. }
  122. /**
  123. * Test if a paste exists.
  124. *
  125. * @access public
  126. * @param string $pasteid
  127. * @return void
  128. */
  129. public function exists($pasteid)
  130. {
  131. return is_file(self::_dataid2path($pasteid) . $pasteid);
  132. }
  133. /**
  134. * Create a comment in a paste.
  135. *
  136. * @access public
  137. * @param string $pasteid
  138. * @param string $parentid
  139. * @param string $commentid
  140. * @param array $comment
  141. * @return bool
  142. */
  143. public function createComment($pasteid, $parentid, $commentid, $comment)
  144. {
  145. $storagedir = self::_dataid2discussionpath($pasteid);
  146. $filename = $pasteid . '.' . $commentid . '.' . $parentid;
  147. if (is_file($storagedir . $filename)) {
  148. return false;
  149. }
  150. if (!is_dir($storagedir)) {
  151. mkdir($storagedir, 0705, true);
  152. }
  153. return (bool) @file_put_contents($storagedir . $filename, json_encode($comment));
  154. }
  155. /**
  156. * Read all comments of paste.
  157. *
  158. * @access public
  159. * @param string $pasteid
  160. * @return array
  161. */
  162. public function readComments($pasteid)
  163. {
  164. $comments = array();
  165. $discdir = self::_dataid2discussionpath($pasteid);
  166. if (is_dir($discdir)) {
  167. // Delete all files in discussion directory
  168. $dir = dir($discdir);
  169. while (false !== ($filename = $dir->read())) {
  170. // Filename is in the form pasteid.commentid.parentid:
  171. // - pasteid is the paste this reply belongs to.
  172. // - commentid is the comment identifier itself.
  173. // - parentid is the comment this comment replies to (It can be pasteid)
  174. if (is_file($discdir . $filename)) {
  175. $comment = json_decode(file_get_contents($discdir . $filename));
  176. $items = explode('.', $filename);
  177. // Add some meta information not contained in file.
  178. $comment->id = $items[1];
  179. $comment->parentid = $items[2];
  180. // Store in array
  181. $key = $this->getOpenSlot($comments, (int) $comment->meta->postdate);
  182. $comments[$key] = $comment;
  183. }
  184. }
  185. $dir->close();
  186. // Sort comments by date, oldest first.
  187. ksort($comments);
  188. }
  189. return $comments;
  190. }
  191. /**
  192. * Test if a comment exists.
  193. *
  194. * @access public
  195. * @param string $pasteid
  196. * @param string $parentid
  197. * @param string $commentid
  198. * @return void
  199. */
  200. public function existsComment($pasteid, $parentid, $commentid)
  201. {
  202. return is_file(
  203. self::_dataid2discussionpath($pasteid) .
  204. $pasteid . '.' . $commentid . '.' . $parentid
  205. );
  206. }
  207. /**
  208. * Returns up to batch size number of paste ids that have expired
  209. *
  210. * @access private
  211. * @param int $batchsize
  212. * @return array
  213. */
  214. protected function _getExpiredPastes($batchsize)
  215. {
  216. $pastes = array();
  217. $firstLevel = array_filter(
  218. scandir(self::$_dir),
  219. array('self', '_isFirstLevelDir')
  220. );
  221. if (count($firstLevel) > 0) {
  222. // try at most 10 times the $batchsize pastes before giving up
  223. for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
  224. $firstKey = array_rand($firstLevel);
  225. $secondLevel = array_filter(
  226. scandir(self::$_dir . $firstLevel[$firstKey]),
  227. array('self', '_isSecondLevelDir')
  228. );
  229. // skip this folder in the next checks if it is empty
  230. if (count($secondLevel) == 0) {
  231. unset($firstLevel[$firstKey]);
  232. continue;
  233. }
  234. $secondKey = array_rand($secondLevel);
  235. $path = self::$_dir . $firstLevel[$firstKey] .
  236. DIRECTORY_SEPARATOR . $secondLevel[$secondKey];
  237. if (!is_dir($path)) {
  238. continue;
  239. }
  240. $thirdLevel = array_filter(
  241. scandir($path),
  242. array('PrivateBin\\model\\paste', 'isValidId')
  243. );
  244. if (count($thirdLevel) == 0) {
  245. continue;
  246. }
  247. $thirdKey = array_rand($thirdLevel);
  248. $pasteid = $thirdLevel[$thirdKey];
  249. if (in_array($pasteid, $pastes)) {
  250. continue;
  251. }
  252. if ($this->exists($pasteid)) {
  253. $data = $this->read($pasteid);
  254. if (
  255. property_exists($data->meta, 'expire_date') &&
  256. $data->meta->expire_date < time()
  257. ) {
  258. $pastes[] = $pasteid;
  259. if (count($pastes) >= $batchsize) {
  260. break;
  261. }
  262. }
  263. }
  264. }
  265. }
  266. return $pastes;
  267. }
  268. /**
  269. * initialize privatebin
  270. *
  271. * @access private
  272. * @static
  273. * @return void
  274. */
  275. private static function _init()
  276. {
  277. // Create storage directory if it does not exist.
  278. if (!is_dir(self::$_dir)) {
  279. mkdir(self::$_dir, 0705);
  280. }
  281. // Create .htaccess file if it does not exist.
  282. if (!is_file(self::$_dir . '.htaccess')) {
  283. file_put_contents(
  284. self::$_dir . '.htaccess',
  285. 'Allow from none' . PHP_EOL .
  286. 'Deny from all' . PHP_EOL
  287. );
  288. }
  289. }
  290. /**
  291. * Convert paste id to storage path.
  292. *
  293. * The idea is to creates subdirectories in order to limit the number of files per directory.
  294. * (A high number of files in a single directory can slow things down.)
  295. * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
  296. * High-trafic websites may want to deepen the directory structure (like Squid does).
  297. *
  298. * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
  299. *
  300. * @access private
  301. * @static
  302. * @param string $dataid
  303. * @return void
  304. */
  305. private static function _dataid2path($dataid)
  306. {
  307. return self::$_dir . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
  308. substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
  309. }
  310. /**
  311. * Convert paste id to discussion storage path.
  312. *
  313. * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
  314. *
  315. * @access private
  316. * @static
  317. * @param string $dataid
  318. * @return void
  319. */
  320. private static function _dataid2discussionpath($dataid)
  321. {
  322. return self::_dataid2path($dataid) . $dataid .
  323. '.discussion' . DIRECTORY_SEPARATOR;
  324. }
  325. /**
  326. * Check that the given element is a valid first level directory.
  327. *
  328. * @access private
  329. * @static
  330. * @param string $element
  331. * @return bool
  332. */
  333. private static function _isFirstLevelDir($element)
  334. {
  335. return self::_isSecondLevelDir($element) &&
  336. is_dir(self::$_dir . DIRECTORY_SEPARATOR . $element);
  337. }
  338. /**
  339. * Check that the given element is a valid second level directory.
  340. *
  341. * @access private
  342. * @static
  343. * @param string $element
  344. * @return bool
  345. */
  346. private static function _isSecondLevelDir($element)
  347. {
  348. return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
  349. }
  350. }