Explorar o código

Merge remote-tracking branch 'upstream/master' into advisory-fix-1

El RIDO hai 1 día
pai
achega
879f049290
Modificáronse 86 ficheiros con 3813 adicións e 4102 borrados
  1. 1 1
      .devcontainer/devcontainer.json
  2. 1 0
      .gitattributes
  3. 1 1
      .github/copilot-instructions.md
  4. 5 6
      .jshintrc
  5. 15 0
      .run/Frontend tests.run.xml
  6. 0 2
      .styleci.yml
  7. 10 2
      CHANGELOG.md
  8. 1 21
      bin/configuration-test-generator
  9. 3 11
      cfg/conf.sample.php
  10. 15 32
      composer.lock
  11. 198 198
      i18n/ko.json
  12. 3 3
      i18n/sl.json
  13. 2 2
      i18n/sv.json
  14. 0 2
      js/.istanbul.yml
  15. 0 5
      js/bootstrap-3.4.1.js
  16. 51 70
      js/common.js
  17. 2 2
      js/eslint.config.js
  18. 0 1
      js/jquery-3.7.1.js
  19. 22 0
      js/jsconfig.json
  20. 8 9
      js/legacy.js
  21. 322 0
      js/package-lock.json
  22. 3 0
      js/package.json
  23. 203 235
      js/privatebin.js
  24. 322 323
      js/test/Alert.js
  25. 171 137
      js/test/AttachmentViewer.js
  26. 68 63
      js/test/Check.js
  27. 142 116
      js/test/CopyToClipboard.js
  28. 46 34
      js/test/CryptTool.js
  29. 118 97
      js/test/DiscussionViewer.js
  30. 70 61
      js/test/Editor.js
  31. 316 268
      js/test/Helper.js
  32. 210 199
      js/test/I18n.js
  33. 211 197
      js/test/Model.js
  34. 207 177
      js/test/PasteStatus.js
  35. 147 104
      js/test/PasteViewer.js
  36. 46 32
      js/test/Prompt.js
  37. 23 15
      js/test/ServerInteraction.js
  38. 243 250
      js/test/TopNav.js
  39. 51 77
      js/test/UiHelper.js
  40. 50 44
      js/test/emailTemplateTest.js
  41. 42 44
      lib/Configuration.php
  42. 17 18
      lib/Controller.php
  43. 1 1
      lib/Data/AbstractData.php
  44. 28 28
      lib/Data/Database.php
  45. 4 4
      lib/Data/Filesystem.php
  46. 22 22
      lib/Data/GoogleCloudStorage.php
  47. 32 32
      lib/Data/S3Storage.php
  48. 0 38
      lib/Exception/JsonException.php
  49. 2 2
      lib/Filter.php
  50. 5 5
      lib/FormatV2.php
  51. 10 10
      lib/I18n.php
  52. 2 24
      lib/Json.php
  53. 1 1
      lib/Model/AbstractModel.php
  54. 5 5
      lib/Model/Comment.php
  55. 2 2
      lib/Persistence/TrafficLimiter.php
  56. 4 4
      lib/Proxy/AbstractProxy.php
  57. 6 6
      lib/Proxy/ShlinkProxy.php
  58. 4 4
      lib/Proxy/YourlsProxy.php
  59. 10 10
      lib/Request.php
  60. 1 1
      lib/TemplateSwitcher.php
  61. 1 1
      lib/View.php
  62. 5 5
      lib/Vizhash16x16.php
  63. 0 714
      tpl/bootstrap.php
  64. 0 1
      tpl/bootstrap5.php
  65. 101 101
      tst/Bootstrap.php
  66. 10 10
      tst/ControllerTest.php
  67. 6 6
      tst/ControllerWithDbTest.php
  68. 7 7
      tst/ControllerWithGcsTest.php
  69. 36 36
      tst/Data/DatabaseTest.php
  70. 16 16
      tst/Data/FilesystemTest.php
  71. 14 14
      tst/Data/GoogleCloudStorageTest.php
  72. 4 4
      tst/FormatV2Test.php
  73. 4 4
      tst/I18nTest.php
  74. 18 18
      tst/JsonApiTest.php
  75. 5 5
      tst/MigrateTest.php
  76. 36 36
      tst/ModelTest.php
  77. 1 1
      tst/Persistence/PurgeLimiterTest.php
  78. 9 9
      tst/Persistence/ServerSaltTest.php
  79. 1 1
      tst/Persistence/TrafficLimiterTest.php
  80. 6 6
      tst/RequestTest.php
  81. 6 25
      tst/ViewTest.php
  82. 1 1
      tst/Vizhash16x16Test.php
  83. 19 19
      tst/YourlsProxyTest.php
  84. 0 1
      vendor/composer/autoload_classmap.php
  85. 0 1
      vendor/composer/autoload_static.php
  86. 2 2
      vendor/composer/installed.php

+ 1 - 1
.devcontainer/devcontainer.json

@@ -36,7 +36,7 @@
         }
     },
     "features": {
-        "ghcr.io/devcontainers-contrib/features/mocha:2": {},
+        "ghcr.io/devcontainers-extra/features/mocha:2": {},
         "ghcr.io/yassinedoghri/devcontainers/php-extensions-installer:1": {
             "extensions": "gd"
         }

+ 1 - 0
.gitattributes

@@ -9,6 +9,7 @@ js/.nycrc.yml export-ignore
 js/common.js export-ignore
 js/eslint.config.js export-ignore
 js/test/ export-ignore
+js/jsconfig.json export-ignore
 .codeclimate.yml export-ignore
 .csslintrc export-ignore
 .devcontainer export-ignore

+ 1 - 1
.github/copilot-instructions.md

@@ -38,7 +38,7 @@
 
 - **No explicit build step** for PHP. The web app is served directly from source.
   - This means **composer directory** need to be comitted (_except_ of big optional dependences like Google Cloud like GCS support or similar!)
-- **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES6+ syntax, except in `legacy.js` (which must be designed to run cleanly even on ancient IE4 or Netscape to display the error message that a browser upgrade is necessary). We are trying to avoid jQuery in any new code and would like to eventually drop use of jQuery. We are considering modularizing the JS logic, but need to ensure to do so in a way that will work both in the browser as well as for node JS driven unit tests.
+- **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES6+ syntax, except in `legacy.js` (which must be designed to run cleanly even on ancient IE4 or Netscape to display the error message that a browser upgrade is necessary). We dropped use of jQuery. We are considering modularizing the JS logic, but need to ensure to do so in a way that will work both in the browser as well as for node JS driven unit tests.
 
 ### Run
 

+ 5 - 6
.jshintrc

@@ -2,7 +2,7 @@
     "bitwise": true,
     "curly": true,
     "eqeqeq": true,
-    "esversion": 6,
+    "esversion": 2018,
     "forin": true,
     "freeze": true,
     "futurehostile": true,
@@ -17,11 +17,12 @@
     "quotmark": "single",
     "singleGroups": true,
     "strict": true,
+    "globalstrict": true,
     "undef": true,
     "unused": true,
-    "jquery": true,
+    "jquery": false,
     "browser": true,
-    "predef": {
+    "globals": {
       "after": true,
       "before": true,
       "cleanup": true,
@@ -36,9 +37,7 @@
       "jsdom": true,
       "require": false,
       "setTimeout": false,
-      "window": true
-    },
-    "globals": {
+      "window": true,
       "DOMPurify": true,
       "kjua": true
     }

+ 15 - 0
.run/Frontend tests.run.xml

@@ -0,0 +1,15 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Frontend tests" type="mocha-javascript-test-runner">
+    <node-interpreter>project</node-interpreter>
+    <node-options />
+    <mocha-package>$PROJECT_DIR$/js/node_modules/mocha</mocha-package>
+    <working-directory>$PROJECT_DIR$/js</working-directory>
+    <pass-parent-env>true</pass-parent-env>
+    <ui>bdd</ui>
+    <extra-mocha-options />
+    <test-kind>DIRECTORY</test-kind>
+    <test-directory>$PROJECT_DIR$/js/test</test-directory>
+    <recursive>true</recursive>
+    <method v="2" />
+  </configuration>
+</component>

+ 0 - 2
.styleci.yml

@@ -5,7 +5,6 @@ risky: false
 enabled:
   - align_equals
   - concat_with_spaces
-  - long_array_syntax
   - no_empty_comment
   - pre_increment
 
@@ -26,7 +25,6 @@ disabled:
   - phpdoc_single_line_var_spacing
   - phpdoc_summary
   - post_increment
-  - short_array_syntax
   - single_line_after_imports
   - unalign_equals
 

+ 10 - 2
CHANGELOG.md

@@ -1,11 +1,19 @@
 # PrivateBin version history
 
+## 2.1.0 (not yet released)
+* CHANGED: We removed jQuery in the Frontend and replaced it with vanilla JS.
+* CHANGED: Removed the unmaintained js-verify and replaced it with fast-check library.
+* CHANGED: Added a `jsconfig.json` in order to check the types of JavaScript.
+* CHANGED: Removed support for Bootstrap 3 as it requires jQuery.
+* FIXED: Prevent browsers guessing MIME types on JSON(LD) API responses (#164)
+* FIXED: Insert quoted base path into JSON-LD documents
+
 ## 2.0.6 (not yet released)
 * CHANGED: Upgrading libraries to: DOMpurify 3.4.12
+* CHANGED: Switch to PHP native JsonException type
 * FIXED: Gracefully handle YOURLS replies with a 200 status code but no shorturl, instead of raising a TypeError
 * FIXED: Return "Invalid data." instead of HTTP 500 on malformed v2 JSON payloads (#1883)
-* FIXED: Prevent browsers guessing MIME types on JSON(LD) API responses (#164)
-* FIXED: Insert quoted base path into JSON-LD documents
+* FIXED: Dead PATH validation guard in Controller, the check for a missing trailing directory separator never ran (#1887)
 
 ## 2.0.5 (2026-07-11)
 * CHANGED: Show OS-specific copy hotkey hint (Cmd+c on Mac, Ctrl+c on others) (#1506)

+ 1 - 21
bin/configuration-test-generator

@@ -183,27 +183,7 @@ new ConfigurationTestGenerator(array(
                 ),
             ),
             'affects' => $vd,
-        ), array(
-            'setting' => 'bootstrap',
-            'tests'   => array(
-                array(
-                    'type' => 'DoesNotMatchRegularExpression',
-                    'args' => array(
-                        '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap5/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#',
-                        '$content',
-                        'removes "bootstrap5" stylesheet correctly',
-                    ),
-                ), array(
-                    'type' => 'MatchesRegularExpression',
-                    'args' => array(
-                        '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap/bootstrap-\d[\d\.]+\d\.css"[^>]*/>#',
-                        '$content',
-                        'outputs "bootstrap" stylesheet correctly',
-                    ),
-                ),
-            ),
-            'affects' => $vd,
-        ),
+        )
     ),
     'main/sizelimit' => array(
         array(

+ 3 - 11
cfg/conf.sample.php

@@ -49,17 +49,9 @@ templateselection = false
 
 ; List of available for selection templates when "templateselection" option is enabled
 availabletemplates[] = "bootstrap5"
-availabletemplates[] = "bootstrap"
-availabletemplates[] = "bootstrap-page"
-availabletemplates[] = "bootstrap-dark"
-availabletemplates[] = "bootstrap-dark-page"
-availabletemplates[] = "bootstrap-compact"
-availabletemplates[] = "bootstrap-compact-page"
-
-; set the template your installs defaults to, defaults to "bootstrap5" (tpl/bootstrap5.php), also
-; bootstrap template (tpl/bootstrap.php) and it's variants: "bootstrap-dark", "bootstrap-compact", "bootstrap-page",
-; which can be combined with "-dark" and "-compact" for "bootstrap-dark-page",
-; "bootstrap-compact-page" - previews at:
+
+; set the template your installs defaults to, defaults to "bootstrap5" (tpl/bootstrap5.php)
+; previews at:
 ; https://privatebin.info/screenshots.html
 ; template = "bootstrap5"
 

+ 15 - 32
composer.lock

@@ -396,20 +396,19 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v5.7.0",
+            "version": "v5.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+                "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
-                "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
+                "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
                 "shasum": ""
             },
             "require": {
-                "ext-ctype": "*",
                 "ext-json": "*",
                 "ext-tokenizer": "*",
                 "php": ">=7.4"
@@ -448,9 +447,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0"
             },
-            "time": "2025-12-06T11:56:16+00:00"
+            "time": "2026-07-04T14:30:18+00:00"
         },
         {
             "name": "phar-io/manifest",
@@ -891,25 +890,25 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.6.34",
+            "version": "9.6.35",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "b36f02317466907a230d3aa1d34467041271ef4a"
+                "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
-                "reference": "b36f02317466907a230d3aa1d34467041271ef4a",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0edba2f3a0c48df3553cb9b640810b30df60302b",
+                "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.5.0 || ^2",
                 "ext-dom": "*",
+                "ext-filter": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
-                "ext-xml": "*",
                 "ext-xmlwriter": "*",
                 "myclabs/deep-copy": "^1.13.4",
                 "phar-io/manifest": "^2.0.4",
@@ -974,31 +973,15 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.35"
             },
             "funding": [
                 {
-                    "url": "https://phpunit.de/sponsors.html",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                },
-                {
-                    "url": "https://liberapay.com/sebastianbergmann",
-                    "type": "liberapay"
-                },
-                {
-                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
-                    "type": "thanks_dev"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
-                    "type": "tidelift"
+                    "url": "https://phpunit.de/sponsoring.html",
+                    "type": "other"
                 }
             ],
-            "time": "2026-01-27T05:45:00+00:00"
+            "time": "2026-07-06T14:48:07+00:00"
         },
         {
             "name": "sebastian/cli-parser",

+ 198 - 198
i18n/ko.json

@@ -1,160 +1,160 @@
 {
     "PrivateBin": "PrivateBin",
-    "%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.",
-    "More information on the <a href=\"https://privatebin.info/\">project page</a>.": "More information on the <a href=\"https://privatebin.info/\">project page</a>.",
-    "Because ignorance is bliss": "Because ignorance is bliss",
-    "Document does not exist, has expired or has been deleted.": "Document does not exist, has expired or has been deleted.",
-    "%s requires php %s or above to work. Sorry.": "%s requires php %s or above to work. Sorry.",
-    "%s requires configuration section [%s] to be present in configuration file.": "%s requires configuration section [%s] to be present in configuration file.",
+    "%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s은 서버가 저장된 데이터에 대해 전혀 알 수 없는 미니멀한 오픈소스 온라인 텍스트 공유 플랫폼(페이스트빈)입니다. 데이터는 256비트 AES를 사용하여 %s브라우저에서%s 암호화/복호화됩니다.",
+    "More information on the <a href=\"https://privatebin.info/\">project page</a>.": "자세한 정보는 <a href=\"https://privatebin.info/\">프로젝트 페이지</a>에서 확인하세요.",
+    "Because ignorance is bliss": "모르는 게 약이니까요",
+    "Document 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.": [
-        "Please wait %d second between each post. (singular)",
-        "Please wait %d seconds between each post. (1st plural)",
-        "Please wait %d seconds between each post. (2nd plural)",
-        "Please wait %d seconds between each post. (3rd plural)",
-        "Please wait %d seconds between each post. (4th plural)",
-        "Please wait %d seconds between each post. (5th plural)"
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요.",
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요.",
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요.",
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요.",
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요.",
+        "다음 게시글을 작성할 때까지 %d초 동안 기다려 주세요."
     ],
-    "Document is limited to %s of encrypted data.": "Document is limited to %s of encrypted data.",
-    "Invalid data.": "Invalid data.",
-    "You are unlucky. Try again.": "You are unlucky. Try again.",
-    "Error saving comment. Sorry.": "Error saving comment. Sorry.",
-    "Error saving document. Sorry.": "Error saving document. Sorry.",
-    "Invalid document ID.": "Invalid document ID.",
-    "Document is not of burn-after-reading type.": "Document is not of burn-after-reading type.",
-    "Wrong deletion token. Document was not deleted.": "Wrong deletion token. Document was not deleted.",
-    "Document was properly deleted.": "Document was properly deleted.",
-    "JavaScript is required for %s to work. Sorry for the inconvenience.": "JavaScript is required for %s to work. Sorry for the inconvenience.",
-    "%s requires a modern browser to work.": "%s requires a modern browser to work.",
-    "New": "New",
-    "Create": "Create",
-    "Clone": "Clone",
-    "Raw text": "Raw text",
-    "Expires": "Expires",
-    "Burn after reading": "Burn after reading",
-    "Open discussion": "Open discussion",
-    "Password (recommended)": "Password (recommended)",
-    "Discussion": "Discussion",
-    "Toggle navigation": "Toggle navigation",
+    "Document is limited to %s of encrypted data.": "문서 크기는 암호화된 데이터 기준 %s로 제한됩니다.",
+    "Invalid data.": "유효하지 않은 데이터입니다.",
+    "You are unlucky. Try again.": "운이 나빴네요. 다시 시도해 주세요.",
+    "Error saving comment. Sorry.": "댓글을 저장하는 중 오류가 발생했습니다. 죄송합니다.",
+    "Error saving document. Sorry.": "문서를 저장하는 중 오류가 발생했습니다. 죄송합니다.",
+    "Invalid document ID.": "유효하지 않은 문서 ID입니다.",
+    "Document is not of burn-after-reading type.": "이 문서는 '읽은 후 삭제' 설정이 적용되지 않았습니다.",
+    "Wrong deletion token. Document was not deleted.": "삭제 토큰이 일치하지 않습니다. 문서가 삭제되지 않았습니다.",
+    "Document was properly deleted.": "문서가 정상적으로 삭제되었습니다.",
+    "JavaScript is required for %s to work. Sorry for the inconvenience.": "%s을(를) 실행하려면 JavaScript가 필요합니다. 이용에 불편을 드려 죄송합니다.",
+    "%s requires a modern browser to work.": "%s을(를) 실행하려면 최신 브라우저가 필요합니다.",
+    "New": "새 문서",
+    "Create": "생성",
+    "Clone": "복제",
+    "Raw text": "원본 텍스트",
+    "Expires": "만료 기간",
+    "Burn after reading": "읽은 후 삭제",
+    "Open discussion": "댓글 허용",
+    "Password (recommended)": "비밀번호 (권장)",
+    "Discussion": "댓글",
+    "Toggle navigation": "네비게이션 토글",
     "%d seconds": [
-        "%d second (singular)",
-        "%d seconds (1st plural)",
-        "%d seconds (2nd plural)",
-        "%d seconds (3rd plural)",
-        "%d seconds (4th plural)",
-        "%d seconds (5th plural)"
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d"
     ],
     "%d minutes": [
-        "%d minute (singular)",
-        "%d minutes (1st plural)",
-        "%d minutes (2nd plural)",
-        "%d minutes (3rd plural)",
-        "%d minutes (4th plural)",
-        "%d minutes (5th plural)"
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d"
     ],
     "%d hours": [
-        "%d hour (singular)",
-        "%d hours (1st plural)",
-        "%d hours (2nd plural)",
-        "%d hours (3rd plural)",
-        "%d hours (4th plural)",
-        "%d hours (5th plural)"
+        "%d시간",
+        "%d시간",
+        "%d시간",
+        "%d시간",
+        "%d시간",
+        "%d시간"
     ],
     "%d days": [
-        "%d day (singular)",
-        "%d days (1st plural)",
-        "%d days (2nd plural)",
-        "%d days (3rd plural)",
-        "%d days (4th plural)",
-        "%d days (5th plural)"
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d"
     ],
     "%d weeks": [
-        "%d week (singular)",
-        "%d weeks (1st plural)",
-        "%d weeks (2nd plural)",
-        "%d weeks (3rd plural)",
-        "%d weeks (4th plural)",
-        "%d weeks (5th plural)"
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d"
     ],
     "%d months": [
-        "%d month (singular)",
-        "%d months (1st plural)",
-        "%d months (2nd plural)",
-        "%d months (3rd plural)",
-        "%d months (4th plural)",
-        "%d months (5th plural)"
+        "%d개월",
+        "%d개월",
+        "%d개월",
+        "%d개월",
+        "%d개월",
+        "%d개월"
     ],
     "%d years": [
-        "%d year (singular)",
-        "%d years (1st plural)",
-        "%d years (2nd plural)",
-        "%d years (3rd plural)",
-        "%d years (4th plural)",
-        "%d years (5th plural)"
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d",
+        "%d"
     ],
-    "Never": "Never",
-    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.",
+    "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.": [
-        "This document will expire in %d second. (singular)",
-        "This document will expire in %d seconds. (1st plural)",
-        "This document will expire in %d seconds. (2nd plural)",
-        "This document will expire in %d seconds. (3rd plural)",
-        "This document will expire in %d seconds. (4th plural)",
-        "This document will expire in %d seconds. (5th plural)"
+        "이 문서는 %d초 후에 만료됩니다.",
+        "이 문서는 %d초 후에 만료됩니다.",
+        "이 문서는 %d초 후에 만료됩니다.",
+        "이 문서는 %d초 후에 만료됩니다.",
+        "이 문서는 %d초 후에 만료됩니다.",
+        "이 문서는 %d초 후에 만료됩니다."
     ],
     "This document will expire in %d minutes.": [
-        "This document will expire in %d minute. (singular)",
-        "This document will expire in %d minutes. (1st plural)",
-        "This document will expire in %d minutes. (2nd plural)",
-        "This document will expire in %d minutes. (3rd plural)",
-        "This document will expire in %d minutes. (4th plural)",
-        "This document will expire in %d minutes. (5th plural)"
+        "이 문서는 %d분 후에 만료됩니다.",
+        "이 문서는 %d분 후에 만료됩니다.",
+        "이 문서는 %d분 후에 만료됩니다.",
+        "이 문서는 %d분 후에 만료됩니다.",
+        "이 문서는 %d분 후에 만료됩니다.",
+        "이 문서는 %d분 후에 만료됩니다."
     ],
     "This document will expire in %d hours.": [
-        "This document will expire in %d hour. (singular)",
-        "This document will expire in %d hours. (1st plural)",
-        "This document will expire in %d hours. (2nd plural)",
-        "This document will expire in %d hours. (3rd plural)",
-        "This document will expire in %d hours. (4th plural)",
-        "This document will expire in %d hours. (5th plural)"
+        "이 문서는 %d시간 후에 만료됩니다.",
+        "이 문서는 %d시간 후에 만료됩니다.",
+        "이 문서는 %d시간 후에 만료됩니다.",
+        "이 문서는 %d시간 후에 만료됩니다.",
+        "이 문서는 %d시간 후에 만료됩니다.",
+        "이 문서는 %d시간 후에 만료됩니다."
     ],
     "This document will expire in %d days.": [
-        "This document will expire in %d day. (singular)",
-        "This document will expire in %d days. (1st plural)",
-        "This document will expire in %d days. (2nd plural)",
-        "This document will expire in %d days. (3rd plural)",
-        "This document will expire in %d days. (4th plural)",
-        "This document will expire in %d days. (5th plural)"
+        "이 문서는 %d일 후에 만료됩니다.",
+        "이 문서는 %d일 후에 만료됩니다.",
+        "이 문서는 %d일 후에 만료됩니다.",
+        "이 문서는 %d일 후에 만료됩니다.",
+        "이 문서는 %d일 후에 만료됩니다.",
+        "이 문서는 %d일 후에 만료됩니다."
     ],
     "This document will expire in %d months.": [
-        "This document will expire in %d month. (singular)",
-        "This document will expire in %d months. (1st plural)",
-        "This document will expire in %d months. (2nd plural)",
-        "This document will expire in %d months. (3rd plural)",
-        "This document will expire in %d months. (4th plural)",
-        "This document will expire in %d months. (5th plural)"
+        "이 문서는 %d개월 후에 만료됩니다.",
+        "이 문서는 %d개월 후에 만료됩니다.",
+        "이 문서는 %d개월 후에 만료됩니다.",
+        "이 문서는 %d개월 후에 만료됩니다.",
+        "이 문서는 %d개월 후에 만료됩니다.",
+        "이 문서는 %d개월 후에 만료됩니다."
     ],
-    "Please enter the password for this document:": "Please enter the password for this document:",
-    "Could not decrypt data (Wrong key?)": "Could not decrypt data (Wrong key?)",
-    "Could not delete the document, it was not stored in burn after reading mode.": "Could not delete the document, 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.": "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.",
-    "Could not decrypt comment; Wrong key?": "Could not decrypt comment; Wrong key?",
-    "Reply": "Reply",
-    "Anonymous": "Anonymous",
-    "Avatar generated from IP address": "Avatar generated from IP address",
-    "Add comment": "Add comment",
-    "Optional nickname…": "Optional nickname…",
-    "Post comment": "Post comment",
-    "Sending comment…": "Sending comment…",
-    "Comment posted.": "Comment posted.",
-    "Could not refresh display: %s": "Could not refresh display: %s",
-    "unknown status": "unknown status",
-    "server error or not responding": "server error or not responding",
-    "Could not post comment: %s": "Could not post comment: %s",
-    "Sending document…": "Sending document…",
-    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>%s</kbd>+<kbd>c</kbd> to copy)</span>": "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>%s</kbd>+<kbd>c</kbd> to copy)</span>",
-    "Delete data": "Delete data",
-    "Could not create document: %s": "Could not create document: %s",
-    "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)",
+    "Please enter the password for this document:": "이 문서의 비밀번호를 입력해 주세요:",
+    "Could not decrypt data (Wrong key?)": "데이터를 복호화할 수 없음 (잘못된 키인지 확인해 주세요.)",
+    "Could not delete the document, 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": "IP 주소로 생성된 아바타",
+    "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 document…": "문서 전송 중…",
+    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>%s</kbd>+<kbd>c</kbd> to copy)</span>": "문서 주소: <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(복사하려면 <kbd>%s</kbd>+<kbd>c</kbd> 키를 누르세요)</span>",
+    "Delete data": "데이터 삭제",
+    "Could not create document: %s": "문서를 생성할 수 없음: %s",
+    "Cannot decrypt document: 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",
     "kB": "kB",
     "MB": "MB",
@@ -164,75 +164,75 @@
     "EB": "EB",
     "ZB": "ZB",
     "YB": "YB",
-    "Format": "Format",
-    "Plain Text": "Plain Text",
-    "Source Code": "Source Code",
-    "Markdown": "Markdown",
-    "Download attachment": "Download attachment",
-    "Cloned: '%s'": "Cloned: '%s'",
-    "The cloned file '%s' was attached to this document.": "The cloned file '%s' was attached to this document.",
-    "Attach a file": "Attach a file",
-    "alternatively drag & drop a file or paste an image from the clipboard": "alternatively drag & drop a file or paste an image from the clipboard",
-    "File too large, to display a preview. Please download the attachment.": "File too large, to display a preview. Please download the attachment.",
-    "Remove attachment": "Remove attachment",
-    "Your browser does not support uploading encrypted files. Please use a newer browser.": "Your browser does not support uploading encrypted files. Please use a newer browser.",
-    "Invalid attachment.": "Invalid attachment.",
-    "Options": "Options",
-    "Shorten URL": "Shorten URL",
-    "Editor": "Editor",
-    "Preview": "Preview",
-    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.",
-    "Decrypt": "Decrypt",
-    "Enter password": "Enter password",
-    "Loading…": "Loading…",
-    "Decrypting document…": "Decrypting document…",
-    "Preparing new document…": "Preparing new document…",
-    "In case this message never disappears please have a look at <a href=\"%s\">this FAQ for information to troubleshoot</a>.": "In case this message never disappears please have a look at <a href=\"%s\">this FAQ for information to troubleshoot</a>.",
-    "+++ no document text +++": "+++ no document text +++",
-    "Could not get document data: %s": "Could not get document data: %s",
-    "QR code": "QR code",
-    "This website is using an insecure HTTP connection! Please use it only for testing.": "This website is using an insecure HTTP connection! Please use it only for testing.",
-    "For more information <a href=\"%s\">see this FAQ entry</a>.": "For more information <a href=\"%s\">see this FAQ entry</a>.",
-    "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.": "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.",
-    "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.",
-    "waiting on user to provide a password": "waiting on user to provide a password",
-    "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.": "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.",
-    "Retry": "Retry",
-    "Showing raw text…": "Showing raw text…",
-    "Notice:": "Notice:",
-    "This link will expire after %s.": "This link will expire after %s.",
-    "This link can only be accessed once, do not use back or refresh button in your browser.": "This link can only be accessed once, do not use back or refresh button in your browser.",
-    "Link:": "Link:",
-    "Recipient may become aware of your timezone, convert time to UTC?": "Recipient may become aware of your timezone, convert time to UTC?",
-    "Use Current Timezone": "Use Current Timezone",
-    "Convert To UTC": "Convert To UTC",
-    "Close": "Close",
-    "Encrypted note on %s": "Encrypted note on %s",
-    "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.",
-    "URL shortener may expose your decrypt key in URL.": "URL shortener may expose your decrypt key in URL.",
-    "URL shortener is enabled by default.": "URL shortener is enabled by default.",
-    "Save document": "Save document",
-    "Your IP is not authorized to create documents.": "Your IP is not authorized to create documents.",
-    "Trying to shorten a URL that isn't pointing at our instance.": "Trying to shorten a URL that isn't pointing at our instance.",
-    "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.",
-    "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.",
-    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.",
-    "This secret message can only be displayed once. Would you like to see it now?": "This secret message can only be displayed once. Would you like to see it now?",
-    "Yes, see it": "Yes, see it",
-    "Dark Mode": "Dark Mode",
-    "Error compressing document, due to missing WebAssembly support.": "Error compressing document, due to missing WebAssembly support.",
-    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.",
-    "Start over": "Start over",
-    "Document copied to clipboard": "Document copied to clipboard",
-    "To copy document press on the copy button or use the clipboard shortcut <kbd>%s</kbd>+<kbd>c</kbd>": "To copy document press on the copy button or use the clipboard shortcut <kbd>%s</kbd>+<kbd>c</kbd>",
-    "Copy link": "Copy link",
-    "Link copied to clipboard": "Link copied to clipboard",
-    "Document text": "Document text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
-    "Show password": "Show password",
-    "Hide password": "Hide password",
-    "Theme": "Theme",
-    "Copy": "Copy",
+    "Format": "형식",
+    "Plain Text": "일반 텍스트",
+    "Source Code": "소스 코드",
+    "Markdown": "마크다운",
+    "Download attachment": "첨부 파일 다운로드",
+    "Cloned: '%s'": "복제됨: '%s'",
+    "The cloned file '%s' was attached to this document.": "복제된 파일 '%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": "URL 단축",
+    "Editor": "편집기",
+    "Preview": "미리 보기",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "%s을(를) 실행하려면 PATH가 \"%s\"(으)로 끝나야 합니다. index.php 파일의 PATH 설정을 업데이트해 주세요.",
+    "Decrypt": "복호화",
+    "Enter password": "비밀번호 입력",
+    "Loading…": "로딩 중…",
+    "Decrypting document…": "문서 복호화 중…",
+    "Preparing new document…": "새 문서 준비 중…",
+    "In case this message never disappears please have a look at <a href=\"%s\">this FAQ for information to troubleshoot</a>.": "이 메시지가 사라지지 않는 경우, 문제 해결을 위해 <a href=\"%s\">이 FAQ 항목</a>을 확인해 주세요.",
+    "+++ no document text +++": "+++ 문서 내용 없음 +++",
+    "Could not get document data: %s": "문서 데이터를 가져올 수 없음: %s",
+    "QR code": "QR 코드",
+    "This website is using an insecure HTTP connection! Please use it only for testing.": "이 웹사이트는 안전하지 않은 HTTP 연결을 사용하고 있습니다! 테스트 용도로만 사용해 주세요.",
+    "For more information <a href=\"%s\">see this FAQ entry</a>.": "자세한 정보는 <a href=\"%s\">이 FAQ 항목</a>을 참조하세요.",
+    "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.": "WebCrypto API를 지원하려면 브라우저에서 HTTPS 연결이 필요할 수 있습니다. <a href=\"%s\">HTTPS로 전환</a>해 보세요.",
+    "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "브라우저가 zlib 압축에 사용되는 WebAssembly를 지원하지 않습니다. 압축되지 않은 문서는 생성할 수 있으나, 압축된 문서는 읽을 수 없습니다.",
+    "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 단축 서비스를 사용하면 URL에 포함된 복호화 키가 노출될 수 있습니다.",
+    "URL shortener is enabled by default.": "URL 단축 기능이 기본으로 활성화되어 있습니다.",
+    "Save document": "문서 저장",
+    "Your IP is not authorized to create documents.": "귀하의 IP는 문서 생성 권한이 없습니다.",
+    "Trying to shorten a URL that isn't pointing at our instance.": "현재 인스턴스를 가리키지 않는 URL을 단축하려고 시도했습니다.",
+    "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "프록시 오류: 프록시 URL이 비어 있습니다. 잘못된 설정 키이거나 누락된 설정 때문일 수 있습니다.",
+    "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "프록시 오류: 프록시 응답을 구문 분석하는 중 오류가 발생했습니다. 잘못된 설정 키이거나 누락된 설정 때문일 수 있습니다.",
+    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "프록시 오류: 잘못된 응답입니다. 키 누락이나 잘못된 값 입력과 같은 설정 오류이거나 일시적인 서버 장애일 수 있습니다.",
+    "This secret message can only be displayed once. Would you like to see it now?": "이 비밀 메시지는 단 한 번만 표시됩니다. 지금 확인하시겠습니까?",
+    "Yes, see it": "네, 확인하겠습니다",
+    "Dark Mode": "다크 모드",
+    "Error compressing document, due to missing WebAssembly support.": "WebAssembly 지원이 누락되어 문서를 압축하는 동안 오류가 발생했습니다.",
+    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "문서 압축 해제 오류: 브라우저가 WebAssembly를 지원하지 않습니다. 이 문서를 보려면 다른 브라우저를 사용해 주세요.",
+    "Start over": "처음부터 다시 시작",
+    "Document copied to clipboard": "문서가 클립보드에 복사됨",
+    "To copy document press on the copy button or use the clipboard shortcut <kbd>%s</kbd>+<kbd>c</kbd>": "문서를 복사하려면 복사 버튼을 누르거나 클립보드 단축키 <kbd>%s</kbd>+<kbd>c</kbd>를 사용하세요",
+    "Copy link": "링크 복사",
+    "Link copied to clipboard": "링크가 클립보드에 복사됨",
+    "Document text": "문서 텍스트",
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "탭 키가 문자로 입력됩니다 (전환하려면 <kbd>Ctrl</kbd>+<kbd>m</kbd> 또는 <kbd>Esc</kbd> 키를 누르세요)",
+    "Show password": "비밀번호 표시",
+    "Hide password": "비밀번호 숨기기",
+    "Theme": "테마",
+    "Copy": "복사",
     "Cmd": "Cmd",
     "Ctrl": "Ctrl"
 }

+ 3 - 3
i18n/sl.json

@@ -105,7 +105,7 @@
         "Ta dokument bo potekel čez %d minuto.",
         "Ta dokument bo potekel čez %d minuti.",
         "Ta dokument bo potekel čez %d minute.",
-        "Ta dokument bo potekel čez %d minut.",
+        "Ta dokument bo potekel čez %d minute.",
         "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minut."
     ],
@@ -113,13 +113,13 @@
         "Ta dokument bo potekel čez %d uro.",
         "Ta dokument bo potekel čez %d uri.",
         "Ta dokument bo potekel čez %d ure.",
-        "Ta dokument bo potekel čez %d ur.",
+        "Ta dokument bo potekel čez %d ure.",
         "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d ur."
     ],
     "This document will expire in %d days.": [
         "Ta dokument bo potekel čez %d dan.",
-        "Ta dokument bo potekel čez %d dni.",
+        "Ta dokument bo potekel čez %d dneva.",
         "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni.",

+ 2 - 2
i18n/sv.json

@@ -151,7 +151,7 @@
     "server error or not responding": "serverfel eller inget svar",
     "Could not post comment: %s": "Kunde inte publicera kommentar: %s",
     "Sending document…": "Skickar dokument…",
-    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>%s</kbd>+<kbd>c</kbd> to copy)</span>": "Ditt dokument är <a id=\\\"pasteurl\\\" href=\\\"%s\\\">%s</a> <span id=\\\"copyhint\\\">(Tryck <kbd>%s</kbd>+<kbd>c</kbd> för att kopiera)</span>",
+    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>%s</kbd>+<kbd>c</kbd> to copy)</span>": "Ditt dokument är <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Tryck <kbd>%s</kbd>+<kbd>c</kbd> för att kopiera)</span>",
     "Delete data": "Radera data",
     "Could not create document: %s": "Kunde inte skapa dokument: %s",
     "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Kan inte dekryptera dokument: Dekrypteringsnyckel saknas i URL:en (använde du en omdirigerare eller URL-förkortare som tar bort delar av URL:en?)",
@@ -232,7 +232,7 @@
     "Show password": "Visa lösenord",
     "Hide password": "Dölj lösenord",
     "Theme": "Tema",
-    "Copy": "Copy",
+    "Copy": "Kopiera",
     "Cmd": "Cmd",
     "Ctrl": "Ctrl"
 }

+ 0 - 2
js/.istanbul.yml

@@ -1,7 +1,5 @@
 ---
 instrumentation:
-  excludes:
-    - jquery-3.7.1.js
   baseline-file: ../tst/log/js-coverage-baseline.json
 reporting:
   dir: ../tst/log/js-coverage-report

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 5
js/bootstrap-3.4.1.js


+ 51 - 70
js/common.js

@@ -2,23 +2,40 @@
 
 // testing prerequisites
 global.assert = require('assert');
-global.jsc = require('jsverify');
+const fc = require('fast-check');
 global.jsdom = require('jsdom-global');
-global.cleanup = global.jsdom();
+// initial DOM environment created by jsdom-global
+let currentCleanup = global.jsdom();
+
+// Recreates the jsdom environment and reloads privatebin.js into it.
+// The reload is necessary because modules close over DOM element references
+// in their init() methods, and pending async callbacks (e.g. setTimeout in
+// CopyToClipboard) would otherwise fire against elements from a dead window.
+global.cleanup = function (...args) {
+    if (typeof currentCleanup === 'function') {
+        currentCleanup();
+    }
+    currentCleanup = global.jsdom(...args);
+    delete require.cache[require.resolve('./privatebin')];
+    delete require.cache[require.resolve('./legacy')];
+    require('./privatebin');
+    global.PrivateBin = window.PrivateBin;
+    return global.cleanup;
+};
 global.fs = require('fs');
 global.WebCrypto = require('@peculiar/webcrypto').Crypto;
 
 // application libraries to test
-global.$ = global.jQuery = require('./jquery-3.7.1');
 global.zlib = require('./zlib').zlib;
 require('./prettify');
-global.prettyPrint = window.PR.prettyPrint;
-global.prettyPrintOne = window.PR.prettyPrintOne;
+global.prettyPrint = window.PR ? window.PR.prettyPrint : function() {};
+global.prettyPrintOne = window.PR ? window.PR.prettyPrintOne : function() {};
 global.showdown = require('./showdown-2.1.0');
 global.DOMPurify = require('./purify-3.4.12');
 global.baseX = require('./base-x-5.0.1').baseX;
 global.Legacy = require('./legacy').Legacy;
 require('./privatebin');
+global.PrivateBin = window.PrivateBin;
 
 // internal variables
 var a2zString    = ['a','b','c','d','e','f','g','h','i','j','k','l','m',
@@ -35,45 +52,8 @@ var a2zString    = ['a','b','c','d','e','f','g','h','i','j','k','l','m',
     ),
     schemas = ['ftp','http','https'],
     supportedLanguages = ['ar', 'bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'jbo', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sk', 'sl', 'th', 'tr', 'uk', 'zh'],
-    mimeTypes = ['image/png', 'application/octet-stream'],
-    formats = ['plaintext', 'markdown', 'syntaxhighlighting'],
-    mimeFile = fs.createReadStream('/etc/mime.types'),
-    mimeLine = '';
-
-// populate mime types from environment
-mimeFile.on('data', function(data) {
-    mimeLine += data;
-    var index = mimeLine.indexOf('\n');
-    while (index > -1) {
-        var line = mimeLine.substring(0, index);
-        mimeLine = mimeLine.substring(index + 1);
-        parseMime(line);
-        index = mimeLine.indexOf('\n');
-    }
-});
-
-mimeFile.on('end', function() {
-    if (mimeLine.length > 0) {
-        parseMime(mimeLine);
-    }
-});
-
-function parseMime(line) {
-    // ignore comments
-    var index = line.indexOf('#');
-    if (index > -1) {
-        line = line.substring(0, index);
-    }
-
-    // ignore bits after tabs
-    index = line.indexOf('\t');
-    if (index > -1) {
-        line = line.substring(0, index);
-    }
-    if (line.length > 0) {
-        mimeTypes.push(line);
-    }
-}
+    mimeTypes = ['image/png', 'application/octet-stream'].concat(Object.keys(require('mime-db'))),
+    formats = ['plaintext', 'markdown', 'syntaxhighlighting'];
 
 // common testing helper functions
 // as of jsDOM 22 the base64 functions provided in the DOM are more restrictive
@@ -88,68 +68,68 @@ exports.btoa = function(text) {
 };
 
 // provides random lowercase characters from a to z
-exports.jscA2zString = function() {
-    return jsc.elements(a2zString);
+exports.fcA2zString = function() {
+    return fc.constantFrom(...a2zString);
 };
 
 // provides random lowercase alpha numeric characters (a to z and 0 to 9)
-exports.jscAlnumString = function() {
-    return jsc.elements(alnumString);
+exports.fcAlnumString = function() {
+    return fc.constantFrom(...alnumString);
 };
 
 //provides random characters allowed in hexadecimal notation
-exports.jscHexString = function() {
-    return jsc.elements(hexString);
+exports.fcHexString = function() {
+    return fc.constantFrom(...hexString);
 };
 
 // provides random characters allowed in GET queries
-exports.jscQueryString = function() {
-    return jsc.elements(queryString);
+exports.fcQueryString = function() {
+    return fc.constantFrom(...queryString);
 };
 
 // provides random characters allowed in hash queries
-exports.jscHashString = function() {
-    return jsc.elements(hashString);
+exports.fcHashString = function() {
+    return fc.constantFrom(...hashString);
 };
 
 // provides random characters allowed in base64 encoded strings
-exports.jscBase64String = function() {
-    return jsc.elements(base64String);
+exports.fcBase64String = function() {
+    return fc.constantFrom(...base64String);
 };
 
 // provides a random URL schema supported by the whatwg-url library
-exports.jscSchemas = function(withFtp = true) {
-    return jsc.elements(withFtp ? schemas : schemas.slice(1));
+exports.fcSchemas = function(withFtp = true) {
+    return fc.constantFrom(...(withFtp ? schemas : schemas.slice(1)));
 };
 
 // provides a random supported language string
-exports.jscSupportedLanguages = function() {
-    return jsc.elements(supportedLanguages);
+exports.fcSupportedLanguages = function() {
+    return fc.constantFrom(...supportedLanguages);
 };
 
 // provides a random mime type
-exports.jscMimeTypes = function() {
-    return jsc.elements(mimeTypes);
+exports.fcMimeTypes = function() {
+    return fc.constantFrom(...mimeTypes);
 };
 
 // provides a random PrivateBin document formatter
-exports.jscFormats = function() {
-    return jsc.elements(formats);
+exports.fcFormats = function() {
+    return fc.constantFrom(...formats);
 };
 
 // provides random URLs
-exports.jscUrl = function(withFragment = true, withQuery = true) {
+exports.fcUrl = function(withFragment = true, withQuery = true) {
     let url = {
-        schema: exports.jscSchemas(),
-        address: jsc.nearray(exports.jscA2zString()),
+        schema: exports.fcSchemas(),
+        address: fc.array(exports.fcA2zString(), {minLength: 1}),
     };
     if (withFragment) {
-        url.fragment = jsc.string;
+        url.fragment = fc.string();
     }
     if(withQuery) {
-        url.query = jsc.array(exports.jscQueryString());
+        url.query = fc.array(exports.fcQueryString());
     }
-    return jsc.record(url);
+    return fc.record(url);
 };
 
 exports.urlToString = function (url) {
@@ -159,6 +139,7 @@ exports.urlToString = function (url) {
 };
 
 exports.enableClipboard = function () {
+    // @ts-ignore
     navigator.clipboard = (function () {
         let savedText = "";
 

+ 2 - 2
js/eslint.config.js

@@ -1,5 +1,5 @@
 const globals = require('globals');
-const { globalIgnores } = require('eslint/config')
+const { globalIgnores } = require('eslint/config');
 
 module.exports = [globalIgnores(["./*.*js", "!./privatebin.js"]), {
     languageOptions: {
@@ -111,7 +111,7 @@ module.exports = [globalIgnores(["./*.*js", "!./privatebin.js"]), {
         yoda: 0,
 
         // Strict
-        strict: 0,
+        strict: 2,
 
         // Variables
         "init-declarations": 0,

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


+ 22 - 0
js/jsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "es2018",
+    "module": "NodeNext",
+    "moduleResolution": "nodenext",
+    "resolveJsonModule": true,
+    "checkJs": true,
+    "strict": true,
+    "noImplicitAny": false,
+    "forceConsistentCasingInFileNames": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "baseUrl": "./js",
+    "paths": {
+      "/*": ["./*"],
+    }
+  },
+  "exclude": [
+    "node_modules",
+    "**/node_modules/*"
+  ]
+}

+ 8 - 9
js/legacy.js

@@ -13,7 +13,6 @@
 /**
  * IMPORTANT NOTICE FOR DEVELOPERS:
  * The logic in this file is intended to run in legacy browsers. Avoid any use of:
- * - jQuery (doesn't work in older browsers)
  * - ES5 or newer in general
  * - const/let, use the traditional var declarations instead
  * - async/await or Promises, use traditional callbacks
@@ -68,26 +67,26 @@
             'Spider',
             'scraper',
             'Scraper',
-            
+
             // Search Engines
             'Mediapartners-Google',
             'BingPreview',
             'Yahoo! Slurp',
-            
+
             // SEO & Analytics
             'Screaming Frog',
-            
+
             // Social Media
             'facebookexternalhit',
-            
+
             // AI & LLM
             'ChatGPT-User',
             'anthropic-ai',
-            
+
             // Security Scanners
             'CensysInspect',
             'Shodan',
-            
+
             // Other Common Crawlers
             '80legs',
             'ia_archiver',
@@ -171,7 +170,7 @@
          * @private
          * @name   Check.isBadBot
          * @function
-         * @return {bool}
+         * @return {Boolean}
          */
         function isBadBot() {
             // check whether a bot user agent part can be found in the current
@@ -262,7 +261,7 @@
          *
          * @name   Check.getStatus
          * @function
-         * @return {bool}
+         * @return {Boolean}
          */
         me.getStatus = function()
         {

+ 322 - 0
js/package-lock.json

@@ -11,8 +11,10 @@
       "devDependencies": {
         "@peculiar/webcrypto": "^1.5.0",
         "eslint": "^9.37.0",
+        "fast-check": "^4.7.0",
         "jsdom": "^26.1.0",
         "jsdom-global": "^3.0.2",
+        "jshint": "^2.13.6",
         "jsverify": "^0.8.3",
         "mime-types": "^3.0.2",
         "mocha": "^11.7.5",
@@ -1121,6 +1123,42 @@
         "node": ">=6"
       }
     },
+    "node_modules/cli": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz",
+      "integrity": "sha512-41U72MB56TfUMGndAKK8vJ78eooOD4Z5NOL4xEfjc0c23s+6EYKXlXsmACBVclLP1yOfWCgEganVzddVrSNoTg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "exit": "0.1.2",
+        "glob": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=0.2.5"
+      }
+    },
+    "node_modules/cli/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/cliui": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -1223,12 +1261,28 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "dev": true
     },
+    "node_modules/console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha512-duS7VP5pvfsNLDvL1O4VOEbw37AI3A4ZUQYemvDlnpGrNu9tprR7BYWpDYwC0Xia0Zxz5ZupdiIrUp0GH1aXfg==",
+      "dev": true,
+      "dependencies": {
+        "date-now": "^0.1.4"
+      }
+    },
     "node_modules/convert-source-map": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
       "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
       "dev": true
     },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1269,6 +1323,12 @@
         "node": ">=18"
       }
     },
+    "node_modules/date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha512-AsElvov3LoNB7tf5k37H2jYSB+ZZPMT5sG2QjJCcdlV5chIv6htBUBUui2IKRjgtKAKtCBN7Zbwa+MtwLjSeNw==",
+      "dev": true
+    },
     "node_modules/debug": {
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1331,6 +1391,66 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/dom-serializer": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "entities": "^2.0.0"
+      }
+    },
+    "node_modules/dom-serializer/node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/dom-serializer/node_modules/entities": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz",
+      "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==",
+      "dev": true,
+      "dependencies": {
+        "domelementtype": "1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+      "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==",
+      "dev": true,
+      "dependencies": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1547,6 +1667,38 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/fast-check": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
+      "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "pure-rand": "^8.0.0"
+      },
+      "engines": {
+        "node": ">=12.17.0"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1674,6 +1826,13 @@
         }
       ]
     },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1828,6 +1987,27 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "node_modules/htmlparser2": {
+      "version": "3.8.3",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
+      "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "1",
+        "domhandler": "2.3",
+        "domutils": "1.5",
+        "entities": "1.0",
+        "readable-stream": "1.1"
+      }
+    },
+    "node_modules/htmlparser2/node_modules/entities": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
+      "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==",
+      "dev": true,
+      "license": "BSD-like"
+    },
     "node_modules/http-proxy-agent": {
       "version": "7.0.2",
       "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -1910,6 +2090,25 @@
         "node": ">=8"
       }
     },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2004,6 +2203,13 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2213,6 +2419,51 @@
         "node": ">=6"
       }
     },
+    "node_modules/jshint": {
+      "version": "2.13.6",
+      "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.13.6.tgz",
+      "integrity": "sha512-IVdB4G0NTTeQZrBoM8C5JFVLjV2KtZ9APgybDA1MK73xb09qFs0jCXyQLnCOp1cSZZZbvhq/6mfXHUTaDkffuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cli": "~1.0.0",
+        "console-browserify": "1.1.x",
+        "exit": "0.1.x",
+        "htmlparser2": "3.8.x",
+        "lodash": "~4.17.21",
+        "minimatch": "~3.0.2",
+        "strip-json-comments": "1.0.x"
+      },
+      "bin": {
+        "jshint": "bin/jshint"
+      }
+    },
+    "node_modules/jshint/node_modules/minimatch": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+      "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/jshint/node_modules/strip-json-comments": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
+      "integrity": "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "strip-json-comments": "cli.js"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
     "node_modules/json-buffer": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -2304,6 +2555,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/lodash.flattendeep": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
@@ -2819,6 +3077,16 @@
         "node": ">=6"
       }
     },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
     "node_modules/optionator": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2942,6 +3210,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -3067,6 +3345,23 @@
         "node": ">=6"
       }
     },
+    "node_modules/pure-rand": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
+      "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/dubzzz"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fast-check"
+        }
+      ],
+      "license": "MIT"
+    },
     "node_modules/pvtsutils": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
@@ -3103,6 +3398,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/readable-stream": {
+      "version": "1.1.14",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+      "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.1",
+        "isarray": "0.0.1",
+        "string_decoder": "~0.10.x"
+      }
+    },
     "node_modules/readdirp": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -3415,6 +3723,13 @@
       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
       "dev": true
     },
+    "node_modules/string_decoder": {
+      "version": "0.10.31",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+      "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -3974,6 +4289,13 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/write-file-atomic": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",

+ 3 - 0
js/package.json

@@ -9,14 +9,17 @@
   "devDependencies": {
     "@peculiar/webcrypto": "^1.5.0",
     "eslint": "^9.37.0",
+    "fast-check": "^4.7.0",
     "jsdom": "^26.1.0",
     "jsdom-global": "^3.0.2",
+    "jshint": "^2.13.6",
     "jsverify": "^0.8.3",
     "mime-types": "^3.0.2",
     "mocha": "^11.7.5",
     "nyc": "^18.0.0"
   },
   "scripts": {
+    "lint": "eslint",
     "test": "mocha",
     "ci-test": "mocha --reporter xunit --reporter-option output=mocha-results.xml"
   },

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 203 - 235
js/privatebin.js


+ 322 - 323
js/test/Alert.js

@@ -1,313 +1,313 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Alert', function () {
     describe('showStatus', function () {
-        jsc.property(
-            'shows a status message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="status">' + message + '</div>';
-                $('body').html(
-                    '<div id="status"></div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showStatus(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a status message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="status">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="status"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a status message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            function (message) {
-                message = message.join('');
-                const expected = '<div id="status" role="alert" ' +
-                    'class="statusmessage alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-info-sign" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info hidden"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showStatus(message);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a status message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                function (message) {
+                    message = message.join('');
+                    const expected = '<div id="status" role="alert" ' +
+                        'class="statusmessage alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-info-sign" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info hidden"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a status message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="status" role="alert" ' +
-                    'class="statusmessage alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info hidden"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showStatus(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a status message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="status" role="alert" ' +
+                        'class="statusmessage alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info hidden"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showWarning', function () {
-        jsc.property(
-            'shows a warning message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage">' + message + '</div>';
-                $('body').html(
-                    '<div id="errormessage"></div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showWarning(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a warning message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a warning message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message) {
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-warning-sign" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showWarning(message);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a warning message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message) {
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-warning-sign" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a warning message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showWarning(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows a warning message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showError', function () {
-        jsc.property(
-            'shows an error message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage">' + message + '</div>';
-                $('body').html(
-                    '<div id="errormessage"></div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showError(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows an error message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows an error message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-alert" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showError(message);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows an error message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-alert" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows an error message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                $('body').html(
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showError(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows an error message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showRemaining', function () {
-        jsc.property(
-            'shows remaining time (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            'integer',
-            function (message, string, number) {
-                message = message.join('');
-                string = string.join('');
-                const expected = '<div id="remainingtime" class="">' + string + message + number + '</div>';
-                $('body').html(
-                    '<div id="remainingtime" class="hidden"></div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows remaining time (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                fc.integer(),
+                function (message, string, number) {
+                    message = message.join('');
+                    string = string.join('');
+                    const expected = '<div id="remainingtime" class="">' + string + message + number + '</div>';
+                    document.body.innerHTML =
+                        '<div id="remainingtime" class="hidden"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows remaining time (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            'integer',
-            function (message, string, number) {
-                message = message.join('');
-                string = string.join('');
-                const expected = '<div id="remainingtime" role="alert" ' +
-                    'class="alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-fire" aria-hidden="true">' +
-                    '</span> <span>' + string + message + number + '</span></div>';
-                $('body').html(
-                    '<div id="remainingtime" role="alert" class="hidden ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-fire" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+        it('shows remaining time (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                fc.integer(),
+                function (message, string, number) {
+                    message = message.join('');
+                    string = string.join('');
+                    const expected = '<div id="remainingtime" role="alert" ' +
+                        'class="alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-fire" aria-hidden="true">' +
+                        '</span> <span>' + string + message + number + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="remainingtime" role="alert" class="hidden ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-fire" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showLoading', function () {
-        jsc.property(
-            'shows a loading message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message, icon) {
-                message = message.join('');
-                icon = icon.join('');
-                const defaultMessage = 'Loading…';
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('shows a loading message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message, icon) {
+                    message = message.join('');
+                    icon = icon.join('');
+                    const defaultMessage = 'Loading…';
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    const expected = '<div id="loadingindicator" class="">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="loadingindicator" class="hidden">' + defaultMessage + '</div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showLoading(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
                 }
-                const expected = '<div id="loadingindicator" class="">' + message + '</div>';
-                $('body').html(
-                    '<div id="loadingindicator" class="hidden">' + defaultMessage + '</div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showLoading(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+            ));
+        });
 
-        jsc.property(
-            'shows a loading message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message, icon) {
-                message = message.join('');
-                icon = icon.join('');
-                const defaultMessage = 'Loading…';
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('shows a loading message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message, icon) {
+                    message = message.join('');
+                    icon = icon.join('');
+                    const defaultMessage = 'Loading…';
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    const expected = '<ul class="nav navbar-nav"><li ' +
+                        'id="loadingindicator" class="navbar-text"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></li></ul>';
+                    document.body.innerHTML =
+                        '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
+                        'class="navbar-text hidden"><span class="glyphicon ' +
+                        'glyphicon-time" aria-hidden="true"></span> ' +
+                        defaultMessage + '</li></ul>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showLoading(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
                 }
-                const expected = '<ul class="nav navbar-nav"><li ' +
-                    'id="loadingindicator" class="navbar-text"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></li></ul>';
-                $('body').html(
-                    '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
-                    'class="navbar-text hidden"><span class="glyphicon ' +
-                    'glyphicon-time" aria-hidden="true"></span> ' +
-                    defaultMessage + '</li></ul>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.showLoading(message, icon);
-                const result = $('body').html();
-                return expected === result;
-            }
-        );
+            ));
+        });
     });
 
     describe('hideLoading', function () {
         it(
             'hides the loading message',
             function() {
-                $('body').html(
+                document.body.innerHTML =
                     '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
                     'class="navbar-text"><span class="glyphicon ' +
                     'glyphicon-time" aria-hidden="true"></span> ' +
-                    'Loading…</li></ul>'
-                );
-                $('body').addClass('loading');
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.hideLoading();
-                assert.ok(!$('body').hasClass('loading'));
-                assert.ok($('#loadingindicator').hasClass('hidden'));
+                    'Loading…</li></ul>';
+                document.body.classList.add('loading');
+                PrivateBin.Alert.init();
+                PrivateBin.Alert.hideLoading();
+                assert.ok(!document.body.classList.contains('loading'));
+                assert.ok(document.getElementById('loadingindicator').classList.contains('hidden'));
             }
         );
     });
@@ -316,64 +316,63 @@ describe('Alert', function () {
         it(
             'hides all messages',
             function() {
-                $('body').html(
+                document.body.innerHTML =
                     '<div id="status" role="alert" class="statusmessage ' +
                     'alert alert-info"><span class="glyphicon ' +
                     'glyphicon-info-sign" aria-hidden="true"></span> </div>' +
                     '<div id="errormessage" role="alert" class="statusmessage ' +
                     'alert alert-danger"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.hideMessages();
-                assert.ok($('#status').hasClass('hidden'));
-                assert.ok($('#errormessage').hasClass('hidden'));
+                    'glyphicon-alert" aria-hidden="true"></span> </div>';
+                PrivateBin.Alert.init();
+                PrivateBin.Alert.hideMessages();
+                assert.ok(document.getElementById('status').classList.contains('hidden'));
+                assert.ok(document.getElementById('errormessage').classList.contains('hidden'));
             }
         );
     });
 
     describe('setCustomHandler', function () {
-        jsc.property(
-            'calls a given handler function',
-            'nat 3',
-            jsc.array(common.jscAlnumString()),
-            function (trigger, message) {
-                message = message.join('');
-                let handlerCalled = false,
-                    defaultMessage = 'Loading…',
-                    functions = [
-                        $.PrivateBin.Alert.showStatus,
-                        $.PrivateBin.Alert.showError,
-                        $.PrivateBin.Alert.showRemaining,
-                        $.PrivateBin.Alert.showLoading
-                    ];
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('calls a given handler function', () => {
+            fc.assert(fc.property(
+                fc.integer({min: 0, max: 3}),
+                fc.array(common.fcAlnumString()),
+                function (trigger, message) {
+                    message = message.join('');
+                    let handlerCalled = false,
+                        defaultMessage = 'Loading…',
+                        functions = [
+                            PrivateBin.Alert.showStatus,
+                            PrivateBin.Alert.showError,
+                            PrivateBin.Alert.showRemaining,
+                            PrivateBin.Alert.showLoading
+                        ];
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    document.body.innerHTML =
+                        '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
+                        'class="navbar-text hidden"><span class="glyphicon ' +
+                        'glyphicon-time" aria-hidden="true"></span> ' +
+                        defaultMessage + '</li></ul>' +
+                        '<div id="remainingtime" role="alert" class="hidden ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-fire" aria-hidden="true"></span> </div>' +
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>' +
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.setCustomHandler(function(id, element) {
+                        handlerCalled = true;
+                        return Math.random() > 0.5 ? true : element;
+                    });
+                    functions[trigger](message);
+                    PrivateBin.Alert.setCustomHandler(null);
+                    return handlerCalled;
                 }
-                $('body').html(
-                    '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
-                    'class="navbar-text hidden"><span class="glyphicon ' +
-                    'glyphicon-time" aria-hidden="true"></span> ' +
-                    defaultMessage + '</li></ul>' +
-                    '<div id="remainingtime" role="alert" class="hidden ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-fire" aria-hidden="true"></span> </div>' +
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>' +
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>'
-                );
-                $.PrivateBin.Alert.init();
-                $.PrivateBin.Alert.setCustomHandler(function(id, $element) {
-                    handlerCalled = true;
-                    return jsc.random(0, 1) ? true : $element;
-                });
-                functions[trigger](message);
-                $.PrivateBin.Alert.setCustomHandler(null);
-                return handlerCalled;
-            }
-        );
+            ));
+        });
     });
 });

+ 171 - 137
js/test/AttachmentViewer.js

@@ -1,140 +1,139 @@
 'use strict';
 const common = require('../common');
+const fc = require('fast-check');
 
 describe('AttachmentViewer', function () {
-    describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () {
+    beforeEach(() => {
+        mockCreateObjectUrl();
+    });
+
+    afterEach(() => {
+        globalThis.cleanup()
+    });
+
+    describe('whole run (setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo)', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'displays & hides data as requested',
-            common.jscMimeTypes(),
-            'string',
-            'string',
-            'string',
-            'string',
-             // eslint-disable-next-line complexity
-            function (mimeType, rawdata, filename, prefix, postfix) {
-                let clean = jsdom(),
-                    data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
-                    mimePrefix = mimeType.substring(0, 6),
-                    previewSupported = (
-                        mimePrefix === 'image/' ||
-                        mimePrefix === 'audio/' ||
-                        mimePrefix === 'video/' ||
-                        mimeType.match(/\/pdf/i)
-                    ),
-                    results = [],
-                    result = '';
-                // text node of attachment will truncate at null byte
-                if (filename === '\u0000') {
-                    filename = '';
-                }
-                prefix  = prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
-                $('body').html(
-                    '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
-                    '<div id="attachment" class="hidden"></div>' +
-                    '<div id="templates">' +
-                        '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
-                            '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
-                            '<a class="alert-link">Download attachment</a>' +
-                        '</div>' +
-                    '</div>'
-                );
-                // mock createObjectURL for jsDOM
-                if (typeof window.URL.createObjectURL === 'undefined') {
-                    Object.defineProperty(
-                        window.URL,
-                        'createObjectURL',
-                        {value: function(blob) {
-                            return 'blob:' + location.origin + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
-                        }}
+        it('displays & hides data as requested', () => {
+            fc.assert(fc.property(
+                common.fcMimeTypes(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                // eslint-disable-next-line complexity
+                function (mimeType, rawdata, filename, prefix, postfix) {
+                    let data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
+                        mimePrefix = mimeType.substring(0, 6),
+                        previewSupported = (
+                            mimePrefix === 'image/' ||
+                            mimePrefix === 'audio/' ||
+                            mimePrefix === 'video/' ||
+                            mimeType.match(/\/pdf/i)
+                        ),
+                        results = [],
+                        result = '';
+                    // text node of attachment will truncate at null byte
+                    if (filename === '\u0000') {
+                        filename = '';
+                    }
+                    prefix  = prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
+                    document.body.innerHTML = (
+                        '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
+                        '<div id="attachment" class="hidden"></div>' +
+                        '<div id="templates">' +
+                            '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
+                                '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
+                                '<a class="alert-link">Download attachment</a>' +
+                            '</div>' +
+                        '</div>'
                     );
-                }
-                $.PrivateBin.AttachmentViewer.init();
-                $.PrivateBin.Model.init();
-                results.push(
-                    !$.PrivateBin.AttachmentViewer.hasAttachment() &&
-                    $('#attachment').hasClass('hidden') &&
-                    $('#attachment').children().length === 0 &&
-                    $('#attachmenttemplate').hasClass('hidden') &&
-                    $('#attachmentPreview').hasClass('hidden')
-                );
-                global.atob = common.atob;
-                if (filename.length) {
-                    $.PrivateBin.AttachmentViewer.setAttachment(data, filename);
-                } else {
-                    $.PrivateBin.AttachmentViewer.setAttachment(data);
-                }
-                // // beyond this point we will get the blob URL instead of the data
-                data = window.URL.createObjectURL(data);
-                const attachment = $.PrivateBin.AttachmentViewer.getAttachments();
-                results.push(
-                    $.PrivateBin.AttachmentViewer.hasAttachment() &&
-                    $('#attachment').hasClass('hidden') &&
-                    $('#attachment').children().length > 0 &&
-                    $('#attachmentPreview').hasClass('hidden') &&
-                    attachment[0][0] === data &&
-                    attachment[0][1] === filename
-                );
-                $.PrivateBin.AttachmentViewer.showAttachment();
-                results.push(
-                    !$('#attachment').hasClass('hidden') &&
-                    $('#attachment').children().length > 0 &&
-                    (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
-                );
-                $.PrivateBin.AttachmentViewer.hideAttachment();
-                results.push(
-                    $('#attachment').hasClass('hidden') &&
-                    (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
-                );
-                if (previewSupported) {
-                    $.PrivateBin.AttachmentViewer.hideAttachmentPreview();
-                    results.push($('#attachmentPreview').hasClass('hidden'));
-                }
-                $.PrivateBin.AttachmentViewer.showAttachment();
-                results.push(
-                    !$('#attachment').hasClass('hidden') &&
-                    (previewSupported ? !$('#attachmentPreview').hasClass('hidden') : $('#attachmentPreview').hasClass('hidden'))
-                );
-                let element = $('<div>');
-                $.PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
-                // messageIDs with links get a relaxed treatment
-                if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
-                    result = $('<textarea>').text((prefix + filename + postfix)).text();
-                } else {
-                    result = DOMPurify.sanitize(
-                        prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix, {
-                            ALLOWED_TAGS: ['a', 'i', 'span'],
-                            ALLOWED_ATTR: ['href', 'id']
-                        }
+                    PrivateBin.AttachmentViewer.init();
+                    PrivateBin.Model.init();
+                    results.push(
+                        !PrivateBin.AttachmentViewer.hasAttachment() &&
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length === 0 &&
+                        document.getElementById('attachmenttemplate').classList.contains('hidden') &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden')
                     );
-                }
-                if (filename.length) {
+                    global.atob = common.atob;
+                    if (filename.length) {
+                        PrivateBin.AttachmentViewer.setAttachment(data, filename);
+                    } else {
+                        PrivateBin.AttachmentViewer.setAttachment(data);
+                    }
+                    // // beyond this point we will get the blob URL instead of the data
+                    data = window.URL.createObjectURL(data);
+                    const attachment = PrivateBin.AttachmentViewer.getAttachments();
+                    results.push(
+                        PrivateBin.AttachmentViewer.hasAttachment() &&
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length > 0 &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden') &&
+                        attachment[0][0] === data &&
+                        attachment[0][1] === filename
+                    );
+                    PrivateBin.AttachmentViewer.showAttachment();
+                    results.push(
+                        !document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length > 0 &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
+                    );
+                    PrivateBin.AttachmentViewer.hideAttachment();
                     results.push(
-                        element.find('a')[0].href === data &&
-                        element.find('a')[0].getAttribute('download') === filename &&
-                        element.find('a')[0].text === result
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
                     );
-                } else {
-                    results.push(element.find('a')[0].href === data);
+                    if (previewSupported) {
+                        PrivateBin.AttachmentViewer.hideAttachmentPreview();
+                        results.push(document.getElementById('attachmentPreview').classList.contains('hidden'));
+                    }
+                    PrivateBin.AttachmentViewer.showAttachment();
+                    results.push(
+                        !document.getElementById('attachment').classList.contains('hidden') &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
+                    );
+                    let element = document.createElement('div');
+                    PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
+                    // messageIDs with links get a relaxed treatment
+                    if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
+                        const tempTA = document.createElement('textarea');
+                        tempTA.textContent = (prefix + filename + postfix);
+                        result = tempTA.textContent;
+                    } else {
+                        result = DOMPurify.sanitize(
+                            prefix + PrivateBin.Helper.htmlEntities(filename) + postfix, {
+                                ALLOWED_TAGS: ['a', 'i', 'span'],
+                                ALLOWED_ATTR: ['href', 'id']
+                            }
+                        );
+                    }
+                    if (filename.length) {
+                        results.push(
+                            element.querySelector('a').href === data &&
+                            element.querySelector('a').getAttribute('download') === filename &&
+                            element.querySelector('a').textContent === result
+                        );
+                    } else {
+                        results.push(element.querySelector('a').href === data);
+                    }
+                    PrivateBin.AttachmentViewer.removeAttachment();
+                    results.push(
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length === 0 &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden')
+                    );
+                    return results.every(element => element);
                 }
-                $.PrivateBin.AttachmentViewer.removeAttachment();
-                results.push(
-                    $('#attachment').hasClass('hidden') &&
-                    $('#attachment').children().length === 0 &&
-                    $('#attachmentPreview').hasClass('hidden')
-                );
-                clean();
-                return results.every(element => element);
-            }
-        );
+            ));
+        });
 
         it(
             'sanitizes file names in attachments',
             function() {
-                const clean = jsdom();
-                $('body').html(
+                document.body.innerHTML = (
                     '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
                     '<div id="attachment" class="hidden"></div>' +
                     '<div id="templates">' +
@@ -144,18 +143,8 @@ describe('AttachmentViewer', function () {
                         '</div>' +
                     '</div>'
                 );
-                // mock createObjectURL for jsDOM
-                if (typeof window.URL.createObjectURL === 'undefined') {
-                    Object.defineProperty(
-                        window.URL,
-                        'createObjectURL',
-                        {value: function(blob) {
-                            return 'blob:' + location.origin + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
-                        }}
-                    );
-                }
-                $.PrivateBin.AttachmentViewer.init();
-                $.PrivateBin.Model.init();
+                PrivateBin.AttachmentViewer.init();
+                PrivateBin.Model.init();
                 global.atob = common.atob;
 
                 const maliciousFileNames = [
@@ -163,12 +152,57 @@ describe('AttachmentViewer', function () {
                     '"><meta http-equiv="refresh" content="0;url=http://example.com/">.txt'
                 ];
                 for (const filename of maliciousFileNames) {
-                    $.PrivateBin.AttachmentViewer.setAttachment('data:;base64,', filename);
-                    assert.ok(!$('body').html().includes(filename));
+                    PrivateBin.AttachmentViewer.setAttachment('data:;base64,', filename);
+                    assert.ok(!document.body.innerHTML.includes(filename));
                 }
-                clean();
             }
         );
+    });
 
+    describe('showAttachment()', function () {
+        it('displays attachment even when attachmentPreview element is missing',
+            function() {
+                document.body.innerHTML = (
+                    '<div id="attachment" class="hidden"></div>' +
+                    '<div id="templates">' +
+                        '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
+                            '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
+                            '<a class="alert-link">Download attachment</a>' +
+                        '</div>' +
+                    '</div>'
+                );
+                // Note: attachmentPreview element is intentionally NOT created
+
+                PrivateBin.AttachmentViewer.init();
+                PrivateBin.Model.init();
+                global.atob = common.atob;
+
+                // Set attachment without preview element
+                PrivateBin.AttachmentViewer.setAttachment('data:text/plain;base64,', 'test.txt');
+
+                // Show attachment should work even without attachmentPreview
+                PrivateBin.AttachmentViewer.showAttachment();
+
+                const attachment = document.getElementById('attachment');
+                assert.ok(!attachment.classList.contains('hidden'), 'Attachment should be visible');
+                assert.ok(attachment.children.length > 0, 'Attachment should have content');
+            }
+        )
     });
+
+    function mockCreateObjectUrl() {
+        if (typeof window.URL.createObjectURL === 'undefined') {
+            Object.defineProperty(
+                window.URL,
+                'createObjectURL',
+                {
+                    value: function (_blob) {
+                        return 'blob:' + location.origin + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
+                    }
+                }
+            );
+        }
+    }
 });
+
+

+ 68 - 63
js/test/Check.js

@@ -1,18 +1,19 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 /* global Legacy, WebCrypto */
 
 describe('Check', function () {
     describe('init', function () {
         this.timeout(30000);
 
-        it('returns false and shows error, if a bot UA is detected', function () {
-            jsc.assert(jsc.forall(
-                'string',
-                jsc.elements(['Bot', 'bot']),
-                'string',
+        it('returns false and shows error, if a bot UA is detected', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.constantFrom('Bot', 'bot'),
+                fc.string(),
                 function (prefix, botBit, suffix) {
-                    const clean = jsdom(
+                    const clean = globalThis.cleanup(
                         '<html><body><div id="errormessage" class="hidden"></div>' +
                         '</body></html>', {
                             'userAgent': prefix + botBit + suffix
@@ -24,66 +25,70 @@ describe('Check', function () {
                     return result;
                 }
             ),
-            {tests: 10});
+            {numRuns: 10});
         });
 
-        jsc.property(
-            'shows error, if no webcrypto is detected',
-            'bool',
-            jsc.elements(['localhost', '127.0.0.1', '[::1]', '']),
-            jsc.nearray(common.jscA2zString()),
-            jsc.elements(['.onion', '.i2p', '']),
-            function (secureProtocol, localhost, domain, tld) {
-                const isDomain = localhost === '',
-                      isSecureContext = secureProtocol || !isDomain || tld.length > 0,
-                      clean = jsdom(
-                          '<html><body><div id="errormessage" class="hidden"></div>' +
-                          '<div id="oldnotice" class="hidden"></div>' +
-                          '<div id="insecurecontextnotice" class="hidden"></div></body></html>',
-                          {
-                              'url': (secureProtocol ? 'https' : 'http' ) + '://' +
-                                     (isDomain ? domain.join('') + tld : localhost) + '/'
-                          }
-                      );
-                Legacy.Check.init();
-                const result1 = Legacy.Check.getInit() && !Legacy.Check.getStatus(),
-                      result2 = isSecureContext === (document.getElementById('insecurecontextnotice').className === 'hidden'),
-                      result3 = (document.getElementById('oldnotice').className !== 'hidden');
-                clean();
-                if (result1 && result2 && result3) {
-                    return true;
+        it('shows error, if no webcrypto is detected', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.constantFrom('localhost', '127.0.0.1', '[::1]', ''),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                fc.constantFrom('.onion', '.i2p', ''),
+                function (secureProtocol, localhost, domain, tld) {
+                    const isDomain = localhost === '',
+                          isSecureContext = secureProtocol || !isDomain || tld.length > 0,
+                          clean = globalThis.cleanup(
+                              '<html><body><div id="errormessage" class="hidden"></div>' +
+                              '<div id="oldnotice" class="hidden"></div>' +
+                              '<div id="insecurecontextnotice" class="hidden"></div></body></html>',
+                              {
+                                  'url': (secureProtocol ? 'https' : 'http' ) + '://' +
+                                         (isDomain ? domain.join('') + tld : localhost) + '/'
+                              }
+                          );
+                    Legacy.Check.init();
+                    const result1 = Legacy.Check.getInit() && !Legacy.Check.getStatus(),
+                          result2 = isSecureContext === (document.getElementById('insecurecontextnotice').className === 'hidden'),
+                          result3 = (document.getElementById('oldnotice').className !== 'hidden');
+                    clean();
+                    if (result1 && result2 && result3) {
+                        return true;
+                    }
+                    console.log(result1, result2, result3);
+                    return false;
                 }
-                console.log(result1, result2, result3);
-                return false;
-            }
-        );
+            ));
+        });
 
-        jsc.property(
-            'shows error, if HTTP only site is detected',
-            'bool',
-            jsc.nearray(common.jscA2zString()),
-            function (secureProtocol, domain) {
-                const clean = jsdom(
-                          '<html><body><div id="httpnotice" class="hidden"></div>' +
-                          '</body></html>',
-                          {
-                              'url': (secureProtocol ? 'https' : 'http' ) + '://' + domain.join('') + '/'
-                          }
-                      );
-                Object.defineProperty(window, 'crypto', {
-                    value: new WebCrypto(),
-                    writeable: false
-                });
-                Legacy.Check.init();
-                const result1 = Legacy.Check.getInit() && Legacy.Check.getStatus(),
-                      result2 = secureProtocol === (document.getElementById('httpnotice').className === 'hidden');
-                clean();
-                if (result1 && result2) {
-                    return true;
+        it('shows error, if HTTP only site is detected', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                function (secureProtocol, domain) {
+                    const clean = globalThis.cleanup(
+                              '<html><body><div id="httpnotice" class="hidden"></div>' +
+                              '</body></html>',
+                              {
+                                  'url': (secureProtocol ? 'https' : 'http' ) + '://' + domain.join('') + '/'
+                              }
+                          );
+                    Object.defineProperty(window, 'crypto', {
+                        value: new WebCrypto(),
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
+                    });
+                    Legacy.Check.init();
+                    const result1 = Legacy.Check.getInit() && Legacy.Check.getStatus(),
+                          result2 = secureProtocol === (document.getElementById('httpnotice').className === 'hidden');
+                    clean();
+                    if (result1 && result2) {
+                        return true;
+                    }
+                    console.log(result1, result2);
+                    return false;
                 }
-                console.log(result1, result2);
-                return false;
-            }
-        );
+            ));
+        });
     });
 });

+ 142 - 116
js/test/CopyToClipboard.js

@@ -1,141 +1,167 @@
 'use strict';
 const common = require('../common');
+const fc = require('fast-check');
 
-describe('CopyToClipboard', function() {
-    this.timeout(30000);
-
-    describe ('Copy document to clipboard', function () {
-        jsc.property('Copy with button click',
-            common.jscFormats(),
-            'nestring',
-            async function (format, text) {
-                var clean = jsdom();
-                common.enableClipboard();
-
-                $('body').html(
-                    '<div id="placeholder"></div>' +
-					'<div id="attachmentPreview" class="hidden"></div>' +
-                    '<h5 id="copyShortcutHint" class="hidden">' +
-						'<small id="copyShortcutHintText"></small>' +
-						'<button type="button" id="copyShortcutHintBtn"></button>' +
-					'</h5>' +
-                    '<div id="prettymessage" class="hidden">' +
-						'<pre id="prettyprint"></pre>' +
-					'</div>'
-                );
-
-                $.PrivateBin.PasteViewer.init();
-                $.PrivateBin.PasteViewer.setFormat(format);
-                $.PrivateBin.PasteViewer.setText(text);
-                $.PrivateBin.PasteViewer.run();
-
-                $.PrivateBin.CopyToClipboard.init();
-
-                $('#copyShortcutHintBtn').trigger('click');
-
-                const savedToClipboardText = await navigator.clipboard.readText();
-
-                clean();
+describe('CopyToClipboard', function () {
+    afterEach(() => {
+        globalThis.cleanup();
+    });
 
-                return text === savedToClipboardText;
-            }
-        );
+    describe('Copy document to clipboard', function () {
+        it('Copy with button click', async function () {
+            await fc.assert(fc.asyncProperty(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                async function (format, text) {
+                    common.enableClipboard();
+
+                    document.body.innerHTML = (
+                        '<div id="status" class="hidden"></div>' +
+                        '<div id="errormessage" class="hidden"></div>' +
+                        '<div id="placeholder" class="hidden">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden">' +
+                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
+                        '<svg id="copySuccessIcon"></svg></button><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>' +
+                        '<h5 id="copyShortcutHint" class="hidden">' +
+                        '<button type="button" id="copyShortcutHintBtn"></button>' +
+                        '</h5>'
+                    );
+
+                    PrivateBin.Alert.init();
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+
+                    PrivateBin.CopyToClipboard.init();
+
+                    document.getElementById('copyShortcutHintBtn').click();
+
+                    const savedToClipboardText = await navigator.clipboard.readText();
+
+                    return text === savedToClipboardText;
+                }
+            ));
+        });
 
         /**
-         * Unfortunately in JSVerify impossible to check if copy with shortcut when user selected some text on the page
+         * Unfortunately, in JSVerify impossible to check if copy with shortcut when user selected some text on the page
          * (the copy document to clipboard should not work in this case) due to lacking window.getSelection() in jsdom.
          */
-        jsc.property('Copy with keyboard shortcut',
-            common.jscFormats(),
-            'nestring',
-            async function (format, text) {
-                var clean = jsdom();
-                common.enableClipboard();
-
-                $('body').html(
-                    '<div id="placeholder"></div>' +
-					'<div id="attachmentPreview" class="hidden"></div>' +
-                    '<h5 id="copyShortcutHint" class="hidden">' +
-						'<small id="copyShortcutHintText"></small>' +
-						'<button type="button" id="copyShortcutHintBtn"></button>' +
-					'</h5>' +
-                    '<div id="prettymessage" class="hidden">' +
-						'<pre id="prettyprint"></pre>' +
-					'</div>'
-                );
-
-                $.PrivateBin.PasteViewer.init();
-                $.PrivateBin.PasteViewer.setFormat(format);
-                $.PrivateBin.PasteViewer.setText(text);
-                $.PrivateBin.PasteViewer.run();
-
-                $.PrivateBin.CopyToClipboard.init();
+        it('Copy with keyboard shortcut', async function () {
+            await fc.assert(fc.asyncProperty(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                async function (format, text) {
+                    common.enableClipboard();
+
+                    document.body.innerHTML = (
+                        '<div id="status" class="hidden"></div>' +
+                        '<div id="errormessage" class="hidden"></div>' +
+                        '<div id="placeholder">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden">' +
+                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
+                        '<svg id="copySuccessIcon"></svg></button><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>' +
+                        '<h5 id="copyShortcutHint" class="hidden">' +
+                        '<button type="button" id="copyShortcutHintBtn"></button>' +
+                        '</h5>'
+                    );
+
+                    PrivateBin.Alert.init();
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+
+                    PrivateBin.CopyToClipboard.init();
+
+                    document.body.dispatchEvent(getClipboardEvent());
+
+                    const copiedTextWithoutSelectedText = await navigator.clipboard.readText();
+
+                    return copiedTextWithoutSelectedText === text;
+                }
+            ));
+        });
 
-                $('body').trigger('copy');
-
-                const copiedTextWithoutSelectedText = await navigator.clipboard.readText();
-
-                clean();
-
-                return copiedTextWithoutSelectedText === text;
+        /**
+         * ClipboardEvent is not supported in jsdom yet, so this creates a mock event to trigger the copy event listener.
+         *
+         * See https://github.com/jsdom/jsdom/issues/1568
+         *
+         * @returns {ClipboardEvent}
+         */
+        function getClipboardEvent() {
+            /** {@type ClipboardEvent} */
+            const clipboardEvent = new Event('copy', {
+                bubbles: true,
+                cancelable: true,
+                composed: true
+            });
+            clipboardEvent['clipboardData'] = {
+                getData: function () {
+                    return '';
+                }
             }
-        );
-    });
-
-
-    jsc.property('Copy link to clipboard',
-        'nestring',
-        async function (text) {
-            var clean = jsdom();
-            common.enableClipboard();
-
-            $('body').html('<button id="copyLink"></button>');
-
-            $.PrivateBin.CopyToClipboard.init();
-            $.PrivateBin.CopyToClipboard.setUrl(text);
-
-            $('#copyLink').trigger('click');
-
-            const copiedText = await navigator.clipboard.readText();
-
-            clean();
-
-            return text === copiedText;
+            return clipboardEvent;
         }
-    );
+    });
 
 
-    describe('Keyboard shortcut hint', function () {
-        jsc.property('Show hint', function () {
-                var clean = jsdom();
+    it('Copy link to clipboard', async function () {
+        await fc.assert(fc.asyncProperty(fc.string(),
+            async function (text) {
+                common.enableClipboard();
 
-                $('body').html('<h5 id="copyShortcutHint" class="hidden"></h5>');
+                document.body.innerHTML = (
+                    '<div id="status" class="hidden"></div>' +
+                    '<div id="errormessage" class="hidden"></div>' +
+                    '<button id="copyLink"></button>'
+                );
 
-                $.PrivateBin.CopyToClipboard.init();
-                $.PrivateBin.CopyToClipboard.showKeyboardShortcutHint();
+                PrivateBin.Alert.init();
+                PrivateBin.CopyToClipboard.init();
+                PrivateBin.CopyToClipboard.setUrl(text);
 
-                const hasHidden = $('#copyShortcutHint').hasClass('hidden');
+                document.getElementById('copyLink').click();
 
-                clean();
+                const copiedText = await navigator.clipboard.readText();
 
-                return !hasHidden;
-            }
+                return text === copiedText;
+            })
         );
+    });
 
-        jsc.property('Hide hint', function () {
-                var clean = jsdom();
-
-                $('body').html('<h5 id="copyShortcutHint"></h5>');
-
-                $.PrivateBin.CopyToClipboard.init();
-                $.PrivateBin.CopyToClipboard.hideKeyboardShortcutHint();
+    describe('Keyboard shortcut hint', function () {
+        it('shows hint', () => {
+            fc.assert(fc.property(fc.string(),
+                function () {
+                    document.body.innerHTML = '<h5 id="copyShortcutHint" class="hidden"></h5>';
+
+                    PrivateBin.CopyToClipboard.init();
+                    PrivateBin.CopyToClipboard.showKeyboardShortcutHint();
+
+                    return !document.getElementById('copyShortcutHint').classList.contains('hidden');
+                }
+            ));
+        });
+    });
 
-                const hasHidden = $('#copyShortcutHint').hasClass('hidden');
+    it('Hide hint', () => {
+        fc.assert(fc.property(
+            fc.string({minLength: 1}),
+            function () {
+                document.body.innerHTML = '<h5 id="copyShortcutHint"></h5>';
 
-                clean();
+                PrivateBin.CopyToClipboard.init();
+                PrivateBin.CopyToClipboard.hideKeyboardShortcutHint();
 
-                return hasHidden;
+                return document.getElementById('copyShortcutHint').classList.contains('hidden');
             }
-        );
+        ));
     });
-});
+});

+ 46 - 34
js/test/CryptTool.js

@@ -1,5 +1,7 @@
 'use strict';
 const common = require('../common');
+const fc = require('fast-check');
+const fs = require('fs');
 
 describe('CryptTool', function () {
     describe('cipher & decipher', function () {
@@ -9,26 +11,28 @@ describe('CryptTool', function () {
         });
 
         this.timeout(30000);
-        it('can en- and decrypt any message', function () {
-            jsc.assert(jsc.forall(
-                'string',
-                'string',
-                'string',
+        it('can en- and decrypt any message', async function () {
+            await fc.assert(fc.asyncProperty(
+                fc.string(),
+                fc.string(),
+                fc.string(),
                 async function (key, password, message) {
-                    const clean = jsdom();
+                    const clean = globalThis.cleanup();
                     // ensure zlib is getting loaded
-                    $.PrivateBin.Controller.initZ();
+                    PrivateBin.Controller.initZlib();
                     Object.defineProperty(window, 'crypto', {
                         value: new WebCrypto(),
-                        writeable: false
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
                     });
                     global.atob = common.atob;
                     global.btoa = common.btoa;
                     message = message.trim();
-                    const cipherMessage = await $.PrivateBin.CryptTool.cipher(
+                    const cipherMessage = await PrivateBin.CryptTool.cipher(
                             key, password, message, []
                         ),
-                        plaintext = await $.PrivateBin.CryptTool.decipher(
+                        plaintext = await PrivateBin.CryptTool.decipher(
                             key, password, cipherMessage
                         );
                     clean();
@@ -37,24 +41,26 @@ describe('CryptTool', function () {
                     return result;
                 }
             ),
-            {tests: 3});
+            {numRuns: 3});
         });
 
         it('does not truncate messages', async function () {
             const message = fs.readFileSync('test/compression-sample.txt', 'ascii').trim(),
-                clean = jsdom();
+                clean = globalThis.cleanup();
             Object.defineProperty(window, 'crypto', {
                 value: new WebCrypto(),
-                writeable: false
+                configurable: true,
+                enumerable: true,
+                writable: false
             });
             // ensure zlib is getting loaded
-            $.PrivateBin.Controller.initZ();
+            PrivateBin.Controller.initZlib();
             global.atob = common.atob;
             global.btoa = common.btoa;
-            const cipherMessage = await $.PrivateBin.CryptTool.cipher(
+            const cipherMessage = await PrivateBin.CryptTool.cipher(
                     'foo', 'bar', message, []
                 ),
-                plaintext = await $.PrivateBin.CryptTool.decipher(
+                plaintext = await PrivateBin.CryptTool.decipher(
                     'foo', 'bar', cipherMessage
                 );
             clean();
@@ -64,10 +70,10 @@ describe('CryptTool', function () {
             assert.strictEqual(message, plaintext);
         });
 
-        it('can en- and decrypt a particular message (#260)', function () {
-            jsc.assert(jsc.forall(
-                'string',
-                'string',
+        it('can en- and decrypt a particular message (#260)', async function () {
+            await fc.assert(fc.asyncProperty(
+                fc.string(),
+                fc.string(),
                 async function (key, password) {
                     const message = `
 1 subgoal
@@ -91,17 +97,21 @@ isWhile : interp (while expr sBody) (MemElem mem) =
 ======================== ( 1 / 1 )
 conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
 `;
-                    const clean = jsdom();
+                    const clean = globalThis.cleanup();
                     // ensure zlib is getting loaded
-                    $.PrivateBin.Controller.initZ();
+                    PrivateBin.Controller.initZlib();
                     Object.defineProperty(window, 'crypto', {
                         value: new WebCrypto(),
-                        writeable: false
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
                     });
-                    const cipherMessage = await $.PrivateBin.CryptTool.cipher(
+                    global.atob = common.atob;
+                    global.btoa = common.btoa;
+                    const cipherMessage = await PrivateBin.CryptTool.cipher(
                             key, password, message, []
                         ),
-                        plaintext = await $.PrivateBin.CryptTool.decipher(
+                        plaintext = await PrivateBin.CryptTool.decipher(
                                 key, password, cipherMessage
                         );
                     clean();
@@ -110,7 +120,7 @@ conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
                     return result;
                 }
             ),
-            {tests: 3});
+            {numRuns: 3});
         });
     });
 
@@ -119,23 +129,25 @@ conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
         let keys = [];
 
         // the parameter is used to ensure the test is run more then one time
-        it('returns random, non-empty keys', function () {
-            jsc.assert(jsc.forall(
-                'integer',
-                function(counter) {
-                    const clean = jsdom();
+        it('returns random, non-empty keys', () => {
+            fc.assert(fc.property(
+                fc.integer(),
+                function() {
+                    const clean = globalThis.cleanup();
                     Object.defineProperty(window, 'crypto', {
                         value: new WebCrypto(),
-                        writeable: false
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
                     });
-                    const key = $.PrivateBin.CryptTool.getSymmetricKey(),
+                    const key = PrivateBin.CryptTool.getSymmetricKey(),
                         result = (key !== '' && keys.indexOf(key) === -1);
                     keys.push(key);
                     clean();
                     return result;
                 }
             ),
-            {tests: 10});
+            {numRuns: 10});
         });
     });
 });

+ 118 - 97
js/test/DiscussionViewer.js

@@ -1,112 +1,133 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('DiscussionViewer', function () {
     describe('handleNotification, prepareNewDiscussion, addComment, finishDiscussion, getReplyMessage, getReplyNickname, getReplyCommentId & highlightComment', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'displays & hides comments as requested',
-            jsc.array(
-                jsc.record({
-                    idArray: jsc.nearray(common.jscAlnumString()),
-                    parentidArray: jsc.nearray(common.jscAlnumString()),
-                    data: jsc.string,
-                    meta: jsc.record({
-                        nickname: jsc.string,
-                        postdate: jsc.nat,
-                        vizhash: jsc.string
+        it('displays & hides comments as requested', () => {
+            fc.assert(fc.property(
+                fc.array(
+                    fc.record({
+                        idArray: fc.array(common.fcAlnumString(), {minLength: 1}),
+                        parentidArray: fc.array(common.fcAlnumString(), {minLength: 1}),
+                        data: fc.string(),
+                        meta: fc.record({
+                            nickname: fc.string(),
+                            postdate: fc.nat(),
+                            vizhash: fc.string()
+                        })
                     })
-                })
-            ),
-            'nat',
-            'bool',
-            'string',
-            'string',
-            jsc.elements(['loading', 'danger', 'other']),
-            'nestring',
-            function (comments, commentKey, fadeOut, nickname, message, alertType, alert) {
-                var clean = jsdom(),
-                    results = [];
-                $('body').html(
-                    '<div id="discussion"><h4>Discussion</h4>' +
-                    '<div id="commentcontainer"></div></div><div id="templates">' +
-                    '<article id="commenttemplate" class="comment">' +
-                    '<div class="commentmeta"><span class="nickname">name</span>' +
-                    '<span class="commentdate">0000-00-00</span></div>' +
-                    '<div class="commentdata">c</div>' +
-                    '<button class="btn btn-default btn-sm">Reply</button>' +
-                    '</article><p id="commenttailtemplate" class="comment">' +
-                    '<button class="btn btn-default btn-sm">Add comment</button>' +
-                    '</p><div id="replytemplate" class="reply hidden">' +
-                    '<input type="text" id="nickname" class="form-control" ' +
-                    'title="Optional nickname…" placeholder="Optional ' +
-                    'nickname…" /><textarea id="replymessage" ' +
-                    'class="replymessage form-control" cols="80" rows="7">' +
-                    '</textarea><br /><div id="replystatus" role="alert" ' +
-                    'class="statusmessage hidden alert"><span class="glyphicon" ' +
-                    'aria-hidden="true"></span> </div><button id="replybutton" ' +
-                    'class="btn btn-default btn-sm">Post comment</button></div></div>'
-                );
-                $.PrivateBin.Model.init();
-                $.PrivateBin.DiscussionViewer.init();
-                results.push(
-                    !$('#discussion').hasClass('hidden')
-                );
-                $.PrivateBin.DiscussionViewer.prepareNewDiscussion();
-                results.push(
-                    $('#discussion').hasClass('hidden')
-                );
-                comments.forEach(function (comment) {
-                    comment.id = comment.idArray.join('');
-                    comment.parentid = comment.parentidArray.join('');
-                    $.PrivateBin.DiscussionViewer.addComment($.PrivateBin.Helper.CommentFactory(comment), comment.data, comment.meta.nickname);
-                });
-                results.push(
-                    $('#discussion').hasClass('hidden')
-                );
-                $.PrivateBin.DiscussionViewer.finishDiscussion();
-                results.push(
-                    !$('#discussion').hasClass('hidden') &&
-                    comments.length + 1 >= $('#commentcontainer').children().length
-                );
-                if (comments.length > 0) {
-                    if (commentKey >= comments.length) {
-                        commentKey = commentKey % comments.length;
+                ),
+                fc.nat(),
+                fc.boolean(),
+                fc.string(),
+                fc.string(),
+                fc.constantFrom('loading', 'danger', 'other'),
+                fc.string({minLength: 1}),
+                function (comments, commentKey, fadeOut, nickname, message, alertType, alert) {
+                    var clean = globalThis.cleanup(),
+                        results = [];
+                    PrivateBin.Model.reset();
+                    PrivateBin.Helper.reset();
+                    document.body.innerHTML = (
+                        '<div id="status" class="hidden"></div>' +
+                        '<div id="errormessage" class="hidden"></div>' +
+                        `<div id="discussion">
+	<h4>Discussion</h4>
+	<div id="commentcontainer"></div>
+</div>
+<div id="templates">
+	<article id="commenttemplate" class="comment">
+		<div class="commentmeta">
+			<span class="nickname">name</span>
+			<span class="commentdate">0000-00-00</span>
+		</div>
+		<div class="commentdata">c</div>
+		<button class="btn btn-default btn-sm">Reply</button>
+	</article>
+	<p id="commenttailtemplate" class="comment">
+		<button class="btn btn-default btn-sm">Add comment</button>
+	</p>
+	<div id="replytemplate" class="reply hidden">
+		<input type="text" id="nickname" class="form-control" title="Optional nickname…"
+			placeholder="Optional nickname…" />
+		<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>
+		<br />
+		<div id="replystatus" role="alert" class="statusmessage hidden alert">
+			<span class="glyphicon" aria-hidden="true"></span>
+		</div>
+		<button id="replybutton" class="btn btn-default btn-sm">Post comment</button>
+	</div>
+</div>
+`
+                    );
+                    PrivateBin.Model.init();
+                    PrivateBin.Alert.init();
+                    PrivateBin.DiscussionViewer.init();
+                    results.push(
+                        !document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    PrivateBin.DiscussionViewer.prepareNewDiscussion();
+                    results.push(
+                        document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    comments.forEach(function (comment) {
+                        comment.id = comment.idArray.join('');
+                        comment.parentid = comment.parentidArray.join('');
+                        comment.getCreated = function() { return comment.meta.postdate; };
+                        comment.getIcon = function() { return comment.meta.vizhash; };
+                        PrivateBin.DiscussionViewer.addComment(comment, comment.data, comment.meta.nickname);
+                    });
+                    results.push(
+                        document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    PrivateBin.DiscussionViewer.finishDiscussion();
+                    results.push(
+                        !document.getElementById('discussion').classList.contains('hidden') &&
+                        comments.length + 1 >= document.getElementById('commentcontainer').children.length
+                    );
+                    if (comments.length > 0) {
+                        if (commentKey >= comments.length) {
+                            commentKey = commentKey % comments.length;
+                        }
+                        PrivateBin.DiscussionViewer.highlightComment(comments[commentKey].id, fadeOut);
+                        results.push(
+                            document.getElementById('comment_' + comments[commentKey].id).classList.contains('highlight')
+                        );
                     }
-                    $.PrivateBin.DiscussionViewer.highlightComment(comments[commentKey].id, fadeOut);
+                    // clicking "Add comment" button should open the reply form
+                    document.getElementById('commentcontainer').querySelector('button').click();
                     results.push(
-                        $('#comment_' + comments[commentKey].id).hasClass('highlight')
+                        document.getElementById('reply') !== null &&
+                        !document.getElementById('reply').classList.contains('hidden')
                     );
-                }
-                $('#commentcontainer').find('button')[0].click();
-                results.push(
-                    !$('#reply').hasClass('hidden')
-                );
-                $('#reply #nickname').val(nickname);
-                $('#reply #replymessage').val(message);
-                $.PrivateBin.DiscussionViewer.getReplyCommentId();
-                results.push(
-                    $.PrivateBin.DiscussionViewer.getReplyNickname() === $('#reply #nickname').val() &&
-                    $.PrivateBin.DiscussionViewer.getReplyMessage() === $('#reply #replymessage').val()
-                );
-                var notificationResult = $.PrivateBin.DiscussionViewer.handleNotification(alertType === 'other' ? alert : alertType);
-                if (alertType === 'loading') {
-                    results.push(notificationResult === false);
-                } else {
+                    document.querySelector('#reply #nickname').value = nickname;
+                    document.querySelector('#reply #replymessage').value = message;
+                    PrivateBin.DiscussionViewer.getReplyCommentId();
                     results.push(
-                        alertType === 'danger' ? (
-                            notificationResult.hasClass('alert-danger') &&
-                            !notificationResult.hasClass('alert-info')
-                        ) : (
-                            !notificationResult.hasClass('alert-danger') &&
-                            notificationResult.hasClass('alert-info')
-                        )
+                        PrivateBin.DiscussionViewer.getReplyNickname() === document.querySelector('#reply #nickname').value &&
+                        PrivateBin.DiscussionViewer.getReplyMessage() === document.querySelector('#reply #replymessage').value
                     );
+                    var notificationResult = PrivateBin.DiscussionViewer.handleNotification(alertType === 'other' ? alert : alertType);
+                    if (alertType === 'loading') {
+                        results.push(notificationResult === false);
+                    } else {
+                        results.push(
+                            alertType === 'danger' ? (
+                                notificationResult.classList.contains('alert-danger') &&
+                                !notificationResult.classList.contains('alert-info')
+                            ) : (
+                                !notificationResult.classList.contains('alert-danger') &&
+                                notificationResult.classList.contains('alert-info')
+                            )
+                        );
+                    }
+                    clean();
+                    return results.every(element => element);
                 }
-                clean();
-                return results.every(element => element);
-            }
-        );
+            ));
+        });
     });
 });

+ 70 - 61
js/test/Editor.js

@@ -1,70 +1,79 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
 
 describe('Editor', function () {
     describe('show, hide, getText, setText & isPreview', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'returns text fed into the textarea, handles editor tabs',
-            'string',
-            function (text) {
-                var clean = jsdom(),
-                    results = [];
-                $('body').html(
-                    '<ul id="editorTabs" class="nav nav-tabs hidden"><li ' +
-                    'role="presentation" class="active"><a id="messageedit" ' +
-                    'href="#">Editor</a></li><li role="presentation"><a ' +
-                    'id="messagepreview" href="#">Preview</a></li></ul><div ' +
-                    'id="placeholder" class="hidden">+++ no document text +++</div>' +
-                    '<div id="prettymessage" class="hidden"><pre id="prettyprint" ' +
-                    'class="prettyprint linenums:1"></pre></div><div ' +
-                    'id="plaintext" class="hidden"></div><p><textarea ' +
-                    'id="message" name="message" cols="80" rows="25" ' +
-                    'class="form-control hidden"></textarea></p>'
-                );
-                $.PrivateBin.Editor.init();
-                results.push(
-                    $('#editorTabs').hasClass('hidden') &&
-                    $('#message').hasClass('hidden')
-                );
-                $.PrivateBin.Editor.show();
-                results.push(
-                    !$('#editorTabs').hasClass('hidden') &&
-                    !$('#message').hasClass('hidden')
-                );
-                $.PrivateBin.Editor.hide();
-                results.push(
-                    $('#editorTabs').hasClass('hidden') &&
-                    $('#message').hasClass('hidden')
-                );
-                $.PrivateBin.Editor.show();
-                $.PrivateBin.Editor.focusInput();
-                results.push(
-                    $.PrivateBin.Editor.getText().length === 0
-                );
-                $.PrivateBin.Editor.setText(text);
-                results.push(
-                    $.PrivateBin.Editor.getText() === $('#message').val()
-                );
-                $.PrivateBin.Editor.setText();
-                results.push(
-                    !$.PrivateBin.Editor.isPreview() &&
-                    !$('#message').hasClass('hidden')
-                );
-                $('#messagepreview').trigger('click');
-                results.push(
-                    $.PrivateBin.Editor.isPreview() &&
-                    $('#message').hasClass('hidden')
-                );
-                $('#messageedit').trigger('click');
-                results.push(
-                    !$.PrivateBin.Editor.isPreview() &&
-                    !$('#message').hasClass('hidden')
-                );
-                clean();
-                return results.every(element => element);
-            }
-        );
+        it('returns text fed into the textarea, handles editor tabs', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (text) {
+                    var clean = globalThis.cleanup(),
+                        results = [];
+                    document.body.innerHTML = (
+                        '<ul id="editorTabs" class="nav nav-tabs hidden">' +
+                            '<li role="presentation" class="active">' +
+                                '<a id="messageedit" href="#">Editor</a>' +
+                            '</li>' +
+                            '<li role="presentation">' +
+                                '<a id="messagepreview" href="#">Preview</a>' +
+                            '</li>' +
+                        '</ul>' +
+                        '<div id="placeholder" class="hidden">+++ no document text +++</div>' +
+                        '<div id="prettymessage" class="hidden">' +
+                            '<pre id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div>' +
+                        '<div id="plaintext" class="hidden"></div>' +
+                        '<p>' +
+                            '<textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea>' +
+                        '</p>' +
+                        '<input id="messagetab" type="checkbox" checked="checked" />'
+                    );
+                    PrivateBin.Editor.init();
+                    results.push(
+                        document.getElementById('editorTabs').classList.contains('hidden') &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.show();
+                    results.push(
+                        !document.getElementById('editorTabs').classList.contains('hidden') &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.hide();
+                    results.push(
+                        document.getElementById('editorTabs').classList.contains('hidden') &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.show();
+                    PrivateBin.Editor.focusInput();
+                    results.push(
+                        PrivateBin.Editor.getText().length === 0
+                    );
+                    PrivateBin.Editor.setText(text);
+                    results.push(
+                        PrivateBin.Editor.getText() === document.getElementById('message').value
+                    );
+                    PrivateBin.Editor.setText();
+                    results.push(
+                        !PrivateBin.Editor.isPreview() &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    document.getElementById('messagepreview').click();
+                    results.push(
+                        PrivateBin.Editor.isPreview() &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    document.getElementById('messageedit').click();
+                    results.push(
+                        !PrivateBin.Editor.isPreview() &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    clean();
+                    return results.every(element => element);
+                }
+            ));
+        });
     });
 });

+ 316 - 268
js/test/Helper.js

@@ -1,45 +1,68 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Helper', function () {
     describe('secondsToHuman', function () {
-        jsc.property('returns an array with a number and a word', 'integer', function (number) {
-            var result = $.PrivateBin.Helper.secondsToHuman(number);
-            return Array.isArray(result) &&
-                result.length === 2 &&
-                result[0] === parseInt(result[0], 10) &&
-                typeof result[1] === 'string';
+        it('returns an array with a number and a word', () => {
+            fc.assert(fc.property(fc.integer(), function (number) {
+                var result = PrivateBin.Helper.secondsToHuman(number);
+                return Array.isArray(result) &&
+                    result.length === 2 &&
+                    result[0] === parseInt(result[0], 10) &&
+                    typeof result[1] === 'string';
+            }));
         });
-        jsc.property('returns seconds on the first array position', 'integer 59', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[0] === number;
+        it('returns seconds on the first array position', () => {
+            fc.assert(fc.property(fc.integer({max: 59}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === number;
+            }));
         });
-        jsc.property('returns seconds on the second array position', 'integer 59', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'second';
+        it('returns seconds on the second array position', () => {
+            fc.assert(fc.property(fc.integer({max: 59}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'second';
+            }));
         });
-        jsc.property('returns minutes on the first array position', 'integer 60 3599', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60);
+        it('returns minutes on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 60, max: 3599}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60);
+            }));
         });
-        jsc.property('returns minutes on the second array position', 'integer 60 3599', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'minute';
+        it('returns minutes on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 60, max: 3599}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'minute';
+            }));
         });
-        jsc.property('returns hours on the first array position', 'integer 3600 86399', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60));
+        it('returns hours on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 3600, max: 86399}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60));
+            }));
         });
-        jsc.property('returns hours on the second array position', 'integer 3600 86399', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'hour';
+        it('returns hours on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 3600, max: 86399}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'hour';
+            }));
         });
-        jsc.property('returns days on the first array position', 'integer 86400 5184000', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24));
+        it('returns days on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 86400, max: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24));
+            }));
         });
-        jsc.property('returns days on the second array position', 'integer 86400 5184000', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'day';
+        it('returns days on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 86400, max: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'day';
+            }));
         });
         // max safe integer as per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5
-        jsc.property('returns months on the first array position', 'integer 5184000 9007199254740991', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30));
+        it('returns months on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 5184001}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30));
+            }));
         });
-        jsc.property('returns months on the second array position', 'integer 5184000 9007199254740991', function (number) {
-            return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'month';
+        it('returns months on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 5184001}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'month';
+            }));
         });
     });
 
@@ -47,307 +70,332 @@ describe('Helper', function () {
     // TODO: This needs to be tested using a browser.
     describe('selectText', function () {
         this.timeout(30000);
-        jsc.property(
-            'selection contains content of given ID',
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            'nearray string',
-            function (ids, contents) {
-                var html = '',
-                    result = true,
-                    clean = jsdom(html);
-                ids.forEach(function(item, i) {
-                    html += '<div id="' + item.join('') + '">' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
-                });
-                // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet.
-                // Once there is one, uncomment the block below to actually check the result.
-                /*
-                ids.forEach(function(item, i) {
-                    $.PrivateBin.Helper.selectText(item.join(''));
-                    result *= (contents[i] || contents[0]) === window.getSelection().toString();
-                });
-                */
-                clean();
-                return Boolean(result);
-            }
-        );
+        it('selection contains content of given ID', () => {
+            fc.assert(fc.property(
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                fc.array(fc.string(), {minLength: 1}),
+                function (ids, contents) {
+                    var html = '',
+                        result = true,
+                        clean = globalThis.cleanup(html);
+                    ids.forEach(function(item, i) {
+                        html += '<div id="' + item.join('') + '">' + PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
+                    });
+                    // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet.
+                    // Once there is one, uncomment the block below to actually check the result.
+                    /*
+                    ids.forEach(function(item, i) {
+                        PrivateBin.Helper.selectText(item.join(''));
+                        result *= (contents[i] || contents[0]) === window.getSelection().toString();
+                    });
+                    */
+                    clean();
+                    return Boolean(result);
+                }
+            ));
+        });
     });
 
     describe('urls2links', function () {
+        function getTextAsRenderedHtml(stringContent) {
+            const tempDiv = document.createElement('div');
+            tempDiv.textContent = stringContent;
+            return tempDiv.innerHTML;
+        }
+
         this.timeout(30000);
         before(function () {
-            cleanup = jsdom();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'ignores non-URL content',
-            'string',
-            function (content) {
-                // eslint-disable-next-line no-control-regex
-                content = content.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                let clean = jsdom();
-                $('body').html('<div id="foo"></div>');
-                let e = $('#foo');
-                e.text(content);
-                $.PrivateBin.Helper.urls2links(e);
-                let result = e.text();
-                clean();
-                return content === result;
-            }
-        );
-        jsc.property(
-            'replaces URLs with anchors',
-            'string',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            'string',
-            function (prefix, url, fragment, postfix) {
-                // eslint-disable-next-line no-control-regex
-                prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                // eslint-disable-next-line no-control-regex
-                postfix  = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
-                url.fragment = fragment.join('');
-                let urlString = common.urlToString(url),
-                    clean = jsdom();
-                $('body').html('<div id="foo"></div>');
-                let e = $('#foo');
+        it('ignores non-URL content', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (content) {
+                    // eslint-disable-next-line no-control-regex
+                    content = content.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
+                    e.textContent = content;
+                    PrivateBin.Helper.urls2links(e);
+                    let result = e.textContent;
+                    clean();
+                    return content === result;
+                }
+            ));
+        });
+        it('replaces URLs with anchors', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                fc.string(),
+                function (prefix, url, fragment, postfix) {
+                    // eslint-disable-next-line no-control-regex
+                    prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    // eslint-disable-next-line no-control-regex
+                    postfix  = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
+                    url.fragment = fragment.join('');
+                    let urlString = common.urlToString(url),
+                        clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
+
+                    // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 or &#x
+                    if (
+                        url.query[-1] === '&' &&
+                        (parseInt(url.fragment.charAt(0), 10) >= 0 || url.fragment.charAt(0) === 'x')
+                    ) {
+                        url.query.pop();
+                        urlString = common.urlToString(url);
+                        postfix = '';
+                    }
+                    e.textContent = prefix + urlString + postfix;
+                    PrivateBin.Helper.urls2links(e);
+
+                    let result = e.innerHTML;
+                    clean();
 
-                // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 or &#x
-                if (
-                    url.query[-1] === '&' &&
-                    (parseInt(url.fragment.charAt(0), 10) >= 0 || url.fragment.charAt(0) === 'x')
-                ) {
-                    url.query.pop();
-                    urlString = common.urlToString(url);
-                    postfix = '';
+                    urlString = getTextAsRenderedHtml(urlString);
+                    const expected = getTextAsRenderedHtml(prefix) + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + getTextAsRenderedHtml(postfix);
+                    const expectedElement = document.createElement('div');
+                    expectedElement.innerHTML = expected;
+                    return expectedElement.innerHTML === result;
+                }
+            ));
+        });
+        it('replaces magnet links with anchors', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(common.fcQueryString()),
+                fc.string(),
+                function (prefix, query, postfix) {
+                    // eslint-disable-next-line no-control-regex
+                    prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    // eslint-disable-next-line no-control-regex
+                    postfix = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
+                    let url  = 'magnet:?' + query.join('').replace(/^&+|&+$/gm, ''),
+                        clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
+                    e.textContent = prefix + url + postfix;
+                    PrivateBin.Helper.urls2links(e);
+                    let result = e.innerHTML;
+                    clean();
+                    url = getTextAsRenderedHtml(url);
+                    const expected = getTextAsRenderedHtml(prefix) + '<a href="' + url + '" target="_blank" rel="nofollow noopener noreferrer">' + url + '</a>' + getTextAsRenderedHtml(postfix);
+                    const expectedElement = document.createElement('div');
+                    expectedElement.innerHTML = expected;
+                    return expectedElement.innerHTML === result;
                 }
-                e.text(prefix + urlString + postfix);
-                $.PrivateBin.Helper.urls2links(e);
-                let result = e.html();
-                clean();
-                urlString = $('<div />').text(urlString).html();
-                const expected = $('<div />').text(prefix).html() + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + $('<div />').text(postfix).html();
-                return expected === result;
-            }
-        );
-        jsc.property(
-            'replaces magnet links with anchors',
-            'string',
-            jsc.array(common.jscQueryString()),
-            'string',
-            function (prefix, query, postfix) {
-                // eslint-disable-next-line no-control-regex
-                prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                // eslint-disable-next-line no-control-regex
-                postfix = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
-                let url  = 'magnet:?' + query.join('').replace(/^&+|&+$/gm, ''),
-                    clean = jsdom();
-                $('body').html('<div id="foo"></div>');
-                let e = $('#foo');
-                e.text(prefix + url + postfix);
-                $.PrivateBin.Helper.urls2links(e);
-                let result = e.html();
-                clean();
-                url = $('<div />').text(url).html();
-                return $('<div />').text(prefix).html() + '<a href="' + url + '" target="_blank" rel="nofollow noopener noreferrer">' + url + '</a>' + $('<div />').text(postfix).html() === result;
-            }
-        );
+            ));
+        });
     });
 
     describe('sprintf', function () {
-        jsc.property(
-            'replaces %s in strings with first given parameter',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + params[0] + postfix;
-                params.unshift(prefix + '%s' + postfix);
-                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d in strings with first given parameter',
-            'string',
-            '(small nearray) nat',
-            'string',
-            function (prefix, params, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + params[0] + postfix;
-                params.unshift(prefix + '%d' + postfix);
-                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d in strings with 0 if first parameter is not a number',
-            'string',
-            '(small nearray) falsy',
-            'string',
-            function (prefix, params, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + '0' + postfix;
-                params.unshift(prefix + '%d' + postfix);
-                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d and %s in strings in order',
-            'string',
-            'nat',
-            'string',
-            'string',
-            'string',
-            function (prefix, uint, middle, string, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '');
-                middle  =  middle.replace(/%(s|d)/g, '');
-                postfix = postfix.replace(/%(s|d)/g, '');
-                var params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
-                    result = prefix + uint + middle + string + postfix;
-                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d and %s in strings in reverse order',
-            'string',
-            'nat',
-            'string',
-            'string',
-            'string',
-            function (prefix, uint, middle, string, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '');
-                middle  =  middle.replace(/%(s|d)/g, '');
-                postfix = postfix.replace(/%(s|d)/g, '');
-                var params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
-                    result = prefix + string + middle + uint + postfix;
-                return result === $.PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
+        it('replaces %s in strings with first given parameter', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + params[0] + postfix;
+                    params.unshift(prefix + '%s' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d in strings with first given parameter', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.nat(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + params[0] + postfix;
+                    params.unshift(prefix + '%d' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d in strings with 0 if first parameter is not a number', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.falsy(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + '0' + postfix;
+                    params.unshift(prefix + '%d' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d and %s in strings in order', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.nat(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                function (prefix, uint, middle, string, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '');
+                    middle  =  middle.replace(/%(s|d)/g, '');
+                    postfix = postfix.replace(/%(s|d)/g, '');
+                    var params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
+                        result = prefix + uint + middle + string + postfix;
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d and %s in strings in reverse order', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.nat(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                function (prefix, uint, middle, string, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '');
+                    middle  =  middle.replace(/%(s|d)/g, '');
+                    postfix = postfix.replace(/%(s|d)/g, '');
+                    var params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
+                        result = prefix + string + middle + uint + postfix;
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
     });
 
     describe('getCookie', function () {
         this.timeout(30000);
         before(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
 /* TODO test fails since jsDOM version 17 - document.cookie remains empty
-        jsc.property(
-            'returns the requested cookie',
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            function (labels, values) {
-                let selectedKey = '', selectedValue = '';
-                const clean = jsdom();
-                labels.forEach(function(item, i) {
-                    const key = item.join(''),
-                        value = (values[i] || values[0]).join('');
-                    document.cookie = key + '=' + value;
-                    if (Math.random() < 1 / i || selectedKey === key)
-                    {
-                        selectedKey = key;
-                        selectedValue = value;
-                    }
-                });
-                const result = $.PrivateBin.Helper.getCookie(selectedKey);
-                $.PrivateBin.Helper.reset();
-                clean();
-                return result === selectedValue;
-            }
-        ); */
+        it('returns the requested cookie', () => {
+            fc.assert(fc.property(
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                function (labels, values) {
+                    let selectedKey = '', selectedValue = '';
+                    const clean = globalThis.cleanup();
+                    labels.forEach(function(item, i) {
+                        const key = item.join(''),
+                            value = (values[i] || values[0]).join('');
+                        document.cookie = key + '=' + value;
+                        if (Math.random() < 1 / i || selectedKey === key)
+                        {
+                            selectedKey = key;
+                            selectedValue = value;
+                        }
+                    });
+                    const result = PrivateBin.Helper.getCookie(selectedKey);
+                    PrivateBin.Helper.reset();
+                    clean();
+                    return result === selectedValue;
+                }
+            ));
+        }); */
     });
 
     describe('baseUri', function () {
         this.timeout(30000);
-        jsc.property(
-            'returns the URL without query & fragment',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            function (schema, url) {
-                url.schema = schema;
-                const fullUrl = common.urlToString(url);
-                delete(url.query);
-                delete(url.fragment);
-                $.PrivateBin.Helper.reset();
-                const expected = common.urlToString(url),
-                    clean = jsdom('', {url: fullUrl}),
-                    result = $.PrivateBin.Helper.baseUri();
-                clean();
-                return expected === result;
-            }
-        );
+        it('returns the URL without query & fragment', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                function (schema, url) {
+                    url.schema = schema;
+                    const fullUrl = common.urlToString(url);
+                    delete(url.query);
+                    delete(url.fragment);
+                    PrivateBin.Helper.reset();
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: fullUrl}),
+                        result = PrivateBin.Helper.baseUri();
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('htmlEntities', function () {
         before(function () {
-            cleanup = jsdom();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'removes all HTML entities from any given string',
-            'string',
-            function (string) {
-                var result = $.PrivateBin.Helper.htmlEntities(string);
-                return !(/[<>]/.test(result)) && !(string.indexOf('&') > -1 && !(/&amp;/.test(result)));
-            }
-        );
+        it('removes all HTML entities from any given string', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (string) {
+                    var result = PrivateBin.Helper.htmlEntities(string);
+                    return !(/[<>]/.test(result)) && !(string.indexOf('&') > -1 && !(/&amp;/.test(result)));
+                }
+            ));
+        });
     });
 
     describe('formatBytes', function () {
-        jsc.property('returns 0 B for 0 bytes', function () {
-            return $.PrivateBin.Helper.formatBytes(0) === '0 B';
+        it('returns 0 B for 0 bytes', function () {
+            return PrivateBin.Helper.formatBytes(0) === '0 B';
         });
 
-        jsc.property('formats bytes < 1000 as B', function () {
-            return $.PrivateBin.Helper.formatBytes(500) === '500 B';
+        it('formats bytes < 1000 as B', function () {
+            return PrivateBin.Helper.formatBytes(500) === '500 B';
         });
 
-        jsc.property('formats kilobytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(1500) === '1.5 kB';
+        it('formats kilobytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(1500) === '1.5 kB';
         });
 
-        jsc.property('formats megabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(2 * 1000 * 1000) === '2 MB';
+        it('formats megabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(2 * 1000 * 1000) === '2 MB';
         });
 
-        jsc.property('formats gigabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(3.45 * 1000 * 1000 * 1000) === '3.45 GB';
+        it('formats gigabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(3.45 * 1000 * 1000 * 1000) === '3.45 GB';
         });
 
-        jsc.property('formats terabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(1.75 * 1000 ** 4) === '1.75 TB';
+        it('formats terabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(1.75 * 1000 ** 4) === '1.75 TB';
         });
 
-        jsc.property('formats petabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(1.5 * 1000 ** 5) === '1.5 PB';
+        it('formats petabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(1.5 * 1000 ** 5) === '1.5 PB';
         });
 
-        jsc.property('formats exabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(1.2345 * 1000 ** 6).startsWith('1.23 EB');
+        it('formats exabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(1.2345 * 1000 ** 6).startsWith('1.23 EB');
         });
 
-        jsc.property('formats yottabytes correctly', function () {
-            return $.PrivateBin.Helper.formatBytes(1.23 * 1000 ** 8).startsWith('1.23 YB');
+        it('formats yottabytes correctly', function () {
+            return PrivateBin.Helper.formatBytes(1.23 * 1000 ** 8).startsWith('1.23 YB');
         });
 
-        jsc.property('rounds to two decimal places', function () {
-            return $.PrivateBin.Helper.formatBytes(1234567) === '1.23 MB';
+        it('rounds to two decimal places', function () {
+            return PrivateBin.Helper.formatBytes(1234567) === '1.23 MB';
         });
     });
 
 
     describe('isBootstrap5', function () {
-        jsc.property('Bootstrap 5 has been detected', function () {
+        it('Bootstrap 5 has been detected', function () {
             global.bootstrap = {};
-            return $.PrivateBin.Helper.isBootstrap5() === true;
+            return PrivateBin.Helper.isBootstrap5() === true;
         });
 
-        jsc.property('Bootstrap 5 has not been detected', function () {
+        it('Bootstrap 5 has not been detected', function () {
             delete global.bootstrap;
-            return $.PrivateBin.Helper.isBootstrap5() === false;
+            return PrivateBin.Helper.isBootstrap5() === false;
         });
     });
 });

+ 210 - 199
js/test/I18n.js

@@ -1,177 +1,188 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('I18n', function () {
     describe('translate', function () {
         this.timeout(30000);
         before(function () {
-            $.PrivateBin.I18n.reset();
+            PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'returns message ID unchanged if no translation found',
-            'string',
-            function (messageId) {
-                messageId   = messageId.replace(/%(s|d)/g, '%%');
-                var plurals = [messageId, messageId + 's'],
-                    fake    = [messageId],
-                    result  = $.PrivateBin.I18n.translate(messageId);
-                $.PrivateBin.I18n.reset();
-
-                var alias = $.PrivateBin.I18n._(messageId);
-                $.PrivateBin.I18n.reset();
-
-                var pluralResult = $.PrivateBin.I18n.translate(plurals);
-                $.PrivateBin.I18n.reset();
-
-                var pluralAlias = $.PrivateBin.I18n._(plurals);
-                $.PrivateBin.I18n.reset();
-
-                var fakeResult = $.PrivateBin.I18n.translate(fake);
-                $.PrivateBin.I18n.reset();
-
-                var fakeAlias = $.PrivateBin.I18n._(fake);
-                $.PrivateBin.I18n.reset();
-
-                if (messageId.indexOf('<a') === -1) {
-                    messageId = $.PrivateBin.Helper.htmlEntities(messageId);
-                } else {
-                    messageId = DOMPurify.sanitize(
-                        messageId, {
-                            ALLOWED_TAGS: ['a', 'i', 'span'],
+        it('returns message ID unchanged if no translation found', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (messageId) {
+                    messageId   = messageId.replace(/%(s|d)/g, '%%');
+                    var plurals = [messageId, messageId + 's'],
+                        fake    = [messageId],
+                        result  = PrivateBin.I18n.translate(messageId);
+                    PrivateBin.I18n.reset();
+
+                    var alias = PrivateBin.I18n._(messageId);
+                    PrivateBin.I18n.reset();
+
+                    var pluralResult = PrivateBin.I18n.translate(plurals);
+                    PrivateBin.I18n.reset();
+
+                    var pluralAlias = PrivateBin.I18n._(plurals);
+                    PrivateBin.I18n.reset();
+
+                    var fakeResult = PrivateBin.I18n.translate(fake);
+                    PrivateBin.I18n.reset();
+
+                    var fakeAlias = PrivateBin.I18n._(fake);
+                    PrivateBin.I18n.reset();
+
+                    if (messageId.indexOf('<a') === -1) {
+                        messageId = PrivateBin.Helper.htmlEntities(messageId);
+                    } else {
+                        messageId = DOMPurify.sanitize(
+                            messageId, {
+                                ALLOWED_TAGS: ['a', 'i', 'span'],
+                                ALLOWED_ATTR: ['href', 'id']
+                            }
+                        );
+                    }
+                    return messageId === result && messageId === alias &&
+                        messageId === pluralResult && messageId === pluralAlias &&
+                        messageId === fakeResult && messageId === fakeAlias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter, encoding all, when no link is in the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    const translation = PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
+                    params.unshift(prefix + '%s' + postfix);
+                    const result = PrivateBin.I18n.translate.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    const alias = PrivateBin.I18n._.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                    const translation = DOMPurify.sanitize(
+                        prefix + '<a href="' + params[0] + '"></a>' + postfix, {
+                            ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
+                            ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
                             ALLOWED_ATTR: ['href', 'id']
                         }
                     );
+                    params.unshift(prefix + '<a href="%s"></a>' + postfix);
+                    const result = PrivateBin.I18n.translate.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    const alias = PrivateBin.I18n._.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    return translation === result && translation === alias;
                 }
-                return messageId === result && messageId === alias &&
-                    messageId === pluralResult && messageId === pluralAlias &&
-                    messageId === fakeResult && messageId === fakeAlias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter, encoding all, when no link is in the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                const translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
-                params.unshift(prefix + '%s' + postfix);
-                const result = $.PrivateBin.I18n.translate.apply(this, params);
-                $.PrivateBin.I18n.reset();
-                const alias = $.PrivateBin.I18n._.apply(this, params);
-                $.PrivateBin.I18n.reset();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                const translation = DOMPurify.sanitize(
-                    prefix + '<a href="' + params[0] + '"></a>' + postfix, {
-                        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-                        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
-                        ALLOWED_ATTR: ['href', 'id']
-                    }
-                );
-                params.unshift(prefix + '<a href="%s"></a>' + postfix);
-                const result = $.PrivateBin.I18n.translate.apply(this, params);
-                $.PrivateBin.I18n.reset();
-                const alias = $.PrivateBin.I18n._.apply(this, params);
-                $.PrivateBin.I18n.reset();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                const translation = $('<textarea>').text((prefix + params[0] + postfix)).text();
-                let args = Array.prototype.slice.call(params);
-                args.unshift(prefix + '%s' + postfix);
-                let clean = jsdom();
-                $('body').html('<div id="i18n"></div>');
-                args.unshift($('#i18n'));
-                $.PrivateBin.I18n.translate.apply(this, args);
-                const result = $('#i18n').text();
-                $.PrivateBin.I18n.reset();
-                clean();
-                clean = jsdom();
-                $('body').html('<div id="i18n"></div>');
-                args[0] = $('#i18n');
-                $.PrivateBin.I18n._.apply(this, args);
-                const alias = $('#i18n').text();
-                $.PrivateBin.I18n.reset();
-                clean();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').trim();
-                params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').trim();
-                const translation = DOMPurify.sanitize(
-                    prefix + '<a href="' + params[0] + '"></a>' + postfix, {
-                        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-                        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
-                        ALLOWED_ATTR: ['href', 'id']
-                    }
-                );
-                let args = Array.prototype.slice.call(params);
-                args.unshift(prefix + '<a href="%s"></a>' + postfix);
-                let clean = jsdom();
-                $('body').html('<div id="i18n"></div>');
-                args.unshift($('#i18n'));
-                $.PrivateBin.I18n.translate.apply(this, args);
-                const result = $('#i18n').html();
-                $.PrivateBin.I18n.reset();
-                clean();
-                clean = jsdom();
-                $('body').html('<div id="i18n"></div>');
-                args[0] = $('#i18n');
-                $.PrivateBin.I18n._.apply(this, args);
-                const alias = $('#i18n').html();
-                $.PrivateBin.I18n.reset();
-                clean();
-                return translation === result && translation === alias;
-            }
-        );
+            ));
+        });
+        it('replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    const tempDiv = document.createElement('textarea');
+                    tempDiv.textContent = (prefix + params[0] + postfix);
+                    const translation = tempDiv.textContent;
+                    let args = Array.prototype.slice.call(params);
+                    args.unshift(prefix + '%s' + postfix);
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    const i18nElement = document.getElementById('i18n');
+                    args.unshift(i18nElement);
+                    PrivateBin.I18n.translate.apply(this, args);
+                    const result = i18nElement.textContent;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    args[0] = document.getElementById('i18n');
+                    PrivateBin.I18n._.apply(this, args);
+                    const alias = document.getElementById('i18n').textContent;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').trim();
+                    params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').trim();
+                    const translation = DOMPurify.sanitize(
+                        prefix + '<a href="' + params[0] + '"></a>' + postfix, {
+                            ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
+                            ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
+                            ALLOWED_ATTR: ['href', 'id']
+                        }
+                    );
+                    let args = Array.prototype.slice.call(params);
+                    args.unshift(prefix + '<a href="%s"></a>' + postfix);
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    const i18nElement2 = document.getElementById('i18n');
+                    args.unshift(i18nElement2);
+                    PrivateBin.I18n.translate.apply(this, args);
+                    const result = i18nElement2.innerHTML;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    args[0] = document.getElementById('i18n');
+                    PrivateBin.I18n._.apply(this, args);
+                    const alias = document.getElementById('i18n').innerHTML;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
     });
 
     describe('getPluralForm', function () {
         before(function () {
-            $.PrivateBin.I18n.reset();
+            PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'returns valid key for plural form',
-            common.jscSupportedLanguages(),
-            'integer',
-            function(language, n) {
-                $.PrivateBin.I18n.reset(language);
-                var result = $.PrivateBin.I18n.getPluralForm(n);
-                // arabic seems to have the highest plural count with 6 forms
-                return result >= 0 && result <= 5;
-            }
-        );
+        it('returns valid key for plural form', () => {
+            fc.assert(fc.property(
+                common.fcSupportedLanguages(),
+                fc.integer(),
+                function(language, n) {
+                    PrivateBin.I18n.reset(language);
+                    var result = PrivateBin.I18n.getPluralForm(n);
+                    // arabic seems to have the highest plural count with 6 forms
+                    return result >= 0 && result <= 5;
+                }
+            ));
+        });
     });
 
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it
@@ -179,53 +190,53 @@ describe('I18n', function () {
     describe('loadTranslations', function () {
         this.timeout(30000);
         before(function () {
-            $.PrivateBin.I18n.reset();
+            PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'downloads and handles any supported language',
-            common.jscSupportedLanguages(),
-            function(language) {
-                // cleanup
-                var clean = jsdom('', {cookie: ['lang=en']});
-                $.PrivateBin.I18n.reset('en');
-                $.PrivateBin.I18n.loadTranslations();
-                clean();
-
-                // mock
-                clean = jsdom('', {cookie: ['lang=' + language]});
-                // eslint-disable-next-line global-require
-                $.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
-                var loadedLang = $.PrivateBin.I18n.getLanguage(),
-                    result = $.PrivateBin.I18n.translate('Never'),
-                    alias  = $.PrivateBin.I18n._('Never');
-                clean();
-                return language === loadedLang && result === alias;
-            }
-        );
-
-        jsc.property(
-            'should default to en',
-            function() {
-                var clean = jsdom('', {url: 'https://privatebin.net/'});
-
-                // when navigator.userLanguage is undefined and no default language
-                // is specified, it would throw an error
-                [ 'language', 'userLanguage' ].forEach(function (key) {
-                    Object.defineProperty(navigator, key, {
-                        value: undefined,
-                        writeable: false
-                    });
+        it('downloads and handles any supported language', () => {
+            fc.assert(fc.property(
+                common.fcSupportedLanguages(),
+                function(language) {
+                    // cleanup
+                    var clean = globalThis.cleanup('', {cookie: ['lang=en']});
+                    PrivateBin.I18n.reset('en');
+                    PrivateBin.I18n.loadTranslations();
+                    clean();
+
+                    // mock
+                    clean = globalThis.cleanup('', {cookie: ['lang=' + language]});
+                    // eslint-disable-next-line global-require
+                    PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
+                    var loadedLang = PrivateBin.I18n.getLanguage(),
+                        result = PrivateBin.I18n.translate('Never'),
+                        alias  = PrivateBin.I18n._('Never');
+                    clean();
+                    return language === loadedLang && result === alias;
+                }
+            ));
+        });
+
+        it('should default to en', () => {
+            var clean = globalThis.cleanup('', {url: 'https://privatebin.net/'});
+
+            // when navigator.userLanguage is undefined and no default language
+            // is specified, it would throw an error
+            [ 'language', 'userLanguage' ].forEach(function (key) {
+                Object.defineProperty(navigator, key, {
+                    value: undefined,
+                    configurable: true,
+                    enumerable: true,
+                    writable: false
                 });
+            });
 
-                $.PrivateBin.I18n.reset('en');
-                $.PrivateBin.I18n.loadTranslations();
-                var result = $.PrivateBin.I18n.translate('Never'),
-                    alias  = $.PrivateBin.I18n._('Never');
+            PrivateBin.I18n.reset('en');
+            PrivateBin.I18n.loadTranslations();
+            var result = PrivateBin.I18n.translate('Never'),
+                alias  = PrivateBin.I18n._('Never');
 
-                clean();
-                return 'Never' === result && 'Never' === alias;
-            }
-        );
+            clean();
+            return 'Never' === result && 'Never' === alias;
+        });
     });
 });

+ 211 - 197
js/test/Model.js

@@ -1,243 +1,257 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Model', function () {
     describe('getExpirationDefault', function () {
         before(function () {
-            $.PrivateBin.Model.reset();
-            cleanup = jsdom();
+            PrivateBin.Model.reset();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "pasteExpiration"',
-            'nearray asciinestring',
-            'string',
-            'small nat',
-            function (keys, value, key) {
-                keys = keys.map($.PrivateBin.Helper.htmlEntities);
-                value = $.PrivateBin.Helper.htmlEntities(value);
-                var content = keys.length > key ? keys[key] : keys[0],
-                    contents = '<select id="pasteExpiration" name="pasteExpiration">';
-                keys.forEach(function(item) {
-                    contents += '<option value="' + item + '"';
-                    if (item === content) {
-                        contents += ' selected="selected"';
-                    }
-                    contents += '>' + value + '</option>';
-                });
-                contents += '</select>';
-                $('body').html(contents);
-                var result = $.PrivateBin.Helper.htmlEntities(
-                    $.PrivateBin.Model.getExpirationDefault()
-                );
-                $.PrivateBin.Model.reset();
-                return content === result;
-            }
-        );
+        it('returns the contents of the element with id "pasteExpiration"', () => {
+            fc.assert(fc.property(
+                fc.array(fc.string({minLength: 1}), {minLength: 1}),
+                fc.string(),
+                fc.nat(),
+                function (keys, value, key) {
+                    keys = keys.map(PrivateBin.Helper.htmlEntities);
+                    value = PrivateBin.Helper.htmlEntities(value);
+                    var content = keys.length > key ? keys[key] : keys[0],
+                        contents = '<select id="pasteExpiration" name="pasteExpiration">';
+                    keys.forEach(function(item) {
+                        contents += '<option value="' + item + '"';
+                        if (item === content) {
+                            contents += ' selected="selected"';
+                        }
+                        contents += '>' + value + '</option>';
+                    });
+                    contents += '</select>';
+                    document.body.innerHTML = contents;
+                    var result = PrivateBin.Helper.htmlEntities(
+                        PrivateBin.Model.getExpirationDefault()
+                    );
+                    PrivateBin.Model.reset();
+                    return content === result;
+                }
+            ));
+        });
     });
 
     describe('getFormatDefault', function () {
         before(function () {
-            $.PrivateBin.Model.reset();
+            PrivateBin.Model.reset();
         });
         after(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "pasteFormatter"',
-            'nearray asciinestring',
-            'string',
-            'small nat',
-            function (keys, value, key) {
-                keys = keys.map($.PrivateBin.Helper.htmlEntities);
-                value = $.PrivateBin.Helper.htmlEntities(value);
-                var content = keys.length > key ? keys[key] : keys[0],
-                    contents = '<select id="pasteFormatter" name="pasteFormatter">';
-                keys.forEach(function(item) {
-                    contents += '<option value="' + item + '"';
-                    if (item === content) {
-                        contents += ' selected="selected"';
-                    }
-                    contents += '>' + value + '</option>';
-                });
-                contents += '</select>';
-                $('body').html(contents);
-                var result = $.PrivateBin.Helper.htmlEntities(
-                    $.PrivateBin.Model.getFormatDefault()
-                );
-                $.PrivateBin.Model.reset();
-                return content === result;
-            }
-        );
+        it('returns the contents of the element with id "pasteFormatter"', () => {
+            fc.assert(fc.property(
+                fc.array(fc.string({minLength: 1}), {minLength: 1}),
+                fc.string(),
+                fc.nat(),
+                function (keys, value, key) {
+                    keys = keys.map(PrivateBin.Helper.htmlEntities);
+                    value = PrivateBin.Helper.htmlEntities(value);
+                    var content = keys.length > key ? keys[key] : keys[0],
+                        contents = '<select id="pasteFormatter" name="pasteFormatter">';
+                    keys.forEach(function(item) {
+                        contents += '<option value="' + item + '"';
+                        if (item === content) {
+                            contents += ' selected="selected"';
+                        }
+                        contents += '>' + value + '</option>';
+                    });
+                    contents += '</select>';
+                    document.body.innerHTML = contents;
+                    var result = PrivateBin.Helper.htmlEntities(
+                        PrivateBin.Model.getFormatDefault()
+                    );
+                    PrivateBin.Model.reset();
+                    return content === result;
+                }
+            ));
+        });
     });
 
     describe('getPasteId', function () {
         this.timeout(30000);
         beforeEach(function () {
-            $.PrivateBin.Model.reset();
+            PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'returns the query string without separator, if any',
-            common.jscUrl(true, false),
-            jsc.tuple(new Array(16).fill(common.jscHexString)),
-            jsc.array(common.jscQueryString()),
-            jsc.array(common.jscQueryString()),
-            function (url, pasteId, queryStart, queryEnd) {
-                if (queryStart.length > 0) {
-                    queryStart.push('&');
-                }
-                if (queryEnd.length > 0) {
-                    queryEnd.unshift('&');
-                }
-                url.query = queryStart.concat(pasteId, queryEnd);
-                const pasteIdString = pasteId.join(''),
-                    clean           = jsdom('', {url: common.urlToString(url)});
-                const result = $.PrivateBin.Model.getPasteId();
-                $.PrivateBin.Model.reset();
-                clean();
-                return pasteIdString === result;
-            }
-        );
-        jsc.property(
-            'throws exception on empty query string',
-            common.jscUrl(true, false),
-            function (url) {
-                const clean = jsdom('', {url: common.urlToString(url)});
-                let result = false;
-                try {
-                    $.PrivateBin.Model.getPasteId();
+        it('returns the query string without separator, if any', () => {
+            fc.assert(fc.property(
+                common.fcUrl(true, false),
+                fc.array(common.fcHexString(), {minLength: 16, maxLength: 16}),
+                fc.array(common.fcQueryString()),
+                fc.array(common.fcQueryString()),
+                function (url, pasteId, queryStart, queryEnd) {
+                    if (queryStart.length > 0) {
+                        queryStart.push('&');
+                    }
+                    if (queryEnd.length > 0) {
+                        queryEnd.unshift('&');
+                    }
+                    url.query = queryStart.concat(pasteId, queryEnd);
+                    const pasteIdString = pasteId.join(''),
+                        clean           = globalThis.cleanup('', {url: common.urlToString(url)});
+                    const result = PrivateBin.Model.getPasteId();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return pasteIdString === result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('throws exception on empty query string', () => {
+            fc.assert(fc.property(
+                common.fcUrl(true, false),
+                function (url) {
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)});
+                    let result = false;
+                    try {
+                        PrivateBin.Model.getPasteId();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                $.PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getPasteKey', function () {
         this.timeout(30000);
         beforeEach(function () {
-            $.PrivateBin.Model.reset();
+            PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'throws exception on v1 URLs',
-            common.jscUrl(),
-            function (url) {
-                url.fragment = '0OIl'; // any non-base58 string
-                const clean = jsdom('', {url: common.urlToString(url)});
-                let result = false;
-                try {
-                    $.PrivateBin.Model.getPasteId();
+        it('throws exception on v1 URLs', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                function (url) {
+                    url.fragment = '0OIl'; // any non-base58 string
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)});
+                    let result = false;
+                    try {
+                        PrivateBin.Model.getPasteId();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('returns the fragment stripped of trailing query parts', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                function (url, trail) {
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
                 }
-                $.PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
-        jsc.property(
-            'returns the fragment stripped of trailing query parts',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            function (url, trail) {
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = $.PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
-                const clean = jsdom('', {url: common.urlToString(url)}),
-                    result = $.PrivateBin.Model.getPasteKey();
-                $.PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'returns the fragment of a v2 URL',
-            common.jscUrl(),
-            function (url) {
-                // base58 strips leading NULL bytes, so the string is padded with these if not found
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = $.PrivateBin.CryptTool.base58encode(fragment);
-                const clean = jsdom('', {url: common.urlToString(url)}),
-                    result = $.PrivateBin.Model.getPasteKey();
-                $.PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'returns the v2 fragment stripped of trailing query parts',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            function (url, trail) {
-                // base58 strips leading NULL bytes, so the string is padded with these if not found
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = $.PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
-                const clean = jsdom('', {url: common.urlToString(url)}),
-                    result = $.PrivateBin.Model.getPasteKey();
-                $.PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'throws exception on empty fragment of the URL',
-            common.jscUrl(false),
-            function (url) {
-                let clean = jsdom('', {url: common.urlToString(url)}),
-                    result = false;
-                try {
-                    $.PrivateBin.Model.getPasteKey();
+            ));
+        });
+        it('returns the fragment of a v2 URL', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                function (url) {
+                    // base58 strips leading NULL bytes, so the string is padded with these if not found
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment);
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('returns the v2 fragment stripped of trailing query parts', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                function (url, trail) {
+                    // base58 strips leading NULL bytes, so the string is padded with these if not found
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
+                }
+            ));
+        });
+        it('throws exception on empty fragment of the URL', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false),
+                function (url) {
+                    let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = false;
+                    try {
+                        PrivateBin.Model.getPasteKey();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                $.PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getTemplate', function () {
         beforeEach(function () {
-            $.PrivateBin.Model.reset();
+            PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "[name]template"',
-            jsc.nearray(common.jscAlnumString()),
-            jsc.nearray(common.jscA2zString()),
-            jsc.nearray(common.jscAlnumString()),
-            function (id, element, value) {
-                id = id.join('');
-                element = element.join('');
-                value = value.join('').trim();
+        it('returns the contents of the element with id "[name]template"', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString(), {minLength: 1}),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                fc.array(common.fcAlnumString(), {minLength: 1}),
+                function (id, element, value) {
+                    id = id.join('');
+                    element = element.join('');
+                    value = value.join('').trim();
 
-                // <br>, <hr>, <img> and <wbr> tags can't contain strings,
-                // table tags can't be alone, so test with a <p> instead
-                if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) {
-                    element = 'p';
-                }
+                    // <br>, <hr>, <img> and <wbr> tags can't contain strings,
+                    // table tags can't be alone, so test with a <p> instead
+                    if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) {
+                        element = 'p';
+                    }
 
-                $('body').html(
-                    '<div id="templates"><' + element + ' id="' + id +
-                    'template">' + value + '</' + element + '></div>'
-                );
-                $.PrivateBin.Model.init();
-                var template = '<' + element + ' id="' + id + '">' + value +
-                    '</' + element + '>',
-                    result = $.PrivateBin.Model.getTemplate(id).wrap('<p/>').parent().html();
-                $.PrivateBin.Model.reset();
-                return template === result;
-            }
-        );
+                    document.body.innerHTML = (
+                        '<div id="templates"><' + element + ' id="' + id +
+                        'template">' + value + '</' + element + '></div>'
+                    );
+                    PrivateBin.Model.init();
+                    var template = '<' + element + ' id="' + id + '">' + value +
+                        '</' + element + '>',
+                        templateEl = PrivateBin.Model.getTemplate(id),
+                        wrapper = document.createElement('p');
+                    wrapper.appendChild(templateEl.cloneNode(true));
+                    var result = wrapper.innerHTML;
+                    PrivateBin.Model.reset();
+                    return template === result;
+                }
+            ));
+        });
     });
 });
 

+ 207 - 177
js/test/PasteStatus.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 function urlStrings(schema, longUrl, shortUrl) {
     longUrl.schema = schema;
@@ -22,37 +23,57 @@ describe('PasteStatus', function () {
     describe('createPasteNotification', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'creates a notification after a successful document upload',
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (url1, url2) {
-                const expected1 = common.urlToString(url1).replace(/&(gt|lt)$/, '&$1a'),
-                    expected2 = common.urlToString(url2).replace(/&(gt|lt)$/, '&$1a'),
-                    clean = jsdom();
-                $('body').html('<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification(expected1, expected2);
-                const result1 = $('#pasteurl')[0].href,
-                    result2 = $('#deletelink')[0].href;
-                clean();
-                return result1 === expected1 && result2 === expected2;
-            }
-        );
+        it('creates a notification after a successful document upload', function () {
+            cleanup();
+            document.body.innerHTML = '<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div><div id="pastesuccess"></div>';
+            PrivateBin.PasteStatus.init();
+            const expected1 = 'https://example.com/long';
+            const expected2 = 'https://example.com/short';
+            PrivateBin.PasteStatus.createPasteNotification(expected1, expected2);
+
+            assert.strictEqual(document.getElementById('pasteurl').href, expected1);
+            assert.strictEqual(document.getElementById('deletelink').href, expected2);
+            assert.ok(!document.getElementById('pastesuccess').classList.contains('hidden'));
+        });
+
+        it('creates a notification after a successful document upload (jsc)', () => {
+            fc.assert(fc.asyncProperty(
+                common.fcUrl(),
+                common.fcUrl(false),
+                async function (url1, url2) {
+                    // sometimes the generator returns incomplete objects, bail out
+                    if (!url1 || !url1.address || !url2 || !url2.address) {
+                        return true;
+                    }
+                    const expected1 = common.urlToString(url1).replace(/&(gt|lt)$/, '&$1a'),
+                        expected2 = common.urlToString(url2).replace(/&(gt|lt)$/, '&$1a');
+                    globalThis.cleanup();
+                    document.body.innerHTML = '<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div><div id="pastesuccess"></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification(expected1, expected2);
+
+                    assert.ok(!document.getElementById('pastesuccess').classList.contains('hidden'));
+
+                    const result2 = document.getElementById('deletelink').href;
+                    return document.getElementById('pasteurl').href === expected1 && result2 === expected2;
+                }
+            ));
+        });
 
         it(
             'shows Cmd hotkey hint on macOS',
             function () {
-                const clean = jsdom();
+                cleanup();
                 Object.defineProperty(navigator, 'userAgent', {
                     value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
                     configurable: true
                 });
-                $('body').html('<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('https://example.com/', 'https://example.com/delete');
-                const hotkey = $('#copyhint kbd').first().text();
-                clean();
+                document.body.innerHTML = '<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div>';
+                PrivateBin.PasteStatus.init();
+                PrivateBin.PasteStatus.createPasteNotification('https://example.com/', 'https://example.com/delete');
+
+                const hotkey = document.querySelector('#copyhint kbd').textContent;
+
                 assert.strictEqual(hotkey, 'Cmd');
             }
         );
@@ -60,16 +81,19 @@ describe('PasteStatus', function () {
         it(
             'shows Ctrl hotkey hint on non-Mac platforms',
             function () {
-                const clean = jsdom();
+                cleanup();
+
                 Object.defineProperty(navigator, 'userAgent', {
                     value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                     configurable: true
                 });
-                $('body').html('<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('https://example.com/', 'https://example.com/delete');
-                const hotkey = $('#copyhint kbd').first().text();
-                clean();
+                document.body.innerHTML = '<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div>';
+
+                PrivateBin.PasteStatus.init();
+                PrivateBin.PasteStatus.createPasteNotification('https://example.com/', 'https://example.com/delete');
+
+                const hotkey = document.querySelector('#copyhint kbd').textContent;
+
                 assert.strictEqual(hotkey, 'Ctrl');
             }
         );
@@ -78,181 +102,187 @@ describe('PasteStatus', function () {
     describe('extractUrl', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'extracts and updates IDN URLs found in given response',
-            common.jscSchemas(false),
-            'nestring',
-            common.jscUrl(),
-            function (schema, domain, url) {
-                domain = domain.replace(/\P{Letter}|[\u{AA}-\u{BA}]/gu, '').toLowerCase();
-                if (domain.length === 0) {
-                    domain = 'a';
+        it('extracts and updates IDN URLs found in given response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                fc.string({minLength: 1}),
+                common.fcUrl(),
+                function (schema, domain, url) {
+                    domain = domain.replace(/\P{Letter}|[\u{AA}-\u{BA}]/gu, '').toLowerCase();
+                    if (domain.length === 0) {
+                        domain = 'a';
+                    }
+                    url.schema = schema;
+                    url.address.unshift('.');
+                    url.address = domain.split('').concat(url.address);
+                    const urlString = common.urlToString(url),
+                        expected = urlString.substring((schema + '://' + domain).length),
+                        clean = globalThis.cleanup();
+
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(urlString);
+
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
+
+                    return result.endsWith(expected) && (
+                        result.startsWith(schema + '://xn--') ||
+                        result.startsWith(schema + '://' + domain)
+                    );
                 }
-                url.schema = schema;
-                url.address.unshift('.');
-                url.address = domain.split('').concat(url.address);
-                const urlString = common.urlToString(url),
-                    expected = urlString.substring((schema + '://' + domain).length),
-                    clean = jsdom();
-
-                $('body').html('<div><div id="pastelink"></div></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('', '');
-                $.PrivateBin.PasteStatus.extractUrl(urlString);
-
-                const result = $('#pasteurl')[0].href;
-                clean();
-
-                return result.endsWith(expected) && (
-                    result.startsWith(schema + '://xn--') ||
-                    result.startsWith(schema + '://' + domain)
-                );
-            }
-        );
+            ));
+        });
 
         // YOURLS API samples from: https://yourls.org/readme.html#API;apireturn
-        jsc.property(
-            'extracts and updates URLs found in YOURLS API JSON response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = {
-                        url: {
-                            keyword: longUrl.address.join(''),
-                            url: longUrlString,
+        it('extracts and updates URLs found in YOURLS API JSON response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = {
+                            url: {
+                                keyword: longUrl.address.join(''),
+                                url: longUrlString,
+                                title: 'example title',
+                                date: '2014-10-24 16:01:39',
+                                ip: '127.0.0.1'
+                            },
+                            status: 'success',
+                            message: longUrlString + ' added to database',
                             title: 'example title',
-                            date: '2014-10-24 16:01:39',
-                            ip: '127.0.0.1'
+                            shorturl: shortUrlString,
+                            statusCode: 200
                         },
-                        status: 'success',
-                        message: longUrlString + ' added to database',
-                        title: 'example title',
-                        shorturl: shortUrlString,
-                        statusCode: 200
-                    },
-                    clean = jsdom();
-
-                $('body').html('<div><div id="pastelink"></div></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('', '');
-                $.PrivateBin.PasteStatus.extractUrl(JSON.stringify(yourlsResponse, undefined, 4));
-
-                const result = $('#pasteurl')[0].href;
-                clean();
-
-                return result === shortUrlString;
-            }
-        );
-        jsc.property(
-            'extracts and updates URLs found in YOURLS API XML response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = '<result>\n' +
-                        '    <keyword>' + longUrl.address.join('') + '</keyword>\n' +
-                        '    <shorturl>' + shortUrlString + '</shorturl>\n' +
-                        '    <longurl>' + longUrlString + '</longurl>\n' +
-                        '    <message>success</message>\n' +
-                        '    <statusCode>200</statusCode>\n' +
+                        clean = globalThis.cleanup();
+
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(JSON.stringify(yourlsResponse, undefined, 4));
+
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
+
+                    return result === shortUrlString;
+                }
+            ));
+        });
+        it('extracts and updates URLs found in YOURLS API XML response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = '<result>\n' +
+                            '    <keyword>' + longUrl.address.join('') + '</keyword>\n' +
+                            '    <shorturl>' + shortUrlString + '</shorturl>\n' +
+                            '    <longurl>' + longUrlString + '</longurl>\n' +
+                            '    <message>success</message>\n' +
+                            '    <statusCode>200</statusCode>\n' +
                         '</result>',
-                    clean = jsdom();
+                        clean = globalThis.cleanup();
 
-                $('body').html('<div><div id="pastelink"></div></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('', '');
-                $.PrivateBin.PasteStatus.extractUrl(yourlsResponse);
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(yourlsResponse);
 
-                const result = $('#pasteurl')[0].href;
-                clean();
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
 
-                return result === shortUrlString;
-            }
-        );
-        jsc.property(
-            'extracts and updates URLs found in YOURLS proxy HTML response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [_, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = '<!DOCTYPE html>\n' +
-                        '<html lang="en">\n' +
-                        '\t<head>\n' +
-                        '\t\t<meta charset="utf-8" />\n' +
-                        '\t\t<meta name="robots" content="noindex" />\n' +
-                        '\t\t<meta name="google" content="notranslate">\n' +
-                        '\t\t<title>PrivateBin</title>\n' +
-                        '\t</head>\n' +
-                        '\t<body>\n' +
-                        '\t\t<p>Your document is <a id="pasteurl" href="' + shortUrlString + '">' + shortUrlString + '</a> <span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></p>\n' +
-                        '\t</body>\n' +
+                    return result === shortUrlString;
+                }
+            ));
+        });
+        it('extracts and updates URLs found in YOURLS proxy HTML response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [_, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = '<!DOCTYPE html>\n' +
+                            '<html lang="en">\n' +
+                            '\t<head>\n' +
+                            '\t\t<meta charset="utf-8" />\n' +
+                            '\t\t<meta name="robots" content="noindex" />\n' +
+                            '\t\t<meta name="google" content="notranslate">\n' +
+                            '\t\t<title>PrivateBin</title>\n' +
+                            '\t</head>\n' +
+                            '\t<body>\n' +
+                            '\t\t<p>Your document is <a id="pasteurl" href="' + shortUrlString + '">' + shortUrlString + '</a> <span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></p>\n' +
+                            '\t</body>\n' +
                         '</html>',
-                    clean = jsdom();
+                        clean = globalThis.cleanup();
 
-                $('body').html('<div><div id="pastelink"></div></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.createPasteNotification('', '');
-                $.PrivateBin.PasteStatus.extractUrl(yourlsResponse);
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(yourlsResponse);
 
-                const result = $('#pasteurl')[0].href;
-                clean();
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
 
-                return result === shortUrlString;
-            }
-        );
+                    return result === shortUrlString;
+                }
+            ));
+        });
     });
 
     describe('showRemainingTime', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'shows burn after reading message or remaining time',
-            'bool',
-            'nat',
-            common.jscUrl(),
-            function (burnafterreading, remainingTime, url) {
-                let clean = jsdom('', {url: common.urlToString(url)}),
-                    result;
-                $('body').html('<div id="remainingtime" class="hidden"></div>');
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.showRemainingTime($.PrivateBin.Helper.PasteFactory({
-                    'adata': [null, null, null, burnafterreading],
-                    'v': 2,
-                    'meta': {
-                        'time_to_live': remainingTime
+        it('shows burn after reading message or remaining time', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.nat(),
+                common.fcUrl(),
+                function (burnafterreading, remainingTime, url) {
+                    let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result;
+                    document.body.innerHTML = '<div id="remainingtime" class="hidden"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.showRemainingTime(PrivateBin.Helper.PasteFactory({
+                        'adata': [null, null, null, burnafterreading],
+                        'v': 2,
+                        'meta': {
+                            'time_to_live': remainingTime
+                        }
+                    }));
+                    if (burnafterreading) {
+                        result = document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
+                                !document.getElementById('remainingtime').classList.contains('hidden');
+                    } else if (remainingTime) {
+                        result =!document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
+                                !document.getElementById('remainingtime').classList.contains('hidden');
+                    } else {
+                        result = document.getElementById('remainingtime').classList.contains('hidden') &&
+                                !document.getElementById('remainingtime').classList.contains('foryoureyesonly');
                     }
-                }));
-                if (burnafterreading) {
-                    result = $('#remainingtime').hasClass('foryoureyesonly') &&
-                            !$('#remainingtime').hasClass('hidden');
-                } else if (remainingTime) {
-                    result =!$('#remainingtime').hasClass('foryoureyesonly') &&
-                            !$('#remainingtime').hasClass('hidden');
-                } else {
-                    result = $('#remainingtime').hasClass('hidden') &&
-                            !$('#remainingtime').hasClass('foryoureyesonly');
+                    clean();
+                    return result;
                 }
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('hideMessages', function () {
         it(
             'hides all messages',
             function() {
-                $('body').html(
+                document.body.innerHTML = (
                     '<div id="remainingtime"></div><div id="pastesuccess"></div>'
                 );
-                $.PrivateBin.PasteStatus.init();
-                $.PrivateBin.PasteStatus.hideMessages();
-                assert.ok($('#remainingtime').hasClass('hidden'));
-                assert.ok($('#pastesuccess').hasClass('hidden'));
+                PrivateBin.PasteStatus.init();
+                PrivateBin.PasteStatus.hideMessages();
+                assert.ok(document.getElementById('remainingtime').classList.contains('hidden'));
+                assert.ok(document.getElementById('pastesuccess').classList.contains('hidden'));
             }
         );
     });

+ 147 - 104
js/test/PasteViewer.js

@@ -1,118 +1,161 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('PasteViewer', function () {
     describe('run, hide, getText, setText, getFormat, setFormat & isPrettyPrinted', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'displays text according to format',
-            common.jscFormats(),
-            'nestring',
-            function (format, text) {
-                var clean = jsdom(),
-                    results = [];
-                $('body').html(
-                    '<div id="placeholder" class="hidden">+++ no document text ' +
-                    '+++</div><div id="prettymessage" class="hidden"><pre ' +
-                    'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div><div id="plaintext" class="hidden"></div>'
-                );
-                $.PrivateBin.PasteViewer.init();
-                $.PrivateBin.PasteViewer.setFormat(format);
-                $.PrivateBin.PasteViewer.setText('');
-                results.push(
-                    $('#placeholder').hasClass('hidden') &&
-                    $('#prettymessage').hasClass('hidden') &&
-                    $('#plaintext').hasClass('hidden') &&
-                    $.PrivateBin.PasteViewer.getFormat() === format &&
-                    $.PrivateBin.PasteViewer.getText() === ''
-                );
-                $.PrivateBin.PasteViewer.run();
-                results.push(
-                    !$('#placeholder').hasClass('hidden') &&
-                    $('#prettymessage').hasClass('hidden') &&
-                    $('#plaintext').hasClass('hidden')
-                );
-                $.PrivateBin.PasteViewer.hide();
-                results.push(
-                    $('#placeholder').hasClass('hidden') &&
-                    $('#prettymessage').hasClass('hidden') &&
-                    $('#plaintext').hasClass('hidden')
-                );
-                $.PrivateBin.PasteViewer.setText(text);
-                $.PrivateBin.PasteViewer.run();
-                results.push(
-                    $('#placeholder').hasClass('hidden') &&
-                    !$.PrivateBin.PasteViewer.isPrettyPrinted() &&
-                    $.PrivateBin.PasteViewer.getText() === text
-                );
-                if (format === 'markdown') {
+        beforeEach(() => {
+            document.body.innerHTML = (
+                '<div id="placeholder" class="hidden">+++ no document text +++</div>' +
+                '<div id="prettymessage" class="hidden"><pre id="prettyprint" class="prettyprint linenums:1"></pre></div>' +
+                '<div id="plaintext" class="hidden"></div>'
+            );
+        });
+
+        afterEach(() => {
+            globalThis.cleanup();
+        });
+
+        it('basic plaintext display works', function () {
+            PrivateBin.PasteViewer.init();
+            PrivateBin.PasteViewer.setFormat('plaintext');
+            PrivateBin.PasteViewer.setText('hello');
+            PrivateBin.PasteViewer.run();
+            assert.strictEqual(PrivateBin.PasteViewer.getText(), 'hello');
+            assert.ok(!document.getElementById('prettymessage').classList.contains('hidden'));
+        });
+
+        it('basic markdown display works', function () {
+            PrivateBin.PasteViewer.init();
+            PrivateBin.PasteViewer.setFormat('markdown');
+            PrivateBin.PasteViewer.setText('hello **bold**');
+            PrivateBin.PasteViewer.run();
+            assert.strictEqual(PrivateBin.PasteViewer.getText(), 'hello **bold**');
+            assert.ok(!document.getElementById('plaintext').classList.contains('hidden'));
+        });
+
+        it('initializes with empty text and shows nothing', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                function (format) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText('');
                     results.push(
-                        $('#prettymessage').hasClass('hidden') &&
-                        !$('#plaintext').hasClass('hidden')
+                        document.getElementById('placeholder').classList.contains('hidden') &&
+                        document.getElementById('prettymessage').classList.contains('hidden') &&
+                        document.getElementById('plaintext').classList.contains('hidden') &&
+                        PrivateBin.PasteViewer.getFormat() === format &&
+                        PrivateBin.PasteViewer.getText() === ''
                     );
-                } else {
+                    return results.every(element => element);
+                }
+            ));
+        });
+
+        it('when no text is given and view is rendered, it shows placeholder', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                function (format) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText('');
+                    PrivateBin.PasteViewer.run();
                     results.push(
-                        !$('#prettymessage').hasClass('hidden') &&
-                        $('#plaintext').hasClass('hidden')
+                        !document.getElementById('placeholder').classList.contains('hidden') &&
+                        document.getElementById('prettymessage').classList.contains('hidden') &&
+                        document.getElementById('plaintext').classList.contains('hidden')
                     );
+                    return results.every(element => element);
                 }
-                clean();
-                return results.every(element => element);
-            }
-        );
+            ));
+        });
 
-        jsc.property(
-            'sanitizes XSS',
-            common.jscFormats(),
-            'string',
-            // @see    {@link https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet}
-            jsc.elements([
-                '<PLAINTEXT>',
-                '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>',
-                '\'\';!--"<XSS>=&{()}',
-                '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>',
-                '\'">><marquee><img src=x onerror=confirm(1)></marquee>">' +
-                '</plaintext\\></|\\><plaintext/onmouseover=prompt(1)>' +
-                '<script>prompt(1)</script>@gmail.com<isindex formaction=java' +
-                // obfuscate script URL from eslint rule: no-script-url
-                'script:alert(/XSS/) type=submit>\'-->"></script>' +
-                '<script>alert(document.cookie)</script>"><img/id="confirm' +
-                '&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">',
-                '<IMG SRC="javascript:alert(\'XSS\');">',
-                '<IMG SRC=javascript:alert(\'XSS\')>',
-                '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>',
-                '<IMG SRC=javascript:alert(&quot;XSS&quot;)>',
-                '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
-                '<a onmouseover="alert(document.cookie)">xxs link</a>',
-                '<a onmouseover=alert(document.cookie)>xxs link</a>',
-                '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
-                '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>',
-                '<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">',
-                '<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>',
-                '<TABLE BACKGROUND="javascript:alert(\'XSS\')">',
-                '<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">',
-                '<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="httx://xss.rocks/xss.js"></SCRIPT>'
-            ]),
-            'string',
-            function (format, prefix, xss, suffix) {
-                var clean = jsdom(),
-                    text = prefix + xss + suffix;
-                $('body').html(
-                    '<div id="placeholder" class="hidden">+++ no document text ' +
-                    '+++</div><div id="prettymessage" class="hidden"><pre ' +
-                    'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div><div id="plaintext" class="hidden"></div>'
-                );
-                $.PrivateBin.PasteViewer.init();
-                $.PrivateBin.PasteViewer.setFormat(format);
-                $.PrivateBin.PasteViewer.setText(text);
-                $.PrivateBin.PasteViewer.run();
-                var result = $('body').html().indexOf(xss) === -1;
-                clean();
-                return result;
-            }
-        );
+        it('displays text according to format', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                function (format, text) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+                    results.push(
+                        document.getElementById('placeholder').classList.contains('hidden') &&
+                        !PrivateBin.PasteViewer.isPrettyPrinted() &&
+                        PrivateBin.PasteViewer.getText() === text
+                    );
+                    if (format === 'markdown') {
+                        results.push(
+                            document.getElementById('prettymessage').classList.contains('hidden') &&
+                            !document.getElementById('plaintext').classList.contains('hidden')
+                        );
+                    } else {
+                        results.push(
+                            !document.getElementById('prettymessage').classList.contains('hidden') &&
+                            document.getElementById('plaintext').classList.contains('hidden')
+                        );
+                    }
+
+                    return results.every(element => element);
+                }
+            ));
+        });
+
+        it('sanitizes XSS', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                fc.string(),
+                fc.constantFrom(
+                    '<PLAINTEXT>',
+                    '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>',
+                    '\'\';!--"<XSS>=&{()}',
+                    '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>',
+                    '\'">><marquee><img src=x onerror=confirm(1)></marquee>">' +
+                    '</plaintext\\></|\\><plaintext/onmouseover=prompt(1)>' +
+                    '<script>prompt(1)</script>@gmail.com<isindex formaction=java' +
+                    // obfuscate script URL from eslint rule: no-script-url
+                    'script:alert(/XSS/) type=submit>\'-->"></script>' +
+                    '<script>alert(document.cookie)</script>"><img/id="confirm' +
+                    '&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">',
+                    '<IMG SRC="javascript:alert(\'XSS\');">',
+                    '<IMG SRC=javascript:alert(\'XSS\')>',
+                    '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>',
+                    '<IMG SRC=javascript:alert(&quot;XSS&quot;)>',
+                    '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
+                    '<a onmouseover="alert(document.cookie)">xxs link</a>',
+                    '<a onmouseover=alert(document.cookie)>xxs link</a>',
+                    '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
+                    '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>',
+                    '<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">',
+                    '<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>',
+                    '<TABLE BACKGROUND="javascript:alert(\'XSS\')">',
+                    '<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">',
+                    '<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="httx://xss.rocks/xss.js"></SCRIPT>'
+                ),
+                fc.string(),
+                function (format, prefix, xss, suffix) {
+                    var text = prefix + xss + suffix;
+                    document.body.innerHTML = (
+                        '<div id="placeholder" class="hidden">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden"><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>'
+                    );
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+                    var result = document.body.innerHTML.indexOf(xss) === -1;
+                    globalThis.cleanup();
+                    return result;
+                }
+            ));
+        });
     });
 });

+ 46 - 32
js/test/Prompt.js

@@ -1,41 +1,55 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
 
 describe('Prompt', function () {
     describe('requestPassword & getPassword', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'returns the password fed into the dialog',
-            'string',
-            function (password) {
-                password = password.replace(/\r+|\n+/g, '');
-                /* const clean = */ jsdom('', {url: 'ftp://example.com/?0000000000000000'});
-                $('body').html(
-                    '<div id="passwordmodal" class="modal fade" role="dialog">' +
-                    '<div class="modal-dialog"><div class="modal-content">' +
-                    '<div class="modal-body"><form id="passwordform" role="form">' +
-                    '<div class="form-group"><input id="passworddecrypt" ' +
-                    'type="password" class="form-control" placeholder="Enter ' +
-                    'password"></div><button type="submit">Decrypt</button>' +
-                    '</form></div></div></div></div>'
-                );
-                $.PrivateBin.Model.reset();
-                $.PrivateBin.Model.init();
-                // eslint-disable-next-line global-require
-                global.bootstrap = require('../bootstrap-5.3.8');
-                $.PrivateBin.Prompt.init();
-                $.PrivateBin.Prompt.requestPassword();
-                $('#passworddecrypt').val(password);
-                // TODO triggers error messages in current jsDOM version, find better solution
-                //$('#passwordform').submit();
-                //var result = $.PrivateBin.Prompt.getPassword();
-                const result = $('#passworddecrypt').val();
-                $.PrivateBin.Model.reset();
-                // TODO triggers error messages in jsDOM since version 11
-                //clean();
-                return result === password;
-            }
-        );
+        it('returns the password fed into the dialog', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (password) {
+                    password = password.replace(/\r+|\n+/g, '');
+                    const clean = globalThis.cleanup('', {url: 'ftp://example.com/?0000000000000000'});
+                    document.body.innerHTML = `
+                        <div id="passwordmodal" class="modal fade" role="dialog">
+                            <div class="modal-dialog">
+                                <div class="modal-content">
+                                    <div class="modal-body">
+                                        <form id="passwordform" role="form">
+                                            <div class="form-group">
+                                                <input id="passworddecrypt" type="password"
+                                                    class="form-control" placeholder="Enter password">
+                                            </div>
+                                            <button type="submit">Decrypt</button>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    `;
+
+                    // Initialize the Prompt module to set up event listeners
+                    PrivateBin.Prompt.init();
+                    PrivateBin.Prompt.requestPassword();
+
+                    // Simulate user input
+                    const passwordInput = document.getElementById('passworddecrypt');
+                    passwordInput.value = password;
+
+                    // Simulate form submission to trigger password capture
+                    const passwordForm = document.getElementById('passwordform');
+                    /** {@type SubmitEvent} */
+                    const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
+                    passwordForm.dispatchEvent(submitEvent);
+
+                    // Verify that getPassword returns the submitted password
+                    const result = PrivateBin.Prompt.getPassword();
+                    clean();
+                    return result === password;
+                }
+            ));
+        });
     });
 });

+ 23 - 15
js/test/ServerInteraction.js

@@ -1,5 +1,6 @@
 'use strict';
-require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('ServerInteraction', function () {
     describe('prepare', function () {
@@ -8,33 +9,40 @@ describe('ServerInteraction', function () {
             await new Promise(resolve => setTimeout(resolve, 1900));
         });
         this.timeout(30000);
-        it('can prepare an encrypted document', function () {
-            jsc.assert(jsc.forall(
-                'string',
-                'string',
-                'string',
+        it('can prepare an encrypted document', async function () {
+            await fc.assert(fc.asyncProperty(
+                fc.string(),
+                fc.string(),
+                fc.string(),
                 async function (key, password, message) {
                     // pause to let async functions conclude
                     await new Promise(resolve => setTimeout(resolve, 300));
-                    let clean = jsdom();
-                    window.crypto = new WebCrypto();
+                    let clean = globalThis.cleanup();
+                    Object.defineProperty(window, 'crypto', {
+                        value: new WebCrypto(),
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
+                    });
+                    global.atob = common.atob;
+                    global.btoa = common.btoa;
                     message = message.trim();
 
-                    $.PrivateBin.ServerInteraction.prepare();
-                    $.PrivateBin.ServerInteraction.setCryptParameters(password, key);
-                    $.PrivateBin.ServerInteraction.setUnencryptedData('adata', [
+                    PrivateBin.ServerInteraction.prepare();
+                    PrivateBin.ServerInteraction.setCryptParameters(password, key);
+                    PrivateBin.ServerInteraction.setUnencryptedData('adata', [
                         // encryption parameters defined by CryptTool, format, discussion, burn after reading
                         null, 'plaintext', 0, 0
                     ]);
-                    $.PrivateBin.ServerInteraction.setUnencryptedData('meta', {'expire': '5min'});
-                    await $.PrivateBin.ServerInteraction.setCipherMessage({'paste': message});
-                    //console.log($.PrivateBin.ServerInteraction.getData());
+                    PrivateBin.ServerInteraction.setUnencryptedData('meta', {'expire': '5min'});
+                    await PrivateBin.ServerInteraction.setCipherMessage({'paste': message});
+                    //console.log(PrivateBin.ServerInteraction.getData());
                     clean();
                     // TODO currently not testing anything and just used to generate v2 pastes for starting development of server side v2 implementation
                     return true;
                 }
             ),
-            {tests: 3});
+            {numRuns: 3});
         });
     });
 });

+ 243 - 250
js/test/TopNav.js

@@ -1,5 +1,13 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
+
+function query(selector) {
+    if (selector.startsWith('#')) {
+        return document.getElementById(selector.slice(1));
+    }
+    return document.querySelector(selector);
+}
 
 describe('TopNav', function () {
     describe('showViewButtons & hideViewButtons', function () {
@@ -11,45 +19,39 @@ describe('TopNav', function () {
             'displays & hides navigation elements for viewing an existing document',
             function () {
                 let results = [];
-                $('body').html(
-                    '<nav class="navbar navbar-inverse navbar-static-top">' +
-                    '<div id="navbar" class="navbar-collapse collapse"><ul ' +
-                    'class="nav navbar-nav"><li><button id="newbutton" ' +
-                    'type="button" class="hidden btn btn-warning navbar-btn">' +
-                    '<span class="glyphicon glyphicon-file" aria-hidden="true">' +
-                    '</span> New</button><button id="clonebutton" type="button"' +
-                    ' class="hidden btn btn-warning navbar-btn">' +
-                    '<span class="glyphicon glyphicon-duplicate" ' +
-                    'aria-hidden="true"></span> Clone</button><button ' +
-                    'id="rawtextbutton" type="button" class="hidden btn ' +
-                    'btn-warning navbar-btn"><span class="glyphicon ' +
-                    'glyphicon-text-background" aria-hidden="true"></span> ' +
-                    'Raw text</button><button id="qrcodelink" type="button" ' +
-                    'data-toggle="modal" data-target="#qrcodemodal" ' +
-                    'class="hidden btn btn-warning navbar-btn"><span ' +
-                    'class="glyphicon glyphicon-qrcode" aria-hidden="true">' +
-                    '</span> QR code</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
-                results.push(
-                    $('#newbutton').hasClass('hidden') &&
-                    $('#clonebutton').hasClass('hidden') &&
-                    $('#rawtextbutton').hasClass('hidden') &&
-                    $('#qrcodelink').hasClass('hidden')
-                );
-                $.PrivateBin.TopNav.showViewButtons();
-                results.push(
-                    !$('#newbutton').hasClass('hidden') &&
-                    !$('#clonebutton').hasClass('hidden') &&
-                    !$('#rawtextbutton').hasClass('hidden') &&
-                    !$('#qrcodelink').hasClass('hidden')
-                );
-                $.PrivateBin.TopNav.hideViewButtons();
-                results.push(
-                    $('#newbutton').hasClass('hidden') &&
-                    $('#clonebutton').hasClass('hidden') &&
-                    $('#rawtextbutton').hasClass('hidden') &&
-                    $('#qrcodelink').hasClass('hidden')
+                document.documentElement.innerHTML =
+                    `<nav class="navbar navbar-inverse navbar-static-top">
+                    <div id="navbar" class="navbar-collapse collapse">
+                    <ul class="nav navbar-nav"><li><button id="newbutton" type="button" class="hidden btn btn-warning navbar-btn">
+                    <span class="glyphicon glyphicon-file" aria-hidden="true">
+                        </span> New</button><button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
+                    <span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> Clone</button>
+                    <button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
+                    <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> Raw text</button>
+                    <button id="downloadtextbutton" type="button" class="hidden btn btn-default navbar-btn"></button>
+                    <button id="qrcodelink" type="button" data-toggle="modal" data-target="#qrcodemodal" class="hidden btn btn-warning navbar-btn"/>
+                    <span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> QR code</button></li></ul></div>
+                    </nav>`;
+                PrivateBin.TopNav.init();
+                results.push(
+                    query('#newbutton').classList.contains('hidden') &&
+                    query('#clonebutton').classList.contains('hidden') &&
+                    query('#rawtextbutton').classList.contains('hidden') &&
+                    query('#qrcodelink').classList.contains('hidden')
+                );
+                PrivateBin.TopNav.showViewButtons();
+                results.push(
+                    !query('#newbutton').classList.contains('hidden') &&
+                    !query('#clonebutton').classList.contains('hidden') &&
+                    !query('#rawtextbutton').classList.contains('hidden') &&
+                    !query('#qrcodelink').classList.contains('hidden')
+                );
+                PrivateBin.TopNav.hideViewButtons();
+                results.push(
+                    query('#newbutton').classList.contains('hidden') &&
+                    query('#clonebutton').classList.contains('hidden') &&
+                    query('#rawtextbutton').classList.contains('hidden') &&
+                    query('#qrcodelink').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -70,7 +72,7 @@ describe('TopNav', function () {
             'displays & hides navigation elements for creating a document',
             function () { // eslint-disable-line complexity
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
                     'type="button" class="hidden">New</button></li><li><a ' +
                     'id="expiration" href="#" class="hidden">Expiration</a>' +
@@ -81,40 +83,39 @@ describe('TopNav', function () {
                     '<li id="attach" class="hidden">Attach a file</li><li>' +
                     '<a id="formatter" href="#" class="hidden">Format</a>' +
                     '</li><li><button id="sendbutton" type="button" ' +
-                    'class="hidden">Create</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
-                results.push(
-                    $('#sendbutton').hasClass('hidden') &&
-                    $('#expiration').hasClass('hidden') &&
-                    $('#formatter').hasClass('hidden') &&
-                    $('#burnafterreadingoption').hasClass('hidden') &&
-                    $('#opendiscussionoption').hasClass('hidden') &&
-                    $('#newbutton').hasClass('hidden') &&
-                    $('#password').hasClass('hidden') &&
-                    $('#attach').hasClass('hidden')
-                );
-                $.PrivateBin.TopNav.showCreateButtons();
-                results.push(
-                    !$('#sendbutton').hasClass('hidden') &&
-                    !$('#expiration').hasClass('hidden') &&
-                    !$('#formatter').hasClass('hidden') &&
-                    !$('#burnafterreadingoption').hasClass('hidden') &&
-                    !$('#opendiscussionoption').hasClass('hidden') &&
-                    !$('#newbutton').hasClass('hidden') &&
-                    !$('#password').hasClass('hidden') &&
-                    !$('#attach').hasClass('hidden')
-                );
-                $.PrivateBin.TopNav.hideCreateButtons();
-                results.push(
-                    $('#sendbutton').hasClass('hidden') &&
-                    $('#expiration').hasClass('hidden') &&
-                    $('#formatter').hasClass('hidden') &&
-                    $('#burnafterreadingoption').hasClass('hidden') &&
-                    $('#opendiscussionoption').hasClass('hidden') &&
-                    $('#newbutton').hasClass('hidden') &&
-                    $('#password').hasClass('hidden') &&
-                    $('#attach').hasClass('hidden')
+                    'class="hidden">Create</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
+                results.push(
+                    query('#sendbutton').classList.contains('hidden') &&
+                    query('#expiration').classList.contains('hidden') &&
+                    query('#formatter').classList.contains('hidden') &&
+                    query('#burnafterreadingoption').classList.contains('hidden') &&
+                    query('#opendiscussionoption').classList.contains('hidden') &&
+                    query('#newbutton').classList.contains('hidden') &&
+                    query('#password').classList.contains('hidden') &&
+                    query('#attach').classList.contains('hidden')
+                );
+                PrivateBin.TopNav.showCreateButtons();
+                results.push(
+                    !query('#sendbutton').classList.contains('hidden') &&
+                    !query('#expiration').classList.contains('hidden') &&
+                    !query('#formatter').classList.contains('hidden') &&
+                    !query('#burnafterreadingoption').classList.contains('hidden') &&
+                    !query('#opendiscussionoption').classList.contains('hidden') &&
+                    !query('#newbutton').classList.contains('hidden') &&
+                    !query('#password').classList.contains('hidden') &&
+                    !query('#attach').classList.contains('hidden')
+                );
+                PrivateBin.TopNav.hideCreateButtons();
+                results.push(
+                    query('#sendbutton').classList.contains('hidden') &&
+                    query('#expiration').classList.contains('hidden') &&
+                    query('#formatter').classList.contains('hidden') &&
+                    query('#burnafterreadingoption').classList.contains('hidden') &&
+                    query('#opendiscussionoption').classList.contains('hidden') &&
+                    query('#newbutton').classList.contains('hidden') &&
+                    query('#password').classList.contains('hidden') &&
+                    query('#attach').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -135,17 +136,16 @@ describe('TopNav', function () {
             'displays the button for creating a document',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li><button id="newbutton" type=' +
-                    '"button" class="hidden">New</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '"button" class="hidden">New</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    $('#newbutton').hasClass('hidden')
+                    query('#newbutton').classList.contains('hidden')
                 );
-                $.PrivateBin.TopNav.showNewPasteButton();
+                PrivateBin.TopNav.showNewPasteButton();
                 results.push(
-                    !$('#newbutton').hasClass('hidden')
+                    !query('#newbutton').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -166,19 +166,18 @@ describe('TopNav', function () {
             'hides the button for cloning a document',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li><button id="clonebutton" ' +
                     'type="button" class="btn btn-warning navbar-btn">' +
                     '<span class="glyphicon glyphicon-duplicate" aria-hidden=' +
-                    '"true"></span> Clone</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '"true"></span> Clone</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$('#clonebutton').hasClass('hidden')
+                    !query('#clonebutton').classList.contains('hidden')
                 );
-                $.PrivateBin.TopNav.hideCloneButton();
+                PrivateBin.TopNav.hideCloneButton();
                 results.push(
-                    $('#clonebutton').hasClass('hidden')
+                    query('#clonebutton').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -199,20 +198,19 @@ describe('TopNav', function () {
             'hides the raw text button',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li><button ' +
                     'id="rawtextbutton" type="button" class="btn ' +
                     'btn-warning navbar-btn"><span class="glyphicon ' +
                     'glyphicon-text-background" aria-hidden="true"></span> ' +
-                    'Raw text</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Raw text</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$('#rawtextbutton').hasClass('hidden')
+                    !query('#rawtextbutton').classList.contains('hidden')
                 );
-                $.PrivateBin.TopNav.hideRawButton();
+                PrivateBin.TopNav.hideRawButton();
                 results.push(
-                    $('#rawtextbutton').hasClass('hidden')
+                    query('#rawtextbutton').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -233,7 +231,7 @@ describe('TopNav', function () {
             'hides the file attachment selection button',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
                     'dropdown"><a href="#" class="dropdown-toggle" data-' +
                     'toggle="dropdown" role="button" aria-haspopup="true" ' +
@@ -242,15 +240,14 @@ describe('TopNav', function () {
                     '<div><input type="file" id="file" name="file" /></div>' +
                     '</li><li id="customattachment" class="hidden"></li><li>' +
                     '<a id="fileremovebutton"  href="#">Remove attachment</a>' +
-                    '</li></ul></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '</li></ul></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$('#filewrap').hasClass('hidden')
+                    !query('#filewrap').classList.contains('hidden')
                 );
-                $.PrivateBin.TopNav.hideFileSelector();
+                PrivateBin.TopNav.hideFileSelector();
                 results.push(
-                    $('#filewrap').hasClass('hidden')
+                    query('#filewrap').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -271,7 +268,7 @@ describe('TopNav', function () {
             'display the custom file attachment',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
                     'dropdown"><a href="#" class="dropdown-toggle" data-' +
                     'toggle="dropdown" role="button" aria-haspopup="true" ' +
@@ -280,15 +277,14 @@ describe('TopNav', function () {
                     '<div><input type="file" id="file" name="file" /></div>' +
                     '</li><li id="customattachment" class="hidden"></li><li>' +
                     '<a id="fileremovebutton"  href="#">Remove attachment</a>' +
-                    '</li></ul></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '</li></ul></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    $('#customattachment').hasClass('hidden')
+                    query('#customattachment').classList.contains('hidden')
                 );
-                $.PrivateBin.TopNav.showCustomAttachment();
+                PrivateBin.TopNav.showCustomAttachment();
                 results.push(
-                    !$('#customattachment').hasClass('hidden')
+                    !query('#customattachment').classList.contains('hidden')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -308,9 +304,8 @@ describe('TopNav', function () {
         it(
             'collapses the navigation when displayed on a small screen',
             function () {
-                const clean = jsdom();
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div class="navbar-header"><button type="button" ' +
                     'class="navbar-toggle collapsed" data-toggle="collapse" ' +
                     'data-target="#navbar" aria-expanded="false" aria-controls' +
@@ -318,34 +313,33 @@ describe('TopNav', function () {
                     'navbar-brand" href=""><img alt="PrivateBin" ' +
                     'src="img/icon.svg" width="38" /></a></div><div ' +
                     'id="navbar"><ul><li><button id="newbutton" type=' +
-                    '"button" class="hidden">New</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '"button" class="hidden">New</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    $('.navbar-toggle').hasClass('collapsed') &&
-                    $('#navbar').attr('aria-expanded') !== 'true'
+                    query('.navbar-toggle').classList.contains('collapsed') &&
+                    query('#navbar').getAttribute('aria-expanded') !== 'true'
                 );
-                $.PrivateBin.TopNav.collapseBar();
+                PrivateBin.TopNav.collapseBar();
                 results.push(
-                    $('.navbar-toggle').hasClass('collapsed') &&
-                    $('#navbar').attr('aria-expanded') !== 'true'
+                    query('.navbar-toggle').classList.contains('collapsed') &&
+                    query('#navbar').getAttribute('aria-expanded') !== 'true'
                 );
                 /*
                 with the upgrade for bootstrap-3.3.7.js to bootstrap-3.4.1.js
                 the mobile interface detection changed to check if the
                 ontouchstart event exists, which broke this section of the test
-                $('.navbar-toggle').trigger('click');
+                const toggleBtn = document.querySelector('.navbar-toggle');
+                toggleBtn.dispatchEvent(new MouseEvent('click'));
                 results.push(
-                    !$('.navbar-toggle').hasClass('collapsed') &&
-                    $('#navbar').attr('aria-expanded') == 'true'
+                    !toggleBtn.classList.contains('collapsed') &&
+                    document.getElementById('navbar').getAttribute('aria-expanded') == 'true'
                 );
-                $.PrivateBin.TopNav.collapseBar();
+                PrivateBin.TopNav.collapseBar();
                 results.push(
-                    $('.navbar-toggle').hasClass('collapsed') &&
-                    $('#navbar').attr('aria-expanded') == 'false'
+                    document.querySelector('.navbar-toggle').classList.contains('collapsed') &&
+                    document.getElementById('navbar').getAttribute('aria-expanded') == 'false'
                 );
                 */
-                clean();
                 const result = results.every(element => element);
                 if (!result) {
                     console.log(results);
@@ -364,36 +358,35 @@ describe('TopNav', function () {
             'reset inputs to defaults (options off)',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="burnafterreading" name="burnafterreading" /> ' +
                     'Burn after reading</label></li><li id="opendiscussionoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="opendiscussion" name="opendiscussion" /> ' +
-                    'Open discussion</label></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Open discussion</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $('#burnafterreading').attr('checked', 'checked');
-                $('#opendiscussion').attr('checked', 'checked');
+                query('#burnafterreading').checked = true;
+                query('#opendiscussion').checked = true;
                 results.push(
-                    $.PrivateBin.TopNav.getBurnAfterReading()
+                    PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    $.PrivateBin.TopNav.getOpenDiscussion()
+                    PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $.PrivateBin.TopNav.resetInput();
+                PrivateBin.TopNav.resetInput();
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -408,35 +401,35 @@ describe('TopNav', function () {
             'reset inputs to defaults (burnafterreading on)',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="burnafterreading" name="burnafterreading" checked="checked" /> ' +
                     'Burn after reading</label></li><li id="opendiscussionoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
-                    'Open discussion</label></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Open discussion</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    $.PrivateBin.TopNav.getBurnAfterReading()
+                    PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $('#burnafterreading').removeAttr('checked');
+                query('#burnafterreading').checked = false;
+                query('#burnafterreading').removeAttribute('checked');
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $.PrivateBin.TopNav.resetInput();
+                PrivateBin.TopNav.resetInput();
                 results.push(
-                    $.PrivateBin.TopNav.getBurnAfterReading()
+                    PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -451,36 +444,37 @@ describe('TopNav', function () {
             'reset inputs to defaults (opendiscussion on)',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="burnafterreading" name="burnafterreading" /> ' +
                     'Burn after reading</label></li><li id="opendiscussionoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
-                    'Open discussion</label></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Open discussion</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    $.PrivateBin.TopNav.getOpenDiscussion()
+                    PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $('#opendiscussion').removeAttr('checked');
-                $('#burnafterreading').prop('checked', true);
+                query('#opendiscussion').checked = false;
+                query('#opendiscussion').removeAttribute('checked');
+                query('#burnafterreading').checked = true;
+                query('#burnafterreading').setAttribute('checked', 'checked');
                 results.push(
-                    $.PrivateBin.TopNav.getBurnAfterReading()
+                    PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $.PrivateBin.TopNav.resetInput();
+                PrivateBin.TopNav.resetInput();
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 results.push(
-                    $.PrivateBin.TopNav.getOpenDiscussion()
+                    PrivateBin.TopNav.getOpenDiscussion()
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -500,13 +494,12 @@ describe('TopNav', function () {
         it(
             'returns the currently selected expiration date',
             function () {
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<select id="pasteExpiration" name="pasteExpiration">' +
                     '<option value="1day">1 day</option>' +
-                    '<option value="never">Never</option></select>'
-                );
-                $.PrivateBin.TopNav.init();
-                assert.strictEqual($.PrivateBin.TopNav.getExpiration(), '1day');
+                    '<option value="never">Never</option></select>';
+                PrivateBin.TopNav.init();
+                assert.strictEqual(PrivateBin.TopNav.getExpiration(), '1day');
                 cleanup();
             }
         );
@@ -558,7 +551,7 @@ describe('TopNav', function () {
             'returns the selected files',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
                     'dropdown"><a href="#" class="dropdown-toggle" data-' +
                     'toggle="dropdown" role="button" aria-haspopup="true" ' +
@@ -567,17 +560,16 @@ describe('TopNav', function () {
                     '<div><input type="file" id="file" name="file" /></div>' +
                     '</li><li id="customattachment" class="hidden"></li><li>' +
                     '<a id="fileremovebutton"  href="#">Remove attachment</a>' +
-                    '</li></ul></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '</li></ul></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    $.PrivateBin.TopNav.getFileList() === null
+                    PrivateBin.TopNav.getFileList() === null
                 );
-                addFileList($('#file')[0], [
+                addFileList(query('#file'), [
                     '../img/logo.svg',
                     '../img/busy.gif'
                 ]);
-                const files = $.PrivateBin.TopNav.getFileList();
+                const files = PrivateBin.TopNav.getFileList();
                 results.push(
                     files[0].name === 'logo.svg' &&
                     files[1].name === 'busy.gif'
@@ -601,23 +593,23 @@ describe('TopNav', function () {
             'returns if the burn-after-reading checkbox was ticked',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="burnafterreading" name="burnafterreading" /> ' +
-                    'Burn after reading</label></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Burn after reading</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
-                $('#burnafterreading').attr('checked', 'checked');
+                query('#burnafterreading').checked = true;
                 results.push(
-                    $.PrivateBin.TopNav.getBurnAfterReading()
+                    PrivateBin.TopNav.getBurnAfterReading()
                 );
-                $('#burnafterreading').removeAttr('checked');
+                query('#burnafterreading').checked = false;
+                query('#burnafterreading').removeAttribute('checked');
                 results.push(
-                    !$.PrivateBin.TopNav.getBurnAfterReading()
+                    !PrivateBin.TopNav.getBurnAfterReading()
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -638,23 +630,23 @@ describe('TopNav', function () {
             'returns if the open-discussion checkbox was ticked',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="opendiscussionoption" ' +
                     'class="hidden"><label><input type="checkbox" ' +
                     'id="opendiscussion" name="opendiscussion" /> ' +
-                    'Open discussion</label></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    'Open discussion</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $('#opendiscussion').attr('checked', 'checked');
+                query('#opendiscussion').checked = true;
                 results.push(
-                    $.PrivateBin.TopNav.getOpenDiscussion()
+                    PrivateBin.TopNav.getOpenDiscussion()
                 );
-                $('#opendiscussion').removeAttr('checked');
+                query('#opendiscussion').checked = false;
+                query('#opendiscussion').removeAttribute('checked');
                 results.push(
-                    !$.PrivateBin.TopNav.getOpenDiscussion()
+                    !PrivateBin.TopNav.getOpenDiscussion()
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -668,41 +660,41 @@ describe('TopNav', function () {
 
     describe('getPassword', function () {
         before(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the password input',
-            'string',
-            function (password) {
-                password = password.replace(/\r+|\n+/g, '');
-                let results = [];
-                $('body').html(
-                    '<nav><div id="navbar"><ul><li><div id="password" ' +
-                    'class="navbar-form hidden"><input type="password" ' +
-                    'id="passwordinput" placeholder="Password (recommended)" ' +
-                    'class="form-control" size="23" /></div></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
-                results.push(
-                    $.PrivateBin.TopNav.getPassword() === ''
-                );
-                $('#passwordinput').val(password);
-                results.push(
-                    $.PrivateBin.TopNav.getPassword() === password
-                );
-                $('#passwordinput').val('');
-                results.push(
-                    $.PrivateBin.TopNav.getPassword() === ''
-                );
-                cleanup();
-                const result = results.every(element => element);
-                if (!result) {
-                    console.log(results);
+        it('returns the contents of the password input', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (password) {
+                    password = password.replace(/\r+|\n+/g, '');
+                    let results = [];
+                    document.documentElement.innerHTML =
+                        '<nav><div id="navbar"><ul><li><div id="password" ' +
+                        'class="navbar-form hidden"><input type="password" ' +
+                        'id="passwordinput" placeholder="Password (recommended)" ' +
+                        'class="form-control" size="23" /></div></li></ul></div></nav>';
+                    PrivateBin.TopNav.init();
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === ''
+                    );
+                    query('#passwordinput').value = password;
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === password
+                    );
+                    query('#passwordinput').value = '';
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === ''
+                    );
+                    globalThis.cleanup();
+                    const result = results.every(element => element);
+                    if (!result) {
+                        console.log(results);
+                    }
+                    return result;
                 }
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getCustomAttachment', function () {
@@ -714,7 +706,7 @@ describe('TopNav', function () {
             'returns the custom attachment element',
             function () {
                 let results = [];
-                $('body').html(
+                document.documentElement.innerHTML =
                     '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
                     'dropdown"><a href="#" class="dropdown-toggle" data-' +
                     'toggle="dropdown" role="button" aria-haspopup="true" ' +
@@ -723,15 +715,14 @@ describe('TopNav', function () {
                     '<div><input type="file" id="file" name="file" /></div>' +
                     '</li><li id="customattachment" class="hidden"></li><li>' +
                     '<a id="fileremovebutton"  href="#">Remove attachment</a>' +
-                    '</li></ul></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
+                    '</li></ul></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
                 results.push(
-                    !$.PrivateBin.TopNav.getCustomAttachment().hasClass('test')
+                    !PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
                 );
-                $('#customattachment').addClass('test');
+                query('#customattachment').classList.add('test');
                 results.push(
-                    $.PrivateBin.TopNav.getCustomAttachment().hasClass('test')
+                    PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
                 );
                 cleanup();
                 const result = results.every(element => element);
@@ -753,7 +744,7 @@ describe('TopNav', function () {
             function () {
                 // Insert any setup code needed for the hideAllButtons function
                 // Example: Initialize the DOM elements required for testing
-                $('body').html(
+                document.body.innerHTML =
                     '<nav class="navbar navbar-inverse navbar-static-top">' +
                     '<div id="navbar" class="navbar-collapse collapse"><ul ' +
                     'class="nav navbar-nav"><li><button id="newbutton" ' +
@@ -770,15 +761,14 @@ describe('TopNav', function () {
                     'data-toggle="modal" data-target="#qrcodemodal" ' +
                     'class="hidden btn btn-warning navbar-btn"><span ' +
                     'class="glyphicon glyphicon-qrcode" aria-hidden="true">' +
-                    '</span> QR code</button></li></ul></div></nav>'
-                );
-                $.PrivateBin.TopNav.init();
-                $.PrivateBin.TopNav.hideAllButtons();
-
-                assert.ok($('#newbutton').hasClass('hidden'));
-                assert.ok($('#clonebutton').hasClass('hidden'));
-                assert.ok($('#rawtextbutton').hasClass('hidden'));
-                assert.ok($('#qrcodelink').hasClass('hidden'));
+                    '</span> QR code</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
+                PrivateBin.TopNav.hideAllButtons();
+
+                assert.ok(query('#newbutton').classList.contains('hidden'));
+                assert.ok(query('#clonebutton').classList.contains('hidden'));
+                assert.ok(query('#rawtextbutton').classList.contains('hidden'));
+                assert.ok(query('#qrcodelink').classList.contains('hidden'));
                 cleanup();
             }
         );
@@ -793,14 +783,17 @@ describe('TopNav', function () {
         it(
             'displays raw text view correctly',
             function () {
-                const clean = jsdom('', {url: 'https://privatebin.net/?0123456789abcdef#1'});
-                $('body').html('<button id="rawtextbutton"></button>');
+                const clean = globalThis.cleanup('', {url: 'https://privatebin.net/?0123456789abcdef#1'});
+                document.documentElement.innerHTML = `
+                <li id="loadingindicator" class="hidden"></li>
+                <button id="rawtextbutton"></button>`;
                 const sample = 'example';
-                $.PrivateBin.PasteViewer.setText(sample);
-                $.PrivateBin.Helper.reset();
-                $.PrivateBin.TopNav.init();
-                $('#rawtextbutton').click();
-                assert.strictEqual($('pre').text(), sample);
+                PrivateBin.Alert.init(); // required because of locading indiator being used
+                PrivateBin.TopNav.init();
+                PrivateBin.PasteViewer.setText(sample);
+                PrivateBin.Helper.reset();
+                query('#rawtextbutton').click();
+                assert.strictEqual(document.querySelector('pre').textContent, sample);
                 clean();
             }
         );

+ 51 - 77
js/test/UiHelper.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('UiHelper', function () {
     // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet.
@@ -7,43 +8,45 @@ describe('UiHelper', function () {
     describe('historyChange', function () {
         this.timeout(30000);
         beforeEach(function () {
-            $.PrivateBin.Helper.reset();
+            PrivateBin.Helper.reset();
             cleanup();
         });
 
-        jsc.property(
-            'redirects to home, when the state is null',
-            common.jscUrl(false, false),
-            function (url) {
-                const expected = common.urlToString(url),
-                    clean = jsdom('', {url: expected});
+        it('redirects to home, when the state is null', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false, false),
+                function (url) {
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: expected});
 
-                $.PrivateBin.UiHelper.mockHistoryChange();
-                $.PrivateBin.Helper.reset();
-                var result = window.location.href;
-                clean();
-                return expected === result;
-            }
-        );
+                    PrivateBin.UiHelper.mockHistoryChange();
+                    PrivateBin.Helper.reset();
+                    var result = window.location.href;
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'does not redirect to home, when a new document is created',
-            common.jscUrl(false),
-            jsc.nearray(common.jscBase64String()),
-            function (url, fragment) {
-                url.fragment = fragment.join('');
-                const expected = common.urlToString(url),
-                    clean = jsdom('', {url: expected});
+        it('does not redirect to home, when a new document is created', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false),
+                fc.array(common.fcBase64String(), {minLength: 1}),
+                function (url, fragment) {
+                    url.fragment = fragment.join('');
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: expected});
 
-                $.PrivateBin.UiHelper.mockHistoryChange([
-                    {type: 'newpaste'}, '', expected
-                ]);
-                $.PrivateBin.Helper.reset();
-                var result = window.location.href;
-                clean();
-                return expected === result;
-            }
-        );
+                    PrivateBin.UiHelper.mockHistoryChange([
+                        {type: 'newpaste'}, '', expected
+                    ]);
+                    PrivateBin.Helper.reset();
+                    var result = window.location.href;
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('reloadHome', function () {
@@ -51,56 +54,27 @@ describe('UiHelper', function () {
         /*
         this.timeout(30000);
         before(function () {
-            $.PrivateBin.Helper.reset();
+            PrivateBin.Helper.reset();
         });
 
-        jsc.property(
-            'redirects to home',
-            common.jscUrl(),
-            function (url) {
-                const clean = jsdom('', {url: common.urlToString(url)});
-                delete(url.query);
-                delete(url.fragment);
-                const expected = common.urlToString(url);
-
-                $.PrivateBin.UiHelper.reloadHome();
-                $.PrivateBin.Helper.reset();
-                var result = window.location.href;
-                clean();
-                return expected === result;
-            }
-        );
-        */
-    });
+        it('redirects to home', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                function (url) {
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)});
+                    delete(url.query);
+                    delete(url.fragment);
+                    const expected = common.urlToString(url);
 
-    describe('isVisible', function () {
-        // TODO As per https://github.com/tmpvar/jsdom/issues/1048 there is no layout support in jsdom, yet.
-        // once it is supported or a workaround is found, uncomment the section below
-        /*
-        before(function () {
-            $.PrivateBin.Helper.reset();
+                    PrivateBin.UiHelper.reloadHome();
+                    PrivateBin.Helper.reset();
+                    var result = window.location.href;
+                    clean();
+                    return expected === result;
+                }
+            ));
         });
-
-        jsc.property(
-            'detect visible elements',
-            jsc.nearray(common.jscAlnumString()),
-            jsc.nearray(common.jscA2zString()),
-            function (id, element) {
-                id = id.join('');
-                element = element.join('');
-                var clean = jsdom(
-                    '<' + element + ' id="' + id + '"></' + element + '>'
-                );
-                var result = $.PrivateBin.UiHelper.isVisible($('#' + id));
-                clean();
-                return result;
-            }
-        );
         */
     });
-
-    describe('scrollTo', function () {
-        // TODO Did not find a way to test that, see isVisible test above
-    });
 });
 

+ 50 - 44
js/test/emailTemplateTest.js

@@ -3,7 +3,7 @@ require('../common');
 
 // DOM builder that mirrors bootstrap5.php navbar
 function buildEmailDomNoShortUrl() {
-    $('body').html(
+    document.documentElement.innerHTML =
         // TopNav expects initially hidden #emaillink BUTTON.
         '<nav><div id="navbar"><ul>' +
           '<li>' +
@@ -15,14 +15,18 @@ function buildEmailDomNoShortUrl() {
             '<button id="emaillink" type="button" class="hidden btn btn-secondary">Email</button>' +
           '</li>' +
         '</ul></div></nav>' +
-        '<input id="burnafterreadingoption" type="checkbox">'
-    );
+        '<input id="burnafterreadingoption" type="checkbox">' +
+        // include dummy email confirm modal for sendEmail
+        '<div id="emailconfirmmodal" class="hidden">' +
+            '<div id="emailconfirm-timezone-current"></div>' +
+            '<div id="emailconfirm-timezone-utc"></div>' +
+        '</div>'
 }
 
 // DOM builder that adds the shortener result block
 function buildEmailDomWithShortUrl() {
     buildEmailDomNoShortUrl();
-    $('body').html(
+    document.documentElement.innerHTML =
         // TopNav expectsinitially hidden #emaillink BUTTON.
         '<nav><div id="navbar"><ul>' +
           '<li>' +
@@ -38,41 +42,41 @@ function buildEmailDomWithShortUrl() {
         '<div id="pastelink">Your document is ' +
           '<a id="pasteurl" href="https://short.example/xYz">https://short.example/xYz</a> ' +
           '<span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span>' +
+        '</div>' +
+        // add a minimal email confirmation modal so sendEmail does not crash
+        '<div id="emailconfirmmodal" class="hidden">' +
+            '<div id="emailconfirm-timezone-current"></div>' +
+            '<div id="emailconfirm-timezone-utc"></div>' +
         '</div>'
-    );
 }
 
 
-function stubWinOpen($element) {
-    const win = $element[0].ownerDocument.defaultView;
-
-    // Some helpers in privatebin.js expect a global document.
-    global.document = win.document;
-
+function makeWindowOpenMock() {
+    const originalOpen = window.open;
     let openedUrl = null;
-    const origOpen = win.open;
+    let mockRestoreFn = null;
 
-    // Prefer simple assignment; if blocked, fall back to defineProperty.
-    try {
-        win.open = function (url) {
+    if (typeof jest !== 'undefined' && typeof jest.spyOn === 'function') {
+        const spy = jest.spyOn(window, 'open').mockImplementation((url) => {
             openedUrl = url;
             return {};
-        };
-    } catch (e) {
-        Object.defineProperty(win, 'open', {
-            value: function (url) {
-                openedUrl = url;
-                return {};
-            },
-            configurable: true,
-            writable: true
         });
+        mockRestoreFn = () => spy.mockRestore();
+    } else {
+        window.open = function (url) {
+            openedUrl = url;
+            return {};
+        };
+        mockRestoreFn = () => { window.open = originalOpen; };
     }
 
     return {
         getUrl: () => openedUrl,
-        restore: () => { try { win.open = origOpen; } catch (e) { /* suppress exception in restore */ } },
-        win
+        restore: () => {
+            if (mockRestoreFn) {
+                mockRestoreFn();
+            }
+        }
     };
 }
 
@@ -84,21 +88,23 @@ function extractMailtoBody(mailtoUrl) {
 }
 
 describe('Email - mail body content (short URL vs. fallback)', function () {
-    before(function () {
+    beforeEach(function () {
         cleanup(); // provided by common
     });
 
     it('Uses the short URL when #pasteurl is present and never includes "undefined"', function () {
-        buildEmailDomWithShortUrl();               // with #pastelink/#pasteurl
-        $.PrivateBin.TopNav.init();
-        $.PrivateBin.TopNav.showEmailButton(0);
+        buildEmailDomWithShortUrl(); // with #pastelink/#pasteurl
+        PrivateBin.TopNav.init();
+        PrivateBin.TopNav.showEmailButton(0);
 
-        const $emailBtn = $('#emaillink');
-        assert.ok(!$emailBtn.hasClass('hidden'), '#emaillink should be visible after showEmailButton');
+        const emailBtn = document.getElementById('emaillink');
+        assert.ok(!emailBtn.classList.contains('hidden'), '#emaillink should be visible after showEmailButton');
 
-        const { getUrl, restore } = stubWinOpen($emailBtn);
+        const { getUrl, restore } = makeWindowOpenMock();
         try {
-            $emailBtn.trigger('click');
+            emailBtn.click();
+            document.getElementById('emailconfirm-timezone-current').click();
+
             const openedUrl = getUrl();
             assert.ok(openedUrl, 'window.open should have been called');
 
@@ -107,30 +113,30 @@ describe('Email - mail body content (short URL vs. fallback)', function () {
             assert.doesNotMatch(body, /undefined/, 'email body must not contain "undefined"');
         } finally {
             restore();
-            cleanup();
         }
     });
 
     it('Falls back to window.location.href when #pasteurl is absent and never includes "undefined"', function () {
-        buildEmailDomNoShortUrl();      // No #pasteurl
-        $.PrivateBin.TopNav.init();
-        $.PrivateBin.TopNav.showEmailButton(0);
+        buildEmailDomNoShortUrl(); // No #pasteurl
+        PrivateBin.TopNav.init();
+        PrivateBin.TopNav.showEmailButton(0);
 
-        const $emailBtn = $('#emaillink');
-        assert.ok(!$emailBtn.hasClass('hidden'), '#emaillink should be visible after showEmailButton');
+        const emailBtn = document.getElementById('emaillink');
+        assert.ok(!emailBtn.classList.contains('hidden'), '#emaillink should be visible after showEmailButton');
 
-        const { getUrl, restore, win } = stubWinOpen($emailBtn);
+        const { getUrl, restore } = makeWindowOpenMock();
         try {
-            $emailBtn.trigger('click');
+            emailBtn.click();
+            document.getElementById('emailconfirm-timezone-current').click();
+
             const openedUrl = getUrl();
             assert.ok(openedUrl, 'window.open should have been called');
 
             const body = extractMailtoBody(openedUrl);
-            assert.match(body, new RegExp(win.location.href), 'email body should include the fallback page URL');
+            assert.match(body, new RegExp(window.location.href), 'email body should include the fallback page URL');
             assert.doesNotMatch(body, /undefined/, 'email body must not contain "undefined"');
         } finally {
             restore();
-            cleanup();
         }
     });
 });

+ 42 - 44
lib/Configuration.php

@@ -33,8 +33,8 @@ class Configuration
      *
      * @var array
      */
-    private static $_defaults = array(
-        'main' => array(
+    private static $_defaults = [
+        'main' => [
             'name'                     => 'PrivateBin',
             'basepath'                 => '',
             'discussion'               => true,
@@ -48,7 +48,7 @@ class Configuration
             'sizelimit'                => 10485760,
             'templateselection'        => false,
             'template'                 => 'bootstrap5',
-            'availabletemplates'       => array(
+            'availabletemplates'       => [
                 'bootstrap5',
                 'bootstrap',
                 'bootstrap-page',
@@ -56,7 +56,7 @@ class Configuration
                 'bootstrap-dark-page',
                 'bootstrap-compact',
                 'bootstrap-compact-page',
-            ),
+            ],
             'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
             'notice'                   => '',
             'languageselection'        => false,
@@ -69,11 +69,11 @@ class Configuration
             'cspheader'                => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'wasm-unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; frame-src blob:; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-modals allow-downloads',
             'httpwarning'              => true,
             'compression'              => 'zlib',
-        ),
-        'expire' => array(
+        ],
+        'expire' => [
             'default' => '1week',
-        ),
-        'expire_options' => array(
+        ],
+        'expire_options' => [
             '5min'   => 300,
             '10min'  => 600,
             '1hour'  => 3600,
@@ -82,53 +82,51 @@ class Configuration
             '1month' => 2592000,
             '1year'  => 31536000,
             'never'  => 0,
-        ),
-        'formatter_options' => array(
+        ],
+        'formatter_options' => [
             'plaintext'          => 'Plain Text',
             'syntaxhighlighting' => 'Source Code',
             'markdown'           => 'Markdown',
-        ),
-        'traffic' => array(
+        ],
+        'traffic' => [
             'limit'     => 10,
             'header'    => '',
             'exempted'  => '',
             'creators'  => '',
-        ),
-        'purge' => array(
+        ],
+        'purge' => [
             'limit'     => 300,
             'batchsize' => 10,
-        ),
-        'model' => array(
+        ],
+        'model' => [
             'class' => 'Filesystem',
-        ),
-        'model_options' => array(
+        ],
+        'model_options' => [
             'dir' => 'data',
-        ),
-        'yourls' => array(
+        ],
+        'yourls' => [
             'signature' => '',
             'apiurl'    => '',
-        ),
-        'shlink' => array(
+        ],
+        'shlink' => [
             'apikey'    => '',
             'apiurl'    => '',
-        ),
+        ],
         // update this array when adding/changing/removing js files
-        'sri' => array(
+        'sri' => [
             'js/base-x-5.0.1.js'     => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==',
-            'js/bootstrap-3.4.1.js'  => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
             'js/bootstrap-5.3.8.js'  => 'sha512-BkZvJ5rZ3zbDCod5seWHpRGg+PRd6ZgE8Nua/OMtcxqm8Wtg0PqwhUUXK5bqvl3oclMt5O+3zjRVX0L+L2j7fA==',
             'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
-            'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
-            'js/legacy.js'           => 'sha512-RQEo1hxpNc37i+jz/D9/JiAZhG8GFx3+SNxjYnI7jUgirDIqrCSj6QPAAZeaidditcWzsJ3jxfEj5lVm7ZwTRQ==',
+            'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-g58rPgrKnvxBDnT6lzYazdEfgY3XhRh7Yt7i1jkJw4oSvN0k/VPZ4zBSL/dCGfXGIOHqlqmX8DO5UFv2XYqsXg==',
+            'js/privatebin.js'       => 'sha512-ah/QdETig/ovH1QPl9Ne+T3VvaP7Bj4Rd9Or9wqQgo5BHge9o02MxAlRGXUZpKviy/27OSzHgz2PwpnH/B2RZg==',
             'js/purify-3.4.12.js'    => 'sha512-Akf6HnAJZm0sWWWI4gp2GYff0NDnHUB02XJE5S7Hdq/Z5xtMjkuFacsDA8ZtViv1gi+onBxMhEMIaGyQeGxBng==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
             'js/zlib.js'             => 'sha512-QOaEwssHqHRRcWJ2Un3Kl2Zhyprzl7T8zmsKN2FppFxW3VR+8UChYOx2iuL0HbXK42fuBWJm5PNQJxufulrt/w==',
-        ),
-    );
+        ],
+    ];
 
     /**
      * parse configuration file and ensure default configuration values are present
@@ -137,8 +135,8 @@ class Configuration
      */
     public function __construct()
     {
-        $basePaths  = array();
-        $config     = array();
+        $basePaths  = [];
+        $config     = [];
         $configPath = getenv('CONFIG_PATH');
         if ($configPath !== false && !empty($configPath)) {
             $basePaths[] = $configPath;
@@ -148,10 +146,10 @@ class Configuration
             $configFile = $basePath . DIRECTORY_SEPARATOR . 'conf.php';
             if (is_readable($configFile)) {
                 $config = parse_ini_file($configFile, true);
-                foreach (array('main', 'model', 'model_options') as $section) {
+                foreach (['main', 'model', 'model_options'] as $section) {
                     if (!array_key_exists($section, $config)) {
                         $name = $config['main']['name'] ?? self::getDefaults()['main']['name'];
-                        throw new TranslatedException(array('%s requires configuration section [%s] to be present in configuration file.', I18n::_($name), $section), 2);
+                        throw new TranslatedException(['%s requires configuration section [%s] to be present in configuration file.', I18n::_($name), $section], 2);
                     }
                 }
                 break;
@@ -173,27 +171,27 @@ class Configuration
                 $section === 'model_options' &&
                 $this->_configuration['model']['class'] === 'Database'
             ) {
-                $values = array(
+                $values = [
                     'dsn' => 'sqlite:' . PATH . 'data' . DIRECTORY_SEPARATOR . 'db.sq3',
                     'tbl' => null,
                     'usr' => null,
                     'pwd' => null,
-                    'opt' => array(),
-                );
+                    'opt' => [],
+                ];
             } elseif (
                 $section === 'model_options' &&
                 $this->_configuration['model']['class'] === 'GoogleCloudStorage'
             ) {
-                $values = array(
+                $values = [
                     'bucket'     => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null,
                     'prefix'     => 'pastes',
                     'uniformacl' => false,
-                );
+                ];
             } elseif (
                 $section === 'model_options' &&
                 $this->_configuration['model']['class'] === 'S3Storage'
             ) {
-                $values = array(
+                $values = [
                     'region'                  => null,
                     'version'                 => null,
                     'endpoint'                => null,
@@ -202,7 +200,7 @@ class Configuration
                     'use_path_style_endpoint' => null,
                     'bucket'                  => null,
                     'prefix'                  => '',
-                );
+                ];
             }
 
             // "*_options" sections don't require all defaults to be set
@@ -232,9 +230,9 @@ class Configuration
                             $result = $config[$section][$key];
                         } elseif (is_bool($val)) {
                             $val = strtolower($config[$section][$key]);
-                            if (in_array($val, array('true', 'yes', 'on'))) {
+                            if (in_array($val, ['true', 'yes', 'on'])) {
                                 $result = true;
-                            } elseif (in_array($val, array('false', 'no', 'off'))) {
+                            } elseif (in_array($val, ['false', 'no', 'off'])) {
                                 $result = false;
                             } else {
                                 $result = (bool) $config[$section][$key];
@@ -313,7 +311,7 @@ class Configuration
     public function getSection($section)
     {
         if (!array_key_exists($section, $this->_configuration)) {
-            throw new TranslatedException(array('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3);
+            throw new TranslatedException(['%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section], 3);
         }
         return $this->_configuration[$section];
     }

+ 17 - 18
lib/Controller.php

@@ -12,7 +12,6 @@
 namespace PrivateBin;
 
 use Exception;
-use PrivateBin\Exception\JsonException;
 use PrivateBin\Exception\TranslatedException;
 use PrivateBin\Persistence\ServerSalt;
 use PrivateBin\Persistence\TrafficLimiter;
@@ -128,7 +127,7 @@ class Controller
             error_log(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION));
             return;
         }
-        if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
+        if (strlen(PATH) > 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
             error_log(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR));
             return;
         }
@@ -207,7 +206,7 @@ class Controller
         // force default language, if language selection is disabled and a default is set
         if (!$this->_conf->getKey('languageselection') && strlen($lang) === 2) {
             $_COOKIE['lang'] = $lang;
-            setcookie('lang', $lang, array('SameSite' => 'Lax', 'Secure' => true));
+            setcookie('lang', $lang, ['SameSite' => 'Lax', 'Secure' => true]);
         }
     }
 
@@ -231,7 +230,7 @@ class Controller
         if (!$this->_conf->getKey('templateselection') && array_key_exists('template', $_COOKIE)) {
             unset($_COOKIE['template']); // ensure value is not re-used in template switcher
             $expiredInAllTimezones = time() - 86400;
-            setcookie('template', '', array('expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true));
+            setcookie('template', '', ['expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true]);
         }
     }
 
@@ -325,7 +324,7 @@ class Controller
                 $paste = $this->_model->getPaste();
                 $paste->setData($data);
                 $paste->store();
-                $this->_json_result($paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
+                $this->_json_result($paste->getId(), ['deletetoken' => $paste->getDeleteToken()]);
             } catch (Exception $e) {
                 $this->_json_error($e->getMessage());
             }
@@ -419,7 +418,7 @@ class Controller
         header('X-Frame-Options: deny');
 
         // label all the expiration options
-        $expire = array();
+        $expire = [];
         foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
             $expire[$time] = ($seconds === 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
         }
@@ -431,22 +430,22 @@ class Controller
         $languageselection = '';
         if ($this->_conf->getKey('languageselection')) {
             $languageselection = I18n::getLanguage();
-            setcookie('lang', $languageselection, array('SameSite' => 'Lax', 'Secure' => true));
+            setcookie('lang', $languageselection, ['SameSite' => 'Lax', 'Secure' => true]);
         }
 
         // set template cookie if that functionality was enabled
         $templateselection = '';
         if ($this->_conf->getKey('templateselection')) {
             $templateselection = TemplateSwitcher::getTemplate();
-            setcookie('template', $templateselection, array('SameSite' => 'Lax', 'Secure' => true));
+            setcookie('template', $templateselection, ['SameSite' => 'Lax', 'Secure' => true]);
         }
 
         // strip policies that are unsupported in meta tag
         $metacspheader = str_replace(
-            array(
+            [
                 'frame-ancestors \'none\'; ',
                 '; sandbox allow-same-origin allow-scripts allow-forms allow-modals allow-downloads',
-            ),
+            ],
             '',
             $this->_conf->getKey('cspheader')
         );
@@ -455,7 +454,7 @@ class Controller
         $page->assign('CSPHEADER', $metacspheader);
         $page->assign('ERROR', I18n::_($this->_error));
         $page->assign('NAME', $this->_conf->getKey('name'));
-        if (in_array($this->_request->getOperation(), array('shlinkproxy', 'yourlsproxy'), true)) {
+        if (in_array($this->_request->getOperation(), ['shlinkproxy', 'yourlsproxy'], true)) {
             $page->assign('SHORTURL', $this->_status);
             $page->draw('shortenerproxy');
             return;
@@ -501,13 +500,13 @@ class Controller
      */
     private function _jsonld($type)
     {
-        if (!in_array($type, array(
+        if (!in_array($type, [
             'comment',
             'commentmeta',
             'paste',
             'pastemeta',
             'types',
-        ))) {
+        ])) {
             $type = '';
         }
         $content = '{}';
@@ -543,10 +542,10 @@ class Controller
      */
     private function _json_error($error)
     {
-        $result = array(
+        $result = [
             'status'  => 1,
             'message' => $error,
-        );
+        ];
         $this->_json = Json::encode($result);
     }
 
@@ -558,13 +557,13 @@ class Controller
      * @param  array $other
      * @throws JsonException
      */
-    private function _json_result($dataid, $other = array())
+    private function _json_result($dataid, $other = [])
     {
-        $result = array(
+        $result = [
             'status' => 0,
             'id'     => $dataid,
             'url'    => $this->_urlBase . '?' . $dataid,
-        ) + $other;
+        ] + $other;
         $this->_json = Json::encode($result);
     }
 

+ 1 - 1
lib/Data/AbstractData.php

@@ -24,7 +24,7 @@ abstract class AbstractData
      * @access protected
      * @var    array
      */
-    protected $_last_cache = array();
+    protected $_last_cache = [];
 
     /**
      * Create a paste.

+ 28 - 28
lib/Data/Database.php

@@ -12,10 +12,10 @@
 namespace PrivateBin\Data;
 
 use Exception;
+use JsonException;
 use PDO;
 use PDOException;
 use PrivateBin\Controller;
-use PrivateBin\Exception\JsonException;
 use PrivateBin\Json;
 
 /**
@@ -158,12 +158,12 @@ class Database extends AbstractData
             return $this->_exec(
                 'INSERT INTO "' . $this->_sanitizeIdentifier('paste') .
                 '" VALUES(?,?,?,?)',
-                array(
+                [
                     $pasteid,
                     Json::encode($paste),
                     $expire_date,
                     Json::encode($meta),
-                )
+                ]
             );
         } catch (Exception $e) {
             error_log('Error while attempting to insert a paste into the database: ' . $e->getMessage());
@@ -183,7 +183,7 @@ class Database extends AbstractData
         try {
             $row = $this->_select(
                 'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') .
-                '" WHERE "dataid" = ?', array($pasteid), true
+                '" WHERE "dataid" = ?', [$pasteid], true
             );
         } catch (PDOException $e) {
             $row = false;
@@ -196,14 +196,14 @@ class Database extends AbstractData
             $paste = Json::decode($row['data']);
         } catch (JsonException $e) {
             error_log('Error while reading a paste from the database: ' . $e->getMessage());
-            $paste = array();
+            $paste = [];
         }
 
         try {
             $paste['meta'] = Json::decode($row['meta']);
         } catch (JsonException $e) {
             error_log('Error while reading a paste from the database: ' . $e->getMessage());
-            $paste['meta'] = array();
+            $paste['meta'] = [];
         }
         $expire_date = (int) $row['expiredate'];
         if ($expire_date > 0) {
@@ -223,11 +223,11 @@ class Database extends AbstractData
     {
         $this->_exec(
             'DELETE FROM "' . $this->_sanitizeIdentifier('paste') .
-            '" WHERE "dataid" = ?', array($pasteid)
+            '" WHERE "dataid" = ?', [$pasteid]
         );
         $this->_exec(
             'DELETE FROM "' . $this->_sanitizeIdentifier('comment') .
-            '" WHERE "pasteid" = ?', array($pasteid)
+            '" WHERE "pasteid" = ?', [$pasteid]
         );
     }
 
@@ -243,7 +243,7 @@ class Database extends AbstractData
         try {
             $row = $this->_select(
                 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') .
-                '" WHERE "dataid" = ?', array($pasteid), true
+                '" WHERE "dataid" = ?', [$pasteid], true
             );
         } catch (PDOException $e) {
             return false;
@@ -277,14 +277,14 @@ class Database extends AbstractData
             return $this->_exec(
                 'INSERT INTO "' . $this->_sanitizeIdentifier('comment') .
                 '" VALUES(?,?,?,?,?,?)',
-                array(
+                [
                     $commentid,
                     $pasteid,
                     $parentid,
                     $data,
                     $meta['icon'],
                     $meta['created'],
-                )
+                ]
             );
         } catch (PDOException $e) {
             error_log('Error while attempting to insert a comment into the database: ' . $e->getMessage());
@@ -303,24 +303,24 @@ class Database extends AbstractData
     {
         $rows = $this->_select(
             'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
-            '" WHERE "pasteid" = ?', array($pasteid)
+            '" WHERE "pasteid" = ?', [$pasteid]
         );
 
         // create comment list
-        $comments = array();
+        $comments = [];
         if (count($rows)) {
             foreach ($rows as $row) {
                 try {
                     $data = Json::decode($row['data']);
                 } catch (JsonException $e) {
                     error_log('Error while reading a comment from the database: ' . $e->getMessage());
-                    $data = array();
+                    $data = [];
                 }
                 $i                          = $this->getOpenSlot($comments, (int) $row['postdate']);
                 $comments[$i]               = $data;
                 $comments[$i]['id']         = $row['dataid'];
                 $comments[$i]['parentid']   = $row['parentid'];
-                $comments[$i]['meta']       = array('created' => (int) $row['postdate']);
+                $comments[$i]['meta']       = ['created' => (int) $row['postdate']];
                 if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) {
                     $comments[$i]['meta']['icon'] = $row['vizhash'];
                 }
@@ -345,7 +345,7 @@ class Database extends AbstractData
             return (bool) $this->_select(
                 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('comment') .
                 '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?',
-                array($pasteid, $parentid, $commentid), true
+                [$pasteid, $parentid, $commentid], true
             );
         } catch (PDOException $e) {
             return false;
@@ -375,7 +375,7 @@ class Database extends AbstractData
         return $this->_exec(
             'UPDATE "' . $this->_sanitizeIdentifier('config') .
             '" SET "value" = ? WHERE "id" = ?',
-            array($value, strtoupper($namespace))
+            [$value, strtoupper($namespace)]
         );
     }
 
@@ -396,13 +396,13 @@ class Database extends AbstractData
             $this->_exec(
                 'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
                 '" VALUES(?,?)',
-                array($configKey, '')
+                [$configKey, '']
             );
 
             // migrate filesystem based salt into database
             $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
             if ($namespace === 'salt' && is_readable($file)) {
-                $fs    = new Filesystem(array('dir' => 'data'));
+                $fs    = new Filesystem(['dir' => 'data']);
                 $value = $fs->getValue('salt');
                 $this->setValue($value, 'salt');
                 if (!unlink($file)) {
@@ -416,7 +416,7 @@ class Database extends AbstractData
                 $this->_last_cache = Json::decode($value);
             } catch (JsonException $e) {
                 error_log('Error decoding JSON from table "config", row "traffic_limiter": ' . $e->getMessage());
-                $this->_last_cache = array();
+                $this->_last_cache = [];
             }
             if (array_key_exists($key, $this->_last_cache)) {
                 return $this->_last_cache[$key];
@@ -440,11 +440,11 @@ class Database extends AbstractData
                 '" WHERE "expiredate" < ? AND "expiredate" != ? ' .
                 ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?')
             );
-            $statement->execute(array(time(), 0, $batchsize));
+            $statement->execute([time(), 0, $batchsize]);
             return $statement->fetchAll(PDO::FETCH_COLUMN, 0);
         } catch (PDOException $e) {
             error_log('Error while attempting to find expired pastes in the database: ' . $e->getMessage());
-            return array();
+            return [];
         }
     }
 
@@ -508,7 +508,7 @@ class Database extends AbstractData
             }
         } elseif ($this->_type === 'oci') {
             // workaround for https://bugs.php.net/bug.php?id=46728
-            $result = array();
+            $result = [];
             while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                 $result[] = array_map('PrivateBin\Data\Database::_sanitizeClob', $row);
             }
@@ -576,7 +576,7 @@ class Database extends AbstractData
         try {
             $row = $this->_select(
                 'SELECT "value" FROM "' . $this->_sanitizeIdentifier('config') .
-                '" WHERE "id" = ?', array($key), true
+                '" WHERE "id" = ?', [$key], true
             );
         } catch (PDOException $e) {
             error_log('Error while attempting to fetch configuration key "' . $key . '" in the database: ' . $e->getMessage());
@@ -604,7 +604,7 @@ class Database extends AbstractData
                 $main_key = ' PRIMARY KEY';
                 break;
         }
-        return array($main_key, $after_key);
+        return [$main_key, $after_key];
     }
 
     /**
@@ -743,7 +743,7 @@ class Database extends AbstractData
         $this->_exec(
             'INSERT INTO "' . $this->_sanitizeIdentifier('config') .
             '" VALUES(?,?)',
-            array('VERSION', Controller::VERSION)
+            ['VERSION', Controller::VERSION]
         );
     }
 
@@ -788,7 +788,7 @@ class Database extends AbstractData
         $supportsDropColumn = true;
         if ($this->_type === 'sqlite') {
             try {
-                $row                = $this->_select('SELECT sqlite_version() AS "v"', array(), true);
+                $row                = $this->_select('SELECT sqlite_version() AS "v"', [], true);
                 $supportsDropColumn = (bool) version_compare($row['v'], '3.35.0', '>=');
             } catch (PDOException $e) {
                 $supportsDropColumn = false;
@@ -902,7 +902,7 @@ class Database extends AbstractData
         $this->_exec(
             'UPDATE "' . $this->_sanitizeIdentifier('config') .
             '" SET "value" = ? WHERE "id" = ?',
-            array(Controller::VERSION, 'VERSION')
+            [Controller::VERSION, 'VERSION']
         );
     }
 }

+ 4 - 4
lib/Data/Filesystem.php

@@ -13,7 +13,7 @@ namespace PrivateBin\Data;
 
 use DirectoryIterator;
 use GlobIterator;
-use PrivateBin\Exception\JsonException;
+use JsonException;
 use PrivateBin\Json;
 
 /**
@@ -205,7 +205,7 @@ class Filesystem extends AbstractData
      */
     public function readComments($pasteid)
     {
-        $comments = array();
+        $comments = [];
         $discdir  = $this->_dataid2discussionpath($pasteid);
         if (is_dir($discdir)) {
             foreach (new DirectoryIterator($discdir) as $file) {
@@ -359,7 +359,7 @@ class Filesystem extends AbstractData
      */
     protected function _getExpiredPastes($batchsize)
     {
-        $pastes = array();
+        $pastes = [];
         $count  = 0;
         $opened = 0;
         $limit  = $batchsize * 10; // try at most 10 times $batchsize pastes before giving up
@@ -388,7 +388,7 @@ class Filesystem extends AbstractData
      */
     public function getAllPastes()
     {
-        $pastes = array();
+        $pastes = [];
         foreach (new GlobIterator($this->_path . self::PASTE_FILE_PATTERN) as $file) {
             if ($file->isFile()) {
                 $pastes[] = $file->getBasename('.php');

+ 22 - 22
lib/Data/GoogleCloudStorage.php

@@ -15,7 +15,7 @@ use Exception;
 use Google\Cloud\Core\Exception\NotFoundException;
 use Google\Cloud\Storage\Bucket;
 use Google\Cloud\Storage\StorageClient;
-use PrivateBin\Exception\JsonException;
+use JsonException;
 use PrivateBin\Json;
 
 class GoogleCloudStorage extends AbstractData
@@ -74,8 +74,8 @@ class GoogleCloudStorage extends AbstractData
         }
 
         $this->_client = class_exists('StorageClientStub', false) ?
-            new \StorageClientStub(array()) :
-            new StorageClient(array('suppressKeyFileNotice' => true));
+            new \StorageClientStub([]) :
+            new StorageClient(['suppressKeyFileNotice' => true]);
         if (isset($bucket)) {
             $this->_bucket = $this->_client->bucket($bucket);
         }
@@ -107,20 +107,20 @@ class GoogleCloudStorage extends AbstractData
      */
     private function _upload($key, &$payload)
     {
-        $metadata = $payload['meta'] ?? array();
+        $metadata = $payload['meta'] ?? [];
         unset($metadata['salt']);
         foreach ($metadata as $k => $v) {
             $metadata[$k] = strval($v);
         }
         try {
-            $data = array(
+            $data = [
                 'name'          => $key,
                 'chunkSize'     => 262144,
-                'metadata'      => array(
+                'metadata'      => [
                     'content-type' => 'application/json',
                     'metadata'     => $metadata,
-                ),
-            );
+                ],
+            ];
             if (!$this->_uniformacl) {
                 $data['predefinedAcl'] = 'private';
             }
@@ -171,7 +171,7 @@ class GoogleCloudStorage extends AbstractData
         $name = $this->_getKey($pasteid);
 
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
+            foreach ($this->_bucket->objects(['prefix' => $name . '/discussion/']) as $comment) {
                 try {
                     $this->_bucket->object($comment->name())->delete();
                 } catch (NotFoundException $e) {
@@ -215,16 +215,16 @@ class GoogleCloudStorage extends AbstractData
      */
     public function readComments($pasteid)
     {
-        $comments = array();
+        $comments = [];
         $prefix   = $this->_getKey($pasteid) . '/discussion/';
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
+            foreach ($this->_bucket->objects(['prefix' => $prefix]) as $key) {
                 $data            = $this->_bucket->object($key->name())->downloadAsString();
                 try {
                     $comment = Json::decode($data);
                 } catch (JsonException $e) {
                     error_log('failed to read comment from ' . $key->name() . ', ' . $e->getMessage());
-                    $comment = array();
+                    $comment = [];
                 }
                 $comment['id']   = basename($key->name());
                 $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
@@ -253,7 +253,7 @@ class GoogleCloudStorage extends AbstractData
     {
         $path = 'config/' . $namespace;
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
+            foreach ($this->_bucket->objects(['prefix' => $path]) as $object) {
                 $name = $object->name();
                 if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
                     continue;
@@ -285,19 +285,19 @@ class GoogleCloudStorage extends AbstractData
             $key = 'config/' . $namespace . '/' . $key;
         }
 
-        $metadata = array('namespace' => $namespace);
+        $metadata = ['namespace' => $namespace];
         if ($namespace !== 'salt') {
             $metadata['value'] = strval($value);
         }
         try {
-            $data = array(
+            $data = [
                 'name'          => $key,
                 'chunkSize'     => 262144,
-                'metadata'      => array(
+                'metadata'      => [
                     'content-type' => 'application/json',
                     'metadata'     => $metadata,
-                ),
-            );
+                ],
+            ];
             if (!$this->_uniformacl) {
                 $data['predefinedAcl'] = 'private';
             }
@@ -333,7 +333,7 @@ class GoogleCloudStorage extends AbstractData
      */
     protected function _getExpiredPastes($batchsize)
     {
-        $expired = array();
+        $expired = [];
 
         $now    = time();
         $prefix = $this->_prefix;
@@ -341,7 +341,7 @@ class GoogleCloudStorage extends AbstractData
             $prefix .= '/';
         }
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
+            foreach ($this->_bucket->objects(['prefix' => $prefix]) as $object) {
                 $expire_at = $object->info()['metadata']['expire_date'] ?? '';
                 if (is_numeric($expire_at) && intval($expire_at) < $now) {
                     array_push($expired, basename($object->name()));
@@ -362,14 +362,14 @@ class GoogleCloudStorage extends AbstractData
      */
     public function getAllPastes()
     {
-        $pastes = array();
+        $pastes = [];
         $prefix = $this->_prefix;
         if (!empty($prefix)) {
             $prefix .= '/';
         }
 
         try {
-            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
+            foreach ($this->_bucket->objects(['prefix' => $prefix]) as $object) {
                 $candidate = substr($object->name(), strlen($prefix));
                 if (!str_contains($candidate, '/')) {
                     $pastes[] = $candidate;

+ 32 - 32
lib/Data/S3Storage.php

@@ -37,7 +37,7 @@ namespace PrivateBin\Data;
 
 use Aws\S3\Exception\S3Exception;
 use Aws\S3\S3Client;
-use PrivateBin\Exception\JsonException;
+use JsonException;
 use PrivateBin\Json;
 
 class S3Storage extends AbstractData
@@ -56,7 +56,7 @@ class S3Storage extends AbstractData
      * @access private
      * @var    array
      */
-    private $_options = array();
+    private $_options = [];
 
     /**
      * S3 bucket
@@ -85,7 +85,7 @@ class S3Storage extends AbstractData
         // AWS SDK will try to load credentials from environment if credentials are not passed via configuration
         // ref: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials.html#default-credential-chain
         if (isset($options['accesskey']) && isset($options['secretkey'])) {
-            $this->_options['credentials'] = array();
+            $this->_options['credentials'] = [];
 
             $this->_options['credentials']['key']    = $options['accesskey'];
             $this->_options['credentials']['secret'] = $options['secretkey'];
@@ -121,15 +121,15 @@ class S3Storage extends AbstractData
      */
     private function _listAllObjects($prefix)
     {
-        $allObjects = array();
-        $options    = array(
+        $allObjects = [];
+        $options    = [
             'Bucket' => $this->_bucket,
             'Prefix' => $prefix,
-        );
+        ];
 
         do {
             $objectsListResponse = $this->_client->listObjects($options);
-            $objects             = $objectsListResponse['Contents'] ?? array();
+            $objects             = $objectsListResponse['Contents'] ?? [];
             foreach ($objects as $object) {
                 $allObjects[]      = $object;
                 $options['Marker'] = $object['Key'];
@@ -165,19 +165,19 @@ class S3Storage extends AbstractData
      */
     private function _upload($key, &$payload)
     {
-        $metadata = $payload['meta'] ?? array();
+        $metadata = $payload['meta'] ?? [];
         unset($metadata['salt']);
         foreach ($metadata as $k => $v) {
             $metadata[$k] = strval($v);
         }
         try {
-            $this->_client->putObject(array(
+            $this->_client->putObject([
                 'Bucket'      => $this->_bucket,
                 'Key'         => $key,
                 'Body'        => Json::encode($payload),
                 'ContentType' => 'application/json',
                 'Metadata'    => $metadata,
-            ));
+            ]);
             return true;
         } catch (S3Exception $e) {
             error_log('failed to upload ' . $key . ' to ' . $this->_bucket . ', ' .
@@ -206,10 +206,10 @@ class S3Storage extends AbstractData
     public function read($pasteid)
     {
         try {
-            $object = $this->_client->getObject(array(
+            $object = $this->_client->getObject([
                 'Bucket' => $this->_bucket,
                 'Key'    => $this->_getKey($pasteid),
-            ));
+            ]);
             $data = $object['Body']->getContents();
             return Json::decode($data);
         } catch (S3Exception $e) {
@@ -232,10 +232,10 @@ class S3Storage extends AbstractData
             $comments = $this->_listAllObjects($name . '/discussion/');
             foreach ($comments as $comment) {
                 try {
-                    $this->_client->deleteObject(array(
+                    $this->_client->deleteObject([
                         'Bucket' => $this->_bucket,
                         'Key'    => $comment['Key'],
-                    ));
+                    ]);
                 } catch (S3Exception $e) {
                     // ignore if already deleted.
                 }
@@ -245,10 +245,10 @@ class S3Storage extends AbstractData
         }
 
         try {
-            $this->_client->deleteObject(array(
+            $this->_client->deleteObject([
                 'Bucket' => $this->_bucket,
                 'Key'    => $name,
-            ));
+            ]);
         } catch (S3Exception $e) {
             // ignore if already deleted
         }
@@ -279,15 +279,15 @@ class S3Storage extends AbstractData
      */
     public function readComments($pasteid)
     {
-        $comments = array();
+        $comments = [];
         $prefix   = $this->_getKey($pasteid) . '/discussion/';
         try {
             $entries = $this->_listAllObjects($prefix);
             foreach ($entries as $entry) {
-                $object = $this->_client->getObject(array(
+                $object = $this->_client->getObject([
                     'Bucket' => $this->_bucket,
                     'Key'    => $entry['Key'],
-                ));
+                ]);
                 $data             = $object['Body']->getContents();
                 $body             = JSON::decode($data);
                 $items            = explode('/', $entry['Key']);
@@ -328,17 +328,17 @@ class S3Storage extends AbstractData
                 if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
                     continue;
                 }
-                $head = $this->_client->headObject(array(
+                $head = $this->_client->headObject([
                     'Bucket' => $this->_bucket,
                     'Key'    => $name,
-                ));
+                ]);
                 $value = $head->get('Metadata')['value'] ?? '';
                 if (is_numeric($value) && intval($value) < $time) {
                     try {
-                        $this->_client->deleteObject(array(
+                        $this->_client->deleteObject([
                             'Bucket' => $this->_bucket,
                             'Key'    => $name,
-                        ));
+                        ]);
                     } catch (S3Exception $e) {
                         // deleted by another instance.
                     }
@@ -367,18 +367,18 @@ class S3Storage extends AbstractData
             $key = $prefix . 'config/' . $namespace . '/' . $key;
         }
 
-        $metadata = array('namespace' => $namespace);
+        $metadata = ['namespace' => $namespace];
         if ($namespace !== 'salt') {
             $metadata['value'] = strval($value);
         }
         try {
-            $this->_client->putObject(array(
+            $this->_client->putObject([
                 'Bucket'      => $this->_bucket,
                 'Key'         => $key,
                 'Body'        => $value,
                 'ContentType' => 'application/json',
                 'Metadata'    => $metadata,
-            ));
+            ]);
         } catch (S3Exception $e) {
             error_log('failed to set key ' . $key . ' to ' . $this->_bucket . ', ' .
                 trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
@@ -404,10 +404,10 @@ class S3Storage extends AbstractData
         }
 
         try {
-            $object = $this->_client->getObject(array(
+            $object = $this->_client->getObject([
                 'Bucket' => $this->_bucket,
                 'Key'    => $key,
-            ));
+            ]);
             return $object['Body']->getContents();
         } catch (S3Exception $e) {
             return '';
@@ -419,7 +419,7 @@ class S3Storage extends AbstractData
      */
     protected function _getExpiredPastes($batchsize)
     {
-        $expired = array();
+        $expired = [];
         $now     = time();
         $prefix  = $this->_prefix;
         if (!empty($prefix)) {
@@ -428,10 +428,10 @@ class S3Storage extends AbstractData
 
         try {
             foreach ($this->_listAllObjects($prefix) as $object) {
-                $head = $this->_client->headObject(array(
+                $head = $this->_client->headObject([
                     'Bucket' => $this->_bucket,
                     'Key'    => $object['Key'],
-                ));
+                ]);
                 $expire_at = $head->get('Metadata')['expire_date'] ?? '';
                 if (is_numeric($expire_at) && intval($expire_at) < $now) {
                     array_push($expired, $object['Key']);
@@ -452,7 +452,7 @@ class S3Storage extends AbstractData
      */
     public function getAllPastes()
     {
-        $pastes = array();
+        $pastes = [];
         $prefix = $this->_prefix;
         if (!empty($prefix)) {
             $prefix .= '/';

+ 0 - 38
lib/Exception/JsonException.php

@@ -1,38 +0,0 @@
-<?php declare(strict_types=1);
-/**
- * PrivateBin
- *
- * a zero-knowledge paste bin
- *
- * @link      https://github.com/PrivateBin/PrivateBin
- * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
- * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
- */
-
-namespace PrivateBin\Exception;
-
-use Exception;
-
-/**
- * JsonException
- *
- * An Exception representing JSON en- or decoding errors.
- */
-class JsonException extends Exception
-{
-    /**
-     * Exception constructor with mandatory JSON error code.
-     *
-     * @access public
-     * @param  int $code
-     */
-    public function __construct(int $code)
-    {
-        $message = 'A JSON error occurred';
-        if (function_exists('json_last_error_msg')) {
-            $message .= ': ' . json_last_error_msg();
-        }
-        $message .= ' (' . $code . ')';
-        parent::__construct($message, 90);
-    }
-}

+ 2 - 2
lib/Filter.php

@@ -46,7 +46,7 @@ class Filter
             default:
                 $unit = rtrim($matches[2], 's');
         }
-        return I18n::_(array('%d ' . $unit, '%d ' . $unit . 's'), (int) $matches[1]);
+        return I18n::_(['%d ' . $unit, '%d ' . $unit . 's'], (int) $matches[1]);
     }
 
     /**
@@ -59,7 +59,7 @@ class Filter
      */
     public static function formatHumanReadableSize($size)
     {
-        $iec = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
+        $iec = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
         $i   = 0;
         while (($size / 1000) >= 1) {
             $size = $size / 1000;

+ 5 - 5
lib/FormatV2.php

@@ -31,7 +31,7 @@ class FormatV2
      */
     public static function isValid(&$message, $isComment = false)
     {
-        $required_keys = array('adata', 'v', 'ct');
+        $required_keys = ['adata', 'v', 'ct'];
         if ($isComment) {
             $required_keys[] = 'pasteid';
             $required_keys[] = 'parentid';
@@ -106,11 +106,11 @@ class FormatV2
             return false;
         }
         // - key size
-        if (!in_array($cipherParams[3], array(128, 192, 256), true)) {
+        if (!in_array($cipherParams[3], [128, 192, 256], true)) {
             return false;
         }
         // - tag size
-        if (!in_array($cipherParams[4], array(64, 96, 128), true)) {
+        if (!in_array($cipherParams[4], [64, 96, 128], true)) {
             return false;
         }
         // - algorithm, must be AES
@@ -118,11 +118,11 @@ class FormatV2
             return false;
         }
         // - mode
-        if (!in_array($cipherParams[6], array('ctr', 'cbc', 'gcm'), true)) {
+        if (!in_array($cipherParams[6], ['ctr', 'cbc', 'gcm'], true)) {
             return false;
         }
         // - compression
-        if (!in_array($cipherParams[7], array('zlib', 'none'), true)) {
+        if (!in_array($cipherParams[7], ['zlib', 'none'], true)) {
             return false;
         }
 

+ 10 - 10
lib/I18n.php

@@ -46,7 +46,7 @@ class I18n
      * @static
      * @var    array
      */
-    protected static $_languageLabels = array();
+    protected static $_languageLabels = [];
 
     /**
      * available languages
@@ -55,7 +55,7 @@ class I18n
      * @static
      * @var    array
      */
-    protected static $_availableLanguages = array();
+    protected static $_availableLanguages = [];
 
     /**
      * path to language files
@@ -73,7 +73,7 @@ class I18n
      * @static
      * @var    array
      */
-    protected static $_translations = array();
+    protected static $_translations = [];
 
     /**
      * translate a string, alias for translate()
@@ -183,7 +183,7 @@ class I18n
 
         // load translations
         if (self::$_language === 'en') {
-            self::$_translations = array();
+            self::$_translations = [];
         } else {
             $data                = file_get_contents(self::_getPath(self::$_language . '.json'));
             self::$_translations = Json::decode($data);
@@ -225,7 +225,7 @@ class I18n
      */
     public static function getBrowserLanguages()
     {
-        $languages = array();
+        $languages = [];
         if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
             $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
             foreach ($languageRanges as $languageRange) {
@@ -239,7 +239,7 @@ class I18n
                         $match[2] = (string) floatval($match[2]);
                     }
                     if (!isset($languages[$match[2]])) {
-                        $languages[$match[2]] = array();
+                        $languages[$match[2]] = [];
                     }
                     $languages[$match[2]][] = strtolower($match[1]);
                 }
@@ -272,7 +272,7 @@ class I18n
      * @throws JsonException
      * @return array
      */
-    public static function getLanguageLabels($languages = array())
+    public static function getLanguageLabels($languages = [])
     {
         $file = self::_getPath('languages.json');
         if (count(self::$_languageLabels) === 0 && is_readable($file)) {
@@ -294,7 +294,7 @@ class I18n
      */
     public static function isRtl()
     {
-        return in_array(self::$_language, array('ar', 'he'));
+        return in_array(self::$_language, ['ar', 'he']);
     }
 
     /**
@@ -402,7 +402,7 @@ class I18n
      */
     protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
     {
-        $matches = array();
+        $matches = [];
         $any     = false;
         foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
             $acceptedQuality = floatval($acceptedQuality);
@@ -419,7 +419,7 @@ class I18n
                     if ($matchingGrade > 0) {
                         $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
                         if (!isset($matches[$q])) {
-                            $matches[$q] = array();
+                            $matches[$q] = [];
                         }
                         if (!in_array($availableValue, $matches[$q])) {
                             $matches[$q][] = $availableValue;

+ 2 - 24
lib/Json.php

@@ -11,8 +11,6 @@
 
 namespace PrivateBin;
 
-use PrivateBin\Exception\JsonException;
-
 /**
  * Json
  *
@@ -31,9 +29,7 @@ class Json
      */
     public static function encode(&$input)
     {
-        $jsonString = json_encode($input);
-        self::_detectError();
-        return $jsonString;
+        return json_encode($input, JSON_THROW_ON_ERROR);
     }
 
     /**
@@ -47,24 +43,6 @@ class Json
      */
     public static function decode(&$input)
     {
-        $output = json_decode($input, true);
-        self::_detectError();
-        return $output;
-    }
-
-    /**
-     * Detects JSON errors and raises an exception if one is found
-     *
-     * @access private
-     * @static
-     * @throws JsonException
-     * @return void
-     */
-    private static function _detectError()
-    {
-        $errorCode = json_last_error();
-        if ($errorCode !== JSON_ERROR_NONE) {
-            throw new JsonException($errorCode);
-        }
+        return json_decode($input, true, 10, JSON_THROW_ON_ERROR);
     }
 }

+ 1 - 1
lib/Model/AbstractModel.php

@@ -50,7 +50,7 @@ abstract class AbstractModel
      * @access protected
      * @var array
      */
-    protected $_data = array('meta' => array());
+    protected $_data = ['meta' => []];
 
     /**
      * Configuration.

+ 5 - 5
lib/Model/Comment.php

@@ -155,14 +155,14 @@ class Comment extends AbstractModel
                 $identicon = new Identicon();
                 $pngdata   = $identicon->getImageDataUri($hmac, 16);
             } elseif ($icon === 'jdenticon') {
-                $jdenticon = new Jdenticon(array(
+                $jdenticon = new Jdenticon([
                     'hash'  => $hmac,
                     'size'  => 16,
-                    'style' => array(
+                    'style' => [
                         'backgroundColor'   => '#fff0', // fully transparent, for dark mode
                         'padding'           => 0,
-                    ),
-                ));
+                    ],
+                ]);
                 $pngdata   = $jdenticon->getImageDataUri('png');
             } elseif ($icon === 'vizhash') {
                 $vh      = new Vizhash16x16();
@@ -172,7 +172,7 @@ class Comment extends AbstractModel
             }
             if (!empty($pngdata)) {
                 if (!array_key_exists('meta', $data)) {
-                    $data['meta'] = array();
+                    $data['meta'] = [];
                 }
                 $data['meta']['icon'] = $pngdata;
             }

+ 2 - 2
lib/Persistence/TrafficLimiter.php

@@ -209,9 +209,9 @@ class TrafficLimiter extends AbstractPersistence
             }
             return true;
         }
-        throw new TranslatedException(array(
+        throw new TranslatedException([
             'Please wait %d seconds between each post.',
             self::$_limit,
-        ));
+        ]);
     }
 }

+ 4 - 4
lib/Proxy/AbstractProxy.php

@@ -11,8 +11,8 @@
 
 namespace PrivateBin\Proxy;
 
+use JsonException;
 use PrivateBin\Configuration;
-use PrivateBin\Exception\JsonException;
 use PrivateBin\Json;
 
 /**
@@ -71,14 +71,14 @@ abstract class AbstractProxy
 
         $data = file_get_contents($proxyUrl, false,
             stream_context_create(
-                array(
+                [
                     'http' => $this->_getProxyPayload($conf, $link),
-                )
+                ]
             )
         );
 
         if ($data === false) {
-            $http_response_header = $http_response_header ?? array();
+            $http_response_header = $http_response_header ?? [];
             $statusCode           = '';
             if (!empty($http_response_header) && preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $http_response_header[0], $matches)) {
                 $statusCode = $matches[1];

+ 6 - 6
lib/Proxy/ShlinkProxy.php

@@ -11,8 +11,8 @@
 
 namespace PrivateBin\Proxy;
 
+use JsonException;
 use PrivateBin\Configuration;
-use PrivateBin\Exception\JsonException;
 use PrivateBin\Json;
 
 /**
@@ -45,20 +45,20 @@ class ShlinkProxy extends AbstractProxy
     {
         $shlink_api_key = $conf->getKey('apikey', 'shlink');
 
-        $body = array(
+        $body = [
             'longUrl' => $link,
-        );
+        ];
 
         try {
-            return array(
+            return [
                 'method'  => 'POST',
                 'header'  => "Content-Type: application/json\r\n" .
                             'X-Api-Key: ' . $shlink_api_key . "\r\n",
                 'content' => Json::encode($body),
-            );
+            ];
         } catch (JsonException $e) {
             error_log('[' . get_class($this) . '] Error encoding body: ' . $e->getMessage());
-            return array();
+            return [];
         }
     }
 

+ 4 - 4
lib/Proxy/YourlsProxy.php

@@ -42,18 +42,18 @@ class YourlsProxy extends AbstractProxy
      */
     protected function _getProxyPayload(Configuration $conf, string $link): array
     {
-        return array(
+        return [
             'method'  => 'POST',
             'header'  => "Content-Type: application/x-www-form-urlencoded\r\n",
             'content' => http_build_query(
-                array(
+                [
                     'signature' => $conf->getKey('signature', 'yourls'),
                     'format'    => 'json',
                     'action'    => 'shorturl',
                     'url'       => $link,
-                )
+                ]
             ),
-        );
+        ];
     }
 
     /**

+ 10 - 10
lib/Request.php

@@ -11,7 +11,7 @@
 
 namespace PrivateBin;
 
-use PrivateBin\Exception\JsonException;
+use JsonException;
 use PrivateBin\Model\Paste;
 
 /**
@@ -64,7 +64,7 @@ class Request
      * @access private
      * @var array
      */
-    private $_params = array();
+    private $_params = [];
 
     /**
      * If we are in a JSON API context
@@ -116,21 +116,21 @@ class Request
                     // a valid JSON scalar (number, bool or string) decodes
                     // without error, but is not a usable set of parameters
                     if (!is_array($this->_params)) {
-                        $this->_params = array();
+                        $this->_params = [];
                     }
                 } catch (JsonException $e) {
                     // ignore error, $this->_params will remain empty
                 }
                 break;
             default:
-                $this->_params = filter_var_array($_GET, array(
+                $this->_params = filter_var_array($_GET, [
                     'deletetoken'      => FILTER_SANITIZE_SPECIAL_CHARS,
                     'jsonld'           => FILTER_SANITIZE_SPECIAL_CHARS,
                     'link'             => FILTER_SANITIZE_URL,
                     'pasteid'          => FILTER_SANITIZE_SPECIAL_CHARS,
                     'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS,
                     'shortenviashlink' => FILTER_SANITIZE_SPECIAL_CHARS,
-                ), false);
+                ], false);
         }
         if (
             !array_key_exists('pasteid', $this->_params) &&
@@ -180,10 +180,10 @@ class Request
      */
     public function getData()
     {
-        $data = array(
+        $data = [
             'adata' => $this->getParam('adata'),
-        );
-        $required_keys = array('v', 'ct');
+        ];
+        $required_keys = ['v', 'ct'];
         $meta          = $this->getParam('meta');
         if (empty($meta)) {
             $required_keys[] = 'pasteid';
@@ -281,7 +281,7 @@ class Request
 
         // advanced case: media type negotiation
         if (!empty($acceptHeader)) {
-            $mediaTypes = array();
+            $mediaTypes = [];
             foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) {
                 if (preg_match(
                     '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#',
@@ -296,7 +296,7 @@ class Request
                         }
                     }
                     if (!isset($mediaTypes[$match[2]])) {
-                        $mediaTypes[$match[2]] = array();
+                        $mediaTypes[$match[2]] = [];
                     }
                     $mediaTypes[$match[2]][] = strtolower($match[1]);
                 }

+ 1 - 1
lib/TemplateSwitcher.php

@@ -34,7 +34,7 @@ class TemplateSwitcher
      * @static
      * @var    array
      */
-    protected static $_availableTemplates = array();
+    protected static $_availableTemplates = [];
 
     /**
      * set available templates

+ 1 - 1
lib/View.php

@@ -26,7 +26,7 @@ class View
      * @access private
      * @var    array
      */
-    private $_variables = array();
+    private $_variables = [];
 
     /**
      * assign variables to be used inside of the template

+ 5 - 5
lib/Vizhash16x16.php

@@ -85,7 +85,7 @@ class Vizhash16x16
         $textlen = strlen($text);
 
         // We convert the hash into an array of integers.
-        $this->VALUES = array();
+        $this->VALUES = [];
         for ($i = 0; $i < $textlen; $i = $i + 2) {
             array_push($this->VALUES, hexdec(substr($text, $i, 2)));
         }
@@ -106,7 +106,7 @@ class Vizhash16x16
         if (($this->getInt() % 2) === 0) {
             $op = 'h';
         }
-        $image = $this->degrade($image, $op, array($r0, $g0, $b0), array(0, 0, 0));
+        $image = $this->degrade($image, $op, [$r0, $g0, $b0], [0, 0, 0]);
 
         for ($i = 0; $i < 7; ++$i) {
             $action = $this->getInt();
@@ -186,11 +186,11 @@ class Vizhash16x16
             $size    = imagesy($img);
             $sizeinv = imagesx($img);
         }
-        $diffs = array(
+        $diffs = [
             ($color2[0] - $color1[0]) / $size,
             ($color2[1] - $color1[1]) / $size,
             ($color2[2] - $color1[2]) / $size,
-        );
+        ];
         for ($i = 0; $i < $size; ++$i) {
             $r = $color1[0] + ((int) $diffs[0] * $i);
             $g = $color1[1] + ((int) $diffs[1] * $i);
@@ -223,7 +223,7 @@ class Vizhash16x16
                 imagefilledellipse($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
                 break;
             case 3:
-                $points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY());
+                $points = [$this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY()];
                 version_compare(PHP_VERSION, '8.1', '<') ? imagefilledpolygon($image, $points, 4, $color) : imagefilledpolygon($image, $points, $color);
                 break;
             default:

+ 0 - 714
tpl/bootstrap.php

@@ -1,714 +0,0 @@
-<?php declare(strict_types=1);
-use PrivateBin\I18n;
-$isCpct = substr($template, 9, 8) === '-compact';
-$isDark = substr($template, 9, 5) === '-dark';
-$isPage = substr($template, -5) === '-page';
-?><!DOCTYPE html>
-<html lang="<?php echo I18n::getLanguage(); ?>"<?php echo I18n::isRtl() ? ' dir="rtl"' : ''; ?>>
-	<head>
-		<meta charset="utf-8" />
-		<meta http-equiv="Content-Security-Policy" content="<?php echo I18n::encode($CSPHEADER); ?>">
-		<meta http-equiv="X-UA-Compatible" content="IE=edge">
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<meta name="robots" content="noindex" />
-		<meta name="google" content="notranslate">
-		<title><?php echo I18n::_($NAME); ?></title>
-<?php
-if (!$isDark) :
-?>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-3.4.1.css" />
-<?php
-endif;
-?>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.4.1.css" />
-<?php
-if ($isDark) :
-?>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
-<?php
-endif;
-?>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-if ($SYNTAXHIGHLIGHTING) :
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    if (!empty($SYNTAXHIGHLIGHTINGTHEME)) :
-?>
-		<link type="text/css" rel="stylesheet" href="css/prettify/<?php echo rawurlencode($SYNTAXHIGHLIGHTINGTHEME); ?>.css?<?php echo rawurlencode($VERSION); ?>" />
-<?php
-    endif;
-endif;
-?>
-		<noscript><link type="text/css" rel="stylesheet" href="css/noscript.css" /></noscript>
-		<?php $this->_linkTag('js/zlib-1.3.2.js'); ?>
-		<?php $this->_scriptTag('js/jquery-3.7.1.js', 'defer'); ?>
-<?php
-if ($QRCODE) :
-?>
-		<?php $this->_scriptTag('js/kjua-0.10.0.js', 'defer'); ?>
-<?php
-endif;
-?>
-		<?php $this->_scriptTag('js/zlib.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/base-x-5.0.1.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/bootstrap-3.4.1.js', 'defer'); ?>
-<?php
-if ($SYNTAXHIGHLIGHTING) :
-?>
-		<?php $this->_scriptTag('js/prettify.js', 'defer'); ?>
-<?php
-endif;
-if ($MARKDOWN) :
-?>
-		<?php $this->_scriptTag('js/showdown-2.1.0.js', 'defer'); ?>
-<?php
-endif;
-?>
-		<?php $this->_scriptTag('js/purify-3.4.12.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
-		<!-- icon -->
-		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />
-		<link rel="icon" type="image/png" href="img/favicon-32x32.png" sizes="32x32" />
-		<link rel="icon" type="image/png" href="img/favicon-16x16.png" sizes="16x16" />
-		<link rel="manifest" href="manifest.json?<?php echo rawurlencode($VERSION); ?>" />
-		<link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#ffcc00" />
-		<link rel="shortcut icon" href="img/favicon.ico">
-		<meta name="msapplication-config" content="browserconfig.xml">
-		<meta name="theme-color" content="#ffe57e" />
-		<!-- Twitter/social media cards -->
-		<meta name="twitter:card" content="summary" />
-		<meta name="twitter:title" content="<?php echo I18n::_('Encrypted note on %s', I18n::_($NAME)) ?>" />
-		<meta name="twitter:description" content="<?php echo I18n::_('Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.') ?>" />
-		<meta name="twitter:image" content="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" />
-		<meta property="og:title" content="<?php echo I18n::_($NAME); ?>" />
-		<meta property="og:site_name" content="<?php echo I18n::_($NAME); ?>" />
-		<meta property="og:description" content="<?php echo I18n::_('Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.') ?>" />
-		<meta property="og:image" content="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" />
-		<meta property="og:image:type" content="image/png" />
-		<meta property="og:image:width" content="180" />
-		<meta property="og:image:height" content="180" />
-	</head>
-	<body role="document" data-compression="<?php echo rawurlencode($COMPRESSION); ?>"<?php
-$class = array();
-if ($isCpct) {
-    $class[] = 'navbar-spacing';
-}
-if ($isDark) {
-    $class[] = 'dark-theme';
-}
-if (count($class)) {
-    echo ' class="', implode(' ', $class), '"';
-}
-?>>
-		<div id="passwordmodal" tabindex="-1" class="modal fade" role="dialog" aria-hidden="true">
-			<div class="modal-dialog" role="document">
-				<div class="modal-content">
-					<div class="modal-body">
-						<form id="passwordform" role="form">
-							<div class="form-group">
-								<label for="passworddecrypt"><span class="glyphicon glyphicon-eye-open"></span> <?php echo I18n::_('Please enter the password for this document:') ?></label>
-								<div class="input-group">
-									<input id="passworddecrypt" type="password" class="form-control input-password" placeholder="<?php echo I18n::_('Enter password') ?>" required="required">
-									<div class="input-group-addon toggle-password" type="button" title="<?php echo I18n::_('Show password'); ?>" aria-label="<?php echo I18n::_('Show password'); ?>">
-										<span class="glyphicon glyphicon-eye-open"></span>
-									</div>
-								</div>
-							</div>
-							<button type="submit" class="btn btn-success btn-block"><span class="glyphicon glyphicon-off"></span> <?php echo I18n::_('Decrypt') ?></button>
-						</form>
-					</div>
-				</div>
-			</div>
-		</div>
-		<div id="loadconfirmmodal" tabindex="-1" class="modal fade" role="dialog" aria-hidden="true">
-			<div class="modal-dialog" role="document">
-				<div class="modal-content">
-					<div class="modal-header">
-						<button type="button" class="close" data-dismiss="modal" aria-label="<?php echo I18n::_('Close') ?>"><span aria-hidden="true">&times;</span></button>
-						<h4 class="modal-title"><?php echo I18n::_('This secret message can only be displayed once. Would you like to see it now?') ?></h4>
-					</div>
-					<div class="modal-body text-center">
-						<button id="loadconfirm-open-now" type="button" class="btn btn-success" data-dismiss="modal"><span class="glyphicon glyphicon-download"></span> <?php echo I18n::_('Yes, see it') ?></button>
-					</div>
-				</div>
-			</div>
-		</div>
-<?php
-if ($QRCODE) :
-?>
-		<div id="qrcodemodal" tabindex="-1" class="modal fade" role="dialog" aria-hidden="true">
-			<div class="modal-dialog" role="document">
-				<div class="modal-content">
-					<div class="modal-header">
-						<button type="button" class="close" data-dismiss="modal" aria-label="<?php echo I18n::_('Close') ?>"><span aria-hidden="true">&times;</span></button>
-						<h4 class="modal-title"><?php echo I18n::_('QR code') ?></h4>
-					</div>
-					<div class="modal-body">
-						<div class="mx-auto" id="qrcode-display"></div>
-					</div>
-				</div>
-			</div>
-		</div>
-<?php
-endif;
-if ($EMAIL) :
-?>
-		<div id="emailconfirmmodal" tabindex="-1" class="modal fade" role="dialog" aria-hidden="true">
-			<div class="modal-dialog" role="document">
-				<div class="modal-content">
-					<div class="modal-header">
-						<button type="button" class="close" data-dismiss="modal" aria-label="<?php echo I18n::_('Close') ?>"><span aria-hidden="true">&times;</span></button>
-						<h4 class="modal-title"><?php echo I18n::_('Recipient may become aware of your timezone, convert time to UTC?') ?></h4>
-					</div>
-					<div class="modal-body row">
-						<div class="col-xs-12 col-md-6">
-							<button id="emailconfirm-timezone-current" type="button" class="btn btn-danger"><span class="glyphicon glyphicon-time"></span> <?php echo I18n::_('Use Current Timezone') ?></button>
-						</div>
-						<div class="col-xs-12 col-md-6 text-right">
-							<button id="emailconfirm-timezone-utc" type="button" class="btn btn-success"><span class="glyphicon glyphicon-globe"></span> <?php echo I18n::_('Convert To UTC') ?></button>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-<?php
-endif;
-?>
-		<nav class="navbar navbar-<?php echo $isDark ? 'inverse' : 'default'; ?> navbar-<?php echo $isCpct ? 'fixed' : 'static'; ?>-top"><?php
-if ($isCpct) :
-?><div class="container"><?php
-endif;
-?>
-			<div class="navbar-header">
-				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-					<span class="sr-only"><?php echo I18n::_('Toggle navigation'); ?></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-					<span class="icon-bar"></span>
-				</button>
-				<a class="reloadlink navbar-brand" href="">
-					<img alt="<?php echo I18n::_($NAME); ?>" src="img/icon.svg" width="38" />
-				</a>
-			</div>
-			<div id="navbar" class="navbar-collapse collapse">
-				<ul class="nav navbar-nav">
-					<li id="loadingindicator" class="navbar-text hidden">
-						<span class="glyphicon glyphicon-time" aria-hidden="true"></span>
-						<?php echo I18n::_('Loading…'), PHP_EOL; ?>
-					</li>
-					<li>
-						<button id="retrybutton" type="button" class="reloadlink hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> <?php echo I18n::_('Retry'), PHP_EOL; ?>
-						</button>
-					</li>
-					<li>
-<?php
-if ($isPage) :
-?>
-						<button id="sendbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Create'), PHP_EOL;
-else :
-?>
-						<button id="newbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL;
-endif;
-?>
-						</button>
-						<button id="clonebutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
-						</button>
-						<button id="rawtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
-						</button>
-						<button id="downloadtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon glyphicon-download-alt" aria-hidden="true"></span> <?php echo I18n::_('Save document'), PHP_EOL; ?>
-						</button>
-<?php
-if ($EMAIL) :
-?>
-
-						<button id="emaillink" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> <?php echo I18n::_('Email'), PHP_EOL; ?>
-						</button>
-<?php
-endif;
-if ($QRCODE) :
-?>
-						<button id="qrcodelink" type="button" data-toggle="modal" data-target="#qrcodemodal" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> <?php echo I18n::_('QR code'), PHP_EOL; ?>
-						</button>
-<?php
-endif;
-?>
-					</li>
-					<li class="dropdown">
-						<select id="pasteExpiration" name="pasteExpiration" class="hidden">
-<?php
-foreach ($EXPIRE as $key => $value) :
-?>
-							<option value="<?php echo $key; ?>"<?php
-    if ($key === $EXPIREDEFAULT) :
-?> selected="selected"<?php
-    endif;
-?>><?php echo $value; ?></option>
-<?php
-endforeach;
-?>
-						</select>
-						<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Expires'); ?>: <span id="pasteExpirationDisplay"><?php echo $EXPIRE[$EXPIREDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-foreach ($EXPIRE as $key => $value) :
-?>
-							<li>
-								<a href="#" data-expiration="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-endforeach;
-?>
-						</ul>
-					</li>
-<?php
-if ($isCpct) :
-?>
-					<li class="dropdown">
-						<a id="formatter" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Options'); ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-							<li id="burnafterreadingoption" class="checkbox hidden">
-								<label>
-									<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-    if ($BURNAFTERREADINGSELECTED) :
-?> checked="checked"<?php
-    endif;
-?> />
-									<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-								</label>
-							</li>
-<?php
-    if ($DISCUSSION) :
-?>
-							<li id="opendiscussionoption" class="checkbox hidden">
-								<label>
-									<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-        if ($OPENDISCUSSION) :
-?> checked="checked"<?php
-        endif;
-?> />
-									<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-								</label>
-							</li>
-<?php
-    endif;
-?>
-							<li role="separator" class="divider"></li>
-							<li>
-								<div>
-									<?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span>
-								</div>
-							</li>
-<?php
-    foreach ($FORMATTER as $key => $value) :
-?>
-							<li>
-								<a href="#" data-format="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-						<select id="pasteFormatter" name="pasteFormatter" class="hidden">
-<?php
-    foreach ($FORMATTER as $key => $value) :
-?>
-							<option value="<?php echo $key; ?>"<?php
-        if ($key === $FORMATTERDEFAULT) :
-?> selected="selected"<?php
-        endif;
-?>><?php echo $value; ?></option>
-<?php
-    endforeach;
-?>
-						</select>
-					</li>
-<?php
-else :
-?>
-					<li>
-						<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="burnafterreading" name="burnafterreading"<?php
-    if ($BURNAFTERREADINGSELECTED) :
-?> checked="checked"<?php
-    endif;
-?> />
-								<?php echo I18n::_('Burn after reading'), PHP_EOL; ?>
-							</label>
-						</div>
-					</li>
-<?php
-    if ($DISCUSSION) :
-?>
-					<li>
-						<div id="opendiscussionoption" class="navbar-text checkbox hidden">
-							<label>
-								<input type="checkbox" id="opendiscussion" name="opendiscussion"<?php
-        if ($OPENDISCUSSION) :
-?> checked="checked"<?php
-        endif;
-?> />
-								<?php echo I18n::_('Open discussion'), PHP_EOL; ?>
-							</label>
-						</div>
-					</li>
-<?php
-    endif;
-endif;
-if ($PASSWORD) :
-?>
-					<li>
-						<div id="password" class="navbar-form hidden">
-							<div class="input-group">
-								<input type="password" id="passwordinput" placeholder="<?php echo I18n::_('Password (recommended)'); ?>" class="form-control input-password" size="23" />
-								<div class="input-group-addon toggle-password" type="button" title="<?php echo I18n::_('Show password'); ?>" aria-label="<?php echo I18n::_('Show password'); ?>">
-									<span class="glyphicon glyphicon-eye-open"></span>
-								</div>
-							</div>
-						</div>
-					</li>
-<?php
-endif;
-if ($FILEUPLOAD) :
-?>
-					<li id="attach" class="hidden dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Attach a file'); ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-							<li id="filewrap">
-								<div>
-									<input type="file" id="file" name="file" multiple />
-								</div>
-								<div id="dragAndDropFileName" class="dragAndDropFile"><?php echo I18n::_('alternatively drag & drop a file or paste an image from the clipboard'); ?></div>
-							</li>
-							<li id="customattachment" class="hidden"></li>
-							<li>
-								<a id="fileremovebutton"  href="#">
-									<?php echo I18n::_('Remove attachment'), PHP_EOL; ?>
-								</a>
-							</li>
-						</ul>
-					</li>
-<?php
-endif;
-if (!$isCpct) :
-?>
-					<li class="dropdown">
-						<select id="pasteFormatter" name="pasteFormatter" class="hidden">
-<?php
-    foreach ($FORMATTER as $key => $value) :
-?>
-							<option value="<?php echo $key; ?>"<?php
-        if ($key === $FORMATTERDEFAULT) :
-?> selected="selected"<?php
-        endif;
-?>><?php echo $value; ?></option>
-<?php
-    endforeach;
-?>
-						</select>
-						<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Format'); ?>: <span id="pasteFormatterDisplay"><?php echo $FORMATTER[$FORMATTERDEFAULT]; ?></span> <span class="caret"></span></a>
-						<ul class="dropdown-menu">
-<?php
-    foreach ($FORMATTER as $key => $value) :
-?>
-							<li>
-								<a href="#" data-format="<?php echo $key; ?>">
-									<?php echo $value, PHP_EOL; ?>
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-				</ul>
-				<ul class="nav navbar-nav pull-right">
-<?php
-if (!empty($LANGUAGESELECTION)) :
-?>
-					<li id="language" class="dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> <?php echo $LANGUAGES[$LANGUAGESELECTION][0]; ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu dropdown-menu-right">
-<?php
-    foreach ($LANGUAGES as $key => $value) :
-?>
-							<li>
-								<a href="#" data-lang="<?php echo $key; ?>">
-									<?php echo $value[0]; ?> (<?php echo $value[1]; ?>)
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-<?php
-if (!empty($TEMPLATESELECTION)) :
-?>
-					<li id="template" class="dropdown">
-						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Theme'); ?>: <?php echo $TEMPLATESELECTION; ?> <span class="caret"></span></a>
-						<ul class="dropdown-menu dropdown-menu-right">
-<?php
-    foreach ($TEMPLATES as $value) :
-?>
-							<li>
-								<a href="#" data-template="<?php echo $value; ?>">
-									<?php echo $value; ?>
-								</a>
-							</li>
-<?php
-    endforeach;
-?>
-						</ul>
-					</li>
-<?php
-endif;
-?>
-				</ul>
-			</div>
-		<?php
-if ($isCpct) :
-?></div><?php
-endif;
-?></nav>
-		<main>
-			<section class="container">
-<?php
-if (!empty($NOTICE)) :
-?>
-				<div role="alert" class="alert alert-info">
-					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-					<?php echo I18n::encode($NOTICE), PHP_EOL; ?>
-				</div>
-<?php
-endif;
-?>
-				<div id="remainingtime" role="alert" class="hidden alert alert-info">
-					<span class="glyphicon glyphicon-fire" aria-hidden="true"></span>
-				</div>
-<?php
-if ($FILEUPLOAD) :
-?>
-				<div id="attachment" class="hidden"></div>
-<?php
-endif;
-?>
-				<div id="status" role="alert" class="clearfix alert alert-<?php echo $ISDELETED ? 'success' : 'info'; echo empty($STATUS) ? ' hidden' : '' ?>">
-					<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
-					<?php echo I18n::encode($STATUS), PHP_EOL; ?>
-<?php
-if ($ISDELETED) :
-?>
-					<button type="button" class="btn btn-default pull-right" id="new-from-alert">
-						<span class="glyphicon glyphicon-repeat"></span>
-						<?php echo I18n::_('Start over'), PHP_EOL; ?>
-					</button>
-<?php
-endif;
-?>
-				</div>
-				<div id="errormessage" role="alert" class="<?php echo empty($ERROR) ? 'hidden' : '' ?> alert alert-danger">
-					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
-					<?php echo I18n::encode($ERROR), PHP_EOL; ?>
-				</div>
-				<noscript>
-					<div id="noscript" role="alert" class="alert alert-<?php echo $isDark ? 'error' : 'warning'; ?>">
-						<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-						<?php echo I18n::_('JavaScript is required for %s to work. Sorry for the inconvenience.', I18n::_($NAME)), PHP_EOL; ?>
-					</div>
-				</noscript>
-				<div id="oldnotice" role="alert" class="hidden alert alert-danger">
-					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
-					<?php echo I18n::_('%s requires a modern browser to work.', I18n::_($NAME)), PHP_EOL; ?>
-					<a href="https://www.mozilla.org/firefox/">Firefox</a>,
-					<a href="https://www.opera.com/">Opera</a>,
-					<a href="https://www.google.com/chrome">Chrome</a>…<br />
-					<span class="small"><?php echo I18n::_('For more information <a href="%s">see this FAQ entry</a>.', 'https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-it-show-me-the-error-privatebin-requires-a-modern-browser-to-work'); ?></span>
-				</div>
-<?php
-if ($HTTPWARNING) :
-?>
-				<div id="httpnotice" role="alert" class="hidden alert alert-danger">
-					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
-					<?php echo I18n::_('This website is using an insecure connection! Please only use it for testing.'); ?><br />
-					<span class="small"><?php echo I18n::_('For more information <a href="%s">see this FAQ entry</a>.', 'https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-it-show-me-an-error-about-an-insecure-connection'); ?></span>
-				</div>
-				<div id="insecurecontextnotice" role="alert" class="hidden alert alert-danger">
-					<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>
-					<?php echo I18n::_('Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href="%s">switching to HTTPS</a>.', $HTTPSLINK), PHP_EOL; ?>
-				</div>
-<?php
-endif;
-?>
-				<div id="pastesuccess" class="hidden">
-					<div class="nav nav-justified">
-						<button id="copyLink" type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Copy link') ?>
-						</button>
-						<a href="#" id="deletelink" class="btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
-							<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
-							<span></span>
-						</a>
-					</div>
-					<div role="alert" class="alert alert-success">
-						<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
-						<div id="pastelink"></div>
-					</div>
-<?php
-if (!empty($URLSHORTENER)) :
-?>
-					<p>
-						<button id="shortenbutton" data-shortener="<?php echo I18n::encode($URLSHORTENER); ?>"
-								<?php if ($SHORTENBYDEFAULT) : ?>
-								data-autoshorten="true"
-								<?php endif; ?>
-								type="button" class="btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> btn-block"
-						>
-							<span class="glyphicon glyphicon-send" aria-hidden="true"></span> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
-						</button>
-					</p>
-					<div role="alert" class="alert alert-danger">
-						<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-						<?php if ($SHORTENBYDEFAULT) : ?>
-							<?php echo I18n::_('URL shortener is enabled by default.'), PHP_EOL; ?>
-						<?php endif; ?>
-						<?php echo I18n::_('URL shortener may expose your decrypt key in URL.'), PHP_EOL; ?>
-					</div>
-<?php
-endif;
-?>
-				</div>
-				<ul id="editorTabs" class="nav nav-tabs hidden">
-					<li role="presentation" class="active"><a role="tab" aria-selected="true" aria-controls="editorTabs" id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
-					<li role="presentation"><a role="tab" aria-selected="false" aria-controls="editorTabs" id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
-					<li role="presentation" class="pull-right">
-<?php
-if ($isPage) :
-?>
-						<button id="newbutton" type="button" class="reloadlink hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?>">
-							<span class="glyphicon glyphicon-file" aria-hidden="true"></span> <?php echo I18n::_('New'), PHP_EOL;
-else :
-?>
-						<button id="sendbutton" type="button" tabindex="2" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?>">
-							<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> <?php echo I18n::_('Create'), PHP_EOL;
-endif;
-?>
-						</button>
-					</li>
-				</ul>
-			</section>
-			<section class="container">
-				<article class="row">
-					<div id="placeholder" class="col-md-12 hidden"><?php echo I18n::_('+++ no document text +++'); ?></div>
-					<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>
-					<h5 id="copyShortcutHint" class="media col-md-12 hidden" style="margin-top: 0;">
-						<div class="media-body media-middle">
-							<small id="copyShortcutHintText" class="hidden-xs">
-								<?php
-									echo I18n::_("To copy document press on the copy button or use the clipboard shortcut <kbd>%s</kbd>+<kbd>c</kbd>", I18n::getCopyHotkey())
-								?>
-							</small>
-						</div>
-						<div class="media-right media-middle">
-							<button type="button" id="copyShortcutHintBtn" class="btn btn-default"><?php echo I18n::_('Copy'); ?></button>
-						</div>
-					</h5>
-					<div id="prettymessage" class="col-md-12 hidden">
-						<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
-					</div>
-					<div id="plaintext" class="col-md-12 hidden"></div>
-					<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" tabindex="1" class="form-control hidden"></textarea></p>
-					<p class="col-md-12 checkbox">
-						<label>
-							<input id="messagetab" type="checkbox" tabindex="3" checked="checked" />
-							<?php echo I18n::_('Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)'), PHP_EOL; ?>
-						</label>
-					</p>
-				</article>
-			</section>
-			<section class="container">
-				<div id="discussion" class="hidden">
-					<h4><?php echo I18n::_('Discussion'); ?></h4>
-					<div id="commentcontainer"></div>
-				</div>
-			</section>
-			<section class="container">
-				<div id="noscript" role="alert" class="alert alert-info noscript-hide">
-					<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
-					<?php echo I18n::_('Loading…'); ?><br />
-					<span class="small"><?php echo I18n::_('In case this message never disappears please have a look at <a href="%s">this FAQ for information to troubleshoot</a>.', 'https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-the-loading-message-not-go-away'); ?></span>
-				</div>
-			</section>
-			<footer class="container">
-				<div class="row">
-					<h4 class="col-md-5 col-xs-8"><?php echo I18n::_($NAME); ?> <small>- <?php echo I18n::_('Because ignorance is bliss'); ?></small></h4>
-					<p class="col-md-1 col-xs-4 text-center"><?php echo $VERSION; ?></p>
-					<p id="aboutbox" class="col-md-6 col-xs-12">
-						<?php echo sprintf(
-                            I18n::_('%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.',
-                                I18n::_($NAME),
-                                '%s', '%s'
-                            ),
-                            '<i>', '</i>'), ' ', $INFO, PHP_EOL;
-                        ?>
-					</p>
-				</div>
-			</footer>
-		</main>
-		<div id="serverdata" class="hidden" aria-hidden="true">
-			<div id="templates">
-				<article id="commenttemplate" class="comment">
-					<div class="commentmeta">
-						<span class="nickname">name</span>
-						<span class="commentdate">0000-00-00</span>
-					</div>
-					<div class="commentdata">c</div>
-					<button class="btn btn-default btn-sm"><?php echo I18n::_('Reply'); ?></button>
-				</article>
-				<p id="commenttailtemplate" class="comment">
-					<button class="btn btn-default btn-sm"><?php echo I18n::_('Add comment'); ?></button>
-				</p>
-				<div id="replytemplate" class="reply hidden">
-					<input type="text" id="nickname" class="form-control" title="<?php echo I18n::_('Optional nickname…'); ?>" placeholder="<?php echo I18n::_('Optional nickname…'); ?>" />
-					<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea><br />
-					<div id="replystatus" role="alert" class="statusmessage hidden alert">
-						<span class="glyphicon" aria-hidden="true"></span>
-					</div>
-					<button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button>
-				</div>
-				<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">
-					<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>
-					<a class="alert-link"><?php echo I18n::_('Download attachment'); ?><span></span></a>
-				</div>
-			</div>
-		</div>
-<?php
-if ($FILEUPLOAD) :
-?>
-		<div id="dropzone" class="hidden" tabindex="-1" aria-hidden="true"></div>
-<?php
-endif;
-?>
-	</body>
-</html>

+ 0 - 1
tpl/bootstrap5.php

@@ -26,7 +26,6 @@ endif;
 ?>
 		<noscript><link type="text/css" rel="stylesheet" href="css/noscript.css" /></noscript>
 		<?php $this->_linkTag('js/zlib-1.3.2.js'); ?>
-		<?php $this->_scriptTag('js/jquery-3.7.1.js', 'defer'); ?>
 <?php
 if ($QRCODE) :
 ?>

+ 101 - 101
tst/Bootstrap.php

@@ -47,9 +47,9 @@ class Helper
      *
      * @var array
      */
-    private static $pasteV2 = array(
-        'adata' => array(
-            array(
+    private static $pasteV2 = [
+        'adata' => [
+            [
                 'gMSNoLOk4z0RnmsYwXZ8mw==',
                 'TZO+JWuIuxs=',
                 100000,
@@ -58,17 +58,17 @@ class Helper
                 'aes',
                 'gcm',
                 'zlib',
-            ),
+            ],
             'plaintext',
             1,
             0,
-        ),
-        'meta' => array(
+        ],
+        'meta' => [
             'expire'  => '5min',
-        ),
+        ],
         'v'  => 2,
         'ct' => 'ME5JF/YBEijp2uYMzLZozbKtWc5wfy6R59NBb7SmRig=',
-    );
+    ];
 
     /**
      * example ID of a comment
@@ -82,17 +82,17 @@ class Helper
      *
      * @var array
      */
-    private static $commentV2 = array(
+    private static $commentV2 = [
         'icon'    => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
         'created' => 1344803528,
-    );
+    ];
 
     /**
      * JS files and their SRI hashes
      *
      * @var array
      */
-    private static $hashes = array();
+    private static $hashes = [];
 
     /**
      * get example paste ID
@@ -110,7 +110,7 @@ class Helper
      * @param  array $meta
      * @return array
      */
-    public static function getPaste(array $meta = array()): array
+    public static function getPaste(array $meta = []): array
     {
         $example                 = self::$pasteV2;
         $example['meta']['salt'] = ServerSalt::generate();
@@ -124,10 +124,10 @@ class Helper
      * @param  array $meta
      * @return array
      */
-    public static function getPastePost(array $meta = array()): array
+    public static function getPastePost(array $meta = []): array
     {
         $example         = self::getPaste($meta);
-        $example['meta'] = array('expire' => $example['meta']['expire']);
+        $example['meta'] = ['expire' => $example['meta']['expire']];
         return $example;
     }
 
@@ -137,7 +137,7 @@ class Helper
      * @param  array $meta
      * @return string
      */
-    public static function getPasteJson(array $meta = array()): string
+    public static function getPasteJson(array $meta = []): string
     {
         return json_encode(self::getPastePost($meta));
     }
@@ -158,7 +158,7 @@ class Helper
      * @param  array $meta
      * @return array
      */
-    public static function getComment(array $meta = array()): array
+    public static function getComment(array $meta = []): array
     {
         $example            = self::$pasteV2;
         $example['adata']   = $example['adata'][0];
@@ -319,7 +319,7 @@ class Helper
     public static function varExportMin($var, $return = false): string
     {
         if (is_array($var)) {
-            $toImplode = array();
+            $toImplode = [];
             foreach ($var as $key => $value) {
                 $toImplode[] = var_export($key, true) . ' => ' . self::varExportMin($value, true);
             }
@@ -383,12 +383,12 @@ class BucketStub extends Bucket
     private $_connection;
     private $_client;
 
-    public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null)
+    public function __construct(ConnectionInterface $connection, $name, array $info = [], $client = null)
     {
         $this->_name       = $name;
         $this->_info       = $info;
         $this->_connection = $connection;
-        $this->_objects    = array();
+        $this->_objects    = [];
         $this->_client     = $client;
     }
 
@@ -402,12 +402,12 @@ class BucketStub extends Bucket
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function exists(array $options = array())
+    public function exists(array $options = [])
     {
         return true;
     }
 
-    public function upload($data, array $options = array())
+    public function upload($data, array $options = [])
     {
         if (!is_string($data) || !array_key_exists('name', $options)) {
             throw new BadMethodCallException('not supported by this stub');
@@ -420,27 +420,27 @@ class BucketStub extends Bucket
         $o->setData($data);
     }
 
-    public function uploadAsync($data, array $options = array())
+    public function uploadAsync($data, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getResumableUploader($data, array $options = array())
+    public function getResumableUploader($data, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getStreamableUploader($data, array $options = array())
+    public function getStreamableUploader($data, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function object($name, array $options = array())
+    public function object($name, array $options = [])
     {
         return $this->_objects[$name] ?? new StorageObjectStub($this->_connection, $name, $this, null, $options);
     }
 
-    public function objects(array $options = array())
+    public function objects(array $options = [])
     {
         $prefix = $options['prefix'] ?? '';
 
@@ -452,7 +452,7 @@ class BucketStub extends Bucket
         );
     }
 
-    public function createNotification($topic, array $options = array())
+    public function createNotification($topic, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -462,32 +462,32 @@ class BucketStub extends Bucket
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function notifications(array $options = array())
+    public function notifications(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function delete(array $options = array())
+    public function delete(array $options = [])
     {
         $this->_client->deleteBucket($this->_name);
     }
 
-    public function update(array $options = array())
+    public function update(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function compose(array $sourceObjects, $name, array $options = array())
+    public function compose(array $sourceObjects, $name, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function info(array $options = array())
+    public function info(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function reload(array $options = array())
+    public function reload(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -497,12 +497,12 @@ class BucketStub extends Bucket
         return $this->_name;
     }
 
-    public static function lifecycle(array $lifecycle = array())
+    public static function lifecycle(array $lifecycle = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function currentLifecycle(array $options = array())
+    public function currentLifecycle(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -517,17 +517,17 @@ class BucketStub extends Bucket
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function lockRetentionPolicy(array $options = array())
+    public function lockRetentionPolicy(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function signedUrl($expires, array $options = array())
+    public function signedUrl($expires, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function generateSignedPostPolicyV4($objectName, $expires, array $options = array())
+    public function generateSignedPostPolicyV4($objectName, $expires, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -546,7 +546,7 @@ class StorageObjectStub extends StorageObject
     private $_exists = false;
     private $_connection;
 
-    public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null)
+    public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = [], $encryptionKey = null, $encryptionKeySHA256 = null)
     {
         $this->_name                            = $name;
         $this->_bucket                          = $bucket;
@@ -562,7 +562,7 @@ class StorageObjectStub extends StorageObject
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function exists(array $options = array())
+    public function exists(array $options = [])
     {
         return array_key_exists($this->_name, $this->_bucket->_objects);
     }
@@ -570,7 +570,7 @@ class StorageObjectStub extends StorageObject
     /**
      * @throws NotFoundException
      */
-    public function delete(array $options = array())
+    public function delete(array $options = [])
     {
         if (array_key_exists($this->_name, $this->_bucket->_objects)) {
             unset($this->_bucket->_objects[$this->_name]);
@@ -582,7 +582,7 @@ class StorageObjectStub extends StorageObject
     /**
      * @throws NotFoundException
      */
-    public function update(array $metadata, array $options = array())
+    public function update(array $metadata, array $options = [])
     {
         if (!$this->_exists) {
             throw new NotFoundException('key ' . $this->_name . ' not found.');
@@ -590,17 +590,17 @@ class StorageObjectStub extends StorageObject
         $this->_info = $metadata;
     }
 
-    public function copy($destination, array $options = array())
+    public function copy($destination, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function rewrite($destination, array $options = array())
+    public function rewrite($destination, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function rename($name, array $options = array())
+    public function rename($name, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -608,7 +608,7 @@ class StorageObjectStub extends StorageObject
     /**
      * @throws NotFoundException
      */
-    public function downloadAsString(array $options = array())
+    public function downloadAsString(array $options = [])
     {
         if (!$this->_exists) {
             throw new NotFoundException('key ' . $this->_name . ' not found.');
@@ -616,42 +616,42 @@ class StorageObjectStub extends StorageObject
         return $this->_data;
     }
 
-    public function downloadToFile($path, array $options = array())
+    public function downloadToFile($path, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function downloadAsStream(array $options = array())
+    public function downloadAsStream(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function downloadAsStreamAsync(array $options = array())
+    public function downloadAsStreamAsync(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function signedUrl($expires, array $options = array())
+    public function signedUrl($expires, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function signedUploadUrl($expires, array $options = array())
+    public function signedUploadUrl($expires, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function beginSignedUploadSession(array $options = array())
+    public function beginSignedUploadSession(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function info(array $options = array())
+    public function info(array $options = [])
     {
-        return $this->_info['metadata'] ?? array();
+        return $this->_info['metadata'] ?? [];
     }
 
-    public function reload(array $options = array())
+    public function reload(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -687,52 +687,52 @@ class StorageObjectStub extends StorageObject
  */
 class ConnectionInterfaceStub implements ConnectionInterface
 {
-    public function deleteAcl(array $args = array())
+    public function deleteAcl(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getAcl(array $args = array())
+    public function getAcl(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function listAcl(array $args = array())
+    public function listAcl(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function insertAcl(array $args = array())
+    public function insertAcl(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function patchAcl(array $args = array())
+    public function patchAcl(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function deleteBucket(array $args = array())
+    public function deleteBucket(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function restoreBucket(array $args = array())
+    public function restoreBucket(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getBucket(array $args = array())
+    public function getBucket(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function listBuckets(array $args = array())
+    public function listBuckets(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function insertBucket(array $args = array())
+    public function insertBucket(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -752,122 +752,122 @@ class ConnectionInterfaceStub implements ConnectionInterface
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function patchBucket(array $args = array())
+    public function patchBucket(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function deleteObject(array $args = array())
+    public function deleteObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function restoreObject(array $args = array())
+    public function restoreObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function copyObject(array $args = array())
+    public function copyObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function rewriteObject(array $args = array())
+    public function rewriteObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function moveObject(array $args = array())
+    public function moveObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function composeObject(array $args = array())
+    public function composeObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getObject(array $args = array())
+    public function getObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function headObject(array $args = array()): array
+    public function headObject(array $args = []): array
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function listObjects(array $args = array())
+    public function listObjects(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function patchObject(array $args = array())
+    public function patchObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function downloadObject(array $args = array())
+    public function downloadObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function insertObject(array $args = array())
+    public function insertObject(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getNotification(array $args = array())
+    public function getNotification(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function deleteNotification(array $args = array())
+    public function deleteNotification(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function insertNotification(array $args = array())
+    public function insertNotification(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function listNotifications(array $args = array())
+    public function listNotifications(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getServiceAccount(array $args = array())
+    public function getServiceAccount(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function lockRetentionPolicy(array $args = array())
+    public function lockRetentionPolicy(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function createHmacKey(array $args = array())
+    public function createHmacKey(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function deleteHmacKey(array $args = array())
+    public function deleteHmacKey(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getHmacKey(array $args = array())
+    public function getHmacKey(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function updateHmacKey(array $args = array())
+    public function updateHmacKey(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function listHmacKeys(array $args = array())
+    public function listHmacKeys(array $args = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -880,18 +880,18 @@ class StorageClientStub extends StorageClient
 {
     private $_config         = null;
     private $_connection     = null;
-    private static $_buckets = array();
+    private static $_buckets = [];
 
-    public function __construct(array $config = array())
+    public function __construct(array $config = [])
     {
         $this->_config     = $config;
         $this->_connection =  new ConnectionInterfaceStub();
     }
 
-    public function bucket($name, $userProject = false, array $config = array())
+    public function bucket($name, $userProject = false, array $config = [])
     {
         if (!array_key_exists($name, self::$_buckets)) {
-            self::$_buckets[$name] = new BucketStub($this->_connection, $name, array(), $this);
+            self::$_buckets[$name] = new BucketStub($this->_connection, $name, [], $this);
         }
         return self::$_buckets[$name];
     }
@@ -908,7 +908,7 @@ class StorageClientStub extends StorageClient
         }
     }
 
-    public function buckets(array $options = array())
+    public function buckets(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -923,7 +923,7 @@ class StorageClientStub extends StorageClient
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function signedUrlUploader($uri, $data, array $options = array())
+    public function signedUrlUploader($uri, $data, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
@@ -933,32 +933,32 @@ class StorageClientStub extends StorageClient
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function getServiceAccount(array $options = array())
+    public function getServiceAccount(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function hmacKeys(array $options = array())
+    public function hmacKeys(array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function hmacKey($accessId, $projectId = null, array $metadata = array())
+    public function hmacKey($accessId, $projectId = null, array $metadata = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function createHmacKey($serviceAccountEmail, array $options = array())
+    public function createHmacKey($serviceAccountEmail, array $options = [])
     {
         throw new BadMethodCallException('not supported by this stub');
     }
 
-    public function createBucket($name, array $options = array())
+    public function createBucket($name, array $options = [])
     {
         if (array_key_exists($name, self::$_buckets)) {
             throw new BadRequestException('already exists');
         }
-        $b                     = new BucketStub($this->_connection, $name, array(), $this);
+        $b                     = new BucketStub($this->_connection, $name, [], $this);
         self::$_buckets[$name] = $b;
         return $b;
     }

+ 10 - 10
tst/ControllerTest.php

@@ -18,7 +18,7 @@ class ControllerTest extends TestCase
     {
         /* Setup Routine */
         $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_data  = new Filesystem(array('dir' => $this->_path));
+        $this->_data  = new Filesystem(['dir' => $this->_path]);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();
@@ -34,9 +34,9 @@ class ControllerTest extends TestCase
 
     public function reset()
     {
-        $_POST   = array();
-        $_GET    = array();
-        $_SERVER = array();
+        $_POST   = [];
+        $_GET    = [];
+        $_SERVER = [];
         if ($this->_data->exists(Helper::getPasteId())) {
             $this->_data->delete(Helper::getPasteId());
         }
@@ -202,7 +202,7 @@ class ControllerTest extends TestCase
         $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
-        $paste = Helper::getPasteJson(array('expire' => 25));
+        $paste = Helper::getPasteJson(['expire' => 25]);
         $file  = Helper::createTempFile();
         file_put_contents($file, $paste);
         Request::setInputStream($file);
@@ -381,7 +381,7 @@ class ControllerTest extends TestCase
         $options                     = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
-        $paste = Helper::getPasteJson(array('expire' => 'foo'));
+        $paste = Helper::getPasteJson(['expire' => 'foo']);
         $file  = Helper::createTempFile();
         file_put_contents($file, $paste);
         Request::setInputStream($file);
@@ -698,7 +698,7 @@ class ControllerTest extends TestCase
      */
     public function testReadExpired()
     {
-        $expiredPaste = Helper::getPaste(array('expire_date' => 1344803344));
+        $expiredPaste = Helper::getPaste(['expire_date' => 1344803344]);
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
         $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_GET[Helper::getPasteId()]       = '';
@@ -882,9 +882,9 @@ class ControllerTest extends TestCase
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $file  = Helper::createTempFile();
-        file_put_contents($file, json_encode(array(
+        file_put_contents($file, json_encode([
             'deletetoken' => 'burnafterreading',
-        )));
+        ]));
         Request::setInputStream($file);
         $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_GET[Helper::getPasteId()]       = '';
@@ -904,7 +904,7 @@ class ControllerTest extends TestCase
      */
     public function testDeleteExpired()
     {
-        $expiredPaste = Helper::getPaste(array('expire_date' => 1000));
+        $expiredPaste = Helper::getPaste(['expire_date' => 1000]);
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste does not exist before being created');
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');

+ 6 - 6
tst/ControllerWithDbTest.php

@@ -8,14 +8,14 @@ require_once 'ControllerTest.php';
 
 class ControllerWithDbTest extends ControllerTest
 {
-    private $_options = array(
+    private $_options = [
         'usr' => null,
         'pwd' => null,
-        'opt' => array(
+        'opt' => [
             PDO::ATTR_ERRMODE    => PDO::ERRMODE_EXCEPTION,
             PDO::ATTR_PERSISTENT => true,
-        ),
-    );
+        ],
+    ];
 
     public function setUp(): void
     {
@@ -36,9 +36,9 @@ class ControllerWithDbTest extends ControllerTest
         parent::reset();
         // but then inject a db config
         $options          = parse_ini_file(CONF, true);
-        $options['model'] = array(
+        $options['model'] = [
             'class' => 'Database',
-        );
+        ];
         $options['model_options'] = $this->_options;
         Helper::createIniFile(CONF, $options);
     }

+ 7 - 7
tst/ControllerWithGcsTest.php

@@ -12,11 +12,11 @@ class ControllerWithGcsTest extends ControllerTest
 {
     private static $_client;
     private static $_bucket;
-    private $_options = array();
+    private $_options = [];
 
     public static function setUpBeforeClass(): void
     {
-        $httpClient = new Client(array('debug'=>false));
+        $httpClient = new Client(['debug'=>false]);
         $handler    = HttpHandlerFactory::build($httpClient);
 
         $name     = 'pb-';
@@ -24,7 +24,7 @@ class ControllerWithGcsTest extends ControllerTest
         for ($i = 0; $i < 29; ++$i) {
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
         }
-        self::$_client = new StorageClientStub(array());
+        self::$_client = new StorageClientStub([]);
         self::$_bucket = self::$_client->createBucket($name);
     }
 
@@ -35,10 +35,10 @@ class ControllerWithGcsTest extends ControllerTest
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        $this->_options = array(
+        $this->_options = [
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
-        );
+        ];
         $this->_data    = new GoogleCloudStorage($this->_options);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
@@ -50,9 +50,9 @@ class ControllerWithGcsTest extends ControllerTest
         parent::reset();
         // but then inject a db config
         $options          = parse_ini_file(CONF, true);
-        $options['model'] = array(
+        $options['model'] = [
             'class' => 'GoogleCloudStorage',
-        );
+        ];
         $options['model_options'] = $this->_options;
         Helper::createIniFile(CONF, $options);
     }

+ 36 - 36
tst/Data/DatabaseTest.php

@@ -12,12 +12,12 @@ class DatabaseTest extends TestCase
 
     private $_path;
 
-    private $_options = array(
+    private $_options = [
         'dsn' => 'sqlite::memory:',
         'usr' => null,
         'pwd' => null,
-        'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-    );
+        'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+    ];
 
     public function setUp(): void
     {
@@ -36,7 +36,7 @@ class DatabaseTest extends TestCase
 
     public function testSaltMigration()
     {
-        ServerSalt::setStore(new Filesystem(array('dir' => 'data')));
+        ServerSalt::setStore(new Filesystem(['dir' => 'data']));
         $salt = ServerSalt::get();
         $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
         $this->assertFileExists($file, 'ServerSalt got initialized and stored on disk');
@@ -80,10 +80,10 @@ class DatabaseTest extends TestCase
         $comment2['parentid'] = Helper::getPasteId();
         $comment2['meta']     = $meta2;
         $this->assertEquals(
-            array(
+            [
                 $comment1['meta']['created']        => $comment1,
                 $comment2['meta']['created'] . '.1' => $comment2,
-            ),
+            ],
             $this->_model->readComments(Helper::getPasteId())
         );
 
@@ -98,7 +98,7 @@ class DatabaseTest extends TestCase
     {
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
-        $original                          = $paste                                = Helper::getPaste(array('expire_date' => 1344803344));
+        $original                          = $paste                                = Helper::getPaste(['expire_date' => 1344803344]);
         $paste['meta']['burnafterreading'] = $original['meta']['burnafterreading'] = true;
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste');
@@ -115,15 +115,15 @@ class DatabaseTest extends TestCase
     public function testPurge()
     {
         $this->_model->delete(Helper::getPasteId());
-        $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z');
-        $ids     = array();
+        $expired = Helper::getPaste(['expire_date' => 1344803344]);
+        $paste   = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys    = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids     = [];
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $this->_model->delete($ids[$key]);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
-            if (in_array($key, array('y', 'z'))) {
+            if (in_array($key, ['y', 'z'])) {
                 $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste");
             } elseif ($key === 'x') {
                 $data = Helper::getPaste();
@@ -135,7 +135,7 @@ class DatabaseTest extends TestCase
         }
         $this->_model->purge(10);
         foreach ($ids as $key => $id) {
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->exists($id), "paste $key exists after purge");
                 $this->_model->delete($id);
             } else {
@@ -148,7 +148,7 @@ class DatabaseTest extends TestCase
     {
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
-        $paste = Helper::getPaste(array('expire' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $paste = Helper::getPaste(['expire' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         ini_set('error_log', '/dev/null');
         $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste');
@@ -161,7 +161,7 @@ class DatabaseTest extends TestCase
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
         $data    = Helper::getPaste();
-        $comment = Helper::getComment(array('icon' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $comment = Helper::getComment(['icon' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $data), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');
@@ -175,64 +175,64 @@ class DatabaseTest extends TestCase
     public function testGetIbmInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'ibm:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetInformixInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'informix:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetMssqlInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'mssql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetMysqlInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'mysql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetOciInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'oci:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetPgsqlInstance()
     {
         $this->expectException(PDOException::class);
-        new Database(array(
+        new Database([
             'dsn' => 'pgsql:', 'usr' => null, 'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        ));
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ]);
     }
 
     public function testGetFooInstance()
     {
         $this->expectException(Exception::class);
         $this->expectExceptionCode(5);
-        new Database(array(
+        new Database([
             'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null,
-        ));
+        ]);
     }
 
     public function testMissingDsn()
@@ -309,7 +309,7 @@ class DatabaseTest extends TestCase
 
         // check if version number was upgraded in created configuration table
         $statement = $db->prepare('SELECT value FROM foo_config WHERE id LIKE ?');
-        $statement->execute(array('VERSION'));
+        $statement->execute(['VERSION']);
         $result = $statement->fetch(PDO::FETCH_ASSOC);
         $statement->closeCursor();
         $this->assertEquals(Controller::VERSION, $result['value']);

+ 16 - 16
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       = new Filesystem(array('dir' => $this->_path));
+        $this->_model       = new Filesystem(['dir' => $this->_path]);
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
@@ -37,7 +37,7 @@ class FilesystemTest extends TestCase
         $this->_model->delete(Helper::getPasteId());
 
         // storing pastes
-        $paste = Helper::getPaste(array('expire_date' => 1344803344));
+        $paste = Helper::getPaste(['expire_date' => 1344803344]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');
@@ -53,7 +53,7 @@ class FilesystemTest extends TestCase
         $comment['id']       = Helper::getCommentId();
         $comment['parentid'] = Helper::getPasteId();
         $this->assertEquals(
-            array($comment['meta']['created'] => $comment),
+            [$comment['meta']['created'] => $comment],
             $this->_model->readComments(Helper::getPasteId())
         );
 
@@ -67,7 +67,7 @@ class FilesystemTest extends TestCase
     public function testFileBasedAttachmentStoreWorks()
     {
         $this->_model->delete(Helper::getPasteId());
-        $original = $paste = Helper::getPaste(array('expire_date' => 1344803344));
+        $original = $paste = Helper::getPaste(['expire_date' => 1344803344]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');
@@ -81,14 +81,14 @@ class FilesystemTest extends TestCase
     public function testPurge()
     {
         mkdir($this->_path . DIRECTORY_SEPARATOR . '00', 0777, true);
-        $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z');
-        $ids     = array();
+        $expired = Helper::getPaste(['expire_date' => 1344803344]);
+        $paste   = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys    = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids     = [];
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste");
             } elseif ($key === 'x') {
                 $data = Helper::getPaste();
@@ -100,7 +100,7 @@ class FilesystemTest extends TestCase
         }
         $this->_model->purge(10);
         foreach ($ids as $key => $id) {
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->exists($id), "paste $key exists after purge");
                 $this->_model->delete($id);
             } else {
@@ -113,7 +113,7 @@ class FilesystemTest extends TestCase
     {
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
-        $paste = Helper::getPaste(array('expire' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $paste = Helper::getPaste(['expire' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         ini_set('error_log', '/dev/null');
         $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste');
@@ -127,7 +127,7 @@ class FilesystemTest extends TestCase
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
         $data    = Helper::getPaste();
-        $comment = Helper::getComment(array('icon' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $comment = Helper::getComment(['icon' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $data), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');
@@ -144,7 +144,7 @@ class FilesystemTest extends TestCase
         $paste     = Helper::getPaste();
         $comment   = Helper::getComment();
         $commentid = Helper::getCommentId();
-        $ids       = array();
+        $ids       = [];
         for ($i = 0, $max = 10; $i < $max; ++$i) {
             $dataid     = Helper::getRandomId();
             $storagedir = $this->_path . DIRECTORY_SEPARATOR . substr($dataid, 0, 2) .
@@ -178,16 +178,16 @@ class FilesystemTest extends TestCase
             $comment             = $comment;
             $comment['id']       = $commentid;
             $comment['parentid'] = $dataid;
-            $this->assertEquals($this->_model->readComments($dataid), array($comment['meta']['created'] => $comment), "comment of $dataid wasn't modified in the conversion");
+            $this->assertEquals($this->_model->readComments($dataid), [$comment['meta']['created'] => $comment], "comment of $dataid wasn't modified in the conversion");
         }
     }
 
     public function testValueFileErrorHandling()
     {
         define('VALID', 'valid content');
-        foreach (array('purge_limiter', 'salt', 'traffic_limiter') as $namespace) {
+        foreach (['purge_limiter', 'salt', 'traffic_limiter'] as $namespace) {
             file_put_contents($this->_invalidPath . DIRECTORY_SEPARATOR . $namespace . '.php', 'invalid content');
-            $model = new Filesystem(array('dir' => $this->_invalidPath));
+            $model = new Filesystem(['dir' => $this->_invalidPath]);
             ob_start(); // hide "invalid content", when file gets included
             $this->assertEquals($model->getValue($namespace), '', 'empty default value returned, invalid content ignored');
             ob_end_clean();

+ 14 - 14
tst/Data/GoogleCloudStorageTest.php

@@ -13,7 +13,7 @@ class GoogleCloudStorageTest extends TestCase
 
     public static function setUpBeforeClass(): void
     {
-        $httpClient = new Client(array('debug'=>false));
+        $httpClient = new Client(['debug'=>false]);
         $handler    = HttpHandlerFactory::build($httpClient);
 
         $name     = 'pb-';
@@ -21,17 +21,17 @@ class GoogleCloudStorageTest extends TestCase
         for ($i = 0; $i < 29; ++$i) {
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
         }
-        self::$_client = new StorageClientStub(array());
+        self::$_client = new StorageClientStub([]);
         self::$_bucket = self::$_client->createBucket($name);
     }
 
     public function setUp(): void
     {
         ini_set('error_log', stream_get_meta_data(tmpfile())['uri']);
-        $this->_model = new GoogleCloudStorage(array(
+        $this->_model = new GoogleCloudStorage([
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
-        ));
+        ]);
     }
 
     public function tearDown(): void
@@ -51,7 +51,7 @@ class GoogleCloudStorageTest extends TestCase
         $this->_model->delete(Helper::getPasteId());
 
         // storing pastes
-        $paste = Helper::getPaste(array('expire_date' => 1344803344));
+        $paste = Helper::getPaste(['expire_date' => 1344803344]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');
@@ -67,7 +67,7 @@ class GoogleCloudStorageTest extends TestCase
         $comment['id']       = Helper::getCommentId();
         $comment['parentid'] = Helper::getPasteId();
         $this->assertEquals(
-            array($comment['meta']['created'] => $comment),
+            [$comment['meta']['created'] => $comment],
             $this->_model->readComments(Helper::getPasteId())
         );
 
@@ -83,14 +83,14 @@ class GoogleCloudStorageTest extends TestCase
      */
     public function testPurge()
     {
-        $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z');
-        $ids     = array();
+        $expired = Helper::getPaste(['expire_date' => 1344803344]);
+        $paste   = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys    = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids     = [];
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste");
             } elseif ($key === 'x') {
                 $data = Helper::getPaste();
@@ -102,7 +102,7 @@ class GoogleCloudStorageTest extends TestCase
         }
         $this->_model->purge(10);
         foreach ($ids as $key => $id) {
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->exists($id), "paste $key exists after purge");
                 $this->_model->delete($id);
             } else {
@@ -114,7 +114,7 @@ class GoogleCloudStorageTest extends TestCase
     public function testErrorDetection()
     {
         $this->_model->delete(Helper::getPasteId());
-        $paste = Helper::getPaste(array('expire' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $paste = Helper::getPaste(['expire' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste');
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist');
@@ -124,7 +124,7 @@ class GoogleCloudStorageTest extends TestCase
     {
         $this->_model->delete(Helper::getPasteId());
         $data    = Helper::getPaste();
-        $comment = Helper::getComment(array('icon' => "Invalid UTF-8 sequence: \xB1\x31"));
+        $comment = Helper::getComment(['icon' => "Invalid UTF-8 sequence: \xB1\x31"]);
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $this->assertTrue($this->_model->create(Helper::getPasteId(), $data), 'store new paste');
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it');

+ 4 - 4
tst/FormatV2Test.php

@@ -82,11 +82,11 @@ class FormatV2Test extends TestCase
         $this->assertFalse(FormatV2::isValid($paste), 'non-numeric version');
 
         $paste          = Helper::getPastePost();
-        $paste['v']     = array();
+        $paste['v']     = [];
         $this->assertFalse(FormatV2::isValid($paste), 'array version');
 
         $paste          = Helper::getPastePost();
-        $paste['ct']    = array('not', 'a', 'string');
+        $paste['ct']    = ['not', 'a', 'string'];
         $this->assertFalse(FormatV2::isValid($paste), 'non-string ciphertext');
 
         $paste          = Helper::getPastePost();
@@ -98,11 +98,11 @@ class FormatV2Test extends TestCase
         $this->assertFalse(FormatV2::isValid($paste), 'non-array cipher parameters');
 
         $paste             = Helper::getPastePost();
-        $paste['adata'][0] = array();
+        $paste['adata'][0] = [];
         $this->assertFalse(FormatV2::isValid($paste), 'empty cipher parameters');
 
         $paste                = Helper::getPastePost();
-        $paste['adata'][0][0] = array();
+        $paste['adata'][0][0] = [];
         $this->assertFalse(FormatV2::isValid($paste), 'non-string iv');
 
         $comment           = Helper::getCommentPost();

+ 4 - 4
tst/I18nTest.php

@@ -8,7 +8,7 @@ class I18nMock extends I18n
 {
     public static function resetAvailableLanguages()
     {
-        self::$_availableLanguages = array();
+        self::$_availableLanguages = [];
     }
 
     public static function resetPath($path = '')
@@ -24,7 +24,7 @@ class I18nMock extends I18n
 
 class I18nTest extends TestCase
 {
-    private $_translations = array();
+    private $_translations = [];
 
     public function setUp(): void
     {
@@ -248,8 +248,8 @@ class I18nTest extends TestCase
 
     public function testMessageIdsExistInAllLanguages()
     {
-        $messageIds = array();
-        $languages  = array();
+        $messageIds = [];
+        $languages  = [];
         foreach (new DirectoryIterator(PATH . 'i18n') as $file) {
             $fileNameLength = strlen($file->getFilename());
             if ($fileNameLength === 7) {       // xx.json

+ 18 - 18
tst/JsonApiTest.php

@@ -19,12 +19,12 @@ class JsonApiTest extends TestCase
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
         }
-        $this->_model = new Filesystem(array('dir' => $this->_path));
+        $this->_model = new Filesystem(['dir' => $this->_path]);
         ServerSalt::setStore($this->_model);
 
-        $_POST   = array();
-        $_GET    = array();
-        $_SERVER = array();
+        $_POST   = [];
+        $_GET    = [];
+        $_SERVER = [];
         if ($this->_model->exists(Helper::getPasteId())) {
             $this->_model->delete(Helper::getPasteId());
         }
@@ -119,9 +119,9 @@ class JsonApiTest extends TestCase
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $paste = $this->_model->read(Helper::getPasteId());
         $file  = Helper::createTempFile();
-        file_put_contents($file, json_encode(array(
+        file_put_contents($file, json_encode([
             'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
-        )));
+        ]));
         Request::setInputStream($file);
         $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_GET[Helper::getPasteId()]       = '';
@@ -147,10 +147,10 @@ class JsonApiTest extends TestCase
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $paste = $this->_model->read(Helper::getPasteId());
         $file  = Helper::createTempFile();
-        file_put_contents($file, json_encode(array(
+        file_put_contents($file, json_encode([
             'pasteid'     => Helper::getPasteId(),
             'deletetoken' => hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']),
-        )));
+        ]));
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['REQUEST_METHOD']        = 'POST';
@@ -415,20 +415,20 @@ class JsonApiTest extends TestCase
 
     public function baseUriProvider()
     {
-        return array(
-            array('/path/shortenviayourls?'),
-            array('/path/index.php/shortenviayourls?'),
-            array('/path?shortenviayourls&'),
-        );
+        return [
+            ['/path/shortenviayourls?'],
+            ['/path/index.php/shortenviayourls?'],
+            ['/path?shortenviayourls&'],
+        ];
     }
 
     public function baseShlinkUriProvider()
     {
-        return array(
-            array('/path/shortenviashlink?'),
-            array('/path/index.php/shortenviashlink?'),
-            array('/path?shortenviashlink&'),
-        );
+        return [
+            ['/path/shortenviashlink?'],
+            ['/path/index.php/shortenviashlink?'],
+            ['/path?shortenviashlink&'],
+        ];
     }
 
     /**

+ 5 - 5
tst/MigrateTest.php

@@ -34,15 +34,15 @@ class MigrateTest extends TestCase
         $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(
+        $options['model']          = [
             'class' => 'Database',
-        );
-        $options['model_options'] = array(
+        ];
+        $options['model_options'] = [
             'dsn' => 'sqlite:' . $this->_path_instance_2 . DIRECTORY_SEPARATOR . 'test.sq3',
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [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);
     }

+ 36 - 36
tst/ModelTest.php

@@ -28,15 +28,15 @@ class ModelTest extends TestCase
         }
         $options                   = parse_ini_file(CONF_SAMPLE, true);
         $options['purge']['limit'] = 0;
-        $options['model']          = array(
+        $options['model']          = [
             'class' => 'Database',
-        );
-        $options['model_options'] = array(
+        ];
+        $options['model_options'] = [
             'dsn' => 'sqlite::memory:',
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ];
         Helper::confBackup();
         Helper::createIniFile(CONF, $options);
         ServerSalt::setStore(new Database($options['model_options']));
@@ -100,7 +100,7 @@ class ModelTest extends TestCase
         $this->_model->getPaste(Helper::getPasteId())->delete();
         $paste = $this->_model->getPaste(Helper::getPasteId());
         $this->assertFalse($paste->exists(), 'paste successfully deleted');
-        $this->assertEquals(array(), $paste->getComments(), 'comment was deleted with paste');
+        $this->assertEquals([], $paste->getComments(), 'comment was deleted with paste');
     }
 
     public function testCommentDefaults()
@@ -141,15 +141,15 @@ class ModelTest extends TestCase
         }
         $options                   = parse_ini_file(CONF_SAMPLE, true);
         $options['purge']['limit'] = 0;
-        $options['model']          = array(
+        $options['model']          = [
             'class' => 'Database',
-        );
-        $options['model_options'] = array(
+        ];
+        $options['model_options'] = [
             'dsn' => 'sqlite:' . $path,
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ];
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 
@@ -182,15 +182,15 @@ class ModelTest extends TestCase
         }
         $options                   = parse_ini_file(CONF_SAMPLE, true);
         $options['purge']['limit'] = 0;
-        $options['model']          = array(
+        $options['model']          = [
             'class' => 'Database',
-        );
-        $options['model_options'] = array(
+        ];
+        $options['model_options'] = [
             'dsn' => 'sqlite:' . $path,
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ];
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 
@@ -257,14 +257,14 @@ class ModelTest extends TestCase
         $comment->get();
         $comment->store();
 
-        $identicon = new Identicon(array(
+        $identicon = new Identicon([
             'hash'  => TrafficLimiter::getHash(),
             'size'  => 16,
-            'style' => array(
+            'style' => [
                 'backgroundColor'   => '#fff0', // fully transparent, for dark mode
                 'padding'           => 0,
-            ),
-        ));
+            ],
+        ]);
         $pngdata = $identicon->getImageDataUri('png');
         $comment = current($this->_model->getPaste(Helper::getPasteId())->get()['comments']);
         $this->assertEquals($pngdata, $comment['meta']['icon'], 'icon gets set');
@@ -371,15 +371,15 @@ class ModelTest extends TestCase
         $conf  = new Configuration;
         $store = new Database($conf->getSection('model_options'));
         $store->delete(Helper::getPasteId());
-        $expired = Helper::getPaste(array('expire_date' => 1344803344));
-        $paste   = Helper::getPaste(array('expire_date' => time() + 3600));
-        $keys    = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z');
-        $ids     = array();
+        $expired = Helper::getPaste(['expire_date' => 1344803344]);
+        $paste   = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys    = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'x', 'y', 'z'];
+        $ids     = [];
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $store->delete($ids[$key]);
             $this->assertFalse($store->exists($ids[$key]), "paste $key does not yet exist");
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($store->create($ids[$key], $paste), "store $key paste");
             } else {
                 $this->assertTrue($store->create($ids[$key], $expired), "store $key paste");
@@ -388,7 +388,7 @@ class ModelTest extends TestCase
         }
         $this->_model->purge(10);
         foreach ($ids as $key => $id) {
-            if (in_array($key, array('x', 'y', 'z'))) {
+            if (in_array($key, ['x', 'y', 'z'])) {
                 $this->assertTrue($this->_model->getPaste($id)->exists(), "paste $key exists after purge");
                 $this->_model->getPaste($id)->delete();
             } else {
@@ -402,15 +402,15 @@ class ModelTest extends TestCase
         $options                                  = parse_ini_file(CONF, true);
         $options['main']['discussiondatedisplay'] = 'false';
         $options['main']['icon']                  = 'none';
-        $options['model']                         = array(
+        $options['model']                         = [
             'class' => 'Database',
-        );
-        $options['model_options']                 = array(
+        ];
+        $options['model_options']                 = [
             'dsn' => 'sqlite::memory:',
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ];
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 
@@ -452,15 +452,15 @@ class ModelTest extends TestCase
     {
         $options                 = parse_ini_file(CONF, true);
         $options['main']['icon'] = 'vizhash';
-        $options['model']        = array(
+        $options['model']        = [
             'class' => 'Database',
-        );
-        $options['model_options'] = array(
+        ];
+        $options['model_options'] = [
             'dsn' => 'sqlite::memory:',
             'usr' => null,
             'pwd' => null,
-            'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
-        );
+            'opt' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
+        ];
         Helper::createIniFile(CONF, $options);
         $model = new Model(new Configuration);
 

+ 1 - 1
tst/Persistence/PurgeLimiterTest.php

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

+ 9 - 9
tst/Persistence/ServerSaltTest.php

@@ -22,7 +22,7 @@ class ServerSaltTest extends TestCase
             mkdir($this->_path);
         }
         ServerSalt::setStore(
-            new Filesystem(array('dir' => $this->_path))
+            new Filesystem(['dir' => $this->_path])
         );
 
         $this->_otherPath = $this->_path . DIRECTORY_SEPARATOR . 'foo';
@@ -45,17 +45,17 @@ class ServerSaltTest extends TestCase
     {
         // generating new salt
         ServerSalt::setStore(
-            new Filesystem(array('dir' => $this->_path))
+            new Filesystem(['dir' => $this->_path])
         );
         $salt = ServerSalt::get();
 
         // try setting a different path and resetting it
         ServerSalt::setStore(
-            new Filesystem(array('dir' => $this->_otherPath))
+            new Filesystem(['dir' => $this->_otherPath])
         );
         $this->assertNotEquals($salt, ServerSalt::get());
         ServerSalt::setStore(
-            new Filesystem(array('dir' => $this->_path))
+            new Filesystem(['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 = new Filesystem(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(['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 = new Filesystem(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(['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 = new Filesystem(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(['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(
-            new Filesystem(array('dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'))
+            new Filesystem(['dir' => $this->_invalidPath . DIRECTORY_SEPARATOR . 'baz'])
         );
-        $store = new Filesystem(array('dir' => $this->_invalidPath));
+        $store = new Filesystem(['dir' => $this->_invalidPath]);
         ServerSalt::setStore($store);
         $salt = ServerSalt::get();
         ServerSalt::setStore($store);

+ 1 - 1
tst/Persistence/TrafficLimiterTest.php

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

+ 6 - 6
tst/RequestTest.php

@@ -7,9 +7,9 @@ class RequestTest extends TestCase
 {
     public function reset()
     {
-        $_SERVER = array();
-        $_GET    = array();
-        $_POST   = array();
+        $_SERVER = [];
+        $_GET    = [];
+        $_POST   = [];
     }
 
     /**
@@ -134,7 +134,7 @@ class RequestTest extends TestCase
         $_SERVER['REQUEST_METHOD']        = 'POST';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['QUERY_STRING']          = $id;
-        $_GET                             = array($id => '');
+        $_GET                             = [$id => ''];
         $file                             = Helper::createTempFile();
         file_put_contents($file, '{"deletetoken":"bar"}');
         Request::setInputStream($file);
@@ -163,7 +163,7 @@ class RequestTest extends TestCase
         // a valid JSON scalar body (number, boolean or quoted string) must not
         // be treated as a set of parameters, so that it is rejected cleanly
         // instead of triggering a type error further down
-        foreach (array('1', '0.0', 'true', 'false', '"example"') as $scalar) {
+        foreach (['1', '0.0', 'true', 'false', '"example"'] as $scalar) {
             $this->reset();
             $_SERVER['REQUEST_METHOD']        = 'POST';
             $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
@@ -239,7 +239,7 @@ class RequestTest extends TestCase
         $this->reset();
         $id              = Helper::getRandomId();
         $path            = '/' . $this->getRandomQueryChars() . '/';
-        $queryParams     = array($id);
+        $queryParams     = [$id];
         $queryParamCount = random_int(1, 5);
         for ($i = 0; $i < $queryParamCount; ++$i) {
             array_push($queryParams, $this->getRandomQueryChars());

+ 6 - 25
tst/ViewTest.php

@@ -12,25 +12,25 @@ class ViewTest extends TestCase
 
     private static $is_deleted = false;
 
-    private static $formatters = array(
+    private static $formatters = [
         'plaintext'          => 'Plain Text',
         'syntaxhighlighting' => 'Source Code',
         'markdown'           => 'Markdown',
-    );
+    ];
 
     private static $formatter_default = 'plaintext';
 
-    private static $expire = array(
+    private static $expire = [
         '5min'  => '5 minutes',
         '1hour' => '1 hour',
         'never' => 'Never',
-    );
+    ];
 
     private static $expire_default = '1hour';
 
     private static $version = 'Version 1.2.3';
 
-    private $_content = array();
+    private $_content = [];
 
     public function setUp(): void
     {
@@ -65,7 +65,7 @@ class ViewTest extends TestCase
         $page->assign('HTTPSLINK', 'https://example.com/');
         $page->assign('COMPRESSION', 'zlib');
         $page->assign('CSPHEADER', 'default-src \'none\'');
-        $page->assign('SRI', array());
+        $page->assign('SRI', []);
 
         foreach (new DirectoryIterator(PATH . 'tpl') as $file) {
             if ($file->getExtension() === 'php') {
@@ -76,25 +76,6 @@ class ViewTest extends TestCase
                 ob_end_clean();
             }
         }
-        // check bootstrap variants
-        $template = 'bootstrap-page';
-        ob_start();
-        $page->draw($template);
-        $this->_content[$template] = ob_get_contents();
-        ob_end_clean();
-        foreach (array('-dark', '-compact') as $suffix) {
-            $template = 'bootstrap' . $suffix;
-            ob_start();
-            $page->draw($template);
-            $this->_content[$template] = ob_get_contents();
-            ob_end_clean();
-
-            $template .= '-page';
-            ob_start();
-            $page->draw($template);
-            $this->_content[$template] = ob_get_contents();
-            ob_end_clean();
-        }
     }
 
     public function testTemplateRendersCorrectly()

+ 1 - 1
tst/Vizhash16x16Test.php

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

+ 19 - 19
tst/YourlsProxyTest.php

@@ -73,16 +73,16 @@ class YourlsProxyTest extends TestCase
 
     public function providerInvalidUrl(): array
     {
-        return array(
-            array(''),
-            array(' '),
-            array('foo'),
-            array('https://'),
-            array('https://example.com'), // missing path and query parameter,
-            array('https://example.com/'), // missing query parameter
-            array('https://example.com?paste=something'), // missing path parameter
-            array('https://example.com@foreign.malicious.example?foo#bar'), // missing path parameter
-        );
+        return [
+            [''],
+            [' '],
+            ['foo'],
+            ['https://'],
+            ['https://example.com'], // missing path and query parameter,
+            ['https://example.com/'], // missing query parameter
+            ['https://example.com?paste=something'], // missing path parameter
+            ['https://example.com@foreign.malicious.example?foo#bar'], // missing path parameter
+        ];
     }
 
     /**
@@ -100,10 +100,10 @@ class YourlsProxyTest extends TestCase
 
     public function providerForeignUrlUsernameTrick(): array
     {
-        return array(
-            array('https://example.com@foreign.malicious.example/?foo#bar'),
-            array('https://example.com/@foreign.malicious.example?foo#bar'),
-        );
+        return [
+            ['https://example.com@foreign.malicious.example/?foo#bar'],
+            ['https://example.com/@foreign.malicious.example?foo#bar'],
+        ];
     }
 
     /**
@@ -118,11 +118,11 @@ class YourlsProxyTest extends TestCase
 
     public function providerForeignUrl(): array
     {
-        return array(
-            array('ftp://example.com/?n=np'), // wrong protocol
-            array('https://other.example.com/?foo#bar'), // wrong domain
-            array('https://other.example.com/?q=https://example.com/?foo#bar'), // domain included inside string
-        );
+        return [
+            ['ftp://example.com/?n=np'], // wrong protocol
+            ['https://other.example.com/?foo#bar'], // wrong domain
+            ['https://other.example.com/?q=https://example.com/?foo#bar'], // domain included inside string
+        ];
     }
 
     public function testYourlsError()

+ 0 - 1
vendor/composer/autoload_classmap.php

@@ -74,7 +74,6 @@ return array(
     'PrivateBin\\Data\\Filesystem' => $baseDir . '/lib/Data/Filesystem.php',
     'PrivateBin\\Data\\GoogleCloudStorage' => $baseDir . '/lib/Data/GoogleCloudStorage.php',
     'PrivateBin\\Data\\S3Storage' => $baseDir . '/lib/Data/S3Storage.php',
-    'PrivateBin\\Exception\\JsonException' => $baseDir . '/lib/Exception/JsonException.php',
     'PrivateBin\\Exception\\TranslatedException' => $baseDir . '/lib/Exception/TranslatedException.php',
     'PrivateBin\\Filter' => $baseDir . '/lib/Filter.php',
     'PrivateBin\\FormatV2' => $baseDir . '/lib/FormatV2.php',

+ 0 - 1
vendor/composer/autoload_static.php

@@ -122,7 +122,6 @@ class ComposerStaticInitDontChange
         'PrivateBin\\Data\\Filesystem' => __DIR__ . '/../..' . '/lib/Data/Filesystem.php',
         'PrivateBin\\Data\\GoogleCloudStorage' => __DIR__ . '/../..' . '/lib/Data/GoogleCloudStorage.php',
         'PrivateBin\\Data\\S3Storage' => __DIR__ . '/../..' . '/lib/Data/S3Storage.php',
-        'PrivateBin\\Exception\\JsonException' => __DIR__ . '/../..' . '/lib/Exception/JsonException.php',
         'PrivateBin\\Exception\\TranslatedException' => __DIR__ . '/../..' . '/lib/Exception/TranslatedException.php',
         'PrivateBin\\Filter' => __DIR__ . '/../..' . '/lib/Filter.php',
         'PrivateBin\\FormatV2' => __DIR__ . '/../..' . '/lib/FormatV2.php',

+ 2 - 2
vendor/composer/installed.php

@@ -3,7 +3,7 @@
         'name' => 'privatebin/privatebin',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '67baf8cca4b7aa1bc57e631b47ab561695c8ce2b',
+        'reference' => '8a77305838325d7be15b9a96e43415a6d2796489',
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -31,7 +31,7 @@
         'privatebin/privatebin' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '67baf8cca4b7aa1bc57e631b47ab561695c8ce2b',
+            'reference' => '8a77305838325d7be15b9a96e43415a6d2796489',
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

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