Browse Source

mostly finished with data model refactoring

El RIDO 11 năm trước cách đây
mục cha
commit
694138c5d4
14 tập tin đã thay đổi với 894 bổ sung364 xóa
  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');
     }
 
     /**