Explorar o código

Merge branch 'master' into php8

El RIDO %!s(int64=3) %!d(string=hai) anos
pai
achega
46c0fc851c

+ 2 - 0
.gitattributes

@@ -1,3 +1,5 @@
+bin/configuration-test-generator export-ignore
+bin/icon-test export-ignore
 doc/ export-ignore
 tst/ export-ignore
 img/browserstack.svg export-ignore

+ 5 - 4
CHANGELOG.md

@@ -1,11 +1,12 @@
 # PrivateBin version history
 
-  * **1.4.1 (not yet released)**
-    * ADDED: Translations for Turkish, Slovak and Greek
+  * **1.5 (not yet released)**
+    * ADDED: script for data storage backend migrations (#1012)
+    * ADDED: Translations for Turkish, Slovak, Greek and Thai
     * ADDED: S3 Storage backend (#994)
-    * CHANGED: Switched to Jdenticons as the default for comment icons (#793)
+    * ADDED: Jdenticons as an option for comment icons (#793)
     * CHANGED: Avoid `SUPER` privilege for setting the `sql_mode` for MariaDB/MySQL (#919)
-    * CHANGED: Upgrading libraries to: zlib 1.2.13
+    * CHANGED: Upgrading libraries to: DOMpurify 2.4.6, jQuery 3.6.1, Showdown 2.1.0 & zlib 1.2.13
     * FIXED: Revert to CREATE INDEX without IF NOT EXISTS clauses, to support MySQL (#943)
     * FIXED: Apply table prefix to indexes as well, to support multiple instances sharing a single database (#943)
     * FIXED: YOURLS integration via new proxy, storing signature in configuration (#725)

+ 2 - 0
CREDITS.md

@@ -31,6 +31,7 @@
 * Austin Huang - Oracle database support
 * Felix J. Ogris - S3 Storage backend
 * Mounir Idrassi & J. Mozdzen - secure YOURLS integration
+* Felix J. Ogris - script for data backend migrations, dropped singleton behaviour of data backends
 
 ## Translations
 * Hexalyse - French
@@ -61,3 +62,4 @@
 * Emir Ensar Rahmanlar - Turkish
 * Stevo984 - Slovak
 * Christos Karamolegkos - Greek
+* jaideejung007 - Thai

+ 5 - 5
INSTALL.md

@@ -26,7 +26,7 @@ install and configure PrivateBin on your server. It's available on
     - `open_basedir` access to `/dev/urandom`
     - mcrypt extension AND `open_basedir` access to `/dev/urandom`
     - com_dotnet extension
-- GD extension
+- GD extension (when using identicon or vizhash icons, jdenticon works without it)
 - zlib extension
 - some disk space or a database supported by [PDO](https://php.net/manual/book.pdo.php)
 - ability to create files and folders in the installation directory and the PATH
@@ -38,10 +38,10 @@ install and configure PrivateBin on your server. It's available on
 ### Changing the Path
 
 In the index.php you can define a different `PATH`. This is useful to secure
-your installation. You can move the configuration, data files, templates and PHP
-libraries (directories cfg, doc, data, lib, tpl, tst and vendor) outside of your
-document root. This new location must still be accessible to your webserver and
-PHP process (see also
+your installation. You can move the utilities, configuration, data files,
+templates and PHP libraries (directories bin, cfg, doc, data, lib, tpl, tst and
+vendor) outside of your document root. This new location must still be
+accessible to your webserver and PHP process (see also
 [open_basedir setting](https://secure.php.net/manual/en/ini.core.php#ini.open-basedir)).
 
 > #### PATH Example

+ 7 - 3
tst/ConfigurationTestGenerator.php → bin/configuration-test-generator

@@ -9,7 +9,9 @@
  * DANGER: Too many options/settings and too high max iteration setting may trigger
  *         a fork bomb. Please save your work before executing this script.
  */
-include 'Bootstrap.php';
+
+define('PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
+include PATH . 'tst' . DIRECTORY_SEPARATOR . 'Bootstrap.php';
 
 $vd  = array('view', 'delete');
 $vcd = array('view', 'create', 'delete');
@@ -392,7 +394,7 @@ class ConfigurationTestGenerator
             }
         }
         $code .= '}' . PHP_EOL;
-        file_put_contents('ConfigurationCombinationsTest.php', $code);
+        file_put_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'tst' . DIRECTORY_SEPARATOR . 'ConfigurationCombinationsTest.php', $code);
     }
 
     /**
@@ -428,7 +430,9 @@ class ConfigurationCombinationsTest extends TestCase
         /* Setup Routine */
         Helper::confBackup();
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+        $this->_model = new Filesystem(array('dir' => $this->_path));
+        ServerSalt::setStore($this->_model);
+        TrafficLimiter::setStore($this->_model);
         $this->reset();
     }
 

+ 61 - 11
tst/IconTest → bin/icon-test

@@ -9,6 +9,7 @@ use Identicon\Generator\GdGenerator;
 use Identicon\Generator\ImageMagickGenerator;
 use Identicon\Generator\SvgGenerator;
 use Identicon\Identicon;
+use Jdenticon\Identicon as Jdenticon;
 use PrivateBin\Vizhash16x16;
 
 
@@ -17,7 +18,19 @@ $vizhash = new Vizhash16x16();
 $identiconGenerators = array(
     'identicon GD' => new Identicon(new GdGenerator()),
     'identicon ImageMagick' => new Identicon(new ImageMagickGenerator()),
-    'identicon SVG' => new Identicon(new SvgGenerator())
+    'identicon SVG' => new Identicon(new SvgGenerator()),
+);
+$jdenticon = new Jdenticon(array(
+    'size'  => 16,
+    'style' => array(
+        'backgroundColor'   => '#fff0', // fully transparent, for dark mode
+        'padding'           => 0,
+    ),
+));
+$jdenticonGenerators = array(
+    'jdenticon' => 'png',
+    'jdenticon ImageMagick' => 'png',
+    'jdenticon SVG' => 'svg',
 );
 $results = array(
     'vizhash' => array(
@@ -35,21 +48,30 @@ $results = array(
     'identicon SVG' => array(
         'lengths' => array(),
         'time' => 0
-    )
+    ),
+    'jdenticon' => array(
+        'lengths' => array(),
+        'time' => 0
+    ),
+    'jdenticon ImageMagick' => array(
+        'lengths' => array(),
+        'time' => 0
+    ),
+    'jdenticon SVG' => array(
+        'lengths' => array(),
+        'time' => 0
+    ),
 );
 $hmacs = array();
 
 echo 'generate ', ITERATIONS, ' hmacs and pre-populate the result array, so tests wont be slowed down', PHP_EOL;
 for ($i = 0; $i < ITERATIONS; ++$i) {
     $hmacs[$i] = hash_hmac('sha512', '127.0.0.1', bin2hex(random_bytes(256)));
-    $results['vizhash']['lengths'][$i] = 0;
-    $results['identicon GD']['lengths'][$i] = 0;
-    $results['identicon ImageMagick']['lengths'][$i] = 0;
-    $results['identicon SVG']['lengths'][$i] = 0;
+    foreach (array_keys($results) as $test) {
+        $results[$test]['lengths'][$i] = 0;
+    }
 }
 
-
-
 echo 'run vizhash tests', PHP_EOL;
 $start = microtime(true);
 foreach ($hmacs as $i => $hmac) {
@@ -60,7 +82,6 @@ foreach ($hmacs as $i => $hmac) {
 }
 $results['vizhash']['time'] = microtime(true) - $start;
 
-
 foreach ($identiconGenerators as $key => $identicon) {
     echo 'run ', $key,' tests', PHP_EOL;
     $start = microtime(true);
@@ -71,9 +92,35 @@ foreach ($identiconGenerators as $key => $identicon) {
     $results[$key]['time'] = microtime(true) - $start;
 }
 
+foreach ($jdenticonGenerators as $key => $format) {
+    echo 'run ', $key,' tests', PHP_EOL;
+    if ($key === 'jdenticon ImageMagick') {
+        $jdenticon->enableImageMagick = true;
+    } else {
+        $jdenticon->enableImageMagick = false;
+    }
+    $start = microtime(true);
+    foreach ($hmacs as $i => $hmac) {
+        $jdenticon->setHash($hmac);
+        $data = $jdenticon->getImageDataUri($format);
+        $results[$key]['lengths'][$i] = strlen($data);
+    }
+    $results[$key]['time'] = microtime(true) - $start;
+}
 
 
-define('PADDING_LENGTH', max(array_map(function ($key) { return strlen($key); }, array_keys($results))) + 1);
+define(
+    'PADDING_LENGTH',
+    max(
+        array_map(
+            function ($key) {
+                return strlen($key);
+            },
+            array_keys($results)
+        )
+    ) + 1
+);
+
 function format_result_line($generator, $min, $max, $avg, $sec) {
     echo str_pad($generator, PADDING_LENGTH, ' '), "\t",
         str_pad($min, 4, ' ', STR_PAD_LEFT), "\t",
@@ -84,7 +131,10 @@ function format_result_line($generator, $min, $max, $avg, $sec) {
 
 echo PHP_EOL;
 format_result_line('Generator:', 'min', 'max', 'avg', 'seconds');
-format_result_line(str_repeat('─', PADDING_LENGTH), str_repeat('─', 4), str_repeat('─', 4), str_repeat('─', 4), str_repeat('─', 7));
+format_result_line(
+    str_repeat('─', PADDING_LENGTH), str_repeat('─', 4), str_repeat('─', 4),
+    str_repeat('─', 4), str_repeat('─', 7)
+);
 foreach ($results as $generator => $result) {
     sort($result['lengths']);
     format_result_line(

+ 199 - 0
bin/migrate

@@ -0,0 +1,199 @@
+#!/usr/bin/env php
+<?php
+
+// change this, if your php files and data is outside of your webservers document root
+define('PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
+
+define('PUBLIC_PATH', __DIR__ . DIRECTORY_SEPARATOR);
+require PATH . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
+
+use PrivateBin\Configuration;
+use PrivateBin\Model;
+
+// third argument in getopt requires PHP >= 7.1
+if (version_compare(PHP_VERSION, '7.1.0') < 0) {
+    dieerr('migrate requires php 7.1 or above to work. Sorry.');
+}
+
+$longopts = array(
+    "delete-after",
+    "delete-during"
+);
+$opts_arr = getopt("fhnv", $longopts, $rest);
+if ($opts_arr === false) {
+    dieerr("Erroneous command line options. Please use -h");
+}
+if (array_key_exists("h", $opts_arr)) {
+    helpexit();
+}
+
+$delete_after    = array_key_exists("delete-after",  $opts_arr);
+$delete_during   = array_key_exists("delete-during", $opts_arr);
+$force_overwrite = array_key_exists("f", $opts_arr);
+$dryrun          = array_key_exists("n", $opts_arr);
+$verbose         = array_key_exists("v", $opts_arr);
+
+if ($rest >= $argc) {
+    dieerr("Missing source configuration directory");
+}
+if ($delete_after && $delete_during) {
+    dieerr("--delete-after and --delete-during are mutually exclusive");
+}
+
+$srcconf = getConfig("source", $argv[$rest]);
+$rest++;
+$dstconf = getConfig("destination", ($rest < $argc ? $argv[$rest] : ""));
+
+if (($srcconf->getSection("model")         == $dstconf->getSection("model")) &&
+    ($srcconf->getSection("model_options") == $dstconf->getSection("model_options"))) {
+    dieerr("Source and destination storage configurations are identical");
+}
+
+$srcmodel = new Model($srcconf);
+$srcstore = $srcmodel->getStore();
+$dstmodel = new Model($dstconf);
+$dststore = $dstmodel->getStore();
+$ids      = $srcstore->getAllPastes();
+
+foreach ($ids as $id) {
+    debug("Reading paste id " . $id);
+    $paste    = $srcstore->read($id);
+    $comments = $srcstore->readComments($id);
+
+    savePaste($force_overwrite, $dryrun, $id, $paste, $dststore);
+    foreach ($comments as $comment) {
+        saveComment($force_overwrite, $dryrun, $id, $comment, $dststore);
+    }
+    if ($delete_during) {
+        deletePaste($dryrun, $id, $srcstore);
+    }
+}
+
+if ($delete_after) {
+    foreach ($ids as $id) {
+        deletePaste($dryrun, $id, $srcstore);
+    }
+}
+
+debug("Done.");
+
+
+function deletePaste($dryrun, $pasteid, $srcstore)
+{
+    if (!$dryrun) {
+        debug("Deleting paste id " . $pasteid);
+        $srcstore->delete($pasteid);
+    } else {
+        debug("Would delete paste id " . $pasteid);
+    }
+}
+
+function saveComment ($force_overwrite, $dryrun, $pasteid, $comment, $dststore)
+{
+    $parentid  = $comment["parentid"];
+    $commentid = $comment["id"];
+
+    if (!$dststore->existsComment($pasteid, $parentid, $commentid)) {
+        if (!$dryrun) {
+            debug("Saving paste id " . $pasteid . ", parent id " .
+                  $parentid . ", comment id " . $commentid);
+            $dststore->createComment($pasteid, $parentid, $commentid, $comment);
+        } else {
+            debug("Would save paste id " . $pasteid . ", parent id " .
+                  $parentid . ", comment id " . $commentid);
+        }
+    } else if ($force_overwrite) {
+        if (!$dryrun) {
+            debug("Overwriting paste id " . $pasteid . ", parent id " .
+                  $parentid . ", comment id " . $commentid);
+            $dststore->createComment($pasteid, $parentid, $commentid, $comment);
+        } else {
+            debug("Would overwrite paste id " . $pasteid . ", parent id " .
+                  $parentid . ", comment id " . $commentid);
+        }
+    } else {
+        if (!$dryrun) {
+            dieerr("Not overwriting paste id " . $pasteid . ", parent id " .
+                   $parentid . ", comment id " . $commentid);
+        } else {
+            dieerr("Would not overwrite paste id " . $pasteid . ", parent id " .
+                   $parentid . ", comment id " . $commentid);
+        }
+    }
+}
+
+function savePaste ($force_overwrite, $dryrun, $pasteid, $paste, $dststore)
+{
+    if (!$dststore->exists($pasteid)) {
+        if (!$dryrun) {
+            debug("Saving paste id " . $pasteid);
+            $dststore->create($pasteid, $paste);
+        } else {
+            debug("Would save paste id " . $pasteid);
+        }
+    } else if ($force_overwrite) {
+        if (!$dryrun) {
+            debug("Overwriting paste id " . $pasteid);
+            $dststore->create($pasteid, $paste);
+        } else {
+            debug("Would overwrite paste id " . $pasteid);
+        }
+    } else {
+        if (!$dryrun) {
+            dieerr("Not overwriting paste id " . $pasteid);
+        } else {
+            dieerr("Would not overwrite paste id " . $pasteid);
+        }
+    }
+}
+
+function getConfig ($target, $confdir)
+{
+    debug("Trying to load " . $target . " conf.php" .
+          ($confdir === "" ? "" : " from " . $confdir));
+
+    putenv("CONFIG_PATH=" . $confdir);
+    $conf = new Configuration;
+    putenv("CONFIG_PATH=");
+
+    return $conf;
+}
+
+function dieerr ($text)
+{
+    fprintf(STDERR, "ERROR: %s" . PHP_EOL, $text);
+    die(1);
+}
+
+function debug ($text) {
+    if ($GLOBALS["verbose"]) {
+        printf("DEBUG: %s" . PHP_EOL, $text);
+    }
+}
+
+function helpexit ()
+{
+    print("migrate.php - Copy data between PrivateBin backends
+
+Usage:
+  migrate [--delete-after] [--delete-during] [-f] [-n] [-v] srcconfdir
+          [<dstconfdir>]
+  migrate [-h]
+
+Options:
+  --delete-after   delete data from source after all pastes and comments have
+                   successfully been copied to the destination
+  --delete-during  delete data from source after the current paste and its
+                   comments have successfully been copied to the destination
+  -f               forcefully overwrite data which already exists at the
+                   destination
+  -n               dry run, do not copy data
+  -v               be verbose
+  <srcconfdir>     use storage backend configration from conf.php found in
+                   this directory as source
+  <dstconfdir>     optionally, use storage backend configration from conf.php
+                   found in this directory as destination; defaults to:
+                   " . PATH . "cfg" . DIRECTORY_SEPARATOR . "conf.php
+");
+    exit();
+}

+ 1 - 1
cfg/conf.sample.php

@@ -70,7 +70,7 @@ languageselection = false
 ; used to get the IP of a comment poster if the server salt is leaked and a
 ; SHA512 HMAC rainbow table is generated for all (relevant) IPs.
 ; Can be set to one these values:
-; "none" / "vizhash" / "identicon" / "jdenticon" (default).
+; "none" / "identicon" (default) / "jdenticon" / "vizhash".
 ; icon = "none"
 
 ; Content Security Policy headers allow a website to restrict what sources are

+ 6 - 6
composer.lock

@@ -370,16 +370,16 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.15.1",
+            "version": "v4.15.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
+                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
-                "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
+                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
                 "shasum": ""
             },
             "require": {
@@ -420,9 +420,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
             },
-            "time": "2022-09-04T07:30:47+00:00"
+            "time": "2022-11-12T15:38:23+00:00"
         },
         {
             "name": "phar-io/manifest",

+ 193 - 0
i18n/th.json

@@ -0,0 +1,193 @@
+{
+    "PrivateBin": "PrivateBin",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s เป็น pastebin ออนไลน์แบบโอเพ่นซอร์สที่มีสไตล์แบบมินิมัลลิสท์ เซิร์ฟเวอร์ไม่สามารถรู้ได้ว่าข้อมูลโค้ดที่มาฝากนั้นเป็นข้อมูลอะไร โดยจะถูก %sเข้ารหัส/ถอดรหัส%s ด้วยกระบวนการ AES จำนวน 256 บิตผ่านเบราว์เซอร์",
+    "More information on the <a href=\"https://privatebin.info/\">project page</a>.": "ข้อมูลเพิ่มเติม ดูได้ที่<a href=\"https://privatebin.info/\">หน้าโครงการ</a>",
+    "Because ignorance is bliss": "ไม่รู้ไม่ชี้ดีที่สุด",
+    "en": "th",
+    "Paste does not exist, has expired or has been deleted.": "การฝากโค้ดไม่มีอยู่ อาจจะหมดอายุหรือถูกลบไปแล้ว",
+    "%s requires php %s or above to work. Sorry.": "ขออภัย %s ต้องใช้ PHP %s ขึ้นไปจึงจะใช้งานได้",
+    "%s requires configuration section [%s] to be present in configuration file.": "%s จำเป็นต้องตั้งค่าตัวแปร [%s] ในไฟล์กำหนดค่า",
+    "Please wait %d seconds between each post.": [
+        "กรุณาเว้นระยะเวลาการส่งข้อมูลอย่างน้อย %d วินาที",
+        "กรุณาเว้นระยะเวลาการส่งข้อมูลอย่างน้อย %d วินาที",
+        "กรุณาเว้นระยะเวลาการส่งข้อมูลอย่างน้อย %d วินาที",
+        "กรุณาเว้นระยะเวลาการส่งข้อมูลอย่างน้อย %d วินาที"
+    ],
+    "Paste is limited to %s of encrypted data.": "การฝากโค้ดแบบเข้ารหัส ขีดจำกัดสูงสุดคือ %s",
+    "Invalid data.": "ข้อมูลไม่ถูกต้อง",
+    "You are unlucky. Try again.": "วันนี้คุณดวงไม่เฮงเลย ลองใหม่อีกครั้งนะ",
+    "Error saving comment. Sorry.": "ขออภัย เกิดข้อผิดพลาดในระหว่างบันทึกความคิดเห็น",
+    "Error saving paste. Sorry.": "ขออภัย เกิดข้อผิดพลาดในระหว่างบันทึกการฝากโค้ด",
+    "Invalid paste ID.": "ID การฝากโค้ดไม่ถูกต้อง",
+    "Paste is not of burn-after-reading type.": "ข้อมูลการฝากโค้ดนี้ไม่ได้เป็นรูปแบบลบทันทีเมื่อเปิดอ่าน",
+    "Wrong deletion token. Paste was not deleted.": "โทเค็นการลบไม่ถูกต้อง ข้อมูลการฝากโค้ดไม่ถูกลบ",
+    "Paste was properly deleted.": "ข้อมูลการฝากโค้ดถูกลบออกเรียบร้อยแล้ว",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.": "จำเป็นต้องใช้ JavaScript เพื่อให้ %s สามารถทำงานได้ ขออภัยในความไม่สะดวก",
+    "%s requires a modern browser to work.": "%s ต้องใช้เบราว์เซอร์สมัยใหม่ถึงจะสามารถใช้งานได้",
+    "New": "ใหม่",
+    "Send": "ส่ง",
+    "Clone": "โคลน",
+    "Raw text": "ข้อความล้วน",
+    "Expires": "หมดอายุ",
+    "Burn after reading": "ลบทันทีเมื่อเปิดอ่าน",
+    "Open discussion": "แสดงความคิดเห็นได้",
+    "Password (recommended)": "รหัสผ่าน (แนะนำให้ใส่)",
+    "Discussion": "ความคิดเห็น",
+    "Toggle navigation": "สลับเปิดปิดการนำทาง",
+    "%d seconds": [
+        "%d วินาที",
+        "%d วินาที",
+        "%d วินาที",
+        "%d วินาที"
+    ],
+    "%d minutes": [
+        "%d นาที",
+        "%d นาที",
+        "%d นาที",
+        "%d นาที"
+    ],
+    "%d hours": [
+        "%d ชั่วโมง",
+        "%d ชั่วโมง",
+        "%d ชั่วโมง",
+        "%d ชั่วโมง"
+    ],
+    "%d days": [
+        "%d วัน",
+        "%d วัน",
+        "%d วัน",
+        "%d วัน"
+    ],
+    "%d weeks": [
+        "%d สัปดาห์",
+        "%d สัปดาห์",
+        "%d สัปดาห์",
+        "%d สัปดาห์"
+    ],
+    "%d months": [
+        "%d เดือน",
+        "%d เดือน",
+        "%d เดือน",
+        "%d เดือน"
+    ],
+    "%d years": [
+        "%d ปี",
+        "%d ปี",
+        "%d ปี",
+        "%d ปี"
+    ],
+    "Never": "ไม่หมดอายุ",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "โปรดทราบ: เว็บไซต์นี้เป็นการให้บริการแบบเบต้า ข้อมูลอาจถูกลบได้ตลอดเวลา หากคุณใช้บริการนี้ในทางที่ผิดอาจจะทำให้ข้อมูลของคุณสูญหายอย่างถาวรได้",
+    "This document will expire in %d seconds.": [
+        "เอกสารนี้จะหมดอายุใน %d วินาที",
+        "เอกสารนี้จะหมดอายุใน %d วินาที",
+        "เอกสารนี้จะหมดอายุใน %d วินาที",
+        "เอกสารนี้จะหมดอายุใน %d วินาที"
+    ],
+    "This document will expire in %d minutes.": [
+        "เอกสารนี้จะหมดอายุใน %d นาที",
+        "เอกสารนี้จะหมดอายุใน %d นาที",
+        "เอกสารนี้จะหมดอายุใน %d นาที",
+        "เอกสารนี้จะหมดอายุใน %d นาที"
+    ],
+    "This document will expire in %d hours.": [
+        "เอกสารนี้จะหมดอายุใน %d ชั่วโมง",
+        "เอกสารนี้จะหมดอายุใน %d ชั่วโมง",
+        "เอกสารนี้จะหมดอายุใน %d ชั่วโมง",
+        "เอกสารนี้จะหมดอายุใน %d ชั่วโมง"
+    ],
+    "This document will expire in %d days.": [
+        "เอกสารนี้จะหมดอายุใน %d วัน",
+        "เอกสารนี้จะหมดอายุใน %d วัน",
+        "เอกสารนี้จะหมดอายุใน %d วัน",
+        "เอกสารนี้จะหมดอายุใน %d วัน"
+    ],
+    "This document will expire in %d months.": [
+        "เอกสารนี้จะหมดอายุใน %d เดือน",
+        "เอกสารนี้จะหมดอายุใน %d เดือน",
+        "เอกสารนี้จะหมดอายุใน %d เดือน",
+        "เอกสารนี้จะหมดอายุใน %d เดือน"
+    ],
+    "Please enter the password for this paste:": "กรุณากรอกรหัสผ่านเพื่อเปิดข้อมูลการฝากโค้ดนี้:",
+    "Could not decrypt data (Wrong key?)": "ไม่สามารถถอดรหัสข้อมูลได้ (คีย์ไม่ถูกต้องหรือไม่)",
+    "Could not delete the paste, it was not stored in burn after reading mode.": "ไม่สามารถลบการฝากโค้ดนี้ได้ เนื่องจากว่าไม่ได้ถูกเก็บไว้ในโหมดลบทันทีเมื่อเปิดอ่าน",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "เก็บไว้ดูคนเดียวนะ อย่าปิดหน้าต่างนี้ ข้อความนี้จะไม่สามารถแสดงได้อีก",
+    "Could not decrypt comment; Wrong key?": "ไม่สามารถถอดรหัสความคิดเห็นได้ คีย์ไม่ถูกต้องหรือไม่",
+    "Reply": "ตอบกลับ",
+    "Anonymous": "ไม่ระบุชื่อ",
+    "Avatar generated from IP address": "อวาตารสร้างมาจากไอพี",
+    "Add comment": "เพิ่มความคิดเห็น",
+    "Optional nickname…": "ใส่ชื่อคนให้ความคิดเห็น…",
+    "Post comment": "ส่งความคิดเห็น",
+    "Sending comment…": "กำลังส่งความคิดเห็น…",
+    "Comment posted.": "ส่งความคิดเห็นแล้ว",
+    "Could not refresh display: %s": "ไม่สามารถรีเฟรชการแสดงผลได้: %s",
+    "unknown status": "ไม่ทราบสถานะ",
+    "server error or not responding": "เซิร์ฟเวอร์มีข้อผิดพลาดหรือไม่ตอบสนอง",
+    "Could not post comment: %s": "ไม่สามารถส่งความคิดเห็นได้: %s",
+    "Sending paste…": "กำลังส่งข้อมูล…",
+    "Your paste is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit [Ctrl]+[c] to copy)</span>": "การฝากโค้ดของคุณอยู่ที่ <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(กดปุ่ม [Ctrl]+[c] เพื่อคัดลอก)</span>",
+    "Delete data": "ลบข้อมูล",
+    "Could not create paste: %s": "ไม่สามารถสร้างข้อมูลการฝากโค้ดได้: %s",
+    "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "ไม่สามารถถอดรหัสข้อมูลการฝากโค้ดได้: คีย์ถอดรหัสที่อยู่ใน URL หายไป (คุณได้ใช้ตัวเปลี่ยนเส้นทางหรือตัวย่อ URL ที่มีการตัดส่วนของ URL ออกหรือไม่)",
+    "B": "B",
+    "KiB": "KiB",
+    "MiB": "MiB",
+    "GiB": "GiB",
+    "TiB": "TiB",
+    "PiB": "PiB",
+    "EiB": "EiB",
+    "ZiB": "ZiB",
+    "YiB": "YiB",
+    "Format": "รูปแบบ",
+    "Plain Text": "ข้อความล้วน",
+    "Source Code": "ซอร์สโค้ด",
+    "Markdown": "Markdown",
+    "Download attachment": "ดาวน์โหลดไฟล์แนบ",
+    "Cloned: '%s'": "โคลนแล้ว: '%s'",
+    "The cloned file '%s' was attached to this paste.": "การโคลนข้อมูลการฝากโค้ด มีไฟล์ '%s' แนบมาด้วย",
+    "Attach a file": "แนบไฟล์",
+    "alternatively drag & drop a file or paste an image from the clipboard": "หรือสามารถลากและวางไฟล์หรือวางรูปภาพจากคลิปบอร์ดได้",
+    "File too large, to display a preview. Please download the attachment.": "ไฟล์มีขนาดใหญ่เกินไปที่จะแสดงตัวอย่าง กรุณาดาวน์โหลดเป็นไฟล์แนบแทน",
+    "Remove attachment": "ลบไฟล์แนบ",
+    "Your browser does not support uploading encrypted files. Please use a newer browser.": "เบราว์เซอร์ของคุณไม่สนับสนุนการอัปโหลดไฟล์แบบเข้ารหัสได้ กรุณาใช้เบราว์เซอร์ที่ใหม่กว่า",
+    "Invalid attachment.": "ไฟล์แนบไม่ถูกต้อง",
+    "Options": "ตัวเลือก",
+    "Shorten URL": "สร้างลิงก์ย่อ",
+    "Editor": "ตัวแก้ไข",
+    "Preview": "ดูตัวอย่าง",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "%s กำหนดให้ PATH ลงท้ายด้วย \"%s\" กรุณาอัปเดต PATH ในไฟล์ index.php ของคุณ",
+    "Decrypt": "ถอดรหัส",
+    "Enter password": "กรอกรหัสผ่าน",
+    "Loading…": "กำลังโหลด…",
+    "Decrypting paste…": "กำลังถอดรหัสข้อมูลการฝากโค้ด…",
+    "Preparing new paste…": "กำลังเตรียมข้อมูลการฝากโค้ดใหม่…",
+    "In case this message never disappears please have a look at <a href=\"%s\">this FAQ for information to troubleshoot</a>.": "ในกรณีที่ข้อความนี้ยังปรากฎให้เห็นอยู่ กรุณาดู<a href=\"%s\">คำถามที่พบบ่อยนี้เพื่อใช้แก้ไขปัญหา</a>",
+    "+++ no paste text +++": "+++ ไม่มีข้อความการฝากโค้ด +++",
+    "Could not get paste data: %s": "ไม่สามารถดึงข้อมูลการฝากโค้ดได้: %s",
+    "QR code": "คิวอาร์โค้ด",
+    "This website is using an insecure HTTP connection! Please use it only for testing.": "เว็บไซต์นี้ใช้การเชื่อมต่อแบบ HTTP ที่ไม่ปลอดภัย! กรุณาใช้เพื่อการทดสอบเท่านั้น",
+    "For more information <a href=\"%s\">see this FAQ entry</a>.": "สำหรับข้อมูลเพิ่มเติม <a href=\"%s\">กรุณาดูรายการคำถามที่พบบ่อยนี้</a>",
+    "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.": "เบราว์เซอร์ของคุณอาจต้องใช้การเชื่อมต่อ HTTPS เพื่อสนับสนุน API แบบ WebCrypto ลอง<a href=\"%s\">เปลี่ยนเป็น HTTPS</a>",
+    "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "เบราว์เซอร์ของคุณไม่สนับสนุน WebAssembly ที่ทำหน้าที่ในการบีบอัดข้อมูลในรูปแบบ zlib คุณยังสามารถสร้างเอกสารที่ไม่บีบอัด แต่จะไม่สามารถอ่านเอกสารที่บีบอัดได้",
+    "waiting on user to provide a password": "กำลังรอผู้ใช้กรอกรหัสผ่าน",
+    "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.": "ไม่สามารถถอดรหัสข้อมูลได้ คุณกรอกรหัสผ่านผิดหรือเปล่า กดปุ่มลองอีกครั้งด้านบน",
+    "Retry": "ลองอีกครั้ง",
+    "Showing raw text…": "กำลังแสดงข้อความล้วน…",
+    "Notice:": "โปรดทราบ:",
+    "This link will expire after %s.": "ลิงก์นี้จะหมดอายุหลังจาก %s",
+    "This link can only be accessed once, do not use back or refresh button in your browser.": "ลิงก์นี้สามารถเข้าถึงได้เพียงครั้งเดียวเท่านั้น ไม่ควรใช้ปุ่มย้อนกลับหรือรีเฟรชหน้าเว็บบนเบราว์เซอร์ของคุณ",
+    "Link:": "ลิงก์:",
+    "Recipient may become aware of your timezone, convert time to UTC?": "ผู้รับอีเมลอาจทราบโซนเวลาของคุณได้ คุณต้องการแปลงโซนเวลาเป็น UTC หรือไม่",
+    "Use Current Timezone": "ใช้โซนเวลาปัจจุบัน",
+    "Convert To UTC": "แปลงเป็น UTC",
+    "Close": "ปิด",
+    "Encrypted note on %s": "เขารหัสบันทึกย่อบน %s",
+    "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "ไปที่ลิงก์นี้เพื่อดูบันทึกย่อทั้งหมด ส่ง URL นี้ให้ใครก็ได้เพื่อให้สามารถเข้าถึงบันทึกย่อได้",
+    "URL shortener may expose your decrypt key in URL.": "เครื่องมือสร้างลิงก์ย่ออาจเปิดเผยคีย์ถอดรหัสของคุณใน URL ได้",
+    "Save paste": "ดาวน์โหลดข้อมูลการฝากโค้ด",
+    "Your IP is not authorized to create pastes.": "IP ของคุณไม่ได้รับอนุญาตให้สร้างการฝากโค้ด",
+    "Trying to shorten a URL that isn't pointing at our instance.": "กำลังพยายามใช้เครื่องมือสร้างลิงก์ย่อ ที่ไม่ได้ชี้ไปที่อินสแตนซ์ของเรา",
+    "Error calling YOURLS. Probably a configuration issue, like wrong or missing \"apiurl\" or \"signature\".": "เกิดข้อผิดพลาดในการเรียก YOURLS อาจเป็นปัญหามาจากการกำหนดค่า เช่น \"apiurl\" หรือ \"signature\" ไม่ถูกต้องหรือขาดหายไป",
+    "Error parsing YOURLS response.": "เกิดข้อผิดพลาดในการแยกวิเคราะห์การตอบสนองของ YOURLS"
+}

+ 3 - 3
js/common.js

@@ -10,14 +10,14 @@ global.fs = require('fs');
 global.WebCrypto = require('@peculiar/webcrypto').Crypto;
 
 // application libraries to test
-global.$ = global.jQuery = require('./jquery-3.6.0');
+global.$ = global.jQuery = require('./jquery-3.6.1');
 global.RawDeflate = require('./rawinflate-0.3').RawDeflate;
 global.zlib = require('./zlib-1.2.13').zlib;
 require('./prettify');
 global.prettyPrint = window.PR.prettyPrint;
 global.prettyPrintOne = window.PR.prettyPrintOne;
-global.showdown = require('./showdown-2.0.3');
-global.DOMPurify = require('./purify-2.3.6');
+global.showdown = require('./showdown-2.1.0');
+global.DOMPurify = require('./purify-2.4.6');
 global.baseX = require('./base-x-4.0.0').baseX;
 global.Legacy = require('./legacy').Legacy;
 require('./bootstrap-3.4.1');

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 1
js/jquery-3.6.0.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
js/jquery-3.6.1.js


+ 2 - 1
js/privatebin.js

@@ -627,7 +627,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          * @prop   {string[]}
          * @readonly
          */
-        const supportedLanguages = ['bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'jbo', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sk', 'sl', 'tr', 'uk', 'zh'];
+        const supportedLanguages = ['bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'jbo', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sk', 'sl', 'th', 'tr', 'uk', 'zh'];
 
         /**
          * built in language
@@ -815,6 +815,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                     return n === 1 ? 0 : (n === 2 ? 1 : ((n < 0 || n > 10) && (n % 10 === 0) ? 2 : 3));
                 case 'id':
                 case 'jbo':
+                case 'th':
                     return 0;
                 case 'lt':
                     return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2);

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 1
js/purify-2.3.6.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
js/purify-2.4.6.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
js/showdown-2.1.0.js


+ 1 - 1
lib/Configuration.php

@@ -53,7 +53,7 @@ class Configuration
             'languagedefault'          => '',
             'urlshortener'             => '',
             'qrcode'                   => true,
-            'icon'                     => 'jdenticon',
+            'icon'                     => 'identicon',
             'cspheader'                => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
             'zerobincompatibility'     => false,
             'httpwarning'              => true,

+ 13 - 49
lib/Data/AbstractData.php

@@ -15,61 +15,17 @@ namespace PrivateBin\Data;
 /**
  * AbstractData
  *
- * Abstract model for data access, implemented as a singleton.
+ * Abstract model for data access
  */
 abstract class AbstractData
 {
-    /**
-     * Singleton instance
-     *
-     * @access protected
-     * @static
-     * @var AbstractData
-     */
-    protected static $_instance = null;
-
     /**
      * cache for the traffic limiter
      *
-     * @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
+     * @var    array
      */
-    protected function __construct()
-    {
-    }
-
-    /**
-     * Enforce singleton, disable cloning
-     *
-     * Instantiate using {@link getInstance()}, this object implements the singleton pattern.
-     *
-     * @access private
-     */
-    private function __clone()
-    {
-    }
-
-    /**
-     * Get instance of singleton
-     *
-     * @access public
-     * @static
-     * @param  array $options
-     * @return AbstractData
-     */
-    public static function getInstance(array $options)
-    {
-    }
+    protected $_last_cache = array();
 
     /**
      * Create a paste.
@@ -150,9 +106,9 @@ abstract class AbstractData
     public function purgeValues($namespace, $time)
     {
         if ($namespace === 'traffic_limiter') {
-            foreach (self::$_last_cache as $key => $last_submission) {
+            foreach ($this->_last_cache as $key => $last_submission) {
                 if ($last_submission <= $time) {
-                    unset(self::$_last_cache[$key]);
+                    unset($this->_last_cache[$key]);
                 }
             }
         }
@@ -207,6 +163,14 @@ abstract class AbstractData
         }
     }
 
+    /**
+     * Returns all paste ids
+     *
+     * @access public
+     * @return array
+     */
+    abstract public function getAllPastes();
+
     /**
      * Get next free slot for comment from postdate.
      *

+ 173 - 217
lib/Data/Database.php

@@ -25,59 +25,43 @@ use PrivateBin\Json;
  */
 class Database extends AbstractData
 {
-    /**
-     * cache for select queries
-     *
-     * @var array
-     */
-    private static $_cache = array();
-
     /**
      * instance of database connection
      *
      * @access private
-     * @static
      * @var PDO
      */
-    private static $_db;
+    private $_db;
 
     /**
      * table prefix
      *
      * @access private
-     * @static
      * @var string
      */
-    private static $_prefix = '';
+    private $_prefix = '';
 
     /**
      * database type
      *
      * @access private
-     * @static
      * @var string
      */
-    private static $_type = '';
+    private $_type = '';
 
     /**
-     * get instance of singleton
+     * instantiates a new Database data backend
      *
      * @access public
-     * @static
      * @param  array $options
      * @throws Exception
-     * @return Database
+     * @return
      */
-    public static function getInstance(array $options)
+    public function __construct(array $options)
     {
-        // if needed initialize the singleton
-        if (!(self::$_instance instanceof self)) {
-            self::$_instance = new self;
-        }
-
         // set table prefix if given
         if (array_key_exists('tbl', $options)) {
-            self::$_prefix = $options['tbl'];
+            $this->_prefix = $options['tbl'];
         }
 
         // initialize the db connection with new options
@@ -94,16 +78,16 @@ class Database extends AbstractData
             $db_tables_exist                            = true;
 
             // setup type and dabase connection
-            self::$_type = strtolower(
+            $this->_type = strtolower(
                 substr($options['dsn'], 0, strpos($options['dsn'], ':'))
             );
             // MySQL uses backticks to quote identifiers by default,
             // tell it to expect ANSI SQL double quotes
-            if (self::$_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
+            if ($this->_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
                 $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
             }
-            $tableQuery = self::_getTableQuery(self::$_type);
-            self::$_db  = new PDO(
+            $tableQuery = $this->_getTableQuery($this->_type);
+            $this->_db  = new PDO(
                 $options['dsn'],
                 $options['usr'],
                 $options['pwd'],
@@ -111,43 +95,41 @@ class Database extends AbstractData
             );
 
             // check if the database contains the required tables
-            $tables = self::$_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
+            $tables = $this->_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
 
             // create paste table if necessary
-            if (!in_array(self::_sanitizeIdentifier('paste'), $tables)) {
-                self::_createPasteTable();
+            if (!in_array($this->_sanitizeIdentifier('paste'), $tables)) {
+                $this->_createPasteTable();
                 $db_tables_exist = false;
             }
 
             // create comment table if necessary
-            if (!in_array(self::_sanitizeIdentifier('comment'), $tables)) {
-                self::_createCommentTable();
+            if (!in_array($this->_sanitizeIdentifier('comment'), $tables)) {
+                $this->_createCommentTable();
                 $db_tables_exist = false;
             }
 
             // create config table if necessary
             $db_version = Controller::VERSION;
-            if (!in_array(self::_sanitizeIdentifier('config'), $tables)) {
-                self::_createConfigTable();
+            if (!in_array($this->_sanitizeIdentifier('config'), $tables)) {
+                $this->_createConfigTable();
                 // if we only needed to create the config table, the DB is older then 0.22
                 if ($db_tables_exist) {
                     $db_version = '0.21';
                 }
             } else {
-                $db_version = self::_getConfig('VERSION');
+                $db_version = $this->_getConfig('VERSION');
             }
 
             // update database structure if necessary
             if (version_compare($db_version, Controller::VERSION, '<')) {
-                self::_upgradeDatabase($db_version);
+                $this->_upgradeDatabase($db_version);
             }
         } else {
             throw new Exception(
                 'Missing configuration for key dsn, usr, pwd or opt in the section model_options, please check your configuration file', 6
             );
         }
-
-        return self::$_instance;
     }
 
     /**
@@ -160,22 +142,12 @@ class Database extends AbstractData
      */
     public function create($pasteid, array $paste)
     {
-        if (
-            array_key_exists($pasteid, self::$_cache)
-        ) {
-            if (false !== self::$_cache[$pasteid]) {
-                return false;
-            } else {
-                unset(self::$_cache[$pasteid]);
-            }
-        }
-
         $expire_date      = 0;
         $opendiscussion   = $burnafterreading = false;
         $attachment       = $attachmentname   = null;
         $meta             = $paste['meta'];
         $isVersion1       = array_key_exists('data', $paste);
-        list($createdKey) = self::_getVersionedKeys($isVersion1 ? 1 : 2);
+        list($createdKey) = $this->_getVersionedKeys($isVersion1 ? 1 : 2);
         $created          = (int) $meta[$createdKey];
         unset($meta[$createdKey], $paste['meta']);
         if (array_key_exists('expire_date', $meta)) {
@@ -204,8 +176,8 @@ class Database extends AbstractData
             $burnafterreading = $paste['adata'][3];
         }
         try {
-            return self::_exec(
-                'INSERT INTO "' . self::_sanitizeIdentifier('paste') .
+            return $this->_exec(
+                'INSERT INTO "' . $this->_sanitizeIdentifier('paste') .
                 '" VALUES(?,?,?,?,?,?,?,?,?)',
                 array(
                     $pasteid,
@@ -233,64 +205,59 @@ class Database extends AbstractData
      */
     public function read($pasteid)
     {
-        if (array_key_exists($pasteid, self::$_cache)) {
-            return self::$_cache[$pasteid];
-        }
-
-        self::$_cache[$pasteid] = false;
         try {
-            $paste = self::_select(
-                'SELECT * FROM "' . self::_sanitizeIdentifier('paste') .
+            $row = $this->_select(
+                'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') .
                 '" WHERE "dataid" = ?', array($pasteid), true
             );
         } catch (Exception $e) {
-            $paste = false;
+            $row = false;
         }
-        if ($paste === false) {
+        if ($row === false) {
             return false;
         }
         // create array
-        $data       = Json::decode($paste['data']);
+        $data       = Json::decode($row['data']);
         $isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2;
         if ($isVersion2) {
-            self::$_cache[$pasteid] = $data;
-            list($createdKey)       = self::_getVersionedKeys(2);
+            $paste            = $data;
+            list($createdKey) = $this->_getVersionedKeys(2);
         } else {
-            self::$_cache[$pasteid] = array('data' => $paste['data']);
-            list($createdKey)       = self::_getVersionedKeys(1);
+            $paste            = array('data' => $row['data']);
+            list($createdKey) = $this->_getVersionedKeys(1);
         }
 
         try {
-            $paste['meta'] = Json::decode($paste['meta']);
+            $row['meta'] = Json::decode($row['meta']);
         } catch (Exception $e) {
-            $paste['meta'] = array();
+            $row['meta'] = array();
         }
-        $paste                                       = self::upgradePreV1Format($paste);
-        self::$_cache[$pasteid]['meta']              = $paste['meta'];
-        self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste['postdate'];
-        $expire_date                                 = (int) $paste['expiredate'];
+        $row                        = self::upgradePreV1Format($row);
+        $paste['meta']              = $row['meta'];
+        $paste['meta'][$createdKey] = (int) $row['postdate'];
+        $expire_date                = (int) $row['expiredate'];
         if ($expire_date > 0) {
-            self::$_cache[$pasteid]['meta']['expire_date'] = $expire_date;
+            $paste['meta']['expire_date'] = $expire_date;
         }
         if ($isVersion2) {
-            return self::$_cache[$pasteid];
+            return $paste;
         }
 
         // support v1 attachments
-        if (array_key_exists('attachment', $paste) && !empty($paste['attachment'])) {
-            self::$_cache[$pasteid]['attachment'] = $paste['attachment'];
-            if (array_key_exists('attachmentname', $paste) && !empty($paste['attachmentname'])) {
-                self::$_cache[$pasteid]['attachmentname'] = $paste['attachmentname'];
+        if (array_key_exists('attachment', $row) && !empty($row['attachment'])) {
+            $paste['attachment'] = $row['attachment'];
+            if (array_key_exists('attachmentname', $row) && !empty($row['attachmentname'])) {
+                $paste['attachmentname'] = $row['attachmentname'];
             }
         }
-        if ($paste['opendiscussion']) {
-            self::$_cache[$pasteid]['meta']['opendiscussion'] = true;
+        if ($row['opendiscussion']) {
+            $paste['meta']['opendiscussion'] = true;
         }
-        if ($paste['burnafterreading']) {
-            self::$_cache[$pasteid]['meta']['burnafterreading'] = true;
+        if ($row['burnafterreading']) {
+            $paste['meta']['burnafterreading'] = true;
         }
 
-        return self::$_cache[$pasteid];
+        return $paste;
     }
 
     /**
@@ -301,19 +268,14 @@ class Database extends AbstractData
      */
     public function delete($pasteid)
     {
-        self::_exec(
-            'DELETE FROM "' . self::_sanitizeIdentifier('paste') .
+        $this->_exec(
+            'DELETE FROM "' . $this->_sanitizeIdentifier('paste') .
             '" WHERE "dataid" = ?', array($pasteid)
         );
-        self::_exec(
-            'DELETE FROM "' . self::_sanitizeIdentifier('comment') .
+        $this->_exec(
+            'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
             '" WHERE "pasteid" = ?', array($pasteid)
         );
-        if (
-            array_key_exists($pasteid, self::$_cache)
-        ) {
-            unset(self::$_cache[$pasteid]);
-        }
     }
 
     /**
@@ -325,12 +287,15 @@ class Database extends AbstractData
      */
     public function exists($pasteid)
     {
-        if (
-            !array_key_exists($pasteid, self::$_cache)
-        ) {
-            self::$_cache[$pasteid] = $this->read($pasteid);
+        try {
+            $row = $this->_select(
+                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
+                '" WHERE "dataid" = ?', array($pasteid), true
+            );
+        } catch (Exception $e) {
+            return false;
         }
-        return (bool) self::$_cache[$pasteid];
+        return (bool) $row;
     }
 
     /**
@@ -352,7 +317,7 @@ class Database extends AbstractData
             $version = 2;
             $data    = Json::encode($comment);
         }
-        list($createdKey, $iconKey) = self::_getVersionedKeys($version);
+        list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
         $meta                       = $comment['meta'];
         unset($comment['meta']);
         foreach (array('nickname', $iconKey) as $key) {
@@ -361,8 +326,8 @@ class Database extends AbstractData
             }
         }
         try {
-            return self::_exec(
-                'INSERT INTO "' . self::_sanitizeIdentifier('comment') .
+            return $this->_exec(
+                'INSERT INTO "' . $this->_sanitizeIdentifier('comment') .
                 '" VALUES(?,?,?,?,?,?,?)',
                 array(
                     $commentid,
@@ -388,8 +353,8 @@ class Database extends AbstractData
      */
     public function readComments($pasteid)
     {
-        $rows = self::_select(
-            'SELECT * FROM "' . self::_sanitizeIdentifier('comment') .
+        $rows = $this->_select(
+            'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
             '" WHERE "pasteid" = ?', array($pasteid)
         );
 
@@ -406,7 +371,7 @@ class Database extends AbstractData
                     $version      = 1;
                     $comments[$i] = array('data' => $row['data']);
                 }
-                list($createdKey, $iconKey) = self::_getVersionedKeys($version);
+                list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
                 $comments[$i]['id']         = $row['dataid'];
                 $comments[$i]['parentid']   = $row['parentid'];
                 $comments[$i]['meta']       = array($createdKey => (int) $row['postdate']);
@@ -433,8 +398,8 @@ class Database extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     {
         try {
-            return (bool) self::_select(
-                'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('comment') .
+            return (bool) $this->_select(
+                'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
                 '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
                 array($pasteid, $parentid, $commentid), true
             );
@@ -455,15 +420,15 @@ class Database extends AbstractData
     public function setValue($value, $namespace, $key = '')
     {
         if ($namespace === 'traffic_limiter') {
-            self::$_last_cache[$key] = $value;
+            $this->_last_cache[$key] = $value;
             try {
-                $value = Json::encode(self::$_last_cache);
+                $value = Json::encode($this->_last_cache);
             } catch (Exception $e) {
                 return false;
             }
         }
-        return self::_exec(
-            'UPDATE "' . self::_sanitizeIdentifier('config') .
+        return $this->_exec(
+            'UPDATE "' . $this->_sanitizeIdentifier('config') .
             '" SET "value" = ? WHERE "id" = ?',
             array($value, strtoupper($namespace))
         );
@@ -483,8 +448,8 @@ class Database extends AbstractData
         $value     = $this->_getConfig($configKey);
         if ($value === '') {
             // initialize the row, so that setValue can rely on UPDATE queries
-            self::_exec(
-                'INSERT INTO "' . self::_sanitizeIdentifier('config') .
+            $this->_exec(
+                'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
                 '" VALUES(?,?)',
                 array($configKey, '')
             );
@@ -492,7 +457,8 @@ class Database extends AbstractData
             // 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');
+                $fs    = new Filesystem(array('dir' => 'data'));
+                $value = $fs->getValue('salt');
                 $this->setValue($value, 'salt');
                 @unlink($file);
                 return $value;
@@ -500,12 +466,12 @@ class Database extends AbstractData
         }
         if ($value && $namespace === 'traffic_limiter') {
             try {
-                self::$_last_cache = Json::decode($value);
+                $this->_last_cache = Json::decode($value);
             } catch (Exception $e) {
-                self::$_last_cache = array();
+                $this->_last_cache = array();
             }
-            if (array_key_exists($key, self::$_last_cache)) {
-                return self::$_last_cache[$key];
+            if (array_key_exists($key, $this->_last_cache)) {
+                return $this->_last_cache[$key];
             }
         }
         return (string) $value;
@@ -520,34 +486,37 @@ class Database extends AbstractData
      */
     protected function _getExpiredPastes($batchsize)
     {
-        $pastes = array();
-        $rows   = self::_select(
-            'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('paste') .
+        $statement = $this->_db->prepare(
+            'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
             '" WHERE "expiredate" < ? AND "expiredate" != ? ' .
-            (self::$_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?'),
-            array(time(), 0, $batchsize)
+            ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?')
         );
-        if (is_array($rows) && count($rows)) {
-            foreach ($rows as $row) {
-                $pastes[] = $row['dataid'];
-            }
-        }
-        return $pastes;
+        $statement->execute(array(time(), 0, $batchsize));
+        return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getAllPastes()
+    {
+        return $this->_db->query(
+            'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . '"'
+        )->fetchAll(PDO::FETCH_COLUMN, 0);
     }
 
     /**
      * execute a statement
      *
      * @access private
-     * @static
      * @param  string $sql
      * @param  array $params
      * @throws PDOException
      * @return bool
      */
-    private static function _exec($sql, array $params)
+    private function _exec($sql, array $params)
     {
-        $statement = self::$_db->prepare($sql);
+        $statement = $this->_db->prepare($sql);
         foreach ($params as $key => &$parameter) {
             $position = $key + 1;
             if (is_int($parameter)) {
@@ -567,20 +536,19 @@ class Database extends AbstractData
      * run a select statement
      *
      * @access private
-     * @static
      * @param  string $sql
      * @param  array $params
      * @param  bool $firstOnly if only the first row should be returned
      * @throws PDOException
      * @return array|false
      */
-    private static function _select($sql, array $params, $firstOnly = false)
+    private function _select($sql, array $params, $firstOnly = false)
     {
-        $statement = self::$_db->prepare($sql);
+        $statement = $this->_db->prepare($sql);
         $statement->execute($params);
         if ($firstOnly) {
             $result = $statement->fetch(PDO::FETCH_ASSOC);
-        } elseif (self::$_type === 'oci') {
+        } elseif ($this->_type === 'oci') {
             // workaround for https://bugs.php.net/bug.php?id=46728
             $result = array();
             while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
@@ -590,7 +558,7 @@ class Database extends AbstractData
             $result = $statement->fetchAll(PDO::FETCH_ASSOC);
         }
         $statement->closeCursor();
-        if (self::$_type === 'oci' && is_array($result)) {
+        if ($this->_type === 'oci' && is_array($result)) {
             // returned CLOB values are streams, convert these into strings
             $result = $firstOnly ?
                 array_map('PrivateBin\Data\Database::_sanitizeClob', $result) :
@@ -603,11 +571,10 @@ class Database extends AbstractData
      * get version dependent key names
      *
      * @access private
-     * @static
      * @param  int $version
      * @return array
      */
-    private static function _getVersionedKeys($version)
+    private function _getVersionedKeys($version)
     {
         if ($version === 1) {
             return array('postdate', 'vizhash');
@@ -619,12 +586,11 @@ class Database extends AbstractData
      * get table list query, depending on the database type
      *
      * @access private
-     * @static
      * @param  string $type
      * @throws Exception
      * @return string
      */
-    private static function _getTableQuery($type)
+    private function _getTableQuery($type)
     {
         switch ($type) {
             case 'ibm':
@@ -675,15 +641,14 @@ class Database extends AbstractData
      * get a value by key from the config table
      *
      * @access private
-     * @static
      * @param  string $key
      * @return string
      */
-    private static function _getConfig($key)
+    private function _getConfig($key)
     {
         try {
-            $row = self::_select(
-                'SELECT "value" FROM "' . self::_sanitizeIdentifier('config') .
+            $row = $this->_select(
+                'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') .
                 '" WHERE "id" = ?', array($key), true
             );
         } catch (PDOException $e) {
@@ -696,14 +661,13 @@ class Database extends AbstractData
      * get the primary key clauses, depending on the database driver
      *
      * @access private
-     * @static
      * @param  string $key
      * @return array
      */
-    private static function _getPrimaryKeyClauses($key = 'dataid')
+    private function _getPrimaryKeyClauses($key = 'dataid')
     {
         $main_key = $after_key = '';
-        switch (self::$_type) {
+        switch ($this->_type) {
             case 'mysql':
             case 'oci':
                 $after_key = ", PRIMARY KEY (\"$key\")";
@@ -721,12 +685,11 @@ class Database extends AbstractData
      * PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB
      *
      * @access private
-     * @static
      * @return string
      */
-    private static function _getDataType()
+    private function _getDataType()
     {
-        switch (self::$_type) {
+        switch ($this->_type) {
             case 'oci':
                 return 'CLOB';
             case 'pgsql':
@@ -742,12 +705,11 @@ class Database extends AbstractData
      * PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB
      *
      * @access private
-     * @static
      * @return string
      */
-    private static function _getAttachmentType()
+    private function _getAttachmentType()
     {
-        switch (self::$_type) {
+        switch ($this->_type) {
             case 'oci':
                 return 'CLOB';
             case 'pgsql':
@@ -763,12 +725,11 @@ class Database extends AbstractData
      * OCI doesn't accept TEXT so it has to be VARCHAR2(4000)
      *
      * @access private
-     * @static
      * @return string
      */
-    private static function _getMetaType()
+    private function _getMetaType()
     {
-        switch (self::$_type) {
+        switch ($this->_type) {
             case 'oci':
                 return 'VARCHAR2(4000)';
             default:
@@ -780,16 +741,15 @@ class Database extends AbstractData
      * create the paste table
      *
      * @access private
-     * @static
      */
-    private static function _createPasteTable()
+    private function _createPasteTable()
     {
-        list($main_key, $after_key) = self::_getPrimaryKeyClauses();
-        $dataType                   = self::_getDataType();
-        $attachmentType             = self::_getAttachmentType();
-        $metaType                   = self::_getMetaType();
-        self::$_db->exec(
-            'CREATE TABLE "' . self::_sanitizeIdentifier('paste') . '" ( ' .
+        list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
+        $dataType                   = $this->_getDataType();
+        $attachmentType             = $this->_getAttachmentType();
+        $metaType                   = $this->_getMetaType();
+        $this->_db->exec(
+            'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
             "\"data\" $attachmentType, " .
             '"postdate" INT, ' .
@@ -806,14 +766,13 @@ class Database extends AbstractData
      * create the paste table
      *
      * @access private
-     * @static
      */
-    private static function _createCommentTable()
+    private function _createCommentTable()
     {
-        list($main_key, $after_key) = self::_getPrimaryKeyClauses();
-        $dataType                   = self::_getDataType();
-        self::$_db->exec(
-            'CREATE TABLE "' . self::_sanitizeIdentifier('comment') . '" ( ' .
+        list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
+        $dataType                   = $this->_getDataType();
+        $this->_db->exec(
+            'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
             '"pasteid" CHAR(16), ' .
             '"parentid" CHAR(16), ' .
@@ -822,15 +781,15 @@ class Database extends AbstractData
             "\"vizhash\" $dataType, " .
             "\"postdate\" INT$after_key )"
         );
-        if (self::$_type === 'oci') {
-            self::$_db->exec(
+        if ($this->_type === 'oci') {
+            $this->_db->exec(
                 'declare
                     already_exists  exception;
                     columns_indexed exception;
                     pragma exception_init( already_exists, -955 );
                     pragma exception_init(columns_indexed, -1408);
                 begin
-                    execute immediate \'create index "comment_parent" on "' . self::_sanitizeIdentifier('comment') . '" ("pasteid")\';
+                    execute immediate \'create index "comment_parent" on "' . $this->_sanitizeIdentifier('comment') . '" ("pasteid")\';
                 exception
                     when already_exists or columns_indexed then
                     NULL;
@@ -838,10 +797,10 @@ class Database extends AbstractData
             );
         } else {
             // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
-            self::$_db->exec(
+            $this->_db->exec(
                 'CREATE INDEX "' .
-                self::_sanitizeIdentifier('comment_parent') . '" ON "' .
-                self::_sanitizeIdentifier('comment') . '" ("pasteid")'
+                $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
+                $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
             );
         }
     }
@@ -850,19 +809,18 @@ class Database extends AbstractData
      * create the paste table
      *
      * @access private
-     * @static
      */
-    private static function _createConfigTable()
+    private function _createConfigTable()
     {
-        list($main_key, $after_key) = self::_getPrimaryKeyClauses('id');
-        $charType                   = self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
-        $textType                   = self::_getMetaType();
-        self::$_db->exec(
-            'CREATE TABLE "' . self::_sanitizeIdentifier('config') .
+        list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
+        $charType                   = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
+        $textType                   = $this->_getMetaType();
+        $this->_db->exec(
+            'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
             "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
         );
-        self::_exec(
-            'INSERT INTO "' . self::_sanitizeIdentifier('config') .
+        $this->_exec(
+            'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
             '" VALUES(?,?)',
             array('VERSION', Controller::VERSION)
         );
@@ -890,90 +848,88 @@ class Database extends AbstractData
      * sanitizes identifiers
      *
      * @access private
-     * @static
      * @param  string $identifier
      * @return string
      */
-    private static function _sanitizeIdentifier($identifier)
+    private function _sanitizeIdentifier($identifier)
     {
-        return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier);
+        return preg_replace('/[^A-Za-z0-9_]+/', '', $this->_prefix . $identifier);
     }
 
     /**
      * upgrade the database schema from an old version
      *
      * @access private
-     * @static
      * @param  string $oldversion
      */
-    private static function _upgradeDatabase($oldversion)
+    private function _upgradeDatabase($oldversion)
     {
-        $dataType       = self::_getDataType();
-        $attachmentType = self::_getAttachmentType();
+        $dataType       = $this->_getDataType();
+        $attachmentType = $this->_getAttachmentType();
         switch ($oldversion) {
             case '0.21':
                 // create the meta column if necessary (pre 0.21 change)
                 try {
-                    self::$_db->exec(
-                        'SELECT "meta" FROM "' . self::_sanitizeIdentifier('paste') . '" ' .
-                        (self::$_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
+                    $this->_db->exec(
+                        'SELECT "meta" FROM "' . $this->_sanitizeIdentifier('paste') . '" ' .
+                        ($this->_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
                     );
                 } catch (PDOException $e) {
-                    self::$_db->exec('ALTER TABLE "' . self::_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
+                    $this->_db->exec('ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
                 }
                 // SQLite only allows one ALTER statement at a time...
-                self::$_db->exec(
-                    'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+                $this->_db->exec(
+                    'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
                     "\" ADD COLUMN \"attachment\" $attachmentType"
                 );
-                self::$_db->exec(
-                    'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
+                $this->_db->exec(
+                    'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
                 );
                 // SQLite doesn't support MODIFY, but it allows TEXT of similar
                 // size as BLOB, so there is no need to change it there
-                if (self::$_type !== 'sqlite') {
-                    self::$_db->exec(
-                        'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+                if ($this->_type !== 'sqlite') {
+                    $this->_db->exec(
+                        'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
                         "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType"
                     );
-                    self::$_db->exec(
-                        'ALTER TABLE "' . self::_sanitizeIdentifier('comment') .
+                    $this->_db->exec(
+                        'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') .
                         "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " .
                         "MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType"
                     );
                 } else {
-                    self::$_db->exec(
+                    $this->_db->exec(
                         'CREATE UNIQUE INDEX IF NOT EXISTS "' .
-                        self::_sanitizeIdentifier('paste_dataid') . '" ON "' .
-                        self::_sanitizeIdentifier('paste') . '" ("dataid")'
+                        $this->_sanitizeIdentifier('paste_dataid') . '" ON "' .
+                        $this->_sanitizeIdentifier('paste') . '" ("dataid")'
                     );
-                    self::$_db->exec(
+                    $this->_db->exec(
                         'CREATE UNIQUE INDEX IF NOT EXISTS "' .
-                        self::_sanitizeIdentifier('comment_dataid') . '" ON "' .
-                        self::_sanitizeIdentifier('comment') . '" ("dataid")'
+                        $this->_sanitizeIdentifier('comment_dataid') . '" ON "' .
+                        $this->_sanitizeIdentifier('comment') . '" ("dataid")'
                     );
                 }
                 // CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
-                self::$_db->exec(
+                $this->_db->exec(
                     'CREATE INDEX "' .
-                    self::_sanitizeIdentifier('comment_parent') . '" ON "' .
-                    self::_sanitizeIdentifier('comment') . '" ("pasteid")'
+                    $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
+                    $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
                 );
                 // no break, continue with updates for 0.22 and later
             case '1.3':
                 // SQLite doesn't support MODIFY, but it allows TEXT of similar
                 // size as BLOB and PostgreSQL uses TEXT, so there is no need
                 // to change it there
-                if (self::$_type !== 'sqlite' && self::$_type !== 'pgsql') {
-                    self::$_db->exec(
-                        'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+                if ($this->_type !== 'sqlite' && $this->_type !== 'pgsql') {
+                    $this->_db->exec(
+                        'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
                         "\" MODIFY COLUMN \"data\" $attachmentType"
                     );
                 }
                 // no break, continue with updates for all newer versions
             default:
-                self::_exec(
-                    'UPDATE "' . self::_sanitizeIdentifier('config') .
+                $this->_exec(
+                    'UPDATE "' . $this->_sanitizeIdentifier('config') .
                     '" SET "value" = ? WHERE "id" = ?',
                     array(Controller::VERSION, 'VERSION')
                 );

+ 89 - 135
lib/Data/Filesystem.php

@@ -40,33 +40,26 @@ class Filesystem extends AbstractData
      * path in which to persist something
      *
      * @access private
-     * @static
      * @var    string
      */
-    private static $_path = 'data';
+    private $_path = 'data';
 
     /**
-     * get instance of singleton
+     * instantiates a new Filesystem data backend
      *
      * @access public
-     * @static
      * @param  array $options
-     * @return Filesystem
+     * @return
      */
-    public static function getInstance(array $options)
+    public function __construct(array $options)
     {
-        // if needed initialize the singleton
-        if (!(self::$_instance instanceof self)) {
-            self::$_instance = new self;
-        }
         // if given update the data directory
         if (
             is_array($options) &&
             array_key_exists('dir', $options)
         ) {
-            self::$_path = $options['dir'];
+            $this->_path = $options['dir'];
         }
-        return self::$_instance;
     }
 
     /**
@@ -79,7 +72,7 @@ class Filesystem extends AbstractData
      */
     public function create($pasteid, array $paste)
     {
-        $storagedir = self::_dataid2path($pasteid);
+        $storagedir = $this->_dataid2path($pasteid);
         $file       = $storagedir . $pasteid . '.php';
         if (is_file($file)) {
             return false;
@@ -87,7 +80,7 @@ class Filesystem extends AbstractData
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return self::_store($file, $paste);
+        return $this->_store($file, $paste);
     }
 
     /**
@@ -101,7 +94,7 @@ class Filesystem extends AbstractData
     {
         if (
             !$this->exists($pasteid) ||
-            !$paste = self::_get(self::_dataid2path($pasteid) . $pasteid . '.php')
+            !$paste = $this->_get($this->_dataid2path($pasteid) . $pasteid . '.php')
         ) {
             return false;
         }
@@ -116,7 +109,7 @@ class Filesystem extends AbstractData
      */
     public function delete($pasteid)
     {
-        $pastedir = self::_dataid2path($pasteid);
+        $pastedir = $this->_dataid2path($pasteid);
         if (is_dir($pastedir)) {
             // Delete the paste itself.
             if (is_file($pastedir . $pasteid . '.php')) {
@@ -124,7 +117,7 @@ class Filesystem extends AbstractData
             }
 
             // Delete discussion if it exists.
-            $discdir = self::_dataid2discussionpath($pasteid);
+            $discdir = $this->_dataid2discussionpath($pasteid);
             if (is_dir($discdir)) {
                 // Delete all files in discussion directory
                 $dir = dir($discdir);
@@ -148,20 +141,20 @@ class Filesystem extends AbstractData
      */
     public function exists($pasteid)
     {
-        $basePath  = self::_dataid2path($pasteid) . $pasteid;
+        $basePath  = $this->_dataid2path($pasteid) . $pasteid;
         $pastePath = $basePath . '.php';
         // convert to PHP protected files if needed
         if (is_readable($basePath)) {
-            self::_prependRename($basePath, $pastePath);
+            $this->_prependRename($basePath, $pastePath);
 
             // convert comments, too
-            $discdir = self::_dataid2discussionpath($pasteid);
+            $discdir = $this->_dataid2discussionpath($pasteid);
             if (is_dir($discdir)) {
                 $dir = dir($discdir);
                 while (false !== ($filename = $dir->read())) {
                     if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) {
                         $commentFilename = $discdir . $filename . '.php';
-                        self::_prependRename($discdir . $filename, $commentFilename);
+                        $this->_prependRename($discdir . $filename, $commentFilename);
                     }
                 }
                 $dir->close();
@@ -182,7 +175,7 @@ class Filesystem extends AbstractData
      */
     public function createComment($pasteid, $parentid, $commentid, array $comment)
     {
-        $storagedir = self::_dataid2discussionpath($pasteid);
+        $storagedir = $this->_dataid2discussionpath($pasteid);
         $file       = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php';
         if (is_file($file)) {
             return false;
@@ -190,7 +183,7 @@ class Filesystem extends AbstractData
         if (!is_dir($storagedir)) {
             mkdir($storagedir, 0700, true);
         }
-        return self::_store($file, $comment);
+        return $this->_store($file, $comment);
     }
 
     /**
@@ -203,7 +196,7 @@ class Filesystem extends AbstractData
     public function readComments($pasteid)
     {
         $comments = array();
-        $discdir  = self::_dataid2discussionpath($pasteid);
+        $discdir  = $this->_dataid2discussionpath($pasteid);
         if (is_dir($discdir)) {
             $dir = dir($discdir);
             while (false !== ($filename = $dir->read())) {
@@ -212,7 +205,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 = self::_get($discdir . $filename);
+                    $comment = $this->_get($discdir . $filename);
                     $items   = explode('.', $filename);
                     // Add some meta information not contained in file.
                     $comment['id']       = $items[1];
@@ -243,7 +236,7 @@ class Filesystem extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     {
         return is_file(
-            self::_dataid2discussionpath($pasteid) .
+            $this->_dataid2discussionpath($pasteid) .
             $pasteid . '.' . $commentid . '.' . $parentid . '.php'
         );
     }
@@ -261,20 +254,20 @@ class Filesystem extends AbstractData
     {
         switch ($namespace) {
             case 'purge_limiter':
-                return self::_storeString(
-                    self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
+                return $this->_storeString(
+                    $this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
                     '<?php' . PHP_EOL . '$GLOBALS[\'purge_limiter\'] = ' . $value . ';'
                 );
             case 'salt':
-                return self::_storeString(
-                    self::$_path . DIRECTORY_SEPARATOR . 'salt.php',
+                return $this->_storeString(
+                    $this->_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) . ';'
+                $this->_last_cache[$key] = $value;
+                return $this->_storeString(
+                    $this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
+                    '<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export($this->_last_cache, true) . ';'
                 );
         }
         return false;
@@ -292,14 +285,14 @@ class Filesystem extends AbstractData
     {
         switch ($namespace) {
             case 'purge_limiter':
-                $file = self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
+                $file = $this->_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';
+                $file = $this->_path . DIRECTORY_SEPARATOR . 'salt.php';
                 if (is_readable($file)) {
                     $items = explode('|', file_get_contents($file));
                     if (is_array($items) && count($items) == 3) {
@@ -308,12 +301,12 @@ class Filesystem extends AbstractData
                 }
                 break;
             case 'traffic_limiter':
-                $file = self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
+                $file = $this->_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];
+                    $this->_last_cache = $GLOBALS['traffic_limiter'];
+                    if (array_key_exists($key, $this->_last_cache)) {
+                        return $this->_last_cache[$key];
                     }
                 }
                 break;
@@ -325,11 +318,10 @@ class Filesystem extends AbstractData
      * get the data
      *
      * @access public
-     * @static
      * @param  string $filename
      * @return array|false $data
      */
-    private static function _get($filename)
+    private function _get($filename)
     {
         return Json::decode(
             substr(
@@ -348,63 +340,25 @@ class Filesystem extends AbstractData
      */
     protected function _getExpiredPastes($batchsize)
     {
-        $pastes     = array();
-        $firstLevel = array_filter(
-            scandir(self::$_path),
-            'PrivateBin\Data\Filesystem::_isFirstLevelDir'
-        );
-        if (count($firstLevel) > 0) {
-            // try at most 10 times the $batchsize pastes before giving up
-            for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
-                $firstKey    = array_rand($firstLevel);
-                $secondLevel = array_filter(
-                    scandir(self::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
-                    'PrivateBin\Data\Filesystem::_isSecondLevelDir'
-                );
-
-                // skip this folder in the next checks if it is empty
-                if (count($secondLevel) == 0) {
-                    unset($firstLevel[$firstKey]);
-                    continue;
-                }
-
-                $secondKey = array_rand($secondLevel);
-                $path      = self::$_path . DIRECTORY_SEPARATOR .
-                    $firstLevel[$firstKey] . DIRECTORY_SEPARATOR .
-                    $secondLevel[$secondKey];
-                if (!is_dir($path)) {
-                    continue;
-                }
-                $thirdLevel = array_filter(
-                    array_map(
-                        function ($filename) {
-                            return strlen($filename) >= 20 ?
-                                substr($filename, 0, -4) :
-                                $filename;
-                        },
-                        scandir($path)
-                    ),
-                    'PrivateBin\\Model\\Paste::isValidId'
-                );
-                if (count($thirdLevel) == 0) {
-                    continue;
-                }
-                $thirdKey = array_rand($thirdLevel);
-                $pasteid  = $thirdLevel[$thirdKey];
-                if (in_array($pasteid, $pastes)) {
-                    continue;
-                }
-
-                if ($this->exists($pasteid)) {
-                    $data = $this->read($pasteid);
-                    if (
-                        array_key_exists('expire_date', $data['meta']) &&
-                        $data['meta']['expire_date'] < time()
-                    ) {
-                        $pastes[] = $pasteid;
-                        if (count($pastes) >= $batchsize) {
-                            break;
-                        }
+        $pastes = array();
+        $files  = $this->_getPasteIterator();
+        $count  = 0;
+        $time   = time();
+        foreach ($files as $file) {
+            if ($file->isDir()) {
+                continue;
+            }
+            $pasteid = $file->getBasename('.php');
+            if ($this->exists($pasteid)) {
+                $data = $this->read($pasteid);
+                if (
+                    array_key_exists('expire_date', $data['meta']) &&
+                    $data['meta']['expire_date'] < $time
+                ) {
+                    $pastes[] = $pasteid;
+                    ++$count;
+                    if ($count >= $batchsize) {
+                        break;
                     }
                 }
             }
@@ -412,6 +366,21 @@ class Filesystem extends AbstractData
         return $pastes;
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function getAllPastes()
+    {
+        $pastes = array();
+        $files  = $this->_getPasteIterator();
+        foreach ($files as $file) {
+            if ($file->isFile()) {
+                $pastes[] = $file->getBasename('.php');
+            }
+        }
+        return $pastes;
+    }
+
     /**
      * Convert paste id to storage path.
      *
@@ -423,13 +392,12 @@ class Filesystem extends AbstractData
      * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
      *
      * @access private
-     * @static
      * @param  string $dataid
      * @return string
      */
-    private static function _dataid2path($dataid)
+    private function _dataid2path($dataid)
     {
-        return self::$_path . DIRECTORY_SEPARATOR .
+        return $this->_path . DIRECTORY_SEPARATOR .
             substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
             substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
     }
@@ -440,56 +408,44 @@ class Filesystem extends AbstractData
      * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
      *
      * @access private
-     * @static
      * @param  string $dataid
      * @return string
      */
-    private static function _dataid2discussionpath($dataid)
+    private function _dataid2discussionpath($dataid)
     {
-        return self::_dataid2path($dataid) . $dataid .
+        return $this->_dataid2path($dataid) . $dataid .
             '.discussion' . DIRECTORY_SEPARATOR;
     }
 
     /**
-     * Check that the given element is a valid first level directory.
-     *
-     * @access private
-     * @static
-     * @param  string $element
-     * @return bool
-     */
-    private static function _isFirstLevelDir($element)
-    {
-        return self::_isSecondLevelDir($element) &&
-            is_dir(self::$_path . DIRECTORY_SEPARATOR . $element);
-    }
-
-    /**
-     * Check that the given element is a valid second level directory.
+     * Get an iterator matching paste files.
      *
      * @access private
-     * @static
-     * @param  string $element
-     * @return bool
+     * @return \GlobIterator
      */
-    private static function _isSecondLevelDir($element)
+    private function _getPasteIterator()
     {
-        return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
+        return new \GlobIterator($this->_path . DIRECTORY_SEPARATOR .
+            '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR .
+            '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR .
+            '[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]' .
+            '[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]*');
+        // need to return both files with and without .php suffix, so they can
+        // be hardened by _prependRename(), which is hooked into exists()
     }
 
     /**
      * store the data
      *
      * @access public
-     * @static
      * @param  string $filename
      * @param  array  $data
      * @return bool
      */
-    private static function _store($filename, array $data)
+    private function _store($filename, array $data)
     {
         try {
-            return self::_storeString(
+            return $this->_storeString(
                 $filename,
                 self::PROTECTION_LINE . PHP_EOL . Json::encode($data)
             );
@@ -502,20 +458,19 @@ class Filesystem extends AbstractData
      * store a string
      *
      * @access public
-     * @static
      * @param  string $filename
      * @param  string $data
      * @return bool
      */
-    private static function _storeString($filename, $data)
+    private function _storeString($filename, $data)
     {
         // Create storage directory if it does not exist.
-        if (!is_dir(self::$_path)) {
-            if (!@mkdir(self::$_path, 0700)) {
+        if (!is_dir($this->_path)) {
+            if (!@mkdir($this->_path, 0700)) {
                 return false;
             }
         }
-        $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
+        $file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
         if (!is_file($file)) {
             $writtenBytes = 0;
             if ($fileCreated = @touch($file)) {
@@ -553,12 +508,11 @@ class Filesystem extends AbstractData
      * 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)
+    private function _prependRename($srcFile, $destFile)
     {
         // don't overwrite already converted file
         if (!is_readable($destFile)) {

+ 61 - 50
lib/Data/GoogleCloudStorage.php

@@ -14,54 +14,43 @@ class GoogleCloudStorage extends AbstractData
      * GCS client
      *
      * @access private
-     * @static
      * @var    StorageClient
      */
-    private static $_client = null;
+    private $_client = null;
 
     /**
      * GCS bucket
      *
      * @access private
-     * @static
      * @var    Bucket
      */
-    private static $_bucket = null;
+    private $_bucket = null;
 
     /**
      * object prefix
      *
      * @access private
-     * @static
      * @var    string
      */
-    private static $_prefix = 'pastes';
+    private $_prefix = 'pastes';
 
     /**
      * bucket acl type
      *
      * @access private
-     * @static
      * @var    bool
      */
-    private static $_uniformacl = false;
+    private $_uniformacl = false;
 
     /**
-     * returns a Google Cloud Storage data backend.
+     * instantiantes a new Google Cloud Storage data backend.
      *
      * @access public
-     * @static
      * @param array $options
-     * @return GoogleCloudStorage
+     * @return
      */
-    public static function getInstance(array $options)
+    public function __construct(array $options)
     {
-        // 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');
         }
@@ -69,24 +58,22 @@ class GoogleCloudStorage extends AbstractData
             $bucket = $options['bucket'];
         }
         if (is_array($options) && array_key_exists('prefix', $options)) {
-            self::$_prefix = $options['prefix'];
+            $this->_prefix = $options['prefix'];
         }
         if (is_array($options) && array_key_exists('uniformacl', $options)) {
-            self::$_uniformacl = $options['uniformacl'];
+            $this->_uniformacl = $options['uniformacl'];
         }
 
-        if (empty(self::$_client)) {
-            self::$_client = class_exists('StorageClientStub', false) ?
-                new \StorageClientStub(array()) :
-                new StorageClient(array('suppressKeyFileNotice' => true));
+        $this->_client = class_exists('StorageClientStub', false) ?
+            new \StorageClientStub(array()) :
+            new StorageClient(array('suppressKeyFileNotice' => true));
+        if (isset($bucket)) {
+            $this->_bucket = $this->_client->bucket($bucket);
         }
-        self::$_bucket = self::$_client->bucket($bucket);
-
-        return self::$_instance;
     }
 
     /**
-     * returns the google storage object key for $pasteid in self::$_bucket.
+     * returns the google storage object key for $pasteid in $this->_bucket.
      *
      * @access private
      * @param $pasteid string to get the key for
@@ -94,14 +81,14 @@ class GoogleCloudStorage extends AbstractData
      */
     private function _getKey($pasteid)
     {
-        if (self::$_prefix != '') {
-            return self::$_prefix . '/' . $pasteid;
+        if ($this->_prefix != '') {
+            return $this->_prefix . '/' . $pasteid;
         }
         return $pasteid;
     }
 
     /**
-     * Uploads the payload in the self::$_bucket under the specified key.
+     * Uploads the payload in the $this->_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.
@@ -126,12 +113,12 @@ class GoogleCloudStorage extends AbstractData
                     'metadata'     => $metadata,
                 ),
             );
-            if (!self::$_uniformacl) {
+            if (!$this->_uniformacl) {
                 $data['predefinedAcl'] = 'private';
             }
-            self::$_bucket->upload(Json::encode($payload), $data);
+            $this->_bucket->upload(Json::encode($payload), $data);
         } catch (Exception $e) {
-            error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' .
+            error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -156,13 +143,13 @@ class GoogleCloudStorage extends AbstractData
     public function read($pasteid)
     {
         try {
-            $o    = self::$_bucket->object($this->_getKey($pasteid));
+            $o    = $this->_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 ' . self::$_bucket->name() . ', ' .
+            error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -176,9 +163,9 @@ class GoogleCloudStorage extends AbstractData
         $name = $this->_getKey($pasteid);
 
         try {
-            foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
+            foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
                 try {
-                    self::$_bucket->object($comment->name())->delete();
+                    $this->_bucket->object($comment->name())->delete();
                 } catch (NotFoundException $e) {
                     // ignore if already deleted.
                 }
@@ -188,7 +175,7 @@ class GoogleCloudStorage extends AbstractData
         }
 
         try {
-            self::$_bucket->object($name)->delete();
+            $this->_bucket->object($name)->delete();
         } catch (NotFoundException $e) {
             // ignore if already deleted
         }
@@ -199,7 +186,7 @@ class GoogleCloudStorage extends AbstractData
      */
     public function exists($pasteid)
     {
-        $o = self::$_bucket->object($this->_getKey($pasteid));
+        $o = $this->_bucket->object($this->_getKey($pasteid));
         return $o->exists();
     }
 
@@ -223,8 +210,8 @@ class GoogleCloudStorage extends AbstractData
         $comments = array();
         $prefix   = $this->_getKey($pasteid) . '/discussion/';
         try {
-            foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) {
-                $comment         = JSON::decode(self::$_bucket->object($key->name())->downloadAsString());
+            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
+                $comment         = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
                 $comment['id']   = basename($key->name());
                 $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
                 $comments[$slot] = $comment;
@@ -241,7 +228,7 @@ class GoogleCloudStorage extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     {
         $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
-        $o    = self::$_bucket->object($name);
+        $o    = $this->_bucket->object($name);
         return $o->exists();
     }
 
@@ -252,7 +239,7 @@ class GoogleCloudStorage extends AbstractData
     {
         $path = 'config/' . $namespace;
         try {
-            foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) {
+            foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
                 $name = $object->name();
                 if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
                     continue;
@@ -300,12 +287,12 @@ class GoogleCloudStorage extends AbstractData
                     'metadata'     => $metadata,
                 ),
             );
-            if (!self::$_uniformacl) {
+            if (!$this->_uniformacl) {
                 $data['predefinedAcl'] = 'private';
             }
-            self::$_bucket->upload($value, $data);
+            $this->_bucket->upload($value, $data);
         } catch (Exception $e) {
-            error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' .
+            error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -323,7 +310,7 @@ class GoogleCloudStorage extends AbstractData
             $key = 'config/' . $namespace . '/' . $key;
         }
         try {
-            $o = self::$_bucket->object($key);
+            $o = $this->_bucket->object($key);
             return $o->downloadAsString();
         } catch (NotFoundException $e) {
             return '';
@@ -338,12 +325,12 @@ class GoogleCloudStorage extends AbstractData
         $expired = array();
 
         $now    = time();
-        $prefix = self::$_prefix;
+        $prefix = $this->_prefix;
         if ($prefix != '') {
             $prefix .= '/';
         }
         try {
-            foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) {
+            foreach ($this->_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']);
@@ -361,4 +348,28 @@ class GoogleCloudStorage extends AbstractData
         }
         return $expired;
     }
+
+    /**
+     * @inheritDoc
+     */
+    public function getAllPastes()
+    {
+        $pastes = array();
+        $prefix = $this->_prefix;
+        if ($prefix != '') {
+            $prefix .= '/';
+        }
+
+        try {
+            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
+                $candidate = substr($object->name(), strlen($prefix));
+                if (strpos($candidate, '/') === false) {
+                    $pastes[] = $candidate;
+                }
+            }
+        } catch (NotFoundException $e) {
+            // no objects in the bucket yet
+        }
+        return $pastes;
+    }
 }

+ 80 - 71
lib/Data/S3Storage.php

@@ -45,86 +45,71 @@ class S3Storage extends AbstractData
      * S3 client
      *
      * @access private
-     * @static
      * @var    S3Client
      */
-    private static $_client = null;
+    private $_client = null;
 
     /**
      * S3 client options
      *
      * @access private
-     * @static
      * @var    array
      */
-    private static $_options = array();
+    private $_options = array();
 
     /**
      * S3 bucket
      *
      * @access private
-     * @static
      * @var    string
      */
-    private static $_bucket = null;
+    private $_bucket = null;
 
     /**
      * S3 prefix for all PrivateBin data in this bucket
      *
      * @access private
-     * @static
      * @var    string
      */
-    private static $_prefix = '';
+    private $_prefix = '';
 
     /**
-     * returns an S3 data backend.
+     * instantiates a new S3 data backend.
      *
      * @access public
-     * @static
      * @param array $options
-     * @return S3Storage
+     * @return
      */
-    public static function getInstance(array $options)
+    public function __construct(array $options)
     {
-        // if needed initialize the singleton
-        if (!(self::$_instance instanceof self)) {
-            self::$_instance = new self;
-        }
-
-        self::$_options                = array();
-        self::$_options['credentials'] = array();
+        $this->_options['credentials'] = array();
 
         if (is_array($options) && array_key_exists('region', $options)) {
-            self::$_options['region'] = $options['region'];
+            $this->_options['region'] = $options['region'];
         }
         if (is_array($options) && array_key_exists('version', $options)) {
-            self::$_options['version'] = $options['version'];
+            $this->_options['version'] = $options['version'];
         }
         if (is_array($options) && array_key_exists('endpoint', $options)) {
-            self::$_options['endpoint'] = $options['endpoint'];
+            $this->_options['endpoint'] = $options['endpoint'];
         }
         if (is_array($options) && array_key_exists('accesskey', $options)) {
-            self::$_options['credentials']['key'] = $options['accesskey'];
+            $this->_options['credentials']['key'] = $options['accesskey'];
         }
         if (is_array($options) && array_key_exists('secretkey', $options)) {
-            self::$_options['credentials']['secret'] = $options['secretkey'];
+            $this->_options['credentials']['secret'] = $options['secretkey'];
         }
         if (is_array($options) && array_key_exists('use_path_style_endpoint', $options)) {
-            self::$_options['use_path_style_endpoint'] = filter_var($options['use_path_style_endpoint'], FILTER_VALIDATE_BOOLEAN);
+            $this->_options['use_path_style_endpoint'] = filter_var($options['use_path_style_endpoint'], FILTER_VALIDATE_BOOLEAN);
         }
         if (is_array($options) && array_key_exists('bucket', $options)) {
-            self::$_bucket = $options['bucket'];
+            $this->_bucket = $options['bucket'];
         }
         if (is_array($options) && array_key_exists('prefix', $options)) {
-            self::$_prefix = $options['prefix'];
-        }
-
-        if (empty(self::$_client)) {
-            self::$_client = new S3Client(self::$_options);
+            $this->_prefix = $options['prefix'];
         }
 
-        return self::$_instance;
+        $this->_client = new S3Client($this->_options);
     }
 
     /**
@@ -138,12 +123,12 @@ class S3Storage extends AbstractData
     {
         $allObjects = array();
         $options    = array(
-            'Bucket' => self::$_bucket,
+            'Bucket' => $this->_bucket,
             'Prefix' => $prefix,
         );
 
         do {
-            $objectsListResponse = self::$_client->listObjects($options);
+            $objectsListResponse = $this->_client->listObjects($options);
             $objects             = $objectsListResponse['Contents'] ?? array();
             foreach ($objects as $object) {
                 $allObjects[]      = $object;
@@ -155,7 +140,7 @@ class S3Storage extends AbstractData
     }
 
     /**
-     * returns the S3 storage object key for $pasteid in self::$_bucket.
+     * returns the S3 storage object key for $pasteid in $this->_bucket.
      *
      * @access private
      * @param $pasteid string to get the key for
@@ -163,14 +148,14 @@ class S3Storage extends AbstractData
      */
     private function _getKey($pasteid)
     {
-        if (self::$_prefix != '') {
-            return self::$_prefix . '/' . $pasteid;
+        if ($this->_prefix != '') {
+            return $this->_prefix . '/' . $pasteid;
         }
         return $pasteid;
     }
 
     /**
-     * Uploads the payload in the self::$_bucket under the specified key.
+     * Uploads the payload in the $this->_bucket under the specified key.
      * The entire payload is stored as a JSON document. The metadata is replicated
      * as the S3 object's metadata except for the fields attachment, attachmentname
      * and salt.
@@ -187,15 +172,15 @@ class S3Storage extends AbstractData
             $metadata[$k] = strval($v);
         }
         try {
-            self::$_client->putObject(array(
-                'Bucket'      => self::$_bucket,
+            $this->_client->putObject(array(
+                'Bucket'      => $this->_bucket,
                 'Key'         => $key,
                 'Body'        => Json::encode($payload),
                 'ContentType' => 'application/json',
                 'Metadata'    => $metadata,
             ));
         } catch (S3Exception $e) {
-            error_log('failed to upload ' . $key . ' to ' . self::$_bucket . ', ' .
+            error_log('failed to upload ' . $key . ' to ' . $this->_bucket . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -220,14 +205,14 @@ class S3Storage extends AbstractData
     public function read($pasteid)
     {
         try {
-            $object = self::$_client->getObject(array(
-                'Bucket' => self::$_bucket,
+            $object = $this->_client->getObject(array(
+                'Bucket' => $this->_bucket,
                 'Key'    => $this->_getKey($pasteid),
             ));
             $data = $object['Body']->getContents();
             return Json::decode($data);
         } catch (S3Exception $e) {
-            error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket . ', ' .
+            error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -244,8 +229,8 @@ class S3Storage extends AbstractData
             $comments = $this->_listAllObjects($name . '/discussion/');
             foreach ($comments as $comment) {
                 try {
-                    self::$_client->deleteObject(array(
-                        'Bucket' => self::$_bucket,
+                    $this->_client->deleteObject(array(
+                        'Bucket' => $this->_bucket,
                         'Key'    => $comment['Key'],
                     ));
                 } catch (S3Exception $e) {
@@ -257,8 +242,8 @@ class S3Storage extends AbstractData
         }
 
         try {
-            self::$_client->deleteObject(array(
-                'Bucket' => self::$_bucket,
+            $this->_client->deleteObject(array(
+                'Bucket' => $this->_bucket,
                 'Key'    => $name,
             ));
         } catch (S3Exception $e) {
@@ -271,7 +256,7 @@ class S3Storage extends AbstractData
      */
     public function exists($pasteid)
     {
-        return self::$_client->doesObjectExistV2(self::$_bucket, $this->_getKey($pasteid));
+        return $this->_client->doesObjectExistV2($this->_bucket, $this->_getKey($pasteid));
     }
 
     /**
@@ -296,8 +281,8 @@ class S3Storage extends AbstractData
         try {
             $entries = $this->_listAllObjects($prefix);
             foreach ($entries as $entry) {
-                $object = self::$_client->getObject(array(
-                    'Bucket' => self::$_bucket,
+                $object = $this->_client->getObject(array(
+                    'Bucket' => $this->_bucket,
                     'Key'    => $entry['Key'],
                 ));
                 $body             = JSON::decode($object['Body']->getContents());
@@ -319,7 +304,7 @@ class S3Storage extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     {
         $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
-        return self::$_client->doesObjectExistV2(self::$_bucket, $name);
+        return $this->_client->doesObjectExistV2($this->_bucket, $name);
     }
 
     /**
@@ -327,7 +312,7 @@ class S3Storage extends AbstractData
      */
     public function purgeValues($namespace, $time)
     {
-        $path = self::$_prefix;
+        $path = $this->_prefix;
         if ($path != '') {
             $path .= '/';
         }
@@ -339,16 +324,16 @@ class S3Storage extends AbstractData
                 if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
                     continue;
                 }
-                $head = self::$_client->headObject(array(
-                    'Bucket' => self::$_bucket,
+                $head = $this->_client->headObject(array(
+                    'Bucket' => $this->_bucket,
                     'Key'    => $name,
                 ));
-                if (array_key_exists('Metadata', $head) && array_key_exists('value', $head['Metadata'])) {
-                    $value = $head['Metadata']['value'];
+                if ($head->get('Metadata') != null && array_key_exists('value', $head->get('Metadata'))) {
+                    $value = $head->get('Metadata')['value'];
                     if (is_numeric($value) && intval($value) < $time) {
                         try {
-                            self::$_client->deleteObject(array(
-                                'Bucket' => self::$_bucket,
+                            $this->_client->deleteObject(array(
+                                'Bucket' => $this->_bucket,
                                 'Key'    => $name,
                             ));
                         } catch (S3Exception $e) {
@@ -369,7 +354,7 @@ class S3Storage extends AbstractData
      */
     public function setValue($value, $namespace, $key = '')
     {
-        $prefix = self::$_prefix;
+        $prefix = $this->_prefix;
         if ($prefix != '') {
             $prefix .= '/';
         }
@@ -385,15 +370,15 @@ class S3Storage extends AbstractData
             $metadata['value'] = strval($value);
         }
         try {
-            self::$_client->putObject(array(
-                'Bucket'      => self::$_bucket,
+            $this->_client->putObject(array(
+                'Bucket'      => $this->_bucket,
                 'Key'         => $key,
                 'Body'        => $value,
                 'ContentType' => 'application/json',
                 'Metadata'    => $metadata,
             ));
         } catch (S3Exception $e) {
-            error_log('failed to set key ' . $key . ' to ' . self::$_bucket . ', ' .
+            error_log('failed to set key ' . $key . ' to ' . $this->_bucket . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
             return false;
         }
@@ -405,7 +390,7 @@ class S3Storage extends AbstractData
      */
     public function getValue($namespace, $key = '')
     {
-        $prefix = self::$_prefix;
+        $prefix = $this->_prefix;
         if ($prefix != '') {
             $prefix .= '/';
         }
@@ -417,8 +402,8 @@ class S3Storage extends AbstractData
         }
 
         try {
-            $object = self::$_client->getObject(array(
-                'Bucket' => self::$_bucket,
+            $object = $this->_client->getObject(array(
+                'Bucket' => $this->_bucket,
                 'Key'    => $key,
             ));
             return $object['Body']->getContents();
@@ -434,19 +419,19 @@ class S3Storage extends AbstractData
     {
         $expired = array();
         $now     = time();
-        $prefix  = self::$_prefix;
+        $prefix  = $this->_prefix;
         if ($prefix != '') {
             $prefix .= '/';
         }
 
         try {
             foreach ($this->_listAllObjects($prefix) as $object) {
-                $head = self::$_client->headObject(array(
-                    'Bucket' => self::$_bucket,
+                $head = $this->_client->headObject(array(
+                    'Bucket' => $this->_bucket,
                     'Key'    => $object['Key'],
                 ));
-                if (array_key_exists('Metadata', $head) && array_key_exists('expire_date', $head['Metadata'])) {
-                    $expire_at = intval($head['Metadata']['expire_date']);
+                if ($head->get('Metadata') != null && array_key_exists('expire_date', $head->get('Metadata'))) {
+                    $expire_at = intval($head->get('Metadata')['expire_date']);
                     if ($expire_at != 0 && $expire_at < $now) {
                         array_push($expired, $object['Key']);
                     }
@@ -461,4 +446,28 @@ class S3Storage extends AbstractData
         }
         return $expired;
     }
+
+    /**
+     * @inheritDoc
+     */
+    public function getAllPastes()
+    {
+        $pastes = array();
+        $prefix = $this->_prefix;
+        if ($prefix != '') {
+            $prefix .= '/';
+        }
+
+        try {
+            foreach ($this->_listAllObjects($prefix) as $object) {
+                $candidate = substr($object['Key'], strlen($prefix));
+                if (strpos($candidate, '/') === false) {
+                    $pastes[] = $candidate;
+                }
+            }
+        } catch (S3Exception $e) {
+            // no objects in the bucket yet
+        }
+        return $pastes;
+    }
 }

+ 1 - 0
lib/I18n.php

@@ -328,6 +328,7 @@ class I18n
                 return $n === 1 ? 0 : ($n === 2 ? 1 : (($n < 0 || $n > 10) && ($n % 10 === 0) ? 2 : 3));
             case 'id':
             case 'jbo':
+            case 'th':
                 return 0;
             case 'lt':
                 return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);

+ 2 - 4
lib/Model.php

@@ -81,10 +81,8 @@ class Model
     public function getStore()
     {
         if ($this->_store === null) {
-            $this->_store = forward_static_call(
-                'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model') . '::getInstance',
-                $this->_conf->getSection('model_options')
-            );
+            $class        = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
+            $this->_store = new $class($this->_conf->getSection('model_options'));
         }
         return $this->_store;
     }

+ 4 - 4
lib/Model/Comment.php

@@ -165,7 +165,10 @@ class Comment extends AbstractModel
         if ($icon != 'none') {
             $pngdata = '';
             $hmac    = TrafficLimiter::getHash();
-            if ($icon == 'jdenticon') {
+            if ($icon == 'identicon') {
+                $identicon = new Identicon();
+                $pngdata   = $identicon->getImageDataUri($hmac, 16);
+            } elseif ($icon == 'jdenticon') {
                 $jdenticon = new Jdenticon(array(
                     'hash'  => $hmac,
                     'size'  => 16,
@@ -175,9 +178,6 @@ class Comment extends AbstractModel
                     ),
                 ));
                 $pngdata   = $jdenticon->getImageDataUri('png');
-            } elseif ($icon == 'identicon') {
-                $identicon = new Identicon();
-                $pngdata   = $identicon->getImageDataUri($hmac, 16);
             } elseif ($icon == 'vizhash') {
                 $vh      = new Vizhash16x16();
                 $pngdata = 'data:image/png;base64,' . base64_encode(

+ 4 - 4
tpl/bootstrap.php

@@ -42,7 +42,7 @@ if ($SYNTAXHIGHLIGHTING) :
 endif;
 ?>
 		<noscript><link type="text/css" rel="stylesheet" href="css/noscript.css" /></noscript>
-		<script type="text/javascript" data-cfasync="false" src="js/jquery-3.6.0.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/jquery-3.6.1.js" integrity="sha512-aVKKRRi/Q/YV+4mjoKBsE4x3H+BkegoM/em46NNlCqNTmUYADjBbeNefNxYV7giUp0VxICtqdrbqU7iVaeZNXA==" crossorigin="anonymous"></script>
 <?php
 if ($QRCODE) :
 ?>
@@ -67,13 +67,13 @@ if ($SYNTAXHIGHLIGHTING) :
 endif;
 if ($MARKDOWN) :
 ?>
-		<script type="text/javascript" data-cfasync="false" src="js/showdown-2.0.3.js" integrity="sha512-vcfjvW3UKHD/4vlQx804cqWK88jFmjsWRsZ8/u5YEcyHB1IituxrXDU7TvdqsFVsMnxpE/UIEo25/SYW+puWHw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/showdown-2.1.0.js" integrity="sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==" crossorigin="anonymous"></script>
 <?php
 endif;
 ?>
-		<script type="text/javascript" data-cfasync="false" src="js/purify-2.3.6.js" integrity="sha512-N1GGPjbqLbwK821ZN7C925WuTwU4aDxz2CEEOXQ6/s6m6MBwVj8fh5fugiE2hzsm0xud3q7jpjZQ4ILnpMREYQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/purify-2.4.6.js" integrity="sha512-+jcx+EqNbaFT4OHS86zGwU1SNAAZ7hG2pJlwMpXoe9AvTp37BrXMQ29g2GhdyQHTvYWaNlTQIkWXYM0Lvt8GiQ==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-sQhu+q8ayRj0LO80orHnhEudlTf5Qx+Yhb/+U84ixMvSdwijuLEHGiEuWctqUPOFSe56L6aeq6z7K0ONpbgaew==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-0he/s7UnjLf2pxLH+EIGImaUBhVzpngrL5PId/QB2FcstucrPUPOhQpqJcGvtmVPyuXckiktEwwrz4Sn4Mi3Tw==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png" sizes="32x32" />

+ 4 - 4
tpl/page.php

@@ -21,7 +21,7 @@ if ($SYNTAXHIGHLIGHTING):
     endif;
 endif;
 ?>
-		<script type="text/javascript" data-cfasync="false" src="js/jquery-3.6.0.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/jquery-3.6.1.js" integrity="sha512-aVKKRRi/Q/YV+4mjoKBsE4x3H+BkegoM/em46NNlCqNTmUYADjBbeNefNxYV7giUp0VxICtqdrbqU7iVaeZNXA==" crossorigin="anonymous"></script>
 <?php
 if ($QRCODE):
 ?>
@@ -45,13 +45,13 @@ if ($SYNTAXHIGHLIGHTING):
 endif;
 if ($MARKDOWN):
 ?>
-		<script type="text/javascript" data-cfasync="false" src="js/showdown-2.0.3.js" integrity="sha512-vcfjvW3UKHD/4vlQx804cqWK88jFmjsWRsZ8/u5YEcyHB1IituxrXDU7TvdqsFVsMnxpE/UIEo25/SYW+puWHw==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/showdown-2.1.0.js" integrity="sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==" crossorigin="anonymous"></script>
 <?php
 endif;
 ?>
-		<script type="text/javascript" data-cfasync="false" src="js/purify-2.3.6.js" integrity="sha512-N1GGPjbqLbwK821ZN7C925WuTwU4aDxz2CEEOXQ6/s6m6MBwVj8fh5fugiE2hzsm0xud3q7jpjZQ4ILnpMREYQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/purify-2.4.6.js" integrity="sha512-+jcx+EqNbaFT4OHS86zGwU1SNAAZ7hG2pJlwMpXoe9AvTp37BrXMQ29g2GhdyQHTvYWaNlTQIkWXYM0Lvt8GiQ==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-sQhu+q8ayRj0LO80orHnhEudlTf5Qx+Yhb/+U84ixMvSdwijuLEHGiEuWctqUPOFSe56L6aeq6z7K0ONpbgaew==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-0he/s7UnjLf2pxLH+EIGImaUBhVzpngrL5PId/QB2FcstucrPUPOhQpqJcGvtmVPyuXckiktEwwrz4Sn4Mi3Tw==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />

+ 10 - 10
tst/Bootstrap.php

@@ -32,9 +32,9 @@ Helper::updateSubresourceIntegrity();
  */
 class StorageClientStub extends StorageClient
 {
-    private $_config     = null;
-    private $_connection = null;
-    private $_buckets    = array();
+    private $_config         = null;
+    private $_connection     = null;
+    private static $_buckets = array();
 
     public function __construct(array $config = array())
     {
@@ -44,11 +44,11 @@ class StorageClientStub extends StorageClient
 
     public function bucket($name, $userProject = false)
     {
-        if (!key_exists($name, $this->_buckets)) {
+        if (!key_exists($name, self::$_buckets)) {
             $b                     = new BucketStub($this->_connection, $name, array(), $this);
-            $this->_buckets[$name] = $b;
+            self::$_buckets[$name] = $b;
         }
-        return $this->_buckets[$name];
+        return self::$_buckets[$name];
     }
 
     /**
@@ -56,8 +56,8 @@ class StorageClientStub extends StorageClient
      */
     public function deleteBucket($name)
     {
-        if (key_exists($name, $this->_buckets)) {
-            unset($this->_buckets[$name]);
+        if (key_exists($name, self::$_buckets)) {
+            unset(self::$_buckets[$name]);
         } else {
             throw new NotFoundException();
         }
@@ -110,11 +110,11 @@ class StorageClientStub extends StorageClient
 
     public function createBucket($name, array $options = array())
     {
-        if (key_exists($name, $this->_buckets)) {
+        if (key_exists($name, self::$_buckets)) {
             throw new BadRequestException('already exists');
         }
         $b                     = new BucketStub($this->_connection, $name, array(), $this);
-        $this->_buckets[$name] = $b;
+        self::$_buckets[$name] = $b;
         return $b;
     }
 }

+ 1 - 1
tst/ControllerTest.php

@@ -17,7 +17,7 @@ class ControllerTest extends TestCase
     {
         /* Setup Routine */
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_data  = Filesystem::getInstance(array('dir' => $this->_path));
+        $this->_data  = new Filesystem(array('dir' => $this->_path));
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();

+ 1 - 1
tst/ControllerWithDbTest.php

@@ -25,7 +25,7 @@ class ControllerWithDbTest extends ControllerTest
             mkdir($this->_path);
         }
         $this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3';
-        $this->_data           = Database::getInstance($this->_options);
+        $this->_data           = new Database($this->_options);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();

+ 1 - 1
tst/ControllerWithGcsTest.php

@@ -39,7 +39,7 @@ class ControllerWithGcsTest extends ControllerTest
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
         );
-        $this->_data    = GoogleCloudStorage::getInstance($this->_options);
+        $this->_data    = new GoogleCloudStorage($this->_options);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();

+ 16 - 16
tst/Data/DatabaseTest.php

@@ -23,7 +23,7 @@ class DatabaseTest extends TestCase
     {
         /* Setup Routine */
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_model = Database::getInstance($this->_options);
+        $this->_model = new Database($this->_options);
     }
 
     public function tearDown(): void
@@ -36,7 +36,7 @@ class DatabaseTest extends TestCase
 
     public function testSaltMigration()
     {
-        ServerSalt::setStore(Filesystem::getInstance(array('dir' => 'data')));
+        ServerSalt::setStore(new Filesystem(array('dir' => 'data')));
         $salt = ServerSalt::get();
         $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
         $this->assertFileExists($file, 'ServerSalt got initialized and stored on disk');
@@ -140,7 +140,7 @@ class DatabaseTest extends TestCase
     public function testGetIbmInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'ibm:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -149,7 +149,7 @@ class DatabaseTest extends TestCase
     public function testGetInformixInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'informix:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -158,7 +158,7 @@ class DatabaseTest extends TestCase
     public function testGetMssqlInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'mssql:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -167,7 +167,7 @@ class DatabaseTest extends TestCase
     public function testGetMysqlInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'mysql:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -176,7 +176,7 @@ class DatabaseTest extends TestCase
     public function testGetOciInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'oci:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -185,7 +185,7 @@ class DatabaseTest extends TestCase
     public function testGetPgsqlInstance()
     {
         $this->expectException(PDOException::class);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'pgsql:', 'usr' => null, 'pwd' => null,
             'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
         ));
@@ -195,7 +195,7 @@ class DatabaseTest extends TestCase
     {
         $this->expectException(Exception::class);
         $this->expectExceptionCode(5);
-        Database::getInstance(array(
+        new Database(array(
             'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null,
         ));
     }
@@ -206,7 +206,7 @@ class DatabaseTest extends TestCase
         unset($options['dsn']);
         $this->expectException(Exception::class);
         $this->expectExceptionCode(6);
-        Database::getInstance($options);
+        new Database($options);
     }
 
     public function testMissingUsr()
@@ -215,7 +215,7 @@ class DatabaseTest extends TestCase
         unset($options['usr']);
         $this->expectException(Exception::class);
         $this->expectExceptionCode(6);
-        Database::getInstance($options);
+        new Database($options);
     }
 
     public function testMissingPwd()
@@ -224,7 +224,7 @@ class DatabaseTest extends TestCase
         unset($options['pwd']);
         $this->expectException(Exception::class);
         $this->expectExceptionCode(6);
-        Database::getInstance($options);
+        new Database($options);
     }
 
     public function testMissingOpt()
@@ -233,7 +233,7 @@ class DatabaseTest extends TestCase
         unset($options['opt']);
         $this->expectException(Exception::class);
         $this->expectExceptionCode(6);
-        Database::getInstance($options);
+        new Database($options);
     }
 
     public function testOldAttachments()
@@ -245,7 +245,7 @@ class DatabaseTest extends TestCase
         }
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['tbl'] = 'bar_';
-        $model                 = Database::getInstance($this->_options);
+        $model                 = new Database($this->_options);
 
         $original               = $paste = Helper::getPasteWithAttachment(1, array('expire_date' => 1344803344));
         $meta                   = $paste['meta'];
@@ -290,7 +290,7 @@ class DatabaseTest extends TestCase
         }
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['tbl'] = 'baz_';
-        $model                 = Database::getInstance($this->_options);
+        $model                 = new Database($this->_options);
         $paste                 = Helper::getPaste(1, array('expire_date' => 1344803344));
         unset($paste['meta']['formatter'], $paste['meta']['opendiscussion'], $paste['meta']['salt']);
         $model->delete(Helper::getPasteId());
@@ -357,7 +357,7 @@ class DatabaseTest extends TestCase
             'vizhash BLOB, ' .
             'postdate INT );'
         );
-        $this->assertInstanceOf('PrivateBin\\Data\\Database', Database::getInstance($this->_options));
+        $this->assertInstanceOf('PrivateBin\\Data\\Database', new Database($this->_options));
 
         // check if version number was upgraded in created configuration table
         $statement = $db->prepare('SELECT value FROM foo_config WHERE id LIKE ?');

+ 1 - 1
tst/Data/FilesystemTest.php

@@ -16,7 +16,7 @@ class FilesystemTest extends TestCase
         /* Setup Routine */
         $this->_path        = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
-        $this->_model       = Filesystem::getInstance(array('dir' => $this->_path));
+        $this->_model       = new Filesystem(array('dir' => $this->_path));
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }

+ 1 - 1
tst/Data/GoogleCloudStorageTest.php

@@ -27,7 +27,7 @@ class GoogleCloudStorageTest extends TestCase
     public function setUp(): void
     {
         ini_set('error_log', stream_get_meta_data(tmpfile())['uri']);
-        $this->_model = GoogleCloudStorage::getInstance(array(
+        $this->_model = new GoogleCloudStorage(array(
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
         ));

+ 1 - 1
tst/JsonApiTest.php

@@ -19,7 +19,7 @@ class JsonApiTest extends TestCase
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+        $this->_model = new Filesystem(array('dir' => $this->_path));
         ServerSalt::setStore($this->_model);
 
         $_POST   = array();

+ 82 - 0
tst/MigrateTest.php

@@ -0,0 +1,82 @@
+<?php
+use PHPUnit\Framework\TestCase;
+use PrivateBin\Data\Database;
+use PrivateBin\Data\Filesystem;
+
+class MigrateTest extends TestCase
+{
+    protected $_model_1;
+
+    protected $_model_2;
+
+    protected $_path;
+
+    protected $_path_instance_1;
+
+    protected $_path_instance_2;
+
+    public function setUp(): void
+    {
+        /* Setup Routine */
+        $this->_path            = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_path_instance_1 = $this->_path . DIRECTORY_SEPARATOR . 'instance_1';
+        $this->_path_instance_2 = $this->_path . DIRECTORY_SEPARATOR . 'instance_2';
+        if (!is_dir($this->_path)) {
+            mkdir($this->_path);
+        }
+        mkdir($this->_path_instance_1);
+        mkdir($this->_path_instance_1 . DIRECTORY_SEPARATOR . 'cfg');
+        mkdir($this->_path_instance_2);
+        mkdir($this->_path_instance_2 . DIRECTORY_SEPARATOR . 'cfg');
+        $options                         = parse_ini_file(CONF_SAMPLE, true);
+        $options['purge']['limit']       = 0;
+        $options['model_options']['dir'] = $this->_path_instance_1 . DIRECTORY_SEPARATOR . 'data';
+        $this->_model_1                  = new Filesystem($options['model_options']);
+        Helper::createIniFile($this->_path_instance_1 . DIRECTORY_SEPARATOR . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php', $options);
+
+        $options['model']          = array(
+            'class' => 'Database',
+        );
+        $options['model_options'] = array(
+            'dsn' => 'sqlite:' . $this->_path_instance_2 . DIRECTORY_SEPARATOR . 'test.sq3',
+            'usr' => null,
+            'pwd' => null,
+            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+        );
+        $this->_model_2 = new Database($options['model_options']);
+        Helper::createIniFile($this->_path_instance_2 . DIRECTORY_SEPARATOR . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php', $options);
+    }
+
+    public function tearDown(): void
+    {
+        /* Tear Down Routine */
+        Helper::rmDir($this->_path);
+    }
+
+    public function testMigrate()
+    {
+        $this->_model_1->delete(Helper::getPasteId());
+        $this->_model_2->delete(Helper::getPasteId());
+
+        // storing paste & comment
+        $this->_model_1->create(Helper::getPasteId(), Helper::getPaste());
+        $this->_model_1->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment());
+
+        // migrate files to database
+        $output    = null;
+        $exit_code = 255;
+        exec('php ' . PATH . 'bin' . DIRECTORY_SEPARATOR . 'migrate --delete-after ' . $this->_path_instance_1 . DIRECTORY_SEPARATOR . 'cfg ' . $this->_path_instance_2 . DIRECTORY_SEPARATOR . 'cfg', $output, $exit_code);
+        $this->assertEquals(0, $exit_code, 'migrate script exits 0');
+        $this->assertFalse($this->_model_1->exists(Helper::getPasteId()), 'paste removed after migrating it');
+        $this->assertFalse($this->_model_1->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment removed after migrating it');
+        $this->assertTrue($this->_model_2->exists(Helper::getPasteId()), 'paste migrated');
+        $this->assertTrue($this->_model_2->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment migrated');
+
+        // migrate back to files
+        $exit_code = 255;
+        exec('php ' . PATH . 'bin' . DIRECTORY_SEPARATOR . 'migrate ' . $this->_path_instance_2 . DIRECTORY_SEPARATOR . 'cfg ' . $this->_path_instance_1 . DIRECTORY_SEPARATOR . 'cfg', $output, $exit_code);
+        $this->assertEquals(0, $exit_code, 'migrate script exits 0');
+        $this->assertTrue($this->_model_1->exists(Helper::getPasteId()), 'paste migrated back');
+        $this->assertTrue($this->_model_1->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment migrated back');
+    }
+}

+ 11 - 17
tst/ModelTest.php

@@ -1,6 +1,6 @@
 <?php
 
-use Jdenticon\Identicon;
+use Identicon\Identicon;
 use PHPUnit\Framework\TestCase;
 use PrivateBin\Configuration;
 use PrivateBin\Data\Database;
@@ -39,7 +39,7 @@ class ModelTest extends TestCase
         );
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
-        ServerSalt::setStore(Database::getInstance($options['model_options']));
+        ServerSalt::setStore(new Database($options['model_options']));
         $this->_conf            = new Configuration;
         $this->_model           = new Model($this->_conf);
         $_SERVER['REMOTE_ADDR'] = '::1';
@@ -157,10 +157,10 @@ class ModelTest extends TestCase
 
     public function testCommentDefaults()
     {
+        $class   = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
         $comment = new Comment(
             $this->_conf,
-            forward_static_call(
-                'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model') . '::getInstance',
+            new $class(
                 $this->_conf->getSection('model_options')
             )
         );
@@ -252,7 +252,10 @@ class ModelTest extends TestCase
         $paste = $model->getPaste();
         $paste->setData($pasteData);
         $paste->store();
-        $paste->exists();
+        $this->assertTrue($paste->exists(), 'paste exists before creating comment');
+
+        $comment = $paste->getComment(Helper::getPasteId());
+        $comment->setData($commentData);
 
         $db = new PDO(
             $options['model_options']['dsn'],
@@ -264,8 +267,6 @@ class ModelTest extends TestCase
         $statement->execute();
         $statement->closeCursor();
 
-        $comment = $paste->getComment(Helper::getPasteId());
-        $comment->setData($commentData);
         $this->expectException(Exception::class);
         $this->expectExceptionCode(70);
         $comment->store();
@@ -307,15 +308,8 @@ class ModelTest extends TestCase
         $comment->get();
         $comment->store();
 
-        $identicon = new Identicon(array(
-            'hash'  => TrafficLimiter::getHash(),
-            'size'  => 16,
-            'style' => array(
-                'backgroundColor'   => '#fff0', // fully transparent, for dark mode
-                'padding'           => 0,
-            ),
-        ));
-        $pngdata   = $identicon->getImageDataUri('png');
+        $identicon = new Identicon();
+        $pngdata   = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
         $comment   = current($this->_model->getPaste(Helper::getPasteId())->get()['comments']);
         $this->assertEquals($pngdata, $comment['meta']['icon'], 'icon gets set');
     }
@@ -428,7 +422,7 @@ class ModelTest extends TestCase
     public function testPurge()
     {
         $conf  = new Configuration;
-        $store = Database::getInstance($conf->getSection('model_options'));
+        $store = new Database($conf->getSection('model_options'));
         $store->delete(Helper::getPasteId());
         $expired = Helper::getPaste(2, array('expire_date' => 1344803344));
         $paste   = Helper::getPaste(2, array('expire_date' => time() + 3600));

+ 1 - 1
tst/Persistence/PurgeLimiterTest.php

@@ -16,7 +16,7 @@ class PurgeLimiterTest extends TestCase
             mkdir($this->_path);
         }
         PurgeLimiter::setStore(
-            Filesystem::getInstance(array('dir' => $this->_path))
+            new Filesystem(array('dir' => $this->_path))
         );
     }
 

+ 9 - 9
tst/Persistence/ServerSaltTest.php

@@ -22,7 +22,7 @@ class ServerSaltTest extends TestCase
             mkdir($this->_path);
         }
         ServerSalt::setStore(
-            Filesystem::getInstance(array('dir' => $this->_path))
+            new Filesystem(array('dir' => $this->_path))
         );
 
         $this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo';
@@ -45,17 +45,17 @@ class ServerSaltTest extends TestCase
     {
         // generating new salt
         ServerSalt::setStore(
-            Filesystem::getInstance(array('dir' => $this->_path))
+            new Filesystem(array('dir' => $this->_path))
         );
         $salt = ServerSalt::get();
 
         // try setting a different path and resetting it
         ServerSalt::setStore(
-            Filesystem::getInstance(array('dir' => $this->_otherPath))
+            new Filesystem(array('dir' => $this->_otherPath))
         );
         $this->assertNotEquals($salt, ServerSalt::get());
         ServerSalt::setStore(
-            Filesystem::getInstance(array('dir' => $this->_path))
+            new Filesystem(array('dir' => $this->_path))
         );
         $this->assertEquals($salt, ServerSalt::get());
     }
@@ -64,7 +64,7 @@ class ServerSaltTest extends TestCase
     {
         // try setting an invalid path
         chmod($this->_invalidPath, 0000);
-        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(array('dir' => $this->_invalidPath));
         ServerSalt::setStore($store);
         $salt = ServerSalt::get();
         ServerSalt::setStore($store);
@@ -77,7 +77,7 @@ class ServerSaltTest extends TestCase
         chmod($this->_invalidPath, 0700);
         file_put_contents($this->_invalidFile, '');
         chmod($this->_invalidFile, 0000);
-        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(array('dir' => $this->_invalidPath));
         ServerSalt::setStore($store);
         $salt = ServerSalt::get();
         ServerSalt::setStore($store);
@@ -94,7 +94,7 @@ class ServerSaltTest extends TestCase
         }
         file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . '.htaccess', '');
         chmod($this->_invalidPath, 0500);
-        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(array('dir' => $this->_invalidPath));
         ServerSalt::setStore($store);
         $salt = ServerSalt::get();
         ServerSalt::setStore($store);
@@ -106,9 +106,9 @@ class ServerSaltTest extends TestCase
         // try creating an invalid path
         chmod($this->_invalidPath, 0000);
         ServerSalt::setStore(
-            Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
+            new Filesystem(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
         );
-        $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(array('dir' => $this->_invalidPath));
         ServerSalt::setStore($store);
         $salt = ServerSalt::get();
         ServerSalt::setStore($store);

+ 1 - 1
tst/Persistence/TrafficLimiterTest.php

@@ -13,7 +13,7 @@ class TrafficLimiterTest extends TestCase
     {
         /* Setup Routine */
         $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit';
-        $store       = Filesystem::getInstance(array('dir' => $this->_path));
+        $store       = new Filesystem(array('dir' => $this->_path));
         ServerSalt::setStore($store);
         TrafficLimiter::setStore($store);
     }

+ 1 - 1
tst/Vizhash16x16Test.php

@@ -19,7 +19,7 @@ class Vizhash16x16Test extends TestCase
             mkdir($this->_path);
         }
         $this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png';
-        ServerSalt::setStore(Filesystem::getInstance(array('dir' => $this->_path)));
+        ServerSalt::setStore(new Filesystem(array('dir' => $this->_path)));
     }
 
     public function tearDown(): void

+ 0 - 1
tst/phpunit.xml

@@ -14,7 +14,6 @@
   </coverage>
   <testsuite name="PrivateBin Test Suite">
     <directory suffix=".php">./</directory>
-    <exclude>ConfigurationTestGenerator.php</exclude>
   </testsuite>
   <logging>
     <testdoxHtml outputFile="log/testdox.html"/>

+ 2 - 2
vendor/composer/installed.php

@@ -5,7 +5,7 @@
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '46013df6201304840c8dab82af63cc9000a6d239',
+        'reference' => 'ba5c859d85244c30711263d61e691217c1bc95e4',
         'name' => 'privatebin/privatebin',
         'dev' => false,
     ),
@@ -43,7 +43,7 @@
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '46013df6201304840c8dab82af63cc9000a6d239',
+            'reference' => 'ba5c859d85244c30711263d61e691217c1bc95e4',
             'dev_requirement' => false,
         ),
         'yzalis/identicon' => array(

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio