Просмотр исходного кода

mostly finished with data model refactoring

El RIDO 10 лет назад
Родитель
Сommit
694138c5d4
14 измененных файлов с 894 добавлено и 364 удалено
  1. 11 0
      lib/configuration.php
  2. 0 13
      lib/filter.php
  3. 71 0
      lib/model.php
  4. 156 0
      lib/model/abstract.php
  5. 181 0
      lib/model/comment.php
  6. 299 0
      lib/model/paste.php
  7. 103 304
      lib/zerobin.php
  8. 2 2
      lib/zerobin/abstract.php
  9. 5 5
      lib/zerobin/data.php
  10. 25 22
      lib/zerobin/db.php
  11. 12 0
      tst/bootstrap.php
  12. 0 7
      tst/filter.php
  13. 24 9
      tst/model.php
  14. 5 2
      tst/zerobin.php

+ 11 - 0
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')

+ 0 - 13
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

+ 71 - 0
lib/model.php

@@ -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 - 0
lib/model/abstract.php

@@ -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 - 0
lib/model/comment.php

@@ -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 - 0
lib/model/paste.php

@@ -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;
+    }
+}

+ 103 - 304
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;
 
-        $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.
+        // 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::_(
+            1, i18n::_(
                 'Please wait %d seconds between each post.',
                 $this->_conf->getKey('limit', 'traffic')
             )
         );
 
-        // Make sure content is not too big.
+        $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'] : '';
+
+        // 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.
+        // The user posts a comment.
         if (
-            $this->_conf->getKey('discussion') &&
-            array_key_exists('opendiscussion', $_POST) &&
-            !empty($_POST['opendiscussion'])
+            array_key_exists('parentid', $_POST) && !empty($_POST['parentid']) &&
+            array_key_exists('pasteid',  $_POST) && !empty($_POST['pasteid'])
         )
         {
-            $opendiscussion = $_POST['opendiscussion'];
-            if ($opendiscussion !== '0')
-            {
-                if ($opendiscussion !== '1') $error = true;
-                $meta['opendiscussion'] = true;
+            $paste = $this->_model->getPaste($_POST['pasteid']);
+            if ($paste->exists()) {
+                try {
+                    $comment = $paste->getComment($_POST['parentid']);
+
+                    if (array_key_exists('nickname', $_POST) && !empty($_POST['nickname'])
+                    ) $comment->setNickname($_POST['nickname']);
+
+                    $comment->setData($data);
+                    $comment->store();
+                } catch(Exception $e) {
+                    return $this->_return_message(1, $e->getMessage());
+                }
+                $this->_return_message(0, $comment->getId());
             }
-        }
-
-        // 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')))
+            else
             {
-                $formatter = $this->_conf->getKey('defaultformatter');
+                $this->_return_message(1, 'Invalid data.');
             }
-            $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']))
+        // The user posts a standard paste.
+        else
         {
-            // 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 != '')
+            $paste = $this->_model->getPaste();
+            try {
+                if ($has_attachment)
                 {
-                    $meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata);
+                    $paste->setAttachment($attachment);
+                    if ($has_attachmentname)
+                        $paste->setAttachmentName($attachmentname);
                 }
-                // Once the avatar is generated, we do not keep the IP address, nor its hash.
-            }
-        }
 
-        if ($error) return $this->_return_message(1, 'Invalid data.');
+                if (array_key_exists('expire', $_POST) && !empty($_POST['expire'])
+                ) $paste->setExpiration($_POST['expire']);
 
-        // Add post date to meta.
-        $meta['postdate'] = time();
+                if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading'])
+                ) $paste->setBurnafterreading($_POST['burnafterreading']);
 
-        // We just want a small hash to avoid collisions:
-        // Half-MD5 (64 bits) will do the trick
-        $dataid = substr(hash('md5', $data), 0, 16);
+                if (array_key_exists('opendiscussion', $_POST) && !empty($_POST['opendiscussion'])
+                ) $paste->setOpendiscussion($_POST['opendiscussion']);
 
-        $storage = array('data' => $data);
+                if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter'])
+                ) $paste->setFormatter($_POST['formatter']);
 
-        // Add meta-information only if necessary.
-        if (count($meta)) $storage['meta'] = $meta;
-
-        // The user posts a comment.
-        if (
-            !empty($_POST['parentid']) &&
-            !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.');
-
-            // Comments do not expire (it's the paste that expires)
-            unset($storage['expire_date']);
-            unset($storage['opendiscussion']);
-
-            // 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);
-        }
-        // 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.');
-
-            // Add attachment and its name, if one was sent
-            if ($has_attachment) $storage['meta']['attachment'] = $attachment;
-            if ($has_attachmentname) $storage['meta']['attachmentname'] = $attachmentname;
-
-            // New paste
-            if (
-                $this->_model()->create($dataid, $storage) === false
-            ) return $this->_return_message(1, 'Error saving paste. Sorry.');
-
-            // 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=<pasteid>&deletetoken=<deletetoken>
-            $deletetoken = hash_hmac('sha1', $dataid, serversalt::get());
-
-            // 0 = no error
-            return $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken));
+                $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;
-        }
-
-        // 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;
+        } catch (Exception $e) {
+            $this->_error = $e->getMessage();
         }
-
-        // 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.';
-            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()
-            )
+        try {
+            $paste = $this->_model->getPaste($dataid);
+            if ($paste->exists())
             {
-                // Delete the paste
-                $this->_model()->delete($dataid);
-                $this->_error = self::GENERIC_ERROR;
+                // 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);
             }
-            // 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);
+                $this->_error = self::GENERIC_ERROR;
             }
+        } catch (Exception $e) {
+            $this->_error = $e->getMessage();
+            return;
         }
-        else
-        {
-            $this->_error = self::GENERIC_ERROR;
-        }
+
         if ($isJson)
         {
             if (strlen($this->_error))

+ 2 - 2
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);
 

+ 5 - 5
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();

+ 25 - 22
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'];
-                    }
-                }
             }
         }
 

+ 12 - 0
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);
                     }

+ 0 - 7
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');

+ 24 - 9
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');
+    }
 }

+ 5 - 2
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');
     }
 
     /**