Paste.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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\Model;
  12. use PrivateBin\Controller;
  13. use PrivateBin\Exception\TranslatedException;
  14. use PrivateBin\Persistence\ServerSalt;
  15. /**
  16. * Paste
  17. *
  18. * Model of a PrivateBin paste.
  19. */
  20. class Paste extends AbstractModel
  21. {
  22. /**
  23. * authenticated data index of paste formatter (plaintext/syntaxhighlighting/markdown)
  24. *
  25. * @const int
  26. */
  27. const ADATA_FORMATTER = 1;
  28. /**
  29. * authenticated data index of open-discussion flag (0/1)
  30. *
  31. * @const int
  32. */
  33. const ADATA_OPEN_DISCUSSION = 2;
  34. /**
  35. * authenticated data index of burn-after-reading flag (0/1)
  36. *
  37. * @const int
  38. */
  39. const ADATA_BURN_AFTER_READING = 3;
  40. /**
  41. * Get paste data.
  42. *
  43. * @access public
  44. * @throws TranslatedException
  45. * @return array
  46. */
  47. public function get()
  48. {
  49. $data = $this->_store->read($this->getId());
  50. if ($data === false) {
  51. throw new TranslatedException(Controller::GENERIC_ERROR, 64);
  52. }
  53. // check if paste has expired and delete it if necessary.
  54. if (array_key_exists('expire_date', $data['meta'])) {
  55. $now = time();
  56. if ($data['meta']['expire_date'] < $now) {
  57. $this->delete();
  58. throw new TranslatedException(Controller::GENERIC_ERROR, 63);
  59. }
  60. // We kindly provide the remaining time before expiration (in seconds)
  61. $data['meta']['time_to_live'] = $data['meta']['expire_date'] - $now;
  62. unset($data['meta']['expire_date']);
  63. }
  64. if (array_key_exists('created', $data['meta'])) {
  65. unset($data['meta']['created']);
  66. }
  67. // check if non-expired burn after reading paste needs to be deleted
  68. if (
  69. array_key_exists('adata', $data) &&
  70. $data['adata'][self::ADATA_BURN_AFTER_READING] === 1
  71. ) {
  72. $this->delete();
  73. }
  74. $data['comments'] = array_values($this->getComments());
  75. $data['comment_count'] = count($data['comments']);
  76. $data['comment_offset'] = 0;
  77. $data['@context'] = '?jsonld=paste';
  78. $this->_data = $data;
  79. return $this->_data;
  80. }
  81. /**
  82. * Store the paste's data.
  83. *
  84. * @access public
  85. * @throws TranslatedException
  86. */
  87. public function store()
  88. {
  89. // Check for improbable collision.
  90. if ($this->exists()) {
  91. throw new TranslatedException(self::COLLISION_ERROR, 75);
  92. }
  93. $this->_data['meta']['salt'] = ServerSalt::generate();
  94. // store paste
  95. if (
  96. $this->_store->create(
  97. $this->getId(),
  98. $this->_data
  99. ) === false
  100. ) {
  101. throw new TranslatedException('Error saving document. Sorry.', 76);
  102. }
  103. }
  104. /**
  105. * Delete the paste.
  106. *
  107. * @access public
  108. */
  109. public function delete()
  110. {
  111. $this->_store->delete($this->getId());
  112. }
  113. /**
  114. * Test if paste exists in store.
  115. *
  116. * @access public
  117. * @return bool
  118. */
  119. public function exists()
  120. {
  121. return $this->_store->exists($this->getId());
  122. }
  123. /**
  124. * Get a comment, optionally a specific instance.
  125. *
  126. * @access public
  127. * @param string $parentId
  128. * @param string $commentId
  129. * @throws TranslatedException
  130. * @return Comment
  131. */
  132. public function getComment($parentId, $commentId = '')
  133. {
  134. if (!$this->exists()) {
  135. throw new TranslatedException(self::INVALID_DATA_ERROR, 62);
  136. }
  137. $comment = new Comment($this->_conf, $this->_store);
  138. $comment->setPaste($this);
  139. $comment->setParentId($parentId);
  140. if ($commentId !== '') {
  141. $comment->setId($commentId);
  142. }
  143. return $comment;
  144. }
  145. /**
  146. * Get all comments, if any.
  147. *
  148. * @access public
  149. * @return array
  150. */
  151. public function getComments()
  152. {
  153. if ($this->_conf->getKey('discussiondatedisplay')) {
  154. return $this->_store->readComments($this->getId());
  155. }
  156. return array_map(function ($comment) {
  157. if (array_key_exists('created', $comment['meta'])) {
  158. unset($comment['meta']['created']);
  159. }
  160. return $comment;
  161. }, $this->_store->readComments($this->getId()));
  162. }
  163. /**
  164. * Generate the "delete" token.
  165. *
  166. * The token is the hmac of the pastes ID signed with the server salt.
  167. * The paste can be deleted by calling:
  168. * https://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
  169. *
  170. * @access public
  171. * @return string
  172. */
  173. public function getDeleteToken()
  174. {
  175. if (!array_key_exists('salt', $this->_data['meta'])) {
  176. $this->get();
  177. }
  178. return hash_hmac('sha256', $this->getId(), $this->_data['meta']['salt']);
  179. }
  180. /**
  181. * Check if paste has discussions enabled.
  182. *
  183. * @access public
  184. * @return bool
  185. */
  186. public function isOpendiscussion()
  187. {
  188. if (!array_key_exists('adata', $this->_data) && !array_key_exists('data', $this->_data)) {
  189. $this->get();
  190. }
  191. return array_key_exists('adata', $this->_data) &&
  192. $this->_data['adata'][self::ADATA_OPEN_DISCUSSION] === 1;
  193. }
  194. /**
  195. * Sanitizes data to conform with current configuration.
  196. *
  197. * @access protected
  198. * @param array $data
  199. */
  200. protected function _sanitize(array &$data)
  201. {
  202. $expiration = $data['meta']['expire'] ?? 0;
  203. unset($data['meta']['expire']);
  204. $expire_options = $this->_conf->getSection('expire_options');
  205. if (array_key_exists($expiration, $expire_options)) {
  206. $expire = $expire_options[$expiration];
  207. } else {
  208. // using getKey() to ensure a default value is present
  209. $expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
  210. }
  211. if ($expire > 0) {
  212. $data['meta']['expire_date'] = time() + $expire;
  213. }
  214. }
  215. /**
  216. * Validate data.
  217. *
  218. * @access protected
  219. * @param array $data
  220. * @throws TranslatedException
  221. */
  222. protected function _validate(array &$data)
  223. {
  224. // reject invalid or disabled formatters
  225. if (!array_key_exists($data['adata'][self::ADATA_FORMATTER], $this->_conf->getSection('formatter_options'))) {
  226. throw new TranslatedException(self::INVALID_DATA_ERROR, 75);
  227. }
  228. // discussion requested, but disabled in config or burn after reading requested as well, or invalid integer
  229. if (
  230. ($data['adata'][self::ADATA_OPEN_DISCUSSION] === 1 && (
  231. !$this->_conf->getKey('discussion') ||
  232. $data['adata'][self::ADATA_BURN_AFTER_READING] === 1
  233. )) ||
  234. ($data['adata'][self::ADATA_OPEN_DISCUSSION] !== 0 && $data['adata'][self::ADATA_OPEN_DISCUSSION] !== 1)
  235. ) {
  236. throw new TranslatedException(self::INVALID_DATA_ERROR, 74);
  237. }
  238. // reject invalid burn after reading
  239. if (
  240. $data['adata'][self::ADATA_BURN_AFTER_READING] !== 0 &&
  241. $data['adata'][self::ADATA_BURN_AFTER_READING] !== 1
  242. ) {
  243. throw new TranslatedException(self::INVALID_DATA_ERROR, 73);
  244. }
  245. }
  246. }