mostly finished with data model refactoring

This commit is contained in:
El RIDO 2015-09-27 03:03:55 +02:00
parent 211d3e4622
commit 694138c5d4
14 changed files with 899 additions and 369 deletions

View file

@ -99,6 +99,17 @@ class configuration
} }
continue; continue;
} }
// provide different defaults for database model
elseif ($section == 'model_options' && $this->_configuration['model']['class'] == 'zerobin_db')
{
$values = array(
'dsn' => 'sqlite:' . PATH . 'data/db.sq3',
'tbl' => null,
'usr' => null,
'pwd' => null,
'opt' => array(PDO::ATTR_PERSISTENT => true),
);
}
foreach ($values as $key => $val) foreach ($values as $key => $val)
{ {
if ($key == 'dir') if ($key == 'dir')

View file

@ -80,19 +80,6 @@ class filter
return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . i18n::_($iec[$i]); return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . i18n::_($iec[$i]);
} }
/**
* validate paste ID
*
* @access public
* @static
* @param string $dataid
* @return bool
*/
public static function is_valid_paste_id($dataid)
{
return (bool) preg_match('#\A[a-f\d]{16}\z#', $dataid);
}
/** /**
* fixed time string comparison operation to prevent timing attacks * fixed time string comparison operation to prevent timing attacks
* https://crackstation.net/hashing-security.htm?=rd#slowequals * https://crackstation.net/hashing-security.htm?=rd#slowequals

71
lib/model.php Normal file
View file

@ -0,0 +1,71 @@
<?php
/**
* ZeroBin
*
* a zero-knowledge paste bin
*
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21.1
*/
/**
* model
*
* Factory of ZeroBin instance models.
*/
class model
{
/**
* Configuration.
*
* @var configuration
*/
private $_conf;
/**
* Data storage.
*
* @var zerobin_abstract
*/
private $_store = null;
/**
* Factory constructor.
*
* @param configuration $conf
*/
public function __construct(configuration $conf)
{
$this->_conf = $conf;
}
/**
* Get a paste, optionally a specific instance.
*
* @param string $pasteId
* @return model_paste
*/
public function getPaste($pasteId = null)
{
$paste = new model_paste($this->_conf, $this->_getStore());
if ($pasteId !== null) $paste->setId($pasteId);
return $paste;
}
/**
* Gets, and creates if neccessary, a store object
*/
private function _getStore()
{
if ($this->_store === null)
{
$this->_store = forward_static_call(
array($this->_conf->getKey('class', 'model'), 'getInstance'),
$this->_conf->getSection('model_options')
);
}
return $this->_store;
}
}

156
lib/model/abstract.php Normal file
View file

@ -0,0 +1,156 @@
<?php
/**
* ZeroBin
*
* a zero-knowledge paste bin
*
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21.1
*/
/**
* model_abstract
*
* Abstract model for ZeroBin objects.
*/
abstract class model_abstract
{
/**
* Instance ID.
*
* @access protected
* @var string
*/
protected $_id = '';
/**
* Instance data.
*
* @access protected
* @var stdClass
*/
protected $_data;
/**
* Configuration.
*
* @access protected
* @var configuration
*/
protected $_conf;
/**
* Data storage.
*
* @access protected
* @var zerobin_abstract
*/
protected $_store;
/**
* Instance constructor.
*
* @access public
* @param configuration $configuration
* @param zerobin_abstract $storage
* @return void
*/
public function __construct(configuration $configuration, zerobin_abstract $storage)
{
$this->_conf = $configuration;
$this->_store = $storage;
$this->_data = new stdClass;
$this->_data->meta = new stdClass;
}
/**
* Get ID.
*
* @access public
* @return string
*/
public function getId()
{
return $this->_id;
}
/**
* Set ID.
*
* @access public
* @throws Exception
* @return void
*/
public function setId($id)
{
if (!self::isValidId($id)) throw new Exception('Invalid paste ID.', 60);
$this->_id = $id;
}
/**
* Set data and recalculate ID.
*
* @access public
* @param string $data
* @throws Exception
* @return void
*/
public function setData($data)
{
if (!sjcl::isValid($data)) throw new Exception('Invalid data.', 61);
$this->_data->data = $data;
// We just want a small hash to avoid collisions:
// Half-MD5 (64 bits) will do the trick
$this->setId(substr(hash('md5', $data), 0, 16));
}
/**
* Get instance data.
*
* @access public
* @return stdObject
*/
abstract public function get();
/**
* Store the instance's data.
*
* @access public
* @throws Exception
* @return void
*/
abstract public function store();
/**
* Delete the current instance.
*
* @access public
* @throws Exception
* @return void
*/
abstract public function delete();
/**
* Test if current instance exists in store.
*
* @access public
* @return bool
*/
abstract public function exists();
/**
* Validate ID.
*
* @access public
* @static
* @param string $id
* @return bool
*/
public static function isValidId($id)
{
return (bool) preg_match('#\A[a-f\d]{16}\z#', (string) $id);
}
}

181
lib/model/comment.php Normal file
View file

@ -0,0 +1,181 @@
<?php
/**
* ZeroBin
*
* a zero-knowledge paste bin
*
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21.1
*/
/**
* model_comment
*
* Model of a ZeroBin comment.
*/
class model_comment extends model_abstract
{
/**
* Instance's parent.
*
* @access private
* @var model_paste
*/
private $_paste;
/**
* Get comment data.
*
* @access public
* @throws Exception
* @return stdObject
*/
public function get()
{
// @todo add support to read specific comment
$comments = $this->_store->readComments($this->getPaste()->getId());
foreach ($comments as $comment) {
if (
$comment->meta->parentid == $this->getParentId() &&
$comment->meta->commentid == $this->getId()
) {
$this->_data = $comment;
break;
}
}
return $this->_data;
}
/**
* Store the comment's data.
*
* @access public
* @throws Exception
* @return void
*/
public function store()
{
// Make sure paste exists.
$pasteid = $this->getPaste()->getId();
if (!$this->getPaste()->exists())
throw new Exception('Invalid data.', 67);
// Make sure the discussion is opened in this paste and in configuration.
if (!$this->getPaste()->isOpendiscussion() || !$this->_conf->getKey('discussion'))
throw new Exception('Invalid data.', 68);
// Check for improbable collision.
if ($this->exists())
throw new Exception('You are unlucky. Try again.', 69);
$this->_data->meta->postdate = time();
// store comment
if (
$this->_store->createComment(
$pasteid,
$this->getParentId(),
$this->getId(),
json_decode(json_encode($this->_data), true)
) === false
) throw new Exception('Error saving comment. Sorry.', 70);
}
/**
* Delete the comment.
*
* @access public
* @throws Exception
* @return void
*/
public function delete()
{
throw new Exception('To delete a comment, delete its parent paste', 64);
}
/**
* Test if comment exists in store.
*
* @access public
* @return bool
*/
public function exists()
{
return $this->_store->existsComment(
$this->getPaste()->getId(),
$this->getParentId(),
$this->getId()
);
}
/**
* Set paste.
*
* @access public
* @param model_paste $paste
* @throws Exception
* @return void
*/
public function setPaste(model_paste $paste)
{
$this->_paste = $paste;
$this->_data->meta->pasteid = $paste->getId();
}
/**
* Get paste.
*
* @access public
* @return model_paste
*/
public function getPaste()
{
return $this->_paste;
}
/**
* Set parent ID.
*
* @access public
* @param string $id
* @throws Exception
* @return void
*/
public function setParentId($id)
{
if (!self::isValidId($id)) throw new Exception('Invalid paste ID.', 65);
$this->_data->meta->parentid = $id;
}
/**
* Get parent ID.
*
* @access public
* @return string
*/
public function getParentId()
{
if (!property_exists($this->_data->meta, 'parentid')) $this->_data->meta->parentid = '';
return $this->_data->meta->parentid;
}
public function setNickname($nickname)
{
if (!sjcl::isValid($nickname)) throw new Exception('Invalid data.', 66);
$this->_data->meta->nickname = $nickname;
// Generation of the anonymous avatar (Vizhash):
// If a nickname is provided, we generate a Vizhash.
// (We assume that if the user did not enter a nickname, he/she wants
// to be anonymous and we will not generate the vizhash.)
$vh = new vizhash16x16();
$pngdata = $vh->generate(trafficlimiter::getIp());
if ($pngdata != '')
{
$this->_data->meta->vizhash = 'data:image/png;base64,' . base64_encode($pngdata);
}
// Once the avatar is generated, we do not keep the IP address, nor its hash.
}
}

299
lib/model/paste.php Normal file
View file

@ -0,0 +1,299 @@
<?php
/**
* ZeroBin
*
* a zero-knowledge paste bin
*
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21.1
*/
/**
* model_paste
*
* Model of a ZeroBin paste.
*/
class model_paste extends model_abstract
{
/**
* Get paste data.
*
* @access public
* @throws Exception
* @return stdObject
*/
public function get()
{
$this->_data = $this->_store->read($this->getId());
// See if paste has expired and delete it if neccessary.
if (property_exists($this->_data->meta, 'expire_date'))
{
if ($this->_data->meta->expire_date < time())
{
$this->delete();
throw new Exception(zerobin::GENERIC_ERROR, 63);
}
// We kindly provide the remaining time before expiration (in seconds)
$this->_data->meta->remaining_time = $this->_data->meta->expire_date - time();
}
// set formatter for for the view.
if (!property_exists($this->_data->meta, 'formatter'))
{
// support < 0.21 syntax highlighting
if (property_exists($this->_data->meta, 'syntaxcoloring') && $this->_data->meta->syntaxcoloring === true)
{
$this->_data->meta->formatter = 'syntaxhighlighting';
}
else
{
$this->_data->meta->formatter = $this->_conf->getKey('defaultformatter');
}
}
return $this->_data;
}
/**
* Store the paste's data.
*
* @access public
* @throws Exception
* @return void
*/
public function store()
{
// Check for improbable collision.
if ($this->exists())
throw new Exception('You are unlucky. Try again.', 75);
$this->_data->meta->postdate = time();
// store paste
if (
$this->_store->create(
$this->getId(),
json_decode(json_encode($this->_data), true)
) === false
) throw new Exception('Error saving paste. Sorry.', 76);
}
/**
* Delete the paste.
*
* @access public
* @throws Exception
* @return void
*/
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 model_comment
*/
public function getComment($parentId, $commentId = null)
{
if (!$this->exists())
{
throw new Exception('Invalid data.', 62);
}
$comment = new model_comment($this->_conf, $this->_store);
$comment->setPaste($this);
$comment->setParentId($parentId);
if ($commentId !== null) $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:
* http://example.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken>
*
* @access public
* @return string
*/
public function getDeleteToken()
{
return hash_hmac('sha1', $this->getId(), serversalt::get());
}
/**
* Set paste's attachment.
*
* @access public
* @param string $attachment
* @throws Exception
* @return void
*/
public function setAttachment($attachment)
{
if (!$this->_conf->getKey('fileupload') || !sjcl::isValid($attachment))
throw new Exception('Invalid attachment.', 71);
$this->_data->meta->attachment = $attachment;
}
/**
* Set paste's attachment name.
*
* @access public
* @param string $attachmentname
* @throws Exception
* @return void
*/
public function setAttachmentName($attachmentname)
{
if (!$this->_conf->getKey('fileupload') || !sjcl::isValid($attachmentname))
throw new Exception('Invalid attachment.', 72);
$this->_data->meta->attachmentname = $attachmentname;
}
/**
* Set paste expiration.
*
* @access public
* @param string $expiration
* @return void
*/
public function setExpiration($expiration)
{
$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) $this->_data->meta->expire_date = time() + $expire;
}
/**
* Set paste's burn-after-reading type.
*
* @access public
* @param string $burnafterreading
* @throws Exception
* @return void
*/
public function setBurnafterreading($burnafterreading = '1')
{
if ($burnafterreading === '0')
{
$this->_data->meta->burnafterreading = false;
}
else
{
if ($burnafterreading !== '1')
throw new Exception('Invalid data.', 73);
$this->_data->meta->burnafterreading = true;
$this->_data->meta->opendiscussion = false;
}
}
/**
* Set paste's discussion state.
*
* @access public
* @param string $opendiscussion
* @throws Exception
* @return void
*/
public function setOpendiscussion($opendiscussion = '1')
{
if (
!$this->_conf->getKey('discussion') ||
$this->isBurnafterreading() ||
$opendiscussion === '0'
)
{
$this->_data->meta->opendiscussion = false;
}
else
{
if ($opendiscussion !== '1')
throw new Exception('Invalid data.', 74);
$this->_data->meta->opendiscussion = true;
}
}
/**
* Set paste's format.
*
* @access public
* @param string $format
* @throws Exception
* @return void
*/
public function setFormatter($format)
{
if (!array_key_exists($format, $this->_conf->getSection('formatter_options')))
{
$format = $this->_conf->getKey('defaultformatter');
}
$this->_data->meta->formatter = $format;
}
/**
* Check if paste is of burn-after-reading type.
*
* @access public
* @throws Exception
* @return boolean
*/
public function isBurnafterreading()
{
if (!property_exists($this->_data, 'data')) $this->get();
return property_exists($this->_data->meta, 'burnafterreading') &&
$this->_data->meta->burnafterreading === true;
}
/**
* Check if paste has discussions enabled.
*
* @access public
* @throws Exception
* @return boolean
*/
public function isOpendiscussion()
{
if (!property_exists($this->_data, 'data')) $this->get();
return property_exists($this->_data->meta, 'opendiscussion') &&
$this->_data->meta->opendiscussion === true;
}
}

View file

@ -80,10 +80,10 @@ class zerobin
private $_json = ''; private $_json = '';
/** /**
* data storage model * Factory of instance models
* *
* @access private * @access private
* @var zerobin_abstract * @var model
*/ */
private $_model; private $_model;
@ -163,25 +163,7 @@ class zerobin
} }
$this->_conf = new configuration; $this->_conf = new configuration;
$this->_model = $this->_conf->getKey('class', 'model'); $this->_model = new model($this->_conf);
}
/**
* get the model, create one if needed
*
* @access private
* @return zerobin_abstract
*/
private function _model()
{
// if needed, initialize the model
if(is_string($this->_model)) {
$this->_model = forward_static_call(
array($this->_model, 'getInstance'),
$this->_conf->getSection('model_options')
);
}
return $this->_model;
} }
/** /**
@ -208,23 +190,22 @@ class zerobin
{ {
$error = false; $error = false;
// Ensure last paste from visitors IP address was more than configured amount of seconds ago.
trafficlimiter::setConfiguration($this->_conf);
if (!trafficlimiter::canPass()) return $this->_return_message(
1, i18n::_(
'Please wait %d seconds between each post.',
$this->_conf->getKey('limit', 'traffic')
)
);
$has_attachment = array_key_exists('attachment', $_POST); $has_attachment = array_key_exists('attachment', $_POST);
$has_attachmentname = $has_attachment && array_key_exists('attachmentname', $_POST) && !empty($_POST['attachmentname']); $has_attachmentname = $has_attachment && array_key_exists('attachmentname', $_POST) && !empty($_POST['attachmentname']);
$data = array_key_exists('data', $_POST) ? $_POST['data'] : ''; $data = array_key_exists('data', $_POST) ? $_POST['data'] : '';
$attachment = $has_attachment ? $_POST['attachment'] : ''; $attachment = $has_attachment ? $_POST['attachment'] : '';
$attachmentname = $has_attachmentname ? $_POST['attachmentname'] : ''; $attachmentname = $has_attachmentname ? $_POST['attachmentname'] : '';
// Make sure last paste from the IP address was more than X seconds ago. // Ensure content is not too big.
trafficlimiter::setConfiguration($this->_conf);
if (!trafficlimiter::canPass()) return $this->_return_message(
1,
i18n::_(
'Please wait %d seconds between each post.',
$this->_conf->getKey('limit', 'traffic')
)
);
// Make sure content is not too big.
$sizelimit = $this->_conf->getKey('sizelimit'); $sizelimit = $this->_conf->getKey('sizelimit');
if ( if (
strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
@ -236,182 +217,62 @@ class zerobin
) )
); );
// Make sure format is correct.
if (!sjcl::isValid($data)) return $this->_return_message(1, 'Invalid data.');
// Make sure attachments are enabled and format is correct.
if($has_attachment)
{
if (
!$this->_conf->getKey('fileupload') ||
!sjcl::isValid($attachment) ||
!($has_attachmentname && sjcl::isValid($attachmentname))
) return $this->_return_message(1, 'Invalid attachment.');
}
// Read additional meta-information.
$meta = array();
// Read expiration date
if (array_key_exists('expire', $_POST) && !empty($_POST['expire']))
{
$selected_expire = (string) $_POST['expire'];
$expire_options = $this->_conf->getSection('expire_options');
if (array_key_exists($selected_expire, $expire_options))
{
$expire = $expire_options[$selected_expire];
}
else
{
$expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
}
if ($expire > 0) $meta['expire_date'] = time() + $expire;
}
// Destroy the paste when it is read.
if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading']))
{
$burnafterreading = $_POST['burnafterreading'];
if ($burnafterreading !== '0')
{
if ($burnafterreading !== '1') $error = true;
$meta['burnafterreading'] = true;
}
}
// Read open discussion flag.
if (
$this->_conf->getKey('discussion') &&
array_key_exists('opendiscussion', $_POST) &&
!empty($_POST['opendiscussion'])
)
{
$opendiscussion = $_POST['opendiscussion'];
if ($opendiscussion !== '0')
{
if ($opendiscussion !== '1') $error = true;
$meta['opendiscussion'] = true;
}
}
// Read formatter flag.
if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter']))
{
$formatter = $_POST['formatter'];
if (!array_key_exists($formatter, $this->_conf->getSection('formatter_options')))
{
$formatter = $this->_conf->getKey('defaultformatter');
}
$meta['formatter'] = $formatter;
}
// You can't have an open discussion on a "Burn after reading" paste:
if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']);
// Optional nickname for comments
if (!empty($_POST['nickname']))
{
// Generation of the anonymous avatar (Vizhash):
// If a nickname is provided, we generate a Vizhash.
// (We assume that if the user did not enter a nickname, he/she wants
// to be anonymous and we will not generate the vizhash.)
$nick = $_POST['nickname'];
if (!sjcl::isValid($nick))
{
$error = true;
}
else
{
$meta['nickname'] = $nick;
$vz = new vizhash16x16();
$pngdata = $vz->generate(trafficlimiter::getIp());
if ($pngdata != '')
{
$meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata);
}
// Once the avatar is generated, we do not keep the IP address, nor its hash.
}
}
if ($error) return $this->_return_message(1, 'Invalid data.');
// Add post date to meta.
$meta['postdate'] = time();
// We just want a small hash to avoid collisions:
// Half-MD5 (64 bits) will do the trick
$dataid = substr(hash('md5', $data), 0, 16);
$storage = array('data' => $data);
// Add meta-information only if necessary.
if (count($meta)) $storage['meta'] = $meta;
// The user posts a comment. // The user posts a comment.
if ( if (
!empty($_POST['parentid']) && array_key_exists('parentid', $_POST) && !empty($_POST['parentid']) &&
!empty($_POST['pasteid']) array_key_exists('pasteid', $_POST) && !empty($_POST['pasteid'])
) )
{ {
$pasteid = (string) $_POST['pasteid']; $paste = $this->_model->getPaste($_POST['pasteid']);
$parentid = (string) $_POST['parentid']; if ($paste->exists()) {
if ( try {
!filter::is_valid_paste_id($pasteid) || $comment = $paste->getComment($_POST['parentid']);
!filter::is_valid_paste_id($parentid)
) return $this->_return_message(1, 'Invalid data.');
// Comments do not expire (it's the paste that expires) if (array_key_exists('nickname', $_POST) && !empty($_POST['nickname'])
unset($storage['expire_date']); ) $comment->setNickname($_POST['nickname']);
unset($storage['opendiscussion']);
// Make sure paste exists. $comment->setData($data);
if ( $comment->store();
!$this->_model()->exists($pasteid) } catch(Exception $e) {
) return $this->_return_message(1, 'Invalid data.'); return $this->_return_message(1, $e->getMessage());
}
// Make sure the discussion is opened in this paste. $this->_return_message(0, $comment->getId());
$paste = $this->_model()->read($pasteid); }
if ( else
!$paste->meta->opendiscussion {
) return $this->_return_message(1, 'Invalid data.'); $this->_return_message(1, 'Invalid data.');
}
// Check for improbable collision.
if (
$this->_model()->existsComment($pasteid, $parentid, $dataid)
) return $this->_return_message(1, 'You are unlucky. Try again.');
// New comment
if (
$this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false
) return $this->_return_message(1, 'Error saving comment. Sorry.');
// 0 = no error
return $this->_return_message(0, $dataid);
} }
// The user posts a standard paste. // The user posts a standard paste.
else else
{ {
// Check for improbable collision. $paste = $this->_model->getPaste();
if ( try {
$this->_model()->exists($dataid) if ($has_attachment)
) return $this->_return_message(1, 'You are unlucky. Try again.'); {
$paste->setAttachment($attachment);
if ($has_attachmentname)
$paste->setAttachmentName($attachmentname);
}
// Add attachment and its name, if one was sent if (array_key_exists('expire', $_POST) && !empty($_POST['expire'])
if ($has_attachment) $storage['meta']['attachment'] = $attachment; ) $paste->setExpiration($_POST['expire']);
if ($has_attachmentname) $storage['meta']['attachmentname'] = $attachmentname;
// New paste if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading'])
if ( ) $paste->setBurnafterreading($_POST['burnafterreading']);
$this->_model()->create($dataid, $storage) === false
) return $this->_return_message(1, 'Error saving paste. Sorry.');
// Generate the "delete" token. if (array_key_exists('opendiscussion', $_POST) && !empty($_POST['opendiscussion'])
// The token is the hmac of the pasteid signed with the server salt. ) $paste->setOpendiscussion($_POST['opendiscussion']);
// The paste can be delete by calling http://example.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken>
$deletetoken = hash_hmac('sha1', $dataid, serversalt::get());
// 0 = no error if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter'])
return $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); ) $paste->setFormatter($_POST['formatter']);
$paste->setData($data);
$paste->store();
} catch (Exception $e) {
return $this->_return_message(1, $e->getMessage());
}
$this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
} }
} }
@ -425,63 +286,48 @@ class zerobin
*/ */
private function _delete($dataid, $deletetoken) private function _delete($dataid, $deletetoken)
{ {
// Is this a valid paste identifier? try {
if (!filter::is_valid_paste_id($dataid)) $paste = $this->_model->getPaste($dataid);
{ if ($paste->exists())
$this->_error = 'Invalid paste ID.';
return;
}
// Check that paste exists.
if (!$this->_model()->exists($dataid))
{
$this->_error = self::GENERIC_ERROR;
return;
}
// Get the paste itself.
$paste = $this->_model()->read($dataid);
// See if paste has expired.
if (
isset($paste->meta->expire_date) &&
$paste->meta->expire_date < time()
)
{
// Delete the paste
$this->_model()->delete($dataid);
$this->_error = self::GENERIC_ERROR;
return;
}
if ($deletetoken == 'burnafterreading') {
if (
isset($paste->meta->burnafterreading) &&
$paste->meta->burnafterreading
)
{ {
// Delete the paste // accessing this property ensures that the paste would be
$this->_model()->delete($dataid); // deleted if it has already expired
$this->_return_message(0, $dataid); $burnafterreading = $paste->isBurnafterreading();
if ($deletetoken == 'burnafterreading')
{
if ($burnafterreading)
{
$paste->delete();
$this->_return_message(0, $dataid);
}
else
{
$this->_return_message(1, 'Paste is not of burn-after-reading type.');
}
}
else
{
// Make sure the token is valid.
serversalt::setPath($this->_conf->getKey('dir', 'traffic'));
if (filter::slow_equals($deletetoken, $paste->getDeleteToken()))
{
// Paste exists and deletion token is valid: Delete the paste.
$paste->delete();
$this->_status = 'Paste was properly deleted.';
}
else
{
$this->_error = 'Wrong deletion token. Paste was not deleted.';
}
}
} }
else else
{ {
$this->_return_message(1, 'Paste is not of burn-after-reading type.'); $this->_error = self::GENERIC_ERROR;
} }
return; } catch (Exception $e) {
$this->_error = $e->getMessage();
} }
// Make sure token is valid.
serversalt::setPath($this->_conf->getKey('dir', 'traffic'));
if (!filter::slow_equals($deletetoken, hash_hmac('sha1', $dataid, serversalt::get())))
{
$this->_error = 'Wrong deletion token. Paste was not deleted.';
return;
}
// Paste exists and deletion token is valid: Delete the paste.
$this->_model()->delete($dataid);
$this->_status = 'Paste was properly deleted.';
} }
/** /**
@ -499,73 +345,26 @@ class zerobin
$dataid = substr($dataid, 0, $pos); $dataid = substr($dataid, 0, $pos);
} }
// Is this a valid paste identifier? try {
if (!filter::is_valid_paste_id($dataid)) $paste = $this->_model->getPaste($dataid);
{ if ($paste->exists())
$this->_error = 'Invalid paste ID.'; {
// The paste itself is the first in the list of encrypted messages.
$messages = array_merge(
array($paste->get()),
$paste->getComments()
);
$this->_data = json_encode($messages);
}
else
{
$this->_error = self::GENERIC_ERROR;
}
} catch (Exception $e) {
$this->_error = $e->getMessage();
return; return;
} }
// Check that paste exists.
if ($this->_model()->exists($dataid))
{
// Get the paste itself.
$paste = $this->_model()->read($dataid);
// See if paste has expired.
if (
isset($paste->meta->expire_date) &&
$paste->meta->expire_date < time()
)
{
// Delete the paste
$this->_model()->delete($dataid);
$this->_error = self::GENERIC_ERROR;
}
// If no error, return the paste.
else
{
// We kindly provide the remaining time before expiration (in seconds)
if (
property_exists($paste->meta, 'expire_date')
) $paste->meta->remaining_time = $paste->meta->expire_date - time();
// The paste itself is the first in the list of encrypted messages.
$messages = array($paste);
// If it's a discussion, get all comments.
if (
property_exists($paste->meta, 'opendiscussion') &&
$paste->meta->opendiscussion
)
{
$messages = array_merge(
$messages,
$this->_model()->readComments($dataid)
);
}
// set formatter for for the view.
if (!property_exists($paste->meta, 'formatter'))
{
// support < 0.21 syntax highlighting
if (property_exists($paste->meta, 'syntaxcoloring') && $paste->meta->syntaxcoloring === true)
{
$paste->meta->formatter = 'syntaxhighlighting';
}
else
{
$paste->meta->formatter = $this->_conf->getKey('defaultformatter');
}
}
$this->_data = json_encode($messages);
}
}
else
{
$this->_error = self::GENERIC_ERROR;
}
if ($isJson) if ($isJson)
{ {
if (strlen($this->_error)) if (strlen($this->_error))

View file

@ -17,7 +17,7 @@
*/ */
abstract class zerobin_abstract abstract class zerobin_abstract
{ {
/** /**
* singleton instance * singleton instance
* *
* @access protected * @access protected
@ -87,7 +87,7 @@ abstract class zerobin_abstract
* *
* @access public * @access public
* @param string $dataid * @param string $dataid
* @return void * @return bool
*/ */
abstract public function exists($pasteid); abstract public function exists($pasteid);

View file

@ -172,16 +172,16 @@ class zerobin_data extends zerobin_abstract
// - pasteid is the paste this reply belongs to. // - pasteid is the paste this reply belongs to.
// - commentid is the comment identifier itself. // - commentid is the comment identifier itself.
// - parentid is the comment this comment replies to (It can be pasteid) // - parentid is the comment this comment replies to (It can be pasteid)
if (is_file($discdir.$filename)) if (is_file($discdir . $filename))
{ {
$comment = json_decode(file_get_contents($discdir.$filename)); $comment = json_decode(file_get_contents($discdir . $filename));
$items = explode('.', $filename); $items = explode('.', $filename);
// Add some meta information not contained in file. // Add some meta information not contained in file.
$comment->meta->commentid=$items[1]; $comment->meta->commentid = $items[1];
$comment->meta->parentid=$items[2]; $comment->meta->parentid = $items[2];
// Store in array // Store in array
$comments[$comment->meta->postdate]=$comment; $comments[$comment->meta->postdate] = $comment;
} }
} }
$dir->close(); $dir->close();

View file

@ -208,7 +208,12 @@ class zerobin_db extends zerobin_abstract
$opendiscussion = $burnafterreading = false; $opendiscussion = $burnafterreading = false;
$meta = $paste['meta']; $meta = $paste['meta'];
unset($meta['postdate']); unset($meta['postdate']);
unset($meta['expire_date']); $expire_date = 0;
if (array_key_exists('expire_date', $paste['meta']))
{
$expire_date = (int) $paste['meta']['expire_date'];
unset($meta['expire_date']);
}
if (array_key_exists('opendiscussion', $paste['meta'])) if (array_key_exists('opendiscussion', $paste['meta']))
{ {
$opendiscussion = (bool) $paste['meta']['opendiscussion']; $opendiscussion = (bool) $paste['meta']['opendiscussion'];
@ -225,7 +230,7 @@ class zerobin_db extends zerobin_abstract
$pasteid, $pasteid,
$paste['data'], $paste['data'],
$paste['meta']['postdate'], $paste['meta']['postdate'],
$paste['meta']['expire_date'], $expire_date,
(int) $opendiscussion, (int) $opendiscussion,
(int) $burnafterreading, (int) $burnafterreading,
json_encode($meta), json_encode($meta),
@ -255,33 +260,31 @@ class zerobin_db extends zerobin_abstract
// create object // create object
self::$_cache[$pasteid] = new stdClass; self::$_cache[$pasteid] = new stdClass;
self::$_cache[$pasteid]->data = $paste['data']; self::$_cache[$pasteid]->data = $paste['data'];
self::$_cache[$pasteid]->meta = json_decode($paste['meta']);
$meta = json_decode($paste['meta']);
if (!is_object($meta)) $meta = new stdClass;
if (property_exists($meta, 'attachment'))
{
self::$_cache[$pasteid]->attachment = $meta->attachment;
unset($meta->attachment);
if (property_exists($meta, 'attachmentname'))
{
self::$_cache[$pasteid]->attachmentname = $meta->attachmentname;
unset($meta->attachmentname);
}
}
self::$_cache[$pasteid]->meta = $meta;
self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate']; self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
self::$_cache[$pasteid]->meta->expire_date = (int) $paste['expiredate']; $expire_date = (int) $paste['expiredate'];
if (
$expire_date > 0
) self::$_cache[$pasteid]->meta->expire_date = $expire_date;
if ( if (
$paste['opendiscussion'] $paste['opendiscussion']
) self::$_cache[$pasteid]->meta->opendiscussion = true; ) self::$_cache[$pasteid]->meta->opendiscussion = true;
if ( if (
$paste['burnafterreading'] $paste['burnafterreading']
) self::$_cache[$pasteid]->meta->burnafterreading = true; ) self::$_cache[$pasteid]->meta->burnafterreading = true;
if (property_exists(self::$_cache[$pasteid]->meta, 'attachment'))
{
self::$_cache[$pasteid]->attachment = self::$_cache[$pasteid]->meta->attachment;
unset(self::$_cache[$pasteid]->meta->attachment);
if (property_exists(self::$_cache[$pasteid]->meta, 'attachmentname'))
{
self::$_cache[$pasteid]->attachmentname = self::$_cache[$pasteid]->meta->attachmentname;
unset(self::$_cache[$pasteid]->meta->attachmentname);
}
}
elseif (array_key_exists('attachment', $paste))
{
self::$_cache[$pasteid]->attachment = $paste['attachment'];
if (array_key_exists('attachmentname', $paste))
{
self::$_cache[$pasteid]->attachmentname = $paste['attachmentname'];
}
}
} }
} }

View file

@ -177,6 +177,18 @@ class helper
continue; continue;
} elseif (is_string($setting)) { } elseif (is_string($setting)) {
$setting = '"' . $setting . '"'; $setting = '"' . $setting . '"';
} elseif (is_array($setting)) {
foreach ($setting as $key => $value) {
if (is_null($value)) {
$value = 'null';
} elseif (is_string($value)) {
$value = '"' . $value . '"';
} else {
$value = var_export($value, true);
}
fwrite($ini, $option . "[$key] = $value" . PHP_EOL);
}
continue;
} else { } else {
$setting = var_export($setting, true); $setting = var_export($setting, true);
} }

View file

@ -62,13 +62,6 @@ class filterTest extends PHPUnit_Framework_TestCase
$this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent)); $this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent));
} }
public function testPasteIdValidation()
{
$this->assertTrue(filter::is_valid_paste_id('a242ab7bdfb2581a'), 'valid paste id');
$this->assertFalse(filter::is_valid_paste_id('foo'), 'invalid hex values');
$this->assertFalse(filter::is_valid_paste_id('../bar/baz'), 'path attack');
}
public function testSlowEquals() public function testSlowEquals()
{ {
$this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string'); $this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string');

View file

@ -22,6 +22,7 @@ class modelTest extends PHPUnit_Framework_TestCase
helper::createIniFile(CONF, $options); helper::createIniFile(CONF, $options);
$this->_conf = new configuration; $this->_conf = new configuration;
$this->_model = new model($this->_conf); $this->_model = new model($this->_conf);
$_SERVER['REMOTE_ADDR'] = '::1';
} }
public function tearDown() public function tearDown()
@ -46,12 +47,14 @@ class modelTest extends PHPUnit_Framework_TestCase
$paste = $this->_model->getPaste(helper::getPasteId()); $paste = $this->_model->getPaste(helper::getPasteId());
$this->assertTrue($paste->exists(), 'paste exists after storing it'); $this->assertTrue($paste->exists(), 'paste exists after storing it');
$paste = $paste->get(); $paste = $paste->get();
foreach (array('data', 'opendiscussion', 'formatter') as $key) { $this->assertEquals($pasteData['data'], $paste->data);
$this->assertEquals($pasteData[$key], $paste->$key); foreach (array('opendiscussion', 'formatter') as $key) {
$this->assertEquals($pasteData['meta'][$key], $paste->meta->$key);
} }
// storing comments // storing comments
$commentData = helper::getComment(); $commentData = helper::getComment();
$paste = $this->_model->getPaste(helper::getPasteId());
$comment = $paste->getComment(helper::getPasteId(), helper::getCommentId()); $comment = $paste->getComment(helper::getPasteId(), helper::getCommentId());
$this->assertFalse($comment->exists(), 'comment does not yet exist'); $this->assertFalse($comment->exists(), 'comment does not yet exist');
@ -75,7 +78,7 @@ class modelTest extends PHPUnit_Framework_TestCase
/** /**
* @expectedException Exception * @expectedException Exception
* @expectedExceptionCode 60 * @expectedExceptionCode 75
*/ */
public function testPasteDuplicate() public function testPasteDuplicate()
{ {
@ -97,7 +100,7 @@ class modelTest extends PHPUnit_Framework_TestCase
/** /**
* @expectedException Exception * @expectedException Exception
* @expectedExceptionCode 60 * @expectedExceptionCode 69
*/ */
public function testCommentDuplicate() public function testCommentDuplicate()
{ {
@ -136,19 +139,31 @@ class modelTest extends PHPUnit_Framework_TestCase
$paste->store(); $paste->store();
$paste = $this->_model->getPaste(helper::getPasteId())->get(); // ID was set based on data $paste = $this->_model->getPaste(helper::getPasteId())->get(); // ID was set based on data
$this->assertEquals(true, $paste->meta->burnafterreading, 'burn after reading takes precedence'); $this->assertEquals(true, property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading, 'burn after reading takes precendence');
$this->assertEquals(false, $paste->meta->opendiscussion, 'opendiscussion is overiden'); $this->assertEquals(false, property_exists($paste->meta, 'opendiscussion') && $paste->meta->opendiscussion, 'opendiscussion is disabled');
$this->assertEquals($this->_conf->getKey('defaultformatter'), $paste->meta->formatter, 'default formatter is set'); $this->assertEquals($this->_conf->getKey('defaultformatter'), $paste->meta->formatter, 'default formatter is set');
$_SERVER['REMOTE_ADDR'] = '::1'; $this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setOpendiscussion();
$paste->store();
$vz = new vizhash16x16(); $vz = new vizhash16x16();
$pngdata = 'data:image/png;base64,' . base64_encode($vz->generate($_SERVER['REMOTE_ADDR'])); $pngdata = 'data:image/png;base64,' . base64_encode($vz->generate($_SERVER['REMOTE_ADDR']));
$comment = $this->_model->getPaste(helper::getPasteId())->getComment(helper::getPasteId()); $comment = $paste->getComment(helper::getPasteId());
$comment->setData($commentData['data']); $comment->setData($commentData['data']);
$comment->setNickname($commentData['meta']['nickname']); $comment->setNickname($commentData['meta']['nickname']);
$comment->store(); $comment->store();
$comment = $paste->getComment(helper::getPasteId(), helper::getCommentId()); $comment = $paste->getComment(helper::getPasteId(), helper::getCommentId())->get();
$this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set'); $this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set');
} }
public function testPasteIdValidation()
{
$this->assertTrue(model_paste::isValidId('a242ab7bdfb2581a'), 'valid paste id');
$this->assertFalse(model_paste::isValidId('foo'), 'invalid hex values');
$this->assertFalse(model_paste::isValidId('../bar/baz'), 'path attack');
}
} }

View file

@ -368,15 +368,18 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
helper::confBackup(); helper::confBackup();
helper::createIniFile(CONF, $options); helper::createIniFile(CONF, $options);
$_POST = helper::getPaste(); $_POST = helper::getComment();
$_POST['pasteid'] = helper::getPasteId();
$_POST['parentid'] = helper::getPasteId();
$_POST['nickname'] = 'foo'; $_POST['nickname'] = 'foo';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$this->_model->create(helper::getPasteId(), helper::getPaste());
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**