318 lines
9.2 KiB
PHP
318 lines
9.2 KiB
PHP
<?php
|
|
/**
|
|
* PrivateBin
|
|
*
|
|
* a zero-knowledge paste bin
|
|
*
|
|
* @link https://github.com/PrivateBin/PrivateBin
|
|
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
|
|
* @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
|
|
* @version 1.2.1
|
|
*/
|
|
|
|
namespace PrivateBin\Model;
|
|
|
|
use Exception;
|
|
use PrivateBin\Controller;
|
|
use PrivateBin\Filter;
|
|
use PrivateBin\Persistence\ServerSalt;
|
|
|
|
/**
|
|
* Paste
|
|
*
|
|
* Model of a PrivateBin paste.
|
|
*/
|
|
class Paste extends AbstractModel
|
|
{
|
|
/**
|
|
* Token for challenge/response.
|
|
*
|
|
* @access protected
|
|
* @var string
|
|
*/
|
|
protected $_token = '';
|
|
|
|
/**
|
|
* Get paste data.
|
|
*
|
|
* @access public
|
|
* @throws Exception
|
|
* @return array
|
|
*/
|
|
public function get()
|
|
{
|
|
// return cached result if one is found
|
|
if (array_key_exists('adata', $this->_data) || array_key_exists('data', $this->_data)) {
|
|
return $this->_data;
|
|
}
|
|
|
|
$data = $this->_store->read($this->getId());
|
|
if ($data === false) {
|
|
throw new Exception(Controller::GENERIC_ERROR, 64);
|
|
}
|
|
|
|
// check if paste has expired and delete it if neccessary.
|
|
if (array_key_exists('expire_date', $data['meta'])) {
|
|
if ($data['meta']['expire_date'] < time()) {
|
|
$this->delete();
|
|
throw new Exception(Controller::GENERIC_ERROR, 63);
|
|
}
|
|
// We kindly provide the remaining time before expiration (in seconds)
|
|
$data['meta']['time_to_live'] = $data['meta']['expire_date'] - time();
|
|
unset($data['meta']['expire_date']);
|
|
}
|
|
|
|
// check if non-expired burn after reading paste needs to be deleted,
|
|
// but don't delete it if an incorrect token was sent
|
|
if (
|
|
(
|
|
(array_key_exists('adata', $data) && $data['adata'][3] === 1) ||
|
|
(array_key_exists('burnafterreading', $data['meta']) && $data['meta']['burnafterreading'])
|
|
) && (
|
|
!array_key_exists('challenge', $data['meta']) ||
|
|
$this->_token === $data['meta']['challenge']
|
|
)
|
|
) {
|
|
$this->delete();
|
|
}
|
|
|
|
// set formatter for the view in version 1 pastes.
|
|
if (array_key_exists('data', $data) && !array_key_exists('formatter', $data['meta'])) {
|
|
// support < 0.21 syntax highlighting
|
|
if (array_key_exists('syntaxcoloring', $data['meta']) && $data['meta']['syntaxcoloring'] === true) {
|
|
$data['meta']['formatter'] = 'syntaxhighlighting';
|
|
} else {
|
|
$data['meta']['formatter'] = $this->_conf->getKey('defaultformatter');
|
|
}
|
|
}
|
|
|
|
// support old paste format with server wide salt
|
|
if (!array_key_exists('salt', $data['meta'])) {
|
|
$data['meta']['salt'] = ServerSalt::get();
|
|
}
|
|
$data['comments'] = array_values($this->getComments());
|
|
$data['comment_count'] = count($data['comments']);
|
|
$data['comment_offset'] = 0;
|
|
$data['@context'] = '?jsonld=paste';
|
|
$this->_data = $data;
|
|
|
|
return $this->_data;
|
|
}
|
|
|
|
/**
|
|
* Store the paste's data.
|
|
*
|
|
* @access public
|
|
* @throws Exception
|
|
*/
|
|
public function store()
|
|
{
|
|
// Check for improbable collision.
|
|
if ($this->exists()) {
|
|
throw new Exception('You are unlucky. Try again.', 75);
|
|
}
|
|
|
|
$this->_data['meta']['created'] = time();
|
|
$this->_data['meta']['salt'] = serversalt::generate();
|
|
// if a challenge was sent, we store the HMAC of paste ID & challenge
|
|
if (array_key_exists('challenge', $this->_data['meta'])) {
|
|
$this->_data['meta']['challenge'] = base64_encode(hash_hmac(
|
|
'sha256', $this->getId(), base64_decode($this->_data['meta']['challenge']), true
|
|
));
|
|
}
|
|
|
|
// store paste
|
|
if (
|
|
$this->_store->create(
|
|
$this->getId(),
|
|
$this->_data
|
|
) === false
|
|
) {
|
|
throw new Exception('Error saving paste. Sorry.', 76);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the paste.
|
|
*
|
|
* @access public
|
|
* @throws Exception
|
|
*/
|
|
public function delete()
|
|
{
|
|
$this->_store->delete($this->getId());
|
|
}
|
|
|
|
/**
|
|
* Test if paste exists in store.
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
public function exists()
|
|
{
|
|
return $this->_store->exists($this->getId());
|
|
}
|
|
|
|
/**
|
|
* Get a comment, optionally a specific instance.
|
|
*
|
|
* @access public
|
|
* @param string $parentId
|
|
* @param string $commentId
|
|
* @throws Exception
|
|
* @return Comment
|
|
*/
|
|
public function getComment($parentId, $commentId = '')
|
|
{
|
|
if (!$this->exists()) {
|
|
throw new Exception('Invalid data.', 62);
|
|
}
|
|
$comment = new Comment($this->_conf, $this->_store);
|
|
$comment->setPaste($this);
|
|
$comment->setParentId($parentId);
|
|
if ($commentId !== '') {
|
|
$comment->setId($commentId);
|
|
}
|
|
return $comment;
|
|
}
|
|
|
|
/**
|
|
* Get all comments, if any.
|
|
*
|
|
* @access public
|
|
* @return array
|
|
*/
|
|
public function getComments()
|
|
{
|
|
return $this->_store->readComments($this->getId());
|
|
}
|
|
|
|
/**
|
|
* Generate the "delete" token.
|
|
*
|
|
* The token is the hmac of the pastes ID signed with the server salt.
|
|
* The paste can be deleted by calling:
|
|
* https://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
|
|
*
|
|
* @access public
|
|
* @return string
|
|
*/
|
|
public function getDeleteToken()
|
|
{
|
|
if (!array_key_exists('salt', $this->_data['meta'])) {
|
|
$this->get();
|
|
}
|
|
return hash_hmac(
|
|
$this->_conf->getKey('zerobincompatibility') ? 'sha1' : 'sha256',
|
|
$this->getId(),
|
|
$this->_data['meta']['salt']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if paste has discussions enabled.
|
|
*
|
|
* @access public
|
|
* @throws Exception
|
|
* @return bool
|
|
*/
|
|
public function isOpendiscussion()
|
|
{
|
|
if (!array_key_exists('adata', $this->_data) && !array_key_exists('data', $this->_data)) {
|
|
$this->get();
|
|
}
|
|
return
|
|
(array_key_exists('adata', $this->_data) && $this->_data['adata'][2] === 1) ||
|
|
(array_key_exists('opendiscussion', $this->_data['meta']) && $this->_data['meta']['opendiscussion']);
|
|
}
|
|
|
|
/**
|
|
* Check if paste challenge matches provided token.
|
|
*
|
|
* @access public
|
|
* @param string $token
|
|
* @throws Exception
|
|
* @return bool
|
|
*/
|
|
public function isTokenCorrect($token)
|
|
{
|
|
$this->_token = $token;
|
|
if (!array_key_exists('challenge', $this->_data['meta'])) {
|
|
$this->get();
|
|
}
|
|
if (array_key_exists('challenge', $this->_data['meta'])) {
|
|
return Filter::slowEquals($token, $this->_data['meta']['challenge']);
|
|
}
|
|
// paste created without challenge, accept every token sent
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if paste salt based HMAC matches provided delete token.
|
|
*
|
|
* @access public
|
|
* @param string $deletetoken
|
|
* @throws Exception
|
|
* @return bool
|
|
*/
|
|
public function isDeleteTokenCorrect($deletetoken)
|
|
{
|
|
return Filter::slowEquals($deletetoken, $this->getDeleteToken());
|
|
}
|
|
|
|
/**
|
|
* Sanitizes data to conform with current configuration.
|
|
*
|
|
* @access protected
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
protected function _sanitize(array $data)
|
|
{
|
|
$expiration = $data['meta']['expire'];
|
|
unset($data['meta']['expire']);
|
|
$expire_options = $this->_conf->getSection('expire_options');
|
|
if (array_key_exists($expiration, $expire_options)) {
|
|
$expire = $expire_options[$expiration];
|
|
} else {
|
|
// using getKey() to ensure a default value is present
|
|
$expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
|
|
}
|
|
if ($expire > 0) {
|
|
$data['meta']['expire_date'] = time() + $expire;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Validate data.
|
|
*
|
|
* @access protected
|
|
* @param array $data
|
|
* @throws Exception
|
|
*/
|
|
protected function _validate(array $data)
|
|
{
|
|
// reject invalid or disabled formatters
|
|
if (!array_key_exists($data['adata'][1], $this->_conf->getSection('formatter_options'))) {
|
|
throw new Exception('Invalid data.', 75);
|
|
}
|
|
|
|
// discussion requested, but disabled in config or burn after reading requested as well, or invalid integer
|
|
if (
|
|
($data['adata'][2] === 1 && ( // open discussion flag
|
|
!$this->_conf->getKey('discussion') ||
|
|
$data['adata'][3] === 1 // burn after reading flag
|
|
)) ||
|
|
($data['adata'][2] !== 0 && $data['adata'][2] !== 1)
|
|
) {
|
|
throw new Exception('Invalid data.', 74);
|
|
}
|
|
|
|
// reject invalid burn after reading
|
|
if ($data['adata'][3] !== 0 && $data['adata'][3] !== 1) {
|
|
throw new Exception('Invalid data.', 73);
|
|
}
|
|
}
|
|
}
|