diff --git a/.gitattributes b/.gitattributes
index 60629c04..c01ff779 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,5 @@
+bin/configuration-test-generator export-ignore
+bin/icon-test export-ignore
doc/ export-ignore
tst/ export-ignore
img/browserstack.svg export-ignore
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b6a607d..2f23d523 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,12 @@
# PrivateBin version history
- * **1.4.1 (not yet released)**
- * ADDED: Translations for Turkish, Slovak and Greek
+ * **1.5 (not yet released)**
+ * ADDED: script for data storage backend migrations (#1012)
+ * ADDED: Translations for Turkish, Slovak, Greek and Thai
* ADDED: S3 Storage backend (#994)
- * CHANGED: Switched to Jdenticons as the default for comment icons (#793)
+ * ADDED: Jdenticons as an option for comment icons (#793)
* CHANGED: Avoid `SUPER` privilege for setting the `sql_mode` for MariaDB/MySQL (#919)
- * CHANGED: Upgrading libraries to: zlib 1.2.13
+ * CHANGED: Upgrading libraries to: DOMpurify 2.4.6, jQuery 3.6.1, Showdown 2.1.0 & zlib 1.2.13
* FIXED: Revert to CREATE INDEX without IF NOT EXISTS clauses, to support MySQL (#943)
* FIXED: Apply table prefix to indexes as well, to support multiple instances sharing a single database (#943)
* FIXED: YOURLS integration via new proxy, storing signature in configuration (#725)
diff --git a/CREDITS.md b/CREDITS.md
index 97c11251..7a19c5fd 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -31,6 +31,7 @@
* Austin Huang - Oracle database support
* Felix J. Ogris - S3 Storage backend
* Mounir Idrassi & J. Mozdzen - secure YOURLS integration
+* Felix J. Ogris - script for data backend migrations, dropped singleton behaviour of data backends
## Translations
* Hexalyse - French
@@ -61,3 +62,4 @@
* Emir Ensar Rahmanlar - Turkish
* Stevo984 - Slovak
* Christos Karamolegkos - Greek
+* jaideejung007 - Thai
diff --git a/INSTALL.md b/INSTALL.md
index f35d68bf..dfdb4b6a 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -26,7 +26,7 @@ install and configure PrivateBin on your server. It's available on
- `open_basedir` access to `/dev/urandom`
- mcrypt extension AND `open_basedir` access to `/dev/urandom`
- com_dotnet extension
-- GD extension
+- GD extension (when using identicon or vizhash icons, jdenticon works without it)
- zlib extension
- some disk space or a database supported by [PDO](https://php.net/manual/book.pdo.php)
- ability to create files and folders in the installation directory and the PATH
@@ -38,10 +38,10 @@ install and configure PrivateBin on your server. It's available on
### Changing the Path
In the index.php you can define a different `PATH`. This is useful to secure
-your installation. You can move the configuration, data files, templates and PHP
-libraries (directories cfg, doc, data, lib, tpl, tst and vendor) outside of your
-document root. This new location must still be accessible to your webserver and
-PHP process (see also
+your installation. You can move the utilities, configuration, data files,
+templates and PHP libraries (directories bin, cfg, doc, data, lib, tpl, tst and
+vendor) outside of your document root. This new location must still be
+accessible to your webserver and PHP process (see also
[open_basedir setting](https://secure.php.net/manual/en/ini.core.php#ini.open-basedir)).
> #### PATH Example
diff --git a/tst/ConfigurationTestGenerator.php b/bin/configuration-test-generator
similarity index 97%
rename from tst/ConfigurationTestGenerator.php
rename to bin/configuration-test-generator
index 86713b99..a5db48a8 100755
--- a/tst/ConfigurationTestGenerator.php
+++ b/bin/configuration-test-generator
@@ -9,7 +9,9 @@
* DANGER: Too many options/settings and too high max iteration setting may trigger
* a fork bomb. Please save your work before executing this script.
*/
-include 'Bootstrap.php';
+
+define('PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
+include PATH . 'tst' . DIRECTORY_SEPARATOR . 'Bootstrap.php';
$vd = array('view', 'delete');
$vcd = array('view', 'create', 'delete');
@@ -392,7 +394,7 @@ class ConfigurationTestGenerator
}
}
$code .= '}' . PHP_EOL;
- file_put_contents('ConfigurationCombinationsTest.php', $code);
+ file_put_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'tst' . DIRECTORY_SEPARATOR . 'ConfigurationCombinationsTest.php', $code);
}
/**
@@ -428,7 +430,9 @@ class ConfigurationCombinationsTest extends TestCase
/* Setup Routine */
Helper::confBackup();
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
- $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+ $this->_model = new Filesystem(array('dir' => $this->_path));
+ ServerSalt::setStore($this->_model);
+ TrafficLimiter::setStore($this->_model);
$this->reset();
}
diff --git a/tst/IconTest b/bin/icon-test
similarity index 60%
rename from tst/IconTest
rename to bin/icon-test
index 7794e478..57145e13 100755
--- a/tst/IconTest
+++ b/bin/icon-test
@@ -9,6 +9,7 @@ use Identicon\Generator\GdGenerator;
use Identicon\Generator\ImageMagickGenerator;
use Identicon\Generator\SvgGenerator;
use Identicon\Identicon;
+use Jdenticon\Identicon as Jdenticon;
use PrivateBin\Vizhash16x16;
@@ -17,7 +18,19 @@ $vizhash = new Vizhash16x16();
$identiconGenerators = array(
'identicon GD' => new Identicon(new GdGenerator()),
'identicon ImageMagick' => new Identicon(new ImageMagickGenerator()),
- 'identicon SVG' => new Identicon(new SvgGenerator())
+ 'identicon SVG' => new Identicon(new SvgGenerator()),
+);
+$jdenticon = new Jdenticon(array(
+ 'size' => 16,
+ 'style' => array(
+ 'backgroundColor' => '#fff0', // fully transparent, for dark mode
+ 'padding' => 0,
+ ),
+));
+$jdenticonGenerators = array(
+ 'jdenticon' => 'png',
+ 'jdenticon ImageMagick' => 'png',
+ 'jdenticon SVG' => 'svg',
);
$results = array(
'vizhash' => array(
@@ -35,21 +48,30 @@ $results = array(
'identicon SVG' => array(
'lengths' => array(),
'time' => 0
- )
+ ),
+ 'jdenticon' => array(
+ 'lengths' => array(),
+ 'time' => 0
+ ),
+ 'jdenticon ImageMagick' => array(
+ 'lengths' => array(),
+ 'time' => 0
+ ),
+ 'jdenticon SVG' => array(
+ 'lengths' => array(),
+ 'time' => 0
+ ),
);
$hmacs = array();
echo 'generate ', ITERATIONS, ' hmacs and pre-populate the result array, so tests wont be slowed down', PHP_EOL;
for ($i = 0; $i < ITERATIONS; ++$i) {
$hmacs[$i] = hash_hmac('sha512', '127.0.0.1', bin2hex(random_bytes(256)));
- $results['vizhash']['lengths'][$i] = 0;
- $results['identicon GD']['lengths'][$i] = 0;
- $results['identicon ImageMagick']['lengths'][$i] = 0;
- $results['identicon SVG']['lengths'][$i] = 0;
+ foreach (array_keys($results) as $test) {
+ $results[$test]['lengths'][$i] = 0;
+ }
}
-
-
echo 'run vizhash tests', PHP_EOL;
$start = microtime(true);
foreach ($hmacs as $i => $hmac) {
@@ -60,7 +82,6 @@ foreach ($hmacs as $i => $hmac) {
}
$results['vizhash']['time'] = microtime(true) - $start;
-
foreach ($identiconGenerators as $key => $identicon) {
echo 'run ', $key,' tests', PHP_EOL;
$start = microtime(true);
@@ -71,9 +92,35 @@ foreach ($identiconGenerators as $key => $identicon) {
$results[$key]['time'] = microtime(true) - $start;
}
+foreach ($jdenticonGenerators as $key => $format) {
+ echo 'run ', $key,' tests', PHP_EOL;
+ if ($key === 'jdenticon ImageMagick') {
+ $jdenticon->enableImageMagick = true;
+ } else {
+ $jdenticon->enableImageMagick = false;
+ }
+ $start = microtime(true);
+ foreach ($hmacs as $i => $hmac) {
+ $jdenticon->setHash($hmac);
+ $data = $jdenticon->getImageDataUri($format);
+ $results[$key]['lengths'][$i] = strlen($data);
+ }
+ $results[$key]['time'] = microtime(true) - $start;
+}
-define('PADDING_LENGTH', max(array_map(function ($key) { return strlen($key); }, array_keys($results))) + 1);
+define(
+ 'PADDING_LENGTH',
+ max(
+ array_map(
+ function ($key) {
+ return strlen($key);
+ },
+ array_keys($results)
+ )
+ ) + 1
+);
+
function format_result_line($generator, $min, $max, $avg, $sec) {
echo str_pad($generator, PADDING_LENGTH, ' '), "\t",
str_pad($min, 4, ' ', STR_PAD_LEFT), "\t",
@@ -84,7 +131,10 @@ function format_result_line($generator, $min, $max, $avg, $sec) {
echo PHP_EOL;
format_result_line('Generator:', 'min', 'max', 'avg', 'seconds');
-format_result_line(str_repeat('─', PADDING_LENGTH), str_repeat('─', 4), str_repeat('─', 4), str_repeat('─', 4), str_repeat('─', 7));
+format_result_line(
+ str_repeat('─', PADDING_LENGTH), str_repeat('─', 4), str_repeat('─', 4),
+ str_repeat('─', 4), str_repeat('─', 7)
+);
foreach ($results as $generator => $result) {
sort($result['lengths']);
format_result_line(
diff --git a/bin/migrate b/bin/migrate
new file mode 100755
index 00000000..515a5e80
--- /dev/null
+++ b/bin/migrate
@@ -0,0 +1,199 @@
+#!/usr/bin/env 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
+ []
+ 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
+ use storage backend configration from conf.php found in
+ this directory as source
+ optionally, use storage backend configration from conf.php
+ found in this directory as destination; defaults to:
+ " . PATH . "cfg" . DIRECTORY_SEPARATOR . "conf.php
+");
+ exit();
+}
diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php
index 541bb751..7dca6d48 100644
--- a/cfg/conf.sample.php
+++ b/cfg/conf.sample.php
@@ -70,7 +70,7 @@ languageselection = false
; used to get the IP of a comment poster if the server salt is leaked and a
; SHA512 HMAC rainbow table is generated for all (relevant) IPs.
; Can be set to one these values:
-; "none" / "vizhash" / "identicon" / "jdenticon" (default).
+; "none" / "identicon" (default) / "jdenticon" / "vizhash".
; icon = "none"
; Content Security Policy headers allow a website to restrict what sources are
diff --git a/composer.lock b/composer.lock
index 70e73039..8f5ef476 100644
--- a/composer.lock
+++ b/composer.lock
@@ -370,16 +370,16 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.15.1",
+ "version": "v4.15.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
+ "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
- "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
+ "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"shasum": ""
},
"require": {
@@ -420,9 +420,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
},
- "time": "2022-09-04T07:30:47+00:00"
+ "time": "2022-11-12T15:38:23+00:00"
},
{
"name": "phar-io/manifest",
diff --git a/i18n/th.json b/i18n/th.json
new file mode 100644
index 00000000..d3c955db
--- /dev/null
+++ b/i18n/th.json
@@ -0,0 +1,193 @@
+{
+ "PrivateBin": "PrivateBin",
+ "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s เป็น pastebin ออนไลน์แบบโอเพ่นซอร์สที่มีสไตล์แบบมินิมัลลิสท์ เซิร์ฟเวอร์ไม่สามารถรู้ได้ว่าข้อมูลโค้ดที่มาฝากนั้นเป็นข้อมูลอะไร โดยจะถูก %sเข้ารหัส/ถอดรหัส%s ด้วยกระบวนการ AES จำนวน 256 บิตผ่านเบราว์เซอร์",
+ "More information on the project page.": "ข้อมูลเพิ่มเติม ดูได้ที่หน้าโครงการ",
+ "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 %s (Hit [Ctrl]+[c] to copy)": "การฝากโค้ดของคุณอยู่ที่ %s (กดปุ่ม [Ctrl]+[c] เพื่อคัดลอก)",
+ "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 this FAQ for information to troubleshoot.": "ในกรณีที่ข้อความนี้ยังปรากฎให้เห็นอยู่ กรุณาดูคำถามที่พบบ่อยนี้เพื่อใช้แก้ไขปัญหา",
+ "+++ 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 see this FAQ entry.": "สำหรับข้อมูลเพิ่มเติม กรุณาดูรายการคำถามที่พบบ่อยนี้",
+ "Your browser may require an HTTPS connection to support the WebCrypto API. Try switching to HTTPS.": "เบราว์เซอร์ของคุณอาจต้องใช้การเชื่อมต่อ HTTPS เพื่อสนับสนุน API แบบ WebCrypto ลองเปลี่ยนเป็น HTTPS",
+ "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"
+}
diff --git a/js/common.js b/js/common.js
index 12e4c882..0d1b6738 100644
--- a/js/common.js
+++ b/js/common.js
@@ -10,14 +10,14 @@ global.fs = require('fs');
global.WebCrypto = require('@peculiar/webcrypto').Crypto;
// application libraries to test
-global.$ = global.jQuery = require('./jquery-3.6.0');
+global.$ = global.jQuery = require('./jquery-3.6.1');
global.RawDeflate = require('./rawinflate-0.3').RawDeflate;
global.zlib = require('./zlib-1.2.13').zlib;
require('./prettify');
global.prettyPrint = window.PR.prettyPrint;
global.prettyPrintOne = window.PR.prettyPrintOne;
-global.showdown = require('./showdown-2.0.3');
-global.DOMPurify = require('./purify-2.3.6');
+global.showdown = require('./showdown-2.1.0');
+global.DOMPurify = require('./purify-2.4.6');
global.baseX = require('./base-x-4.0.0').baseX;
global.Legacy = require('./legacy').Legacy;
require('./bootstrap-3.4.1');
diff --git a/js/jquery-3.6.0.js b/js/jquery-3.6.0.js
deleted file mode 100644
index c4c6022f..00000000
--- a/js/jquery-3.6.0.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */
-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 10) && (n % 10 === 0) ? 2 : 3));
case 'id':
case 'jbo':
+ case 'th':
return 0;
case 'lt':
return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
diff --git a/js/purify-2.3.6.js b/js/purify-2.3.6.js
deleted file mode 100644
index 1d0a5933..00000000
--- a/js/purify-2.3.6.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),B=a(/^aria-[\-\w]+$/),P=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),G=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=a(/^html$/i),q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Y(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.6",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,m=t.NamedNodeMap,A=void 0===m?t.NamedNodeMap||t.MozNamedAttrMap:m,$=t.HTMLFormElement,X=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=w(J,"cloneNode"),ee=w(J,"nextSibling"),te=w(J,"childNodes"),ne=w(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,me=r.importNode,fe={};try{fe=x(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var pe=H,he=U,ge=z,ye=B,ve=j,be=G,Te=P,Ne=null,Ae=E({},[].concat(Y(k),Y(S),Y(_),Y(O),Y(M))),Ee=null,xe=E({},[].concat(Y(L),Y(R),Y(I),Y(F))),we=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,Se=null,_e=!0,De=!0,Oe=!1,Ce=!1,Me=!1,Le=!1,Re=!1,Ie=!1,Fe=!1,He=!1,Ue=!0,ze=!0,Be=!1,Pe={},je=null,Ge=E({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),We=null,qe=E({},["audio","video","img","source","image","track"]),Ye=null,Ke=E({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml",Ze=Xe,Je=!1,Qe=void 0,et=["application/xhtml+xml","text/html"],tt="text/html",nt=void 0,rt=null,ot=o.createElement("form"),it=function(e){return e instanceof RegExp||e instanceof Function},at=function(e){rt&&rt===e||(e&&"object"===(void 0===e?"undefined":q(e))||(e={}),e=x(e),Ne="ALLOWED_TAGS"in e?E({},e.ALLOWED_TAGS):Ae,Ee="ALLOWED_ATTR"in e?E({},e.ALLOWED_ATTR):xe,Ye="ADD_URI_SAFE_ATTR"in e?E(x(Ke),e.ADD_URI_SAFE_ATTR):Ke,We="ADD_DATA_URI_TAGS"in e?E(x(qe),e.ADD_DATA_URI_TAGS):qe,je="FORBID_CONTENTS"in e?E({},e.FORBID_CONTENTS):Ge,ke="FORBID_TAGS"in e?E({},e.FORBID_TAGS):{},Se="FORBID_ATTR"in e?E({},e.FORBID_ATTR):{},Pe="USE_PROFILES"in e&&e.USE_PROFILES,_e=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ce=e.SAFE_FOR_TEMPLATES||!1,Me=e.WHOLE_DOCUMENT||!1,Ie=e.RETURN_DOM||!1,Fe=e.RETURN_DOM_FRAGMENT||!1,He=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,ze=!1!==e.KEEP_CONTENT,Be=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||Xe,e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Qe=Qe=-1===et.indexOf(e.PARSER_MEDIA_TYPE)?tt:e.PARSER_MEDIA_TYPE,nt="application/xhtml+xml"===Qe?function(e){return e}:h,Ce&&(De=!1),Fe&&(Ie=!0),Pe&&(Ne=E({},[].concat(Y(M))),Ee=[],!0===Pe.html&&(E(Ne,k),E(Ee,L)),!0===Pe.svg&&(E(Ne,S),E(Ee,R),E(Ee,F)),!0===Pe.svgFilters&&(E(Ne,_),E(Ee,R),E(Ee,F)),!0===Pe.mathMl&&(E(Ne,O),E(Ee,I),E(Ee,F))),e.ADD_TAGS&&(Ne===Ae&&(Ne=x(Ne)),E(Ne,e.ADD_TAGS)),e.ADD_ATTR&&(Ee===xe&&(Ee=x(Ee)),E(Ee,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&E(Ye,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(je===Ge&&(je=x(je)),E(je,e.FORBID_CONTENTS)),ze&&(Ne["#text"]=!0),Me&&E(Ne,["html","head","body"]),Ne.table&&(E(Ne,["tbody"]),delete ke.tbody),i&&i(e),rt=e)},lt=E({},["mi","mo","mn","ms","mtext"]),ct=E({},["foreignobject","desc","title","annotation-xml"]),st=E({},S);E(st,_),E(st,D);var ut=E({},O);E(ut,C);var mt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Xe,tagName:"template"});var n=h(e.tagName),r=h(t.tagName);if(e.namespaceURI===$e)return t.namespaceURI===Xe?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||lt[r]):Boolean(st[n]);if(e.namespaceURI===Ve)return t.namespaceURI===Xe?"math"===n:t.namespaceURI===$e?"math"===n&&ct[r]:Boolean(ut[n]);if(e.namespaceURI===Xe){if(t.namespaceURI===$e&&!ct[r])return!1;if(t.namespaceURI===Ve&&!lt[r])return!1;var o=E({},["title","style","font","a","script"]);return!ut[n]&&(o[n]||!st[n])}return!1},ft=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},dt=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(Ie||Fe)try{ft(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},pt=function(e){var t=void 0,n=void 0;if(Re)e=""+e;else{var r=g(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Qe&&(e=''+e+"");var i=oe?oe.createHTML(e):e;if(Ze===Xe)try{t=(new X).parseFromString(i,Qe)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===Xe?ue.call(t,Me?"html":"body")[0]:Me?t.documentElement:a},ht=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},gt=function(e){return e instanceof $&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof A)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},yt=function(e){return"object"===(void 0===c?"undefined":q(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":q(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},vt=function(e,t,r){de[e]&&f(de[e],(function(e){e.call(n,t,r,rt)}))},bt=function(e){var t=void 0;if(vt("beforeSanitizeElements",e,null),gt(e))return ft(e),!0;if(g(e.nodeName,/[\u0080-\uFFFF]/))return ft(e),!0;var r=nt(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:r,allowedTags:Ne}),!yt(e.firstElementChild)&&(!yt(e.content)||!yt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return ft(e),!0;if("select"===r&&T(/=0;--a)o.insertBefore(Q(i[a],!0),ee(e))}return ft(e),!0}return e instanceof s&&!mt(e)?(ft(e),!0):"noscript"!==r&&"noembed"!==r||!T(/<\/no(script|embed)/i,e.innerHTML)?(Ce&&3===e.nodeType&&(t=e.textContent,t=y(t,pe," "),t=y(t,he," "),e.textContent!==t&&(p(n.removed,{element:e.cloneNode()}),e.textContent=t)),vt("afterSanitizeElements",e,null),!1):(ft(e),!0)},Tt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in o||n in ot))return!1;if(De&&!Se[t]&&T(ge,t));else if(_e&&T(ye,t));else if(!Ee[t]||Se[t]){if(!(Nt(e)&&(we.tagNameCheck instanceof RegExp&&T(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&T(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&T(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ye[t]);else if(T(Te,y(n,be,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==v(n,"data:")||!We[e]){if(Oe&&!T(ve,y(n,be,"")));else if(n)return!1}else;return!0},Nt=function(e){return e.indexOf("-")>0},At=function(e){var t=void 0,r=void 0,o=void 0,i=void 0;vt("beforeSanitizeAttributes",e,null);var a=e.attributes;if(a){var l={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};for(i=a.length;i--;){var c=t=a[i],s=c.name,u=c.namespaceURI;if(r=b(t.value),o=nt(s),l.attrName=o,l.attrValue=r,l.keepAttr=!0,l.forceKeepAttr=void 0,vt("uponSanitizeAttribute",e,l),r=l.attrValue,!l.forceKeepAttr&&(dt(s,e),l.keepAttr))if(T(/\/>/i,r))dt(s,e);else{Ce&&(r=y(r,pe," "),r=y(r,he," "));var m=nt(e.nodeName);if(Tt(m,o,r))try{u?e.setAttributeNS(u,s,r):e.setAttribute(s,r),d(n.removed)}catch(e){}}}vt("afterSanitizeAttributes",e,null)}},Et=function e(t){var n=void 0,r=ht(t);for(vt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)vt("uponSanitizeShadowNode",n,null),bt(n)||(n.content instanceof a&&e(n.content),At(n));vt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,o){var i=void 0,l=void 0,s=void 0,u=void 0,m=void 0;if((Je=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!yt(e)){if("function"!=typeof e.toString)throw N("toString is not a function");if("string"!=typeof(e=e.toString()))throw N("dirty is not a string, aborting")}if(!n.isSupported){if("object"===q(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(yt(e))return t.toStaticHTML(e.outerHTML)}return e}if(Le||at(o),n.removed=[],"string"==typeof e&&(Be=!1),Be){if(e.nodeName){var f=nt(e.nodeName);if(!Ne[f]||ke[f])throw N("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)1===(l=(i=pt("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===l.nodeName||"HTML"===l.nodeName?i=l:i.appendChild(l);else{if(!Ie&&!Ce&&!Me&&-1===e.indexOf("<"))return oe&&He?oe.createHTML(e):e;if(!(i=pt(e)))return Ie?null:He?ie:""}i&&Re&&ft(i.firstChild);for(var d=ht(Be?e:i);s=d.nextNode();)3===s.nodeType&&s===u||bt(s)||(s.content instanceof a&&Et(s.content),At(s),u=s);if(u=null,Be)return e;if(Ie){if(Fe)for(m=se.call(i.ownerDocument);i.firstChild;)m.appendChild(i.firstChild);else m=i;return Ee.shadowroot&&(m=me.call(r,m,!0)),m}var p=Me?i.outerHTML:i.innerHTML;return Me&&Ne["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&T(W,i.ownerDocument.doctype.name)&&(p="\n"+p),Ce&&(p=y(p,pe," "),p=y(p,he," ")),oe&&He?oe.createHTML(p):p},n.setConfig=function(e){at(e),Le=!0},n.clearConfig=function(){rt=null,Le=!1},n.isValidAttribute=function(e,t,n){rt||at({});var r=nt(e),o=nt(t);return Tt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(de[e]=de[e]||[],p(de[e],t))},n.removeHook=function(e){de[e]&&d(de[e])},n.removeHooks=function(e){de[e]&&(de[e]=[])},n.removeAllHooks=function(){de={}},n}()}));
diff --git a/js/purify-2.4.6.js b/js/purify-2.4.6.js
new file mode 100644
index 00000000..f45a176b
--- /dev/null
+++ b/js/purify-2.4.6.js
@@ -0,0 +1,2 @@
+/*! @license DOMPurify 2.4.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.4.1/LICENSE */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function r(e,o,a){return r=n()?Reflect.construct:function(e,n,r){var o=[null];o.push.apply(o,n);var a=new(Function.bind.apply(e,o));return r&&t(a,r.prototype),a},r.apply(null,arguments)}function o(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return a(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),o=1;o/gm),Y=f(/\${[\w\W]*}/gm),$=f(/^data-[\-\w.\u00B7-\uFFFF]/),K=f(/^aria-[\-\w]+$/),V=f(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),X=f(/^(?:\w+script|data):/i),Z=f(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),J=f(/^html$/i),Q=function(){return"undefined"==typeof window?null:window},ee=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,o="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(o)&&(r=n.currentScript.getAttribute(o));var a="dompurify"+(r?"#"+r:"");try{return t.createPolicy(a,{createHTML:function(e){return e},createScriptURL:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+a+" could not be created."),null}};var te=function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Q(),r=function(e){return t(e)};if(r.version="2.4.1",r.removed=[],!n||!n.document||9!==n.document.nodeType)return r.isSupported=!1,r;var a=n.document,i=n.document,l=n.DocumentFragment,c=n.HTMLTemplateElement,u=n.Node,s=n.Element,f=n.NodeFilter,p=n.NamedNodeMap,d=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,h=n.HTMLFormElement,g=n.DOMParser,y=n.trustedTypes,O=s.prototype,te=R(O,"cloneNode"),ne=R(O,"nextSibling"),re=R(O,"childNodes"),oe=R(O,"parentNode");if("function"==typeof c){var ae=i.createElement("template");ae.content&&ae.content.ownerDocument&&(i=ae.content.ownerDocument)}var ie=ee(y,a),le=ie?ie.createHTML(""):"",ce=i,ue=ce.implementation,se=ce.createNodeIterator,me=ce.createDocumentFragment,fe=ce.getElementsByTagName,pe=a.importNode,de={};try{de=L(i).documentMode?i.documentMode:{}}catch(e){}var he={};r.isSupported="function"==typeof oe&&ue&&void 0!==ue.createHTMLDocument&&9!==de;var ge,ye,be=W,ve=q,Te=Y,Ne=$,Ae=K,Ee=X,we=Z,Se=V,xe=null,_e=D({},[].concat(o(M),o(C),o(I),o(U),o(z))),ke=null,Oe=D({},[].concat(o(P),o(j),o(B),o(G))),De=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Le=null,Re=null,Me=!0,Ce=!0,Ie=!1,Fe=!1,Ue=!1,He=!1,ze=!1,Pe=!1,je=!1,Be=!1,Ge=!0,We=!1,qe="user-content-",Ye=!0,$e=!1,Ke={},Ve=null,Xe=D({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ze=null,Je=D({},["audio","video","img","source","image","track"]),Qe=null,et=D({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),tt="http://www.w3.org/1998/Math/MathML",nt="http://www.w3.org/2000/svg",rt="http://www.w3.org/1999/xhtml",ot=rt,at=!1,it=null,lt=D({},[tt,nt,rt],A),ct=["application/xhtml+xml","text/html"],ut="text/html",st=null,mt=i.createElement("form"),ft=function(e){return e instanceof RegExp||e instanceof Function},pt=function(t){st&&st===t||(t&&"object"===e(t)||(t={}),t=L(t),ge=ge=-1===ct.indexOf(t.PARSER_MEDIA_TYPE)?ut:t.PARSER_MEDIA_TYPE,ye="application/xhtml+xml"===ge?A:N,xe="ALLOWED_TAGS"in t?D({},t.ALLOWED_TAGS,ye):_e,ke="ALLOWED_ATTR"in t?D({},t.ALLOWED_ATTR,ye):Oe,it="ALLOWED_NAMESPACES"in t?D({},t.ALLOWED_NAMESPACES,A):lt,Qe="ADD_URI_SAFE_ATTR"in t?D(L(et),t.ADD_URI_SAFE_ATTR,ye):et,Ze="ADD_DATA_URI_TAGS"in t?D(L(Je),t.ADD_DATA_URI_TAGS,ye):Je,Ve="FORBID_CONTENTS"in t?D({},t.FORBID_CONTENTS,ye):Xe,Le="FORBID_TAGS"in t?D({},t.FORBID_TAGS,ye):{},Re="FORBID_ATTR"in t?D({},t.FORBID_ATTR,ye):{},Ke="USE_PROFILES"in t&&t.USE_PROFILES,Me=!1!==t.ALLOW_ARIA_ATTR,Ce=!1!==t.ALLOW_DATA_ATTR,Ie=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Fe=t.SAFE_FOR_TEMPLATES||!1,Ue=t.WHOLE_DOCUMENT||!1,Pe=t.RETURN_DOM||!1,je=t.RETURN_DOM_FRAGMENT||!1,Be=t.RETURN_TRUSTED_TYPE||!1,ze=t.FORCE_BODY||!1,Ge=!1!==t.SANITIZE_DOM,We=t.SANITIZE_NAMED_PROPS||!1,Ye=!1!==t.KEEP_CONTENT,$e=t.IN_PLACE||!1,Se=t.ALLOWED_URI_REGEXP||Se,ot=t.NAMESPACE||rt,t.CUSTOM_ELEMENT_HANDLING&&ft(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&ft(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Fe&&(Ce=!1),je&&(Pe=!0),Ke&&(xe=D({},o(z)),ke=[],!0===Ke.html&&(D(xe,M),D(ke,P)),!0===Ke.svg&&(D(xe,C),D(ke,j),D(ke,G)),!0===Ke.svgFilters&&(D(xe,I),D(ke,j),D(ke,G)),!0===Ke.mathMl&&(D(xe,U),D(ke,B),D(ke,G))),t.ADD_TAGS&&(xe===_e&&(xe=L(xe)),D(xe,t.ADD_TAGS,ye)),t.ADD_ATTR&&(ke===Oe&&(ke=L(ke)),D(ke,t.ADD_ATTR,ye)),t.ADD_URI_SAFE_ATTR&&D(Qe,t.ADD_URI_SAFE_ATTR,ye),t.FORBID_CONTENTS&&(Ve===Xe&&(Ve=L(Ve)),D(Ve,t.FORBID_CONTENTS,ye)),Ye&&(xe["#text"]=!0),Ue&&D(xe,["html","head","body"]),xe.table&&(D(xe,["tbody"]),delete Le.tbody),m&&m(t),st=t)},dt=D({},["mi","mo","mn","ms","mtext"]),ht=D({},["foreignobject","desc","title","annotation-xml"]),gt=D({},["title","style","font","a","script"]),yt=D({},C);D(yt,I),D(yt,F);var bt=D({},U);D(bt,H);var vt=function(e){var t=oe(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});var n=N(e.tagName),r=N(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===nt?t.namespaceURI===rt?"svg"===n:t.namespaceURI===tt?"svg"===n&&("annotation-xml"===r||dt[r]):Boolean(yt[n]):e.namespaceURI===tt?t.namespaceURI===rt?"math"===n:t.namespaceURI===nt?"math"===n&&ht[r]:Boolean(bt[n]):e.namespaceURI===rt?!(t.namespaceURI===nt&&!ht[r])&&(!(t.namespaceURI===tt&&!dt[r])&&(!bt[n]&&(gt[n]||!yt[n]))):!("application/xhtml+xml"!==ge||!it[e.namespaceURI]))},Tt=function(e){T(r.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=le}catch(t){e.remove()}}},Nt=function(e,t){try{T(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){T(r.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!ke[e])if(Pe||je)try{Tt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},At=function(e){var t,n;if(ze)e=""+e;else{var r=E(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===ge&&ot===rt&&(e=''+e+"");var o=ie?ie.createHTML(e):e;if(ot===rt)try{t=(new g).parseFromString(o,ge)}catch(e){}if(!t||!t.documentElement){t=ue.createDocument(ot,"template",null);try{t.documentElement.innerHTML=at?"":o}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(i.createTextNode(n),a.childNodes[0]||null),ot===rt?fe.call(t,Ue?"html":"body")[0]:Ue?t.documentElement:a},Et=function(e){return se.call(e.ownerDocument||e,e,f.SHOW_ELEMENT|f.SHOW_COMMENT|f.SHOW_TEXT,null,!1)},wt=function(e){return e instanceof h&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof d)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},St=function(t){return"object"===e(u)?t instanceof u:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},xt=function(e,t,n){he[e]&&b(he[e],(function(e){e.call(r,t,n,st)}))},_t=function(e){var t;if(xt("beforeSanitizeElements",e,null),wt(e))return Tt(e),!0;if(_(/[\u0080-\uFFFF]/,e.nodeName))return Tt(e),!0;var n=ye(e.nodeName);if(xt("uponSanitizeElement",e,{tagName:n,allowedTags:xe}),e.hasChildNodes()&&!St(e.firstElementChild)&&(!St(e.content)||!St(e.content.firstElementChild))&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return Tt(e),!0;if("select"===n&&_(/=0;--i)o.insertBefore(te(a[i],!0),ne(e))}return Tt(e),!0}return e instanceof s&&!vt(e)?(Tt(e),!0):"noscript"!==n&&"noembed"!==n||!_(/<\/no(script|embed)/i,e.innerHTML)?(Fe&&3===e.nodeType&&(t=e.textContent,t=w(t,be," "),t=w(t,ve," "),t=w(t,Te," "),e.textContent!==t&&(T(r.removed,{element:e.cloneNode()}),e.textContent=t)),xt("afterSanitizeElements",e,null),!1):(Tt(e),!0)},kt=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in i||n in mt))return!1;if(Ce&&!Re[t]&&_(Ne,t));else if(Me&&_(Ae,t));else if(!ke[t]||Re[t]){if(!(Ot(e)&&(De.tagNameCheck instanceof RegExp&&_(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&_(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&_(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Qe[t]);else if(_(Se,w(n,we,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==S(n,"data:")||!Ze[e]){if(Ie&&!_(Ee,w(n,we,"")));else if(n)return!1}else;return!0},Ot=function(e){return e.indexOf("-")>0},Dt=function(t){var n,o,a,i;xt("beforeSanitizeAttributes",t,null);var l=t.attributes;if(l){var c={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:ke};for(i=l.length;i--;){var u=n=l[i],s=u.name,m=u.namespaceURI;if(o="value"===s?n.value:x(n.value),a=ye(s),c.attrName=a,c.attrValue=o,c.keepAttr=!0,c.forceKeepAttr=void 0,xt("uponSanitizeAttribute",t,c),o=c.attrValue,!c.forceKeepAttr&&(Nt(s,t),c.keepAttr))if(_(/\/>/i,o))Nt(s,t);else{Fe&&(o=w(o,be," "),o=w(o,ve," "),o=w(o,Te," "));var f=ye(t.nodeName);if(kt(f,a,o)){if(!We||"id"!==a&&"name"!==a||(Nt(s,t),o=qe+o),ie&&"object"===e(y)&&"function"==typeof y.getAttributeType)if(m);else switch(y.getAttributeType(f,a)){case"TrustedHTML":o=ie.createHTML(o);break;case"TrustedScriptURL":o=ie.createScriptURL(o)}try{m?t.setAttributeNS(m,s,o):t.setAttribute(s,o),v(r.removed)}catch(e){}}}}xt("afterSanitizeAttributes",t,null)}},Lt=function e(t){var n,r=Et(t);for(xt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)xt("uponSanitizeShadowNode",n,null),_t(n)||(n.content instanceof l&&e(n.content),Dt(n));xt("afterSanitizeShadowDOM",t,null)};return r.sanitize=function(t){var o,i,c,s,m,f=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if((at=!t)&&(t="\x3c!--\x3e"),"string"!=typeof t&&!St(t)){if("function"!=typeof t.toString)throw k("toString is not a function");if("string"!=typeof(t=t.toString()))throw k("dirty is not a string, aborting")}if(!r.isSupported){if("object"===e(n.toStaticHTML)||"function"==typeof n.toStaticHTML){if("string"==typeof t)return n.toStaticHTML(t);if(St(t))return n.toStaticHTML(t.outerHTML)}return t}if(He||pt(f),r.removed=[],"string"==typeof t&&($e=!1),$e){if(t.nodeName){var p=ye(t.nodeName);if(!xe[p]||Le[p])throw k("root node is forbidden and cannot be sanitized in-place")}}else if(t instanceof u)1===(i=(o=At("\x3c!----\x3e")).ownerDocument.importNode(t,!0)).nodeType&&"BODY"===i.nodeName||"HTML"===i.nodeName?o=i:o.appendChild(i);else{if(!Pe&&!Fe&&!Ue&&-1===t.indexOf("<"))return ie&&Be?ie.createHTML(t):t;if(!(o=At(t)))return Pe?null:Be?le:""}o&&ze&&Tt(o.firstChild);for(var d=Et($e?t:o);c=d.nextNode();)3===c.nodeType&&c===s||_t(c)||(c.content instanceof l&&Lt(c.content),Dt(c),s=c);if(s=null,$e)return t;if(Pe){if(je)for(m=me.call(o.ownerDocument);o.firstChild;)m.appendChild(o.firstChild);else m=o;return ke.shadowroot&&(m=pe.call(a,m,!0)),m}var h=Ue?o.outerHTML:o.innerHTML;return Ue&&xe["!doctype"]&&o.ownerDocument&&o.ownerDocument.doctype&&o.ownerDocument.doctype.name&&_(J,o.ownerDocument.doctype.name)&&(h="\n"+h),Fe&&(h=w(h,be," "),h=w(h,ve," "),h=w(h,Te," ")),ie&&Be?ie.createHTML(h):h},r.setConfig=function(e){pt(e),He=!0},r.clearConfig=function(){st=null,He=!1},r.isValidAttribute=function(e,t,n){st||pt({});var r=ye(e),o=ye(t);return kt(r,o,n)},r.addHook=function(e,t){"function"==typeof t&&(he[e]=he[e]||[],T(he[e],t))},r.removeHook=function(e){if(he[e])return v(he[e])},r.removeHooks=function(e){he[e]&&(he[e]=[])},r.removeAllHooks=function(){he={}},r}();return te}));
diff --git a/js/showdown-2.0.3.js b/js/showdown-2.1.0.js
similarity index 99%
rename from js/showdown-2.0.3.js
rename to js/showdown-2.1.0.js
index 6c25ae4d..0a470552 100644
--- a/js/showdown-2.0.3.js
+++ b/js/showdown-2.1.0.js
@@ -1,2 +1,2 @@
-/*! showdown v 2.0.3 - 08-03-2022 */
+/*! showdown v 2.1.0 - 21-04-2022 */
!function(){function a(e){"use strict";var r={omitExtraWLInCodeBlocks:{defaultValue:!1,describe:"Omit the default extra whiteline added to code blocks",type:"boolean"},noHeaderId:{defaultValue:!1,describe:"Turn on/off generated header id",type:"boolean"},prefixHeaderId:{defaultValue:!1,describe:"Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic 'section-' prefix",type:"string"},rawPrefixHeaderId:{defaultValue:!1,describe:'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)',type:"boolean"},ghCompatibleHeaderId:{defaultValue:!1,describe:"Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)",type:"boolean"},rawHeaderId:{defaultValue:!1,describe:"Remove only spaces, ' and \" from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids",type:"boolean"},headerLevelStart:{defaultValue:!1,describe:"The header blocks level start",type:"integer"},parseImgDimensions:{defaultValue:!1,describe:"Turn on/off image dimension parsing",type:"boolean"},simplifiedAutoLink:{defaultValue:!1,describe:"Turn on/off GFM autolink style",type:"boolean"},excludeTrailingPunctuationFromURLs:{defaultValue:!1,describe:"Excludes trailing punctuation from links generated with autoLinking",type:"boolean"},literalMidWordUnderscores:{defaultValue:!1,describe:"Parse midword underscores as literal underscores",type:"boolean"},literalMidWordAsterisks:{defaultValue:!1,describe:"Parse midword asterisks as literal asterisks",type:"boolean"},strikethrough:{defaultValue:!1,describe:"Turn on/off strikethrough support",type:"boolean"},tables:{defaultValue:!1,describe:"Turn on/off tables support",type:"boolean"},tablesHeaderId:{defaultValue:!1,describe:"Add an id to table headers",type:"boolean"},ghCodeBlocks:{defaultValue:!0,describe:"Turn on/off GFM fenced code blocks support",type:"boolean"},tasklists:{defaultValue:!1,describe:"Turn on/off GFM tasklist support",type:"boolean"},smoothLivePreview:{defaultValue:!1,describe:"Prevents weird effects in live previews due to incomplete input",type:"boolean"},smartIndentationFix:{defaultValue:!1,describe:"Tries to smartly fix indentation in es6 strings",type:"boolean"},disableForced4SpacesIndentedSublists:{defaultValue:!1,describe:"Disables the requirement of indenting nested sublists by 4 spaces",type:"boolean"},simpleLineBreaks:{defaultValue:!1,describe:"Parses simple line breaks as
(GFM Style)",type:"boolean"},requireSpaceBeforeHeadingText:{defaultValue:!1,describe:"Makes adding a space between `#` and the header text mandatory (GFM Style)",type:"boolean"},ghMentions:{defaultValue:!1,describe:"Enables github @mentions",type:"boolean"},ghMentionsLink:{defaultValue:"https://github.com/{u}",describe:"Changes the link generated by @mentions. Only applies if ghMentions option is enabled.",type:"string"},encodeEmails:{defaultValue:!0,describe:"Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities",type:"boolean"},openLinksInNewWindow:{defaultValue:!1,describe:"Open all links in new windows",type:"boolean"},backslashEscapesHTMLTags:{defaultValue:!1,describe:"Support for HTML Tag escaping. ex: foo
",type:"boolean"},emoji:{defaultValue:!1,describe:"Enable emoji support. Ex: `this is a :smile: emoji`",type:"boolean"},underline:{defaultValue:!1,describe:"Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``",type:"boolean"},ellipsis:{defaultValue:!0,describe:"Replaces three dots with the ellipsis unicode character",type:"boolean"},completeHTMLDocument:{defaultValue:!1,describe:"Outputs a complete html document, including ``, `` and `` tags",type:"boolean"},metadata:{defaultValue:!1,describe:"Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).",type:"boolean"},splitAdjacentBlockquotes:{defaultValue:!1,describe:"Split adjacent blockquote blocks",type:"boolean"}};if(!1===e)return JSON.parse(JSON.stringify(r));var t,a={};for(t in r)r.hasOwnProperty(t)&&(a[t]=r[t].defaultValue);return a}var x={},t={},d={},p=a(!0),h="vanilla",_={github:{omitExtraWLInCodeBlocks:!0,simplifiedAutoLink:!0,excludeTrailingPunctuationFromURLs:!0,literalMidWordUnderscores:!0,strikethrough:!0,tables:!0,tablesHeaderId:!0,ghCodeBlocks:!0,tasklists:!0,disableForced4SpacesIndentedSublists:!0,simpleLineBreaks:!0,requireSpaceBeforeHeadingText:!0,ghCompatibleHeaderId:!0,ghMentions:!0,backslashEscapesHTMLTags:!0,emoji:!0,splitAdjacentBlockquotes:!0},original:{noHeaderId:!0,ghCodeBlocks:!1},ghost:{omitExtraWLInCodeBlocks:!0,parseImgDimensions:!0,simplifiedAutoLink:!0,excludeTrailingPunctuationFromURLs:!0,literalMidWordUnderscores:!0,strikethrough:!0,tables:!0,tablesHeaderId:!0,ghCodeBlocks:!0,tasklists:!0,smoothLivePreview:!0,simpleLineBreaks:!0,requireSpaceBeforeHeadingText:!0,ghMentions:!1,encodeEmails:!0},vanilla:a(!0),allOn:function(){"use strict";var e,r=a(!0),t={};for(e in r)r.hasOwnProperty(e)&&(t[e]=!0);return t}()};function g(e,r){"use strict";var t=r?"Error in "+r+" extension->":"Error in unnamed extension",a={valid:!0,error:""};x.helper.isArray(e)||(e=[e]);for(var n=0;n").replace(/&/g,"&")};function u(e,r,t,a){"use strict";var n,s,o,i=-1<(a=a||"").indexOf("g"),l=new RegExp(r+"|"+t,"g"+a.replace(/g/g,"")),c=new RegExp(r,a.replace(/g/g,"")),u=[];do{for(n=0;p=l.exec(e);)if(c.test(p[0]))n++||(o=(s=l.lastIndex)-p[0].length);else if(n&&!--n){var d=p.index+p[0].length,p={left:{start:o,end:s},match:{start:s,end:p.index},right:{start:p.index,end:d},wholeMatch:{start:o,end:d}};if(u.push(p),!i)return u}}while(n&&(l.lastIndex=s));return u}function s(u){"use strict";return function(e,r,t,a,n,s,o){var i=t=t.replace(x.helper.regexes.asteriskDashAndColon,x.helper.escapeCharactersCallback),l="",c="",r=r||"",o=o||"";return/^www\./i.test(t)&&(t=t.replace(/^www\./i,"http://www.")),u.excludeTrailingPunctuationFromURLs&&s&&(l=s),r+'"+i+""+l+o}}function o(n,s){"use strict";return function(e,r,t){var a="mailto:";return r=r||"",t=x.subParser("unescapeSpecialChars")(t,n,s),n.encodeEmails?(a=x.helper.encodeEmailAddress(a+t),t=x.helper.encodeEmailAddress(t)):a+=t,r+''+t+""}}x.helper.matchRecursiveRegExp=function(e,r,t,a){"use strict";for(var n=u(e,r,t,a),s=[],o=0;o>=0,t=String(t||" "),e.length>r?String(e):((r-=e.length)>t.length&&(t+=t.repeat(r/t.length)),String(e)+t.slice(0,r))},"undefined"==typeof console&&(console={warn:function(e){"use strict";alert(e)},log:function(e){"use strict";alert(e)},error:function(e){"use strict";throw e}}),x.helper.regexes={asteriskDashAndColon:/([*_:~])/g},x.helper.emojis={"+1":"👍","-1":"👎",100:"💯",1234:"🔢","1st_place_medal":"🥇","2nd_place_medal":"🥈","3rd_place_medal":"🥉","8ball":"🎱",a:"🅰️",ab:"🆎",abc:"🔤",abcd:"🔡",accept:"🉑",aerial_tramway:"🚡",airplane:"✈️",alarm_clock:"⏰",alembic:"⚗️",alien:"👽",ambulance:"🚑",amphora:"🏺",anchor:"⚓️",angel:"👼",anger:"💢",angry:"😠",anguished:"😧",ant:"🐜",apple:"🍎",aquarius:"♒️",aries:"♈️",arrow_backward:"◀️",arrow_double_down:"⏬",arrow_double_up:"⏫",arrow_down:"⬇️",arrow_down_small:"🔽",arrow_forward:"▶️",arrow_heading_down:"⤵️",arrow_heading_up:"⤴️",arrow_left:"⬅️",arrow_lower_left:"↙️",arrow_lower_right:"↘️",arrow_right:"➡️",arrow_right_hook:"↪️",arrow_up:"⬆️",arrow_up_down:"↕️",arrow_up_small:"🔼",arrow_upper_left:"↖️",arrow_upper_right:"↗️",arrows_clockwise:"🔃",arrows_counterclockwise:"🔄",art:"🎨",articulated_lorry:"🚛",artificial_satellite:"🛰",astonished:"😲",athletic_shoe:"👟",atm:"🏧",atom_symbol:"⚛️",avocado:"🥑",b:"🅱️",baby:"👶",baby_bottle:"🍼",baby_chick:"🐤",baby_symbol:"🚼",back:"🔙",bacon:"🥓",badminton:"🏸",baggage_claim:"🛄",baguette_bread:"🥖",balance_scale:"⚖️",balloon:"🎈",ballot_box:"🗳",ballot_box_with_check:"☑️",bamboo:"🎍",banana:"🍌",bangbang:"‼️",bank:"🏦",bar_chart:"📊",barber:"💈",baseball:"⚾️",basketball:"🏀",basketball_man:"⛹️",basketball_woman:"⛹️♀️",bat:"🦇",bath:"🛀",bathtub:"🛁",battery:"🔋",beach_umbrella:"🏖",bear:"🐻",bed:"🛏",bee:"🐝",beer:"🍺",beers:"🍻",beetle:"🐞",beginner:"🔰",bell:"🔔",bellhop_bell:"🛎",bento:"🍱",biking_man:"🚴",bike:"🚲",biking_woman:"🚴♀️",bikini:"👙",biohazard:"☣️",bird:"🐦",birthday:"🎂",black_circle:"⚫️",black_flag:"🏴",black_heart:"🖤",black_joker:"🃏",black_large_square:"⬛️",black_medium_small_square:"◾️",black_medium_square:"◼️",black_nib:"✒️",black_small_square:"▪️",black_square_button:"🔲",blonde_man:"👱",blonde_woman:"👱♀️",blossom:"🌼",blowfish:"🐡",blue_book:"📘",blue_car:"🚙",blue_heart:"💙",blush:"😊",boar:"🐗",boat:"⛵️",bomb:"💣",book:"📖",bookmark:"🔖",bookmark_tabs:"📑",books:"📚",boom:"💥",boot:"👢",bouquet:"💐",bowing_man:"🙇",bow_and_arrow:"🏹",bowing_woman:"🙇♀️",bowling:"🎳",boxing_glove:"🥊",boy:"👦",bread:"🍞",bride_with_veil:"👰",bridge_at_night:"🌉",briefcase:"💼",broken_heart:"💔",bug:"🐛",building_construction:"🏗",bulb:"💡",bullettrain_front:"🚅",bullettrain_side:"🚄",burrito:"🌯",bus:"🚌",business_suit_levitating:"🕴",busstop:"🚏",bust_in_silhouette:"👤",busts_in_silhouette:"👥",butterfly:"🦋",cactus:"🌵",cake:"🍰",calendar:"📆",call_me_hand:"🤙",calling:"📲",camel:"🐫",camera:"📷",camera_flash:"📸",camping:"🏕",cancer:"♋️",candle:"🕯",candy:"🍬",canoe:"🛶",capital_abcd:"🔠",capricorn:"♑️",car:"🚗",card_file_box:"🗃",card_index:"📇",card_index_dividers:"🗂",carousel_horse:"🎠",carrot:"🥕",cat:"🐱",cat2:"🐈",cd:"💿",chains:"⛓",champagne:"🍾",chart:"💹",chart_with_downwards_trend:"📉",chart_with_upwards_trend:"📈",checkered_flag:"🏁",cheese:"🧀",cherries:"🍒",cherry_blossom:"🌸",chestnut:"🌰",chicken:"🐔",children_crossing:"🚸",chipmunk:"🐿",chocolate_bar:"🍫",christmas_tree:"🎄",church:"⛪️",cinema:"🎦",circus_tent:"🎪",city_sunrise:"🌇",city_sunset:"🌆",cityscape:"🏙",cl:"🆑",clamp:"🗜",clap:"👏",clapper:"🎬",classical_building:"🏛",clinking_glasses:"🥂",clipboard:"📋",clock1:"🕐",clock10:"🕙",clock1030:"🕥",clock11:"🕚",clock1130:"🕦",clock12:"🕛",clock1230:"🕧",clock130:"🕜",clock2:"🕑",clock230:"🕝",clock3:"🕒",clock330:"🕞",clock4:"🕓",clock430:"🕟",clock5:"🕔",clock530:"🕠",clock6:"🕕",clock630:"🕡",clock7:"🕖",clock730:"🕢",clock8:"🕗",clock830:"🕣",clock9:"🕘",clock930:"🕤",closed_book:"📕",closed_lock_with_key:"🔐",closed_umbrella:"🌂",cloud:"☁️",cloud_with_lightning:"🌩",cloud_with_lightning_and_rain:"⛈",cloud_with_rain:"🌧",cloud_with_snow:"🌨",clown_face:"🤡",clubs:"♣️",cocktail:"🍸",coffee:"☕️",coffin:"⚰️",cold_sweat:"😰",comet:"☄️",computer:"💻",computer_mouse:"🖱",confetti_ball:"🎊",confounded:"😖",confused:"😕",congratulations:"㊗️",construction:"🚧",construction_worker_man:"👷",construction_worker_woman:"👷♀️",control_knobs:"🎛",convenience_store:"🏪",cookie:"🍪",cool:"🆒",policeman:"👮",copyright:"©️",corn:"🌽",couch_and_lamp:"🛋",couple:"👫",couple_with_heart_woman_man:"💑",couple_with_heart_man_man:"👨❤️👨",couple_with_heart_woman_woman:"👩❤️👩",couplekiss_man_man:"👨❤️💋👨",couplekiss_man_woman:"💏",couplekiss_woman_woman:"👩❤️💋👩",cow:"🐮",cow2:"🐄",cowboy_hat_face:"🤠",crab:"🦀",crayon:"🖍",credit_card:"💳",crescent_moon:"🌙",cricket:"🏏",crocodile:"🐊",croissant:"🥐",crossed_fingers:"🤞",crossed_flags:"🎌",crossed_swords:"⚔️",crown:"👑",cry:"😢",crying_cat_face:"😿",crystal_ball:"🔮",cucumber:"🥒",cupid:"💘",curly_loop:"➰",currency_exchange:"💱",curry:"🍛",custard:"🍮",customs:"🛃",cyclone:"🌀",dagger:"🗡",dancer:"💃",dancing_women:"👯",dancing_men:"👯♂️",dango:"🍡",dark_sunglasses:"🕶",dart:"🎯",dash:"💨",date:"📅",deciduous_tree:"🌳",deer:"🦌",department_store:"🏬",derelict_house:"🏚",desert:"🏜",desert_island:"🏝",desktop_computer:"🖥",male_detective:"🕵️",diamond_shape_with_a_dot_inside:"💠",diamonds:"♦️",disappointed:"😞",disappointed_relieved:"😥",dizzy:"💫",dizzy_face:"😵",do_not_litter:"🚯",dog:"🐶",dog2:"🐕",dollar:"💵",dolls:"🎎",dolphin:"🐬",door:"🚪",doughnut:"🍩",dove:"🕊",dragon:"🐉",dragon_face:"🐲",dress:"👗",dromedary_camel:"🐪",drooling_face:"🤤",droplet:"💧",drum:"🥁",duck:"🦆",dvd:"📀","e-mail":"📧",eagle:"🦅",ear:"👂",ear_of_rice:"🌾",earth_africa:"🌍",earth_americas:"🌎",earth_asia:"🌏",egg:"🥚",eggplant:"🍆",eight_pointed_black_star:"✴️",eight_spoked_asterisk:"✳️",electric_plug:"🔌",elephant:"🐘",email:"✉️",end:"🔚",envelope_with_arrow:"📩",euro:"💶",european_castle:"🏰",european_post_office:"🏤",evergreen_tree:"🌲",exclamation:"❗️",expressionless:"😑",eye:"👁",eye_speech_bubble:"👁🗨",eyeglasses:"👓",eyes:"👀",face_with_head_bandage:"🤕",face_with_thermometer:"🤒",fist_oncoming:"👊",factory:"🏭",fallen_leaf:"🍂",family_man_woman_boy:"👪",family_man_boy:"👨👦",family_man_boy_boy:"👨👦👦",family_man_girl:"👨👧",family_man_girl_boy:"👨👧👦",family_man_girl_girl:"👨👧👧",family_man_man_boy:"👨👨👦",family_man_man_boy_boy:"👨👨👦👦",family_man_man_girl:"👨👨👧",family_man_man_girl_boy:"👨👨👧👦",family_man_man_girl_girl:"👨👨👧👧",family_man_woman_boy_boy:"👨👩👦👦",family_man_woman_girl:"👨👩👧",family_man_woman_girl_boy:"👨👩👧👦",family_man_woman_girl_girl:"👨👩👧👧",family_woman_boy:"👩👦",family_woman_boy_boy:"👩👦👦",family_woman_girl:"👩👧",family_woman_girl_boy:"👩👧👦",family_woman_girl_girl:"👩👧👧",family_woman_woman_boy:"👩👩👦",family_woman_woman_boy_boy:"👩👩👦👦",family_woman_woman_girl:"👩👩👧",family_woman_woman_girl_boy:"👩👩👧👦",family_woman_woman_girl_girl:"👩👩👧👧",fast_forward:"⏩",fax:"📠",fearful:"😨",feet:"🐾",female_detective:"🕵️♀️",ferris_wheel:"🎡",ferry:"⛴",field_hockey:"🏑",file_cabinet:"🗄",file_folder:"📁",film_projector:"📽",film_strip:"🎞",fire:"🔥",fire_engine:"🚒",fireworks:"🎆",first_quarter_moon:"🌓",first_quarter_moon_with_face:"🌛",fish:"🐟",fish_cake:"🍥",fishing_pole_and_fish:"🎣",fist_raised:"✊",fist_left:"🤛",fist_right:"🤜",flags:"🎏",flashlight:"🔦",fleur_de_lis:"⚜️",flight_arrival:"🛬",flight_departure:"🛫",floppy_disk:"💾",flower_playing_cards:"🎴",flushed:"😳",fog:"🌫",foggy:"🌁",football:"🏈",footprints:"👣",fork_and_knife:"🍴",fountain:"⛲️",fountain_pen:"🖋",four_leaf_clover:"🍀",fox_face:"🦊",framed_picture:"🖼",free:"🆓",fried_egg:"🍳",fried_shrimp:"🍤",fries:"🍟",frog:"🐸",frowning:"😦",frowning_face:"☹️",frowning_man:"🙍♂️",frowning_woman:"🙍",middle_finger:"🖕",fuelpump:"⛽️",full_moon:"🌕",full_moon_with_face:"🌝",funeral_urn:"⚱️",game_die:"🎲",gear:"⚙️",gem:"💎",gemini:"♊️",ghost:"👻",gift:"🎁",gift_heart:"💝",girl:"👧",globe_with_meridians:"🌐",goal_net:"🥅",goat:"🐐",golf:"⛳️",golfing_man:"🏌️",golfing_woman:"🏌️♀️",gorilla:"🦍",grapes:"🍇",green_apple:"🍏",green_book:"📗",green_heart:"💚",green_salad:"🥗",grey_exclamation:"❕",grey_question:"❔",grimacing:"😬",grin:"😁",grinning:"😀",guardsman:"💂",guardswoman:"💂♀️",guitar:"🎸",gun:"🔫",haircut_woman:"💇",haircut_man:"💇♂️",hamburger:"🍔",hammer:"🔨",hammer_and_pick:"⚒",hammer_and_wrench:"🛠",hamster:"🐹",hand:"✋",handbag:"👜",handshake:"🤝",hankey:"💩",hatched_chick:"🐥",hatching_chick:"🐣",headphones:"🎧",hear_no_evil:"🙉",heart:"❤️",heart_decoration:"💟",heart_eyes:"😍",heart_eyes_cat:"😻",heartbeat:"💓",heartpulse:"💗",hearts:"♥️",heavy_check_mark:"✔️",heavy_division_sign:"➗",heavy_dollar_sign:"💲",heavy_heart_exclamation:"❣️",heavy_minus_sign:"➖",heavy_multiplication_x:"✖️",heavy_plus_sign:"➕",helicopter:"🚁",herb:"🌿",hibiscus:"🌺",high_brightness:"🔆",high_heel:"👠",hocho:"🔪",hole:"🕳",honey_pot:"🍯",horse:"🐴",horse_racing:"🏇",hospital:"🏥",hot_pepper:"🌶",hotdog:"🌭",hotel:"🏨",hotsprings:"♨️",hourglass:"⌛️",hourglass_flowing_sand:"⏳",house:"🏠",house_with_garden:"🏡",houses:"🏘",hugs:"🤗",hushed:"😯",ice_cream:"🍨",ice_hockey:"🏒",ice_skate:"⛸",icecream:"🍦",id:"🆔",ideograph_advantage:"🉐",imp:"👿",inbox_tray:"📥",incoming_envelope:"📨",tipping_hand_woman:"💁",information_source:"ℹ️",innocent:"😇",interrobang:"⁉️",iphone:"📱",izakaya_lantern:"🏮",jack_o_lantern:"🎃",japan:"🗾",japanese_castle:"🏯",japanese_goblin:"👺",japanese_ogre:"👹",jeans:"👖",joy:"😂",joy_cat:"😹",joystick:"🕹",kaaba:"🕋",key:"🔑",keyboard:"⌨️",keycap_ten:"🔟",kick_scooter:"🛴",kimono:"👘",kiss:"💋",kissing:"😗",kissing_cat:"😽",kissing_closed_eyes:"😚",kissing_heart:"😘",kissing_smiling_eyes:"😙",kiwi_fruit:"🥝",koala:"🐨",koko:"🈁",label:"🏷",large_blue_circle:"🔵",large_blue_diamond:"🔷",large_orange_diamond:"🔶",last_quarter_moon:"🌗",last_quarter_moon_with_face:"🌜",latin_cross:"✝️",laughing:"😆",leaves:"🍃",ledger:"📒",left_luggage:"🛅",left_right_arrow:"↔️",leftwards_arrow_with_hook:"↩️",lemon:"🍋",leo:"♌️",leopard:"🐆",level_slider:"🎚",libra:"♎️",light_rail:"🚈",link:"🔗",lion:"🦁",lips:"👄",lipstick:"💄",lizard:"🦎",lock:"🔒",lock_with_ink_pen:"🔏",lollipop:"🍭",loop:"➿",loud_sound:"🔊",loudspeaker:"📢",love_hotel:"🏩",love_letter:"💌",low_brightness:"🔅",lying_face:"🤥",m:"Ⓜ️",mag:"🔍",mag_right:"🔎",mahjong:"🀄️",mailbox:"📫",mailbox_closed:"📪",mailbox_with_mail:"📬",mailbox_with_no_mail:"📭",man:"👨",man_artist:"👨🎨",man_astronaut:"👨🚀",man_cartwheeling:"🤸♂️",man_cook:"👨🍳",man_dancing:"🕺",man_facepalming:"🤦♂️",man_factory_worker:"👨🏭",man_farmer:"👨🌾",man_firefighter:"👨🚒",man_health_worker:"👨⚕️",man_in_tuxedo:"🤵",man_judge:"👨⚖️",man_juggling:"🤹♂️",man_mechanic:"👨🔧",man_office_worker:"👨💼",man_pilot:"👨✈️",man_playing_handball:"🤾♂️",man_playing_water_polo:"🤽♂️",man_scientist:"👨🔬",man_shrugging:"🤷♂️",man_singer:"👨🎤",man_student:"👨🎓",man_teacher:"👨🏫",man_technologist:"👨💻",man_with_gua_pi_mao:"👲",man_with_turban:"👳",tangerine:"🍊",mans_shoe:"👞",mantelpiece_clock:"🕰",maple_leaf:"🍁",martial_arts_uniform:"🥋",mask:"😷",massage_woman:"💆",massage_man:"💆♂️",meat_on_bone:"🍖",medal_military:"🎖",medal_sports:"🏅",mega:"📣",melon:"🍈",memo:"📝",men_wrestling:"🤼♂️",menorah:"🕎",mens:"🚹",metal:"🤘",metro:"🚇",microphone:"🎤",microscope:"🔬",milk_glass:"🥛",milky_way:"🌌",minibus:"🚐",minidisc:"💽",mobile_phone_off:"📴",money_mouth_face:"🤑",money_with_wings:"💸",moneybag:"💰",monkey:"🐒",monkey_face:"🐵",monorail:"🚝",moon:"🌔",mortar_board:"🎓",mosque:"🕌",motor_boat:"🛥",motor_scooter:"🛵",motorcycle:"🏍",motorway:"🛣",mount_fuji:"🗻",mountain:"⛰",mountain_biking_man:"🚵",mountain_biking_woman:"🚵♀️",mountain_cableway:"🚠",mountain_railway:"🚞",mountain_snow:"🏔",mouse:"🐭",mouse2:"🐁",movie_camera:"🎥",moyai:"🗿",mrs_claus:"🤶",muscle:"💪",mushroom:"🍄",musical_keyboard:"🎹",musical_note:"🎵",musical_score:"🎼",mute:"🔇",nail_care:"💅",name_badge:"📛",national_park:"🏞",nauseated_face:"🤢",necktie:"👔",negative_squared_cross_mark:"❎",nerd_face:"🤓",neutral_face:"😐",new:"🆕",new_moon:"🌑",new_moon_with_face:"🌚",newspaper:"📰",newspaper_roll:"🗞",next_track_button:"⏭",ng:"🆖",no_good_man:"🙅♂️",no_good_woman:"🙅",night_with_stars:"🌃",no_bell:"🔕",no_bicycles:"🚳",no_entry:"⛔️",no_entry_sign:"🚫",no_mobile_phones:"📵",no_mouth:"😶",no_pedestrians:"🚷",no_smoking:"🚭","non-potable_water":"🚱",nose:"👃",notebook:"📓",notebook_with_decorative_cover:"📔",notes:"🎶",nut_and_bolt:"🔩",o:"⭕️",o2:"🅾️",ocean:"🌊",octopus:"🐙",oden:"🍢",office:"🏢",oil_drum:"🛢",ok:"🆗",ok_hand:"👌",ok_man:"🙆♂️",ok_woman:"🙆",old_key:"🗝",older_man:"👴",older_woman:"👵",om:"🕉",on:"🔛",oncoming_automobile:"🚘",oncoming_bus:"🚍",oncoming_police_car:"🚔",oncoming_taxi:"🚖",open_file_folder:"📂",open_hands:"👐",open_mouth:"😮",open_umbrella:"☂️",ophiuchus:"⛎",orange_book:"📙",orthodox_cross:"☦️",outbox_tray:"📤",owl:"🦉",ox:"🐂",package:"📦",page_facing_up:"📄",page_with_curl:"📃",pager:"📟",paintbrush:"🖌",palm_tree:"🌴",pancakes:"🥞",panda_face:"🐼",paperclip:"📎",paperclips:"🖇",parasol_on_ground:"⛱",parking:"🅿️",part_alternation_mark:"〽️",partly_sunny:"⛅️",passenger_ship:"🛳",passport_control:"🛂",pause_button:"⏸",peace_symbol:"☮️",peach:"🍑",peanuts:"🥜",pear:"🍐",pen:"🖊",pencil2:"✏️",penguin:"🐧",pensive:"😔",performing_arts:"🎭",persevere:"😣",person_fencing:"🤺",pouting_woman:"🙎",phone:"☎️",pick:"⛏",pig:"🐷",pig2:"🐖",pig_nose:"🐽",pill:"💊",pineapple:"🍍",ping_pong:"🏓",pisces:"♓️",pizza:"🍕",place_of_worship:"🛐",plate_with_cutlery:"🍽",play_or_pause_button:"⏯",point_down:"👇",point_left:"👈",point_right:"👉",point_up:"☝️",point_up_2:"👆",police_car:"🚓",policewoman:"👮♀️",poodle:"🐩",popcorn:"🍿",post_office:"🏣",postal_horn:"📯",postbox:"📮",potable_water:"🚰",potato:"🥔",pouch:"👝",poultry_leg:"🍗",pound:"💷",rage:"😡",pouting_cat:"😾",pouting_man:"🙎♂️",pray:"🙏",prayer_beads:"📿",pregnant_woman:"🤰",previous_track_button:"⏮",prince:"🤴",princess:"👸",printer:"🖨",purple_heart:"💜",purse:"👛",pushpin:"📌",put_litter_in_its_place:"🚮",question:"❓",rabbit:"🐰",rabbit2:"🐇",racehorse:"🐎",racing_car:"🏎",radio:"📻",radio_button:"🔘",radioactive:"☢️",railway_car:"🚃",railway_track:"🛤",rainbow:"🌈",rainbow_flag:"🏳️🌈",raised_back_of_hand:"🤚",raised_hand_with_fingers_splayed:"🖐",raised_hands:"🙌",raising_hand_woman:"🙋",raising_hand_man:"🙋♂️",ram:"🐏",ramen:"🍜",rat:"🐀",record_button:"⏺",recycle:"♻️",red_circle:"🔴",registered:"®️",relaxed:"☺️",relieved:"😌",reminder_ribbon:"🎗",repeat:"🔁",repeat_one:"🔂",rescue_worker_helmet:"⛑",restroom:"🚻",revolving_hearts:"💞",rewind:"⏪",rhinoceros:"🦏",ribbon:"🎀",rice:"🍚",rice_ball:"🍙",rice_cracker:"🍘",rice_scene:"🎑",right_anger_bubble:"🗯",ring:"💍",robot:"🤖",rocket:"🚀",rofl:"🤣",roll_eyes:"🙄",roller_coaster:"🎢",rooster:"🐓",rose:"🌹",rosette:"🏵",rotating_light:"🚨",round_pushpin:"📍",rowing_man:"🚣",rowing_woman:"🚣♀️",rugby_football:"🏉",running_man:"🏃",running_shirt_with_sash:"🎽",running_woman:"🏃♀️",sa:"🈂️",sagittarius:"♐️",sake:"🍶",sandal:"👡",santa:"🎅",satellite:"📡",saxophone:"🎷",school:"🏫",school_satchel:"🎒",scissors:"✂️",scorpion:"🦂",scorpius:"♏️",scream:"😱",scream_cat:"🙀",scroll:"📜",seat:"💺",secret:"㊙️",see_no_evil:"🙈",seedling:"🌱",selfie:"🤳",shallow_pan_of_food:"🥘",shamrock:"☘️",shark:"🦈",shaved_ice:"🍧",sheep:"🐑",shell:"🐚",shield:"🛡",shinto_shrine:"⛩",ship:"🚢",shirt:"👕",shopping:"🛍",shopping_cart:"🛒",shower:"🚿",shrimp:"🦐",signal_strength:"📶",six_pointed_star:"🔯",ski:"🎿",skier:"⛷",skull:"💀",skull_and_crossbones:"☠️",sleeping:"😴",sleeping_bed:"🛌",sleepy:"😪",slightly_frowning_face:"🙁",slightly_smiling_face:"🙂",slot_machine:"🎰",small_airplane:"🛩",small_blue_diamond:"🔹",small_orange_diamond:"🔸",small_red_triangle:"🔺",small_red_triangle_down:"🔻",smile:"😄",smile_cat:"😸",smiley:"😃",smiley_cat:"😺",smiling_imp:"😈",smirk:"😏",smirk_cat:"😼",smoking:"🚬",snail:"🐌",snake:"🐍",sneezing_face:"🤧",snowboarder:"🏂",snowflake:"❄️",snowman:"⛄️",snowman_with_snow:"☃️",sob:"😭",soccer:"⚽️",soon:"🔜",sos:"🆘",sound:"🔉",space_invader:"👾",spades:"♠️",spaghetti:"🍝",sparkle:"❇️",sparkler:"🎇",sparkles:"✨",sparkling_heart:"💖",speak_no_evil:"🙊",speaker:"🔈",speaking_head:"🗣",speech_balloon:"💬",speedboat:"🚤",spider:"🕷",spider_web:"🕸",spiral_calendar:"🗓",spiral_notepad:"🗒",spoon:"🥄",squid:"🦑",stadium:"🏟",star:"⭐️",star2:"🌟",star_and_crescent:"☪️",star_of_david:"✡️",stars:"🌠",station:"🚉",statue_of_liberty:"🗽",steam_locomotive:"🚂",stew:"🍲",stop_button:"⏹",stop_sign:"🛑",stopwatch:"⏱",straight_ruler:"📏",strawberry:"🍓",stuck_out_tongue:"😛",stuck_out_tongue_closed_eyes:"😝",stuck_out_tongue_winking_eye:"😜",studio_microphone:"🎙",stuffed_flatbread:"🥙",sun_behind_large_cloud:"🌥",sun_behind_rain_cloud:"🌦",sun_behind_small_cloud:"🌤",sun_with_face:"🌞",sunflower:"🌻",sunglasses:"😎",sunny:"☀️",sunrise:"🌅",sunrise_over_mountains:"🌄",surfing_man:"🏄",surfing_woman:"🏄♀️",sushi:"🍣",suspension_railway:"🚟",sweat:"😓",sweat_drops:"💦",sweat_smile:"😅",sweet_potato:"🍠",swimming_man:"🏊",swimming_woman:"🏊♀️",symbols:"🔣",synagogue:"🕍",syringe:"💉",taco:"🌮",tada:"🎉",tanabata_tree:"🎋",taurus:"♉️",taxi:"🚕",tea:"🍵",telephone_receiver:"📞",telescope:"🔭",tennis:"🎾",tent:"⛺️",thermometer:"🌡",thinking:"🤔",thought_balloon:"💭",ticket:"🎫",tickets:"🎟",tiger:"🐯",tiger2:"🐅",timer_clock:"⏲",tipping_hand_man:"💁♂️",tired_face:"😫",tm:"™️",toilet:"🚽",tokyo_tower:"🗼",tomato:"🍅",tongue:"👅",top:"🔝",tophat:"🎩",tornado:"🌪",trackball:"🖲",tractor:"🚜",traffic_light:"🚥",train:"🚋",train2:"🚆",tram:"🚊",triangular_flag_on_post:"🚩",triangular_ruler:"📐",trident:"🔱",triumph:"😤",trolleybus:"🚎",trophy:"🏆",tropical_drink:"🍹",tropical_fish:"🐠",truck:"🚚",trumpet:"🎺",tulip:"🌷",tumbler_glass:"🥃",turkey:"🦃",turtle:"🐢",tv:"📺",twisted_rightwards_arrows:"🔀",two_hearts:"💕",two_men_holding_hands:"👬",two_women_holding_hands:"👭",u5272:"🈹",u5408:"🈴",u55b6:"🈺",u6307:"🈯️",u6708:"🈷️",u6709:"🈶",u6e80:"🈵",u7121:"🈚️",u7533:"🈸",u7981:"🈲",u7a7a:"🈳",umbrella:"☔️",unamused:"😒",underage:"🔞",unicorn:"🦄",unlock:"🔓",up:"🆙",upside_down_face:"🙃",v:"✌️",vertical_traffic_light:"🚦",vhs:"📼",vibration_mode:"📳",video_camera:"📹",video_game:"🎮",violin:"🎻",virgo:"♍️",volcano:"🌋",volleyball:"🏐",vs:"🆚",vulcan_salute:"🖖",walking_man:"🚶",walking_woman:"🚶♀️",waning_crescent_moon:"🌘",waning_gibbous_moon:"🌖",warning:"⚠️",wastebasket:"🗑",watch:"⌚️",water_buffalo:"🐃",watermelon:"🍉",wave:"👋",wavy_dash:"〰️",waxing_crescent_moon:"🌒",wc:"🚾",weary:"😩",wedding:"💒",weight_lifting_man:"🏋️",weight_lifting_woman:"🏋️♀️",whale:"🐳",whale2:"🐋",wheel_of_dharma:"☸️",wheelchair:"♿️",white_check_mark:"✅",white_circle:"⚪️",white_flag:"🏳️",white_flower:"💮",white_large_square:"⬜️",white_medium_small_square:"◽️",white_medium_square:"◻️",white_small_square:"▫️",white_square_button:"🔳",wilted_flower:"🥀",wind_chime:"🎐",wind_face:"🌬",wine_glass:"🍷",wink:"😉",wolf:"🐺",woman:"👩",woman_artist:"👩🎨",woman_astronaut:"👩🚀",woman_cartwheeling:"🤸♀️",woman_cook:"👩🍳",woman_facepalming:"🤦♀️",woman_factory_worker:"👩🏭",woman_farmer:"👩🌾",woman_firefighter:"👩🚒",woman_health_worker:"👩⚕️",woman_judge:"👩⚖️",woman_juggling:"🤹♀️",woman_mechanic:"👩🔧",woman_office_worker:"👩💼",woman_pilot:"👩✈️",woman_playing_handball:"🤾♀️",woman_playing_water_polo:"🤽♀️",woman_scientist:"👩🔬",woman_shrugging:"🤷♀️",woman_singer:"👩🎤",woman_student:"👩🎓",woman_teacher:"👩🏫",woman_technologist:"👩💻",woman_with_turban:"👳♀️",womans_clothes:"👚",womans_hat:"👒",women_wrestling:"🤼♀️",womens:"🚺",world_map:"🗺",worried:"😟",wrench:"🔧",writing_hand:"✍️",x:"❌",yellow_heart:"💛",yen:"💴",yin_yang:"☯️",yum:"😋",zap:"⚡️",zipper_mouth_face:"🤐",zzz:"💤",octocat:'',showdown:"S"},x.Converter=function(e){"use strict";var r,t,n={},i=[],l=[],o={},a=h,s={parsed:{},raw:"",format:""};for(r in e=e||{},p)p.hasOwnProperty(r)&&(n[r]=p[r]);if("object"!=typeof e)throw Error("Converter expects the passed parameter to be an object, but "+typeof e+" was passed instead.");for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);function c(e,r){if(r=r||null,x.helper.isString(e)){if(r=e=x.helper.stdExtName(e),x.extensions[e]){console.warn("DEPRECATION WARNING: "+e+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!");var t=x.extensions[e],a=e;if("function"==typeof t&&(t=t(new x.Converter)),x.helper.isArray(t)||(t=[t]),!(a=g(t,a)).valid)throw Error(a.error);for(var n=0;n[ \t]+,">¨NBSP;<"),!r){if(!window||!window.document)throw new Error("HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM");r=window.document}for(var r=r.createElement("div"),t=(r.innerHTML=e,{preList:function(e){for(var r=e.querySelectorAll("pre"),t=[],a=0;a'}else t.push(r[a].innerHTML),r[a].innerHTML="",r[a].setAttribute("prenum",a.toString());return t}(r)}),a=(!function e(r){for(var t=0;t? ?(['"].*['"])?\)$/m))a="";else if(!a){if(a="#"+(t=t||r.toLowerCase().replace(/ ?\n/g," ")),x.helper.isUndefined(l.gUrls[t]))return e;a=l.gUrls[t],x.helper.isUndefined(l.gTitles[t])||(o=l.gTitles[t])}return e='"+r+""}return e=(e=(e=(e=(e=l.converter._dispatch("anchors.before",e,i,l)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g,r)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,r)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?([\S]+?(?:\([\S]*?\)[\S]*?)?)>?(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,r)).replace(/\[([^\[\]]+)]()()()()()/g,r),i.ghMentions&&(e=e.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gim,function(e,r,t,a,n){if("\\"===t)return r+a;if(!x.helper.isString(i.ghMentionsLink))throw new Error("ghMentionsLink option must be a string");t="";return r+'"+a+""})),e=l.converter._dispatch("anchors.after",e,i,l)});var i=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi,l=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi,c=/()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi,m=/(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gim,f=/<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi;x.subParser("autoLinks",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("autoLinks.before",e,r,t)).replace(c,s(r))).replace(f,o(r,t)),e=t.converter._dispatch("autoLinks.after",e,r,t)}),x.subParser("simplifiedAutoLinks",function(e,r,t){"use strict";return r.simplifiedAutoLink?(e=t.converter._dispatch("simplifiedAutoLinks.before",e,r,t),e=(e=r.excludeTrailingPunctuationFromURLs?e.replace(l,s(r)):e.replace(i,s(r))).replace(m,o(r,t)),t.converter._dispatch("simplifiedAutoLinks.after",e,r,t)):e}),x.subParser("blockGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("blockGamut.before",e,r,t),e=x.subParser("blockQuotes")(e,r,t),e=x.subParser("headers")(e,r,t),e=x.subParser("horizontalRule")(e,r,t),e=x.subParser("lists")(e,r,t),e=x.subParser("codeBlocks")(e,r,t),e=x.subParser("tables")(e,r,t),e=x.subParser("hashHTMLBlocks")(e,r,t),e=x.subParser("paragraphs")(e,r,t),e=t.converter._dispatch("blockGamut.after",e,r,t)}),x.subParser("blockQuotes",function(e,r,t){"use strict";e=t.converter._dispatch("blockQuotes.before",e,r,t);var a=/(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm;return r.splitAdjacentBlockquotes&&(a=/^ {0,3}>[\s\S]*?(?:\n\n)/gm),e=(e+="\n\n").replace(a,function(e){return e=(e=(e=e.replace(/^[ \t]*>[ \t]?/gm,"")).replace(/¨0/g,"")).replace(/^[ \t]+$/gm,""),e=x.subParser("githubCodeBlocks")(e,r,t),e=(e=(e=x.subParser("blockGamut")(e,r,t)).replace(/(^|\n)/g,"$1 ")).replace(/(\s*[^\r]+?<\/pre>)/gm,function(e,r){return r.replace(/^ /gm,"¨0").replace(/¨0/g,"")}),x.subParser("hashBlock")("\n"+e+"\n
",r,t)}),e=t.converter._dispatch("blockQuotes.after",e,r,t)}),x.subParser("codeBlocks",function(e,n,s){"use strict";e=s.converter._dispatch("codeBlocks.before",e,n,s);return e=(e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,r,t){var a="\n",r=x.subParser("outdent")(r,n,s);return r=x.subParser("encodeCode")(r,n,s),r=""+(r=(r=(r=x.subParser("detab")(r,n,s)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+(a=n.omitExtraWLInCodeBlocks?"":a)+"
",x.subParser("hashBlock")(r,n,s)+t})).replace(/¨0/,""),e=s.converter._dispatch("codeBlocks.after",e,n,s)}),x.subParser("codeSpans",function(e,n,s){"use strict";return e=(e=void 0===(e=s.converter._dispatch("codeSpans.before",e,n,s))?"":e).replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,r,t,a){return a=(a=a.replace(/^([ \t]*)/g,"")).replace(/[ \t]*$/g,""),a=r+""+(a=x.subParser("encodeCode")(a,n,s))+"
",a=x.subParser("hashHTMLSpans")(a,n,s)}),e=s.converter._dispatch("codeSpans.after",e,n,s)}),x.subParser("completeHTMLDocument",function(e,r,t){"use strict";if(!r.completeHTMLDocument)return e;e=t.converter._dispatch("completeHTMLDocument.before",e,r,t);var a,n="html",s="\n",o="",i='\n',l="",c="";for(a in void 0!==t.metadata.parsed.doctype&&(s="\n","html"!==(n=t.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==n||(i='')),t.metadata.parsed)if(t.metadata.parsed.hasOwnProperty(a))switch(a.toLowerCase()){case"doctype":break;case"title":o=""+t.metadata.parsed.title+"\n";break;case"charset":i="html"===n||"html5"===n?'\n':'\n';break;case"language":case"lang":l=' lang="'+t.metadata.parsed[a]+'"',c+='\n';break;default:c+='\n'}return e=s+"\n\n"+o+i+c+"\n\n"+e.trim()+"\n\n",e=t.converter._dispatch("completeHTMLDocument.after",e,r,t)}),x.subParser("detab",function(e,r,t){"use strict";return e=(e=(e=(e=(e=(e=t.converter._dispatch("detab.before",e,r,t)).replace(/\t(?=\t)/g," ")).replace(/\t/g,"¨A¨B")).replace(/¨B(.+?)¨A/g,function(e,r){for(var t=r,a=4-t.length%4,n=0;n/g,">"),e=t.converter._dispatch("encodeAmpsAndAngles.after",e,r,t)}),x.subParser("encodeBackslashEscapes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("encodeBackslashEscapes.before",e,r,t)).replace(/\\(\\)/g,x.helper.escapeCharactersCallback)).replace(/\\([`*_{}\[\]()>#+.!~=|:-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeBackslashEscapes.after",e,r,t)}),x.subParser("encodeCode",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("encodeCode.before",e,r,t)).replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeCode.after",e,r,t)}),x.subParser("escapeSpecialCharsWithinTagAttributes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",e,r,t)).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)})).replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)}),e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",e,r,t)}),x.subParser("githubCodeBlocks",function(e,s,o){"use strict";return s.ghCodeBlocks?(e=o.converter._dispatch("githubCodeBlocks.before",e,s,o),e=(e=(e+="¨0").replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g,function(e,r,t,a){var n=s.omitExtraWLInCodeBlocks?"":"\n";return a=x.subParser("encodeCode")(a,s,o),a=""+(a=(a=(a=x.subParser("detab")(a,s,o)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+n+"
",a=x.subParser("hashBlock")(a,s,o),"\n\n¨G"+(o.ghCodeBlocks.push({text:e,codeblock:a})-1)+"G\n\n"})).replace(/¨0/,""),o.converter._dispatch("githubCodeBlocks.after",e,s,o)):e}),x.subParser("hashBlock",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("hashBlock.before",e,r,t)).replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n",e=t.converter._dispatch("hashBlock.after",e,r,t)}),x.subParser("hashCodeTags",function(e,n,s){"use strict";e=s.converter._dispatch("hashCodeTags.before",e,n,s);return e=x.helper.replaceRecursiveRegExp(e,function(e,r,t,a){t=t+x.subParser("encodeCode")(r,n,s)+a;return"¨C"+(s.gHtmlSpans.push(t)-1)+"C"},"]*>","
","gim"),e=s.converter._dispatch("hashCodeTags.after",e,n,s)}),x.subParser("hashElement",function(e,r,t){"use strict";return function(e,r){return r=(r=(r=r.replace(/\n\n/g,"\n")).replace(/^\n/,"")).replace(/\n+$/g,""),r="\n\n¨K"+(t.gHtmlBlocks.push(r)-1)+"K\n\n"}}),x.subParser("hashHTMLBlocks",function(e,r,n){"use strict";e=n.converter._dispatch("hashHTMLBlocks.before",e,r,n);function t(e,r,t,a){return-1!==t.search(/\bmarkdown\b/)&&(e=t+n.converter.makeHtml(r)+a),"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"}var a=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"];r.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,r){return"<"+r+">"}));for(var s=0;s]*>)","im"),i="<"+a[s]+"\\b[^>]*>",l=""+a[s]+">";-1!==(c=x.helper.regexIndexOf(e,o));){var c=x.helper.splitAtIndex(e,c),u=x.helper.replaceRecursiveRegExp(c[1],t,i,l,"im");if(u===c[1])break;e=c[0].concat(u)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=(e=x.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm")).replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=n.converter._dispatch("hashHTMLBlocks.after",e,r,n)}),x.subParser("hashHTMLSpans",function(e,r,t){"use strict";function a(e){return"¨C"+(t.gHtmlSpans.push(e)-1)+"C"}return e=(e=(e=(e=(e=t.converter._dispatch("hashHTMLSpans.before",e,r,t)).replace(/<[^>]+?\/>/gi,a)).replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,a)).replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,a)).replace(/<[^>]+?>/gi,a),e=t.converter._dispatch("hashHTMLSpans.after",e,r,t)}),x.subParser("unhashHTMLSpans",function(e,r,t){"use strict";e=t.converter._dispatch("unhashHTMLSpans.before",e,r,t);for(var a=0;a]*>\\s*]*>","^ {0,3}
\\s*
","gim"),e=s.converter._dispatch("hashPreCodeTags.after",e,n,s)}),x.subParser("headers",function(e,n,s){"use strict";e=s.converter._dispatch("headers.before",e,n,s);var o=isNaN(parseInt(n.headerLevelStart))?1:parseInt(n.headerLevelStart),r=n.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,t=n.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm,r=(e=(e=e.replace(r,function(e,r){var t=x.subParser("spanGamut")(r,n,s),r=n.noHeaderId?"":' id="'+i(r)+'"',r=""+t+"";return x.subParser("hashBlock")(r,n,s)})).replace(t,function(e,r){var t=x.subParser("spanGamut")(r,n,s),r=n.noHeaderId?"":' id="'+i(r)+'"',a=o+1,r=""+t+"";return x.subParser("hashBlock")(r,n,s)}),n.requireSpaceBeforeHeadingText?/^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm:/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm);function i(e){var r=e=n.customizedHeaderId&&(r=e.match(/\{([^{]+?)}\s*$/))&&r[1]?r[1]:e,e=x.helper.isString(n.prefixHeaderId)?n.prefixHeaderId:!0===n.prefixHeaderId?"section-":"";return n.rawPrefixHeaderId||(r=e+r),r=(n.ghCompatibleHeaderId?r.replace(/ /g,"-").replace(/&/g,"").replace(/¨T/g,"").replace(/¨D/g,"").replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g,""):n.rawHeaderId?r.replace(/ /g,"-").replace(/&/g,"&").replace(/¨T/g,"¨").replace(/¨D/g,"$").replace(/["']/g,"-"):r.replace(/[^\w]/g,"")).toLowerCase(),n.rawPrefixHeaderId&&(r=e+r),s.hashLinkCounts[r]?r=r+"-"+s.hashLinkCounts[r]++:s.hashLinkCounts[r]=1,r}return e=e.replace(r,function(e,r,t){var a=t,a=(n.customizedHeaderId&&(a=t.replace(/\s?\{([^{]+?)}\s*$/,"")),x.subParser("spanGamut")(a,n,s)),t=n.noHeaderId?"":' id="'+i(t)+'"',r=o-1+r.length,t=""+a+"";return x.subParser("hashBlock")(t,n,s)}),e=s.converter._dispatch("headers.after",e,n,s)}),x.subParser("horizontalRule",function(e,r,t){"use strict";e=t.converter._dispatch("horizontalRule.before",e,r,t);var a=x.subParser("hashBlock")("
",r,t);return e=(e=(e=e.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm,a)).replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm,a)).replace(/^ {0,2}( ?_){3,}[ \t]*$/gm,a),e=t.converter._dispatch("horizontalRule.after",e,r,t)}),x.subParser("images",function(e,r,d){"use strict";function l(e,r,t,a,n,s,o,i){var l=d.gUrls,c=d.gTitles,u=d.gDimensions;if(t=t.toLowerCase(),i=i||"",-1? ?(['"].*['"])?\)$/m))a="";else if(""===a||null===a){if(a="#"+(t=""!==t&&null!==t?t:r.toLowerCase().replace(/ ?\n/g," ")),x.helper.isUndefined(l[t]))return e;a=l[t],x.helper.isUndefined(c[t])||(i=c[t]),x.helper.isUndefined(u[t])||(n=u[t].width,s=u[t].height)}r=r.replace(/"/g,""").replace(x.helper.regexes.asteriskDashAndColon,x.helper.escapeCharactersCallback);e='"}return e=(e=(e=(e=(e=(e=d.converter._dispatch("images.before",e,r,d)).replace(/!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g,l)).replace(/!\[([^\]]*?)][ \t]*()\([ \t]?(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,function(e,r,t,a,n,s,o,i){return l(e,r,t,a=a.replace(/\s/g,""),n,s,0,i)})).replace(/!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g,l)).replace(/!\[([^\]]*?)][ \t]*()\([ \t]?([\S]+?(?:\([\S]*?\)[\S]*?)?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,l)).replace(/!\[([^\[\]]+)]()()()()()/g,l),e=d.converter._dispatch("images.after",e,r,d)}),x.subParser("italicsAndBold",function(e,r,t){"use strict";return e=t.converter._dispatch("italicsAndBold.before",e,r,t),e=r.literalMidWordUnderscores?(e=(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""})).replace(/\b_(\S[\s\S]*?)_\b/g,function(e,r){return""+r+""}):(e=(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/_([^\s_][\s\S]*?)_/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=r.literalMidWordAsterisks?(e=(e=e.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g,function(e,r,t){return r+""+t+""})).replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g,function(e,r,t){return r+""+t+""})).replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g,function(e,r,t){return r+""+t+""}):(e=(e=e.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/\*\*(\S[\s\S]*?)\*\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/\*([^\s*][\s\S]*?)\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=t.converter._dispatch("italicsAndBold.after",e,r,t)}),x.subParser("lists",function(e,d,c){"use strict";function p(e,r){c.gListLevel++,e=e.replace(/\n{2,}$/,"\n");var t=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm,l=/\n[ \t]*\n(?!¨0)/.test(e+="¨0");return d.disableForced4SpacesIndentedSublists&&(t=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm),e=(e=e.replace(t,function(e,r,t,a,n,s,o){o=o&&""!==o.trim();var n=x.subParser("outdent")(n,d,c),i="";return s&&d.tasklists&&(i=' class="task-list-item" style="list-style-type: none;"',n=n.replace(/^[ \t]*\[(x|X| )?]/m,function(){var e='"})),n=n.replace(/^([-*+]|\d\.)[ \t]+[\S\n ]*/g,function(e){return"¨A"+e}),n=""+(n=(n=r||-1\n"})).replace(/¨0/g,""),c.gListLevel--,e=r?e.replace(/\s+$/,""):e}function h(e,r){if("ol"===r){r=e.match(/^ *(\d+)\./);if(r&&"1"!==r[1])return' start="'+r[1]+'"'}return""}function n(n,s,o){var e,i=d.disableForced4SpacesIndentedSublists?/^ ?\d+\.[ \t]/gm:/^ {0,3}\d+\.[ \t]/gm,l=d.disableForced4SpacesIndentedSublists?/^ ?[*+-][ \t]/gm:/^ {0,3}[*+-][ \t]/gm,c="ul"===s?i:l,u="";return-1!==n.search(c)?function e(r){var t=r.search(c),a=h(n,s);-1!==t?(u+="\n\n<"+s+a+">\n"+p(r.slice(0,t),!!o)+""+s+">\n",c="ul"===(s="ul"===s?"ol":"ul")?i:l,e(r.slice(t))):u+="\n\n<"+s+a+">\n"+p(r,!!o)+""+s+">\n"}(n):(e=h(n,s),u="\n\n<"+s+e+">\n"+p(n,!!o)+""+s+">\n"),u}return e=c.converter._dispatch("lists.before",e,d,c),e+="¨0",e=(e=c.gListLevel?e.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t){return n(r,-1"),i+="
",n.push(i))}for(s=n.length,o=0;o]*>\s*]*>/.test(c)&&(u=!0)}n[o]=c}return e=(e=(e=n.join("\n")).replace(/^\n+/g,"")).replace(/\n+$/g,""),t.converter._dispatch("paragraphs.after",e,r,t)}),x.subParser("runExtension",function(e,r,t,a){"use strict";return e.filter?r=e.filter(r,a.converter,t):e.regex&&((a=e.regex)instanceof RegExp||(a=new RegExp(a,"g")),r=r.replace(a,e.replace)),r}),x.subParser("spanGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("spanGamut.before",e,r,t),e=x.subParser("codeSpans")(e,r,t),e=x.subParser("escapeSpecialCharsWithinTagAttributes")(e,r,t),e=x.subParser("encodeBackslashEscapes")(e,r,t),e=x.subParser("images")(e,r,t),e=x.subParser("anchors")(e,r,t),e=x.subParser("autoLinks")(e,r,t),e=x.subParser("simplifiedAutoLinks")(e,r,t),e=x.subParser("emoji")(e,r,t),e=x.subParser("underline")(e,r,t),e=x.subParser("italicsAndBold")(e,r,t),e=x.subParser("strikethrough")(e,r,t),e=x.subParser("ellipsis")(e,r,t),e=x.subParser("hashHTMLSpans")(e,r,t),e=x.subParser("encodeAmpsAndAngles")(e,r,t),r.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
\n")):e=e.replace(/ +\n/g,"
\n"),e=t.converter._dispatch("spanGamut.after",e,r,t)}),x.subParser("strikethrough",function(e,t,a){"use strict";return t.strikethrough&&(e=(e=a.converter._dispatch("strikethrough.before",e,t,a)).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,r){return r=r,""+(r=t.simplifiedAutoLink?x.subParser("simplifiedAutoLinks")(r,t,a):r)+""}),e=a.converter._dispatch("strikethrough.after",e,t,a)),e}),x.subParser("stripLinkDefinitions",function(i,l,c){"use strict";function e(e,r,t,a,n,s,o){return r=r.toLowerCase(),i.toLowerCase().split(r).length-1<2?e:(t.match(/^data:.+?\/.+?;base64,/)?c.gUrls[r]=t.replace(/\s/g,""):c.gUrls[r]=x.subParser("encodeAmpsAndAngles")(t,l,c),s?s+o:(o&&(c.gTitles[r]=o.replace(/"|'/g,""")),l.parseImgDimensions&&a&&n&&(c.gDimensions[r]={width:a,height:n}),""))}return i=(i=(i=(i+="¨0").replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,e)).replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*([^>\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,e)).replace(/¨0/,"")}),x.subParser("tables",function(e,y,P){"use strict";if(!y.tables)return e;function r(e){for(var r=e.split("\n"),t=0;t"+(n=x.subParser("spanGamut")(n,y,P))+"\n"));for(t=0;t"+x.subParser("spanGamut")(i,y,P)+"\n"));h.push(_)}for(var m=d,f=h,b="\n\n\n",w=m.length,k=0;k\n
\n\n",k=0;k\n";for(var v=0;v\n"}return b+="\n
\n"}return e=(e=(e=(e=P.converter._dispatch("tables.before",e,y,P)).replace(/\\(\|)/g,x.helper.escapeCharactersCallback)).replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm,r)).replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,r),e=P.converter._dispatch("tables.after",e,y,P)}),x.subParser("underline",function(e,r,t){"use strict";return r.underline?(e=t.converter._dispatch("underline.before",e,r,t),e=(e=r.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/(_)/g,x.helper.escapeCharactersCallback),t.converter._dispatch("underline.after",e,r,t)):e}),x.subParser("unescapeSpecialChars",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("unescapeSpecialChars.before",e,r,t)).replace(/¨E(\d+)E/g,function(e,r){r=parseInt(r);return String.fromCharCode(r)}),e=t.converter._dispatch("unescapeSpecialChars.after",e,r,t)}),x.subParser("makeMarkdown.blockquote",function(e,r){"use strict";var t="";if(e.hasChildNodes())for(var a=e.childNodes,n=a.length,s=0;s ")}),x.subParser("makeMarkdown.codeBlock",function(e,r){"use strict";var t=e.getAttribute("language"),e=e.getAttribute("precodenum");return"```"+t+"\n"+r.preList[e]+"\n```"}),x.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),x.subParser("makeMarkdown.emphasis",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="*";for(var a=e.childNodes,n=a.length,s=0;s",e.hasAttribute("width")&&e.hasAttribute("height")&&(r+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"),r}),x.subParser("makeMarkdown.links",function(e,r){"use strict";var t="";if(e.hasChildNodes()&&e.hasAttribute("href")){for(var a=e.childNodes,n=a.length,t="[",s=0;s"),e.hasAttribute("title")&&(t+=' "'+e.getAttribute("title")+'"'),t+=")"}return t}),x.subParser("makeMarkdown.list",function(e,r,t){"use strict";var a="";if(!e.hasChildNodes())return"";for(var n=e.childNodes,s=n.length,o=e.getAttribute("start")||1,i=0;i"+r.preList[e]+""}),x.subParser("makeMarkdown.strikethrough",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="~~";for(var a=e.childNodes,n=a.length,s=0;str>th"),s=e.querySelectorAll("tbody>tr"),o=0;o/g,"\\$1>")).replace(/^#/gm,"\\#")).replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3")).replace(/^( {0,3}\d+)\./gm,"$1\\.")).replace(/^( {0,3})([+-])/gm,"$1\\$2")).replace(/]([\s]*)\(/g,"\\]$1\\(")).replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")});"function"==typeof define&&define.amd?define(function(){"use strict";return x}):"undefined"!=typeof module&&module.exports?module.exports=x:this.showdown=x}.call(this);
diff --git a/lib/Configuration.php b/lib/Configuration.php
index f1e2897f..5588791a 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -53,7 +53,7 @@ class Configuration
'languagedefault' => '',
'urlshortener' => '',
'qrcode' => true,
- 'icon' => 'jdenticon',
+ 'icon' => 'identicon',
'cspheader' => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
'zerobincompatibility' => false,
'httpwarning' => true,
diff --git a/lib/Data/AbstractData.php b/lib/Data/AbstractData.php
index 05353ce0..b28fd539 100644
--- a/lib/Data/AbstractData.php
+++ b/lib/Data/AbstractData.php
@@ -15,61 +15,17 @@ namespace PrivateBin\Data;
/**
* AbstractData
*
- * Abstract model for data access, implemented as a singleton.
+ * Abstract model for data access
*/
abstract class AbstractData
{
- /**
- * Singleton instance
- *
- * @access protected
- * @static
- * @var AbstractData
- */
- protected static $_instance = null;
-
/**
* cache for the traffic limiter
*
- * @access private
- * @static
+ * @access protected
* @var array
*/
- protected static $_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)
- {
- }
+ protected $_last_cache = array();
/**
* Create a paste.
@@ -150,9 +106,9 @@ abstract class AbstractData
public function purgeValues($namespace, $time)
{
if ($namespace === 'traffic_limiter') {
- foreach (self::$_last_cache as $key => $last_submission) {
+ foreach ($this->_last_cache as $key => $last_submission) {
if ($last_submission <= $time) {
- unset(self::$_last_cache[$key]);
+ unset($this->_last_cache[$key]);
}
}
}
@@ -207,6 +163,14 @@ abstract class AbstractData
}
}
+ /**
+ * Returns all paste ids
+ *
+ * @access public
+ * @return array
+ */
+ abstract public function getAllPastes();
+
/**
* Get next free slot for comment from postdate.
*
diff --git a/lib/Data/Database.php b/lib/Data/Database.php
index 3ca7b756..4636f3ce 100644
--- a/lib/Data/Database.php
+++ b/lib/Data/Database.php
@@ -25,59 +25,43 @@ use PrivateBin\Json;
*/
class Database extends AbstractData
{
- /**
- * cache for select queries
- *
- * @var array
- */
- private static $_cache = array();
-
/**
* instance of database connection
*
* @access private
- * @static
* @var PDO
*/
- private static $_db;
+ private $_db;
/**
* table prefix
*
* @access private
- * @static
* @var string
*/
- private static $_prefix = '';
+ private $_prefix = '';
/**
* database type
*
* @access private
- * @static
* @var string
*/
- private static $_type = '';
+ private $_type = '';
/**
- * get instance of singleton
+ * instantiates a new Database data backend
*
* @access public
- * @static
* @param array $options
* @throws Exception
- * @return Database
+ * @return
*/
- public static function getInstance(array $options)
+ public function __construct(array $options)
{
- // if needed initialize the singleton
- if (!(self::$_instance instanceof self)) {
- self::$_instance = new self;
- }
-
// set table prefix if given
if (array_key_exists('tbl', $options)) {
- self::$_prefix = $options['tbl'];
+ $this->_prefix = $options['tbl'];
}
// initialize the db connection with new options
@@ -94,16 +78,16 @@ class Database extends AbstractData
$db_tables_exist = true;
// setup type and dabase connection
- self::$_type = strtolower(
+ $this->_type = strtolower(
substr($options['dsn'], 0, strpos($options['dsn'], ':'))
);
// MySQL uses backticks to quote identifiers by default,
// tell it to expect ANSI SQL double quotes
- if (self::$_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
+ if ($this->_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
$options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
}
- $tableQuery = self::_getTableQuery(self::$_type);
- self::$_db = new PDO(
+ $tableQuery = $this->_getTableQuery($this->_type);
+ $this->_db = new PDO(
$options['dsn'],
$options['usr'],
$options['pwd'],
@@ -111,43 +95,41 @@ class Database extends AbstractData
);
// check if the database contains the required tables
- $tables = self::$_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
+ $tables = $this->_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if necessary
- if (!in_array(self::_sanitizeIdentifier('paste'), $tables)) {
- self::_createPasteTable();
+ if (!in_array($this->_sanitizeIdentifier('paste'), $tables)) {
+ $this->_createPasteTable();
$db_tables_exist = false;
}
// create comment table if necessary
- if (!in_array(self::_sanitizeIdentifier('comment'), $tables)) {
- self::_createCommentTable();
+ if (!in_array($this->_sanitizeIdentifier('comment'), $tables)) {
+ $this->_createCommentTable();
$db_tables_exist = false;
}
// create config table if necessary
$db_version = Controller::VERSION;
- if (!in_array(self::_sanitizeIdentifier('config'), $tables)) {
- self::_createConfigTable();
+ if (!in_array($this->_sanitizeIdentifier('config'), $tables)) {
+ $this->_createConfigTable();
// if we only needed to create the config table, the DB is older then 0.22
if ($db_tables_exist) {
$db_version = '0.21';
}
} else {
- $db_version = self::_getConfig('VERSION');
+ $db_version = $this->_getConfig('VERSION');
}
// update database structure if necessary
if (version_compare($db_version, Controller::VERSION, '<')) {
- self::_upgradeDatabase($db_version);
+ $this->_upgradeDatabase($db_version);
}
} else {
throw new Exception(
'Missing configuration for key dsn, usr, pwd or opt in the section model_options, please check your configuration file', 6
);
}
-
- return self::$_instance;
}
/**
@@ -160,22 +142,12 @@ class Database extends AbstractData
*/
public function create($pasteid, array $paste)
{
- if (
- array_key_exists($pasteid, self::$_cache)
- ) {
- if (false !== self::$_cache[$pasteid]) {
- return false;
- } else {
- unset(self::$_cache[$pasteid]);
- }
- }
-
$expire_date = 0;
$opendiscussion = $burnafterreading = false;
$attachment = $attachmentname = null;
$meta = $paste['meta'];
$isVersion1 = array_key_exists('data', $paste);
- list($createdKey) = self::_getVersionedKeys($isVersion1 ? 1 : 2);
+ list($createdKey) = $this->_getVersionedKeys($isVersion1 ? 1 : 2);
$created = (int) $meta[$createdKey];
unset($meta[$createdKey], $paste['meta']);
if (array_key_exists('expire_date', $meta)) {
@@ -204,8 +176,8 @@ class Database extends AbstractData
$burnafterreading = $paste['adata'][3];
}
try {
- return self::_exec(
- 'INSERT INTO "' . self::_sanitizeIdentifier('paste') .
+ return $this->_exec(
+ 'INSERT INTO "' . $this->_sanitizeIdentifier('paste') .
'" VALUES(?,?,?,?,?,?,?,?,?)',
array(
$pasteid,
@@ -233,64 +205,59 @@ class Database extends AbstractData
*/
public function read($pasteid)
{
- if (array_key_exists($pasteid, self::$_cache)) {
- return self::$_cache[$pasteid];
- }
-
- self::$_cache[$pasteid] = false;
try {
- $paste = self::_select(
- 'SELECT * FROM "' . self::_sanitizeIdentifier('paste') .
+ $row = $this->_select(
+ 'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') .
'" WHERE "dataid" = ?', array($pasteid), true
);
} catch (Exception $e) {
- $paste = false;
+ $row = false;
}
- if ($paste === false) {
+ if ($row === false) {
return false;
}
// create array
- $data = Json::decode($paste['data']);
+ $data = Json::decode($row['data']);
$isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2;
if ($isVersion2) {
- self::$_cache[$pasteid] = $data;
- list($createdKey) = self::_getVersionedKeys(2);
+ $paste = $data;
+ list($createdKey) = $this->_getVersionedKeys(2);
} else {
- self::$_cache[$pasteid] = array('data' => $paste['data']);
- list($createdKey) = self::_getVersionedKeys(1);
+ $paste = array('data' => $row['data']);
+ list($createdKey) = $this->_getVersionedKeys(1);
}
try {
- $paste['meta'] = Json::decode($paste['meta']);
+ $row['meta'] = Json::decode($row['meta']);
} catch (Exception $e) {
- $paste['meta'] = array();
+ $row['meta'] = array();
}
- $paste = self::upgradePreV1Format($paste);
- self::$_cache[$pasteid]['meta'] = $paste['meta'];
- self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste['postdate'];
- $expire_date = (int) $paste['expiredate'];
+ $row = self::upgradePreV1Format($row);
+ $paste['meta'] = $row['meta'];
+ $paste['meta'][$createdKey] = (int) $row['postdate'];
+ $expire_date = (int) $row['expiredate'];
if ($expire_date > 0) {
- self::$_cache[$pasteid]['meta']['expire_date'] = $expire_date;
+ $paste['meta']['expire_date'] = $expire_date;
}
if ($isVersion2) {
- return self::$_cache[$pasteid];
+ return $paste;
}
// support v1 attachments
- if (array_key_exists('attachment', $paste) && !empty($paste['attachment'])) {
- self::$_cache[$pasteid]['attachment'] = $paste['attachment'];
- if (array_key_exists('attachmentname', $paste) && !empty($paste['attachmentname'])) {
- self::$_cache[$pasteid]['attachmentname'] = $paste['attachmentname'];
+ if (array_key_exists('attachment', $row) && !empty($row['attachment'])) {
+ $paste['attachment'] = $row['attachment'];
+ if (array_key_exists('attachmentname', $row) && !empty($row['attachmentname'])) {
+ $paste['attachmentname'] = $row['attachmentname'];
}
}
- if ($paste['opendiscussion']) {
- self::$_cache[$pasteid]['meta']['opendiscussion'] = true;
+ if ($row['opendiscussion']) {
+ $paste['meta']['opendiscussion'] = true;
}
- if ($paste['burnafterreading']) {
- self::$_cache[$pasteid]['meta']['burnafterreading'] = true;
+ if ($row['burnafterreading']) {
+ $paste['meta']['burnafterreading'] = true;
}
- return self::$_cache[$pasteid];
+ return $paste;
}
/**
@@ -301,19 +268,14 @@ class Database extends AbstractData
*/
public function delete($pasteid)
{
- self::_exec(
- 'DELETE FROM "' . self::_sanitizeIdentifier('paste') .
+ $this->_exec(
+ 'DELETE FROM "' . $this->_sanitizeIdentifier('paste') .
'" WHERE "dataid" = ?', array($pasteid)
);
- self::_exec(
- 'DELETE FROM "' . self::_sanitizeIdentifier('comment') .
+ $this->_exec(
+ 'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
'" WHERE "pasteid" = ?', array($pasteid)
);
- if (
- array_key_exists($pasteid, self::$_cache)
- ) {
- unset(self::$_cache[$pasteid]);
- }
}
/**
@@ -325,12 +287,15 @@ class Database extends AbstractData
*/
public function exists($pasteid)
{
- if (
- !array_key_exists($pasteid, self::$_cache)
- ) {
- self::$_cache[$pasteid] = $this->read($pasteid);
+ try {
+ $row = $this->_select(
+ 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
+ '" WHERE "dataid" = ?', array($pasteid), true
+ );
+ } catch (Exception $e) {
+ return false;
}
- return (bool) self::$_cache[$pasteid];
+ return (bool) $row;
}
/**
@@ -352,7 +317,7 @@ class Database extends AbstractData
$version = 2;
$data = Json::encode($comment);
}
- list($createdKey, $iconKey) = self::_getVersionedKeys($version);
+ list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
$meta = $comment['meta'];
unset($comment['meta']);
foreach (array('nickname', $iconKey) as $key) {
@@ -361,8 +326,8 @@ class Database extends AbstractData
}
}
try {
- return self::_exec(
- 'INSERT INTO "' . self::_sanitizeIdentifier('comment') .
+ return $this->_exec(
+ 'INSERT INTO "' . $this->_sanitizeIdentifier('comment') .
'" VALUES(?,?,?,?,?,?,?)',
array(
$commentid,
@@ -388,8 +353,8 @@ class Database extends AbstractData
*/
public function readComments($pasteid)
{
- $rows = self::_select(
- 'SELECT * FROM "' . self::_sanitizeIdentifier('comment') .
+ $rows = $this->_select(
+ 'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
'" WHERE "pasteid" = ?', array($pasteid)
);
@@ -406,7 +371,7 @@ class Database extends AbstractData
$version = 1;
$comments[$i] = array('data' => $row['data']);
}
- list($createdKey, $iconKey) = self::_getVersionedKeys($version);
+ list($createdKey, $iconKey) = $this->_getVersionedKeys($version);
$comments[$i]['id'] = $row['dataid'];
$comments[$i]['parentid'] = $row['parentid'];
$comments[$i]['meta'] = array($createdKey => (int) $row['postdate']);
@@ -433,8 +398,8 @@ class Database extends AbstractData
public function existsComment($pasteid, $parentid, $commentid)
{
try {
- return (bool) self::_select(
- 'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('comment') .
+ return (bool) $this->_select(
+ 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
'" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
array($pasteid, $parentid, $commentid), true
);
@@ -455,15 +420,15 @@ class Database extends AbstractData
public function setValue($value, $namespace, $key = '')
{
if ($namespace === 'traffic_limiter') {
- self::$_last_cache[$key] = $value;
+ $this->_last_cache[$key] = $value;
try {
- $value = Json::encode(self::$_last_cache);
+ $value = Json::encode($this->_last_cache);
} catch (Exception $e) {
return false;
}
}
- return self::_exec(
- 'UPDATE "' . self::_sanitizeIdentifier('config') .
+ return $this->_exec(
+ 'UPDATE "' . $this->_sanitizeIdentifier('config') .
'" SET "value" = ? WHERE "id" = ?',
array($value, strtoupper($namespace))
);
@@ -483,8 +448,8 @@ class Database extends AbstractData
$value = $this->_getConfig($configKey);
if ($value === '') {
// initialize the row, so that setValue can rely on UPDATE queries
- self::_exec(
- 'INSERT INTO "' . self::_sanitizeIdentifier('config') .
+ $this->_exec(
+ 'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
'" VALUES(?,?)',
array($configKey, '')
);
@@ -492,7 +457,8 @@ class Database extends AbstractData
// migrate filesystem based salt into database
$file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
if ($namespace === 'salt' && is_readable($file)) {
- $value = Filesystem::getInstance(array('dir' => 'data'))->getValue('salt');
+ $fs = new Filesystem(array('dir' => 'data'));
+ $value = $fs->getValue('salt');
$this->setValue($value, 'salt');
@unlink($file);
return $value;
@@ -500,12 +466,12 @@ class Database extends AbstractData
}
if ($value && $namespace === 'traffic_limiter') {
try {
- self::$_last_cache = Json::decode($value);
+ $this->_last_cache = Json::decode($value);
} catch (Exception $e) {
- self::$_last_cache = array();
+ $this->_last_cache = array();
}
- if (array_key_exists($key, self::$_last_cache)) {
- return self::$_last_cache[$key];
+ if (array_key_exists($key, $this->_last_cache)) {
+ return $this->_last_cache[$key];
}
}
return (string) $value;
@@ -520,34 +486,37 @@ class Database extends AbstractData
*/
protected function _getExpiredPastes($batchsize)
{
- $pastes = array();
- $rows = self::_select(
- 'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('paste') .
+ $statement = $this->_db->prepare(
+ 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
'" WHERE "expiredate" < ? AND "expiredate" != ? ' .
- (self::$_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?'),
- array(time(), 0, $batchsize)
+ ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?')
);
- if (is_array($rows) && count($rows)) {
- foreach ($rows as $row) {
- $pastes[] = $row['dataid'];
- }
- }
- return $pastes;
+ $statement->execute(array(time(), 0, $batchsize));
+ return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAllPastes()
+ {
+ return $this->_db->query(
+ 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . '"'
+ )->fetchAll(PDO::FETCH_COLUMN, 0);
}
/**
* execute a statement
*
* @access private
- * @static
* @param string $sql
* @param array $params
* @throws PDOException
* @return bool
*/
- private static function _exec($sql, array $params)
+ private function _exec($sql, array $params)
{
- $statement = self::$_db->prepare($sql);
+ $statement = $this->_db->prepare($sql);
foreach ($params as $key => &$parameter) {
$position = $key + 1;
if (is_int($parameter)) {
@@ -567,20 +536,19 @@ class Database extends AbstractData
* run a select statement
*
* @access private
- * @static
* @param string $sql
* @param array $params
* @param bool $firstOnly if only the first row should be returned
* @throws PDOException
* @return array|false
*/
- private static function _select($sql, array $params, $firstOnly = false)
+ private function _select($sql, array $params, $firstOnly = false)
{
- $statement = self::$_db->prepare($sql);
+ $statement = $this->_db->prepare($sql);
$statement->execute($params);
if ($firstOnly) {
$result = $statement->fetch(PDO::FETCH_ASSOC);
- } elseif (self::$_type === 'oci') {
+ } elseif ($this->_type === 'oci') {
// workaround for https://bugs.php.net/bug.php?id=46728
$result = array();
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
@@ -590,7 +558,7 @@ class Database extends AbstractData
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
}
$statement->closeCursor();
- if (self::$_type === 'oci' && is_array($result)) {
+ if ($this->_type === 'oci' && is_array($result)) {
// returned CLOB values are streams, convert these into strings
$result = $firstOnly ?
array_map('PrivateBin\Data\Database::_sanitizeClob', $result) :
@@ -603,11 +571,10 @@ class Database extends AbstractData
* get version dependent key names
*
* @access private
- * @static
* @param int $version
* @return array
*/
- private static function _getVersionedKeys($version)
+ private function _getVersionedKeys($version)
{
if ($version === 1) {
return array('postdate', 'vizhash');
@@ -619,12 +586,11 @@ class Database extends AbstractData
* get table list query, depending on the database type
*
* @access private
- * @static
* @param string $type
* @throws Exception
* @return string
*/
- private static function _getTableQuery($type)
+ private function _getTableQuery($type)
{
switch ($type) {
case 'ibm':
@@ -675,15 +641,14 @@ class Database extends AbstractData
* get a value by key from the config table
*
* @access private
- * @static
* @param string $key
* @return string
*/
- private static function _getConfig($key)
+ private function _getConfig($key)
{
try {
- $row = self::_select(
- 'SELECT "value" FROM "' . self::_sanitizeIdentifier('config') .
+ $row = $this->_select(
+ 'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') .
'" WHERE "id" = ?', array($key), true
);
} catch (PDOException $e) {
@@ -696,14 +661,13 @@ class Database extends AbstractData
* get the primary key clauses, depending on the database driver
*
* @access private
- * @static
* @param string $key
* @return array
*/
- private static function _getPrimaryKeyClauses($key = 'dataid')
+ private function _getPrimaryKeyClauses($key = 'dataid')
{
$main_key = $after_key = '';
- switch (self::$_type) {
+ switch ($this->_type) {
case 'mysql':
case 'oci':
$after_key = ", PRIMARY KEY (\"$key\")";
@@ -721,12 +685,11 @@ class Database extends AbstractData
* PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB
*
* @access private
- * @static
* @return string
*/
- private static function _getDataType()
+ private function _getDataType()
{
- switch (self::$_type) {
+ switch ($this->_type) {
case 'oci':
return 'CLOB';
case 'pgsql':
@@ -742,12 +705,11 @@ class Database extends AbstractData
* PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB
*
* @access private
- * @static
* @return string
*/
- private static function _getAttachmentType()
+ private function _getAttachmentType()
{
- switch (self::$_type) {
+ switch ($this->_type) {
case 'oci':
return 'CLOB';
case 'pgsql':
@@ -763,12 +725,11 @@ class Database extends AbstractData
* OCI doesn't accept TEXT so it has to be VARCHAR2(4000)
*
* @access private
- * @static
* @return string
*/
- private static function _getMetaType()
+ private function _getMetaType()
{
- switch (self::$_type) {
+ switch ($this->_type) {
case 'oci':
return 'VARCHAR2(4000)';
default:
@@ -780,16 +741,15 @@ class Database extends AbstractData
* create the paste table
*
* @access private
- * @static
*/
- private static function _createPasteTable()
+ private function _createPasteTable()
{
- list($main_key, $after_key) = self::_getPrimaryKeyClauses();
- $dataType = self::_getDataType();
- $attachmentType = self::_getAttachmentType();
- $metaType = self::_getMetaType();
- self::$_db->exec(
- 'CREATE TABLE "' . self::_sanitizeIdentifier('paste') . '" ( ' .
+ list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
+ $dataType = $this->_getDataType();
+ $attachmentType = $this->_getAttachmentType();
+ $metaType = $this->_getMetaType();
+ $this->_db->exec(
+ 'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
"\"dataid\" CHAR(16) NOT NULL$main_key, " .
"\"data\" $attachmentType, " .
'"postdate" INT, ' .
@@ -806,14 +766,13 @@ class Database extends AbstractData
* create the paste table
*
* @access private
- * @static
*/
- private static function _createCommentTable()
+ private function _createCommentTable()
{
- list($main_key, $after_key) = self::_getPrimaryKeyClauses();
- $dataType = self::_getDataType();
- self::$_db->exec(
- 'CREATE TABLE "' . self::_sanitizeIdentifier('comment') . '" ( ' .
+ list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
+ $dataType = $this->_getDataType();
+ $this->_db->exec(
+ 'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
"\"dataid\" CHAR(16) NOT NULL$main_key, " .
'"pasteid" CHAR(16), ' .
'"parentid" CHAR(16), ' .
@@ -822,15 +781,15 @@ class Database extends AbstractData
"\"vizhash\" $dataType, " .
"\"postdate\" INT$after_key )"
);
- if (self::$_type === 'oci') {
- self::$_db->exec(
+ if ($this->_type === 'oci') {
+ $this->_db->exec(
'declare
already_exists exception;
columns_indexed exception;
pragma exception_init( already_exists, -955 );
pragma exception_init(columns_indexed, -1408);
begin
- execute immediate \'create index "comment_parent" on "' . self::_sanitizeIdentifier('comment') . '" ("pasteid")\';
+ execute immediate \'create index "comment_parent" on "' . $this->_sanitizeIdentifier('comment') . '" ("pasteid")\';
exception
when already_exists or columns_indexed then
NULL;
@@ -838,10 +797,10 @@ class Database extends AbstractData
);
} else {
// CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
- self::$_db->exec(
+ $this->_db->exec(
'CREATE INDEX "' .
- self::_sanitizeIdentifier('comment_parent') . '" ON "' .
- self::_sanitizeIdentifier('comment') . '" ("pasteid")'
+ $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
+ $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
);
}
}
@@ -850,19 +809,18 @@ class Database extends AbstractData
* create the paste table
*
* @access private
- * @static
*/
- private static function _createConfigTable()
+ private function _createConfigTable()
{
- list($main_key, $after_key) = self::_getPrimaryKeyClauses('id');
- $charType = self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
- $textType = self::_getMetaType();
- self::$_db->exec(
- 'CREATE TABLE "' . self::_sanitizeIdentifier('config') .
+ list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
+ $charType = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
+ $textType = $this->_getMetaType();
+ $this->_db->exec(
+ 'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
"\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
);
- self::_exec(
- 'INSERT INTO "' . self::_sanitizeIdentifier('config') .
+ $this->_exec(
+ 'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
'" VALUES(?,?)',
array('VERSION', Controller::VERSION)
);
@@ -890,90 +848,88 @@ class Database extends AbstractData
* sanitizes identifiers
*
* @access private
- * @static
* @param string $identifier
* @return string
*/
- private static function _sanitizeIdentifier($identifier)
+ private function _sanitizeIdentifier($identifier)
{
- return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier);
+ return preg_replace('/[^A-Za-z0-9_]+/', '', $this->_prefix . $identifier);
}
/**
* upgrade the database schema from an old version
*
* @access private
- * @static
* @param string $oldversion
*/
- private static function _upgradeDatabase($oldversion)
+ private function _upgradeDatabase($oldversion)
{
- $dataType = self::_getDataType();
- $attachmentType = self::_getAttachmentType();
+ $dataType = $this->_getDataType();
+ $attachmentType = $this->_getAttachmentType();
switch ($oldversion) {
case '0.21':
// create the meta column if necessary (pre 0.21 change)
try {
- self::$_db->exec(
- 'SELECT "meta" FROM "' . self::_sanitizeIdentifier('paste') . '" ' .
- (self::$_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
+ $this->_db->exec(
+ 'SELECT "meta" FROM "' . $this->_sanitizeIdentifier('paste') . '" ' .
+ ($this->_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1')
);
} catch (PDOException $e) {
- self::$_db->exec('ALTER TABLE "' . self::_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
+ $this->_db->exec('ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT');
}
// SQLite only allows one ALTER statement at a time...
- self::$_db->exec(
- 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+ $this->_db->exec(
+ 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
"\" ADD COLUMN \"attachment\" $attachmentType"
);
- self::$_db->exec(
- 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
+ $this->_db->exec(
+ 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType"
);
// SQLite doesn't support MODIFY, but it allows TEXT of similar
// size as BLOB, so there is no need to change it there
- if (self::$_type !== 'sqlite') {
- self::$_db->exec(
- 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+ if ($this->_type !== 'sqlite') {
+ $this->_db->exec(
+ 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
"\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType"
);
- self::$_db->exec(
- 'ALTER TABLE "' . self::_sanitizeIdentifier('comment') .
+ $this->_db->exec(
+ 'ALTER TABLE "' . $this->_sanitizeIdentifier('comment') .
"\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " .
"MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType"
);
} else {
- self::$_db->exec(
+ $this->_db->exec(
'CREATE UNIQUE INDEX IF NOT EXISTS "' .
- self::_sanitizeIdentifier('paste_dataid') . '" ON "' .
- self::_sanitizeIdentifier('paste') . '" ("dataid")'
+ $this->_sanitizeIdentifier('paste_dataid') . '" ON "' .
+ $this->_sanitizeIdentifier('paste') . '" ("dataid")'
);
- self::$_db->exec(
+ $this->_db->exec(
'CREATE UNIQUE INDEX IF NOT EXISTS "' .
- self::_sanitizeIdentifier('comment_dataid') . '" ON "' .
- self::_sanitizeIdentifier('comment') . '" ("dataid")'
+ $this->_sanitizeIdentifier('comment_dataid') . '" ON "' .
+ $this->_sanitizeIdentifier('comment') . '" ("dataid")'
);
}
// CREATE INDEX IF NOT EXISTS not supported as of Oracle MySQL <= 8.0
- self::$_db->exec(
+ $this->_db->exec(
'CREATE INDEX "' .
- self::_sanitizeIdentifier('comment_parent') . '" ON "' .
- self::_sanitizeIdentifier('comment') . '" ("pasteid")'
+ $this->_sanitizeIdentifier('comment_parent') . '" ON "' .
+ $this->_sanitizeIdentifier('comment') . '" ("pasteid")'
);
// no break, continue with updates for 0.22 and later
case '1.3':
// SQLite doesn't support MODIFY, but it allows TEXT of similar
// size as BLOB and PostgreSQL uses TEXT, so there is no need
// to change it there
- if (self::$_type !== 'sqlite' && self::$_type !== 'pgsql') {
- self::$_db->exec(
- 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') .
+ if ($this->_type !== 'sqlite' && $this->_type !== 'pgsql') {
+ $this->_db->exec(
+ 'ALTER TABLE "' . $this->_sanitizeIdentifier('paste') .
"\" MODIFY COLUMN \"data\" $attachmentType"
);
}
// no break, continue with updates for all newer versions
default:
- self::_exec(
- 'UPDATE "' . self::_sanitizeIdentifier('config') .
+ $this->_exec(
+ 'UPDATE "' . $this->_sanitizeIdentifier('config') .
'" SET "value" = ? WHERE "id" = ?',
array(Controller::VERSION, 'VERSION')
);
diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php
index 9674513c..1f00b577 100644
--- a/lib/Data/Filesystem.php
+++ b/lib/Data/Filesystem.php
@@ -40,33 +40,26 @@ class Filesystem extends AbstractData
* path in which to persist something
*
* @access private
- * @static
* @var string
*/
- private static $_path = 'data';
+ private $_path = 'data';
/**
- * get instance of singleton
+ * instantiates a new Filesystem data backend
*
* @access public
- * @static
* @param array $options
- * @return Filesystem
+ * @return
*/
- public static function getInstance(array $options)
+ public function __construct(array $options)
{
- // if needed initialize the singleton
- if (!(self::$_instance instanceof self)) {
- self::$_instance = new self;
- }
// if given update the data directory
if (
is_array($options) &&
array_key_exists('dir', $options)
) {
- self::$_path = $options['dir'];
+ $this->_path = $options['dir'];
}
- return self::$_instance;
}
/**
@@ -79,7 +72,7 @@ class Filesystem extends AbstractData
*/
public function create($pasteid, array $paste)
{
- $storagedir = self::_dataid2path($pasteid);
+ $storagedir = $this->_dataid2path($pasteid);
$file = $storagedir . $pasteid . '.php';
if (is_file($file)) {
return false;
@@ -87,7 +80,7 @@ class Filesystem extends AbstractData
if (!is_dir($storagedir)) {
mkdir($storagedir, 0700, true);
}
- return self::_store($file, $paste);
+ return $this->_store($file, $paste);
}
/**
@@ -101,7 +94,7 @@ class Filesystem extends AbstractData
{
if (
!$this->exists($pasteid) ||
- !$paste = self::_get(self::_dataid2path($pasteid) . $pasteid . '.php')
+ !$paste = $this->_get($this->_dataid2path($pasteid) . $pasteid . '.php')
) {
return false;
}
@@ -116,7 +109,7 @@ class Filesystem extends AbstractData
*/
public function delete($pasteid)
{
- $pastedir = self::_dataid2path($pasteid);
+ $pastedir = $this->_dataid2path($pasteid);
if (is_dir($pastedir)) {
// Delete the paste itself.
if (is_file($pastedir . $pasteid . '.php')) {
@@ -124,7 +117,7 @@ class Filesystem extends AbstractData
}
// Delete discussion if it exists.
- $discdir = self::_dataid2discussionpath($pasteid);
+ $discdir = $this->_dataid2discussionpath($pasteid);
if (is_dir($discdir)) {
// Delete all files in discussion directory
$dir = dir($discdir);
@@ -148,20 +141,20 @@ class Filesystem extends AbstractData
*/
public function exists($pasteid)
{
- $basePath = self::_dataid2path($pasteid) . $pasteid;
+ $basePath = $this->_dataid2path($pasteid) . $pasteid;
$pastePath = $basePath . '.php';
// convert to PHP protected files if needed
if (is_readable($basePath)) {
- self::_prependRename($basePath, $pastePath);
+ $this->_prependRename($basePath, $pastePath);
// convert comments, too
- $discdir = self::_dataid2discussionpath($pasteid);
+ $discdir = $this->_dataid2discussionpath($pasteid);
if (is_dir($discdir)) {
$dir = dir($discdir);
while (false !== ($filename = $dir->read())) {
if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) {
$commentFilename = $discdir . $filename . '.php';
- self::_prependRename($discdir . $filename, $commentFilename);
+ $this->_prependRename($discdir . $filename, $commentFilename);
}
}
$dir->close();
@@ -182,7 +175,7 @@ class Filesystem extends AbstractData
*/
public function createComment($pasteid, $parentid, $commentid, array $comment)
{
- $storagedir = self::_dataid2discussionpath($pasteid);
+ $storagedir = $this->_dataid2discussionpath($pasteid);
$file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php';
if (is_file($file)) {
return false;
@@ -190,7 +183,7 @@ class Filesystem extends AbstractData
if (!is_dir($storagedir)) {
mkdir($storagedir, 0700, true);
}
- return self::_store($file, $comment);
+ return $this->_store($file, $comment);
}
/**
@@ -203,7 +196,7 @@ class Filesystem extends AbstractData
public function readComments($pasteid)
{
$comments = array();
- $discdir = self::_dataid2discussionpath($pasteid);
+ $discdir = $this->_dataid2discussionpath($pasteid);
if (is_dir($discdir)) {
$dir = dir($discdir);
while (false !== ($filename = $dir->read())) {
@@ -212,7 +205,7 @@ class Filesystem extends AbstractData
// - commentid is the comment identifier itself.
// - parentid is the comment this comment replies to (It can be pasteid)
if (is_file($discdir . $filename)) {
- $comment = self::_get($discdir . $filename);
+ $comment = $this->_get($discdir . $filename);
$items = explode('.', $filename);
// Add some meta information not contained in file.
$comment['id'] = $items[1];
@@ -243,7 +236,7 @@ class Filesystem extends AbstractData
public function existsComment($pasteid, $parentid, $commentid)
{
return is_file(
- self::_dataid2discussionpath($pasteid) .
+ $this->_dataid2discussionpath($pasteid) .
$pasteid . '.' . $commentid . '.' . $parentid . '.php'
);
}
@@ -261,20 +254,20 @@ class Filesystem extends AbstractData
{
switch ($namespace) {
case 'purge_limiter':
- return self::_storeString(
- self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
+ return $this->_storeString(
+ $this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
'_storeString(
+ $this->_path . DIRECTORY_SEPARATOR . 'salt.php',
'_last_cache[$key] = $value;
+ return $this->_storeString(
+ $this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
+ '_last_cache, true) . ';'
);
}
return false;
@@ -292,14 +285,14 @@ class Filesystem extends AbstractData
{
switch ($namespace) {
case 'purge_limiter':
- $file = self::$_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
+ $file = $this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php';
if (is_readable($file)) {
require $file;
return $GLOBALS['purge_limiter'];
}
break;
case 'salt':
- $file = self::$_path . DIRECTORY_SEPARATOR . 'salt.php';
+ $file = $this->_path . DIRECTORY_SEPARATOR . 'salt.php';
if (is_readable($file)) {
$items = explode('|', file_get_contents($file));
if (is_array($items) && count($items) == 3) {
@@ -308,12 +301,12 @@ class Filesystem extends AbstractData
}
break;
case 'traffic_limiter':
- $file = self::$_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
+ $file = $this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php';
if (is_readable($file)) {
require $file;
- self::$_last_cache = $GLOBALS['traffic_limiter'];
- if (array_key_exists($key, self::$_last_cache)) {
- return self::$_last_cache[$key];
+ $this->_last_cache = $GLOBALS['traffic_limiter'];
+ if (array_key_exists($key, $this->_last_cache)) {
+ return $this->_last_cache[$key];
}
}
break;
@@ -325,11 +318,10 @@ class Filesystem extends AbstractData
* get the data
*
* @access public
- * @static
* @param string $filename
* @return array|false $data
*/
- private static function _get($filename)
+ private function _get($filename)
{
return Json::decode(
substr(
@@ -348,63 +340,25 @@ class Filesystem extends AbstractData
*/
protected function _getExpiredPastes($batchsize)
{
- $pastes = array();
- $firstLevel = array_filter(
- scandir(self::$_path),
- 'PrivateBin\Data\Filesystem::_isFirstLevelDir'
- );
- if (count($firstLevel) > 0) {
- // try at most 10 times the $batchsize pastes before giving up
- for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
- $firstKey = array_rand($firstLevel);
- $secondLevel = array_filter(
- scandir(self::$_path . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]),
- 'PrivateBin\Data\Filesystem::_isSecondLevelDir'
- );
-
- // skip this folder in the next checks if it is empty
- if (count($secondLevel) == 0) {
- unset($firstLevel[$firstKey]);
- continue;
- }
-
- $secondKey = array_rand($secondLevel);
- $path = self::$_path . DIRECTORY_SEPARATOR .
- $firstLevel[$firstKey] . DIRECTORY_SEPARATOR .
- $secondLevel[$secondKey];
- if (!is_dir($path)) {
- continue;
- }
- $thirdLevel = array_filter(
- array_map(
- function ($filename) {
- return strlen($filename) >= 20 ?
- substr($filename, 0, -4) :
- $filename;
- },
- scandir($path)
- ),
- 'PrivateBin\\Model\\Paste::isValidId'
- );
- if (count($thirdLevel) == 0) {
- continue;
- }
- $thirdKey = array_rand($thirdLevel);
- $pasteid = $thirdLevel[$thirdKey];
- if (in_array($pasteid, $pastes)) {
- continue;
- }
-
- if ($this->exists($pasteid)) {
- $data = $this->read($pasteid);
- if (
- array_key_exists('expire_date', $data['meta']) &&
- $data['meta']['expire_date'] < time()
- ) {
- $pastes[] = $pasteid;
- if (count($pastes) >= $batchsize) {
- break;
- }
+ $pastes = array();
+ $files = $this->_getPasteIterator();
+ $count = 0;
+ $time = time();
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ continue;
+ }
+ $pasteid = $file->getBasename('.php');
+ if ($this->exists($pasteid)) {
+ $data = $this->read($pasteid);
+ if (
+ array_key_exists('expire_date', $data['meta']) &&
+ $data['meta']['expire_date'] < $time
+ ) {
+ $pastes[] = $pasteid;
+ ++$count;
+ if ($count >= $batchsize) {
+ break;
}
}
}
@@ -412,6 +366,21 @@ class Filesystem extends AbstractData
return $pastes;
}
+ /**
+ * @inheritDoc
+ */
+ public function getAllPastes()
+ {
+ $pastes = array();
+ $files = $this->_getPasteIterator();
+ foreach ($files as $file) {
+ if ($file->isFile()) {
+ $pastes[] = $file->getBasename('.php');
+ }
+ }
+ return $pastes;
+ }
+
/**
* Convert paste id to storage path.
*
@@ -423,13 +392,12 @@ class Filesystem extends AbstractData
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
*
* @access private
- * @static
* @param string $dataid
* @return string
*/
- private static function _dataid2path($dataid)
+ private function _dataid2path($dataid)
{
- return self::$_path . DIRECTORY_SEPARATOR .
+ return $this->_path . DIRECTORY_SEPARATOR .
substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
}
@@ -440,56 +408,44 @@ class Filesystem extends AbstractData
* eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
*
* @access private
- * @static
* @param string $dataid
* @return string
*/
- private static function _dataid2discussionpath($dataid)
+ private function _dataid2discussionpath($dataid)
{
- return self::_dataid2path($dataid) . $dataid .
+ return $this->_dataid2path($dataid) . $dataid .
'.discussion' . DIRECTORY_SEPARATOR;
}
/**
- * Check that the given element is a valid first level directory.
+ * Get an iterator matching paste files.
*
* @access private
- * @static
- * @param string $element
- * @return bool
+ * @return \GlobIterator
*/
- private static function _isFirstLevelDir($element)
+ private function _getPasteIterator()
{
- return self::_isSecondLevelDir($element) &&
- is_dir(self::$_path . DIRECTORY_SEPARATOR . $element);
- }
-
- /**
- * Check that the given element is a valid second level directory.
- *
- * @access private
- * @static
- * @param string $element
- * @return bool
- */
- private static function _isSecondLevelDir($element)
- {
- return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
+ return new \GlobIterator($this->_path . DIRECTORY_SEPARATOR .
+ '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR .
+ '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR .
+ '[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]' .
+ '[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]*');
+ // need to return both files with and without .php suffix, so they can
+ // be hardened by _prependRename(), which is hooked into exists()
}
/**
* store the data
*
* @access public
- * @static
* @param string $filename
* @param array $data
* @return bool
*/
- private static function _store($filename, array $data)
+ private function _store($filename, array $data)
{
try {
- return self::_storeString(
+ return $this->_storeString(
$filename,
self::PROTECTION_LINE . PHP_EOL . Json::encode($data)
);
@@ -502,20 +458,19 @@ class Filesystem extends AbstractData
* store a string
*
* @access public
- * @static
* @param string $filename
* @param string $data
* @return bool
*/
- private static function _storeString($filename, $data)
+ private function _storeString($filename, $data)
{
// Create storage directory if it does not exist.
- if (!is_dir(self::$_path)) {
- if (!@mkdir(self::$_path, 0700)) {
+ if (!is_dir($this->_path)) {
+ if (!@mkdir($this->_path, 0700)) {
return false;
}
}
- $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess';
+ $file = $this->_path . DIRECTORY_SEPARATOR . '.htaccess';
if (!is_file($file)) {
$writtenBytes = 0;
if ($fileCreated = @touch($file)) {
@@ -553,12 +508,11 @@ class Filesystem extends AbstractData
* rename a file, prepending the protection line at the beginning
*
* @access public
- * @static
* @param string $srcFile
* @param string $destFile
* @return void
*/
- private static function _prependRename($srcFile, $destFile)
+ private function _prependRename($srcFile, $destFile)
{
// don't overwrite already converted file
if (!is_readable($destFile)) {
diff --git a/lib/Data/GoogleCloudStorage.php b/lib/Data/GoogleCloudStorage.php
index e8763258..e07e8182 100644
--- a/lib/Data/GoogleCloudStorage.php
+++ b/lib/Data/GoogleCloudStorage.php
@@ -14,54 +14,43 @@ class GoogleCloudStorage extends AbstractData
* GCS client
*
* @access private
- * @static
* @var StorageClient
*/
- private static $_client = null;
+ private $_client = null;
/**
* GCS bucket
*
* @access private
- * @static
* @var Bucket
*/
- private static $_bucket = null;
+ private $_bucket = null;
/**
* object prefix
*
* @access private
- * @static
* @var string
*/
- private static $_prefix = 'pastes';
+ private $_prefix = 'pastes';
/**
* bucket acl type
*
* @access private
- * @static
* @var bool
*/
- private static $_uniformacl = false;
+ private $_uniformacl = false;
/**
- * returns a Google Cloud Storage data backend.
+ * instantiantes a new Google Cloud Storage data backend.
*
* @access public
- * @static
* @param array $options
- * @return GoogleCloudStorage
+ * @return
*/
- public static function getInstance(array $options)
+ public function __construct(array $options)
{
- // if needed initialize the singleton
- if (!(self::$_instance instanceof self)) {
- self::$_instance = new self;
- }
-
- $bucket = null;
if (getenv('PRIVATEBIN_GCS_BUCKET')) {
$bucket = getenv('PRIVATEBIN_GCS_BUCKET');
}
@@ -69,24 +58,22 @@ class GoogleCloudStorage extends AbstractData
$bucket = $options['bucket'];
}
if (is_array($options) && array_key_exists('prefix', $options)) {
- self::$_prefix = $options['prefix'];
+ $this->_prefix = $options['prefix'];
}
if (is_array($options) && array_key_exists('uniformacl', $options)) {
- self::$_uniformacl = $options['uniformacl'];
+ $this->_uniformacl = $options['uniformacl'];
}
- if (empty(self::$_client)) {
- self::$_client = class_exists('StorageClientStub', false) ?
- new \StorageClientStub(array()) :
- new StorageClient(array('suppressKeyFileNotice' => true));
+ $this->_client = class_exists('StorageClientStub', false) ?
+ new \StorageClientStub(array()) :
+ new StorageClient(array('suppressKeyFileNotice' => true));
+ if (isset($bucket)) {
+ $this->_bucket = $this->_client->bucket($bucket);
}
- self::$_bucket = self::$_client->bucket($bucket);
-
- return self::$_instance;
}
/**
- * returns the google storage object key for $pasteid in self::$_bucket.
+ * returns the google storage object key for $pasteid in $this->_bucket.
*
* @access private
* @param $pasteid string to get the key for
@@ -94,14 +81,14 @@ class GoogleCloudStorage extends AbstractData
*/
private function _getKey($pasteid)
{
- if (self::$_prefix != '') {
- return self::$_prefix . '/' . $pasteid;
+ if ($this->_prefix != '') {
+ return $this->_prefix . '/' . $pasteid;
}
return $pasteid;
}
/**
- * Uploads the payload in the self::$_bucket under the specified key.
+ * Uploads the payload in the $this->_bucket under the specified key.
* The entire payload is stored as a JSON document. The metadata is replicated
* as the GCS object's metadata except for the fields attachment, attachmentname
* and salt.
@@ -126,12 +113,12 @@ class GoogleCloudStorage extends AbstractData
'metadata' => $metadata,
),
);
- if (!self::$_uniformacl) {
+ if (!$this->_uniformacl) {
$data['predefinedAcl'] = 'private';
}
- self::$_bucket->upload(Json::encode($payload), $data);
+ $this->_bucket->upload(Json::encode($payload), $data);
} catch (Exception $e) {
- error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' .
+ error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -156,13 +143,13 @@ class GoogleCloudStorage extends AbstractData
public function read($pasteid)
{
try {
- $o = self::$_bucket->object($this->_getKey($pasteid));
+ $o = $this->_bucket->object($this->_getKey($pasteid));
$data = $o->downloadAsString();
return Json::decode($data);
} catch (NotFoundException $e) {
return false;
} catch (Exception $e) {
- error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket->name() . ', ' .
+ error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -176,9 +163,9 @@ class GoogleCloudStorage extends AbstractData
$name = $this->_getKey($pasteid);
try {
- foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
+ foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
try {
- self::$_bucket->object($comment->name())->delete();
+ $this->_bucket->object($comment->name())->delete();
} catch (NotFoundException $e) {
// ignore if already deleted.
}
@@ -188,7 +175,7 @@ class GoogleCloudStorage extends AbstractData
}
try {
- self::$_bucket->object($name)->delete();
+ $this->_bucket->object($name)->delete();
} catch (NotFoundException $e) {
// ignore if already deleted
}
@@ -199,7 +186,7 @@ class GoogleCloudStorage extends AbstractData
*/
public function exists($pasteid)
{
- $o = self::$_bucket->object($this->_getKey($pasteid));
+ $o = $this->_bucket->object($this->_getKey($pasteid));
return $o->exists();
}
@@ -223,8 +210,8 @@ class GoogleCloudStorage extends AbstractData
$comments = array();
$prefix = $this->_getKey($pasteid) . '/discussion/';
try {
- foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) {
- $comment = JSON::decode(self::$_bucket->object($key->name())->downloadAsString());
+ foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
+ $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
$comment['id'] = basename($key->name());
$slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
$comments[$slot] = $comment;
@@ -241,7 +228,7 @@ class GoogleCloudStorage extends AbstractData
public function existsComment($pasteid, $parentid, $commentid)
{
$name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
- $o = self::$_bucket->object($name);
+ $o = $this->_bucket->object($name);
return $o->exists();
}
@@ -252,7 +239,7 @@ class GoogleCloudStorage extends AbstractData
{
$path = 'config/' . $namespace;
try {
- foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) {
+ foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
$name = $object->name();
if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
continue;
@@ -300,12 +287,12 @@ class GoogleCloudStorage extends AbstractData
'metadata' => $metadata,
),
);
- if (!self::$_uniformacl) {
+ if (!$this->_uniformacl) {
$data['predefinedAcl'] = 'private';
}
- self::$_bucket->upload($value, $data);
+ $this->_bucket->upload($value, $data);
} catch (Exception $e) {
- error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' .
+ error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -323,7 +310,7 @@ class GoogleCloudStorage extends AbstractData
$key = 'config/' . $namespace . '/' . $key;
}
try {
- $o = self::$_bucket->object($key);
+ $o = $this->_bucket->object($key);
return $o->downloadAsString();
} catch (NotFoundException $e) {
return '';
@@ -338,12 +325,12 @@ class GoogleCloudStorage extends AbstractData
$expired = array();
$now = time();
- $prefix = self::$_prefix;
+ $prefix = $this->_prefix;
if ($prefix != '') {
$prefix .= '/';
}
try {
- foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) {
+ foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
$metadata = $object->info()['metadata'];
if ($metadata != null && array_key_exists('expire_date', $metadata)) {
$expire_at = intval($metadata['expire_date']);
@@ -361,4 +348,28 @@ class GoogleCloudStorage extends AbstractData
}
return $expired;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function getAllPastes()
+ {
+ $pastes = array();
+ $prefix = $this->_prefix;
+ if ($prefix != '') {
+ $prefix .= '/';
+ }
+
+ try {
+ foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
+ $candidate = substr($object->name(), strlen($prefix));
+ if (strpos($candidate, '/') === false) {
+ $pastes[] = $candidate;
+ }
+ }
+ } catch (NotFoundException $e) {
+ // no objects in the bucket yet
+ }
+ return $pastes;
+ }
}
diff --git a/lib/Data/S3Storage.php b/lib/Data/S3Storage.php
index d741e099..f2746507 100644
--- a/lib/Data/S3Storage.php
+++ b/lib/Data/S3Storage.php
@@ -45,86 +45,71 @@ class S3Storage extends AbstractData
* S3 client
*
* @access private
- * @static
* @var S3Client
*/
- private static $_client = null;
+ private $_client = null;
/**
* S3 client options
*
* @access private
- * @static
* @var array
*/
- private static $_options = array();
+ private $_options = array();
/**
* S3 bucket
*
* @access private
- * @static
* @var string
*/
- private static $_bucket = null;
+ private $_bucket = null;
/**
* S3 prefix for all PrivateBin data in this bucket
*
* @access private
- * @static
* @var string
*/
- private static $_prefix = '';
+ private $_prefix = '';
/**
- * returns an S3 data backend.
+ * instantiates a new S3 data backend.
*
* @access public
- * @static
* @param array $options
- * @return S3Storage
+ * @return
*/
- public static function getInstance(array $options)
+ public function __construct(array $options)
{
- // if needed initialize the singleton
- if (!(self::$_instance instanceof self)) {
- self::$_instance = new self;
- }
-
- self::$_options = array();
- self::$_options['credentials'] = array();
+ $this->_options['credentials'] = array();
if (is_array($options) && array_key_exists('region', $options)) {
- self::$_options['region'] = $options['region'];
+ $this->_options['region'] = $options['region'];
}
if (is_array($options) && array_key_exists('version', $options)) {
- self::$_options['version'] = $options['version'];
+ $this->_options['version'] = $options['version'];
}
if (is_array($options) && array_key_exists('endpoint', $options)) {
- self::$_options['endpoint'] = $options['endpoint'];
+ $this->_options['endpoint'] = $options['endpoint'];
}
if (is_array($options) && array_key_exists('accesskey', $options)) {
- self::$_options['credentials']['key'] = $options['accesskey'];
+ $this->_options['credentials']['key'] = $options['accesskey'];
}
if (is_array($options) && array_key_exists('secretkey', $options)) {
- self::$_options['credentials']['secret'] = $options['secretkey'];
+ $this->_options['credentials']['secret'] = $options['secretkey'];
}
if (is_array($options) && array_key_exists('use_path_style_endpoint', $options)) {
- self::$_options['use_path_style_endpoint'] = filter_var($options['use_path_style_endpoint'], FILTER_VALIDATE_BOOLEAN);
+ $this->_options['use_path_style_endpoint'] = filter_var($options['use_path_style_endpoint'], FILTER_VALIDATE_BOOLEAN);
}
if (is_array($options) && array_key_exists('bucket', $options)) {
- self::$_bucket = $options['bucket'];
+ $this->_bucket = $options['bucket'];
}
if (is_array($options) && array_key_exists('prefix', $options)) {
- self::$_prefix = $options['prefix'];
+ $this->_prefix = $options['prefix'];
}
- if (empty(self::$_client)) {
- self::$_client = new S3Client(self::$_options);
- }
-
- return self::$_instance;
+ $this->_client = new S3Client($this->_options);
}
/**
@@ -138,12 +123,12 @@ class S3Storage extends AbstractData
{
$allObjects = array();
$options = array(
- 'Bucket' => self::$_bucket,
+ 'Bucket' => $this->_bucket,
'Prefix' => $prefix,
);
do {
- $objectsListResponse = self::$_client->listObjects($options);
+ $objectsListResponse = $this->_client->listObjects($options);
$objects = $objectsListResponse['Contents'] ?? array();
foreach ($objects as $object) {
$allObjects[] = $object;
@@ -155,7 +140,7 @@ class S3Storage extends AbstractData
}
/**
- * returns the S3 storage object key for $pasteid in self::$_bucket.
+ * returns the S3 storage object key for $pasteid in $this->_bucket.
*
* @access private
* @param $pasteid string to get the key for
@@ -163,14 +148,14 @@ class S3Storage extends AbstractData
*/
private function _getKey($pasteid)
{
- if (self::$_prefix != '') {
- return self::$_prefix . '/' . $pasteid;
+ if ($this->_prefix != '') {
+ return $this->_prefix . '/' . $pasteid;
}
return $pasteid;
}
/**
- * Uploads the payload in the self::$_bucket under the specified key.
+ * Uploads the payload in the $this->_bucket under the specified key.
* The entire payload is stored as a JSON document. The metadata is replicated
* as the S3 object's metadata except for the fields attachment, attachmentname
* and salt.
@@ -187,15 +172,15 @@ class S3Storage extends AbstractData
$metadata[$k] = strval($v);
}
try {
- self::$_client->putObject(array(
- 'Bucket' => self::$_bucket,
+ $this->_client->putObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $key,
'Body' => Json::encode($payload),
'ContentType' => 'application/json',
'Metadata' => $metadata,
));
} catch (S3Exception $e) {
- error_log('failed to upload ' . $key . ' to ' . self::$_bucket . ', ' .
+ error_log('failed to upload ' . $key . ' to ' . $this->_bucket . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -220,14 +205,14 @@ class S3Storage extends AbstractData
public function read($pasteid)
{
try {
- $object = self::$_client->getObject(array(
- 'Bucket' => self::$_bucket,
+ $object = $this->_client->getObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $this->_getKey($pasteid),
));
$data = $object['Body']->getContents();
return Json::decode($data);
} catch (S3Exception $e) {
- error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket . ', ' .
+ error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -244,8 +229,8 @@ class S3Storage extends AbstractData
$comments = $this->_listAllObjects($name . '/discussion/');
foreach ($comments as $comment) {
try {
- self::$_client->deleteObject(array(
- 'Bucket' => self::$_bucket,
+ $this->_client->deleteObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $comment['Key'],
));
} catch (S3Exception $e) {
@@ -257,8 +242,8 @@ class S3Storage extends AbstractData
}
try {
- self::$_client->deleteObject(array(
- 'Bucket' => self::$_bucket,
+ $this->_client->deleteObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $name,
));
} catch (S3Exception $e) {
@@ -271,7 +256,7 @@ class S3Storage extends AbstractData
*/
public function exists($pasteid)
{
- return self::$_client->doesObjectExistV2(self::$_bucket, $this->_getKey($pasteid));
+ return $this->_client->doesObjectExistV2($this->_bucket, $this->_getKey($pasteid));
}
/**
@@ -296,8 +281,8 @@ class S3Storage extends AbstractData
try {
$entries = $this->_listAllObjects($prefix);
foreach ($entries as $entry) {
- $object = self::$_client->getObject(array(
- 'Bucket' => self::$_bucket,
+ $object = $this->_client->getObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $entry['Key'],
));
$body = JSON::decode($object['Body']->getContents());
@@ -319,7 +304,7 @@ class S3Storage extends AbstractData
public function existsComment($pasteid, $parentid, $commentid)
{
$name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
- return self::$_client->doesObjectExistV2(self::$_bucket, $name);
+ return $this->_client->doesObjectExistV2($this->_bucket, $name);
}
/**
@@ -327,7 +312,7 @@ class S3Storage extends AbstractData
*/
public function purgeValues($namespace, $time)
{
- $path = self::$_prefix;
+ $path = $this->_prefix;
if ($path != '') {
$path .= '/';
}
@@ -339,16 +324,16 @@ class S3Storage extends AbstractData
if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
continue;
}
- $head = self::$_client->headObject(array(
- 'Bucket' => self::$_bucket,
+ $head = $this->_client->headObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $name,
));
- if (array_key_exists('Metadata', $head) && array_key_exists('value', $head['Metadata'])) {
- $value = $head['Metadata']['value'];
+ if ($head->get('Metadata') != null && array_key_exists('value', $head->get('Metadata'))) {
+ $value = $head->get('Metadata')['value'];
if (is_numeric($value) && intval($value) < $time) {
try {
- self::$_client->deleteObject(array(
- 'Bucket' => self::$_bucket,
+ $this->_client->deleteObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $name,
));
} catch (S3Exception $e) {
@@ -369,7 +354,7 @@ class S3Storage extends AbstractData
*/
public function setValue($value, $namespace, $key = '')
{
- $prefix = self::$_prefix;
+ $prefix = $this->_prefix;
if ($prefix != '') {
$prefix .= '/';
}
@@ -385,15 +370,15 @@ class S3Storage extends AbstractData
$metadata['value'] = strval($value);
}
try {
- self::$_client->putObject(array(
- 'Bucket' => self::$_bucket,
+ $this->_client->putObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $key,
'Body' => $value,
'ContentType' => 'application/json',
'Metadata' => $metadata,
));
} catch (S3Exception $e) {
- error_log('failed to set key ' . $key . ' to ' . self::$_bucket . ', ' .
+ error_log('failed to set key ' . $key . ' to ' . $this->_bucket . ', ' .
trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
return false;
}
@@ -405,7 +390,7 @@ class S3Storage extends AbstractData
*/
public function getValue($namespace, $key = '')
{
- $prefix = self::$_prefix;
+ $prefix = $this->_prefix;
if ($prefix != '') {
$prefix .= '/';
}
@@ -417,8 +402,8 @@ class S3Storage extends AbstractData
}
try {
- $object = self::$_client->getObject(array(
- 'Bucket' => self::$_bucket,
+ $object = $this->_client->getObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $key,
));
return $object['Body']->getContents();
@@ -434,19 +419,19 @@ class S3Storage extends AbstractData
{
$expired = array();
$now = time();
- $prefix = self::$_prefix;
+ $prefix = $this->_prefix;
if ($prefix != '') {
$prefix .= '/';
}
try {
foreach ($this->_listAllObjects($prefix) as $object) {
- $head = self::$_client->headObject(array(
- 'Bucket' => self::$_bucket,
+ $head = $this->_client->headObject(array(
+ 'Bucket' => $this->_bucket,
'Key' => $object['Key'],
));
- if (array_key_exists('Metadata', $head) && array_key_exists('expire_date', $head['Metadata'])) {
- $expire_at = intval($head['Metadata']['expire_date']);
+ if ($head->get('Metadata') != null && array_key_exists('expire_date', $head->get('Metadata'))) {
+ $expire_at = intval($head->get('Metadata')['expire_date']);
if ($expire_at != 0 && $expire_at < $now) {
array_push($expired, $object['Key']);
}
@@ -461,4 +446,28 @@ class S3Storage extends AbstractData
}
return $expired;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function getAllPastes()
+ {
+ $pastes = array();
+ $prefix = $this->_prefix;
+ if ($prefix != '') {
+ $prefix .= '/';
+ }
+
+ try {
+ foreach ($this->_listAllObjects($prefix) as $object) {
+ $candidate = substr($object['Key'], strlen($prefix));
+ if (strpos($candidate, '/') === false) {
+ $pastes[] = $candidate;
+ }
+ }
+ } catch (S3Exception $e) {
+ // no objects in the bucket yet
+ }
+ return $pastes;
+ }
}
diff --git a/lib/I18n.php b/lib/I18n.php
index c3c635f7..5c469381 100644
--- a/lib/I18n.php
+++ b/lib/I18n.php
@@ -328,6 +328,7 @@ class I18n
return $n === 1 ? 0 : ($n === 2 ? 1 : (($n < 0 || $n > 10) && ($n % 10 === 0) ? 2 : 3));
case 'id':
case 'jbo':
+ case 'th':
return 0;
case 'lt':
return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
diff --git a/lib/Model.php b/lib/Model.php
index 360cf68c..f7fdc232 100644
--- a/lib/Model.php
+++ b/lib/Model.php
@@ -81,10 +81,8 @@ class Model
public function getStore()
{
if ($this->_store === null) {
- $this->_store = forward_static_call(
- 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model') . '::getInstance',
- $this->_conf->getSection('model_options')
- );
+ $class = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
+ $this->_store = new $class($this->_conf->getSection('model_options'));
}
return $this->_store;
}
diff --git a/lib/Model/Comment.php b/lib/Model/Comment.php
index 8e4142ee..ffb27fc4 100644
--- a/lib/Model/Comment.php
+++ b/lib/Model/Comment.php
@@ -165,7 +165,10 @@ class Comment extends AbstractModel
if ($icon != 'none') {
$pngdata = '';
$hmac = TrafficLimiter::getHash();
- if ($icon == 'jdenticon') {
+ if ($icon == 'identicon') {
+ $identicon = new Identicon();
+ $pngdata = $identicon->getImageDataUri($hmac, 16);
+ } elseif ($icon == 'jdenticon') {
$jdenticon = new Jdenticon(array(
'hash' => $hmac,
'size' => 16,
@@ -175,9 +178,6 @@ class Comment extends AbstractModel
),
));
$pngdata = $jdenticon->getImageDataUri('png');
- } elseif ($icon == 'identicon') {
- $identicon = new Identicon();
- $pngdata = $identicon->getImageDataUri($hmac, 16);
} elseif ($icon == 'vizhash') {
$vh = new Vizhash16x16();
$pngdata = 'data:image/png;base64,' . base64_encode(
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index 3dd49135..d5c57f63 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -42,7 +42,7 @@ if ($SYNTAXHIGHLIGHTING) :
endif;
?>
-
+
@@ -67,13 +67,13 @@ if ($SYNTAXHIGHLIGHTING) :
endif;
if ($MARKDOWN) :
?>
-
+
-
+
-
+
diff --git a/tpl/page.php b/tpl/page.php
index f91cb7dd..34d0079e 100644
--- a/tpl/page.php
+++ b/tpl/page.php
@@ -21,7 +21,7 @@ if ($SYNTAXHIGHLIGHTING):
endif;
endif;
?>
-
+
@@ -45,13 +45,13 @@ if ($SYNTAXHIGHLIGHTING):
endif;
if ($MARKDOWN):
?>
-
+
-
+
-
+
diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php
index 5b6012f0..48a91cb8 100644
--- a/tst/Bootstrap.php
+++ b/tst/Bootstrap.php
@@ -32,9 +32,9 @@ Helper::updateSubresourceIntegrity();
*/
class StorageClientStub extends StorageClient
{
- private $_config = null;
- private $_connection = null;
- private $_buckets = array();
+ private $_config = null;
+ private $_connection = null;
+ private static $_buckets = array();
public function __construct(array $config = array())
{
@@ -44,11 +44,11 @@ class StorageClientStub extends StorageClient
public function bucket($name, $userProject = false)
{
- if (!key_exists($name, $this->_buckets)) {
+ if (!key_exists($name, self::$_buckets)) {
$b = new BucketStub($this->_connection, $name, array(), $this);
- $this->_buckets[$name] = $b;
+ self::$_buckets[$name] = $b;
}
- return $this->_buckets[$name];
+ return self::$_buckets[$name];
}
/**
@@ -56,8 +56,8 @@ class StorageClientStub extends StorageClient
*/
public function deleteBucket($name)
{
- if (key_exists($name, $this->_buckets)) {
- unset($this->_buckets[$name]);
+ if (key_exists($name, self::$_buckets)) {
+ unset(self::$_buckets[$name]);
} else {
throw new NotFoundException();
}
@@ -110,11 +110,11 @@ class StorageClientStub extends StorageClient
public function createBucket($name, array $options = array())
{
- if (key_exists($name, $this->_buckets)) {
+ if (key_exists($name, self::$_buckets)) {
throw new BadRequestException('already exists');
}
$b = new BucketStub($this->_connection, $name, array(), $this);
- $this->_buckets[$name] = $b;
+ self::$_buckets[$name] = $b;
return $b;
}
}
diff --git a/tst/ControllerTest.php b/tst/ControllerTest.php
index 6656cf66..5c1ae2b2 100644
--- a/tst/ControllerTest.php
+++ b/tst/ControllerTest.php
@@ -17,7 +17,7 @@ class ControllerTest extends TestCase
{
/* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
- $this->_data = Filesystem::getInstance(array('dir' => $this->_path));
+ $this->_data = new Filesystem(array('dir' => $this->_path));
ServerSalt::setStore($this->_data);
TrafficLimiter::setStore($this->_data);
$this->reset();
diff --git a/tst/ControllerWithDbTest.php b/tst/ControllerWithDbTest.php
index c64b71eb..d8ec1114 100644
--- a/tst/ControllerWithDbTest.php
+++ b/tst/ControllerWithDbTest.php
@@ -25,7 +25,7 @@ class ControllerWithDbTest extends ControllerTest
mkdir($this->_path);
}
$this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3';
- $this->_data = Database::getInstance($this->_options);
+ $this->_data = new Database($this->_options);
ServerSalt::setStore($this->_data);
TrafficLimiter::setStore($this->_data);
$this->reset();
diff --git a/tst/ControllerWithGcsTest.php b/tst/ControllerWithGcsTest.php
index 9921d106..3e1b828d 100644
--- a/tst/ControllerWithGcsTest.php
+++ b/tst/ControllerWithGcsTest.php
@@ -39,7 +39,7 @@ class ControllerWithGcsTest extends ControllerTest
'bucket' => self::$_bucket->name(),
'prefix' => 'pastes',
);
- $this->_data = GoogleCloudStorage::getInstance($this->_options);
+ $this->_data = new GoogleCloudStorage($this->_options);
ServerSalt::setStore($this->_data);
TrafficLimiter::setStore($this->_data);
$this->reset();
diff --git a/tst/Data/DatabaseTest.php b/tst/Data/DatabaseTest.php
index 20c72acf..1c5b79de 100644
--- a/tst/Data/DatabaseTest.php
+++ b/tst/Data/DatabaseTest.php
@@ -23,7 +23,7 @@ class DatabaseTest extends TestCase
{
/* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
- $this->_model = Database::getInstance($this->_options);
+ $this->_model = new Database($this->_options);
}
public function tearDown(): void
@@ -36,7 +36,7 @@ class DatabaseTest extends TestCase
public function testSaltMigration()
{
- ServerSalt::setStore(Filesystem::getInstance(array('dir' => 'data')));
+ ServerSalt::setStore(new Filesystem(array('dir' => 'data')));
$salt = ServerSalt::get();
$file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
$this->assertFileExists($file, 'ServerSalt got initialized and stored on disk');
@@ -140,7 +140,7 @@ class DatabaseTest extends TestCase
public function testGetIbmInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'ibm:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -149,7 +149,7 @@ class DatabaseTest extends TestCase
public function testGetInformixInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'informix:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -158,7 +158,7 @@ class DatabaseTest extends TestCase
public function testGetMssqlInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'mssql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -167,7 +167,7 @@ class DatabaseTest extends TestCase
public function testGetMysqlInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'mysql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -176,7 +176,7 @@ class DatabaseTest extends TestCase
public function testGetOciInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'oci:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -185,7 +185,7 @@ class DatabaseTest extends TestCase
public function testGetPgsqlInstance()
{
$this->expectException(PDOException::class);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'pgsql:', 'usr' => null, 'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
));
@@ -195,7 +195,7 @@ class DatabaseTest extends TestCase
{
$this->expectException(Exception::class);
$this->expectExceptionCode(5);
- Database::getInstance(array(
+ new Database(array(
'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null,
));
}
@@ -206,7 +206,7 @@ class DatabaseTest extends TestCase
unset($options['dsn']);
$this->expectException(Exception::class);
$this->expectExceptionCode(6);
- Database::getInstance($options);
+ new Database($options);
}
public function testMissingUsr()
@@ -215,7 +215,7 @@ class DatabaseTest extends TestCase
unset($options['usr']);
$this->expectException(Exception::class);
$this->expectExceptionCode(6);
- Database::getInstance($options);
+ new Database($options);
}
public function testMissingPwd()
@@ -224,7 +224,7 @@ class DatabaseTest extends TestCase
unset($options['pwd']);
$this->expectException(Exception::class);
$this->expectExceptionCode(6);
- Database::getInstance($options);
+ new Database($options);
}
public function testMissingOpt()
@@ -233,7 +233,7 @@ class DatabaseTest extends TestCase
unset($options['opt']);
$this->expectException(Exception::class);
$this->expectExceptionCode(6);
- Database::getInstance($options);
+ new Database($options);
}
public function testOldAttachments()
@@ -245,7 +245,7 @@ class DatabaseTest extends TestCase
}
$this->_options['dsn'] = 'sqlite:' . $path;
$this->_options['tbl'] = 'bar_';
- $model = Database::getInstance($this->_options);
+ $model = new Database($this->_options);
$original = $paste = Helper::getPasteWithAttachment(1, array('expire_date' => 1344803344));
$meta = $paste['meta'];
@@ -290,7 +290,7 @@ class DatabaseTest extends TestCase
}
$this->_options['dsn'] = 'sqlite:' . $path;
$this->_options['tbl'] = 'baz_';
- $model = Database::getInstance($this->_options);
+ $model = new Database($this->_options);
$paste = Helper::getPaste(1, array('expire_date' => 1344803344));
unset($paste['meta']['formatter'], $paste['meta']['opendiscussion'], $paste['meta']['salt']);
$model->delete(Helper::getPasteId());
@@ -357,7 +357,7 @@ class DatabaseTest extends TestCase
'vizhash BLOB, ' .
'postdate INT );'
);
- $this->assertInstanceOf('PrivateBin\\Data\\Database', Database::getInstance($this->_options));
+ $this->assertInstanceOf('PrivateBin\\Data\\Database', new Database($this->_options));
// check if version number was upgraded in created configuration table
$statement = $db->prepare('SELECT value FROM foo_config WHERE id LIKE ?');
diff --git a/tst/Data/FilesystemTest.php b/tst/Data/FilesystemTest.php
index b2a9a202..67366be2 100644
--- a/tst/Data/FilesystemTest.php
+++ b/tst/Data/FilesystemTest.php
@@ -16,7 +16,7 @@ class FilesystemTest extends TestCase
/* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
$this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
- $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+ $this->_model = new Filesystem(array('dir' => $this->_path));
if (!is_dir($this->_path)) {
mkdir($this->_path);
}
diff --git a/tst/Data/GoogleCloudStorageTest.php b/tst/Data/GoogleCloudStorageTest.php
index 4e13ee2a..b7cfe349 100644
--- a/tst/Data/GoogleCloudStorageTest.php
+++ b/tst/Data/GoogleCloudStorageTest.php
@@ -27,7 +27,7 @@ class GoogleCloudStorageTest extends TestCase
public function setUp(): void
{
ini_set('error_log', stream_get_meta_data(tmpfile())['uri']);
- $this->_model = GoogleCloudStorage::getInstance(array(
+ $this->_model = new GoogleCloudStorage(array(
'bucket' => self::$_bucket->name(),
'prefix' => 'pastes',
));
diff --git a/tst/JsonApiTest.php b/tst/JsonApiTest.php
index 1e04af17..68644de2 100644
--- a/tst/JsonApiTest.php
+++ b/tst/JsonApiTest.php
@@ -19,7 +19,7 @@ class JsonApiTest extends TestCase
if (!is_dir($this->_path)) {
mkdir($this->_path);
}
- $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
+ $this->_model = new Filesystem(array('dir' => $this->_path));
ServerSalt::setStore($this->_model);
$_POST = array();
diff --git a/tst/MigrateTest.php b/tst/MigrateTest.php
new file mode 100644
index 00000000..81828b62
--- /dev/null
+++ b/tst/MigrateTest.php
@@ -0,0 +1,82 @@
+_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');
+ }
+}
diff --git a/tst/ModelTest.php b/tst/ModelTest.php
index f57d85d0..03855405 100644
--- a/tst/ModelTest.php
+++ b/tst/ModelTest.php
@@ -1,6 +1,6 @@
_conf = new Configuration;
$this->_model = new Model($this->_conf);
$_SERVER['REMOTE_ADDR'] = '::1';
@@ -157,10 +157,10 @@ class ModelTest extends TestCase
public function testCommentDefaults()
{
+ $class = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
$comment = new Comment(
$this->_conf,
- forward_static_call(
- 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model') . '::getInstance',
+ new $class(
$this->_conf->getSection('model_options')
)
);
@@ -252,7 +252,10 @@ class ModelTest extends TestCase
$paste = $model->getPaste();
$paste->setData($pasteData);
$paste->store();
- $paste->exists();
+ $this->assertTrue($paste->exists(), 'paste exists before creating comment');
+
+ $comment = $paste->getComment(Helper::getPasteId());
+ $comment->setData($commentData);
$db = new PDO(
$options['model_options']['dsn'],
@@ -264,8 +267,6 @@ class ModelTest extends TestCase
$statement->execute();
$statement->closeCursor();
- $comment = $paste->getComment(Helper::getPasteId());
- $comment->setData($commentData);
$this->expectException(Exception::class);
$this->expectExceptionCode(70);
$comment->store();
@@ -307,15 +308,8 @@ class ModelTest extends TestCase
$comment->get();
$comment->store();
- $identicon = new Identicon(array(
- 'hash' => TrafficLimiter::getHash(),
- 'size' => 16,
- 'style' => array(
- 'backgroundColor' => '#fff0', // fully transparent, for dark mode
- 'padding' => 0,
- ),
- ));
- $pngdata = $identicon->getImageDataUri('png');
+ $identicon = new Identicon();
+ $pngdata = $identicon->getImageDataUri(TrafficLimiter::getHash(), 16);
$comment = current($this->_model->getPaste(Helper::getPasteId())->get()['comments']);
$this->assertEquals($pngdata, $comment['meta']['icon'], 'icon gets set');
}
@@ -428,7 +422,7 @@ class ModelTest extends TestCase
public function testPurge()
{
$conf = new Configuration;
- $store = Database::getInstance($conf->getSection('model_options'));
+ $store = new Database($conf->getSection('model_options'));
$store->delete(Helper::getPasteId());
$expired = Helper::getPaste(2, array('expire_date' => 1344803344));
$paste = Helper::getPaste(2, array('expire_date' => time() + 3600));
diff --git a/tst/Persistence/PurgeLimiterTest.php b/tst/Persistence/PurgeLimiterTest.php
index 303f5e73..656818ed 100644
--- a/tst/Persistence/PurgeLimiterTest.php
+++ b/tst/Persistence/PurgeLimiterTest.php
@@ -16,7 +16,7 @@ class PurgeLimiterTest extends TestCase
mkdir($this->_path);
}
PurgeLimiter::setStore(
- Filesystem::getInstance(array('dir' => $this->_path))
+ new Filesystem(array('dir' => $this->_path))
);
}
diff --git a/tst/Persistence/ServerSaltTest.php b/tst/Persistence/ServerSaltTest.php
index 214cb581..99210560 100644
--- a/tst/Persistence/ServerSaltTest.php
+++ b/tst/Persistence/ServerSaltTest.php
@@ -22,7 +22,7 @@ class ServerSaltTest extends TestCase
mkdir($this->_path);
}
ServerSalt::setStore(
- Filesystem::getInstance(array('dir' => $this->_path))
+ new Filesystem(array('dir' => $this->_path))
);
$this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo';
@@ -45,17 +45,17 @@ class ServerSaltTest extends TestCase
{
// generating new salt
ServerSalt::setStore(
- Filesystem::getInstance(array('dir' => $this->_path))
+ new Filesystem(array('dir' => $this->_path))
);
$salt = ServerSalt::get();
// try setting a different path and resetting it
ServerSalt::setStore(
- Filesystem::getInstance(array('dir' => $this->_otherPath))
+ new Filesystem(array('dir' => $this->_otherPath))
);
$this->assertNotEquals($salt, ServerSalt::get());
ServerSalt::setStore(
- Filesystem::getInstance(array('dir' => $this->_path))
+ new Filesystem(array('dir' => $this->_path))
);
$this->assertEquals($salt, ServerSalt::get());
}
@@ -64,7 +64,7 @@ class ServerSaltTest extends TestCase
{
// try setting an invalid path
chmod($this->_invalidPath, 0000);
- $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+ $store = new Filesystem(array('dir' => $this->_invalidPath));
ServerSalt::setStore($store);
$salt = ServerSalt::get();
ServerSalt::setStore($store);
@@ -77,7 +77,7 @@ class ServerSaltTest extends TestCase
chmod($this->_invalidPath, 0700);
file_put_contents($this->_invalidFile, '');
chmod($this->_invalidFile, 0000);
- $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+ $store = new Filesystem(array('dir' => $this->_invalidPath));
ServerSalt::setStore($store);
$salt = ServerSalt::get();
ServerSalt::setStore($store);
@@ -94,7 +94,7 @@ class ServerSaltTest extends TestCase
}
file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . '.htaccess', '');
chmod($this->_invalidPath, 0500);
- $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+ $store = new Filesystem(array('dir' => $this->_invalidPath));
ServerSalt::setStore($store);
$salt = ServerSalt::get();
ServerSalt::setStore($store);
@@ -106,9 +106,9 @@ class ServerSaltTest extends TestCase
// try creating an invalid path
chmod($this->_invalidPath, 0000);
ServerSalt::setStore(
- Filesystem::getInstance(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
+ new Filesystem(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
);
- $store = Filesystem::getInstance(array('dir' => $this->_invalidPath));
+ $store = new Filesystem(array('dir' => $this->_invalidPath));
ServerSalt::setStore($store);
$salt = ServerSalt::get();
ServerSalt::setStore($store);
diff --git a/tst/Persistence/TrafficLimiterTest.php b/tst/Persistence/TrafficLimiterTest.php
index c9bb6d7d..8f7c0e8c 100644
--- a/tst/Persistence/TrafficLimiterTest.php
+++ b/tst/Persistence/TrafficLimiterTest.php
@@ -13,7 +13,7 @@ class TrafficLimiterTest extends TestCase
{
/* Setup Routine */
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit';
- $store = Filesystem::getInstance(array('dir' => $this->_path));
+ $store = new Filesystem(array('dir' => $this->_path));
ServerSalt::setStore($store);
TrafficLimiter::setStore($store);
}
diff --git a/tst/Vizhash16x16Test.php b/tst/Vizhash16x16Test.php
index 168ef206..63782e51 100644
--- a/tst/Vizhash16x16Test.php
+++ b/tst/Vizhash16x16Test.php
@@ -19,7 +19,7 @@ class Vizhash16x16Test extends TestCase
mkdir($this->_path);
}
$this->_file = $this->_path . DIRECTORY_SEPARATOR . 'vizhash.png';
- ServerSalt::setStore(Filesystem::getInstance(array('dir' => $this->_path)));
+ ServerSalt::setStore(new Filesystem(array('dir' => $this->_path)));
}
public function tearDown(): void
diff --git a/tst/phpunit.xml b/tst/phpunit.xml
index 504ba1b8..881888f0 100644
--- a/tst/phpunit.xml
+++ b/tst/phpunit.xml
@@ -14,7 +14,6 @@
./
- ConfigurationTestGenerator.php
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index e1f0c12e..5da6b5b2 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -5,7 +5,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => '46013df6201304840c8dab82af63cc9000a6d239',
+ 'reference' => 'ba5c859d85244c30711263d61e691217c1bc95e4',
'name' => 'privatebin/privatebin',
'dev' => false,
),
@@ -43,7 +43,7 @@
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => '46013df6201304840c8dab82af63cc9000a6d239',
+ 'reference' => 'ba5c859d85244c30711263d61e691217c1bc95e4',
'dev_requirement' => false,
),
'yzalis/identicon' => array(