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

Merge branch 'persistence-into-data'

El RIDO 5 лет назад
Родитель
Сommit
5f2daa5cd6

+ 2 - 0
CHANGELOG.md

@@ -10,6 +10,8 @@
     * CHANGED: Language selection cookie only transmitted over HTTPS (#472)
     * CHANGED: Upgrading libraries to: random_compat 2.0.20
     * CHANGED: Removed automatic `.ini` configuration file migration (#808)
+    * CHANGED: Removed configurable `dir` for `traffic` & `purge` limiters (#419)
+    * CHANGED: Server salt, traffic and purge limiter now stored in the storage backend (#419)
   * **1.3.5 (2021-04-05)**
     * ADDED: Translation for Hebrew, Lithuanian, Indonesian and Catalan
     * ADDED: Make the project info configurable (#681)

+ 0 - 6
cfg/conf.sample.php

@@ -143,9 +143,6 @@ limit = 10
 ; set the HTTP header containing the visitors IP address, i.e. X_FORWARDED_FOR
 ; header = "X_FORWARDED_FOR"
 
-; directory to store the traffic limits in
-dir = PATH "data"
-
 [purge]
 ; minimum time limit between two purgings of expired pastes, it is only
 ; triggered when pastes are created
@@ -157,9 +154,6 @@ limit = 300
 ; site
 batchsize = 10
 
-; directory to store the purge limit in
-dir = PATH "data"
-
 [model]
 ; name of data model class to load and directory for storage
 ; the default model "Filesystem" stores everything in the filesystem

+ 0 - 2
lib/Configuration.php

@@ -80,13 +80,11 @@ class Configuration
         'traffic' => array(
             'limit'      => 10,
             'header'     => null,
-            'dir'        => 'data',
             'exemptedIp' => null,
         ),
         'purge' => array(
             'limit'     => 300,
             'batchsize' => 10,
-            'dir'       => 'data',
         ),
         'model' => array(
             'class' => 'Filesystem',

+ 11 - 15
lib/Controller.php

@@ -162,7 +162,6 @@ class Controller
         $this->_model   = new Model($this->_conf);
         $this->_request = new Request;
         $this->_urlBase = $this->_request->getRequestUri();
-        ServerSalt::setPath($this->_conf->getKey('dir', 'traffic'));
 
         // set default language
         $lang = $this->_conf->getKey('languagedefault');
@@ -196,20 +195,17 @@ class Controller
      */
     private function _create()
     {
-        try {
-            // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
-            TrafficLimiter::setConfiguration($this->_conf);
-            if (!TrafficLimiter::canPass()) {
-                $this->_return_message(
-                    1, I18n::_(
-                        'Please wait %d seconds between each post.',
-                        $this->_conf->getKey('limit', 'traffic')
-                    )
-                );
-                return;
-            }
-        } catch (Exception $e) {
-            $this->_return_message(1, I18n::_($e->getMessage()));
+        // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
+        ServerSalt::setStore($this->_model->getStore());
+        TrafficLimiter::setConfiguration($this->_conf);
+        TrafficLimiter::setStore($this->_model->getStore());
+        if (!TrafficLimiter::canPass()) {
+            $this->_return_message(
+                1, I18n::_(
+                    'Please wait %d seconds between each post.',
+                    $this->_conf->getKey('limit', 'traffic')
+                )
+            );
             return;
         }
 

+ 56 - 7
lib/Data/AbstractData.php

@@ -15,12 +15,12 @@ namespace PrivateBin\Data;
 /**
  * AbstractData
  *
- * Abstract model for PrivateBin data access, implemented as a singleton.
+ * Abstract model for data access, implemented as a singleton.
  */
 abstract class AbstractData
 {
     /**
-     * singleton instance
+     * Singleton instance
      *
      * @access protected
      * @static
@@ -29,9 +29,18 @@ abstract class AbstractData
     protected static $_instance = null;
 
     /**
-     * enforce singleton, disable constructor
+     * cache for the traffic limiter
      *
-     * Instantiate using {@link getInstance()}, privatebin is a singleton object.
+     * @access private
+     * @static
+     * @var    array
+     */
+    protected static $_last_cache = array();
+
+    /**
+     * Enforce singleton, disable constructor
+     *
+     * Instantiate using {@link getInstance()}, this object implements the singleton pattern.
      *
      * @access protected
      */
@@ -40,9 +49,9 @@ abstract class AbstractData
     }
 
     /**
-     * enforce singleton, disable cloning
+     * Enforce singleton, disable cloning
      *
-     * Instantiate using {@link getInstance()}, privatebin is a singleton object.
+     * Instantiate using {@link getInstance()}, this object implements the singleton pattern.
      *
      * @access private
      */
@@ -51,7 +60,7 @@ abstract class AbstractData
     }
 
     /**
-     * get instance of singleton
+     * Get instance of singleton
      *
      * @access public
      * @static
@@ -130,6 +139,46 @@ abstract class AbstractData
      */
     abstract public function existsComment($pasteid, $parentid, $commentid);
 
+    /**
+     * Purge outdated entries.
+     *
+     * @access public
+     * @param  string $namespace
+     * @param  int $time
+     * @return void
+     */
+    public function purgeValues($namespace, $time)
+    {
+        if ($namespace === 'traffic_limiter') {
+            foreach (self::$_last_cache as $key => $last_submission) {
+                if ($last_submission <= $time) {
+                    unset(self::$_last_cache[$key]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Save a value.
+     *
+     * @access public
+     * @param  string $value
+     * @param  string $namespace
+     * @param  string $key
+     * @return bool
+     */
+    abstract public function setValue($value, $namespace, $key = '');
+
+    /**
+     * Load a value.
+     *
+     * @access public
+     * @param  string $namespace
+     * @param  string $key
+     * @return string
+     */
+    abstract public function getValue($namespace, $key = '');
+
     /**
      * Returns up to batch size number of paste ids that have expired
      *

+ 129 - 43
lib/Data/Database.php

@@ -198,21 +198,25 @@ class Database extends AbstractData
             $opendiscussion   = $paste['adata'][2];
             $burnafterreading = $paste['adata'][3];
         }
-        return self::_exec(
-            'INSERT INTO ' . self::_sanitizeIdentifier('paste') .
-            ' VALUES(?,?,?,?,?,?,?,?,?)',
-            array(
-                $pasteid,
-                $isVersion1 ? $paste['data'] : Json::encode($paste),
-                $created,
-                $expire_date,
-                (int) $opendiscussion,
-                (int) $burnafterreading,
-                Json::encode($meta),
-                $attachment,
-                $attachmentname,
-            )
-        );
+        try {
+            return self::_exec(
+                'INSERT INTO ' . self::_sanitizeIdentifier('paste') .
+                ' VALUES(?,?,?,?,?,?,?,?,?)',
+                array(
+                    $pasteid,
+                    $isVersion1 ? $paste['data'] : Json::encode($paste),
+                    $created,
+                    $expire_date,
+                    (int) $opendiscussion,
+                    (int) $burnafterreading,
+                    Json::encode($meta),
+                    $attachment,
+                    $attachmentname,
+                )
+            );
+        } catch (Exception $e) {
+            return false;
+        }
     }
 
     /**
@@ -229,11 +233,14 @@ class Database extends AbstractData
         }
 
         self::$_cache[$pasteid] = false;
-        $paste                  = self::_select(
-            'SELECT * FROM ' . self::_sanitizeIdentifier('paste') .
-            ' WHERE dataid = ?', array($pasteid), true
-        );
-
+        try {
+            $paste = self::_select(
+                'SELECT * FROM ' . self::_sanitizeIdentifier('paste') .
+                ' WHERE dataid = ?', array($pasteid), true
+            );
+        } catch (Exception $e) {
+            $paste = false;
+        }
         if ($paste === false) {
             return false;
         }
@@ -348,19 +355,23 @@ class Database extends AbstractData
                 $meta[$key] = null;
             }
         }
-        return self::_exec(
-            'INSERT INTO ' . self::_sanitizeIdentifier('comment') .
-            ' VALUES(?,?,?,?,?,?,?)',
-            array(
-                $commentid,
-                $pasteid,
-                $parentid,
-                $data,
-                $meta['nickname'],
-                $meta[$iconKey],
-                $meta[$createdKey],
-            )
-        );
+        try {
+            return self::_exec(
+                'INSERT INTO ' . self::_sanitizeIdentifier('comment') .
+                ' VALUES(?,?,?,?,?,?,?)',
+                array(
+                    $commentid,
+                    $pasteid,
+                    $parentid,
+                    $data,
+                    $meta['nickname'],
+                    $meta[$iconKey],
+                    $meta[$createdKey],
+                )
+            );
+        } catch (Exception $e) {
+            return false;
+        }
     }
 
     /**
@@ -416,13 +427,85 @@ class Database extends AbstractData
      */
     public function existsComment($pasteid, $parentid, $commentid)
     {
-        return (bool) self::_select(
-            'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') .
-            ' WHERE pasteid = ? AND parentid = ? AND dataid = ?',
-            array($pasteid, $parentid, $commentid), true
+        try {
+            return (bool) self::_select(
+                'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') .
+                ' WHERE pasteid = ? AND parentid = ? AND dataid = ?',
+                array($pasteid, $parentid, $commentid), true
+            );
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * Save a value.
+     *
+     * @access public
+     * @param  string $value
+     * @param  string $namespace
+     * @param  string $key
+     * @return bool
+     */
+    public function setValue($value, $namespace, $key = '')
+    {
+        if ($namespace === 'traffic_limiter') {
+            self::$_last_cache[$key] = $value;
+            try {
+                $value = Json::encode(self::$_last_cache);
+            } catch (Exception $e) {
+                return false;
+            }
+        }
+        return self::_exec(
+            'UPDATE ' . self::_sanitizeIdentifier('config') .
+            ' SET value = ? WHERE id = ?',
+            array($value, strtoupper($namespace))
         );
     }
 
+    /**
+     * Load a value.
+     *
+     * @access public
+     * @param  string $namespace
+     * @param  string $key
+     * @return string
+     */
+    public function getValue($namespace, $key = '')
+    {
+        $configKey = strtoupper($namespace);
+        $value     = $this->_getConfig($configKey);
+        if ($value === '') {
+            // initialize the row, so that setValue can rely on UPDATE queries
+            self::_exec(
+                'INSERT INTO ' . self::_sanitizeIdentifier('config') .
+                ' VALUES(?,?)',
+                array($configKey, '')
+            );
+
+            // migrate filesystem based salt into database
+            $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
+            if ($namespace === 'salt' && is_readable($file)) {
+                $value = Filesystem::getInstance(array('dir' => 'data'))->getValue('salt');
+                $this->setValue($value, 'salt');
+                @unlink($file);
+                return $value;
+            }
+        }
+        if ($value && $namespace === 'traffic_limiter') {
+            try {
+                self::$_last_cache = Json::decode($value);
+            } catch (Exception $e) {
+                self::$_last_cache = array();
+            }
+            if (array_key_exists($key, self::$_last_cache)) {
+                return self::$_last_cache[$key];
+            }
+        }
+        return (string) $value;
+    }
+
     /**
      * Returns up to batch size number of paste ids that have expired
      *
@@ -563,16 +646,19 @@ class Database extends AbstractData
      * @access private
      * @static
      * @param  string $key
-     * @throws PDOException
      * @return string
      */
     private static function _getConfig($key)
     {
-        $row = self::_select(
-            'SELECT value FROM ' . self::_sanitizeIdentifier('config') .
-            ' WHERE id = ?', array($key), true
-        );
-        return $row['value'];
+        try {
+            $row = self::_select(
+                'SELECT value FROM ' . self::_sanitizeIdentifier('config') .
+                ' WHERE id = ?', array($key), true
+            );
+        } catch (PDOException $e) {
+            return '';
+        }
+        return $row ? $row['value'] : '';
     }
 
     /**

+ 226 - 19
lib/Data/Filesystem.php

@@ -12,7 +12,8 @@
 
 namespace PrivateBin\Data;
 
-use PrivateBin\Persistence\DataStore;
+use Exception;
+use PrivateBin\Json;
 
 /**
  * Filesystem
@@ -21,6 +22,29 @@ use PrivateBin\Persistence\DataStore;
  */
 class Filesystem extends AbstractData
 {
+    /**
+     * first line in paste or comment files, to protect their contents from browsing exposed data directories
+     *
+     * @const string
+     */
+    const PROTECTION_LINE = '<?php http_response_code(403); /*';
+
+    /**
+     * line in generated .htaccess files, to protect exposed directories from being browsable on apache web servers
+     *
+     * @const string
+     */
+    const HTACCESS_LINE = 'Require all denied';
+
+    /**
+     * path in which to persist something
+     *
+     * @access private
+     * @static
+     * @var    string
+     */
+    private static $_path = 'data';
+
     /**
      * get instance of singleton
      *
@@ -40,7 +64,7 @@ class Filesystem extends AbstractData
             is_array($options) &&
             array_key_exists('dir', $options)
         ) {
-            DataStore::setPath($options['dir']);
+            self::$_path = $options['dir'];
         }
         return self::$_instance;
     }
@@ -63,7 +87,7 @@ class Filesystem extends AbstractData
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return DataStore::store($file, $paste);
+        return self::_store($file, $paste);
     }
 
     /**
@@ -75,12 +99,13 @@ class Filesystem extends AbstractData
      */
     public function read($pasteid)
     {
-        if (!$this->exists($pasteid)) {
+        if (
+            !$this->exists($pasteid) ||
+            !$paste = self::_get(self::_dataid2path($pasteid) . $pasteid . '.php')
+        ) {
             return false;
         }
-        return self::upgradePreV1Format(
-            DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php')
-        );
+        return self::upgradePreV1Format($paste);
     }
 
     /**
@@ -127,7 +152,7 @@ class Filesystem extends AbstractData
         $pastePath = $basePath . '.php';
         // convert to PHP protected files if needed
         if (is_readable($basePath)) {
-            DataStore::prependRename($basePath, $pastePath);
+            self::_prependRename($basePath, $pastePath);
 
             // convert comments, too
             $discdir = self::_dataid2discussionpath($pasteid);
@@ -136,7 +161,7 @@ class Filesystem extends AbstractData
                 while (false !== ($filename = $dir->read())) {
                     if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) {
                         $commentFilename = $discdir . $filename . '.php';
-                        DataStore::prependRename($discdir . $filename, $commentFilename);
+                        self::_prependRename($discdir . $filename, $commentFilename);
                     }
                 }
                 $dir->close();
@@ -165,7 +190,7 @@ class Filesystem extends AbstractData
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return DataStore::store($file, $comment);
+        return self::_store($file, $comment);
     }
 
     /**
@@ -187,7 +212,7 @@ class Filesystem extends AbstractData
                 // - commentid is the comment identifier itself.
                 // - parentid is the comment this comment replies to (It can be pasteid)
                 if (is_file($discdir . $filename)) {
-                    $comment = DataStore::get($discdir . $filename);
+                    $comment = self::_get($discdir . $filename);
                     $items   = explode('.', $filename);
                     // Add some meta information not contained in file.
                     $comment['id']       = $items[1];
@@ -223,6 +248,97 @@ class Filesystem extends AbstractData
         );
     }
 
+    /**
+     * Save a value.
+     *
+     * @access public
+     * @param  string $value
+     * @param  string $namespace
+     * @param  string $key
+     * @return bool
+     */
+    public function setValue($value, $namespace, $key = '')
+    {
+        switch ($namespace) {
+            case 'purge_limiter':
+                return self::_storeString(
+                    self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
+                    '<?php' . PHP_EOL . '$GLOBALS[\'purge_limiter\'] = ' . $value . ';'
+                );
+            case 'salt':
+                return self::_storeString(
+                    self::$_path . DIRECTORY_SEPARATOR . 'salt.php',
+                    '<?php # |' . $value . '|'
+                );
+            case 'traffic_limiter':
+                self::$_last_cache[$key] = $value;
+                return self::_storeString(
+                    self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
+                    '<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export(self::$_last_cache, true) . ';'
+                );
+        }
+        return false;
+    }
+
+    /**
+     * Load a value.
+     *
+     * @access public
+     * @param  string $namespace
+     * @param  string $key
+     * @return string
+     */
+    public function getValue($namespace, $key = '')
+    {
+        switch ($namespace) {
+            case 'purge_limiter':
+                $file = self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
+                if (is_readable($file)) {
+                    require $file;
+                    return $GLOBALS['purge_limiter'];
+                }
+                break;
+            case 'salt':
+                $file = self::$_path . DIRECTORY_SEPARATOR . 'salt.php';
+                if (is_readable($file)) {
+                    $items = explode('|', file_get_contents($file));
+                    if (is_array($items) && count($items) == 3) {
+                        return $items[1];
+                    }
+                }
+                break;
+            case 'traffic_limiter':
+                $file = self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
+                if (is_readable($file)) {
+                    require $file;
+                    self::$_last_cache = $GLOBALS['traffic_limiter'];
+                    if (array_key_exists($key, self::$_last_cache)) {
+                        return self::$_last_cache[$key];
+                    }
+                }
+                break;
+        }
+        return '';
+    }
+
+    /**
+     * get the data
+     *
+     * @access public
+     * @static
+     * @param  string $filename
+     * @return array|false $data
+     */
+    private static function _get($filename)
+    {
+        return Json::decode(
+            substr(
+                file_get_contents($filename),
+                strlen(self::PROTECTION_LINE . PHP_EOL)
+            )
+        );
+    }
+
     /**
      * Returns up to batch size number of paste ids that have expired
      *
@@ -233,9 +349,8 @@ class Filesystem extends AbstractData
     protected function _getExpiredPastes($batchsize)
     {
         $pastes     = array();
-        $mainpath   = DataStore::getPath();
         $firstLevel = array_filter(
-            scandir($mainpath),
+            scandir(self::$_path),
             'self::_isFirstLevelDir'
         );
         if (count($firstLevel) > 0) {
@@ -243,7 +358,7 @@ class Filesystem extends AbstractData
             for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
                 $firstKey    = array_rand($firstLevel);
                 $secondLevel = array_filter(
-                    scandir($mainpath . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
+                    scandir(self::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
                     'self::_isSecondLevelDir'
                 );
 
@@ -254,7 +369,7 @@ class Filesystem extends AbstractData
                 }
 
                 $secondKey = array_rand($secondLevel);
-                $path      = $mainpath . DIRECTORY_SEPARATOR .
+                $path      = self::$_path . DIRECTORY_SEPARATOR .
                     $firstLevel[$firstKey] . DIRECTORY_SEPARATOR .
                     $secondLevel[$secondKey];
                 if (!is_dir($path)) {
@@ -314,10 +429,9 @@ class Filesystem extends AbstractData
      */
     private static function _dataid2path($dataid)
     {
-        return DataStore::getPath(
+        return self::$_path . DIRECTORY_SEPARATOR .
             substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
-            substr($dataid, 2, 2) . DIRECTORY_SEPARATOR
-        );
+            substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
     }
 
     /**
@@ -347,7 +461,7 @@ class Filesystem extends AbstractData
     private static function _isFirstLevelDir($element)
     {
         return self::_isSecondLevelDir($element) &&
-            is_dir(DataStore::getPath($element));
+            is_dir(self::$_path . DIRECTORY_SEPARATOR . $element);
     }
 
     /**
@@ -362,4 +476,97 @@ class Filesystem extends AbstractData
     {
         return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
     }
+
+    /**
+     * store the data
+     *
+     * @access public
+     * @static
+     * @param  string $filename
+     * @param  array  $data
+     * @return bool
+     */
+    private static function _store($filename, array $data)
+    {
+        try {
+            return self::_storeString(
+                $filename,
+                self::PROTECTION_LINE . PHP_EOL . Json::encode($data)
+            );
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * store a string
+     *
+     * @access public
+     * @static
+     * @param  string $filename
+     * @param  string $data
+     * @return bool
+     */
+    private static function _storeString($filename, $data)
+    {
+        // Create storage directory if it does not exist.
+        if (!is_dir(self::$_path)) {
+            if (!@mkdir(self::$_path, 0700)) {
+                return false;
+            }
+        }
+        $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
+        if (!is_file($file)) {
+            $writtenBytes = 0;
+            if ($fileCreated = @touch($file)) {
+                $writtenBytes = @file_put_contents(
+                    $file,
+                    self::HTACCESS_LINE . PHP_EOL,
+                    LOCK_EX
+                );
+            }
+            if (
+                $fileCreated === false ||
+                $writtenBytes === false ||
+                $writtenBytes < strlen(self::HTACCESS_LINE . PHP_EOL)
+            ) {
+                return false;
+            }
+        }
+
+        $fileCreated  = true;
+        $writtenBytes = 0;
+        if (!is_file($filename)) {
+            $fileCreated = @touch($filename);
+        }
+        if ($fileCreated) {
+            $writtenBytes = @file_put_contents($filename, $data, LOCK_EX);
+        }
+        if ($fileCreated === false || $writtenBytes === false || $writtenBytes < strlen($data)) {
+            return false;
+        }
+        @chmod($filename, 0640); // protect file from access by other users on the host
+        return true;
+    }
+
+    /**
+     * rename a file, prepending the protection line at the beginning
+     *
+     * @access public
+     * @static
+     * @param  string $srcFile
+     * @param  string $destFile
+     * @return void
+     */
+    private static function _prependRename($srcFile, $destFile)
+    {
+        // don't overwrite already converted file
+        if (!is_readable($destFile)) {
+            $handle = fopen($srcFile, 'r', false, stream_context_create());
+            file_put_contents($destFile, self::PROTECTION_LINE . PHP_EOL);
+            file_put_contents($destFile, $handle, FILE_APPEND);
+            fclose($handle);
+        }
+        unlink($srcFile);
+    }
 }

+ 145 - 50
lib/Data/GoogleCloudStorage.php

@@ -4,11 +4,39 @@ namespace PrivateBin\Data;
 
 use Exception;
 use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Storage\Bucket;
 use Google\Cloud\Storage\StorageClient;
 use PrivateBin\Json;
 
 class GoogleCloudStorage extends AbstractData
 {
+    /**
+     * GCS client
+     *
+     * @access private
+     * @static
+     * @var    StorageClient
+     */
+    private static $_client = null;
+
+    /**
+     * GCS bucket
+     *
+     * @access private
+     * @static
+     * @var    Bucket
+     */
+    private static $_bucket = null;
+
+    /**
+     * object prefix
+     *
+     * @access private
+     * @static
+     * @var    string
+     */
+    private static $_prefix = 'pastes';
+
     /**
      * returns a Google Cloud Storage data backend.
      *
@@ -19,10 +47,12 @@ class GoogleCloudStorage extends AbstractData
      */
     public static function getInstance(array $options)
     {
-        $client = null;
-        $bucket = null;
-        $prefix = 'pastes';
+        // if needed initialize the singleton
+        if (!(self::$_instance instanceof self)) {
+            self::$_instance = new self;
+        }
 
+        $bucket = null;
         if (getenv('PRIVATEBIN_GCS_BUCKET')) {
             $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
         }
@@ -30,53 +60,36 @@ class GoogleCloudStorage extends AbstractData
             $bucket = $options['bucket'];
         }
         if (is_array($options) && array_key_exists('prefix', $options)) {
-            $prefix = $options['prefix'];
-        }
-        if (is_array($options) && array_key_exists('client', $options)) {
-            $client = $options['client'];
+            self::$_prefix = $options['prefix'];
         }
 
-        if (!(self::$_instance instanceof self)) {
-            self::$_instance = new self($bucket, $prefix, $client);
-        }
-        return self::$_instance;
-    }
-
-    protected $_client = null;
-    protected $_bucket = null;
-    protected $_prefix = 'pastes';
-
-    public function __construct($bucket, $prefix, $client = null)
-    {
-        parent::__construct();
-        if ($client == null) {
-            $this->_client = new StorageClient(array('suppressKeyFileNotice' => true));
-        } else {
-            // use given client for test purposes
-            $this->_client = $client;
+        if (empty(self::$_client)) {
+            self::$_client = class_exists('StorageClientStub', false) ?
+                new \StorageClientStub(array()) :
+                new StorageClient(array('suppressKeyFileNotice' => true));
         }
+        self::$_bucket = self::$_client->bucket($bucket);
 
-        $this->_bucket = $this->_client->bucket($bucket);
-        if ($prefix != null) {
-            $this->_prefix = $prefix;
-        }
+        return self::$_instance;
     }
 
     /**
-     * returns the google storage object key for $pasteid in $this->_bucket.
+     * returns the google storage object key for $pasteid in self::$_bucket.
+     *
+     * @access private
      * @param $pasteid string to get the key for
      * @return string
      */
     private function _getKey($pasteid)
     {
-        if ($this->_prefix != '') {
-            return $this->_prefix . '/' . $pasteid;
+        if (self::$_prefix != '') {
+            return self::$_prefix . '/' . $pasteid;
         }
         return $pasteid;
     }
 
     /**
-     * Uploads the payload in the $this->_bucket under the specified key.
+     * Uploads the payload in the self::$_bucket under the specified key.
      * The entire payload is stored as a JSON document. The metadata is replicated
      * as the GCS object's metadata except for the fields attachment, attachmentname
      * and salt.
@@ -85,7 +98,7 @@ class GoogleCloudStorage extends AbstractData
      * @param $payload array to store
      * @return bool true if successful, otherwise false.
      */
-    private function upload($key, $payload)
+    private function _upload($key, $payload)
     {
         $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
         unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
@@ -93,7 +106,7 @@ class GoogleCloudStorage extends AbstractData
             $metadata[$k] = strval($v);
         }
         try {
-            $this->_bucket->upload(Json::encode($payload), array(
+            self::$_bucket->upload(Json::encode($payload), array(
                 'name'          => $key,
                 'chunkSize'     => 262144,
                 'predefinedAcl' => 'private',
@@ -103,7 +116,7 @@ class GoogleCloudStorage extends AbstractData
                 ),
             ));
         } catch (Exception $e) {
-            error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
+            error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -119,7 +132,7 @@ class GoogleCloudStorage extends AbstractData
             return false;
         }
 
-        return $this->upload($this->_getKey($pasteid), $paste);
+        return $this->_upload($this->_getKey($pasteid), $paste);
     }
 
     /**
@@ -128,13 +141,13 @@ class GoogleCloudStorage extends AbstractData
     public function read($pasteid)
     {
         try {
-            $o    = $this->_bucket->object($this->_getKey($pasteid));
+            $o    = self::$_bucket->object($this->_getKey($pasteid));
             $data = $o->downloadAsString();
             return Json::decode($data);
         } catch (NotFoundException $e) {
             return false;
         } catch (Exception $e) {
-            error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
+            error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket->name() . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -148,9 +161,9 @@ class GoogleCloudStorage extends AbstractData
         $name = $this->_getKey($pasteid);
 
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
+            foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
                 try {
-                    $this->_bucket->object($comment->name())->delete();
+                    self::$_bucket->object($comment->name())->delete();
                 } catch (NotFoundException $e) {
                     // ignore if already deleted.
                 }
@@ -160,7 +173,7 @@ class GoogleCloudStorage extends AbstractData
         }
 
         try {
-            $this->_bucket->object($name)->delete();
+            self::$_bucket->object($name)->delete();
         } catch (NotFoundException $e) {
             // ignore if already deleted
         }
@@ -171,7 +184,7 @@ class GoogleCloudStorage extends AbstractData
      */
     public function exists($pasteid)
     {
-        $o = $this->_bucket->object($this->_getKey($pasteid));
+        $o = self::$_bucket->object($this->_getKey($pasteid));
         return $o->exists();
     }
 
@@ -184,7 +197,7 @@ class GoogleCloudStorage extends AbstractData
             return false;
         }
         $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
-        return $this->upload($key, $comment);
+        return $this->_upload($key, $comment);
     }
 
     /**
@@ -195,8 +208,8 @@ class GoogleCloudStorage extends AbstractData
         $comments = array();
         $prefix   = $this->_getKey($pasteid) . '/discussion/';
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
-                $comment         = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
+            foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) {
+                $comment         = JSON::decode(self::$_bucket->object($key->name())->downloadAsString());
                 $comment['id']   = basename($key->name());
                 $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
                 $comments[$slot] = $comment;
@@ -213,10 +226,92 @@ class GoogleCloudStorage extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     {
         $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
-        $o    = $this->_bucket->object($name);
+        $o    = self::$_bucket->object($name);
         return $o->exists();
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function purgeValues($namespace, $time)
+    {
+        $path = 'config/' . $namespace;
+        try {
+            foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) {
+                $name = $object->name();
+                if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
+                    continue;
+                }
+                $info = $object->info();
+                if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
+                    $value = $info['metadata']['value'];
+                    if (is_numeric($value) && intval($value) < $time) {
+                        try {
+                            $object->delete();
+                        } catch (NotFoundException $e) {
+                            // deleted by another instance.
+                        }
+                    }
+                }
+            }
+        } catch (NotFoundException $e) {
+            // no objects in the bucket yet
+        }
+    }
+
+    /**
+     * For GoogleCloudStorage, the value will also be stored in the metadata for the
+     * namespaces traffic_limiter and purge_limiter.
+     * @inheritDoc
+     */
+    public function setValue($value, $namespace, $key = '')
+    {
+        if ($key === '') {
+            $key = 'config/' . $namespace;
+        } else {
+            $key = 'config/' . $namespace . '/' . $key;
+        }
+
+        $metadata = array('namespace' => $namespace);
+        if ($namespace != 'salt') {
+            $metadata['value'] = strval($value);
+        }
+        try {
+            self::$_bucket->upload($value, array(
+                'name'          => $key,
+                'chunkSize'     => 262144,
+                'predefinedAcl' => 'private',
+                'metadata'      => array(
+                    'content-type' => 'application/json',
+                    'metadata'     => $metadata,
+                ),
+            ));
+        } catch (Exception $e) {
+            error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' .
+                trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getValue($namespace, $key = '')
+    {
+        if ($key === '') {
+            $key = 'config/' . $namespace;
+        } else {
+            $key = 'config/' . $namespace . '/' . $key;
+        }
+        try {
+            $o = self::$_bucket->object($key);
+            return $o->downloadAsString();
+        } catch (NotFoundException $e) {
+            return '';
+        }
+    }
+
     /**
      * @inheritDoc
      */
@@ -225,12 +320,12 @@ class GoogleCloudStorage extends AbstractData
         $expired = array();
 
         $now    = time();
-        $prefix = $this->_prefix;
+        $prefix = self::$_prefix;
         if ($prefix != '') {
-            $prefix = $prefix . '/';
+            $prefix .= '/';
         }
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
+            foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) {
                 $metadata = $object->info()['metadata'];
                 if ($metadata != null && array_key_exists('expire_date', $metadata)) {
                     $expire_at = intval($metadata['expire_date']);

+ 5 - 0
lib/FormatV2.php

@@ -52,6 +52,11 @@ class FormatV2
             }
         }
 
+        // Make sure adata is an array.
+        if (!is_array($message['adata'])) {
+            return false;
+        }
+
         $cipherParams = $isComment ? $message['adata'] : $message['adata'][0];
 
         // Make sure some fields are base64 data:

+ 3 - 3
lib/Json.php

@@ -44,13 +44,13 @@ class Json
      * @static
      * @param  string $input
      * @throws Exception
-     * @return array
+     * @return mixed
      */
     public static function decode($input)
     {
-        $array = json_decode($input, true);
+        $output = json_decode($input, true);
         self::_detectError();
-        return $array;
+        return $output;
     }
 
     /**

+ 4 - 3
lib/Model.php

@@ -54,7 +54,7 @@ class Model
      */
     public function getPaste($pasteId = null)
     {
-        $paste = new Paste($this->_conf, $this->_getStore());
+        $paste = new Paste($this->_conf, $this->getStore());
         if ($pasteId !== null) {
             $paste->setId($pasteId);
         }
@@ -67,8 +67,9 @@ class Model
     public function purge()
     {
         PurgeLimiter::setConfiguration($this->_conf);
+        PurgeLimiter::setStore($this->getStore());
         if (PurgeLimiter::canPurge()) {
-            $this->_getStore()->purge($this->_conf->getKey('batchsize', 'purge'));
+            $this->getStore()->purge($this->_conf->getKey('batchsize', 'purge'));
         }
     }
 
@@ -77,7 +78,7 @@ class Model
      *
      * @return Data\AbstractData
      */
-    private function _getStore()
+    public function getStore()
     {
         if ($this->_store === null) {
             $this->_store = forward_static_call(

+ 1 - 1
lib/Model/Paste.php

@@ -93,7 +93,7 @@ class Paste extends AbstractModel
         }
 
         $this->_data['meta']['created'] = time();
-        $this->_data['meta']['salt']    = serversalt::generate();
+        $this->_data['meta']['salt']    = ServerSalt::generate();
 
         // store paste
         if (

+ 7 - 98
lib/Persistence/AbstractPersistence.php

@@ -12,7 +12,7 @@
 
 namespace PrivateBin\Persistence;
 
-use Exception;
+use PrivateBin\Data\AbstractData;
 
 /**
  * AbstractPersistence
@@ -22,114 +22,23 @@ use Exception;
 abstract class AbstractPersistence
 {
     /**
-     * path in which to persist something
+     * data storage to use to persist something
      *
      * @access private
      * @static
-     * @var    string
+     * @var AbstractData
      */
-    private static $_path = 'data';
+    protected static $_store;
 
     /**
      * set the path
      *
      * @access public
      * @static
-     * @param  string $path
+     * @param  AbstractData $store
      */
-    public static function setPath($path)
+    public static function setStore(AbstractData $store)
     {
-        self::$_path = $path;
-    }
-
-    /**
-     * get the path
-     *
-     * @access public
-     * @static
-     * @param  string $filename
-     * @return string
-     */
-    public static function getPath($filename = null)
-    {
-        if (strlen($filename)) {
-            return self::$_path . DIRECTORY_SEPARATOR . $filename;
-        } else {
-            return self::$_path;
-        }
-    }
-
-    /**
-     * checks if the file exists
-     *
-     * @access protected
-     * @static
-     * @param  string $filename
-     * @return bool
-     */
-    protected static function _exists($filename)
-    {
-        self::_initialize();
-        return is_file(self::$_path . DIRECTORY_SEPARATOR . $filename);
-    }
-
-    /**
-     * prepares path for storage
-     *
-     * @access protected
-     * @static
-     * @throws Exception
-     */
-    protected static function _initialize()
-    {
-        // Create storage directory if it does not exist.
-        if (!is_dir(self::$_path)) {
-            if (!@mkdir(self::$_path, 0700)) {
-                throw new Exception('unable to create directory ' . self::$_path, 10);
-            }
-        }
-        $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
-        if (!is_file($file)) {
-            $writtenBytes = 0;
-            if ($fileCreated = @touch($file)) {
-                $writtenBytes = @file_put_contents(
-                    $file,
-                    'Require all denied' . PHP_EOL,
-                    LOCK_EX
-                );
-            }
-            if ($fileCreated === false || $writtenBytes === false || $writtenBytes < 19) {
-                throw new Exception('unable to write to file ' . $file, 11);
-            }
-        }
-    }
-
-    /**
-     * store the data
-     *
-     * @access protected
-     * @static
-     * @param  string $filename
-     * @param  string $data
-     * @throws Exception
-     * @return string
-     */
-    protected static function _store($filename, $data)
-    {
-        self::_initialize();
-        $file         = self::$_path . DIRECTORY_SEPARATOR . $filename;
-        $fileCreated  = true;
-        $writtenBytes = 0;
-        if (!is_file($file)) {
-            $fileCreated = @touch($file);
-        }
-        if ($fileCreated) {
-            $writtenBytes = @file_put_contents($file, $data, LOCK_EX);
-        }
-        if ($fileCreated === false || $writtenBytes === false || $writtenBytes < strlen($data)) {
-            throw new Exception('unable to write to file ' . $file, 13);
-        }
-        @chmod($file, 0640); // protect file access
-        return $file;
+        self::$_store = $store;
     }
 }

+ 0 - 96
lib/Persistence/DataStore.php

@@ -1,96 +0,0 @@
-<?php
-/**
- * PrivateBin
- *
- * a zero-knowledge paste bin
- *
- * @link      https://github.com/PrivateBin/PrivateBin
- * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
- * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
- * @version   1.3.5
- */
-
-namespace PrivateBin\Persistence;
-
-use Exception;
-use PrivateBin\Json;
-
-/**
- * DataStore
- *
- * Handles data storage for Data\Filesystem.
- */
-class DataStore extends AbstractPersistence
-{
-    /**
-     * first line in file, to protect its contents
-     *
-     * @const string
-     */
-    const PROTECTION_LINE = '<?php http_response_code(403); /*';
-
-    /**
-     * store the data
-     *
-     * @access public
-     * @static
-     * @param  string $filename
-     * @param  array  $data
-     * @return bool
-     */
-    public static function store($filename, $data)
-    {
-        $path = self::getPath();
-        if (strpos($filename, $path) === 0) {
-            $filename = substr($filename, strlen($path));
-        }
-        try {
-            self::_store(
-                $filename,
-                self::PROTECTION_LINE . PHP_EOL . Json::encode($data)
-            );
-            return true;
-        } catch (Exception $e) {
-            return false;
-        }
-    }
-
-    /**
-     * get the data
-     *
-     * @access public
-     * @static
-     * @param  string $filename
-     * @return array|false $data
-     */
-    public static function get($filename)
-    {
-        return Json::decode(
-            substr(
-                file_get_contents($filename),
-                strlen(self::PROTECTION_LINE . PHP_EOL)
-            )
-        );
-    }
-
-    /**
-     * rename a file, prepending the protection line at the beginning
-     *
-     * @access public
-     * @static
-     * @param  string $srcFile
-     * @param  string $destFile
-     * @return void
-     */
-    public static function prependRename($srcFile, $destFile)
-    {
-        // don't overwrite already converted file
-        if (!is_readable($destFile)) {
-            $handle = fopen($srcFile, 'r', false, stream_context_create());
-            file_put_contents($destFile, self::PROTECTION_LINE . PHP_EOL);
-            file_put_contents($destFile, $handle, FILE_APPEND);
-            fclose($handle);
-        }
-        unlink($srcFile);
-    }
-}

+ 8 - 13
lib/Persistence/PurgeLimiter.php

@@ -52,7 +52,6 @@ class PurgeLimiter extends AbstractPersistence
     public static function setConfiguration(Configuration $conf)
     {
         self::setLimit($conf->getKey('limit', 'purge'));
-        self::setPath($conf->getKey('dir', 'purge'));
     }
 
     /**
@@ -60,7 +59,6 @@ class PurgeLimiter extends AbstractPersistence
      *
      * @access public
      * @static
-     * @throws \Exception
      * @return bool
      */
     public static function canPurge()
@@ -71,17 +69,14 @@ class PurgeLimiter extends AbstractPersistence
         }
 
         $now  = time();
-        $file = 'purge_limiter.php';
-        if (self::_exists($file)) {
-            require self::getPath($file);
-            $pl = $GLOBALS['purge_limiter'];
-            if ($pl + self::$_limit >= $now) {
-                return false;
-            }
+        $pl   = (int) self::$_store->getValue('purge_limiter');
+        if ($pl + self::$_limit >= $now) {
+            return false;
         }
-
-        $content = '<?php' . PHP_EOL . '$GLOBALS[\'purge_limiter\'] = ' . $now . ';';
-        self::_store($file, $content);
-        return true;
+        $hasStored = self::$_store->setValue((string) $now, 'purge_limiter');
+        if (!$hasStored) {
+            error_log('failed to store the purge limiter, skipping purge cycle to avoid getting stuck in a purge loop');
+        }
+        return $hasStored;
     }
 }

+ 11 - 28
lib/Persistence/ServerSalt.php

@@ -12,7 +12,7 @@
 
 namespace PrivateBin\Persistence;
 
-use Exception;
+use PrivateBin\Data\AbstractData;
 
 /**
  * ServerSalt
@@ -26,15 +26,6 @@ use Exception;
  */
 class ServerSalt extends AbstractPersistence
 {
-    /**
-     * file where salt is saved to
-     *
-     * @access private
-     * @static
-     * @var    string
-     */
-    private static $_file = 'salt.php';
-
     /**
      * generated salt
      *
@@ -53,8 +44,7 @@ class ServerSalt extends AbstractPersistence
      */
     public static function generate()
     {
-        $randomSalt = bin2hex(random_bytes(256));
-        return $randomSalt;
+        return bin2hex(random_bytes(256));
     }
 
     /**
@@ -62,7 +52,6 @@ class ServerSalt extends AbstractPersistence
      *
      * @access public
      * @static
-     * @throws Exception
      * @return string
      */
     public static function get()
@@ -71,20 +60,14 @@ class ServerSalt extends AbstractPersistence
             return self::$_salt;
         }
 
-        if (self::_exists(self::$_file)) {
-            if (is_readable(self::getPath(self::$_file))) {
-                $items = explode('|', file_get_contents(self::getPath(self::$_file)));
-            }
-            if (!isset($items) || !is_array($items) || count($items) != 3) {
-                throw new Exception('unable to read file ' . self::getPath(self::$_file), 20);
-            }
-            self::$_salt = $items[1];
+        $salt = self::$_store->getValue('salt');
+        if ($salt) {
+            self::$_salt = $salt;
         } else {
             self::$_salt = self::generate();
-            self::_store(
-                self::$_file,
-                '<?php # |' . self::$_salt . '|'
-            );
+            if (!self::$_store->setValue(self::$_salt, 'salt')) {
+                error_log('failed to store the server salt, delete tokens, traffic limiter and user icons won\'t work');
+            }
         }
         return self::$_salt;
     }
@@ -94,11 +77,11 @@ class ServerSalt extends AbstractPersistence
      *
      * @access public
      * @static
-     * @param  string $path
+     * @param  AbstractData $store
      */
-    public static function setPath($path)
+    public static function setStore(AbstractData $store)
     {
         self::$_salt = '';
-        parent::setPath($path);
+        parent::setStore($store);
     }
 }

+ 10 - 34
lib/Persistence/TrafficLimiter.php

@@ -13,7 +13,6 @@
 
 namespace PrivateBin\Persistence;
 
-use Exception;
 use IPLib\Factory;
 use PrivateBin\Configuration;
 
@@ -85,7 +84,6 @@ class TrafficLimiter extends AbstractPersistence
     public static function setConfiguration(Configuration $conf)
     {
         self::setLimit($conf->getKey('limit', 'traffic'));
-        self::setPath($conf->getKey('dir', 'traffic'));
         self::setExemptedIp($conf->getKey('exemptedIp', 'traffic'));
 
         if (($option = $conf->getKey('header', 'traffic')) !== null) {
@@ -134,13 +132,7 @@ class TrafficLimiter extends AbstractPersistence
             return false;
         }
 
-        // Ip-lib throws an exception when something goes wrong, if so we want to catch it and set contained to false
-        try {
-            return $address->matches($range);
-        } catch (Exception $e) {
-            // If something is wrong with matching the ip, we assume it doesn't match
-            return false;
-        }
+        return $address->matches($range);
     }
 
     /**
@@ -150,7 +142,6 @@ class TrafficLimiter extends AbstractPersistence
      *
      * @access public
      * @static
-     * @throws Exception
      * @return bool
      */
     public static function canPass()
@@ -170,35 +161,20 @@ class TrafficLimiter extends AbstractPersistence
             }
         }
 
-        $file = 'traffic_limiter.php';
-        if (self::_exists($file)) {
-            require self::getPath($file);
-            $tl = $GLOBALS['traffic_limiter'];
-        } else {
-            $tl = array();
-        }
-
-        // purge file of expired hashes to keep it small
-        $now = time();
-        foreach ($tl as $key => $time) {
-            if ($time + self::$_limit < $now) {
-                unset($tl[$key]);
-            }
-        }
-
         // this hash is used as an array key, hence a shorter algo is used
         $hash = self::getHash('sha256');
-        if (array_key_exists($hash, $tl) && ($tl[$hash] + self::$_limit >= $now)) {
+        $now  = time();
+        $tl   = (int) self::$_store->getValue('traffic_limiter', $hash);
+        self::$_store->purgeValues('traffic_limiter', $now - self::$_limit);
+        if ($tl > 0 && ($tl + self::$_limit >= $now)) {
             $result = false;
         } else {
-            $tl[$hash] = time();
-            $result    = true;
+            $tl     = time();
+            $result = true;
+        }
+        if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) {
+            error_log('failed to store the traffic limiter, it probably contains outdated information');
         }
-        self::_store(
-            $file,
-            '<?php' . PHP_EOL .
-            '$GLOBALS[\'traffic_limiter\'] = ' . var_export($tl, true) . ';'
-        );
         return $result;
     }
 }

+ 6 - 9
lib/Request.php

@@ -108,7 +108,9 @@ class Request
             case 'DELETE':
             case 'PUT':
             case 'POST':
-                $this->_params = Json::decode(
+                // it might be a creation or a deletion, the latter is detected below
+                $this->_operation = 'create';
+                $this->_params    = Json::decode(
                     file_get_contents(self::$_inputStream)
                 );
                 break;
@@ -125,15 +127,10 @@ class Request
         }
 
         // prepare operation, depending on current parameters
-        if (
-            array_key_exists('ct', $this->_params) &&
-            !empty($this->_params['ct'])
-        ) {
-            $this->_operation = 'create';
-        } elseif (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
+        if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
             if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) {
                 $this->_operation = 'delete';
-            } else {
+            } elseif ($this->_operation != 'create') {
                 $this->_operation = 'read';
             }
         } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) {
@@ -172,7 +169,7 @@ class Request
             $data['meta'] = $meta;
         }
         foreach ($required_keys as $key) {
-            $data[$key] = $this->getParam($key);
+            $data[$key] = $this->getParam($key, $key == 'v' ? 1 : '');
         }
         // forcing a cast to int or float
         $data['v'] = $data['v'] + 0;

+ 591 - 1
tst/Bootstrap.php

@@ -1,5 +1,11 @@
 <?php
 
+use Google\Cloud\Core\Exception\BadRequestException;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Storage\Bucket;
+use Google\Cloud\Storage\Connection\ConnectionInterface;
+use Google\Cloud\Storage\StorageClient;
+use Google\Cloud\Storage\StorageObject;
 use PrivateBin\Persistence\ServerSalt;
 
 error_reporting(E_ALL | E_STRICT);
@@ -21,6 +27,586 @@ if (!defined('CONF_SAMPLE')) {
 require PATH . 'vendor/autoload.php';
 Helper::updateSubresourceIntegrity();
 
+/**
+ * Class StorageClientStub provides a limited stub for performing the unit test
+ */
+class StorageClientStub extends StorageClient
+{
+    private $_config     = null;
+    private $_connection = null;
+    private $_buckets    = array();
+
+    public function __construct(array $config = array())
+    {
+        $this->_config     = $config;
+        $this->_connection =  new ConnectionInterfaceStub();
+    }
+
+    public function bucket($name, $userProject = false)
+    {
+        if (!key_exists($name, $this->_buckets)) {
+            $b                     = new BucketStub($this->_connection, $name, array(), $this);
+            $this->_buckets[$name] = $b;
+        }
+        return $this->_buckets[$name];
+    }
+
+    /**
+     * @throws \Google\Cloud\Core\Exception\NotFoundException
+     */
+    public function deleteBucket($name)
+    {
+        if (key_exists($name, $this->_buckets)) {
+            unset($this->_buckets[$name]);
+        } else {
+            throw new NotFoundException();
+        }
+    }
+
+    public function buckets(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function registerStreamWrapper($protocol = null)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function unregisterStreamWrapper($protocol = null)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function signedUrlUploader($uri, $data, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getServiceAccount(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function hmacKeys(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function hmacKey($accessId, $projectId = null, array $metadata = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function createHmacKey($serviceAccountEmail, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function createBucket($name, array $options = array())
+    {
+        if (key_exists($name, $this->_buckets)) {
+            throw new BadRequestException('already exists');
+        }
+        $b                     = new BucketStub($this->_connection, $name, array(), $this);
+        $this->_buckets[$name] = $b;
+        return $b;
+    }
+}
+
+/**
+ * Class BucketStub stubs a GCS bucket.
+ */
+class BucketStub extends Bucket
+{
+    public $_objects;
+    private $_name;
+    private $_info;
+    private $_connection;
+    private $_client;
+
+    public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null)
+    {
+        $this->_name       = $name;
+        $this->_info       = $info;
+        $this->_connection = $connection;
+        $this->_objects    = array();
+        $this->_client     = $client;
+    }
+
+    public function acl()
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function defaultAcl()
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function exists()
+    {
+        return true;
+    }
+
+    public function upload($data, array $options = array())
+    {
+        if (!is_string($data) || !key_exists('name', $options)) {
+            throw new BadMethodCallException('not supported by this stub');
+        }
+
+        $name                             = $options['name'];
+        $generation                       = '1';
+        $o                                = new StorageObjectStub($this->_connection, $name, $this, $generation, $options);
+        $this->_objects[$options['name']] = $o;
+        $o->setData($data);
+    }
+
+    public function uploadAsync($data, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getResumableUploader($data, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getStreamableUploader($data, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function object($name, array $options = array())
+    {
+        if (key_exists($name, $this->_objects)) {
+            return $this->_objects[$name];
+        } else {
+            return new StorageObjectStub($this->_connection, $name, $this, null, $options);
+        }
+    }
+
+    public function objects(array $options = array())
+    {
+        $prefix = key_exists('prefix', $options) ? $options['prefix'] : '';
+
+        return new CallbackFilterIterator(
+                new ArrayIterator($this->_objects),
+                function ($current, $key, $iterator) use ($prefix) {
+                    return substr($key, 0, strlen($prefix)) == $prefix;
+                }
+        );
+    }
+
+    public function createNotification($topic, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function notification($id)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function notifications(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function delete(array $options = array())
+    {
+        $this->_client->deleteBucket($this->_name);
+    }
+
+    public function update(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function compose(array $sourceObjects, $name, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function info(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function reload(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function name()
+    {
+        return $this->_name;
+    }
+
+    public static function lifecycle(array $lifecycle = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function currentLifecycle(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function isWritable($file = null)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function iam()
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function lockRetentionPolicy(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function signedUrl($expires, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function generateSignedPostPolicyV4($objectName, $expires, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+}
+
+/**
+ * Class StorageObjectStub stubs a GCS storage object.
+ */
+class StorageObjectStub extends StorageObject
+{
+    private $_name;
+    private $_data;
+    private $_info;
+    private $_bucket;
+    private $_generation;
+    private $_exists = false;
+    private $_connection;
+
+    public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null)
+    {
+        $this->_name                            = $name;
+        $this->_bucket                          = $bucket;
+        $this->_generation                      = $generation;
+        $this->_info                            = $info;
+        $this->_connection                      = $connection;
+        $timeCreated                            = new Datetime();
+        $this->_info['metadata']['timeCreated'] = $timeCreated->format('Y-m-d\TH:i:s.u\Z');
+    }
+
+    public function acl()
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function exists(array $options = array())
+    {
+        return key_exists($this->_name, $this->_bucket->_objects);
+    }
+
+    /**
+     * @throws NotFoundException
+     */
+    public function delete(array $options = array())
+    {
+        if (key_exists($this->_name, $this->_bucket->_objects)) {
+            unset($this->_bucket->_objects[$this->_name]);
+        } else {
+            throw new NotFoundException('key ' . $this->_name . ' not found.');
+        }
+    }
+
+    /**
+     * @throws NotFoundException
+     */
+    public function update(array $metadata, array $options = array())
+    {
+        if (!$this->_exists) {
+            throw new NotFoundException('key ' . $this->_name . ' not found.');
+        }
+        $this->_info = $metadata;
+    }
+
+    public function copy($destination, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function rewrite($destination, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function rename($name, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    /**
+     * @throws NotFoundException
+     */
+    public function downloadAsString(array $options = array())
+    {
+        if (!$this->_exists) {
+            throw new NotFoundException('key ' . $this->_name . ' not found.');
+        }
+        return $this->_data;
+    }
+
+    public function downloadToFile($path, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function downloadAsStream(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function downloadAsStreamAsync(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function signedUrl($expires, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function signedUploadUrl($expires, array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function beginSignedUploadSession(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function info(array $options = array())
+    {
+        return key_exists('metadata',$this->_info) ? $this->_info['metadata'] : array();
+    }
+
+    public function reload(array $options = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function name()
+    {
+        return $this->_name;
+    }
+
+    public function identity()
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function gcsUri()
+    {
+        return sprintf(
+            'gs://%s/%s',
+            $this->_bucket->name(),
+            $this->_name
+        );
+    }
+
+    public function setData($data)
+    {
+        $this->_data   = $data;
+        $this->_exists = true;
+    }
+}
+
+/**
+ * Class ConnectionInterfaceStub required for the stubs.
+ */
+class ConnectionInterfaceStub implements ConnectionInterface
+{
+    public function deleteAcl(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getAcl(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function listAcl(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function insertAcl(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function patchAcl(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function deleteBucket(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getBucket(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function listBuckets(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function insertBucket(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getBucketIamPolicy(array $args)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function setBucketIamPolicy(array $args)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function testBucketIamPermissions(array $args)
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function patchBucket(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function deleteObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function copyObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function rewriteObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function composeObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function listObjects(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function patchObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function downloadObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function insertObject(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getNotification(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function deleteNotification(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function insertNotification(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function listNotifications(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getServiceAccount(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function lockRetentionPolicy(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function createHmacKey(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function deleteHmacKey(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function getHmacKey(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function updateHmacKey(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
+    public function listHmacKeys(array $args = array())
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+}
+
+/**
+ * Class Helper provides unit tests pastes and comments of various formats
+ */
 class Helper
 {
     /**
@@ -155,7 +741,11 @@ class Helper
     public static function getPastePost($version = 2, array $meta = array())
     {
         $example         = self::getPaste($version, $meta);
-        $example['meta'] = array('expire' => $example['meta']['expire']);
+        if ($version == 2) {
+            $example['meta'] = array('expire' => $example['meta']['expire']);
+        } else {
+            unset($example['meta']['postdate']);
+        }
         return $example;
     }
 

+ 0 - 2
tst/ConfigurationTest.php

@@ -17,8 +17,6 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase
         $this->_minimalConfig                   = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]';
         $this->_options                         = Configuration::getDefaults();
         $this->_options['model_options']['dir'] = PATH . $this->_options['model_options']['dir'];
-        $this->_options['traffic']['dir']       = PATH . $this->_options['traffic']['dir'];
-        $this->_options['purge']['dir']         = PATH . $this->_options['purge']['dir'];
         $this->_path                            = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_cfg';
         if (!is_dir($this->_path)) {
             mkdir($this->_path);

+ 0 - 4
tst/ConfigurationTestGenerator.php

@@ -428,8 +428,6 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase
         Helper::confBackup();
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
-        ServerSalt::setPath($this->_path);
-        TrafficLimiter::setPath($this->_path);
         $this->reset();
     }
 
@@ -449,8 +447,6 @@ class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase
         if ($this->_model->exists(Helper::getPasteId()))
             $this->_model->delete(Helper::getPasteId());
         $configuration['model_options']['dir'] = $this->_path;
-        $configuration['traffic']['dir']       = $this->_path;
-        $configuration['purge']['dir']         = $this->_path;
         Helper::createIniFile(CONF, $configuration);
     }
 

+ 28 - 26
tst/ControllerTest.php

@@ -17,6 +17,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase
         /* Setup Routine */
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_data  = Filesystem::getInstance(array('dir' => $this->_path));
+        ServerSalt::setStore($this->_data);
+        TrafficLimiter::setStore($this->_data);
         $this->reset();
     }
 
@@ -37,11 +39,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase
             $this->_data->delete(Helper::getPasteId());
         }
         $options                         = parse_ini_file(CONF_SAMPLE, true);
-        $options['purge']['dir']         = $this->_path;
-        $options['traffic']['dir']       = $this->_path;
         $options['model_options']['dir'] = $this->_path;
         Helper::createIniFile(CONF, $options);
-        ServerSalt::setPath($this->_path);
     }
 
     /**
@@ -49,6 +48,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase
      */
     public function testView()
     {
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
+        $_GET[Helper::getPasteId()]       = '';
         ob_start();
         new Controller;
         $content = ob_get_contents();
@@ -127,28 +128,6 @@ class ControllerTest extends PHPUnit_Framework_TestCase
         );
     }
 
-    /**
-     * @runInSeparateProcess
-     */
-    public function testHtaccess()
-    {
-        $htaccess = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
-        @unlink($htaccess);
-
-        $paste = Helper::getPasteJson();
-        $file  = tempnam(sys_get_temp_dir(), 'FOO');
-        file_put_contents($file, $paste);
-        Request::setInputStream($file);
-        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        ob_start();
-        new Controller;
-        ob_end_clean();
-
-        $this->assertFileExists($htaccess, 'htaccess recreated');
-    }
-
     /**
      * @expectedException Exception
      * @expectedExceptionCode 2
@@ -493,6 +472,29 @@ class ControllerTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data');
     }
 
+    /**
+     * @runInSeparateProcess
+     */
+    public function testCreateInvalidFormat()
+    {
+        $options                     = parse_ini_file(CONF, true);
+        $options['traffic']['limit'] = 0;
+        Helper::createIniFile(CONF, $options);
+        $file = tempnam(sys_get_temp_dir(), 'FOO');
+        file_put_contents($file, Helper::getPasteJson(1));
+        Request::setInputStream($file);
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
+        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REMOTE_ADDR']           = '::1';
+        ob_start();
+        new Controller;
+        $content = ob_get_contents();
+        ob_end_clean();
+        $response = json_decode($content, true);
+        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data');
+    }
+
     /**
      * @runInSeparateProcess
      */
@@ -541,7 +543,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase
         ob_end_clean();
         $response = json_decode($content, true);
         $this->assertEquals(1, $response['status'], 'outputs error status');
-        $this->assertFalse($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'paste exists after posting data');
+        $this->assertFalse($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after posting data');
     }
 
     /**

+ 4 - 0
tst/ControllerWithDbTest.php

@@ -1,6 +1,8 @@
 <?php
 
 use PrivateBin\Data\Database;
+use PrivateBin\Persistence\ServerSalt;
+use PrivateBin\Persistence\TrafficLimiter;
 
 require_once 'ControllerTest.php';
 
@@ -24,6 +26,8 @@ class ControllerWithDbTest extends ControllerTest
         }
         $this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3';
         $this->_data           = Database::getInstance($this->_options);
+        ServerSalt::setStore($this->_data);
+        TrafficLimiter::setStore($this->_data);
         $this->reset();
     }
 

+ 59 - 0
tst/ControllerWithGcsTest.php

@@ -0,0 +1,59 @@
+<?php
+
+use Google\Auth\HttpHandler\HttpHandlerFactory;
+use GuzzleHttp\Client;
+use PrivateBin\Data\GoogleCloudStorage;
+use PrivateBin\Persistence\ServerSalt;
+use PrivateBin\Persistence\TrafficLimiter;
+
+require_once 'ControllerTest.php';
+
+class ControllerWithGcsTest extends ControllerTest
+{
+    private static $_client;
+    private static $_bucket;
+    private $_options = array();
+
+    public static function setUpBeforeClass()
+    {
+        $httpClient = new Client(array('debug'=>false));
+        $handler    = HttpHandlerFactory::build($httpClient);
+
+        $name     = 'pb-';
+        $alphabet = 'abcdefghijklmnopqrstuvwxyz';
+        for ($i = 0; $i < 29; ++$i) {
+            $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
+        }
+        self::$_client = new StorageClientStub(array());
+        self::$_bucket = self::$_client->createBucket($name);
+    }
+
+    public function setUp()
+    {
+        /* Setup Routine */
+        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        if (!is_dir($this->_path)) {
+            mkdir($this->_path);
+        }
+        $this->_options = array(
+            'bucket' => self::$_bucket->name(),
+            'prefix' => 'pastes',
+        );
+        $this->_data    = GoogleCloudStorage::getInstance($this->_options);
+        ServerSalt::setStore($this->_data);
+        TrafficLimiter::setStore($this->_data);
+        $this->reset();
+    }
+
+    public function reset()
+    {
+        parent::reset();
+        // but then inject a db config
+        $options          = parse_ini_file(CONF, true);
+        $options['model'] = array(
+            'class' => 'GoogleCloudStorage',
+        );
+        $options['model_options'] = $this->_options;
+        Helper::createIniFile(CONF, $options);
+    }
+}

+ 57 - 0
tst/Data/DatabaseTest.php

@@ -2,6 +2,8 @@
 
 use PrivateBin\Controller;
 use PrivateBin\Data\Database;
+use PrivateBin\Data\Filesystem;
+use PrivateBin\Persistence\ServerSalt;
 
 class DatabaseTest extends PHPUnit_Framework_TestCase
 {
@@ -31,6 +33,19 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
         }
     }
 
+    public function testSaltMigration()
+    {
+        ServerSalt::setStore(Filesystem::getInstance(array('dir' => 'data')));
+        $salt = ServerSalt::get();
+        $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
+        $this->assertFileExists($file, 'ServerSalt got initialized and stored on disk');
+        $this->assertNotEquals($salt, '');
+        ServerSalt::setStore($this->_model);
+        ServerSalt::get();
+        $this->assertFileNotExists($file, 'legacy ServerSalt got removed');
+        $this->assertEquals($salt, ServerSalt::get(), 'ServerSalt got preserved & migrated');
+    }
+
     public function testDatabaseBasedDataStoreWorks()
     {
         $this->_model->delete(Helper::getPasteId());
@@ -287,6 +302,48 @@ class DatabaseTest extends PHPUnit_Framework_TestCase
         Helper::rmDir($this->_path);
     }
 
+    public function testCorruptMeta()
+    {
+        mkdir($this->_path);
+        $path = $this->_path . DIRECTORY_SEPARATOR . 'meta-test.sq3';
+        if (is_file($path)) {
+            unlink($path);
+        }
+        $this->_options['dsn'] = 'sqlite:' . $path;
+        $this->_options['tbl'] = 'baz_';
+        $model                 = Database::getInstance($this->_options);
+        $paste                 = Helper::getPaste(1, array('expire_date' => 1344803344));
+        unset($paste['meta']['formatter'], $paste['meta']['opendiscussion'], $paste['meta']['salt']);
+        $model->delete(Helper::getPasteId());
+
+        $db = new PDO(
+            $this->_options['dsn'],
+            $this->_options['usr'],
+            $this->_options['pwd'],
+            $this->_options['opt']
+        );
+        $statement = $db->prepare('INSERT INTO baz_paste VALUES(?,?,?,?,?,?,?,?,?)');
+        $statement->execute(
+            array(
+                Helper::getPasteId(),
+                $paste['data'],
+                $paste['meta']['postdate'],
+                $paste['meta']['expire_date'],
+                0,
+                0,
+                '{',
+                null,
+                null,
+            )
+        );
+        $statement->closeCursor();
+
+        $this->assertTrue($model->exists(Helper::getPasteId()), 'paste exists after storing it');
+        $this->assertEquals($paste, $model->read(Helper::getPasteId()));
+
+        Helper::rmDir($this->_path);
+    }
+
     public function testTableUpgrade()
     {
         mkdir($this->_path);

+ 1 - 0
tst/Data/FilesystemTest.php

@@ -117,6 +117,7 @@ class FilesystemTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste');
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist');
+        $this->assertFalse($this->_model->setValue('foo', 'non existing namespace'), 'rejects setting value in non existing namespace');
     }
 
     public function testCommentErrorDetection()

+ 41 - 575
tst/Data/GoogleCloudStorageTest.php

@@ -1,12 +1,6 @@
 <?php
 
 use Google\Auth\HttpHandler\HttpHandlerFactory;
-use Google\Cloud\Core\Exception\BadRequestException;
-use Google\Cloud\Core\Exception\NotFoundException;
-use Google\Cloud\Storage\Bucket;
-use Google\Cloud\Storage\Connection\ConnectionInterface;
-use Google\Cloud\Storage\StorageClient;
-use Google\Cloud\Storage\StorageObject;
 use GuzzleHttp\Client;
 use PrivateBin\Data\GoogleCloudStorage;
 
@@ -31,14 +25,11 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase
 
     public function setUp()
     {
-        // do not report E_NOTICE as fsouza/fake-gcs-server does not return a `generation` value in the response
-        // which the Google Cloud Storage PHP library expects.
-        error_reporting(E_ERROR | E_WARNING | E_PARSE);
         ini_set('error_log', stream_get_meta_data(tmpfile())['uri']);
         $this->_model = GoogleCloudStorage::getInstance(array(
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
-            'client' => self::$_client, ));
+        ));
     }
 
     public function tearDown()
@@ -46,7 +37,6 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase
         foreach (self::$_bucket->objects() as $object) {
             $object->delete();
         }
-        error_reporting(E_ALL);
     }
 
     public static function tearDownAfterClass()
@@ -138,579 +128,55 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist');
     }
-}
-
-/**
- * Class StorageClientStub provides a limited stub for performing the unit test
- */
-class StorageClientStub extends StorageClient
-{
-    private $_config     = null;
-    private $_connection = null;
-    private $_buckets    = array();
-
-    public function __construct(array $config = array())
-    {
-        $this->_config     = $config;
-        $this->_connection =  new ConnectionInterfaceStub();
-    }
-
-    public function bucket($name, $userProject = false)
-    {
-        if (!key_exists($name, $this->_buckets)) {
-            $b                     = new BucketStub($this->_connection, $name, array(), $this);
-            $this->_buckets[$name] = $b;
-        }
-        return $this->_buckets[$name];
-    }
 
     /**
-     * @throws \Google\Cloud\Core\Exception\NotFoundException
+     * @throws Exception
      */
-    public function deleteBucket($name)
-    {
-        if (key_exists($name, $this->_buckets)) {
-            unset($this->_buckets[$name]);
-        } else {
-            throw new NotFoundException();
-        }
-    }
-
-    public function buckets(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function registerStreamWrapper($protocol = null)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function unregisterStreamWrapper($protocol = null)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function signedUrlUploader($uri, $data, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getServiceAccount(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function hmacKeys(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function hmacKey($accessId, $projectId = null, array $metadata = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function createHmacKey($serviceAccountEmail, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function createBucket($name, array $options = array())
-    {
-        if (key_exists($name, $this->_buckets)) {
-            throw new BadRequestException('already exists');
-        }
-        $b                     = new BucketStub($this->_connection, $name, array(), $this);
-        $this->_buckets[$name] = $b;
-        return $b;
-    }
-}
-
-/**
- * Class BucketStub stubs a GCS bucket.
- */
-class BucketStub extends Bucket
-{
-    public $_objects;
-    private $_name;
-    private $_info;
-    private $_connection;
-    private $_client;
-
-    public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null)
-    {
-        $this->_name       = $name;
-        $this->_info       = $info;
-        $this->_connection = $connection;
-        $this->_objects    = array();
-        $this->_client     = $client;
-    }
-
-    public function acl()
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function defaultAcl()
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function exists()
-    {
-        return true;
-    }
-
-    public function upload($data, array $options = array())
-    {
-        if (!is_string($data) || !key_exists('name', $options)) {
-            throw new BadMethodCallException('not supported by this stub');
-        }
-
-        $name                             = $options['name'];
-        $generation                       = '1';
-        $o                                = new StorageObjectStub($this->_connection, $name, $this, $generation, $options);
-        $this->_objects[$options['name']] = $o;
-        $o->setData($data);
-    }
-
-    public function uploadAsync($data, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getResumableUploader($data, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getStreamableUploader($data, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function object($name, array $options = array())
-    {
-        if (key_exists($name, $this->_objects)) {
-            return $this->_objects[$name];
-        } else {
-            return new StorageObjectStub($this->_connection, $name, $this, null, $options);
-        }
-    }
-
-    public function objects(array $options = array())
-    {
-        $prefix = key_exists('prefix', $options) ? $options['prefix'] : '';
-
-        return new CallbackFilterIterator(
-                new ArrayIterator($this->_objects),
-                function ($current, $key, $iterator) use ($prefix) {
-                    return substr($key, 0, strlen($prefix)) == $prefix;
-                }
-        );
-    }
-
-    public function createNotification($topic, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function notification($id)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function notifications(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function delete(array $options = array())
-    {
-        $this->_client->deleteBucket($this->_name);
-    }
-
-    public function update(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function compose(array $sourceObjects, $name, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function info(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function reload(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function name()
-    {
-        return $this->_name;
-    }
-
-    public static function lifecycle(array $lifecycle = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function currentLifecycle(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function isWritable($file = null)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function iam()
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function lockRetentionPolicy(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function signedUrl($expires, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function generateSignedPostPolicyV4($objectName, $expires, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-}
-
-/**
- * Class StorageObjectStub stubs a GCS storage object.
- */
-class StorageObjectStub extends StorageObject
-{
-    private $_name;
-    private $_data;
-    private $_info;
-    private $_bucket;
-    private $_generation;
-    private $_exists = false;
-    private $_connection;
-
-    public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null)
-    {
-        $this->_name       = $name;
-        $this->_bucket     = $bucket;
-        $this->_generation = $generation;
-        $this->_info       = $info;
-        $this->_connection = $connection;
-    }
-
-    public function acl()
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function exists(array $options = array())
-    {
-        return key_exists($this->_name, $this->_bucket->_objects);
+    public function testKeyValueStore()
+    {
+        $salt = bin2hex(random_bytes(256));
+        $this->_model->setValue($salt, 'salt', '');
+        $storedSalt = $this->_model->getValue('salt', '');
+        $this->assertEquals($salt, $storedSalt);
+        $this->_model->purgeValues('salt', time() + 60);
+        $this->assertEquals('', $this->_model->getValue('salt', 'master'));
+
+        $client = hash_hmac('sha512', '127.0.0.1', $salt);
+        $expire = time();
+        $this->_model->setValue(strval($expire), 'traffic_limiter', $client);
+        $storedExpired = $this->_model->getValue('traffic_limiter', $client);
+        $this->assertEquals(strval($expire), $storedExpired);
+
+        $this->_model->purgeValues('traffic_limiter', time() - 60);
+        $this->assertEquals($storedExpired, $this->_model->getValue('traffic_limiter', $client));
+        $this->_model->purgeValues('traffic_limiter', time() + 60);
+        $this->assertEquals('', $this->_model->getValue('traffic_limiter', $client));
+
+        $purgeAt = $expire + (15 * 60);
+        $this->_model->setValue(strval($purgeAt), 'purge_limiter', '');
+        $storedPurgedAt = $this->_model->getValue('purge_limiter', '');
+        $this->assertEquals(strval($purgeAt), $storedPurgedAt);
+        $this->_model->purgeValues('purge_limiter', $purgeAt + 60);
+        $this->assertEquals('', $this->_model->getValue('purge_limiter', ''));
+        $this->assertEquals('', $this->_model->getValue('purge_limiter', 'at'));
     }
 
     /**
-     * @throws NotFoundException
+     * @throws Exception
      */
-    public function delete(array $options = array())
+    public function testKeyValuePurgeTrafficLimiter()
     {
-        if (key_exists($this->_name, $this->_bucket->_objects)) {
-            unset($this->_bucket->_objects[$this->_name]);
-        } else {
-            throw new NotFoundException('key ' . $this->_name . ' not found.');
-        }
-    }
+        $salt   = bin2hex(random_bytes(256));
+        $client = hash_hmac('sha512', '127.0.0.1', $salt);
+        $expire = time();
+        $this->_model->setValue(strval($expire), 'traffic_limiter', $client);
+        $storedExpired = $this->_model->getValue('traffic_limiter', $client);
+        $this->assertEquals(strval($expire), $storedExpired);
 
-    /**
-     * @throws NotFoundException
-     */
-    public function update(array $metadata, array $options = array())
-    {
-        if (!$this->_exists) {
-            throw new NotFoundException('key ' . $this->_name . ' not found.');
-        }
-        $this->_info = $metadata;
-    }
+        $this->_model->purgeValues('traffic_limiter', time() - 60);
+        $this->assertEquals($storedExpired, $this->_model->getValue('traffic_limiter', $client));
 
-    public function copy($destination, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function rewrite($destination, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function rename($name, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    /**
-     * @throws NotFoundException
-     */
-    public function downloadAsString(array $options = array())
-    {
-        if (!$this->_exists) {
-            throw new NotFoundException('key ' . $this->_name . ' not found.');
-        }
-        return $this->_data;
-    }
-
-    public function downloadToFile($path, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function downloadAsStream(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function downloadAsStreamAsync(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function signedUrl($expires, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function signedUploadUrl($expires, array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function beginSignedUploadSession(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function info(array $options = array())
-    {
-        return key_exists('metadata',$this->_info) ? $this->_info['metadata'] : array();
-    }
-
-    public function reload(array $options = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function name()
-    {
-        return $this->_name;
-    }
-
-    public function identity()
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function gcsUri()
-    {
-        return sprintf(
-            'gs://%s/%s',
-            $this->_bucket->name(),
-            $this->_name
-        );
-    }
-
-    public function setData($data)
-    {
-        $this->_data   = $data;
-        $this->_exists = true;
-    }
-}
-
-/**
- * Class ConnectionInterfaceStub required for the stubs.
- */
-class ConnectionInterfaceStub implements ConnectionInterface
-{
-    public function deleteAcl(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getAcl(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function listAcl(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function insertAcl(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function patchAcl(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function deleteBucket(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getBucket(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function listBuckets(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function insertBucket(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getBucketIamPolicy(array $args)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function setBucketIamPolicy(array $args)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function testBucketIamPermissions(array $args)
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function patchBucket(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function deleteObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function copyObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function rewriteObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function composeObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function listObjects(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function patchObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function downloadObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function insertObject(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getNotification(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function deleteNotification(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function insertNotification(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function listNotifications(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getServiceAccount(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function lockRetentionPolicy(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function createHmacKey(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function deleteHmacKey(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function getHmacKey(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function updateHmacKey(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
-    }
-
-    public function listHmacKeys(array $args = array())
-    {
-        throw new BadMethodCallException('not supported by this stub');
+        $this->_model->purgeValues('traffic_limiter', time() + 60);
+        $this->assertEquals('', $this->_model->getValue('traffic_limiter', $client));
     }
 }

+ 1 - 3
tst/JsonApiTest.php

@@ -16,7 +16,7 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
         /* Setup Routine */
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
-        ServerSalt::setPath($this->_path);
+        ServerSalt::setStore($this->_model);
 
         $_POST   = array();
         $_GET    = array();
@@ -25,8 +25,6 @@ class JsonApiTest extends PHPUnit_Framework_TestCase
             $this->_model->delete(Helper::getPasteId());
         }
         $options                         = parse_ini_file(CONF_SAMPLE, true);
-        $options['purge']['dir']         = $this->_path;
-        $options['traffic']['dir']       = $this->_path;
         $options['model_options']['dir'] = $this->_path;
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);

+ 156 - 1
tst/ModelTest.php

@@ -25,7 +25,6 @@ class ModelTest extends PHPUnit_Framework_TestCase
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        ServerSalt::setPath($this->_path);
         $options                   = parse_ini_file(CONF_SAMPLE, true);
         $options['purge']['limit'] = 0;
         $options['model']          = array(
@@ -39,6 +38,7 @@ class ModelTest extends PHPUnit_Framework_TestCase
         );
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
+        ServerSalt::setStore(Database::getInstance($options['model_options']));
         $this->_conf            = new Configuration;
         $this->_model           = new Model($this->_conf);
         $_SERVER['REMOTE_ADDR'] = '::1';
@@ -102,6 +102,58 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(array(), $paste->getComments(), 'comment was deleted with paste');
     }
 
+    public function testPasteV1()
+    {
+        $pasteData = Helper::getPaste(1);
+        unset($pasteData['meta']['formatter']);
+
+        $path = $this->_path . DIRECTORY_SEPARATOR . 'v1-test.sq3';
+        if (is_file($path)) {
+            unlink($path);
+        }
+        $options                   = parse_ini_file(CONF_SAMPLE, true);
+        $options['purge']['limit'] = 0;
+        $options['model']          = array(
+            'class' => 'Database',
+        );
+        $options['model_options'] = array(
+            'dsn' => 'sqlite:' . $path,
+            'usr' => null,
+            'pwd' => null,
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+        );
+        Helper::createIniFile(CONF, $options);
+        $model = new Model(new Configuration);
+        $model->getPaste('0000000000000000')->exists(); // triggers database table creation
+        $model->getPaste(Helper::getPasteId())->delete(); // deletes the cache
+
+        $db = new PDO(
+            $options['model_options']['dsn'],
+            $options['model_options']['usr'],
+            $options['model_options']['pwd'],
+            $options['model_options']['opt']
+        );
+        $statement = $db->prepare('INSERT INTO paste VALUES(?,?,?,?,?,?,?,?,?)');
+        $statement->execute(
+            array(
+                Helper::getPasteId(),
+                $pasteData['data'],
+                $pasteData['meta']['postdate'],
+                0,
+                0,
+                0,
+                json_encode($pasteData['meta']),
+                null,
+                null,
+            )
+        );
+        $statement->closeCursor();
+
+        $paste = $model->getPaste(Helper::getPasteId());
+        $this->assertNotEmpty($paste->getDeleteToken(), 'excercise the condition to load the data from storage');
+        $this->assertEquals('plaintext', $paste->get()['meta']['formatter'], 'paste got created with default formatter');
+    }
+
     public function testCommentDefaults()
     {
         $comment = new Comment(
@@ -133,6 +185,97 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $paste->store();
     }
 
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 76
+     */
+    public function testStoreFail()
+    {
+        $path = $this->_path . DIRECTORY_SEPARATOR . 'model-store-test.sq3';
+        if (is_file($path)) {
+            unlink($path);
+        }
+        $options                   = parse_ini_file(CONF_SAMPLE, true);
+        $options['purge']['limit'] = 0;
+        $options['model']          = array(
+            'class' => 'Database',
+        );
+        $options['model_options'] = array(
+            'dsn' => 'sqlite:' . $path,
+            'usr' => null,
+            'pwd' => null,
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+        );
+        Helper::createIniFile(CONF, $options);
+        $model = new Model(new Configuration);
+
+        $pasteData   = Helper::getPastePost();
+        $model->getPaste(Helper::getPasteId())->delete();
+        $model->getPaste(Helper::getPasteId())->exists();
+
+        $db = new PDO(
+            $options['model_options']['dsn'],
+            $options['model_options']['usr'],
+            $options['model_options']['pwd'],
+            $options['model_options']['opt']
+        );
+        $statement = $db->prepare('DROP TABLE paste');
+        $statement->execute();
+        $statement->closeCursor();
+
+        $paste = $model->getPaste();
+        $paste->setData($pasteData);
+        $paste->store();
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 70
+     */
+    public function testCommentStoreFail()
+    {
+        $path = $this->_path . DIRECTORY_SEPARATOR . 'model-test.sq3';
+        if (is_file($path)) {
+            unlink($path);
+        }
+        $options                   = parse_ini_file(CONF_SAMPLE, true);
+        $options['purge']['limit'] = 0;
+        $options['model']          = array(
+            'class' => 'Database',
+        );
+        $options['model_options'] = array(
+            'dsn' => 'sqlite:' . $path,
+            'usr' => null,
+            'pwd' => null,
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+        );
+        Helper::createIniFile(CONF, $options);
+        $model = new Model(new Configuration);
+
+        $pasteData   = Helper::getPastePost();
+        $commentData = Helper::getCommentPost();
+        $model->getPaste(Helper::getPasteId())->delete();
+
+        $paste = $model->getPaste();
+        $paste->setData($pasteData);
+        $paste->store();
+        $paste->exists();
+
+        $db = new PDO(
+            $options['model_options']['dsn'],
+            $options['model_options']['usr'],
+            $options['model_options']['pwd'],
+            $options['model_options']['opt']
+        );
+        $statement = $db->prepare('DROP TABLE comment');
+        $statement->execute();
+        $statement->closeCursor();
+
+        $comment = $paste->getComment(Helper::getPasteId());
+        $comment->setData($commentData);
+        $comment->store();
+    }
+
     /**
      * @expectedException Exception
      * @expectedExceptionCode 69
@@ -195,6 +338,18 @@ class ModelTest extends PHPUnit_Framework_TestCase
         $paste->get();
     }
 
+    /**
+     * @expectedException Exception
+     * @expectedExceptionCode 75
+     */
+    public function testInvalidPasteFormat()
+    {
+        $pasteData             = Helper::getPastePost();
+        $pasteData['adata'][1] = 'format does not exist';
+        $paste                 = $this->_model->getPaste();
+        $paste->setData($pasteData);
+    }
+
     /**
      * @expectedException Exception
      * @expectedExceptionCode 60

+ 4 - 1
tst/Persistence/PurgeLimiterTest.php

@@ -1,5 +1,6 @@
 <?php
 
+use PrivateBin\Data\Filesystem;
 use PrivateBin\Persistence\PurgeLimiter;
 
 class PurgeLimiterTest extends PHPUnit_Framework_TestCase
@@ -13,7 +14,9 @@ class PurgeLimiterTest extends PHPUnit_Framework_TestCase
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        PurgeLimiter::setPath($this->_path);
+        PurgeLimiter::setStore(
+            Filesystem::getInstance(array('dir' => $this->_path))
+        );
     }
 
     public function tearDown()

+ 36 - 28
tst/Persistence/ServerSaltTest.php

@@ -1,5 +1,6 @@
 <?php
 
+use PrivateBin\Data\Filesystem;
 use PrivateBin\Persistence\ServerSalt;
 
 class ServerSaltTest extends PHPUnit_Framework_TestCase
@@ -19,7 +20,9 @@ class ServerSaltTest extends PHPUnit_Framework_TestCase
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        ServerSalt::setPath($this->_path);
+        ServerSalt::setStore(
+            Filesystem::getInstance(array('dir' => $this->_path))
+        );
 
         $this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo';
 
@@ -40,46 +43,46 @@ class ServerSaltTest extends PHPUnit_Framework_TestCase
     public function testGeneration()
     {
         // generating new salt
-        ServerSalt::setPath($this->_path);
+        ServerSalt::setStore(
+            Filesystem::getInstance(array('dir' => $this->_path))
+        );
         $salt = ServerSalt::get();
 
         // try setting a different path and resetting it
-        ServerSalt::setPath($this->_otherPath);
+        ServerSalt::setStore(
+            Filesystem::getInstance(array('dir' => $this->_otherPath))
+        );
         $this->assertNotEquals($salt, ServerSalt::get());
-        ServerSalt::setPath($this->_path);
+        ServerSalt::setStore(
+            Filesystem::getInstance(array('dir' => $this->_path))
+        );
         $this->assertEquals($salt, ServerSalt::get());
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 11
-     */
     public function testPathShenanigans()
     {
         // try setting an invalid path
         chmod($this->_invalidPath, 0000);
-        ServerSalt::setPath($this->_invalidPath);
-        ServerSalt::get();
+        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        ServerSalt::setStore($store);
+        $salt = ServerSalt::get();
+        ServerSalt::setStore($store);
+        $this->assertNotEquals($salt, ServerSalt::get());
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 20
-     */
     public function testFileRead()
     {
         // try setting an invalid file
         chmod($this->_invalidPath, 0700);
         file_put_contents($this->_invalidFile, '');
         chmod($this->_invalidFile, 0000);
-        ServerSalt::setPath($this->_invalidPath);
-        ServerSalt::get();
+        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        ServerSalt::setStore($store);
+        $salt = ServerSalt::get();
+        ServerSalt::setStore($store);
+        $this->assertNotEquals($salt, ServerSalt::get());
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 13
-     */
     public function testFileWrite()
     {
         // try setting an invalid file
@@ -90,19 +93,24 @@ class ServerSaltTest extends PHPUnit_Framework_TestCase
         }
         file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . '.htaccess', '');
         chmod($this->_invalidPath, 0500);
-        ServerSalt::setPath($this->_invalidPath);
-        ServerSalt::get();
+        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        ServerSalt::setStore($store);
+        $salt = ServerSalt::get();
+        ServerSalt::setStore($store);
+        $this->assertNotEquals($salt, ServerSalt::get());
     }
 
-    /**
-     * @expectedException Exception
-     * @expectedExceptionCode 10
-     */
     public function testPermissionShenanigans()
     {
         // try creating an invalid path
         chmod($this->_invalidPath, 0000);
-        ServerSalt::setPath($this->_invalidPath . DIRECTORY_SEPARATOR . 'baz');
-        ServerSalt::get();
+        ServerSalt::setStore(
+            Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
+        );
+        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        ServerSalt::setStore($store);
+        $salt = ServerSalt::get();
+        ServerSalt::setStore($store);
+        $this->assertNotEquals($salt, ServerSalt::get());
     }
 }

+ 14 - 4
tst/Persistence/TrafficLimiterTest.php

@@ -1,5 +1,7 @@
 <?php
 
+use PrivateBin\Data\Filesystem;
+use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Persistence\TrafficLimiter;
 
 class TrafficLimiterTest extends PHPUnit_Framework_TestCase
@@ -10,7 +12,9 @@ class TrafficLimiterTest extends PHPUnit_Framework_TestCase
     {
         /* Setup Routine */
         $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit';
-        TrafficLimiter::setPath($this->_path);
+        $store       = Filesystem::getInstance(array('dir' => $this->_path));
+        ServerSalt::setStore($store);
+        TrafficLimiter::setStore($store);
     }
 
     public function tearDown()
@@ -19,11 +23,17 @@ class TrafficLimiterTest extends PHPUnit_Framework_TestCase
         Helper::rmDir($this->_path . DIRECTORY_SEPARATOR);
     }
 
+    public function testHtaccess()
+    {
+        $htaccess = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
+        @unlink($htaccess);
+        $_SERVER['REMOTE_ADDR'] = 'foobar';
+        TrafficLimiter::canPass();
+        $this->assertFileExists($htaccess, 'htaccess recreated');
+    }
+
     public function testTrafficGetsLimited()
     {
-        $this->assertEquals($this->_path, TrafficLimiter::getPath());
-        $file = 'baz';
-        $this->assertEquals($this->_path . DIRECTORY_SEPARATOR . $file, TrafficLimiter::getPath($file));
         TrafficLimiter::setLimit(4);
         $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
         $this->assertTrue(TrafficLimiter::canPass(), 'first request may pass');

+ 2 - 1
tst/Vizhash16x16Test.php

@@ -1,5 +1,6 @@
 <?php
 
+use PrivateBin\Data\Filesystem;
 use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Vizhash16x16;
 
@@ -17,7 +18,7 @@ class Vizhash16x16Test extends PHPUnit_Framework_TestCase
             mkdir($this->_path);
         }
         $this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png';
-        ServerSalt::setPath($this->_path);
+        ServerSalt::setStore(Filesystem::getInstance(array('dir' => $this->_path)));
     }
 
     public function tearDown()