Răsfoiți Sursa

implemented zerobin_db model, added more options for paste expiration, made comments and max data size configurable

Simon Rupf 14 ani în urmă
părinte
comite
421e6cba97
7 a modificat fișierele cu 432 adăugiri și 49 ștergeri
  1. 96 0
      DOCUMENTATION.md
  2. 29 16
      cfg/conf.ini
  3. 36 14
      lib/zerobin.php
  4. 1 1
      lib/zerobin/abstract.php
  5. 2 2
      lib/zerobin/data.php
  6. 265 15
      lib/zerobin/db.php
  7. 3 1
      tpl/page.html

+ 96 - 0
DOCUMENTATION.md

@@ -0,0 +1,96 @@
+Documentation
+=============
+
+For Administrators
+------------------
+
+In the index.php in the main folder you can define a different PATH. This is 
+useful if you want to secure your installation and want to move the 
+configuration, data files, templates and PHP libraries (directories cfg, lib 
+and tpl) outside of your document root. This new location must still be 
+accessible to your webserver / PHP process.
+
+> ### PATH Example ###
+> Your zerobin installation lives in a subfolder called "paste" inside of your 
+> document root. The URL looks like this:
+> http://example.com/paste/
+> The ZeroBin folder on your webserver is really:
+> /home/example.com/htdocs/paste
+> 
+> When setting the path like this:
+> define('PATH', '../../secret/zerobin/');
+> ZeroBin will look for your includes here:
+> /home/example.com/secret/zerobin
+
+In the file "cfg/conf.ini" you can configure ZeroBin. The config file is 
+divided into multiple sections, which are enclosed in square brackets. In the 
+"[main]" section you can enable or disable the discussion feature, set the 
+limit of stored pastes and comments in bytes. The "[traffic]" section lets you 
+set a time limit in seconds. Users may not post more often the this limit to 
+your ZeroBin.
+
+Finally the "[model]" and "[model_options]" sections let you configure your 
+favourite way of storing the pastes and discussions on your server. 
+"zerobin_data" is the default model, which stores everything in files in the 
+data folder. This is the recommended setup for low traffic sites. Under high 
+load, in distributed setups or if you are not allowed to store files locally, 
+you might want to switch to the "zerobin_db" model. This lets you store your 
+data in a database. Basically all databases, that are supported by PDO (PHP 
+data objects) may be used. Automatic table creation is provided for pdo_ibm, 
+pdo_informix, pdo_mssql, pdo_mysql, pdo_oci, pdo_pgsql and pdo_sqlite. You may 
+want to provide a table prefix, if you have to share the zerobin database with 
+another application. The table prefix option is called "tbl".
+
+> ### Note ###
+> The "zerobin_db" model has only been tested with sqlite and MySQL, although
+> it would not be recommended to use sqlite in a production environment. If you
+> gain any experience running ZeroBin on other RDBMS, let us know.
+
+For reference or if you want to create the table schema for yourself:
+
+    CREATE TABLE prefix_paste (
+        dataid CHAR(16),
+        data TEXT,
+        postdate INT,
+        expiredate INT,
+        opendiscussion INT,
+        burnafterreading INT
+    );
+    
+    CREATE TABLE prefix_comment (
+        dataid CHAR(16),
+        pasteid CHAR(16),
+        parentid CHAR(16),
+        data TEXT,
+        nickname VARCHAR(255),
+        vizhash TEXT,
+        postdate INT
+    );
+
+For Developers 
+--------------
+If you want to create your own data models, you might want to know how the arrays, that you have to store, look like:
+
+    public function create($pasteid, $paste)
+    {
+        $pasteid = substr(hash('md5', $paste['data']), 0, 16);
+        
+        $paste['data']                      // text
+        $paste['meta']['postdate']          // int UNIX timestamp
+        $paste['meta']['expire_date']       // int UNIX timestamp
+        $paste['meta']['opendiscussion']    // true (if false it is unset)
+        $paste['meta']['burnafterreading']  // true (if false it is unset; if true, then opendiscussion is unset)
+    }
+
+    public function createComment($pasteid, $parentid, $commentid, $comment)
+    {
+        $pasteid  // the id of the paste this comment belongs to
+        $parentid // the id of the parent of this comment, may be the paste id itself
+        $commentid = substr(hash('md5', $paste['data']), 0, 16);
+        
+        $paste['data']                      // text
+        $paste['meta']['nickname']          // text or null (if anonymous)
+        $paste['meta']['vizhash']           // text or null (if anonymous)
+        $paste['meta']['postdate']          // int UNIX timestamp
+    }
+

+ 29 - 16
cfg/conf.ini

@@ -7,28 +7,41 @@
 ; @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
 ; @version   0.15
 
-; time limit between calls from the same IP address in seconds
-traffic_limit = 10
-traffic_dir = PATH "data"
+[main]
+; enable or disable discussions
+opendiscussion = true
 
 ; size limit per paste or comment in bytes
-size_limit = 2000000
+sizelimit = 2097152
+
+[traffic]
+; time limit between calls from the same IP address in seconds
+limit = 10
+dir = PATH "data"
 
+[model]
 ; name of data model class to load and directory for storage
 ; the default model "zerobin_data" stores everything in the filesystem
-model = zerobin_data
-model_options["dir"] = PATH "data"
+class = zerobin_data
+[model_options]
+dir = PATH "data"
 
+;[model]
 ; example of DB configuration for MySQL
-;model = zerobin_db
-;model_options["dsn"] = "mysql:host=localhost;dbname=zerobin"
-;model_options["usr"] = "zerobin"
-;model_options["pwd"] = "Z3r0P4ss"
-;model_options["opt"][PDO::ATTR_PERSISTENT] = true
+;class = zerobin_db
+;[model_options]
+;dsn = "mysql:host=localhost;dbname=zerobin;charset=UTF8"
+;tbl = "zerobin_"	; table prefix
+;usr = "zerobin"
+;pwd = "Z3r0P4ss"
+;opt[12] = true	  ; PDO::ATTR_PERSISTENT
 
+;[model]
 ; example of DB configuration for SQLite
-;model = zerobin_db
-;model_options["dsn"] = "sqlite:" PATH "data"/db.sq3"
-;model_options["usr"] = null
-;model_options["pwd"] = null
-;model_options["opt"] = null
+;[model_options]
+;class = zerobin_db
+;dsn = "sqlite:" PATH "data/db.sq3"
+;usr = null
+;pwd = null
+;opt[12] = true	; PDO::ATTR_PERSISTENT
+

+ 36 - 14
lib/zerobin.php

@@ -103,8 +103,8 @@ class zerobin
             );
         }
 
-        $this->_conf = parse_ini_file(PATH . 'cfg/conf.ini');
-        $this->_model = $this->_conf['model'];
+        $this->_conf = parse_ini_file(PATH . 'cfg/conf.ini', true);
+        $this->_model = $this->_conf['model']['class'];
     }
 
     /**
@@ -117,7 +117,10 @@ class zerobin
     {
         // if needed, initialize the model
         if(is_string($this->_model)) {
-            $this->_model = forward_static_call(array($this->_model, 'getInstance'), $this->_conf['model_options']);
+            $this->_model = forward_static_call(
+                array($this->_model, 'getInstance'),
+                $this->_conf['model_options']
+            );
         }
         return $this->_model;
     }
@@ -129,7 +132,7 @@ class zerobin
      * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct)
      *
      * All optional data will go to meta information:
-     * expire (optional) = expiration delay (never,10min,1hour,1day,1month,1year,burn) (default:never)
+     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
      * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
      * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct)
      * parentid (optional) = in discussion, which comment this comment replies to.
@@ -143,18 +146,30 @@ class zerobin
         header('Content-type: application/json');
         $error = false;
 
-        // Make sure last paste from the IP address was more than 10 seconds ago.
-        trafficlimiter::setLimit($this->_conf['traffic_limit']);
-        trafficlimiter::setPath($this->_conf['traffic_dir']);
+        // Make sure last paste from the IP address was more than X seconds ago.
+        trafficlimiter::setLimit($this->_conf['traffic']['limit']);
+        trafficlimiter::setPath($this->_conf['traffic']['dir']);
         if (
             !trafficlimiter::canPass($_SERVER['REMOTE_ADDR'])
-        ) $this->_return_message(1, 'Please wait 10 seconds between each post.');
+        ) $this->_return_message(
+            1,
+            'Please wait ' .
+            $this->_conf['traffic']['limit'] .
+            ' seconds between each post.'
+        );
 
         // Make sure content is not too big.
         $data = $_POST['data'];
         if (
-            strlen($data) > 2000000
-        ) $this->_return_message(1, 'Paste is limited to 2 MB of encrypted data.');
+            strlen($data) > $this->_conf['main']['sizelimit']
+        ) $this->_return_message(
+            1,
+            'Paste is limited to ' .
+            $this->_conf['main']['sizelimit'] .
+            ' ' .
+            filter::size_humanreadable($this->_conf['main']['sizelimit']) .
+            ' of encrypted data.'
+        );
 
         // Make sure format is correct.
         if (!sjcl::isValid($data)) $this->_return_message(1, 'Invalid data.');
@@ -167,6 +182,12 @@ class zerobin
         {
             switch ($_POST['expire'])
             {
+                case 'burn':
+                    $meta['burnafterreading'] = true;
+                    break;
+                case '5min':
+                    $meta['expire_date'] = time()+5*60;
+                    break;
                 case '10min':
                     $meta['expire_date'] = time()+10*60;
                     break;
@@ -176,19 +197,19 @@ class zerobin
                 case '1day':
                     $meta['expire_date'] = time()+24*60*60;
                     break;
+                case '1week':
+                    $meta['expire_date'] = time()+7*24*60*60;
+                    break;
                 case '1month':
                     $meta['expire_date'] = strtotime('+1 month');
                     break;
                 case '1year':
                     $meta['expire_date'] = strtotime('+1 year');
-                    break;
-                case 'burn':
-                    $meta['burnafterreading'] = true;
             }
         }
 
         // Read open discussion flag.
-        if (!empty($_POST['opendiscussion']))
+        if ($this->_conf['main']['opendiscussion'] && !empty($_POST['opendiscussion']))
         {
             $opendiscussion = $_POST['opendiscussion'];
             if ($opendiscussion != 0)
@@ -381,6 +402,7 @@ class zerobin
         // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
         $page->assign('CIPHERDATA', htmlspecialchars($this->_data, ENT_NOQUOTES));
         $page->assign('ERRORMESSAGE', $this->_error);
+        $page->assign('OPENDISCUSSION', $this->_conf['main']['opendiscussion']);
         $page->assign('VERSION', self::VERSION);
         $page->draw('page');
     }

+ 1 - 1
lib/zerobin/abstract.php

@@ -49,7 +49,7 @@ abstract class zerobin_abstract
      *
      * @access public
      * @static
-     * @return zerobin
+     * @return zerobin_abstract
      */
     abstract public static function getInstance($options);
 

+ 2 - 2
lib/zerobin/data.php

@@ -29,9 +29,9 @@ class zerobin_data extends zerobin_abstract
      *
      * @access public
      * @static
-     * @return zerobin
+     * @return zerobin_data
      */
-    public static function getInstance($options)
+    public static function getInstance($options = null)
     {
         // if given update the data directory
         if (

+ 265 - 15
lib/zerobin/db.php

@@ -17,6 +17,13 @@
  */
 class zerobin_db extends zerobin_abstract
 {
+    /*
+     * @access private
+     * @static
+     * @var array to cache select queries
+     */
+    private static $_cache = array();
+
     /*
      * @access private
      * @static
@@ -24,31 +31,137 @@ class zerobin_db extends zerobin_abstract
      */
     private static $_db;
 
+    /*
+     * @access private
+     * @static
+     * @var string table prefix
+     */
+    private static $_prefix = '';
+
+    /*
+     * @access private
+     * @static
+     * @var string database type
+     */
+    private static $_type = '';
+
     /**
      * get instance of singleton
      *
      * @access public
      * @static
-     * @return zerobin
+     * @throws Exception
+     * @return zerobin_db
      */
-    public static function getInstance($options)
+    public static function getInstance($options = null)
     {
         // if needed initialize the singleton
         if(null === self::$_instance) {
             parent::$_instance = new self;
         }
-        if (
-        	is_array($options) &&
-        	array_key_exists('dsn', $options) &&
-        	array_key_exists('usr', $options) &&
-        	array_key_exists('pwd', $options) &&
-        	array_key_exists('opt', $options)
-        ) self::$_db = new PDO(
-            $options['dsn'],
-            $options['usr'],
-            $options['pwd'],
-            $options['opt']
-        );
+
+        if (is_array($options))
+        {
+            // set table prefix if given
+            if (array_key_exists('tbl', $options)) self::$_prefix = $options['tbl'];
+
+            // initialize the db connection with new options
+            if (
+                array_key_exists('dsn', $options) &&
+                array_key_exists('usr', $options) &&
+                array_key_exists('pwd', $options) &&
+                array_key_exists('opt', $options)
+            )
+            {
+                self::$_db = new PDO(
+                    $options['dsn'],
+                    $options['usr'],
+                    $options['pwd'],
+                    $options['opt']
+                );
+
+                // check if the database contains the required tables
+                self::$_type = strtolower(
+                    substr($options['dsn'], 0, strpos($options['dsn'], ':'))
+                );
+                switch(self::$_type)
+                {
+                    case 'ibm':
+                        $sql = 'SELECT tabname FROM SYSCAT.TABLES ';
+                        break;
+                    case 'informix':
+                        $sql = 'SELECT tabname FROM systables ';
+                        break;
+                    case 'mssql':
+                        $sql = "SELECT name FROM sysobjects "
+                             . "WHERE type = 'U' ORDER BY name";
+                        break;
+                    case 'mysql':
+                        $sql = 'SHOW TABLES';
+                        break;
+                    case 'oci':
+                        $sql = 'SELECT table_name FROM all_tables';
+                        break;
+                    case 'pgsql':
+                        $sql = "SELECT c.relname AS table_name "
+                             . "FROM pg_class c, pg_user u "
+                             . "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
+                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
+                             . "AND c.relname !~ '^(pg_|sql_)' "
+                             . "UNION "
+                             . "SELECT c.relname AS table_name "
+                             . "FROM pg_class c "
+                             . "WHERE c.relkind = 'r' "
+                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
+                             . "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
+                             . "AND c.relname !~ '^pg_'";
+                        break;
+                    case 'sqlite':
+                        $sql = "SELECT name FROM sqlite_master WHERE type='table' "
+                             . "UNION ALL SELECT name FROM sqlite_temp_master "
+                             . "WHERE type='table' ORDER BY name";
+                        break;
+                    default:
+                        throw new Exception(
+                            'PDO type ' .
+                            self::$_type .
+                            ' is currently not supported.'
+                        );
+                }
+                $statement = self::$_db->query($sql);
+                $tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
+
+                // create paste table if needed
+                if (!array_key_exists(self::$_prefix . 'paste', $tables))
+                {
+                    self::$_db->exec(
+                        'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
+                        'dataid CHAR(16), ' .
+                        'data TEXT, ' .
+                        'postdate INT, ' .
+                        'expiredate INT, ' .
+                        'opendiscussion INT, ' .
+                        'burnafterreading INT );'
+                    );
+                }
+
+                // create comment table if needed
+                if (!array_key_exists(self::$_prefix . 'comment', $tables))
+                {
+                    self::$_db->exec(
+                        'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
+                        'dataid CHAR(16), ' .
+                        'pasteid CHAR(16), ' .
+                        'parentid CHAR(16), ' .
+                        'data TEXT, ' .
+                        'nickname VARCHAR(255), ' .
+                        'vizhash TEXT, ' .
+                        'postdate INT );'
+                    );
+                }
+            }
+        }
+
         return parent::$_instance;
     }
 
@@ -58,10 +171,27 @@ class zerobin_db extends zerobin_abstract
      * @access public
      * @param  string $pasteid
      * @param  array  $paste
-     * @return int|false
+     * @return bool
      */
     public function create($pasteid, $paste)
     {
+        if (
+            !array_key_exists('opendiscussion', $paste['meta'])
+        ) $paste['meta']['opendiscussion'] = false;
+        if (
+            !array_key_exists('burnafterreading', $paste['meta'])
+        ) $paste['meta']['burnafterreading'] = false;
+        return self::_exec(
+            'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?)',
+            array(
+                $pasteid,
+                $paste['data'],
+                $paste['meta']['postdate'],
+                $paste['meta']['expire_date'],
+                (int) $paste['meta']['opendiscussion'],
+                (int) $paste['meta']['burnafterreading'],
+            )
+        );
     }
 
     /**
@@ -73,6 +203,27 @@ class zerobin_db extends zerobin_abstract
      */
     public function read($pasteid)
     {
+        if (
+            !array_key_exists($pasteid, self::$_cache)
+        ) self::$_cache[$pasteid] = self::_select(
+            'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
+            array($pasteid), true
+        );
+
+        // create object
+        $paste = new stdClass;
+        $paste->data = self::$_cache[$pasteid]['data'];
+        $paste->meta = new stdClass;
+        $paste->meta->postdate = (int) self::$_cache[$pasteid]['postdate'];
+        $paste->meta->expire_date = (int) self::$_cache[$pasteid]['expiredate'];
+        if (
+            self::$_cache[$pasteid]['opendiscussion']
+        ) $paste->meta->opendiscussion = true;
+        if (
+            self::$_cache[$pasteid]['burnafterreading']
+        ) $paste->meta->burnafterreading = true;
+
+        return $paste;
     }
 
     /**
@@ -84,6 +235,14 @@ class zerobin_db extends zerobin_abstract
      */
     public function delete($pasteid)
     {
+        self::_exec(
+            'DELETE FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
+            array($pasteid)
+        );
+        self::_exec(
+            'DELETE FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
+            array($pasteid)
+        );
     }
 
     /**
@@ -95,6 +254,13 @@ class zerobin_db extends zerobin_abstract
      */
     public function exists($pasteid)
     {
+        if (
+            !array_key_exists($pasteid, self::$_cache)
+        ) self::$_cache[$pasteid] = self::_select(
+            'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
+            array($pasteid), true
+        );
+        return (bool) self::$_cache[$pasteid];
     }
 
     /**
@@ -109,6 +275,18 @@ class zerobin_db extends zerobin_abstract
      */
     public function createComment($pasteid, $parentid, $commentid, $comment)
     {
+        return self::_exec(
+            'INSERT INTO ' . self::$_prefix . 'comment VALUES(?,?,?,?,?,?,?)',
+            array(
+                $pasteid,
+                $parentid,
+                $commentid,
+                $comment['data'],
+                $comment['meta']['nickname'],
+                $comment['meta']['vizhash'],
+                $comment['meta']['postdate'],
+            )
+        );
     }
 
     /**
@@ -120,6 +298,33 @@ class zerobin_db extends zerobin_abstract
      */
     public function readComments($pasteid)
     {
+        $rows = self::_select(
+            'SELECT * FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
+            array($pasteid)
+        );
+
+        // create object
+        $commentTemplate = new stdClass;
+        $commentTemplate->meta = new stdClass;
+
+        // create comment list
+        $comments = array();
+        if (count($rows))
+        {
+            foreach ($rows as $row)
+            {
+                $i = (int) $row['postdate'];
+                $comments[$i] = clone $commentTemplate;
+                $comments[$i]->data = $row['data'];
+                $comments[$i]->meta->nickname = $row['nickname'];
+                $comments[$i]->meta->vizhash = $row['vizhash'];
+                $comments[$i]->meta->postdate = $i;
+                $comments[$i]->meta->commentid = $row['dataid'];
+                $comments[$i]->meta->parentid = $row['parentid'];
+            }
+            ksort($comments);
+        }
+        return $comments;
     }
 
     /**
@@ -133,5 +338,50 @@ class zerobin_db extends zerobin_abstract
      */
     public function existsComment($pasteid, $parentid, $commentid)
     {
+        return (bool) self::_select(
+            'SELECT dataid FROM ' . self::$_prefix . 'comment ' .
+            'WHERE pasteid = ? AND parentid = ? AND dataid = ?',
+            array($pasteid, $parentid, $commentid), true
+        );
+    }
+
+    /**
+     * execute a statement
+     *
+     * @access private
+     * @static
+     * @param  string $sql
+     * @param  array $params
+     * @throws PDOException
+     * @return array
+     */
+    private static function _exec($sql, array $params)
+    {
+        $statement = self::$_db->prepare($sql);
+        $result = $statement->execute($params);
+        $statement->closeCursor();
+        return $result;
+    }
+
+    /**
+     * run a select statement
+     *
+     * @access private
+     * @static
+     * @param  string $sql
+     * @param  array $params
+     * @param  bool $firstOnly if only the first row should be returned
+     * @throws PDOException
+     * @return array
+     */
+    private static function _select($sql, array $params, $firstOnly = false)
+    {
+        $statement = self::$_db->prepare($sql);
+        $statement->execute($params);
+        $result = $firstOnly ?
+            $statement->fetch(PDO::FETCH_ASSOC) :
+            $statement->fetchAll(PDO::FETCH_ASSOC);
+        $statement->closeCursor();
+        return $result;
     }
 }

+ 3 - 1
tpl/page.html

@@ -48,9 +48,11 @@
       <div id="expiration" style="display:none;">Expire: 
       <select id="pasteExpiration" name="pasteExpiration">
         <option value="burn">Burn after reading</option>
+        <option value="5min">5 minutes</option>
         <option value="10min">10 minutes</option>
         <option value="1hour">1 hour</option>
         <option value="1day">1 day</option>
+        <option value="1week">1 week</option>
         <option value="1month" selected="selected">1 month</option>
         <option value="1year">1 year</option>
         <option value="never">Never</option>
@@ -67,7 +69,7 @@
       </div>
         <input id="password" value="Optional password..." style="display:none;" />
         <div id="opendisc" class="button" style="display:none;">
-           <input type="checkbox" id="opendiscussion" name="opendiscussion" />
+           <input type="checkbox" id="opendiscussion" name="opendiscussion" {if="!$OPENDISCUSSION"} disabled="disabled"{/if} />
            <label for="opendiscussion">Open discussion</label>
         </div>
     </div>