diff --git a/lib/configuration.php b/lib/configuration.php index d65f5df8..4bb151a9 100644 --- a/lib/configuration.php +++ b/lib/configuration.php @@ -99,6 +99,17 @@ class configuration } 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) { if ($key == 'dir') diff --git a/lib/filter.php b/lib/filter.php index 3e2a64e2..55ca709f 100644 --- a/lib/filter.php +++ b/lib/filter.php @@ -80,19 +80,6 @@ class filter 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 * https://crackstation.net/hashing-security.htm?=rd#slowequals diff --git a/lib/model.php b/lib/model.php new file mode 100644 index 00000000..9a5e1f47 --- /dev/null +++ b/lib/model.php @@ -0,0 +1,71 @@ +_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; + } +} \ No newline at end of file diff --git a/lib/model/abstract.php b/lib/model/abstract.php new file mode 100644 index 00000000..6beb9f8c --- /dev/null +++ b/lib/model/abstract.php @@ -0,0 +1,156 @@ +_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); + } +} diff --git a/lib/model/comment.php b/lib/model/comment.php new file mode 100644 index 00000000..ba77884c --- /dev/null +++ b/lib/model/comment.php @@ -0,0 +1,181 @@ +_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. + } +} diff --git a/lib/model/paste.php b/lib/model/paste.php new file mode 100644 index 00000000..5159c90c --- /dev/null +++ b/lib/model/paste.php @@ -0,0 +1,299 @@ +_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=&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; + } +} diff --git a/lib/zerobin.php b/lib/zerobin.php index f0c68976..51de2dee 100644 --- a/lib/zerobin.php +++ b/lib/zerobin.php @@ -80,10 +80,10 @@ class zerobin private $_json = ''; /** - * data storage model + * Factory of instance models * * @access private - * @var zerobin_abstract + * @var model */ private $_model; @@ -163,25 +163,7 @@ class zerobin } $this->_conf = new configuration; - $this->_model = $this->_conf->getKey('class', 'model'); - } - - /** - * 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; + $this->_model = new model($this->_conf); } /** @@ -208,23 +190,22 @@ class zerobin { $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_attachmentname = $has_attachment && array_key_exists('attachmentname', $_POST) && !empty($_POST['attachmentname']); $data = array_key_exists('data', $_POST) ? $_POST['data'] : ''; $attachment = $has_attachment ? $_POST['attachment'] : ''; $attachmentname = $has_attachmentname ? $_POST['attachmentname'] : ''; - // Make sure last paste from the IP address was more than X 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') - ) - ); - - // Make sure content is not too big. + // Ensure content is not too big. $sizelimit = $this->_conf->getKey('sizelimit'); if ( 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. if ( - !empty($_POST['parentid']) && - !empty($_POST['pasteid']) + array_key_exists('parentid', $_POST) && !empty($_POST['parentid']) && + array_key_exists('pasteid', $_POST) && !empty($_POST['pasteid']) ) { - $pasteid = (string) $_POST['pasteid']; - $parentid = (string) $_POST['parentid']; - if ( - !filter::is_valid_paste_id($pasteid) || - !filter::is_valid_paste_id($parentid) - ) return $this->_return_message(1, 'Invalid data.'); + $paste = $this->_model->getPaste($_POST['pasteid']); + if ($paste->exists()) { + try { + $comment = $paste->getComment($_POST['parentid']); - // Comments do not expire (it's the paste that expires) - unset($storage['expire_date']); - unset($storage['opendiscussion']); + if (array_key_exists('nickname', $_POST) && !empty($_POST['nickname']) + ) $comment->setNickname($_POST['nickname']); - // Make sure paste exists. - if ( - !$this->_model()->exists($pasteid) - ) return $this->_return_message(1, 'Invalid data.'); - - // Make sure the discussion is opened in this paste. - $paste = $this->_model()->read($pasteid); - if ( - !$paste->meta->opendiscussion - ) return $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); + $comment->setData($data); + $comment->store(); + } catch(Exception $e) { + return $this->_return_message(1, $e->getMessage()); + } + $this->_return_message(0, $comment->getId()); + } + else + { + $this->_return_message(1, 'Invalid data.'); + } } // The user posts a standard paste. else { - // Check for improbable collision. - if ( - $this->_model()->exists($dataid) - ) return $this->_return_message(1, 'You are unlucky. Try again.'); + $paste = $this->_model->getPaste(); + try { + if ($has_attachment) + { + $paste->setAttachment($attachment); + if ($has_attachmentname) + $paste->setAttachmentName($attachmentname); + } - // Add attachment and its name, if one was sent - if ($has_attachment) $storage['meta']['attachment'] = $attachment; - if ($has_attachmentname) $storage['meta']['attachmentname'] = $attachmentname; + if (array_key_exists('expire', $_POST) && !empty($_POST['expire']) + ) $paste->setExpiration($_POST['expire']); - // New paste - if ( - $this->_model()->create($dataid, $storage) === false - ) return $this->_return_message(1, 'Error saving paste. Sorry.'); + if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading']) + ) $paste->setBurnafterreading($_POST['burnafterreading']); - // Generate the "delete" token. - // The token is the hmac of the pasteid signed with the server salt. - // The paste can be delete by calling http://example.com/zerobin/?pasteid=&deletetoken= - $deletetoken = hash_hmac('sha1', $dataid, serversalt::get()); + if (array_key_exists('opendiscussion', $_POST) && !empty($_POST['opendiscussion']) + ) $paste->setOpendiscussion($_POST['opendiscussion']); - // 0 = no error - return $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); + if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter']) + ) $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) { - // Is this a valid paste identifier? - if (!filter::is_valid_paste_id($dataid)) - { - $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 - ) + try { + $paste = $this->_model->getPaste($dataid); + if ($paste->exists()) { - // Delete the paste - $this->_model()->delete($dataid); - $this->_return_message(0, $dataid); + // accessing this property ensures that the paste would be + // deleted if it has already expired + $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 { - $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); } - // Is this a valid paste identifier? - if (!filter::is_valid_paste_id($dataid)) - { - $this->_error = 'Invalid paste ID.'; + try { + $paste = $this->_model->getPaste($dataid); + if ($paste->exists()) + { + // 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; } - // 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 (strlen($this->_error)) diff --git a/lib/zerobin/abstract.php b/lib/zerobin/abstract.php index fa82eb5d..89e11503 100644 --- a/lib/zerobin/abstract.php +++ b/lib/zerobin/abstract.php @@ -17,7 +17,7 @@ */ abstract class zerobin_abstract { - /** + /** * singleton instance * * @access protected @@ -87,7 +87,7 @@ abstract class zerobin_abstract * * @access public * @param string $dataid - * @return void + * @return bool */ abstract public function exists($pasteid); diff --git a/lib/zerobin/data.php b/lib/zerobin/data.php index 7e811435..89f029c5 100644 --- a/lib/zerobin/data.php +++ b/lib/zerobin/data.php @@ -172,16 +172,16 @@ class zerobin_data extends zerobin_abstract // - pasteid is the paste this reply belongs to. // - commentid is the comment identifier itself. // - 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); // Add some meta information not contained in file. - $comment->meta->commentid=$items[1]; - $comment->meta->parentid=$items[2]; + $comment->meta->commentid = $items[1]; + $comment->meta->parentid = $items[2]; // Store in array - $comments[$comment->meta->postdate]=$comment; + $comments[$comment->meta->postdate] = $comment; } } $dir->close(); diff --git a/lib/zerobin/db.php b/lib/zerobin/db.php index 20749016..12486757 100644 --- a/lib/zerobin/db.php +++ b/lib/zerobin/db.php @@ -208,7 +208,12 @@ class zerobin_db extends zerobin_abstract $opendiscussion = $burnafterreading = false; $meta = $paste['meta']; 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'])) { $opendiscussion = (bool) $paste['meta']['opendiscussion']; @@ -225,7 +230,7 @@ class zerobin_db extends zerobin_abstract $pasteid, $paste['data'], $paste['meta']['postdate'], - $paste['meta']['expire_date'], + $expire_date, (int) $opendiscussion, (int) $burnafterreading, json_encode($meta), @@ -255,33 +260,31 @@ class zerobin_db extends zerobin_abstract // create object self::$_cache[$pasteid] = new stdClass; 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->expire_date = (int) $paste['expiredate']; + $expire_date = (int) $paste['expiredate']; + if ( + $expire_date > 0 + ) self::$_cache[$pasteid]->meta->expire_date = $expire_date; if ( $paste['opendiscussion'] ) self::$_cache[$pasteid]->meta->opendiscussion = true; if ( $paste['burnafterreading'] ) 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']; - } - } } } diff --git a/tst/bootstrap.php b/tst/bootstrap.php index 07e7d841..947bc379 100644 --- a/tst/bootstrap.php +++ b/tst/bootstrap.php @@ -177,6 +177,18 @@ class helper continue; } elseif (is_string($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 { $setting = var_export($setting, true); } diff --git a/tst/filter.php b/tst/filter.php index a1888e4c..b4c8743d 100644 --- a/tst/filter.php +++ b/tst/filter.php @@ -62,13 +62,6 @@ class filterTest extends PHPUnit_Framework_TestCase $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() { $this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string'); diff --git a/tst/model.php b/tst/model.php index 8a270d69..aefa8f8c 100644 --- a/tst/model.php +++ b/tst/model.php @@ -22,6 +22,7 @@ class modelTest extends PHPUnit_Framework_TestCase helper::createIniFile(CONF, $options); $this->_conf = new configuration; $this->_model = new model($this->_conf); + $_SERVER['REMOTE_ADDR'] = '::1'; } public function tearDown() @@ -46,12 +47,14 @@ class modelTest extends PHPUnit_Framework_TestCase $paste = $this->_model->getPaste(helper::getPasteId()); $this->assertTrue($paste->exists(), 'paste exists after storing it'); $paste = $paste->get(); - foreach (array('data', 'opendiscussion', 'formatter') as $key) { - $this->assertEquals($pasteData[$key], $paste->$key); + $this->assertEquals($pasteData['data'], $paste->data); + foreach (array('opendiscussion', 'formatter') as $key) { + $this->assertEquals($pasteData['meta'][$key], $paste->meta->$key); } // storing comments $commentData = helper::getComment(); + $paste = $this->_model->getPaste(helper::getPasteId()); $comment = $paste->getComment(helper::getPasteId(), helper::getCommentId()); $this->assertFalse($comment->exists(), 'comment does not yet exist'); @@ -75,7 +78,7 @@ class modelTest extends PHPUnit_Framework_TestCase /** * @expectedException Exception - * @expectedExceptionCode 60 + * @expectedExceptionCode 75 */ public function testPasteDuplicate() { @@ -97,7 +100,7 @@ class modelTest extends PHPUnit_Framework_TestCase /** * @expectedException Exception - * @expectedExceptionCode 60 + * @expectedExceptionCode 69 */ public function testCommentDuplicate() { @@ -136,19 +139,31 @@ class modelTest extends PHPUnit_Framework_TestCase $paste->store(); $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(false, $paste->meta->opendiscussion, 'opendiscussion is overiden'); + $this->assertEquals(true, property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading, 'burn after reading takes precendence'); + $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'); - $_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(); $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->setNickname($commentData['meta']['nickname']); $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'); } + + 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'); + } } \ No newline at end of file diff --git a/tst/zerobin.php b/tst/zerobin.php index 16fc4b8c..6a1028ef 100644 --- a/tst/zerobin.php +++ b/tst/zerobin.php @@ -368,15 +368,18 @@ class zerobinTest extends PHPUnit_Framework_TestCase $options['traffic']['limit'] = 0; helper::confBackup(); helper::createIniFile(CONF, $options); - $_POST = helper::getPaste(); + $_POST = helper::getComment(); + $_POST['pasteid'] = helper::getPasteId(); + $_POST['parentid'] = helper::getPasteId(); $_POST['nickname'] = 'foo'; $_SERVER['REMOTE_ADDR'] = '::1'; + $this->_model->create(helper::getPasteId(), helper::getPaste()); ob_start(); new zerobin; $content = ob_get_contents(); $response = json_decode($content, true); $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'); } /**