From ba90d0cae29e2debb4e1343befb35a153be2e9c0 Mon Sep 17 00:00:00 2001
From: Simon Rupf <simon@rupf.net>
Date: Sun, 29 Apr 2012 19:15:06 +0200
Subject: [PATCH] Refactoring of code base - modularized code, introduced
 configuration, started working on a PDO based DB connector

---
 cfg/conf.ini            |  31 +++
 index.php               | 351 ++--------------------------------
 lib/filter.php          |  34 ++++
 lib/rain.tpl.class.php  |  56 +++---
 lib/sjcl.php            |  64 +++++++
 lib/traffic_limiter.php | 111 +++++++++++
 lib/vizhash_gd_zero.php |  46 ++---
 lib/zerobin.php         | 406 ++++++++++++++++++++++++++++++++++++++++
 lib/zerobin_data.php    | 283 ++++++++++++++++++++++++++++
 lib/zerobin_db.php      | 176 +++++++++++++++++
 10 files changed, 1170 insertions(+), 388 deletions(-)
 create mode 100644 cfg/conf.ini
 create mode 100644 lib/filter.php
 create mode 100644 lib/sjcl.php
 create mode 100644 lib/traffic_limiter.php
 create mode 100644 lib/zerobin.php
 create mode 100644 lib/zerobin_data.php
 create mode 100644 lib/zerobin_db.php

diff --git a/cfg/conf.ini b/cfg/conf.ini
new file mode 100644
index 00000000..17687e57
--- /dev/null
+++ b/cfg/conf.ini
@@ -0,0 +1,31 @@
+; 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.15
+
+; timelimit between calls from the same IP address in seconds
+traffic_limit = 10
+traffic_dir = PATH "data"
+
+; 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"
+
+; 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
+
+; 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
diff --git a/index.php b/index.php
index ddd28f61..6b4835c7 100644
--- a/index.php
+++ b/index.php
@@ -1,339 +1,16 @@
 <?php
-/*
-ZeroBin - a zero-knowledge paste bin
-Please see project page: http://sebsauvage.net/wiki/doku.php?id=php:zerobin
-*/
-$VERSION='Alpha 0.15';
-if (version_compare(PHP_VERSION, '5.2.6') < 0) die('ZeroBin requires php 5.2.6 or above to work. Sorry.');
-require_once "lib/vizhash_gd_zero.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.15
+ */
 
-// In case stupid admin has left magic_quotes enabled in php.ini:
-if (get_magic_quotes_gpc())
-{
-    function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; }
-    $_POST = array_map('stripslashes_deep', $_POST);
-    $_GET = array_map('stripslashes_deep', $_GET);
-    $_COOKIE = array_map('stripslashes_deep', $_COOKIE);
-}
-
-// trafic_limiter : Make sure the IP address makes at most 1 request every 10 seconds.
-// Will return false if IP address made a call less than 10 seconds ago.
-function trafic_limiter_canPass($ip)
-{
-    $tfilename='./data/trafic_limiter.php';
-    if (!is_file($tfilename))
-    {
-        file_put_contents($tfilename,"<?php\n\$GLOBALS['trafic_limiter']=array();\n?>");
-        chmod($tfilename,0705);
-    }
-    require $tfilename;
-    $tl=$GLOBALS['trafic_limiter'];
-    if (!empty($tl[$ip]) && ($tl[$ip]+10>=time()))
-    {
-        return false;
-        // FIXME: purge file of expired IPs to keep it small
-    }
-    $tl[$ip]=time();
-    file_put_contents($tfilename, "<?php\n\$GLOBALS['trafic_limiter']=".var_export($tl,true).";\n?>");
-    return true;
-}
-
-/* Convert paste id to storage path.
-   The idea is to creates subdirectories in order to limit the number of files per directory.
-   (A high number of files in a single directory can slow things down.)
-   eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
-   High-trafic websites may want to deepen the directory structure (like Squid does).
-
-   eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
-*/
-function dataid2path($dataid)
-{
-    return 'data/'.substr($dataid,0,2).'/'.substr($dataid,2,2).'/';
-}
-
-/* Convert paste id to discussion storage path.
-   eg. 'e3570978f9e4aa90' --> 'data/e3/57/e3570978f9e4aa90.discussion/'
-*/
-function dataid2discussionpath($dataid)
-{
-    return dataid2path($dataid).$dataid.'.discussion/';
-}
-
-// Checks if a json string is a proper SJCL encrypted message.
-// False if format is incorrect.
-function validSJCL($jsonstring)
-{
-    $accepted_keys=array('iv','salt','ct');
-
-    // Make sure content is valid json
-    $decoded = json_decode($jsonstring);
-    if ($decoded==null) return false;
-    $decoded = (array)$decoded;
-
-    // Make sure required fields are present and that they are base64 data.
-    foreach($accepted_keys as $k)
-    {
-        if (!array_key_exists($k,$decoded))  { return false; }
-        if (base64_decode($decoded[$k],$strict=true)==null) { return false; }
-    }
-
-    // Make sure no additionnal keys were added.
-    if (count(array_intersect(array_keys($decoded),$accepted_keys))!=3) { return false; }
-
-    // FIXME: Reject data if entropy is too low ?
-
-    // Make sure some fields have a reasonable size.
-    if (strlen($decoded['iv'])>24) return false;
-    if (strlen($decoded['salt'])>14) return false;
-    return true;
-}
-
-// Delete a paste and its discussion.
-// Input: $pasteid : the paste identifier.
-function deletePaste($pasteid)
-{
-    // Delete the paste itself
-    unlink(dataid2path($pasteid).$pasteid);
-
-    // Delete discussion if it exists.
-    $discdir = dataid2discussionpath($pasteid);
-    if (is_dir($discdir))
-    {
-        // Delete all files in discussion directory
-        $dhandle = opendir($discdir);
-        while (false !== ($filename = readdir($dhandle)))
-        {
-            if (is_file($discdir.$filename))  unlink($discdir.$filename);
-        }
-        closedir($dhandle);
-
-        // Delete the discussion directory.
-        rmdir($discdir);
-    }
-}
-
-if (!empty($_POST['data'])) // Create new paste/comment
-{
-    /* POST contains:
-         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)
-         opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
-         nickname (optional) = son encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct)
-         parentid (optional) = in discussion, which comment this comment replies to.
-         pasteid (optional) = in discussion, which paste this comment belongs to.
-    */
-
-    header('Content-type: application/json');
-    $error = false;
-
-    // Create storage directory if it does not exist.
-    if (!is_dir('data'))
-    {
-        mkdir('data',0705);
-        file_put_contents('data/.htaccess',"Allow from none\nDeny from all\n");
-    }
-
-    // Make sure last paste from the IP address was more than 10 seconds ago.
-    if (!trafic_limiter_canPass($_SERVER['REMOTE_ADDR']))
-        { echo json_encode(array('status'=>1,'message'=>'Please wait 10 seconds between each post.')); exit; }
-
-    // Make sure content is not too big.
-    $data = $_POST['data'];
-    if (strlen($data)>2000000)
-        { echo json_encode(array('status'=>1,'message'=>'Paste is limited to 2 Mb of encrypted data.')); exit; }
-
-    // Make sure format is correct.
-    if (!validSJCL($data))
-        { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
-
-    // Read additional meta-information.
-    $meta=array();
-
-    // Read expiration date
-    if (!empty($_POST['expire']))
-    {
-        $expire=$_POST['expire'];
-        if ($expire=='10min') $meta['expire_date']=time()+10*60;
-        elseif ($expire=='1hour') $meta['expire_date']=time()+60*60;
-        elseif ($expire=='1day') $meta['expire_date']=time()+24*60*60;
-        elseif ($expire=='1month') $meta['expire_date']=time()+30*24*60*60; // Well this is not *exactly* one month, it's 30 days.
-        elseif ($expire=='1year') $meta['expire_date']=time()+365*24*60*60;
-        elseif ($expire=='burn') $meta['burnafterreading']=true;
-    }
-
-    // Read open discussion flag
-    if (!empty($_POST['opendiscussion']))
-    {
-        $opendiscussion = $_POST['opendiscussion'];
-        if ($opendiscussion!='0' && $opendiscussion!='1') { $error=true; }
-        if ($opendiscussion!='0') { $meta['opendiscussion']=true; }
-    }
-
-    // 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']))
-    {
-        $nick = $_POST['nickname'];
-        if (!validSJCL($nick))
-        {
-            $error=true;
-        }
-        else
-        {
-            $meta['nickname']=$nick;
-
-            // 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.)
-            $vz = new vizhash16x16();
-            $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']);
-            if ($pngdata!='') $meta['vizhash'] = 'data:image/png;base64,'.base64_encode($pngdata);
-            // Once the avatar is generated, we do not keep the IP address, nor its hash.
-        }
-    }
-
-    if ($error)
-    {
-        echo json_encode(array('status'=>1,'message'=>'Invalid data.'));
-        exit;
-    }
-
-    // Add post date to meta.
-    $meta['postdate']=time();
-
-    // We just want a small hash to avoid collisions: Half-MD5 (64 bits) will do the trick
-    $dataid = substr(hash('md5',$data),0,16);
-
-    $is_comment = (!empty($_POST['parentid']) && !empty($_POST['pasteid'])); // Is this post a comment ?
-    $storage = array('data'=>$data);
-    if (count($meta)>0) $storage['meta'] = $meta;  // Add meta-information only if necessary.
-
-    if ($is_comment) // The user posts a comment.
-    {
-        $pasteid = $_POST['pasteid'];
-        $parentid = $_POST['parentid'];
-        if (!preg_match('/[a-f\d]{16}/',$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
-        if (!preg_match('/[a-f\d]{16}/',$parentid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
-
-        unset($storage['expire_date']); // Comment do not expire (it's the paste that expires)
-        unset($storage['opendiscussion']);
-
-        // Make sure paste exists.
-        $storagedir = dataid2path($pasteid);
-        if (!is_file($storagedir.$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
-
-        // Make sure the discussion is opened in this paste.
-        $paste=json_decode(file_get_contents($storagedir.$pasteid));
-        if (!$paste->meta->opendiscussion) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; }
-
-        $discdir = dataid2discussionpath($pasteid);
-        $filename = $pasteid.'.'.$dataid.'.'.$parentid;
-        if (!is_dir($discdir)) mkdir($discdir,$mode=0705,$recursive=true);
-        if (is_file($discdir.$filename)) // Oups... improbable collision.
-        {
-            echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.'));
-            exit;
-        }
-
-        file_put_contents($discdir.$filename,json_encode($storage));
-        echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error
-        exit;
-    }
-    else // a standard paste.
-    {
-        $storagedir = dataid2path($dataid);
-        if (!is_dir($storagedir)) mkdir($storagedir,$mode=0705,$recursive=true);
-        if (is_file($storagedir.$dataid)) // Oups... improbable collision.
-        {
-            echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.'));
-            exit;
-        }
-        // New paste
-        file_put_contents($storagedir.$dataid,json_encode($storage));
-        echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error
-        exit;
-    }
-
-echo json_encode(array('status'=>1,'message'=>'Server error.'));
-exit;
-}
-
-$CIPHERDATA='';
-$ERRORMESSAGE='';
-if (!empty($_SERVER['QUERY_STRING']))  // Display an existing paste.
-{
-    $dataid = $_SERVER['QUERY_STRING'];
-    if (preg_match('/[a-f\d]{16}/',$dataid))  // Is this a valid paste identifier ?
-    {
-        $filename = dataid2path($dataid).$dataid;
-        if (is_file($filename)) // Check that paste exists.
-        {
-            // Get the paste itself.
-            $paste=json_decode(file_get_contents($filename));
-
-            // See if paste has expired.
-            if (isset($paste->meta->expire_date) && $paste->meta->expire_date<time())
-            {
-                deletePaste($dataid);  // Delete the paste
-                $ERRORMESSAGE='Paste does not exist or has expired.';
-            }
-
-            if ($ERRORMESSAGE=='') // If no error, return the paste.
-            {
-                // 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();
-
-                $messages = array($paste); // The paste itself is the first in the list of encrypted messages.
-                // If it's a discussion, get all comments.
-                if (property_exists($paste->meta, 'opendiscussion') && $paste->meta->opendiscussion)
-                {
-                    $comments=array();
-                    $datadir = dataid2discussionpath($dataid);
-                    if (!is_dir($datadir)) mkdir($datadir,$mode=0705,$recursive=true);
-                    $dhandle = opendir($datadir);
-                    while (false !== ($filename = readdir($dhandle)))
-                    {
-                        if (is_file($datadir.$filename))
-                        {
-                            $comment=json_decode(file_get_contents($datadir.$filename));
-                            // Filename is in the form pasteid.commentid.parentid:
-                            // - 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)
-                            $items=explode('.',$filename);
-                            $comment->meta->commentid=$items[1]; // Add some meta information not contained in file.
-                            $comment->meta->parentid=$items[2];
-                            $comments[$comment->meta->postdate]=$comment; // Store in table
-                        }
-                    }
-                    closedir($dhandle);
-                    ksort($comments); // Sort comments by date, oldest first.
-                    $messages = array_merge($messages, $comments);
-                }
-                $CIPHERDATA = json_encode($messages);
-
-                // If the paste was meant to be read only once, delete it.
-                if (property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading) deletePaste($dataid);
-            }
-        }
-        else
-        {
-            $ERRORMESSAGE='Paste does not exist or has expired.';
-        }
-    }
-}
-
-
-require_once "lib/rain.tpl.class.php";
-header('Content-Type: text/html; charset=utf-8');
-$page = new RainTPL;
-$page->assign('CIPHERDATA',htmlspecialchars($CIPHERDATA,ENT_NOQUOTES));  // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
-$page->assign('VERSION',$VERSION);
-$page->assign('ERRORMESSAGE',$ERRORMESSAGE);
-$page->draw('page');
-?>
+// change this, if your php files and data is outside of your webservers document root
+define('PATH', '');
+require_once PATH . 'lib/zerobin.php';
+new zerobin;
diff --git a/lib/filter.php b/lib/filter.php
new file mode 100644
index 00000000..aa66d75f
--- /dev/null
+++ b/lib/filter.php
@@ -0,0 +1,34 @@
+<?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.15
+ */
+
+/**
+ * filter
+ *
+ * Provides data filtering functions.
+ */
+class filter
+{
+    /**
+     * strips slashes deeply
+     *
+     * @access public
+     * @static
+     * @param  mixed $value
+     * @return mixed
+     */
+    public static function stripslashes_deep($value)
+    {
+        return is_array($value) ?
+            array_map('filter::stripslashes_deep', $value) :
+            stripslashes($value);
+    }
+}
diff --git a/lib/rain.tpl.class.php b/lib/rain.tpl.class.php
index ea83b2c1..817e6450 100644
--- a/lib/rain.tpl.class.php
+++ b/lib/rain.tpl.class.php
@@ -21,7 +21,7 @@ class RainTPL{
 		 *
 		 * @var string
 		 */
-		static $tpl_dir = "tpl/";
+		static $tpl_dir = 'tpl/';
 
 
 		/**
@@ -29,7 +29,7 @@ class RainTPL{
 		 *
 		 * @var string
 		 */
-		static $cache_dir = "tmp/";
+		static $cache_dir = 'tmp/';
 
 
 		/**
@@ -81,10 +81,10 @@ class RainTPL{
 		 *
 		 */
 		static $check_template_update = true;
-                
+
 
 		/**
-		 * PHP tags <? ?> 
+		 * PHP tags <? ?>
 		 * True: php tags are enabled into the template
 		 * False: php tags are disabled into the template and rendered as html
 		 *
@@ -92,7 +92,7 @@ class RainTPL{
 		 */
 		static $php_enabled = false;
 
-		
+
 		/**
 		 * Debug mode flag.
 		 * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated.
@@ -257,9 +257,9 @@ class RainTPL{
 
 			$tpl_basename                       = basename( $tpl_name );														// template basename
 			$tpl_basedir                        = strpos($tpl_name,"/") ? dirname($tpl_name) . '/' : null;						// template basedirectory
-			$tpl_dir                            = self::$tpl_dir . $tpl_basedir;								// template directory
+			$tpl_dir                            = PATH . self::$tpl_dir . $tpl_basedir;								// template directory
 			$this->tpl['tpl_filename']          = $tpl_dir . $tpl_basename . '.' . self::$tpl_ext;	// template filename
-			$temp_compiled_filename             = self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum));
+			$temp_compiled_filename             = PATH . self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum));
 			$this->tpl['compiled_filename']     = $temp_compiled_filename . '.rtpl.php';	// cache filename
 			$this->tpl['cache_filename']        = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php';	// static cache filename
 
@@ -271,7 +271,7 @@ class RainTPL{
 
 			// file doesn't exsist, or the template was updated, Rain will compile the template
 			if( !file_exists( $this->tpl['compiled_filename'] ) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime( $this->tpl['tpl_filename'] ) ) ){
-				$this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename'] );
+				$this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], PATH . self::$cache_dir, $this->tpl['compiled_filename'] );
 				return true;
 			}
 			$this->tpl['checked'] = true;
@@ -285,7 +285,7 @@ class RainTPL{
 	*/
 	protected function xml_reSubstitution($capture) {
     		return "<?php echo '<?xml ".stripslashes($capture[1])." ?>'; ?>";
-	} 
+	}
 
 	/**
 	 * Compile and write the compiled template file
@@ -304,11 +304,11 @@ class RainTPL{
 			$template_code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $template_code );
 
 		//xml re-substitution
-		$template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code ); 
+		$template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code );
 
 		//compile template
 		$template_compiled = "<?php if(!class_exists('raintpl')){exit;}?>" . $this->compileTemplate( $template_code, $tpl_basedir );
-		
+
 
 		// fix the php-eating-newline-after-closing-tag-problem
 		$template_compiled = str_replace( "?>\n", "?>\n\n", $template_compiled );
@@ -413,7 +413,7 @@ class RainTPL{
 
 				// if the cache is active
 				if( isset($code[ 2 ]) ){
-					
+
 					//dynamic include
 					$compiled_code .= '<?php $tpl = new '.get_class($this).';' .
 								 'if( $cache = $tpl->cache( $template = basename("'.$include_var.'") ) )' .
@@ -426,7 +426,7 @@ class RainTPL{
 								 '} ?>';
 				}
 				else{
-	
+
 					//dynamic include
 					$compiled_code .= '<?php $tpl = new '.get_class($this).';' .
 									  '$tpl_dir_temp = self::$tpl_dir;' .
@@ -434,8 +434,8 @@ class RainTPL{
 									  ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
 									  '$tpl->draw( dirname("'.$include_var.'") . ( substr("'.$include_var.'",-1,1) != "/" ? "/" : "" ) . basename("'.$include_var.'") );'.
 									  '?>';
-					
-					
+
+
 				}
 
 			}
@@ -548,7 +548,7 @@ class RainTPL{
 				else
 					// parse the function
 					$parsed_function = $function . $this->var_replace( $code[ 2 ], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );
-				
+
 				//if code
 				$compiled_code .=   "<?php echo $parsed_function; ?>";
 			}
@@ -582,8 +582,8 @@ class RainTPL{
 		}
 		return $compiled_code;
 	}
-	
-	
+
+
 	/**
 	 * Reduce a path, eg. www/library/../filepath//file => www/filepath/file
 	 * @param type $path
@@ -611,8 +611,8 @@ class RainTPL{
 
 		if( self::$path_replace ){
 
-			$tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir;
-			
+			$tpl_dir = self::$base_url . PATH . self::$tpl_dir . $tpl_basedir;
+
 			// reduce the path
 			$path = $this->reduce_path($tpl_dir);
 
@@ -683,7 +683,7 @@ class RainTPL{
 			$this->function_check( $tag );
 
 			$extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );
-            
+
 
 			// check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
 			$is_init_variable = preg_match( "/^(\s*?)\=[^=](.*?)$/", $extra_var );
@@ -712,7 +712,7 @@ class RainTPL{
 
 			//if there's a function
 			if( $function_var ){
-                
+
                 // check if there's a function or a static method and separate, function by parameters
 				$function_var = str_replace("::", "@double_dot@", $function_var );
 
@@ -786,7 +786,7 @@ class RainTPL{
 
                             // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
                             $is_init_variable = preg_match( "/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var );
-                            
+
                             //function associate to variable
                             $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;
 
@@ -805,16 +805,16 @@ class RainTPL{
 
                             //transform .$variable in ["$variable"] and .variable in ["variable"]
                             $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path );
-                            
+
                             // if is an assignment also assign the variable to $this->var['value']
                             if( $is_init_variable )
                                 $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var;
 
-                                
+
 
                             //if there's a function
                             if( $function_var ){
-                                
+
                                     // check if there's a function or a static method and separate, function by parameters
                                     $function_var = str_replace("::", "@double_dot@", $function_var );
 
@@ -855,13 +855,13 @@ class RainTPL{
                                             $php_var = '$' . $var_name . $variable_path;
                             }else
                                     $php_var = '$' . $var_name . $variable_path;
-                            
+
                             // compile the variable for php
                             if( isset( $function ) )
                                     $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                             else
                                     $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;
-                            
+
                             $html = str_replace( $tag, $php_var, $html );
 
 
diff --git a/lib/sjcl.php b/lib/sjcl.php
new file mode 100644
index 00000000..0b4c3816
--- /dev/null
+++ b/lib/sjcl.php
@@ -0,0 +1,64 @@
+<?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.15
+ */
+
+/**
+ * sjcl
+ *
+ * Provides SJCL validation function.
+ */
+class sjcl
+{
+    /**
+     * SJCL validator
+     *
+     * Checks if a json string is a proper SJCL encrypted message.
+     *
+     * @access public
+     * @static
+     * @param  string $encoded JSON
+     * @return bool
+     */
+    public static function isValid($encoded)
+    {
+        $accepted_keys = array('iv','salt','ct');
+
+        // Make sure content is valid json
+        $decoded = json_decode($encoded);
+        if (is_null($decoded)) return false;
+        $decoded = (array) $decoded;
+
+        // Make sure required fields are present and contain base64 data.
+        foreach($accepted_keys as $k)
+        {
+            if (!array_key_exists($k, $decoded)) return false;
+            if (is_null(base64_decode($decoded[$k], $strict=true))) return false;
+        }
+
+        // Make sure no additionnal keys were added.
+        if (
+            count(
+                array_intersect(
+                    array_keys($decoded),
+                    $accepted_keys
+                )
+            ) != 3
+        ) return false;
+
+        // FIXME: Reject data if entropy is too low?
+
+        // Make sure some fields have a reasonable size.
+        if (strlen($decoded['iv']) > 24) return false;
+        if (strlen($decoded['salt']) > 14) return false;
+
+        return true;
+    }
+}
diff --git a/lib/traffic_limiter.php b/lib/traffic_limiter.php
new file mode 100644
index 00000000..c2cc4e8a
--- /dev/null
+++ b/lib/traffic_limiter.php
@@ -0,0 +1,111 @@
+<?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.15
+ */
+
+/**
+ * traffic_limiter
+ *
+ * Handles traffic limiting, so no user does more than one call per 10 seconds.
+ */
+class traffic_limiter
+{
+    /**
+     * @access private
+     * @static
+     * @var    int
+     */
+    private static $_limit = 10;
+
+    /**
+     * @access private
+     * @static
+     * @var    string
+     */
+    private static $_path = 'data';
+
+    /**
+     * set the time limit in seconds
+     *
+     * @access public
+     * @static
+     * @param  int $limit
+     * @return void
+     */
+    public static function setLimit($limit)
+    {
+        self::$_limit = $limit;
+    }
+
+    /**
+     * set the path
+     *
+     * @access public
+     * @static
+     * @param  string $path
+     * @return void
+     */
+    public static function setPath($path)
+    {
+        self::$_path = $path;
+    }
+
+    /**
+     * traffic limiter
+     *
+     * Make sure the IP address makes at most 1 request every 10 seconds.
+     *
+     * @access public
+     * @static
+     * @param  string $ip
+     * @return bool
+     */
+    public static function canPass($ip)
+    {
+        if (!is_dir(self::$_path)) mkdir(self::$_path, 0705, true);
+        $file = self::$_path . '/traffic_limiter.php';
+        if (!is_file($file))
+        {
+            file_put_contents(
+                $file,
+                '<?php' . PHP_EOL .
+                '$GLOBALS[\'traffic_limiter\'] = array();' . PHP_EOL
+            );
+            chmod($file, 0705);
+        }
+
+        require $file;
+        $tl = $GLOBALS['traffic_limiter'];
+
+        // purge file of expired IPs to keep it small
+        foreach($tl as $key => $time)
+        {
+            if ($time + 10 < time())
+            {
+                unset($tl[$key]);
+            }
+        }
+
+        if (array_key_exists($ip, $tl) && ($tl[$ip] + 10 >= time()))
+        {
+            $result = false;
+        } else {
+            $tl[$ip] = time();
+            $result = true;
+        }
+        file_put_contents(
+            $file,
+            '<?php' . PHP_EOL .
+            '$GLOBALS[\'traffic_limiter\'] = ' .
+            var_export($tl, true) . ';' . PHP_EOL
+        );
+        return $result;
+    }
+}
diff --git a/lib/vizhash_gd_zero.php b/lib/vizhash_gd_zero.php
index c9e081f2..110f4088 100644
--- a/lib/vizhash_gd_zero.php
+++ b/lib/vizhash_gd_zero.php
@@ -22,17 +22,17 @@ class vizhash16x16
     {
         $this->width=16;
         $this->height=16;
-        
+
         // Read salt from file (and create it if does not exist).
         // The salt will make vizhash avatar unique on each ZeroBin installation
         // to prevent IP checking.
-        $saltfile = 'data/salt.php';
+        $saltfile = PATH . 'data/salt.php';
         if (!is_file($saltfile))
             file_put_contents($saltfile,'<?php /* |'.$this->randomSalt().'| */ ?>');
         $items=explode('|',file_get_contents($saltfile));
         $this->salt = $items[1];
-    }  
-    
+    }
+
     // Generate a 16x16 png corresponding to $text.
     // Input: $text (string)
     // Output: PNG data. Or empty string if GD is not available.
@@ -61,14 +61,14 @@ class vizhash16x16
         $image = $this->degrade($image,$op,array($r0,$g0,$b0),array(0,0,0));
 
         for($i=0; $i<7; $i=$i+1)
-        {     
+        {
             $action=$this->getInt();
             $color = imagecolorallocate($image, $r,$g,$b);
             $r = ($r0 + $this->getInt()/25)%256;
             $g = ($g0 + $this->getInt()/25)%256;
             $b = ($b0 + $this->getInt()/25)%256;
             $r0=$r; $g0=$g; $b0=$b;
-            $this->drawshape($image,$action,$color);   
+            $this->drawshape($image,$action,$color);
         }
 
         $color = imagecolorallocate($image,$this->getInt(),$this->getInt(),$this->getInt());
@@ -78,10 +78,10 @@ class vizhash16x16
         $imagedata = ob_get_contents();
         ob_end_clean();
         imagedestroy($image);
-        
+
         return $imagedata;
-    } 
-    
+    }
+
     // Generate a large random hexadecimal salt.
     private function randomSalt()
     {
@@ -89,25 +89,25 @@ class vizhash16x16
         for($i=0;$i<6;$i++) { $randomSalt.=base_convert(mt_rand(),10,16); }
         return $randomSalt;
     }
-   
-    
+
+
     private function getInt() // Returns a single integer from the $VALUES array (0...255)
     {
-        $v= $this->VALUES[$this->VALUES_INDEX]; 
+        $v= $this->VALUES[$this->VALUES_INDEX];
         $this->VALUES_INDEX++;
         $this->VALUES_INDEX %= count($this->VALUES); // Warp around the array
         return $v;
     }
-    private function getX() // Returns a single integer from the array (roughly mapped to image width) 
+    private function getX() // Returns a single integer from the array (roughly mapped to image width)
     {
         return $this->width*$this->getInt()/256;
     }
 
-    private function getY() // Returns a single integer from the array (roughly mapped to image height) 
-    { 
+    private function getY() // Returns a single integer from the array (roughly mapped to image height)
+    {
         return $this->height*$this->getInt()/256;
-    }  
-    
+    }
+
     # Gradient function taken from:
     # http://www.supportduweb.com/scripts_tutoriaux-code-source-41-gd-faire-un-degrade-en-php-gd-fonction-degrade-imagerie.html
     private function degrade($img,$direction,$color1,$color2)
@@ -129,17 +129,17 @@ class vizhash16x16
             }
             return $img;
     }
-    
+
     private function drawshape($image,$action,$color)
     {
         switch($action%7)
         {
             case 0:
-                ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color);  
+                ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color);
                 break;
             case 1:
             case 2:
-                ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);  
+                ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
                 break;
             case 3:
                 $points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(),$this->getX(), $this->getY());
@@ -150,9 +150,9 @@ class vizhash16x16
             case 6:
                 $start=$this->getInt()*360/256; $end=$start+$this->getInt()*180/256;
                 ImageFilledArc ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(),$start,$end,$color,IMG_ARC_PIE);
-                break;     
+                break;
         }
-    }    
-}    
+    }
+}
 
 ?>
\ No newline at end of file
diff --git a/lib/zerobin.php b/lib/zerobin.php
new file mode 100644
index 00000000..2e389fe1
--- /dev/null
+++ b/lib/zerobin.php
@@ -0,0 +1,406 @@
+<?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.15
+ */
+
+/**
+ * zerobin
+ *
+ * Controller, puts it all together.
+ */
+class zerobin
+{
+    /*
+     * @const string version
+     */
+    const VERSION = 'Alpha 0.15';
+
+    /**
+     * @access private
+     * @var    array
+     */
+    private $_conf = array(
+        'model' => 'zerobin_data',
+    );
+
+    /**
+     * @access private
+     * @var    string
+     */
+    private $_data = '';
+
+    /**
+     * @access private
+     * @var    string
+     */
+    private $_error = '';
+
+    /**
+     * @access private
+     * @var    zerobin_data
+     */
+    private $_model;
+
+    /**
+     * constructor
+     *
+     * initializes and runs ZeroBin
+     *
+     * @access public
+     */
+    public function __construct()
+    {
+        if (version_compare(PHP_VERSION, '5.2.6') < 0)
+            die('ZeroBin requires php 5.2.6 or above to work. Sorry.');
+
+        // In case stupid admin has left magic_quotes enabled in php.ini.
+        if (get_magic_quotes_gpc())
+        {
+            require_once PATH . 'lib/filter.php';
+            $_POST   = array_map('filter::stripslashes_deep', $_POST);
+            $_GET    = array_map('filter::stripslashes_deep', $_GET);
+            $_COOKIE = array_map('filter::stripslashes_deep', $_COOKIE);
+        }
+
+        // Load config from ini file.
+        $this->_init();
+
+        // Create new paste or comment.
+        if (!empty($_POST['data']))
+        {
+            $this->_create();
+        }
+        // Display an existing paste.
+        elseif (!empty($_SERVER['QUERY_STRING']))
+        {
+            $this->_read();
+        }
+
+        // Display ZeroBin frontend
+        $this->_view();
+    }
+
+    /**
+     * initialize zerobin
+     *
+     * @access private
+     * @return void
+     */
+    private function _init()
+    {
+        $this->_conf = parse_ini_file(PATH . 'cfg/conf.ini');
+        $this->_model = $this->_conf['model'];
+    }
+
+    /**
+     * get the model, create one if needed
+     *
+     * @access private
+     * @return zerobin_data
+     */
+    private function _model()
+    {
+        // if needed, initialize the model
+        if(is_string($this->_model)) {
+            require_once PATH . 'lib/' . $this->_model . '.php';
+            $this->_model = forward_static_call(array($this->_model, 'getInstance'), $this->_conf['model_options']);
+        }
+        return $this->_model;
+    }
+
+    /**
+     * Store new paste or comment.
+     *
+     * POST contains:
+     * 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)
+     * 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.
+     * pasteid (optional) = in discussion, which paste this comment belongs to.
+     *
+     * @access private
+     * @return void
+     */
+    private function _create()
+    {
+        header('Content-type: application/json');
+        $error = false;
+
+        // Make sure last paste from the IP address was more than 10 seconds ago.
+        require_once PATH . 'lib/traffic_limiter.php';
+        traffic_limiter::setLimit($this->_conf['traffic_limit']);
+        traffic_limiter::setPath($this->_conf['traffic_dir']);
+        if (
+            !traffic_limiter::canPass($_SERVER['REMOTE_ADDR'])
+        ) $this->_return_message(1, 'Please wait 10 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.');
+
+        // Make sure format is correct.
+        require_once PATH . 'lib/sjcl.php';
+        if (!sjcl::isValid($data)) $this->_return_message(1, 'Invalid data.');
+
+        // Read additional meta-information.
+        $meta=array();
+
+        // Read expiration date
+        if (!empty($_POST['expire']))
+        {
+            switch ($_POST['expire'])
+            {
+                case '10min':
+                    $meta['expire_date'] = time()+10*60;
+                    break;
+                case '1hour':
+                    $meta['expire_date'] = time()+60*60;
+                    break;
+                case '1day':
+                    $meta['expire_date'] = time()+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']))
+        {
+            $opendiscussion = $_POST['opendiscussion'];
+            if ($opendiscussion != 0)
+            {
+                if ($opendiscussion != 1) $error = true;
+                $meta['opendiscussion'] = true;
+            }
+        }
+
+        // You can't have an open discussion on a "Burn after reading" paste:
+        if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']);
+
+        // Optional nickname for comments
+        if (!empty($_POST['nickname']))
+        {
+            // Generation of the anonymous avatar (Vizhash):
+            // If a nickname is provided, we generate a Vizhash.
+            // (We assume that if the user did not enter a nickname, he/she wants
+            // to be anonymous and we will not generate the vizhash.)
+            $nick = $_POST['nickname'];
+            if (!sjcl::isValid($nick))
+            {
+                $error = true;
+            }
+            else
+            {
+                require_once PATH . 'lib/vizhash_gd_zero.php';
+                $meta['nickname'] = $nick;
+                $vz = new vizhash16x16();
+                $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']);
+                if ($pngdata != '')
+                {
+                    $meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata);
+                }
+                // Once the avatar is generated, we do not keep the IP address, nor its hash.
+            }
+        }
+
+        if ($error) $this->_return_message(1, 'Invalid data.');
+
+        // Add post date to meta.
+        $meta['postdate'] = time();
+
+        // We just want a small hash to avoid collisions:
+        // Half-MD5 (64 bits) will do the trick
+        $dataid = substr(hash('md5', $data), 0, 16);
+
+        $storage = array('data' => $data);
+
+        // Add meta-information only if necessary.
+        if (count($meta)) $storage['meta'] = $meta;
+
+        // The user posts a comment.
+        if (
+            !empty($_POST['parentid']) &&
+            !empty($_POST['pasteid'])
+        )
+        {
+            $pasteid  = $_POST['pasteid'];
+            $parentid = $_POST['parentid'];
+            if (
+                !preg_match('/[a-f\d]{16}/', $pasteid) ||
+                !preg_match('/[a-f\d]{16}/', $parentid)
+            ) $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)
+            ) $this->_return_message(1, 'Invalid data.');
+
+            // Make sure the discussion is opened in this paste.
+            $paste = $this->_model()->read($pasteid);
+            if (
+                !$paste->meta->opendiscussion
+            ) $this->_return_message(1, 'Invalid data.');
+
+            // Check for improbable collision.
+            if (
+                $this->_model()->existsComment($pasteid, $parentid, $dataid)
+            ) $this->_return_message(1, 'You are unlucky. Try again.');
+
+            // New comment
+            if (
+                $this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false
+            ) $this->_return_message(1, 'Error saving comment. Sorry.');
+
+            // 0 = no error
+            $this->_return_message(0, $dataid);
+        }
+        // The user posts a standard paste.
+        else
+        {
+            // Check for improbable collision.
+            if (
+                $this->_model()->exists($dataid)
+            ) $this->_return_message(1, 'You are unlucky. Try again.');
+
+            // New paste
+            if (
+                $this->_model()->create($dataid, $storage) === false
+            ) $this->_return_message(1, 'Error saving paste. Sorry.');
+
+            // 0 = no error
+            $this->_return_message(0, $dataid);
+        }
+
+        $this->_return_message(1, 'Server error.');
+    }
+
+    /**
+     * Read an existing paste or comment.
+     *
+     * @access private
+     * @return void
+     */
+    private function _read()
+    {
+        $dataid = $_SERVER['QUERY_STRING'];
+
+        // Is this a valid paste identifier?
+        if (preg_match('/[a-f\d]{16}/', $dataid))
+        {
+            // Check that paste exists.
+            if ($this->_model()->exists($dataid))
+            {
+                // Get the paste itself.
+                $paste = $this->_model()->read($dataid);
+
+                // See if paste has expired.
+                if (
+                    isset($paste->meta->expire_date) &&
+                    $paste->meta->expire_date < time()
+                )
+                {
+                    // Delete the paste
+                    $this->_model()->delete($dataid);
+                    $this->_error = 'Paste does not exist or has expired.';
+                }
+                // 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)
+                        );
+                    }
+                    $this->_data = json_encode($messages);
+
+                    // If the paste was meant to be read only once, delete it.
+                    if (
+                        property_exists($paste->meta, 'burnafterreading') &&
+                        $paste->meta->burnafterreading
+                    ) $this->_model()->delete($dataid);
+                }
+            }
+            else
+            {
+                $this->_error = 'Paste does not exist or has expired.';
+            }
+        }
+    }
+
+    /**
+     * Display ZeroBin frontend.
+     *
+     * @access private
+     * @return void
+     */
+    private function _view()
+    {
+        require_once PATH . 'lib/rain.tpl.class.php';
+        header('Content-Type: text/html; charset=utf-8');
+        $page = new RainTPL;
+        // 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('VERSION', self::VERSION);
+        $page->draw('page');
+    }
+
+    /**
+     * return JSON encoded message and exit
+     *
+     * @access private
+     * @param  bool $status
+     * @param  string $message
+     * @return void
+     */
+    private function _return_message($status, $message)
+    {
+        $result = array('status' => $status);
+        if ($status)
+        {
+            $result['message'] = $message;
+        }
+        else
+        {
+            $result['id'] = $message;
+        }
+        exit(json_encode($result));
+    }
+}
diff --git a/lib/zerobin_data.php b/lib/zerobin_data.php
new file mode 100644
index 00000000..8d1e1732
--- /dev/null
+++ b/lib/zerobin_data.php
@@ -0,0 +1,283 @@
+<?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.15
+ */
+
+/**
+ * zerobin_data
+ *
+ * Model for data access, implemented as a singleton.
+ */
+class zerobin_data
+{
+    /*
+     * @access private
+     * @static
+     * @var string directory where data is stored
+     */
+    private static $_dir = 'data/';
+
+    /**
+     * singleton instance
+     *
+     * @access private
+     * @static
+     * @var zerobin
+     */
+    private static $_instance = null;
+
+    /**
+     * enforce singleton, disable constructor
+     *
+     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
+     *
+     * @access protected
+     */
+    protected function __construct() {}
+
+    /**
+     * enforce singleton, disable cloning
+     *
+     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
+     *
+     * @access private
+     */
+    private function __clone() {}
+
+    /**
+     * get instance of singleton
+     *
+     * @access public
+     * @static
+     * @return zerobin
+     */
+    public static function getInstance($options)
+    {
+        // if needed initialize the singleton
+        if(null === self::$_instance) {
+            self::$_instance = new self;
+            self::_init();
+        }
+        if (
+        	is_array($options) && 
+        	array_key_exists('dir', $options)
+        ) self::$_dir = $options['dir'];
+        return self::$_instance;
+    }
+
+    /**
+     * Create a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @param  array  $paste
+     * @return int|false
+     */
+    public function create($pasteid, $paste)
+    {
+        $storagedir = self::_dataid2path($pasteid);
+        if (is_file($storagedir . $pasteid)) return false;
+        if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
+        return file_put_contents($storagedir . $pasteid, json_encode($paste));
+    }
+
+    /**
+     * Read a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return string
+     */
+    public function read($pasteid)
+    {
+        if(!$this->exists($pasteid)) return json_decode(
+            '{"data":"","meta":{"burnafterreading":true,"postdate":0}}'
+        );
+        return json_decode(
+            file_get_contents(self::_dataid2path($pasteid) . $pasteid)
+        );
+    }
+
+    /**
+     * Delete a paste and its discussion.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return void
+     */
+    public function delete($pasteid)
+    {
+        // Delete the paste itself.
+        unlink(self::_dataid2path($pasteid) . $pasteid);
+
+        // Delete discussion if it exists.
+        $discdir = self::_dataid2discussionpath($pasteid);
+        if (is_dir($discdir))
+        {
+            // Delete all files in discussion directory
+            $dir = dir($discdir);
+            while (false !== ($filename = $dir->read()))
+            {
+                if (is_file($discdir.$filename)) unlink($discdir.$filename);
+            }
+            $dir->close();
+
+            // Delete the discussion directory.
+            rmdir($discdir);
+        }
+    }
+
+    /**
+     * Test if a paste exists.
+     *
+     * @access public
+     * @param  string $dataid
+     * @return void
+     */
+    public function exists($pasteid)
+    {
+        return is_file(self::_dataid2path($pasteid) . $pasteid);
+    }
+
+    /**
+     * Create a comment in a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @param  string $parentid
+     * @param  string $commentid
+     * @param  array  $comment
+     * @return int|false
+     */
+    public function createComment($pasteid, $parentid, $commentid, $comment)
+    {
+        $storagedir = self::_dataid2discussionpath($pasteid);
+        $filename = $pasteid . '.' . $commentid . '.' . $parentid;
+        if (is_file($storagedir . $filename)) return false;
+        if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
+        return file_put_contents($storagedir . $filename, json_encode($comment));
+    }
+
+    /**
+     * Read all comments of paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return array
+     */
+    public function readComments($pasteid)
+    {
+        $comments = array();
+        $discdir = self::_dataid2discussionpath($pasteid);
+        if (is_dir($discdir))
+        {
+            // Delete all files in discussion directory
+            $dir = dir($discdir);
+            while (false !== ($filename = $dir->read()))
+            {
+                // Filename is in the form pasteid.commentid.parentid:
+                // - 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))
+                {
+                    $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];
+
+                    // Store in array
+                    $comments[$comment->meta->postdate]=$comment;
+                }
+            }
+            $dir->close();
+
+            // Sort comments by date, oldest first.
+            ksort($comments);
+        }
+        return $comments;
+    }
+
+    /**
+     * Test if a comment exists.
+     *
+     * @access public
+     * @param  string $dataid
+     * @param  string $parentid
+     * @param  string $commentid
+     * @return void
+     */
+    public function existsComment($pasteid, $parentid, $commentid)
+    {
+        return is_file(
+            self::_dataid2discussionpath($pasteid) .
+            $pasteid . '.' . $dataid . '.' . $parentid
+        );
+    }
+
+    /**
+     * initialize zerobin
+     *
+     * @access private
+     * @static
+     * @return void
+     */
+    private static function _init()
+    {
+        if (defined('PATH')) self::$_dir = PATH . self::$_dir;
+
+        // Create storage directory if it does not exist.
+        if (!is_dir(self::$_dir))
+        {
+            mkdir(self::$_dir, 0705);
+            file_put_contents(
+                self::$_dir . '.htaccess',
+                'Allow from none' . PHP_EOL .
+                'Deny from all'. PHP_EOL
+            );
+        }
+    }
+
+    /**
+     * Convert paste id to storage path.
+     *
+     * The idea is to creates subdirectories in order to limit the number of files per directory.
+     * (A high number of files in a single directory can slow things down.)
+     * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
+     * High-trafic websites may want to deepen the directory structure (like Squid does).
+     *
+     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
+     *
+     * @access private
+     * @static
+     * @param  string $dataid
+     * @return void
+     */
+    private static function _dataid2path($dataid)
+    {
+        return self::$_dir . substr($dataid,0,2) . '/' . substr($dataid,2,2) . '/';
+    }
+
+    /**
+     * Convert paste id to discussion storage path.
+     *
+     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
+     *
+     * @access private
+     * @static
+     * @param  string $dataid
+     * @return void
+     */
+    private static function _dataid2discussionpath($dataid)
+    {
+        return self::_dataid2path($dataid) . $dataid . '.discussion/';
+    }
+}
diff --git a/lib/zerobin_db.php b/lib/zerobin_db.php
new file mode 100644
index 00000000..51fbe8de
--- /dev/null
+++ b/lib/zerobin_db.php
@@ -0,0 +1,176 @@
+<?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.15
+ */
+
+/**
+ * zerobin_db
+ *
+ * Model for DB access, implemented as a singleton.
+ */
+class zerobin_db
+{
+    /*
+     * @access private
+     * @static
+     * @var PDO instance of database connection
+     */
+    private static $_db;
+
+	/**
+     * singleton instance
+     *
+     * @access private
+     * @static
+     * @var zerobin
+     */
+    private static $_instance = null;
+
+    /**
+     * enforce singleton, disable constructor
+     *
+     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
+     *
+     * @access protected
+     */
+    protected function __construct() {}
+
+    /**
+     * enforce singleton, disable cloning
+     *
+     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
+     *
+     * @access private
+     */
+    private function __clone() {}
+
+    /**
+     * get instance of singleton
+     *
+     * @access public
+     * @static
+     * @return zerobin
+     */
+    public static function getInstance($options)
+    {
+        // if needed initialize the singleton
+        if(null === self::$_instance) {
+            self::$_instance = new self;
+            self::_init();
+        }
+        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']
+        );
+        return self::$_instance;
+    }
+
+    /**
+     * Create a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @param  array  $paste
+     * @return int|false
+     */
+    public function create($pasteid, $paste)
+    {
+    }
+
+    /**
+     * Read a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return string
+     */
+    public function read($pasteid)
+    {
+    }
+
+    /**
+     * Delete a paste and its discussion.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return void
+     */
+    public function delete($pasteid)
+    {
+    }
+
+    /**
+     * Test if a paste exists.
+     *
+     * @access public
+     * @param  string $dataid
+     * @return void
+     */
+    public function exists($pasteid)
+    {
+    }
+
+    /**
+     * Create a comment in a paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @param  string $parentid
+     * @param  string $commentid
+     * @param  array  $comment
+     * @return int|false
+     */
+    public function createComment($pasteid, $parentid, $commentid, $comment)
+    {
+    }
+
+    /**
+     * Read all comments of paste.
+     *
+     * @access public
+     * @param  string $pasteid
+     * @return array
+     */
+    public function readComments($pasteid)
+    {
+    }
+
+    /**
+     * Test if a comment exists.
+     *
+     * @access public
+     * @param  string $dataid
+     * @param  string $parentid
+     * @param  string $commentid
+     * @return void
+     */
+    public function existsComment($pasteid, $parentid, $commentid)
+    {
+    }
+
+    /**
+     * initialize zerobin
+     *
+     * @access private
+     * @static
+     * @return void
+     */
+    private static function _init()
+    {
+    }
+}