Merge branch 'master' into php8

pull/1121/head
El RIDO 2022-11-17 06:04:12 +01:00
commit 46c0fc851c
No known key found for this signature in database
GPG Key ID: 0F5C940A6BD81F92
44 changed files with 1062 additions and 642 deletions

2
.gitattributes vendored
View File

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

View File

@ -1,11 +1,12 @@
# PrivateBin version history # PrivateBin version history
* **1.4.1 (not yet released)** * **1.5 (not yet released)**
* ADDED: Translations for Turkish, Slovak and Greek * ADDED: script for data storage backend migrations (#1012)
* ADDED: Translations for Turkish, Slovak, Greek and Thai
* ADDED: S3 Storage backend (#994) * 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: 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: 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: 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) * FIXED: YOURLS integration via new proxy, storing signature in configuration (#725)

View File

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

View File

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

View File

@ -9,7 +9,9 @@
* DANGER: Too many options/settings and too high max iteration setting may trigger * DANGER: Too many options/settings and too high max iteration setting may trigger
* a fork bomb. Please save your work before executing this script. * 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'); $vd = array('view', 'delete');
$vcd = array('view', 'create', 'delete'); $vcd = array('view', 'create', 'delete');
@ -392,7 +394,7 @@ class ConfigurationTestGenerator
} }
} }
$code .= '}' . PHP_EOL; $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 */ /* Setup Routine */
Helper::confBackup(); Helper::confBackup();
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $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(); $this->reset();
} }

View File

@ -9,6 +9,7 @@ use Identicon\Generator\GdGenerator;
use Identicon\Generator\ImageMagickGenerator; use Identicon\Generator\ImageMagickGenerator;
use Identicon\Generator\SvgGenerator; use Identicon\Generator\SvgGenerator;
use Identicon\Identicon; use Identicon\Identicon;
use Jdenticon\Identicon as Jdenticon;
use PrivateBin\Vizhash16x16; use PrivateBin\Vizhash16x16;
@ -17,7 +18,19 @@ $vizhash = new Vizhash16x16();
$identiconGenerators = array( $identiconGenerators = array(
'identicon GD' => new Identicon(new GdGenerator()), 'identicon GD' => new Identicon(new GdGenerator()),
'identicon ImageMagick' => new Identicon(new ImageMagickGenerator()), '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( $results = array(
'vizhash' => array( 'vizhash' => array(
@ -35,21 +48,30 @@ $results = array(
'identicon SVG' => array( 'identicon SVG' => array(
'lengths' => array(), 'lengths' => array(),
'time' => 0 'time' => 0
) ),
'jdenticon' => array(
'lengths' => array(),
'time' => 0
),
'jdenticon ImageMagick' => array(
'lengths' => array(),
'time' => 0
),
'jdenticon SVG' => array(
'lengths' => array(),
'time' => 0
),
); );
$hmacs = array(); $hmacs = array();
echo 'generate ', ITERATIONS, ' hmacs and pre-populate the result array, so tests wont be slowed down', PHP_EOL; echo 'generate ', ITERATIONS, ' hmacs and pre-populate the result array, so tests wont be slowed down', PHP_EOL;
for ($i = 0; $i < ITERATIONS; ++$i) { for ($i = 0; $i < ITERATIONS; ++$i) {
$hmacs[$i] = hash_hmac('sha512', '127.0.0.1', bin2hex(random_bytes(256))); $hmacs[$i] = hash_hmac('sha512', '127.0.0.1', bin2hex(random_bytes(256)));
$results['vizhash']['lengths'][$i] = 0; foreach (array_keys($results) as $test) {
$results['identicon GD']['lengths'][$i] = 0; $results[$test]['lengths'][$i] = 0;
$results['identicon ImageMagick']['lengths'][$i] = 0; }
$results['identicon SVG']['lengths'][$i] = 0;
} }
echo 'run vizhash tests', PHP_EOL; echo 'run vizhash tests', PHP_EOL;
$start = microtime(true); $start = microtime(true);
foreach ($hmacs as $i => $hmac) { foreach ($hmacs as $i => $hmac) {
@ -60,7 +82,6 @@ foreach ($hmacs as $i => $hmac) {
} }
$results['vizhash']['time'] = microtime(true) - $start; $results['vizhash']['time'] = microtime(true) - $start;
foreach ($identiconGenerators as $key => $identicon) { foreach ($identiconGenerators as $key => $identicon) {
echo 'run ', $key,' tests', PHP_EOL; echo 'run ', $key,' tests', PHP_EOL;
$start = microtime(true); $start = microtime(true);
@ -71,9 +92,35 @@ foreach ($identiconGenerators as $key => $identicon) {
$results[$key]['time'] = microtime(true) - $start; $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) { function format_result_line($generator, $min, $max, $avg, $sec) {
echo str_pad($generator, PADDING_LENGTH, ' '), "\t", echo str_pad($generator, PADDING_LENGTH, ' '), "\t",
str_pad($min, 4, ' ', STR_PAD_LEFT), "\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; echo PHP_EOL;
format_result_line('Generator:', 'min', 'max', 'avg', 'seconds'); 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) { foreach ($results as $generator => $result) {
sort($result['lengths']); sort($result['lengths']);
format_result_line( format_result_line(

199
bin/migrate Executable file
View File

@ -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();
}

View File

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

12
composer.lock generated
View File

@ -370,16 +370,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v4.15.1", "version": "v4.15.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -420,9 +420,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "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", "name": "phar-io/manifest",

193
i18n/th.json Normal file
View File

@ -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"
}

View File

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

2
js/jquery-3.6.0.js vendored

File diff suppressed because one or more lines are too long

2
js/jquery-3.6.1.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -627,7 +627,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
* @prop {string[]} * @prop {string[]}
* @readonly * @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 * 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)); return n === 1 ? 0 : (n === 2 ? 1 : ((n < 0 || n > 10) && (n % 10 === 0) ? 2 : 3));
case 'id': case 'id':
case 'jbo': case 'jbo':
case 'th':
return 0; return 0;
case 'lt': case 'lt':
return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2); return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2);

File diff suppressed because one or more lines are too long

2
js/purify-2.4.6.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -53,7 +53,7 @@ class Configuration
'languagedefault' => '', 'languagedefault' => '',
'urlshortener' => '', 'urlshortener' => '',
'qrcode' => true, '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', '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, 'zerobincompatibility' => false,
'httpwarning' => true, 'httpwarning' => true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ if ($SYNTAXHIGHLIGHTING) :
endif; endif;
?> ?>
<noscript><link type="text/css" rel="stylesheet" href="css/noscript.css" /></noscript> <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 <?php
if ($QRCODE) : if ($QRCODE) :
?> ?>
@ -67,13 +67,13 @@ if ($SYNTAXHIGHLIGHTING) :
endif; endif;
if ($MARKDOWN) : 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 <?php
endif; 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/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 --> <!-- icon -->
<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" /> <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" /> <link rel="icon" type="image/png" href="img/favicon-32x32.png" sizes="32x32" />

View File

@ -21,7 +21,7 @@ if ($SYNTAXHIGHLIGHTING):
endif; endif;
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 <?php
if ($QRCODE): if ($QRCODE):
?> ?>
@ -45,13 +45,13 @@ if ($SYNTAXHIGHLIGHTING):
endif; endif;
if ($MARKDOWN): 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 <?php
endif; 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/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 --> <!-- icon -->
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" /> <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" /> <link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />

View File

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

View File

@ -17,7 +17,7 @@ class ControllerTest extends TestCase
{ {
/* Setup Routine */ /* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $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); ServerSalt::setStore($this->_data);
TrafficLimiter::setStore($this->_data); TrafficLimiter::setStore($this->_data);
$this->reset(); $this->reset();

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ class FilesystemTest extends TestCase
/* Setup Routine */ /* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data'; $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
$this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar'; $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)) { if (!is_dir($this->_path)) {
mkdir($this->_path); mkdir($this->_path);
} }

View File

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

View File

@ -19,7 +19,7 @@ class JsonApiTest extends TestCase
if (!is_dir($this->_path)) { if (!is_dir($this->_path)) {
mkdir($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); ServerSalt::setStore($this->_model);
$_POST = array(); $_POST = array();

82
tst/MigrateTest.php Normal file
View File

@ -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');
}
}

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ class TrafficLimiterTest extends TestCase
{ {
/* Setup Routine */ /* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit'; $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); ServerSalt::setStore($store);
TrafficLimiter::setStore($store); TrafficLimiter::setStore($store);
} }

View File

@ -19,7 +19,7 @@ class Vizhash16x16Test extends TestCase
mkdir($this->_path); mkdir($this->_path);
} }
$this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png'; $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 public function tearDown(): void

View File

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

View File

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