فهرست منبع

Merge branch 'PrivateBin:master' into fix-new-paste-default-formatter

rugk 1 هفته پیش
والد
کامیت
6965a9d5f0
100فایلهای تغییر یافته به همراه1708 افزوده شده و 786 حذف شده
  1. 1 1
      .devcontainer/devcontainer.json
  2. 1 0
      .github/copilot-instructions.md
  3. 4 4
      .github/dependabot.yml
  4. 2 2
      .github/workflows/codeql-analysis.yml
  5. 1 1
      .github/workflows/eslint.yml
  6. 1 1
      .github/workflows/snyk-scan.yml
  7. 1 1
      .github/workflows/test-results.yml
  8. 4 5
      .github/workflows/tests.yml
  9. 0 2
      .styleci.yml
  10. 17 6
      CHANGELOG.md
  11. 1 1
      CREDITS.md
  12. 2 2
      Makefile
  13. 1 1
      README.md
  14. 2 2
      SECURITY.md
  15. 1 1
      bin/migrate
  16. 1 1
      cfg/conf.sample.php
  17. 1 1
      composer.json
  18. 13 13
      composer.lock
  19. 83 0
      css/bootstrap5/privatebin.css
  20. 54 3
      doc/Installation.md
  21. 2 1
      i18n/ar.json
  22. 2 1
      i18n/bg.json
  23. 2 1
      i18n/ca.json
  24. 2 1
      i18n/co.json
  25. 2 1
      i18n/cs.json
  26. 2 1
      i18n/de.json
  27. 2 1
      i18n/el.json
  28. 2 1
      i18n/en.json
  29. 2 1
      i18n/es.json
  30. 2 1
      i18n/et.json
  31. 2 1
      i18n/fa.json
  32. 2 1
      i18n/fi.json
  33. 2 1
      i18n/fr.json
  34. 2 1
      i18n/he.json
  35. 2 1
      i18n/hi.json
  36. 2 1
      i18n/hu.json
  37. 2 1
      i18n/id.json
  38. 2 1
      i18n/it.json
  39. 2 1
      i18n/ja.json
  40. 2 1
      i18n/jbo.json
  41. 2 1
      i18n/ko.json
  42. 2 1
      i18n/ku.json
  43. 2 1
      i18n/la.json
  44. 2 1
      i18n/lt.json
  45. 2 1
      i18n/nl.json
  46. 2 1
      i18n/no.json
  47. 2 1
      i18n/oc.json
  48. 3 2
      i18n/pl.json
  49. 2 1
      i18n/pt.json
  50. 2 1
      i18n/ro.json
  51. 2 1
      i18n/ru.json
  52. 16 15
      i18n/sk.json
  53. 15 14
      i18n/sl.json
  54. 2 1
      i18n/sv.json
  55. 2 1
      i18n/th.json
  56. 2 1
      i18n/tr.json
  57. 2 1
      i18n/uk.json
  58. 2 1
      i18n/zh-tw.json
  59. 2 1
      i18n/zh.json
  60. 1 1
      js/common.js
  61. 433 209
      js/package-lock.json
  62. 7 5
      js/package.json
  63. 88 36
      js/privatebin.js
  64. 0 1
      js/purify-3.4.12.js
  65. 4 0
      js/purify-3.4.17.js
  66. 11 0
      js/test/I18n.js
  67. 282 0
      js/test/TopNav.js
  68. 17 5
      js/test/UiHelper.js
  69. 4 10
      lib/Configuration.php
  70. 10 20
      lib/Controller.php
  71. 30 25
      lib/Data/Database.php
  72. 12 12
      lib/Data/Filesystem.php
  73. 7 7
      lib/Data/GoogleCloudStorage.php
  74. 13 13
      lib/Data/S3Storage.php
  75. 1 1
      lib/Filter.php
  76. 6 1
      lib/FormatV2.php
  77. 49 21
      lib/I18n.php
  78. 1 1
      lib/Model.php
  79. 2 2
      lib/Model/AbstractModel.php
  80. 25 5
      lib/Model/Comment.php
  81. 4 4
      lib/Model/Paste.php
  82. 2 2
      lib/Persistence/PurgeLimiter.php
  83. 3 3
      lib/Persistence/TrafficLimiter.php
  84. 1 1
      lib/Proxy/AbstractProxy.php
  85. 44 8
      lib/Request.php
  86. 4 1
      lib/TemplateSwitcher.php
  87. 1 1
      lib/View.php
  88. 11 9
      lib/Vizhash16x16.php
  89. 5 5
      tpl/bootstrap5.php
  90. 1 1
      tst/AdministrationEmptyDirsTest.php
  91. 34 26
      tst/Bootstrap.php
  92. 20 20
      tst/ConfigurationTest.php
  93. 149 122
      tst/ControllerTest.php
  94. 2 2
      tst/ControllerWithDbTest.php
  95. 4 4
      tst/ControllerWithGcsTest.php
  96. 21 21
      tst/Data/DatabaseTest.php
  97. 16 16
      tst/Data/FilesystemTest.php
  98. 9 9
      tst/Data/GoogleCloudStorageTest.php
  99. 35 27
      tst/FormatV2Test.php
  100. 42 14
      tst/I18nTest.php

+ 1 - 1
.devcontainer/devcontainer.json

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

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

@@ -55,6 +55,7 @@
   - Always run after code changes to backend logic.
   - Always run after code changes to backend logic.
   - If `vendor/bin/phpunit` does not exist, ensure `composer install` completed without errors.
   - If `vendor/bin/phpunit` does not exist, ensure `composer install` completed without errors.
 
 
+- **JS Tests:**
   ```sh
   ```sh
   cd ./js
   cd ./js
   npm run test
   npm run test

+ 4 - 4
.github/dependabot.yml

@@ -12,7 +12,7 @@ updates:
     directory: "/"
     directory: "/"
     schedule:
     schedule:
       interval: "daily"
       interval: "daily"
- - package-ecosystem: "npm"
-   directory: "/js"
-   schedule:
-     interval: "daily"
+  - package-ecosystem: "npm"
+    directory: "/js"
+    schedule:
+      interval: "daily"

+ 2 - 2
.github/workflows/codeql-analysis.yml

@@ -43,7 +43,7 @@ jobs:
 
 
     # Initializes the CodeQL tools for scanning.
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v4
+      uses: github/codeql-action/init@v4.39.1
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -52,4 +52,4 @@ jobs:
         # queries: ./path/to/local/query, your-org/your-repo/queries@main
         # queries: ./path/to/local/query, your-org/your-repo/queries@main
 
 
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v4
+      uses: github/codeql-action/analyze@v4.39.1

+ 1 - 1
.github/workflows/eslint.yml

@@ -26,7 +26,7 @@ jobs:
       - name: Set up Node.js
       - name: Set up Node.js
         uses: actions/setup-node@v7
         uses: actions/setup-node@v7
         with:
         with:
-          node-version: '20'
+          node-version: '22'
 
 
       - name: Install dependencies
       - name: Install dependencies
         run: |
         run: |

+ 1 - 1
.github/workflows/snyk-scan.yml

@@ -35,6 +35,6 @@ jobs:
         with:
         with:
           args: --sarif-file-output=snyk.sarif
           args: --sarif-file-output=snyk.sarif
       - name: Upload result to GitHub Code Scanning
       - name: Upload result to GitHub Code Scanning
-        uses: github/codeql-action/upload-sarif@v4
+        uses: github/codeql-action/upload-sarif@v4.39.1
         with:
         with:
           sarif_file: snyk.sarif
           sarif_file: snyk.sarif

+ 1 - 1
.github/workflows/test-results.yml

@@ -25,7 +25,7 @@ jobs:
 
 
     steps:
     steps:
        - name: Download and Extract Artifacts
        - name: Download and Extract Artifacts
-         uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7
+         uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477
          with:
          with:
           run_id: ${{ github.event.workflow_run.id }}
           run_id: ${{ github.event.workflow_run.id }}
           path: artifacts
           path: artifacts

+ 4 - 5
.github/workflows/tests.yml

@@ -31,10 +31,9 @@ jobs:
       matrix:
       matrix:
         php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
         php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
         experimental: [false]
         experimental: [false]
-# uncomment this to start testing on development release
-#        include:
-#          - php-versions: '8.6' # development release, things can break
-#            experimental: true
+        include:
+          - php-versions: '8.6' # development release, things can break
+            experimental: true
     env:
     env:
       extensions: gd, sqlite3
       extensions: gd, sqlite3
       extensions-cache-key-name: phpextensions
       extensions-cache-key-name: phpextensions
@@ -226,7 +225,7 @@ jobs:
     - name: Setup Node
     - name: Setup Node
       uses: actions/setup-node@v7
       uses: actions/setup-node@v7
       with:
       with:
-        node-version: '20'
+        node-version: '22'
         cache: 'npm'
         cache: 'npm'
         cache-dependency-path: 'js/package-lock.json'
         cache-dependency-path: 'js/package-lock.json'
 
 

+ 0 - 2
.styleci.yml

@@ -3,7 +3,6 @@ preset: recommended
 risky: false
 risky: false
 
 
 enabled:
 enabled:
-  - align_equals
   - concat_with_spaces
   - concat_with_spaces
   - no_empty_comment
   - no_empty_comment
   - pre_increment
   - pre_increment
@@ -26,7 +25,6 @@ disabled:
   - phpdoc_summary
   - phpdoc_summary
   - post_increment
   - post_increment
   - single_line_after_imports
   - single_line_after_imports
-  - unalign_equals
 
 
 finder:
 finder:
   path:
   path:

+ 17 - 6
CHANGELOG.md

@@ -1,19 +1,30 @@
 # PrivateBin version history
 # PrivateBin version history
 
 
-## 2.1.0 (not yet released)
+## 2.1.1 (not yet released)
+* ADDED: Translation for Korean (#2071)
+* CHANGED: Database backend now logs errors when reading comments, like it already did for comment existence checks
+* CHANGED: Check `sizelimit` over full request, before attempting decoding
+* FIXED: Comments must now be unique in cipher text within a document, not reuse existing cipher texts
+* FIXED: Creating a document failed with a JavaScript error when the `discussion`, `password`, e-mail or QR code option is disabled, as the top navigation accessed the omitted elements
+* FIXED: Slovenian plural forms were stored in the wrong order, i.e. "1 uri" instead of "1 ura" for one hour (#1951)
+* FIXED: Lithuanian plural formula selected the "few" form for numbers ending in 0 above 10, i.e. "20 sekundės" instead of "20 sekundžių" (#1956)
+
+## 2.1.0 (2026-10-18)
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section
 * ADDED: Translation for Chinese (Traditional)
 * ADDED: Translation for Chinese (Traditional)
 * ADDED: Subject line to the "Email" share button's mailto link (#928)
 * ADDED: Subject line to the "Email" share button's mailto link (#928)
-* 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.
+* ADDED: Send the document with Ctrl+S, or Cmd+S on macOS (#739)
+* 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
 * CHANGED: Encode documents with `TextEncoder` in `compress()`, instead of the `utf16To8()` percent-encoding round trip, which allocated ~20 times the payload
 * CHANGED: Encode documents with `TextEncoder` in `compress()`, instead of the `utf16To8()` percent-encoding round trip, which allocated ~20 times the payload
-* CHANGED: Upgrading libraries to: ip-lib 1.22.1
+* CHANGED: Upgrading libraries to: DOMpurify 3.4.17, ip-lib 1.22.1 & polyfill-php80 1.43.0
 * FIXED: Decode decompressed documents with `TextDecoder` instead of `utf8To16()`, which allocated ~3.4 GB for a 100 MB document and killed the tab (#2027)
 * FIXED: Decode decompressed documents with `TextDecoder` instead of `utf8To16()`, which allocated ~3.4 GB for a 100 MB document and killed the tab (#2027)
 * FIXED: Prevent browsers guessing MIME types on JSON(LD) API responses (#164)
 * FIXED: Prevent browsers guessing MIME types on JSON(LD) API responses (#164)
 * FIXED: Insert quoted base path into JSON-LD documents
 * FIXED: Insert quoted base path into JSON-LD documents
 * FIXED: Several potential issues in handling comment files (#1901)
 * FIXED: Several potential issues in handling comment files (#1901)
+* FIXED: Sanitize Markdown HTML from CSS classes & styles
 
 
 ## 2.0.6 (2026-08-08)
 ## 2.0.6 (2026-08-08)
 * CHANGED: Stricter MIME type validation, divergent files get no preview and forced download link
 * CHANGED: Stricter MIME type validation, divergent files get no preview and forced download link

+ 1 - 1
CREDITS.md

@@ -39,7 +39,7 @@
 ## Translations
 ## Translations
 
 
 * Hexalyse - French
 * Hexalyse - French
-* Kuba Niewiarowski - Polish
+* Kuba Niewiarowski, Mariusz Krzaczkowski - Polish
 * Gabbalo - German
 * Gabbalo - German
 * Draky50110 - French
 * Draky50110 - French
 * Filip Muki Dobranić - Slovene
 * Filip Muki Dobranić - Slovene

+ 2 - 2
Makefile

@@ -1,7 +1,7 @@
 .PHONY: all coverage coverage-js coverage-php doc doc-js doc-php increment sign test test-js test-php help
 .PHONY: all coverage coverage-js coverage-php doc doc-js doc-php increment sign test test-js test-php help
 
 
-CURRENT_VERSION = 2.0.6
-VERSION ?= 2.0.6
+CURRENT_VERSION = 2.1.0
+VERSION ?= 2.1.1
 VERSION_FILES = README.md SECURITY.md doc/Installation.md js/package.json lib/Controller.php Makefile
 VERSION_FILES = README.md SECURITY.md doc/Installation.md js/package.json lib/Controller.php Makefile
 REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g")
 REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g")
 REGEX_VERSION := $(shell echo $(VERSION) | sed "s/\./\\\./g")
 REGEX_VERSION := $(shell echo $(VERSION) | sed "s/\./\\\./g")

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 # [![PrivateBin](https://raw.githubusercontent.com/PrivateBin/assets/master/images/preview/logoSmall.png)](https://privatebin.info/)
 # [![PrivateBin](https://raw.githubusercontent.com/PrivateBin/assets/master/images/preview/logoSmall.png)](https://privatebin.info/)
 
 
-*Current version: 2.0.6*
+*Current version: 2.1.0*
 
 
 **PrivateBin** is a minimalist, open source online
 **PrivateBin** is a minimalist, open source online
 [pastebin](https://en.wikipedia.org/wiki/Pastebin)
 [pastebin](https://en.wikipedia.org/wiki/Pastebin)

+ 2 - 2
SECURITY.md

@@ -4,8 +4,8 @@
 
 
 | Version | Supported          |
 | Version | Supported          |
 | ------- | ------------------ |
 | ------- | ------------------ |
-| 2.0.6   | :heavy_check_mark: |
-| < 2.0.6 | :x:                |
+| 2.1.0   | :heavy_check_mark: |
+| < 2.1.0 | :x:                |
 
 
 ## Reporting a Vulnerability
 ## Reporting a Vulnerability
 
 

+ 1 - 1
bin/migrate

@@ -20,7 +20,7 @@ use PrivateBin\Configuration;
 use PrivateBin\Model;
 use PrivateBin\Model;
 
 
 // third argument in getopt requires PHP >= 7.1
 // third argument in getopt requires PHP >= 7.1
-if (version_compare(PHP_VERSION, '7.1.0') < 0) {
+if (version_compare(PHP_VERSION, '7.1.0', '<')) {
     dieerr('migrate requires php 7.1 or above to work. Sorry.');
     dieerr('migrate requires php 7.1 or above to work. Sorry.');
 }
 }
 
 

+ 1 - 1
cfg/conf.sample.php

@@ -40,7 +40,7 @@ defaultformatter = "plaintext"
 ; syntaxhighlightingtheme = "sons-of-obsidian"
 ; syntaxhighlightingtheme = "sons-of-obsidian"
 
 
 ; size limit per document or comment in bytes, defaults to 10 Megabytes
 ; size limit per document or comment in bytes, defaults to 10 Megabytes
-sizelimit = 10000000
+sizelimit = 10485760
 
 
 ; by default PrivateBin use "bootstrap5" template (tpl/bootstrap5.php).
 ; by default PrivateBin use "bootstrap5" template (tpl/bootstrap5.php).
 ; Optionally you can enable the template selection menu, which uses
 ; Optionally you can enable the template selection menu, which uses

+ 1 - 1
composer.json

@@ -27,7 +27,7 @@
 		"php": "^7.4 || ^8.0",
 		"php": "^7.4 || ^8.0",
 		"jdenticon/jdenticon": "2.0.0",
 		"jdenticon/jdenticon": "2.0.0",
 		"mlocati/ip-lib": "1.22.1",
 		"mlocati/ip-lib": "1.22.1",
-		"symfony/polyfill-php80": "1.34.0",
+		"symfony/polyfill-php80": "1.43.0",
 		"yzalis/identicon": "2.0.0"
 		"yzalis/identicon": "2.0.0"
 	},
 	},
 	"suggest" : {
 	"suggest" : {

+ 13 - 13
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "527cd12822b23540702e67b958cbfdcf",
+    "content-hash": "1090fd2525b19170bbc0f0216583a2ca",
     "packages": [
     "packages": [
         {
         {
             "name": "jdenticon/jdenticon",
             "name": "jdenticon/jdenticon",
@@ -128,16 +128,16 @@
         },
         },
         {
         {
             "name": "symfony/polyfill-php80",
             "name": "symfony/polyfill-php80",
-            "version": "v1.34.0",
+            "version": "v1.43.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+                "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
-                "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
+                "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -188,7 +188,7 @@
                 "shim"
                 "shim"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.34.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -208,7 +208,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2026-04-10T16:19:22+00:00"
+            "time": "2026-09-25T15:50:05+00:00"
         },
         },
         {
         {
             "name": "yzalis/identicon",
             "name": "yzalis/identicon",
@@ -890,16 +890,16 @@
         },
         },
         {
         {
             "name": "phpunit/phpunit",
             "name": "phpunit/phpunit",
-            "version": "9.6.36",
+            "version": "9.6.37",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9"
+                "reference": "aceaa623912caf8e80bffea22fc4103a0a01565f"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/abab27ed286d3e1246fbbfe6b56bfd732d945ec9",
-                "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aceaa623912caf8e80bffea22fc4103a0a01565f",
+                "reference": "aceaa623912caf8e80bffea22fc4103a0a01565f",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -973,7 +973,7 @@
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.36"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.37"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -981,7 +981,7 @@
                     "type": "other"
                     "type": "other"
                 }
                 }
             ],
             ],
-            "time": "2026-08-11T06:25:15+00:00"
+            "time": "2026-09-23T09:59:59+00:00"
         },
         },
         {
         {
             "name": "sebastian/cli-parser",
             "name": "sebastian/cli-parser",

+ 83 - 0
css/bootstrap5/privatebin.css

@@ -60,3 +60,86 @@ html[dir="rtl"] #deletelink, html[dir="rtl"] #qrcodemodalClose {
 #sendbutton svg {
 #sendbutton svg {
 	transform: translateY(1.5px);
 	transform: translateY(1.5px);
 }
 }
+
+/* Markdown rendering */
+
+#plaintext {
+	max-width: 1050px;
+	margin: 2rem auto 5rem;
+	padding: 0 1.5rem;
+	font-size: 16px;
+	line-height: 1.7;
+}
+
+#plaintext h1 {
+	margin: 0 0 1.75rem;
+	padding-bottom: 0.65rem;
+	border-bottom: 2px solid var(--bs-border-color);
+	font-size: 2.25rem;
+}
+
+#plaintext h2 {
+	margin: 2.75rem 0 1.25rem;
+	padding-bottom: 0.4rem;
+	border-bottom: 1px solid var(--bs-border-color);
+	font-size: 1.65rem;
+}
+
+#plaintext h3 {
+	margin: 2rem 0 1rem;
+	font-size: 1.3rem;
+}
+
+#plaintext p {
+	margin-bottom: 1.15rem;
+}
+
+#plaintext pre {
+	margin: 1rem 0 1.5rem;
+	padding: 1rem 1.2rem;
+	overflow-x: auto;
+	background: var(--bs-tertiary-bg);
+	border: 1px solid var(--bs-border-color);
+	border-radius: 0.5rem;
+	box-shadow: inset 3px 0 0 var(--bs-primary);
+	line-height: 1.55;
+}
+
+#plaintext pre code {
+	padding: 0;
+	color: var(--bs-body-color);
+	background: transparent;
+	white-space: pre;
+}
+
+#plaintext :not(pre) > code {
+	padding: 0.15em 0.4em;
+	color: var(--bs-body-color);
+	background: var(--bs-tertiary-bg);
+	border: 1px solid var(--bs-border-color);
+	border-radius: 0.3rem;
+}
+
+#plaintext ul,
+#plaintext ol {
+	margin-bottom: 1.25rem;
+}
+
+#plaintext blockquote {
+	padding: 0.5rem 1rem;
+	color: var(--bs-secondary-color);
+	border-left: 4px solid var(--bs-primary);
+}
+
+#plaintext table {
+	width: 100%;
+	margin: 1.5rem 0;
+}
+
+@media (max-width: 768px) {
+	#plaintext {
+		margin-top: 1rem;
+		padding: 0 1rem;
+		font-size: 15px;
+	}
+}

+ 54 - 3
doc/Installation.md

@@ -19,6 +19,18 @@ for more information.
 
 
 - [Config Configuration - PrivateBin by @e1mo](https://galaxy.ansible.com/ui/standalone/roles/e1mo/privatebin/) ([Github source code](https://git.sr.ht/~e1mo/ansible-role-privatebin)): Deploy PrivateBin configuration to disk with a customized configuration.
 - [Config Configuration - PrivateBin by @e1mo](https://galaxy.ansible.com/ui/standalone/roles/e1mo/privatebin/) ([Github source code](https://git.sr.ht/~e1mo/ansible-role-privatebin)): Deploy PrivateBin configuration to disk with a customized configuration.
 
 
+**NOTE:** There are also container images and a Helm chart for Kubernetes:
+
+- [Container images](https://github.com/PrivateBin/docker-nginx-fpm-alpine#image-variants): Pre-installed PrivateBin on Nginx and php-fpm or [FreeUnit](https://github.com/PrivateBin/docker-unit-alpine/#image-variants), both Alpine-based, available on Docker Hub (`privatebin/…`) and GitHub (`ghcr.io/privatebin/…`), with variants for file based, database, Google Cloud and S3 storage.
+
+- [Helm chart](https://github.com/PrivateBin/helm-chart#quick-start): Deploys the container image to Kubernetes:
+
+  ```bash
+  helm repo add privatebin https://privatebin.github.io/helm-chart
+  helm repo update
+  helm install privatebin privatebin/privatebin
+  ```
+
 ### Minimal Requirements
 ### Minimal Requirements
 
 
 - PHP version 7.4 or above
 - PHP version 7.4 or above
@@ -129,10 +141,49 @@ you can also adjust an existing `robots.txt` file. More bot detection is
 implemented in JavaScript, but note none of these mechanisms is a 100% fail-safe
 implemented in JavaScript, but note none of these mechanisms is a 100% fail-safe
 way to prevent non-human visitors on your site.
 way to prevent non-human visitors on your site.
 
 
+#### Server-side User-Agent blocking
+
+Client-side checks and `robots.txt` only help after a request reaches PrivateBin
+(or when a crawler chooses to honour `robots.txt`). You can additionally reject
+obvious crawler User-Agents in the web server, before PHP runs. That reduces
+noise in logs and lowers the chance that automated clients fetch paste URLs.
+
 A `.htaccess.disabled` file is provided in the root dir of PrivateBin. It blocks
 A `.htaccess.disabled` file is provided in the root dir of PrivateBin. It blocks
 some known robots and link-scanning bots. If you use Apache, you can rename the
 some known robots and link-scanning bots. If you use Apache, you can rename the
-file to `.htaccess` to enable this feature. If you use another webserver, you
-have to configure it manually to do the same.
+file to `.htaccess` to enable this feature. The shipped rules return HTTP 403
+when the User-Agent matches patterns such as `bot`, `spider`, `crawl`, embedded
+URLs, WhatsApp, Skype preview, or `facebookexternalhit`, while still allowing
+the Let's Encrypt validation User-Agent.
+
+If you use another web server, configure an equivalent deny list manually.
+For nginx, a `map` based on `$http_user_agent` is a common approach:
+
+```nginx
+map $http_user_agent $privatebin_bad_bot {
+    default                                                 0;
+    ~*Let's Encrypt validation server                        0;
+    ~*(?:bot|spider|crawl|https?://|WhatsApp|SkypeUriPreview|facebookexternalhit) 1;
+}
+
+server {
+    # ... existing PrivateBin server block ...
+
+    if ($privatebin_bad_bot) {
+        return 403;
+    }
+}
+```
+
+Tune the patterns for your environment. Broad matches such as `bot` can block
+legitimate tools or uptime monitors that put those substrings in their
+User-Agent. Prefer keeping health checks on simple HTTPS GETs that do not need
+to execute the client-side JavaScript bot detection.
+
+Operators of the official container images can apply the same idea in the
+image web-server config (for example nginx in
+[docker-nginx-fpm-alpine](https://github.com/PrivateBin/docker-nginx-fpm-alpine)
+or the Unit config in
+[docker-unit-alpine](https://github.com/PrivateBin/docker-unit-alpine)).
 
 
 ### On using Cloudflare
 ### On using Cloudflare
 
 
@@ -206,7 +257,7 @@ CREATE INDEX parent ON prefix_comment(pasteid);
 CREATE TABLE prefix_config (
 CREATE TABLE prefix_config (
     id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id)
     id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id)
 );
 );
-INSERT INTO prefix_config VALUES('VERSION', '2.0.6');
+INSERT INTO prefix_config VALUES('VERSION', '2.1.0');
 ```
 ```
 
 
 In **PostgreSQL**, the `data`, `attachment`, `nickname` and `vizhash` columns
 In **PostgreSQL**, the `data`, `attachment`, `nickname` and `vizhash` columns

+ 2 - 1
i18n/ar.json

@@ -234,5 +234,6 @@
     "Theme": "السمة",
     "Theme": "السمة",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/bg.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/ca.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/co.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Theme": "Tema",
     "Copy": "Cupià",
     "Copy": "Cupià",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Appughjà nant’à %s+s per creà u ducumentu è piazzallu nant’à %s"
 }
 }

+ 2 - 1
i18n/cs.json

@@ -234,5 +234,6 @@
     "Theme": "Vzhled",
     "Theme": "Vzhled",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/de.json

@@ -234,5 +234,6 @@
     "Theme": "Design",
     "Theme": "Design",
     "Copy": "Kopieren",
     "Copy": "Kopieren",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Strg"
+    "Ctrl": "Strg",
+    "Hit %s+s to create the document, storing it on %s": "Drücke %s+s um das Dokument zu erstellen und es auf %s zu speichern"
 }
 }

+ 2 - 1
i18n/el.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/en.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/es.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Theme": "Tema",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/et.json

@@ -234,5 +234,6 @@
     "Theme": "Teema",
     "Theme": "Teema",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/fa.json

@@ -234,5 +234,6 @@
     "Theme": "تم",
     "Theme": "تم",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/fi.json

@@ -234,5 +234,6 @@
     "Theme": "Teema",
     "Theme": "Teema",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/fr.json

@@ -234,5 +234,6 @@
     "Theme": "Thème",
     "Theme": "Thème",
     "Copy": "Copier",
     "Copy": "Copier",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Appuyer sur %s+s pour créer le document, le stocker sur %s"
 }
 }

+ 2 - 1
i18n/he.json

@@ -234,5 +234,6 @@
     "Theme": "נושא",
     "Theme": "נושא",
     "Copy": "העתק",
     "Copy": "העתק",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/hi.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/hu.json

@@ -234,5 +234,6 @@
     "Theme": "Téma",
     "Theme": "Téma",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/id.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/it.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Theme": "Tema",
     "Copy": "Copiare",
     "Copy": "Copiare",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Premi %s+s per creare il documento, salvandolo su %s"
 }
 }

+ 2 - 1
i18n/ja.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/jbo.json

@@ -234,5 +234,6 @@
     "Theme": "jvinu",
     "Theme": "jvinu",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/ko.json

@@ -234,5 +234,6 @@
     "Theme": "테마",
     "Theme": "테마",
     "Copy": "복사",
     "Copy": "복사",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "%s+s를 눌러 문서를 생성하고 %s에 저장하세요."
 }
 }

+ 2 - 1
i18n/ku.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/la.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/lt.json

@@ -234,5 +234,6 @@
     "Theme": "Apipavidalinimas",
     "Theme": "Apipavidalinimas",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/nl.json

@@ -234,5 +234,6 @@
     "Theme": "Thema",
     "Theme": "Thema",
     "Copy": "Kopieëren",
     "Copy": "Kopieëren",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/no.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/oc.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 3 - 2
i18n/pl.json

@@ -232,7 +232,8 @@
     "Show password": "Pokaż hasło",
     "Show password": "Pokaż hasło",
     "Hide password": "Ukryj hasło",
     "Hide password": "Ukryj hasło",
     "Theme": "Motyw",
     "Theme": "Motyw",
-    "Copy": "Copy",
+    "Copy": "Kopiuj",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Naciśnij %s+s, aby utworzyć dokument i zapisać go w %s"
 }
 }

+ 2 - 1
i18n/pt.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/ro.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/ru.json

@@ -234,5 +234,6 @@
     "Theme": "Тема",
     "Theme": "Тема",
     "Copy": "Скопировать",
     "Copy": "Скопировать",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 16 - 15
i18n/sk.json

@@ -210,29 +210,30 @@
     "Encrypted note on %s": "Zašifrovaná poznámka na %s",
     "Encrypted note on %s": "Zašifrovaná poznámka na %s",
     "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Ak chcete zobraziť poznámku, navštívte tento odkaz. Poskytnutie adresy URL komukoľvek im umožní prístup aj k poznámke.",
     "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Ak chcete zobraziť poznámku, navštívte tento odkaz. Poskytnutie adresy URL komukoľvek im umožní prístup aj k poznámke.",
     "URL shortener may expose your decrypt key in URL.": "Skracovač adries URL môže odhaliť váš dešifrovací kľúč v adrese URL.",
     "URL shortener may expose your decrypt key in URL.": "Skracovač adries URL môže odhaliť váš dešifrovací kľúč v adrese URL.",
-    "URL shortener is enabled by default.": "URL shortener is enabled by default.",
+    "URL shortener is enabled by default.": "Skracovač adries URL je predvolene povolený.",
     "Save document": "Uložiť príspevok",
     "Save document": "Uložiť príspevok",
     "Your IP is not authorized to create documents.": "Vaša IP adresa nie je oprávnená vytvárať príspevky.",
     "Your IP is not authorized to create documents.": "Vaša IP adresa nie je oprávnená vytvárať príspevky.",
     "Trying to shorten a URL that isn't pointing at our instance.": "Pokúšate sa skrátiť adresu URL, ktorá neukazuje na túto inštanciu.",
     "Trying to shorten a URL that isn't pointing at our instance.": "Pokúšate sa skrátiť adresu URL, ktorá neukazuje na túto inštanciu.",
-    "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?",
+    "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Chyba proxy: Adresa URL proxy je prázdna. Môže ísť o problém s konfiguráciou, napríklad nesprávne alebo chýbajúce konfiguračné kľúče.",
+    "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Chyba servera proxy: Chyba pri analýze odpovede servera proxy. Môže ísť o problém s konfiguráciou, napríklad nesprávne alebo chýbajúce konfiguračné kľúče.",
+    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Chyba servera proxy: zlá odpoveď. Môže to byť problém s konfiguráciou, napríklad nesprávne alebo chýbajúce konfiguračné kľúče alebo dočasný výpadok.",
+    "This secret message can only be displayed once. Would you like to see it now?": "Táto tajná správa môže byť zobrazená iba raz. Chceli by ste to teraz vidieť?",
     "Yes, see it": "Áno, zobraziť",
     "Yes, see it": "Áno, zobraziť",
     "Dark Mode": "Tmavý Režim",
     "Dark Mode": "Tmavý Režim",
-    "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.",
+    "Error compressing document, due to missing WebAssembly support.": "Chyba pri komprimácii dokumentu z dôvodu chýbajúcej podpory WebAssembly.",
+    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Chyba pri dekomprimácii dokumentu, váš prehliadač nepodporuje WebAssembly. Na zobrazenie tohto dokumentu použite iný prehliadač.",
     "Start over": "Začať odznova",
     "Start over": "Začať odznova",
     "Document copied to clipboard": "Kópia vložená do schránky",
     "Document copied to clipboard": "Kópia vložená do schránky",
-    "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>",
+    "To copy document press on the copy button or use the clipboard shortcut <kbd>%s</kbd>+<kbd>c</kbd>": "Ak chcete skopírovať dokument, stlačte tlačidlo kopírovania alebo použite skratku zo schránky <kbd>%s</kbd>+<kbd>c</kbd>",
     "Copy link": "Kopírovať odkaz",
     "Copy link": "Kopírovať odkaz",
     "Link copied to clipboard": "Odkaz vložený do schránky",
     "Link copied to clipboard": "Odkaz vložený do schránky",
-    "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",
+    "Document text": "Text dokumentu",
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Kláves tabulátora slúži ako znak (pre prepnutie stlačte <kbd>Ctrl</kbd>+<kbd>m</kbd> alebo <kbd>Esc</kbd>)",
+    "Show password": "Zobraziť heslo",
+    "Hide password": "Skryť heslo",
+    "Theme": "Téma",
+    "Copy": "Kopírovať",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 15 - 14
i18n/sl.json

@@ -7,11 +7,11 @@
     "%s requires php %s or above to work. Sorry.": "Oprosti, %s za delovanje potrebuje vsaj php %s.",
     "%s requires php %s or above to work. Sorry.": "Oprosti, %s za delovanje potrebuje vsaj php %s.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "Please wait %d seconds between each post.": [
     "Please wait %d seconds between each post.": [
+        "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundo pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundo pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundi pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundi pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekunde pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekunde pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
-        "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo."
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo."
     ],
     ],
     "Document is limited to %s of encrypted data.": "Velikost prilepka je omejena na %s zakodiranih podatkov.",
     "Document is limited to %s of encrypted data.": "Velikost prilepka je omejena na %s zakodiranih podatkov.",
@@ -36,101 +36,101 @@
     "Discussion": "Razprava",
     "Discussion": "Razprava",
     "Toggle navigation": "Preklopi navigacijo",
     "Toggle navigation": "Preklopi navigacijo",
     "%d seconds": [
     "%d seconds": [
+        "%d sekund",
         "%d sekunda",
         "%d sekunda",
         "%d sekundi",
         "%d sekundi",
         "%d sekunde",
         "%d sekunde",
         "%d sekund",
         "%d sekund",
-        "%d sekund",
         "%d sekund"
         "%d sekund"
     ],
     ],
     "%d minutes": [
     "%d minutes": [
+        "%d minut",
         "%d minuta",
         "%d minuta",
         "%d minuti",
         "%d minuti",
         "%d minute",
         "%d minute",
         "%d minut",
         "%d minut",
-        "%d minut",
         "%d minut"
         "%d minut"
     ],
     ],
     "%d hours": [
     "%d hours": [
+        "%d ur",
         "%d ura",
         "%d ura",
         "%d uri",
         "%d uri",
         "%d ure",
         "%d ure",
         "%d ur",
         "%d ur",
-        "%d ur",
         "%d ur"
         "%d ur"
     ],
     ],
     "%d days": [
     "%d days": [
+        "%d dni",
         "%d dan",
         "%d dan",
         "%d dneva",
         "%d dneva",
         "%d dnevi",
         "%d dnevi",
         "%d dni",
         "%d dni",
-        "%d dni",
         "%d dni"
         "%d dni"
     ],
     ],
     "%d weeks": [
     "%d weeks": [
+        "%d tednov",
         "%d teden",
         "%d teden",
         "%d tedna",
         "%d tedna",
         "%d tedni",
         "%d tedni",
         "%d tednov",
         "%d tednov",
-        "%d tednov",
         "%d tednov"
         "%d tednov"
     ],
     ],
     "%d months": [
     "%d months": [
+        "%d mesecev",
         "%d mesec",
         "%d mesec",
         "%d meseca",
         "%d meseca",
         "%d meseci",
         "%d meseci",
         "%d mesecev",
         "%d mesecev",
-        "%d mesecev",
         "%d mesecev"
         "%d mesecev"
     ],
     ],
     "%d years": [
     "%d years": [
+        "%d let",
         "%d leto",
         "%d leto",
         "%d leti",
         "%d leti",
         "%d leta",
         "%d leta",
         "%d let",
         "%d let",
-        "%d let",
         "%d let"
         "%d let"
     ],
     ],
     "Never": "Nikoli",
     "Never": "Nikoli",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Ne pozabi: To je testna storitev: Podatki so lahko kadarkoli pobrisani. Mucki bodo umrli, če boš zlorabljala to storitev.",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Ne pozabi: To je testna storitev: Podatki so lahko kadarkoli pobrisani. Mucki bodo umrli, če boš zlorabljala to storitev.",
     "This document will expire in %d seconds.": [
     "This document will expire in %d seconds.": [
+        "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekundo.",
         "Ta dokument bo potekel čez %d sekundo.",
         "Ta dokument bo potekel čez %d sekundi.",
         "Ta dokument bo potekel čez %d sekundi.",
         "Ta dokument bo potekel čez %d sekunde.",
         "Ta dokument bo potekel čez %d sekunde.",
         "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekund.",
-        "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekund."
         "Ta dokument bo potekel čez %d sekund."
     ],
     ],
     "This document will expire in %d minutes.": [
     "This document will expire in %d minutes.": [
+        "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minuto.",
         "Ta dokument bo potekel čez %d minuto.",
         "Ta dokument bo potekel čez %d minuti.",
         "Ta dokument bo potekel čez %d minuti.",
         "Ta dokument bo potekel čez %d minute.",
         "Ta dokument bo potekel čez %d minute.",
-        "Ta dokument bo potekel čez %d minute.",
         "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minut."
         "Ta dokument bo potekel čez %d minut."
     ],
     ],
     "This document will expire in %d hours.": [
     "This document will expire in %d hours.": [
+        "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d uro.",
         "Ta dokument bo potekel čez %d uro.",
         "Ta dokument bo potekel čez %d uri.",
         "Ta dokument bo potekel čez %d uri.",
         "Ta dokument bo potekel čez %d ure.",
         "Ta dokument bo potekel čez %d ure.",
-        "Ta dokument bo potekel čez %d ure.",
         "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d ur."
         "Ta dokument bo potekel čez %d ur."
     ],
     ],
     "This document will expire in %d days.": [
     "This document will expire in %d days.": [
+        "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dan.",
         "Ta dokument bo potekel čez %d dan.",
         "Ta dokument bo potekel čez %d dneva.",
         "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.",
         "Ta dokument bo potekel čez %d dni.",
-        "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni."
         "Ta dokument bo potekel čez %d dni."
     ],
     ],
     "This document will expire in %d months.": [
     "This document will expire in %d months.": [
+        "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesec.",
         "Ta dokument bo potekel čez %d mesec.",
         "Ta dokument bo potekel čez %d meseca.",
         "Ta dokument bo potekel čez %d meseca.",
         "Ta dokument bo potekel čez %d mesece.",
         "Ta dokument bo potekel čez %d mesece.",
         "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesecev.",
-        "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesecev."
         "Ta dokument bo potekel čez %d mesecev."
     ],
     ],
     "Please enter the password for this document:": "Prosim vnesi geslo tega prilepka:",
     "Please enter the password for this document:": "Prosim vnesi geslo tega prilepka:",
@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Theme": "Tema",
     "Copy": "Kopiraj",
     "Copy": "Kopiraj",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/sv.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Theme": "Tema",
     "Copy": "Kopiera",
     "Copy": "Kopiera",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/th.json

@@ -234,5 +234,6 @@
     "Theme": "ธีม ",
     "Theme": "ธีม ",
     "Copy": "คัดลอก",
     "Copy": "คัดลอก",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/tr.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Theme": "Theme",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/uk.json

@@ -234,5 +234,6 @@
     "Theme": "Тема",
     "Theme": "Тема",
     "Copy": "Copy",
     "Copy": "Copy",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/zh-tw.json

@@ -234,5 +234,6 @@
     "Theme": "佈景主題",
     "Theme": "佈景主題",
     "Copy": "複製",
     "Copy": "複製",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 2 - 1
i18n/zh.json

@@ -234,5 +234,6 @@
     "Theme": "主题",
     "Theme": "主题",
     "Copy": "复制",
     "Copy": "复制",
     "Cmd": "Cmd",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }
 }

+ 1 - 1
js/common.js

@@ -31,7 +31,7 @@ require('./prettify');
 global.prettyPrint = window.PR ? window.PR.prettyPrint : function() {};
 global.prettyPrint = window.PR ? window.PR.prettyPrint : function() {};
 global.prettyPrintOne = window.PR ? window.PR.prettyPrintOne : function() {};
 global.prettyPrintOne = window.PR ? window.PR.prettyPrintOne : function() {};
 global.showdown = require('./showdown-2.1.0');
 global.showdown = require('./showdown-2.1.0');
-global.DOMPurify = require('./purify-3.4.12');
+global.DOMPurify = require('./purify-3.4.17');
 global.baseX = require('./base-x-5.0.1').baseX;
 global.baseX = require('./base-x-5.0.1').baseX;
 global.Legacy = require('./legacy').Legacy;
 global.Legacy = require('./legacy').Legacy;
 require('./privatebin');
 require('./privatebin');

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 433 - 209
js/package-lock.json


+ 7 - 5
js/package.json

@@ -1,21 +1,23 @@
 {
 {
   "name": "privatebin",
   "name": "privatebin",
-  "version": "2.0.6",
+  "version": "2.1.0",
   "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).",
   "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).",
   "main": "privatebin.js",
   "main": "privatebin.js",
   "directories": {
   "directories": {
     "test": "test"
     "test": "test"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@eslint/js": "^10.0.1",
     "@peculiar/webcrypto": "^1.5.0",
     "@peculiar/webcrypto": "^1.5.0",
-    "eslint": "^9.37.0",
-    "fast-check": "^4.7.0",
-    "jsdom": "^26.1.0",
+    "eslint": "^10.13.0",
+    "fast-check": "^4.10.2",
+    "globals": "^17.13.0",
+    "jsdom": "^30.1.3",
     "jsdom-global": "^3.0.2",
     "jsdom-global": "^3.0.2",
     "jshint": "^2.13.6",
     "jshint": "^2.13.6",
     "jsverify": "^0.8.3",
     "jsverify": "^0.8.3",
     "mime-types": "^3.0.2",
     "mime-types": "^3.0.2",
-    "mocha": "^12.0.0",
+    "mocha": "^12.0.3",
     "nyc": "^18.0.0"
     "nyc": "^18.0.0"
   },
   },
   "scripts": {
   "scripts": {

+ 88 - 36
js/privatebin.js

@@ -56,7 +56,7 @@ window.PrivateBin = (function () {
      */
      */
     const purifyHtmlConfig = {
     const purifyHtmlConfig = {
         ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
         ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-        ALLOWED_ATTR: ['href', 'id', 'target', 'rel', 'class', 'style'],
+        FORBID_ATTR: ['id', 'class', 'style'],
         USE_PROFILES: {
         USE_PROFILES: {
             html: true
             html: true
         }
         }
@@ -72,9 +72,8 @@ window.PrivateBin = (function () {
      */
      */
     const purifyHtmlConfigStrictSubset = {
     const purifyHtmlConfigStrictSubset = {
         ALLOWED_URI_REGEXP: purifyHtmlConfig.ALLOWED_URI_REGEXP,
         ALLOWED_URI_REGEXP: purifyHtmlConfig.ALLOWED_URI_REGEXP,
-        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
-        ALLOWED_ATTR: ['href', 'id', 'target', 'rel'],
-        ADD_ATTR: ['target', 'rel']
+        ALLOWED_TAGS: ['a'],
+        ALLOWED_ATTR: ['href']
     };
     };
 
 
     /**
     /**
@@ -89,6 +88,20 @@ window.PrivateBin = (function () {
         }
         }
     };
     };
 
 
+    /**
+     * DOMpurify settings for translated messages, which are allowed to include limited HTML.
+     *
+     * NOTE: The key {@link purifyHtmlConfig.USE_PROFILES} **must not** be included,
+     * as otherwise `USE_PROFILES` takes precedence over {@link purifyHtmlConfigStrictSubset.ALLOWED_TAGS}.
+     *
+     * @private
+     */
+    const purifyTranslationConfig = {
+        ALLOWED_URI_REGEXP: /^(?:https?:)/i, // the only URL expected here is the generated document, so only http & https
+        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
+        ALLOWED_ATTR: ['href', 'id'] // one of the messages contains an id attribute
+    };
+
     /**
     /**
      * URL fragment prefix requiring load confirmation
      * URL fragment prefix requiring load confirmation
      *
      *
@@ -665,7 +678,7 @@ window.PrivateBin = (function () {
          * @prop   {string[]}
          * @prop   {string[]}
          * @readonly
          * @readonly
          */
          */
-        const supportedLanguages = ['ar', 'bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'jbo', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ro', 'ru', 'sk', 'sl', 'sv', 'th', 'tr', 'uk', 'zh', 'zh-tw'];
+        const supportedLanguages = ['ar', 'bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'jbo', 'ko', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ro', 'ru', 'sk', 'sl', 'sv', 'th', 'tr', 'uk', 'zh', 'zh-tw'];
 
 
         /**
         /**
          * built in language
          * built in language
@@ -805,7 +818,7 @@ window.PrivateBin = (function () {
 
 
             if (containsHtml) {
             if (containsHtml) {
                 // only allow tags/attributes we actually use in translations
                 // only allow tags/attributes we actually use in translations
-                output = DOMPurify.sanitize(output, purifyHtmlConfigStrictSubset);
+                output = DOMPurify.sanitize(output, purifyTranslationConfig);
             }
             }
 
 
             // if element is given, insert translation
             // if element is given, insert translation
@@ -862,10 +875,11 @@ window.PrivateBin = (function () {
                 case 'id':
                 case 'id':
                 case 'ja':
                 case 'ja':
                 case 'jbo':
                 case 'jbo':
+                case 'ko':
                 case 'th':
                 case 'th':
                     return 0;
                     return 0;
                 case 'lt':
                 case 'lt':
-                    return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
+                    return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20)) ? 1 : 2);
                 case 'pl':
                 case 'pl':
                     return n === 1 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
                     return n === 1 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
                 case 'ro':
                 case 'ro':
@@ -1573,11 +1587,11 @@ window.PrivateBin = (function () {
          * @name   UiHelper.historyChange
          * @name   UiHelper.historyChange
          * @private
          * @private
          * @function
          * @function
-         * @param  {Event} event
+         * @param  {PopStateEvent} event
          */
          */
         function historyChange(event) {
         function historyChange(event) {
             let currentLocation = Helper.baseUri();
             let currentLocation = Helper.baseUri();
-            if (event.originalEvent.state === null && // no state object passed
+            if (event.state === null && // no state object passed
                 event.target.location.href === currentLocation && // target location is home page
                 event.target.location.href === currentLocation && // target location is home page
                 window.location.href === currentLocation // and we are not already on the home page
                 window.location.href === currentLocation // and we are not already on the home page
             ) {
             ) {
@@ -1599,22 +1613,6 @@ window.PrivateBin = (function () {
         };
         };
 
 
 
 
-        /**
-         * trigger a history (pop) state change
-         *
-         * used to test the UiHelper.historyChange private function
-         *
-         * @name   UiHelper.mockHistoryChange
-         * @function
-         * @param  {string} state   (optional) state to mock
-         */
-        me.mockHistoryChange = function (state) {
-            if (typeof state === 'undefined') {
-                state = null;
-            }
-            historyChange({ originalEvent: new PopStateEvent('popstate', { state: state }), target: window });
-        };
-
         /**
         /**
          * initialize
          * initialize
          *
          *
@@ -2595,11 +2593,13 @@ window.PrivateBin = (function () {
 
 
             if (format === 'markdown') {
             if (format === 'markdown') {
                 const converter = new showdown.Converter({
                 const converter = new showdown.Converter({
+                    disallowRawHTML: true,
+                    excludeTrailingPunctuationFromURLs: true,
+                    safeMode: true,
+                    simplifiedAutoLink: true,
                     strikethrough: true,
                     strikethrough: true,
                     tables: true,
                     tables: true,
-                    tablesHeaderId: true,
-                    simplifiedAutoLink: true,
-                    excludeTrailingPunctuationFromURLs: true
+                    tablesHeaderId: true
                 });
                 });
                 // let showdown convert the HTML and sanitize HTML *afterwards*!
                 // let showdown convert the HTML and sanitize HTML *afterwards*!
                 if (plainText) {
                 if (plainText) {
@@ -4045,6 +4045,31 @@ window.PrivateBin = (function () {
             Controller.newPaste();
             Controller.newPaste();
         }
         }
 
 
+        /**
+         * sends the document on [Ctrl]+[S] or, on macOS, [Cmd]+[S]
+         *
+         * Only acts while the send button is shown, so the browser's own
+         * "save page" still works when viewing an existing document.
+         *
+         * @name   TopNav.sendOnShortcut
+         * @private
+         * @function
+         * @param  {KeyboardEvent} event
+         */
+        function sendOnShortcut(event) {
+            // the physical S key, so non-Latin keyboard layouts work, too
+            const isSaveKey = event.code === 'KeyS';
+            // Alt is excluded because AltGr is reported as Ctrl+Alt and some
+            // layouts type letters with AltGr+S, Shift to leave "Save page as"
+            // ([Ctrl]+[Shift]+[S]) to the browser
+            const hasCorrectModifiers = (event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey;
+            const isButtonAvailable = sendButton && !sendButton.classList.contains('hidden') && !sendButton.disabled;
+            if (isSaveKey && hasCorrectModifiers && isButtonAvailable) {
+                event.preventDefault();
+                sendButton.click();
+            }
+        }
+
         /**
         /**
          * retrys some callback registered before
          * retrys some callback registered before
          *
          *
@@ -4240,7 +4265,9 @@ window.PrivateBin = (function () {
             cloneButton.classList.remove('hidden');
             cloneButton.classList.remove('hidden');
             rawTextButton.classList.remove('hidden');
             rawTextButton.classList.remove('hidden');
             downloadTextButton.classList.remove('hidden');
             downloadTextButton.classList.remove('hidden');
-            qrCodeLink.classList.remove('hidden');
+            if (qrCodeLink) {
+                qrCodeLink.classList.remove('hidden');
+            }
 
 
             viewButtonsDisplayed = true;
             viewButtonsDisplayed = true;
         };
         };
@@ -4260,7 +4287,9 @@ window.PrivateBin = (function () {
             newButton.classList.add('hidden');
             newButton.classList.add('hidden');
             rawTextButton.classList.add('hidden');
             rawTextButton.classList.add('hidden');
             downloadTextButton.classList.add('hidden');
             downloadTextButton.classList.add('hidden');
-            qrCodeLink.classList.add('hidden');
+            if (qrCodeLink) {
+                qrCodeLink.classList.add('hidden');
+            }
             me.hideEmailButton();
             me.hideEmailButton();
 
 
             viewButtonsDisplayed = false;
             viewButtonsDisplayed = false;
@@ -4295,8 +4324,12 @@ window.PrivateBin = (function () {
             expiration.classList.remove('hidden');
             expiration.classList.remove('hidden');
             formatter.classList.remove('hidden');
             formatter.classList.remove('hidden');
             newButton.classList.remove('hidden');
             newButton.classList.remove('hidden');
-            openDiscussionOption.classList.remove('hidden');
-            password.classList.remove('hidden');
+            if (openDiscussionOption) {
+                openDiscussionOption.classList.remove('hidden');
+            }
+            if (password) {
+                password.classList.remove('hidden');
+            }
             sendButton.classList.remove('hidden');
             sendButton.classList.remove('hidden');
 
 
             createButtonsDisplayed = true;
             createButtonsDisplayed = true;
@@ -4318,8 +4351,12 @@ window.PrivateBin = (function () {
             expiration.classList.add('hidden');
             expiration.classList.add('hidden');
             formatter.classList.add('hidden');
             formatter.classList.add('hidden');
             burnAfterReadingOption.classList.add('hidden');
             burnAfterReadingOption.classList.add('hidden');
-            openDiscussionOption.classList.add('hidden');
-            password.classList.add('hidden');
+            if (openDiscussionOption) {
+                openDiscussionOption.classList.add('hidden');
+            }
+            if (password) {
+                password.classList.add('hidden');
+            }
             if (attach) {
             if (attach) {
                 attach.classList.add('hidden');
                 attach.classList.add('hidden');
             }
             }
@@ -4345,6 +4382,11 @@ window.PrivateBin = (function () {
          */
          */
         me.showRetryButton = function () {
         me.showRetryButton = function () {
             retryButton.classList.remove('hidden');
             retryButton.classList.remove('hidden');
+            // The Bootstrap 5 template gives Retry its own list item. That item
+            // still takes a flex gap while only the button is hidden.
+            if (Helper.isBootstrap5()) {
+                retryButton.parentElement.classList.remove('hidden');
+            }
         };
         };
 
 
         /**
         /**
@@ -4355,6 +4397,9 @@ window.PrivateBin = (function () {
          */
          */
         me.hideRetryButton = function () {
         me.hideRetryButton = function () {
             retryButton.classList.add('hidden');
             retryButton.classList.add('hidden');
+            if (Helper.isBootstrap5()) {
+                retryButton.parentElement.classList.add('hidden');
+            }
         };
         };
 
 
         /**
         /**
@@ -4366,6 +4411,10 @@ window.PrivateBin = (function () {
          * @param {bool|undefined} optionalBurnAfterReading
          * @param {bool|undefined} optionalBurnAfterReading
          */
          */
         me.showEmailButton = function (optionalRemainingTimeInSeconds, optionalBurnAfterReading) {
         me.showEmailButton = function (optionalRemainingTimeInSeconds, optionalBurnAfterReading) {
+            if (!emailLink) {
+                return;
+            }
+
             try {
             try {
                 // we cache expiration date in closure to avoid inaccurate expiration datetime
                 // we cache expiration date in closure to avoid inaccurate expiration datetime
                 const expirationDate = Helper.calculateExpirationDate(
                 const expirationDate = Helper.calculateExpirationDate(
@@ -4438,7 +4487,9 @@ window.PrivateBin = (function () {
          * @function
          * @function
          */
          */
         me.hideQrCodeButton = function () {
         me.hideQrCodeButton = function () {
-            qrCodeLink.classList.add('hidden');
+            if (qrCodeLink) {
+                qrCodeLink.classList.add('hidden');
+            }
         };
         };
 
 
         /**
         /**
@@ -4525,7 +4576,7 @@ window.PrivateBin = (function () {
             if (openDiscussion) {
             if (openDiscussion) {
                 openDiscussion.checked = openDiscussionDefault;
                 openDiscussion.checked = openDiscussionDefault;
             }
             }
-            if (openDiscussionDefault || !burnAfterReadingDefault) {
+            if (openDiscussionOption && (openDiscussionDefault || !burnAfterReadingDefault)) {
                 openDiscussionOption.classList.remove('buttondisabled');
                 openDiscussionOption.classList.remove('buttondisabled');
             }
             }
             if (burnAfterReadingDefault || !openDiscussionDefault) {
             if (burnAfterReadingDefault || !openDiscussionDefault) {
@@ -4733,6 +4784,7 @@ window.PrivateBin = (function () {
             }
             }
             if (sendButton) {
             if (sendButton) {
                 sendButton.addEventListener('click', PasteEncrypter.sendPaste);
                 sendButton.addEventListener('click', PasteEncrypter.sendPaste);
+                document.addEventListener('keydown', sendOnShortcut);
             }
             }
             if (cloneButton) {
             if (cloneButton) {
                 cloneButton.addEventListener('click', Controller.clonePaste);
                 cloneButton.addEventListener('click', Controller.clonePaste);

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
js/purify-3.4.12.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
js/purify-3.4.17.js


+ 11 - 0
js/test/I18n.js

@@ -183,6 +183,17 @@ describe('I18n', function () {
                 }
                 }
             ));
             ));
         });
         });
+
+        it('routes Lithuanian cardinal forms by the last digit', () => {
+            PrivateBin.I18n.reset('lt');
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(1), 0);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(2), 1);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(10), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(22), 1);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(110), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(111), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(120), 2);
+        });
     });
     });
 
 
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it

+ 282 - 0
js/test/TopNav.js

@@ -10,11 +10,83 @@ function query(selector) {
 }
 }
 
 
 describe('TopNav', function () {
 describe('TopNav', function () {
+    describe('showEmailButton', function () {
+        afterEach(function () {
+            cleanup();
+        });
+
+        it('does nothing when the email button is omitted from the page', function () {
+            document.documentElement.innerHTML =
+                '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
+                'type="button">New</button></li></ul></div></nav>';
+            PrivateBin.TopNav.init();
+
+            const originalShowError = PrivateBin.Alert.showError;
+            let errorShown = false;
+            PrivateBin.Alert.showError = function () {
+                errorShown = true;
+            };
+
+            try {
+                assert.doesNotThrow(() => PrivateBin.TopNav.showEmailButton());
+                assert.strictEqual(errorShown, false);
+            } finally {
+                PrivateBin.Alert.showError = originalShowError;
+            }
+        });
+    });
+
+    describe('hideBurnAfterReadingButtons', function () {
+        it(
+            'hides buttons when the QR code button is disabled',
+            function () {
+                document.documentElement.innerHTML =
+                    '<nav><button id="clonebutton"></button></nav>';
+                PrivateBin.TopNav.init();
+
+                assert.doesNotThrow(function () {
+                    PrivateBin.TopNav.hideBurnAfterReadingButtons();
+                });
+                assert.ok(query('#clonebutton').classList.contains('hidden'));
+                cleanup();
+            }
+        );
+    });
+
     describe('showViewButtons & hideViewButtons', function () {
     describe('showViewButtons & hideViewButtons', function () {
         before(function () {
         before(function () {
             cleanup();
             cleanup();
         });
         });
 
 
+        it(
+            'shows and hides navigation elements when the QR code button is disabled',
+            function () {
+                document.documentElement.innerHTML =
+                    '<nav><button id="newbutton" class="hidden"></button>' +
+                    '<button id="clonebutton" class="hidden"></button>' +
+                    '<button id="rawtextbutton" class="hidden"></button>' +
+                    '<button id="downloadtextbutton" class="hidden"></button></nav>';
+                PrivateBin.TopNav.init();
+
+                assert.doesNotThrow(function () {
+                    PrivateBin.TopNav.showViewButtons();
+                });
+                assert.ok(!query('#newbutton').classList.contains('hidden'));
+                assert.ok(!query('#clonebutton').classList.contains('hidden'));
+                assert.ok(!query('#rawtextbutton').classList.contains('hidden'));
+                assert.ok(!query('#downloadtextbutton').classList.contains('hidden'));
+
+                assert.doesNotThrow(function () {
+                    PrivateBin.TopNav.hideViewButtons();
+                });
+                assert.ok(query('#newbutton').classList.contains('hidden'));
+                assert.ok(query('#clonebutton').classList.contains('hidden'));
+                assert.ok(query('#rawtextbutton').classList.contains('hidden'));
+                assert.ok(query('#downloadtextbutton').classList.contains('hidden'));
+                cleanup();
+            }
+        );
+
         it(
         it(
             'displays & hides navigation elements for viewing an existing document',
             'displays & hides navigation elements for viewing an existing document',
             function () {
             function () {
@@ -125,6 +197,113 @@ describe('TopNav', function () {
                 assert.ok(result);
                 assert.ok(result);
             }
             }
         );
         );
+
+        it(
+            'tolerates missing optional elements (discussion & password disabled)',
+            function () {
+                // with discussion = false and password = false in the
+                // configuration, the template omits these elements entirely
+                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>' +
+                    '</li><li><div id="burnafterreadingoption" class="hidden">' +
+                    'Burn after reading</div></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();
+                assert.doesNotThrow(() => PrivateBin.TopNav.showCreateButtons());
+                assert.ok(!query('#sendbutton').classList.contains('hidden'));
+                assert.ok(!query('#burnafterreadingoption').classList.contains('hidden'));
+                assert.doesNotThrow(() => PrivateBin.TopNav.hideCreateButtons());
+                assert.ok(query('#sendbutton').classList.contains('hidden'));
+                assert.ok(query('#burnafterreadingoption').classList.contains('hidden'));
+                cleanup();
+            }
+        );
+    });
+
+    describe('send shortcut', function () {
+        let sendCount;
+
+        // builds the create-document navbar and stubs the actual send, so no
+        // server interaction happens
+        function setUpCreateMode() {
+            cleanup();
+            sendCount = 0;
+            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>' +
+                '</li><li><div id="burnafterreadingoption" class="hidden">' +
+                'Burn after reading</div></li><li><div id="opendiscussion' +
+                'option" class="hidden">Open discussion</div></li><li>' +
+                '<div id="password" class="hidden">Password</div></li>' +
+                '<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.PasteEncrypter.sendPaste = function () {
+                sendCount++;
+            };
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showCreateButtons();
+        }
+
+        // dispatches a keydown on the document, returns whether the default was prevented
+        function pressKey(options) {
+            const event = new window.KeyboardEvent('keydown', Object.assign(
+                {bubbles: true, cancelable: true}, options
+            ));
+            document.dispatchEvent(event);
+            return event.defaultPrevented;
+        }
+
+        afterEach(function () {
+            cleanup();
+        });
+
+        it('sends the document on Ctrl+S and suppresses the browser save dialog', function () {
+            setUpCreateMode();
+            const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
+            assert.strictEqual(sendCount, 1);
+            assert.ok(prevented);
+        });
+
+        it('sends the document on Cmd+S (macOS)', function () {
+            setUpCreateMode();
+            const prevented = pressKey({key: 's', code: 'KeyS', metaKey: true});
+            assert.strictEqual(sendCount, 1);
+            assert.ok(prevented);
+        });
+
+        it('sends on the physical S key of non-Latin keyboard layouts', function () {
+            setUpCreateMode();
+            // on a Russian layout the S key produces "ы"
+            pressKey({key: 'ы', code: 'KeyS', ctrlKey: true});
+            assert.strictEqual(sendCount, 1);
+        });
+
+        it('leaves Ctrl+S to the browser when viewing a document', function () {
+            setUpCreateMode();
+            PrivateBin.TopNav.hideCreateButtons();
+            const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
+            assert.strictEqual(sendCount, 0);
+            assert.ok(!prevented);
+        });
+
+        it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt), with Shift, or other keys', function () {
+            setUpCreateMode();
+            const prevented = [
+                pressKey({key: 's', code: 'KeyS'}),
+                pressKey({key: 's', code: 'KeyS', ctrlKey: true, altKey: true}),
+                pressKey({key: 'S', code: 'KeyS', ctrlKey: true, shiftKey: true}),
+                pressKey({key: 'd', code: 'KeyD', ctrlKey: true})
+            ];
+            assert.strictEqual(sendCount, 0);
+            assert.deepStrictEqual(prevented, [false, false, false, false]);
+        });
     });
     });
 
 
     describe('showNewPasteButton', function () {
     describe('showNewPasteButton', function () {
@@ -484,6 +663,21 @@ describe('TopNav', function () {
                 assert.ok(result);
                 assert.ok(result);
             }
             }
         );
         );
+
+        it(
+            'tolerates a missing discussion option (discussion disabled)',
+            function () {
+                document.documentElement.innerHTML =
+                    '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
+                    'class="buttondisabled"><label><input type="checkbox" ' +
+                    'id="burnafterreading" name="burnafterreading" /> ' +
+                    'Burn after reading</label></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
+                assert.doesNotThrow(() => PrivateBin.TopNav.resetInput());
+                assert.ok(!query('#burnafterreadingoption').classList.contains('buttondisabled'));
+                cleanup();
+            }
+        );
     });
     });
 
 
     describe('getExpiration', function () {
     describe('getExpiration', function () {
@@ -774,6 +968,94 @@ describe('TopNav', function () {
         );
         );
     });
     });
 
 
+    describe('showRetryButton & hideRetryButton', function () {
+        afterEach(function () {
+            delete global.bootstrap;
+            cleanup();
+        });
+
+        it(
+            'shows and hides the Bootstrap 5 retry button with its list item and runs the callback once',
+            function () {
+                cleanup();
+                global.bootstrap = {};
+                document.documentElement.innerHTML =
+                    '<nav><div id="navbar"><ul class="navbar-nav">' +
+                    '<li class="nav-item hidden"><button id="retrybutton" ' +
+                    'type="button" class="hidden">Retry</button></li>' +
+                    '<li><button id="newbutton" type="button">New</button>' +
+                    '</li></ul></div></nav>';
+                PrivateBin.TopNav.init();
+                let calls = 0;
+                PrivateBin.TopNav.setRetryCallback(function () {
+                    calls++;
+                });
+                const retryItem = query('#retrybutton').parentElement;
+
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.ok(retryItem.classList.contains('hidden'));
+
+                PrivateBin.TopNav.showRetryButton();
+                assert.ok(!query('#retrybutton').classList.contains('hidden'));
+                assert.ok(!retryItem.classList.contains('hidden'));
+
+                query('#retrybutton').click();
+                assert.strictEqual(calls, 1);
+
+                PrivateBin.TopNav.hideRetryButton();
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.ok(retryItem.classList.contains('hidden'));
+
+                PrivateBin.TopNav.showRetryButton();
+                PrivateBin.TopNav.hideRetryButton();
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.ok(retryItem.classList.contains('hidden'));
+                assert.ok(!query('#newbutton').classList.contains('hidden'));
+                assert.strictEqual(calls, 1);
+            }
+        );
+
+        it(
+            'toggles only the retry button on legacy Bootstrap 3 and page templates',
+            function () {
+                cleanup();
+                delete global.bootstrap;
+                document.documentElement.innerHTML =
+                    '<nav><div id="navbar"><ul class="nav navbar-nav"><li>' +
+                    '<button id="retrybutton" type="button" class="hidden">' +
+                    'Retry</button><button id="newbutton" type="button">' +
+                    'New</button></li></ul></div></nav>';
+                PrivateBin.TopNav.init();
+                let calls = 0;
+                PrivateBin.TopNav.setRetryCallback(function () {
+                    calls++;
+                });
+                const retryItem = query('#retrybutton').parentElement;
+
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.ok(!retryItem.classList.contains('hidden'));
+
+                PrivateBin.TopNav.showRetryButton();
+                assert.ok(!query('#retrybutton').classList.contains('hidden'));
+                assert.ok(!retryItem.classList.contains('hidden'));
+                assert.ok(!query('#newbutton').classList.contains('hidden'));
+
+                query('#retrybutton').click();
+                assert.strictEqual(calls, 1);
+
+                PrivateBin.TopNav.hideRetryButton();
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.ok(!retryItem.classList.contains('hidden'));
+                assert.ok(!query('#newbutton').classList.contains('hidden'));
+
+                PrivateBin.TopNav.showRetryButton();
+                PrivateBin.TopNav.hideRetryButton();
+                assert.ok(query('#retrybutton').classList.contains('hidden'));
+                assert.strictEqual(calls, 1);
+            }
+        );
+    });
+
     describe('rawText', function () {
     describe('rawText', function () {
         before(function () {
         before(function () {
             cleanup();
             cleanup();

+ 17 - 5
js/test/UiHelper.js

@@ -3,8 +3,6 @@ const common = require('../common');
 const fc = require('fast-check');
 const fc = require('fast-check');
 
 
 describe('UiHelper', function () {
 describe('UiHelper', function () {
-    // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet.
-    // for now we use a mock function to trigger the event
     describe('historyChange', function () {
     describe('historyChange', function () {
         this.timeout(30000);
         this.timeout(30000);
         beforeEach(function () {
         beforeEach(function () {
@@ -12,6 +10,21 @@ describe('UiHelper', function () {
             cleanup();
             cleanup();
         });
         });
 
 
+
+        /** @param {object?} state */
+        function dispatchPopState(state = null) {
+            let eventError = null;
+            const captureError = event => {
+                eventError = event.error;
+                event.preventDefault();
+            };
+            window.addEventListener('error', captureError);
+            PrivateBin.UiHelper.init();
+            window.dispatchEvent(new window.PopStateEvent('popstate', {state: state}));
+            window.removeEventListener('error', captureError);
+            assert.strictEqual(eventError, null);
+        }
+
         it('redirects to home, when the state is null', () => {
         it('redirects to home, when the state is null', () => {
             fc.assert(fc.property(
             fc.assert(fc.property(
                 common.fcUrl(false, false),
                 common.fcUrl(false, false),
@@ -19,7 +32,7 @@ describe('UiHelper', function () {
                     const expected = common.urlToString(url),
                     const expected = common.urlToString(url),
                         clean = globalThis.cleanup('', {url: expected});
                         clean = globalThis.cleanup('', {url: expected});
 
 
-                    PrivateBin.UiHelper.mockHistoryChange();
+                    dispatchPopState();
                     PrivateBin.Helper.reset();
                     PrivateBin.Helper.reset();
                     var result = window.location.href;
                     var result = window.location.href;
                     clean();
                     clean();
@@ -37,7 +50,7 @@ describe('UiHelper', function () {
                     const expected = common.urlToString(url),
                     const expected = common.urlToString(url),
                         clean = globalThis.cleanup('', {url: expected});
                         clean = globalThis.cleanup('', {url: expected});
 
 
-                    PrivateBin.UiHelper.mockHistoryChange([
+                    dispatchPopState([
                         {type: 'newpaste'}, '', expected
                         {type: 'newpaste'}, '', expected
                     ]);
                     ]);
                     PrivateBin.Helper.reset();
                     PrivateBin.Helper.reset();
@@ -77,4 +90,3 @@ describe('UiHelper', function () {
         */
         */
     });
     });
 });
 });
-

+ 4 - 10
lib/Configuration.php

@@ -50,12 +50,6 @@ class Configuration
             'template'                 => 'bootstrap5',
             'template'                 => 'bootstrap5',
             'availabletemplates'       => [
             'availabletemplates'       => [
                 'bootstrap5',
                 'bootstrap5',
-                'bootstrap',
-                'bootstrap-page',
-                'bootstrap-dark',
-                'bootstrap-dark-page',
-                'bootstrap-compact',
-                'bootstrap-compact-page',
             ],
             ],
             'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
             'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
             'notice'                   => '',
             'notice'                   => '',
@@ -124,8 +118,8 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-uqpdo+52wd4/uzOgfilGFw1SX3faeOuHWSRvfRwTUj8q7WPUoIArxVmp1DOeZzZUhEI/4NDJkNsQvvrKp8uQ/w==',
-            'js/purify-3.4.12.js'    => 'sha512-Akf6HnAJZm0sWWWI4gp2GYff0NDnHUB02XJE5S7Hdq/Z5xtMjkuFacsDA8ZtViv1gi+onBxMhEMIaGyQeGxBng==',
+            'js/privatebin.js'       => 'sha512-jXV3mKXtezHb3zaJ/Ijhj4EVT9IoB/RQGhrVxFE6ID8zAZIE3otvIL3Nf+L7AR78xNNCTm0jLJSvHeNPVxoJIw==',
+            'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
             'js/zlib.js'             => 'sha512-QOaEwssHqHRRcWJ2Un3Kl2Zhyprzl7T8zmsKN2FppFxW3VR+8UChYOx2iuL0HbXK42fuBWJm5PNQJxufulrt/w==',
             'js/zlib.js'             => 'sha512-QOaEwssHqHRRcWJ2Un3Kl2Zhyprzl7T8zmsKN2FppFxW3VR+8UChYOx2iuL0HbXK42fuBWJm5PNQJxufulrt/w==',
@@ -139,8 +133,8 @@ class Configuration
      */
      */
     public function __construct()
     public function __construct()
     {
     {
-        $basePaths  = [];
-        $config     = [];
+        $basePaths = [];
+        $config = [];
         $configPath = getenv('CONFIG_PATH');
         $configPath = getenv('CONFIG_PATH');
         if ($configPath !== false && !empty($configPath)) {
         if ($configPath !== false && !empty($configPath)) {
             $basePaths[] = $configPath;
             $basePaths[] = $configPath;

+ 10 - 20
lib/Controller.php

@@ -32,7 +32,7 @@ class Controller
      *
      *
      * @const string
      * @const string
      */
      */
-    const VERSION = '2.0.6';
+    const VERSION = '2.1.0';
 
 
     /**
     /**
      * minimal required PHP version
      * minimal required PHP version
@@ -92,7 +92,7 @@ class Controller
      * Factory of instance models
      * Factory of instance models
      *
      *
      * @access private
      * @access private
-     * @var    model
+     * @var    Model
      */
      */
     private $_model;
     private $_model;
 
 
@@ -100,7 +100,7 @@ class Controller
      * request
      * request
      *
      *
      * @access private
      * @access private
-     * @var    request
+     * @var    Request
      */
      */
     private $_request;
     private $_request;
 
 
@@ -124,7 +124,7 @@ class Controller
      */
      */
     public function __construct(?Configuration $config = null)
     public function __construct(?Configuration $config = null)
     {
     {
-        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
+        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION, '<')) {
             error_log(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION));
             error_log(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION));
             return;
             return;
         }
         }
@@ -189,7 +189,7 @@ class Controller
      */
      */
     private function _init()
     private function _init()
     {
     {
-        $this->_model   = new Model($this->_conf);
+        $this->_model = new Model($this->_conf);
         $this->_request = new Request;
         $this->_request = new Request;
         $this->_urlBase = $this->_request->getRequestUri();
         $this->_urlBase = $this->_request->getRequestUri();
 
 
@@ -226,7 +226,7 @@ class Controller
     private function _setDefaultTemplate()
     private function _setDefaultTemplate()
     {
     {
         $templates = $this->_conf->getKey('availabletemplates');
         $templates = $this->_conf->getKey('availabletemplates');
-        $template  = $this->_conf->getKey('template');
+        $template = $this->_conf->getKey('template');
         if (!in_array($template, $templates, true)) {
         if (!in_array($template, $templates, true)) {
             $templates[] = $template;
             $templates[] = $template;
         }
         }
@@ -280,14 +280,15 @@ class Controller
         ServerSalt::setStore($this->_model->getStore());
         ServerSalt::setStore($this->_model->getStore());
         TrafficLimiter::setConfiguration($this->_conf);
         TrafficLimiter::setConfiguration($this->_conf);
         TrafficLimiter::setStore($this->_model->getStore());
         TrafficLimiter::setStore($this->_model->getStore());
+        $data = [];
         try {
         try {
             TrafficLimiter::canPass();
             TrafficLimiter::canPass();
+            $data = $this->_request->getData($this->_conf->getKey('sizelimit'));
         } catch (TranslatedException $e) {
         } catch (TranslatedException $e) {
             $this->_json_error($e->getMessage());
             $this->_json_error($e->getMessage());
             return;
             return;
         }
         }
 
 
-        $data      = $this->_request->getData();
         $isComment = array_key_exists('pasteid', $data) &&
         $isComment = array_key_exists('pasteid', $data) &&
             !empty($data['pasteid']) &&
             !empty($data['pasteid']) &&
             array_key_exists('parentid', $data) &&
             array_key_exists('parentid', $data) &&
@@ -296,17 +297,6 @@ class Controller
             $this->_json_error(I18n::_('Invalid data.'));
             $this->_json_error(I18n::_('Invalid data.'));
             return;
             return;
         }
         }
-        $sizelimit = $this->_conf->getKey('sizelimit');
-        // Ensure content is not too big.
-        if (strlen($data['ct']) > $sizelimit) {
-            $this->_json_error(
-                I18n::_(
-                    'Document is limited to %s of encrypted data.',
-                    Filter::formatHumanReadableSize($sizelimit)
-                )
-            );
-            return;
-        }
 
 
         // The user posts a comment.
         // The user posts a comment.
         if ($isComment) {
         if ($isComment) {
@@ -356,7 +346,7 @@ class Controller
                 if (hash_equals($paste->getDeleteToken(), $deletetoken)) {
                 if (hash_equals($paste->getDeleteToken(), $deletetoken)) {
                     // Document exists and deletion token is valid: Delete the it.
                     // Document exists and deletion token is valid: Delete the it.
                     $paste->delete();
                     $paste->delete();
-                    $this->_status     = 'Document was properly deleted.';
+                    $this->_status = 'Document was properly deleted.';
                     $this->_is_deleted = true;
                     $this->_is_deleted = true;
                 } else {
                 } else {
                     $this->_error = 'Wrong deletion token. Document was not deleted.';
                     $this->_error = 'Wrong deletion token. Document was not deleted.';
@@ -517,7 +507,7 @@ class Controller
             $type = '';
             $type = '';
         }
         }
         $content = '{}';
         $content = '{}';
-        $file    = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
+        $file = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
         if (is_readable($file)) {
         if (is_readable($file)) {
             $content = str_replace(
             $content = str_replace(
                 '?jsonld=',
                 '?jsonld=',

+ 30 - 25
lib/Data/Database.php

@@ -71,12 +71,12 @@ class Database extends AbstractData
             array_key_exists('opt', $options)
             array_key_exists('opt', $options)
         ) {
         ) {
             // set default options
             // set default options
-            $options['opt'][PDO::ATTR_ERRMODE]          = PDO::ERRMODE_EXCEPTION;
+            $options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
             $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
             $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
             if (!array_key_exists(PDO::ATTR_PERSISTENT, $options['opt'])) {
             if (!array_key_exists(PDO::ATTR_PERSISTENT, $options['opt'])) {
                 $options['opt'][PDO::ATTR_PERSISTENT] = true;
                 $options['opt'][PDO::ATTR_PERSISTENT] = true;
             }
             }
-            $db_tables_exist                            = true;
+            $db_tables_exist = true;
 
 
             // setup type and dabase connection
             // setup type and dabase connection
             $this->_type = strtolower(
             $this->_type = strtolower(
@@ -86,14 +86,14 @@ class Database extends AbstractData
             // tell it to expect ANSI SQL double quotes
             // tell it to expect ANSI SQL double quotes
             if ($this->_type === 'mysql') {
             if ($this->_type === 'mysql') {
                 // deprecated as of PHP 8.5
                 // deprecated as of PHP 8.5
-                if (version_compare(PHP_VERSION, '8.5') < 0 && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
+                if (version_compare(PHP_VERSION, '8.5', '<') && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
                     $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
                     $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
                 } elseif (defined('Pdo\Mysql::ATTR_INIT_COMMAND')) {
                 } elseif (defined('Pdo\Mysql::ATTR_INIT_COMMAND')) {
                     $options['opt'][Pdo\Mysql::ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
                     $options['opt'][Pdo\Mysql::ATTR_INIT_COMMAND] = "SET SESSION sql_mode='ANSI_QUOTES'";
                 }
                 }
             }
             }
             $tableQuery = $this->_getTableQuery($this->_type);
             $tableQuery = $this->_getTableQuery($this->_type);
-            $this->_db  = new PDO(
+            $this->_db = new PDO(
                 $options['dsn'],
                 $options['dsn'],
                 $options['usr'],
                 $options['usr'],
                 $options['pwd'],
                 $options['pwd'],
@@ -148,8 +148,8 @@ class Database extends AbstractData
      */
      */
     public function create($pasteid, array &$paste)
     public function create($pasteid, array &$paste)
     {
     {
-        $expire_date      = 0;
-        $meta             = $paste['meta'];
+        $expire_date = 0;
+        $meta = $paste['meta'];
         if (array_key_exists('expire_date', $meta)) {
         if (array_key_exists('expire_date', $meta)) {
             $expire_date = (int) $meta['expire_date'];
             $expire_date = (int) $meta['expire_date'];
             unset($meta['expire_date']);
             unset($meta['expire_date']);
@@ -301,10 +301,15 @@ class Database extends AbstractData
      */
      */
     public function readComments($pasteid)
     public function readComments($pasteid)
     {
     {
-        $rows = $this->_select(
-            'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
-            '" WHERE "pasteid" = ?', [$pasteid]
-        );
+        try {
+            $rows = $this->_select(
+                'SELECT * FROM "' . $this->_sanitizeIdentifier('comment') .
+                '" WHERE "pasteid" = ?', [$pasteid]
+            );
+        } catch (PDOException $e) {
+            error_log('Error while reading comments from the database: ' . $e->getMessage());
+            return [];
+        }
 
 
         // create comment list
         // create comment list
         $comments = [];
         $comments = [];
@@ -316,11 +321,11 @@ class Database extends AbstractData
                     error_log('Error while reading a comment from the database: ' . $e->getMessage());
                     error_log('Error while reading a comment from the database: ' . $e->getMessage());
                     $data = [];
                     $data = [];
                 }
                 }
-                $i                          = $this->getOpenSlot($comments, (int) $row['postdate']);
-                $comments[$i]               = $data;
-                $comments[$i]['id']         = $row['dataid'];
-                $comments[$i]['parentid']   = $row['parentid'];
-                $comments[$i]['meta']       = ['created' => (int) $row['postdate']];
+                $i = $this->getOpenSlot($comments, (int) $row['postdate']);
+                $comments[$i] = $data;
+                $comments[$i]['id'] = $row['dataid'];
+                $comments[$i]['parentid'] = $row['parentid'];
+                $comments[$i]['meta'] = ['created' => (int) $row['postdate']];
                 if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) {
                 if (array_key_exists('vizhash', $row) && !empty($row['vizhash'])) {
                     $comments[$i]['meta']['icon'] = $row['vizhash'];
                     $comments[$i]['meta']['icon'] = $row['vizhash'];
                 }
                 }
@@ -389,7 +394,7 @@ class Database extends AbstractData
     public function getValue($namespace, $key = '')
     public function getValue($namespace, $key = '')
     {
     {
         $configKey = strtoupper($namespace);
         $configKey = strtoupper($namespace);
-        $value     = $this->_getConfig($configKey);
+        $value = $this->_getConfig($configKey);
         if ($value === '') {
         if ($value === '') {
             // initialize the row, so that setValue can rely on UPDATE queries
             // initialize the row, so that setValue can rely on UPDATE queries
             $this->_exec(
             $this->_exec(
@@ -401,7 +406,7 @@ class Database extends AbstractData
             // migrate filesystem based salt into database
             // migrate filesystem based salt into database
             $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
             $file = 'data' . DIRECTORY_SEPARATOR . 'salt.php';
             if ($namespace === 'salt' && is_readable($file)) {
             if ($namespace === 'salt' && is_readable($file)) {
-                $fs    = new Filesystem(['dir' => 'data']);
+                $fs = new Filesystem(['dir' => 'data']);
                 $value = $fs->getValue('salt');
                 $value = $fs->getValue('salt');
                 $this->setValue($value, 'salt');
                 $this->setValue($value, 'salt');
                 if (!unlink($file)) {
                 if (!unlink($file)) {
@@ -469,7 +474,7 @@ class Database extends AbstractData
     private function _exec($sql, array $params)
     private function _exec($sql, array $params)
     {
     {
         $statement = $this->_db->prepare($sql);
         $statement = $this->_db->prepare($sql);
-        $position  = 1;
+        $position = 1;
         foreach ($params as &$parameter) {
         foreach ($params as &$parameter) {
             if (is_int($parameter)) {
             if (is_int($parameter)) {
                 $statement->bindParam($position, $parameter, PDO::PARAM_INT);
                 $statement->bindParam($position, $parameter, PDO::PARAM_INT);
@@ -672,8 +677,8 @@ class Database extends AbstractData
     private function _createPasteTable()
     private function _createPasteTable()
     {
     {
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
-        $attachmentType             = $this->_getAttachmentType();
-        $metaType                   = $this->_getMetaType();
+        $attachmentType = $this->_getAttachmentType();
+        $metaType = $this->_getMetaType();
         $this->_db->exec(
         $this->_db->exec(
             'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
             'CREATE TABLE "' . $this->_sanitizeIdentifier('paste') . '" ( ' .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
@@ -691,7 +696,7 @@ class Database extends AbstractData
     private function _createCommentTable()
     private function _createCommentTable()
     {
     {
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses();
-        $dataType                   = $this->_getDataType();
+        $dataType = $this->_getDataType();
         $this->_db->exec(
         $this->_db->exec(
             'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
             'CREATE TABLE "' . $this->_sanitizeIdentifier('comment') . '" ( ' .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
             "\"dataid\" CHAR(16) NOT NULL$main_key, " .
@@ -733,8 +738,8 @@ class Database extends AbstractData
     private function _createConfigTable()
     private function _createConfigTable()
     {
     {
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
         list($main_key, $after_key) = $this->_getPrimaryKeyClauses('id');
-        $charType                   = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
-        $textType                   = $this->_getMetaType();
+        $charType = $this->_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)';
+        $textType = $this->_getMetaType();
         $this->_db->exec(
         $this->_db->exec(
             'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
             'CREATE TABLE "' . $this->_sanitizeIdentifier('config') .
             "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
             "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )"
@@ -787,7 +792,7 @@ class Database extends AbstractData
         $supportsDropColumn = true;
         $supportsDropColumn = true;
         if ($this->_type === 'sqlite') {
         if ($this->_type === 'sqlite') {
             try {
             try {
-                $row                = $this->_select('SELECT sqlite_version() AS "v"', [], true);
+                $row = $this->_select('SELECT sqlite_version() AS "v"', [], true);
                 $supportsDropColumn = (bool) version_compare($row['v'], '3.35.0', '>=');
                 $supportsDropColumn = (bool) version_compare($row['v'], '3.35.0', '>=');
             } catch (PDOException $e) {
             } catch (PDOException $e) {
                 $supportsDropColumn = false;
                 $supportsDropColumn = false;
@@ -804,7 +809,7 @@ class Database extends AbstractData
      */
      */
     private function _upgradeDatabase($oldversion)
     private function _upgradeDatabase($oldversion)
     {
     {
-        $dataType       = $this->_getDataType();
+        $dataType = $this->_getDataType();
         $attachmentType = $this->_getAttachmentType();
         $attachmentType = $this->_getAttachmentType();
         if (version_compare($oldversion, '0.21', '<=')) {
         if (version_compare($oldversion, '0.21', '<=')) {
             // create the meta column if necessary (pre 0.21 change)
             // create the meta column if necessary (pre 0.21 change)

+ 12 - 12
lib/Data/Filesystem.php

@@ -95,7 +95,7 @@ class Filesystem extends AbstractData
     public function create($pasteid, array &$paste)
     public function create($pasteid, array &$paste)
     {
     {
         $storagedir = $this->_dataid2path($pasteid);
         $storagedir = $this->_dataid2path($pasteid);
-        $file       = $storagedir . $pasteid . '.php';
+        $file = $storagedir . $pasteid . '.php';
         if (is_file($file)) {
         if (is_file($file)) {
             return false;
             return false;
         }
         }
@@ -163,7 +163,7 @@ class Filesystem extends AbstractData
      */
      */
     public function exists($pasteid)
     public function exists($pasteid)
     {
     {
-        $basePath  = $this->_dataid2path($pasteid) . $pasteid;
+        $basePath = $this->_dataid2path($pasteid) . $pasteid;
         $pastePath = $basePath . '.php';
         $pastePath = $basePath . '.php';
         // convert to PHP protected files if needed
         // convert to PHP protected files if needed
         if (is_readable($basePath)) {
         if (is_readable($basePath)) {
@@ -197,7 +197,7 @@ class Filesystem extends AbstractData
     public function createComment($pasteid, $parentid, $commentid, array &$comment)
     public function createComment($pasteid, $parentid, $commentid, array &$comment)
     {
     {
         $storagedir = $this->_dataid2discussionpath($pasteid);
         $storagedir = $this->_dataid2discussionpath($pasteid);
-        $file       = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php';
+        $file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php';
         if (is_file($file)) {
         if (is_file($file)) {
             return false;
             return false;
         }
         }
@@ -217,7 +217,7 @@ class Filesystem extends AbstractData
     public function readComments($pasteid)
     public function readComments($pasteid)
     {
     {
         $comments = [];
         $comments = [];
-        $discdir  = $this->_dataid2discussionpath($pasteid);
+        $discdir = $this->_dataid2discussionpath($pasteid);
         if (is_dir($discdir)) {
         if (is_dir($discdir)) {
             foreach (new GlobIterator($discdir . DIRECTORY_SEPARATOR . $pasteid .
             foreach (new GlobIterator($discdir . DIRECTORY_SEPARATOR . $pasteid .
                 '.' . self::ID_PATTERN . '.' . self::ID_PATTERN . '.php') as $file
                 '.' . self::ID_PATTERN . '.' . self::ID_PATTERN . '.php') as $file
@@ -237,11 +237,11 @@ class Filesystem extends AbstractData
                     }
                     }
                     $items = explode('.', $file->getBasename('.php'));
                     $items = explode('.', $file->getBasename('.php'));
                     // Add some meta information not contained in file.
                     // Add some meta information not contained in file.
-                    $comment['id']       = $items[1];
+                    $comment['id'] = $items[1];
                     $comment['parentid'] = $items[2];
                     $comment['parentid'] = $items[2];
 
 
                     // Store in array
                     // Store in array
-                    $key            = $this->getOpenSlot(
+                    $key = $this->getOpenSlot(
                         $comments,
                         $comments,
                         $comment['meta']['created']
                         $comment['meta']['created']
                     );
                     );
@@ -293,7 +293,7 @@ class Filesystem extends AbstractData
                 break;
                 break;
             case 'traffic_limiter':
             case 'traffic_limiter':
                 $this->_last_cache[$key] = $value;
                 $this->_last_cache[$key] = $value;
-                $content                 = '<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export($this->_last_cache, true) . ';';
+                $content = '<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export($this->_last_cache, true) . ';';
                 break;
                 break;
             default:
             default:
                 return false;
                 return false;
@@ -377,11 +377,11 @@ class Filesystem extends AbstractData
     protected function _getExpiredPastes($batchsize)
     protected function _getExpiredPastes($batchsize)
     {
     {
         $pastes = [];
         $pastes = [];
-        $count  = 0;
+        $count = 0;
         $opened = 0;
         $opened = 0;
-        $limit  = $batchsize * 10; // try at most 10 times $batchsize pastes before giving up
-        $time   = time();
-        $files  = $this->getAllPastes();
+        $limit = $batchsize * 10; // try at most 10 times $batchsize pastes before giving up
+        $time = time();
+        $files = $this->getAllPastes();
         shuffle($files);
         shuffle($files);
         foreach ($files as $pasteid) {
         foreach ($files as $pasteid) {
             if ($this->exists($pasteid)) {
             if ($this->exists($pasteid)) {
@@ -506,7 +506,7 @@ class Filesystem extends AbstractData
             }
             }
         }
         }
 
 
-        $fileCreated  = true;
+        $fileCreated = true;
         $writtenBytes = 0;
         $writtenBytes = 0;
         if (!is_file($filename)) {
         if (!is_file($filename)) {
             $fileCreated = @touch($filename);
             $fileCreated = @touch($filename);

+ 7 - 7
lib/Data/GoogleCloudStorage.php

@@ -151,7 +151,7 @@ class GoogleCloudStorage extends AbstractData
     public function read($pasteid)
     public function read($pasteid)
     {
     {
         try {
         try {
-            $o    = $this->_bucket->object($this->_getKey($pasteid));
+            $o = $this->_bucket->object($this->_getKey($pasteid));
             $data = $o->downloadAsString();
             $data = $o->downloadAsString();
             return Json::decode($data);
             return Json::decode($data);
         } catch (NotFoundException $e) {
         } catch (NotFoundException $e) {
@@ -216,18 +216,18 @@ class GoogleCloudStorage extends AbstractData
     public function readComments($pasteid)
     public function readComments($pasteid)
     {
     {
         $comments = [];
         $comments = [];
-        $prefix   = $this->_getKey($pasteid) . '/discussion/';
+        $prefix = $this->_getKey($pasteid) . '/discussion/';
         try {
         try {
             foreach ($this->_bucket->objects(['prefix' => $prefix]) as $key) {
             foreach ($this->_bucket->objects(['prefix' => $prefix]) as $key) {
-                $data            = $this->_bucket->object($key->name())->downloadAsString();
+                $data = $this->_bucket->object($key->name())->downloadAsString();
                 try {
                 try {
                     $comment = Json::decode($data);
                     $comment = Json::decode($data);
                 } catch (JsonException $e) {
                 } catch (JsonException $e) {
                     error_log('failed to read comment from ' . $key->name() . ', ' . $e->getMessage());
                     error_log('failed to read comment from ' . $key->name() . ', ' . $e->getMessage());
                     $comment = [];
                     $comment = [];
                 }
                 }
-                $comment['id']   = basename($key->name());
-                $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
+                $comment['id'] = basename($key->name());
+                $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
                 $comments[$slot] = $comment;
                 $comments[$slot] = $comment;
             }
             }
         } catch (NotFoundException $e) {
         } catch (NotFoundException $e) {
@@ -242,7 +242,7 @@ class GoogleCloudStorage extends AbstractData
     public function existsComment($pasteid, $parentid, $commentid)
     public function existsComment($pasteid, $parentid, $commentid)
     {
     {
         $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
         $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
-        $o    = $this->_bucket->object($name);
+        $o = $this->_bucket->object($name);
         return $o->exists();
         return $o->exists();
     }
     }
 
 
@@ -335,7 +335,7 @@ class GoogleCloudStorage extends AbstractData
     {
     {
         $expired = [];
         $expired = [];
 
 
-        $now    = time();
+        $now = time();
         $prefix = $this->_prefix;
         $prefix = $this->_prefix;
         if (!empty($prefix)) {
         if (!empty($prefix)) {
             $prefix .= '/';
             $prefix .= '/';

+ 13 - 13
lib/Data/S3Storage.php

@@ -87,7 +87,7 @@ class S3Storage extends AbstractData
         if (isset($options['accesskey']) && isset($options['secretkey'])) {
         if (isset($options['accesskey']) && isset($options['secretkey'])) {
             $this->_options['credentials'] = [];
             $this->_options['credentials'] = [];
 
 
-            $this->_options['credentials']['key']    = $options['accesskey'];
+            $this->_options['credentials']['key'] = $options['accesskey'];
             $this->_options['credentials']['secret'] = $options['secretkey'];
             $this->_options['credentials']['secret'] = $options['secretkey'];
         }
         }
         if (array_key_exists('region', $options)) {
         if (array_key_exists('region', $options)) {
@@ -122,16 +122,16 @@ class S3Storage extends AbstractData
     private function _listAllObjects($prefix)
     private function _listAllObjects($prefix)
     {
     {
         $allObjects = [];
         $allObjects = [];
-        $options    = [
+        $options = [
             'Bucket' => $this->_bucket,
             'Bucket' => $this->_bucket,
             'Prefix' => $prefix,
             'Prefix' => $prefix,
         ];
         ];
 
 
         do {
         do {
             $objectsListResponse = $this->_client->listObjects($options);
             $objectsListResponse = $this->_client->listObjects($options);
-            $objects             = $objectsListResponse['Contents'] ?? [];
+            $objects = $objectsListResponse['Contents'] ?? [];
             foreach ($objects as $object) {
             foreach ($objects as $object) {
-                $allObjects[]      = $object;
+                $allObjects[] = $object;
                 $options['Marker'] = $object['Key'];
                 $options['Marker'] = $object['Key'];
             }
             }
         } while ($objectsListResponse['IsTruncated']);
         } while ($objectsListResponse['IsTruncated']);
@@ -280,7 +280,7 @@ class S3Storage extends AbstractData
     public function readComments($pasteid)
     public function readComments($pasteid)
     {
     {
         $comments = [];
         $comments = [];
-        $prefix   = $this->_getKey($pasteid) . '/discussion/';
+        $prefix = $this->_getKey($pasteid) . '/discussion/';
         try {
         try {
             $entries = $this->_listAllObjects($prefix);
             $entries = $this->_listAllObjects($prefix);
             foreach ($entries as $entry) {
             foreach ($entries as $entry) {
@@ -288,13 +288,13 @@ class S3Storage extends AbstractData
                     'Bucket' => $this->_bucket,
                     'Bucket' => $this->_bucket,
                     'Key'    => $entry['Key'],
                     'Key'    => $entry['Key'],
                 ]);
                 ]);
-                $data             = $object['Body']->getContents();
-                $body             = JSON::decode($data);
-                $items            = explode('/', $entry['Key']);
-                $body['id']       = $items[3];
+                $data = $object['Body']->getContents();
+                $body = JSON::decode($data);
+                $items = explode('/', $entry['Key']);
+                $body['id'] = $items[3];
                 $body['parentid'] = $items[2];
                 $body['parentid'] = $items[2];
-                $slot             = $this->getOpenSlot($comments, (int) $object['Metadata']['created']);
-                $comments[$slot]  = $body;
+                $slot = $this->getOpenSlot($comments, (int) $object['Metadata']['created']);
+                $comments[$slot] = $body;
             }
             }
         } catch (S3Exception $e) {
         } catch (S3Exception $e) {
             // no comments found
             // no comments found
@@ -420,8 +420,8 @@ class S3Storage extends AbstractData
     protected function _getExpiredPastes($batchsize)
     protected function _getExpiredPastes($batchsize)
     {
     {
         $expired = [];
         $expired = [];
-        $now     = time();
-        $prefix  = $this->_prefix;
+        $now = time();
+        $prefix = $this->_prefix;
         if (!empty($prefix)) {
         if (!empty($prefix)) {
             $prefix .= '/';
             $prefix .= '/';
         }
         }

+ 1 - 1
lib/Filter.php

@@ -60,7 +60,7 @@ class Filter
     public static function formatHumanReadableSize($size)
     public static function formatHumanReadableSize($size)
     {
     {
         $iec = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
         $iec = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-        $i   = 0;
+        $i = 0;
         while (($size / 1000) >= 1) {
         while (($size / 1000) >= 1) {
             $size = $size / 1000;
             $size = $size / 1000;
             ++$i;
             ++$i;

+ 6 - 1
lib/FormatV2.php

@@ -39,7 +39,7 @@ class FormatV2
             $required_keys[] = 'meta';
             $required_keys[] = 'meta';
         }
         }
 
 
-        // Make sure no additionnal keys were added.
+        // Make sure no additional keys were added.
         if (count(array_keys($message)) !== count($required_keys)) {
         if (count(array_keys($message)) !== count($required_keys)) {
             return false;
             return false;
         }
         }
@@ -63,6 +63,11 @@ class FormatV2
             return false;
             return false;
         }
         }
 
 
+        // Make sure the adata parameters of pastes are a properly sized array,
+        if (!$isComment && count($message['adata']) !== 4) {
+            return false;
+        }
+
         // Make sure the ciphertext and the cipher parameters used in the
         // Make sure the ciphertext and the cipher parameters used in the
         // string operations below are actually strings, so that malformed
         // string operations below are actually strings, so that malformed
         // input yields "Invalid data." instead of a fatal type error.
         // input yields "Invalid data." instead of a fatal type error.

+ 49 - 21
lib/I18n.php

@@ -14,6 +14,9 @@ namespace PrivateBin;
 use AppendIterator;
 use AppendIterator;
 use GlobIterator;
 use GlobIterator;
 
 
+use function array_key_exists;
+use function in_array;
+
 /**
 /**
  * I18n
  * I18n
  *
  *
@@ -116,8 +119,8 @@ class I18n
         array_unshift($args, $messageId);
         array_unshift($args, $messageId);
         if (is_array(self::$_translations[$messageId])) {
         if (is_array(self::$_translations[$messageId])) {
             $number = (int) $args[1];
             $number = (int) $args[1];
-            $key    = self::_getPluralForm($number);
-            $max    = count(self::$_translations[$messageId]) - 1;
+            $key = self::_getPluralForm($number);
+            $max = count(self::$_translations[$messageId]) - 1;
             if ($key > $max) {
             if ($key > $max) {
                 $key = $max;
                 $key = $max;
             }
             }
@@ -176,23 +179,47 @@ class I18n
         }
         }
         // find a translation file matching the browsers language preferences
         // find a translation file matching the browsers language preferences
         else {
         else {
-            self::$_language = self::_getMatchingLanguage(
-                self::_normalizeBrowserLanguages(
-                    self::getBrowserLanguages(), $availableLanguages
-                ),
-                $availableLanguages
-            );
+            self::$_language = self::_getLanguageFromHttpHeader($availableLanguages);
         }
         }
 
 
         // load translations
         // load translations
         if (self::$_language === 'en') {
         if (self::$_language === 'en') {
             self::$_translations = [];
             self::$_translations = [];
         } else {
         } else {
-            $data                = file_get_contents(self::_getPath(self::$_language . '.json'));
+            $data = file_get_contents(self::_getPath(self::$_language . '.json'));
             self::$_translations = Json::decode($data);
             self::$_translations = Json::decode($data);
         }
         }
     }
     }
 
 
+    /**
+     * get languages bsed on the HTTP header
+     *
+     * @access private
+     * @static
+     * @param  array $availableLanguages
+     * @return string
+     */
+    private static function _getLanguageFromHttpHeader($availableLanguages)
+    {
+        if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
+            return self::$_languageFallback;
+        }
+
+        // as per https://stackoverflow.com/a/9051957/5008962 we can use the I10n module
+        // if loaded, but it only returns the primary preference (aka one string)
+        if (function_exists('locale_accept_from_http')) {
+            $detectedBestLanguage = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+            if (in_array($detectedBestLanguage, $availableLanguages)) {
+                return $detectedBestLanguage;
+            }
+        }
+
+        return self::_getMatchingLanguage(
+            self::_normalizeBrowserLanguages(self::getBrowserLanguages(), $availableLanguages),
+            $availableLanguages
+        );
+    }
+
     /**
     /**
      * get list of available translations based on files found
      * get list of available translations based on files found
      *
      *
@@ -204,7 +231,7 @@ class I18n
     {
     {
         if (count(self::$_availableLanguages) === 0) {
         if (count(self::$_availableLanguages) === 0) {
             self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
             self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
-            $languageIterator            = new AppendIterator();
+            $languageIterator = new AppendIterator();
             $languageIterator->append(new GlobIterator(self::_getPath('??.json')));
             $languageIterator->append(new GlobIterator(self::_getPath('??.json')));
             $languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
             $languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
             $languageIterator->append(new GlobIterator(self::_getPath('??-??.json'))); // for regional variants like zh-tw
             $languageIterator->append(new GlobIterator(self::_getPath('??-??.json'))); // for regional variants like zh-tw
@@ -234,7 +261,7 @@ class I18n
             $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
             $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
             foreach ($languageRanges as $languageRange) {
             foreach ($languageRanges as $languageRange) {
                 if (preg_match(
                 if (preg_match(
-                    '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/',
+                    '/^(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?$/',
                     trim($languageRange), $match
                     trim($languageRange), $match
                 )) {
                 )) {
                     if (!isset($match[2])) {
                     if (!isset($match[2])) {
@@ -265,7 +292,7 @@ class I18n
     protected static function _normalizeBrowserLanguages($languages, $availableLanguages)
     protected static function _normalizeBrowserLanguages($languages, $availableLanguages)
     {
     {
         // Base zh stays before zh-tw because regional locales are appended after base locales.
         // Base zh stays before zh-tw because regional locales are appended after base locales.
-        $hasSimplifiedChinese  = in_array('zh', $availableLanguages, true);
+        $hasSimplifiedChinese = in_array('zh', $availableLanguages, true);
         $hasTraditionalChinese = in_array('zh-tw', $availableLanguages, true);
         $hasTraditionalChinese = in_array('zh-tw', $availableLanguages, true);
         foreach ($languages as $quality => $languageRanges) {
         foreach ($languages as $quality => $languageRanges) {
             foreach ($languageRanges as $index => $languageRange) {
             foreach ($languageRanges as $index => $languageRange) {
@@ -313,7 +340,7 @@ class I18n
     {
     {
         $file = self::_getPath('languages.json');
         $file = self::_getPath('languages.json');
         if (count(self::$_languageLabels) === 0 && is_readable($file)) {
         if (count(self::$_languageLabels) === 0 && is_readable($file)) {
-            $data                  = file_get_contents($file);
+            $data = file_get_contents($file);
             self::$_languageLabels = Json::decode($data);
             self::$_languageLabels = Json::decode($data);
         }
         }
         if (count($languages) === 0) {
         if (count($languages) === 0) {
@@ -408,10 +435,11 @@ class I18n
             case 'id':
             case 'id':
             case 'ja':
             case 'ja':
             case 'jbo':
             case 'jbo':
+            case 'ko':
             case 'th':
             case 'th':
                 return 0;
                 return 0;
             case 'lt':
             case 'lt':
-                return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
+                return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20)) ? 1 : 2);
             case 'pl':
             case 'pl':
                 return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
                 return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
             case 'ro':
             case 'ro':
@@ -441,18 +469,18 @@ class I18n
     protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
     protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
     {
     {
         $matches = [];
         $matches = [];
-        $any     = false;
+        $any = false;
         foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
         foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
             $acceptedQuality = floatval($acceptedQuality);
             $acceptedQuality = floatval($acceptedQuality);
             if ($acceptedQuality === 0.0) {
             if ($acceptedQuality === 0.0) {
                 continue;
                 continue;
             }
             }
-            foreach ($availableLanguages as $availableValue) {
-                $availableQuality = 1.0;
-                foreach ($acceptedValues as $acceptedValue) {
-                    if ($acceptedValue === '*') {
-                        $any = true;
-                    }
+            foreach ($acceptedValues as $acceptedValue) {
+                if ($acceptedValue === '*') {
+                    $any = true;
+                }
+                foreach ($availableLanguages as $availableValue) {
+                    $availableQuality = 1.0;
                     $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
                     $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
                     if ($matchingGrade > 0) {
                     if ($matchingGrade > 0) {
                         $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
                         $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);

+ 1 - 1
lib/Model.php

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

+ 2 - 2
lib/Model/AbstractModel.php

@@ -77,8 +77,8 @@ abstract class AbstractModel
      */
      */
     public function __construct(Configuration $configuration, AbstractData $storage)
     public function __construct(Configuration $configuration, AbstractData $storage)
     {
     {
-        $this->_conf       = $configuration;
-        $this->_store      = $storage;
+        $this->_conf = $configuration;
+        $this->_store = $storage;
     }
     }
 
 
     /**
     /**

+ 25 - 5
lib/Model/Comment.php

@@ -56,6 +56,26 @@ class Comment extends AbstractModel
             throw new TranslatedException(self::COLLISION_ERROR, 69);
             throw new TranslatedException(self::COLLISION_ERROR, 69);
         }
         }
 
 
+        // Comment IDs must be unique within the paste, regardless of the
+        // parent, and the parent must be either the paste or an existing comment.
+        $commentid = $this->getId();
+        $parentid = $this->getParentId();
+        $parentExists = $parentid === $pasteid;
+        if ($commentid === $pasteid) {
+            throw new TranslatedException(self::COLLISION_ERROR, 72);
+        }
+        foreach ($this->_store->readComments($pasteid) as $comment) {
+            if ($comment['id'] === $commentid) {
+                throw new TranslatedException(self::COLLISION_ERROR, 69);
+            }
+            if ($comment['id'] === $parentid) {
+                $parentExists = true;
+            }
+        }
+        if (!$parentExists) {
+            throw new TranslatedException(self::INVALID_DATA_ERROR, 71);
+        }
+
         $this->_data['meta']['created'] = time();
         $this->_data['meta']['created'] = time();
 
 
         // store comment
         // store comment
@@ -94,7 +114,7 @@ class Comment extends AbstractModel
      */
      */
     public function setPaste(Paste &$paste)
     public function setPaste(Paste &$paste)
     {
     {
-        $this->_paste           = $paste;
+        $this->_paste = $paste;
         $this->_data['pasteid'] = $paste->getId();
         $this->_data['pasteid'] = $paste->getId();
     }
     }
 
 
@@ -150,10 +170,10 @@ class Comment extends AbstractModel
         $icon = $this->_conf->getKey('icon');
         $icon = $this->_conf->getKey('icon');
         if ($icon !== 'none') {
         if ($icon !== 'none') {
             $pngdata = '';
             $pngdata = '';
-            $hmac    = TrafficLimiter::getHash();
+            $hmac = TrafficLimiter::getHash();
             if ($icon === 'identicon') {
             if ($icon === 'identicon') {
                 $identicon = new Identicon();
                 $identicon = new Identicon();
-                $pngdata   = $identicon->getImageDataUri($hmac, 16);
+                $pngdata = $identicon->getImageDataUri($hmac, 16);
             } elseif ($icon === 'jdenticon') {
             } elseif ($icon === 'jdenticon') {
                 $jdenticon = new Jdenticon([
                 $jdenticon = new Jdenticon([
                     'hash'  => $hmac,
                     'hash'  => $hmac,
@@ -163,9 +183,9 @@ class Comment extends AbstractModel
                         'padding'           => 0,
                         'padding'           => 0,
                     ],
                     ],
                 ]);
                 ]);
-                $pngdata   = $jdenticon->getImageDataUri('png');
+                $pngdata = $jdenticon->getImageDataUri('png');
             } elseif ($icon === 'vizhash') {
             } elseif ($icon === 'vizhash') {
-                $vh      = new Vizhash16x16();
+                $vh = new Vizhash16x16();
                 $pngdata = 'data:image/png;base64,' . base64_encode(
                 $pngdata = 'data:image/png;base64,' . base64_encode(
                     $vh->generate($hmac)
                     $vh->generate($hmac)
                 );
                 );

+ 4 - 4
lib/Model/Paste.php

@@ -77,11 +77,11 @@ class Paste extends AbstractModel
             $this->delete();
             $this->delete();
         }
         }
 
 
-        $data['comments']       = array_values($this->getComments());
-        $data['comment_count']  = count($data['comments']);
+        $data['comments'] = array_values($this->getComments());
+        $data['comment_count'] = count($data['comments']);
         $data['comment_offset'] = 0;
         $data['comment_offset'] = 0;
-        $data['@context']       = '?jsonld=paste';
-        $this->_data            = $data;
+        $data['@context'] = '?jsonld=paste';
+        $this->_data = $data;
 
 
         return $this->_data;
         return $this->_data;
     }
     }

+ 2 - 2
lib/Persistence/PurgeLimiter.php

@@ -67,8 +67,8 @@ class PurgeLimiter extends AbstractPersistence
             return true;
             return true;
         }
         }
 
 
-        $now  = time();
-        $pl   = (int) self::$_store->getValue('purge_limiter');
+        $now = time();
+        $pl = (int) self::$_store->getValue('purge_limiter');
         if ($pl + self::$_limit >= $now) {
         if ($pl + self::$_limit >= $now) {
             return false;
             return false;
         }
         }

+ 3 - 3
lib/Persistence/TrafficLimiter.php

@@ -144,7 +144,7 @@ class TrafficLimiter extends AbstractPersistence
             $ipRange = trim($ipRange);
             $ipRange = trim($ipRange);
         }
         }
         $address = Factory::parseAddressString($_SERVER[self::$_ipKey]);
         $address = Factory::parseAddressString($_SERVER[self::$_ipKey]);
-        $range   = Factory::parseRangeString(
+        $range = Factory::parseRangeString(
             $ipRange,
             $ipRange,
             ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
             ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
         );
         );
@@ -200,8 +200,8 @@ class TrafficLimiter extends AbstractPersistence
 
 
         // used as array key, which are limited in length, hence using algo with shorter range
         // used as array key, which are limited in length, hence using algo with shorter range
         $hash = self::getHash('sha256');
         $hash = self::getHash('sha256');
-        $now  = time();
-        $tl   = (int) self::$_store->getValue('traffic_limiter', $hash);
+        $now = time();
+        $tl = (int) self::$_store->getValue('traffic_limiter', $hash);
         self::$_store->purgeValues('traffic_limiter', $now - self::$_limit);
         self::$_store->purgeValues('traffic_limiter', $now - self::$_limit);
         if ($tl === 0 || ($tl + self::$_limit) < $now) {
         if ($tl === 0 || ($tl + self::$_limit) < $now) {
             if (!self::$_store->setValue((string) $now, 'traffic_limiter', $hash)) {
             if (!self::$_store->setValue((string) $now, 'traffic_limiter', $hash)) {

+ 1 - 1
lib/Proxy/AbstractProxy.php

@@ -87,7 +87,7 @@ abstract class AbstractProxy
 
 
         if ($data === false) {
         if ($data === false) {
             $http_response_header = $http_response_header ?? [];
             $http_response_header = $http_response_header ?? [];
-            $statusCode           = '';
+            $statusCode = '';
             if (!empty($http_response_header) && preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $http_response_header[0], $matches)) {
             if (!empty($http_response_header) && preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $http_response_header[0], $matches)) {
                 $statusCode = $matches[1];
                 $statusCode = $matches[1];
             }
             }

+ 44 - 8
lib/Request.php

@@ -13,6 +13,7 @@ namespace PrivateBin;
 
 
 use JsonException;
 use JsonException;
 use PrivateBin\Model\Paste;
 use PrivateBin\Model\Paste;
+use PrivateBin\Exception\TranslatedException;
 
 
 /**
 /**
  * Request
  * Request
@@ -42,6 +43,14 @@ class Request
      */
      */
     const MIME_XHTML = 'application/xhtml+xml';
     const MIME_XHTML = 'application/xhtml+xml';
 
 
+    /**
+     * Input stream length for size checks
+     *
+     * @access private
+     * @var int
+     */
+    private $_bodyLength = 0;
+
     /**
     /**
      * Input stream to use for PUT parameter parsing
      * Input stream to use for PUT parameter parsing
      *
      *
@@ -111,7 +120,8 @@ class Request
                 // it might be a creation or a deletion, the latter is detected below
                 // it might be a creation or a deletion, the latter is detected below
                 $this->_operation = 'create';
                 $this->_operation = 'create';
                 try {
                 try {
-                    $data          = file_get_contents(self::$_inputStream);
+                    $data = file_get_contents(self::$_inputStream);
+                    $this->_bodyLength = strlen($data);
                     $this->_params = Json::decode($data);
                     $this->_params = Json::decode($data);
                     // a valid JSON scalar (number, bool or string) decodes
                     // a valid JSON scalar (number, bool or string) decodes
                     // without error, but is not a usable set of parameters
                     // without error, but is not a usable set of parameters
@@ -144,15 +154,31 @@ class Request
         }
         }
 
 
         // prepare operation, depending on current parameters
         // prepare operation, depending on current parameters
-        if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
-            if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) {
+        if (
+            array_key_exists('pasteid', $this->_params) &&
+            is_string($this->_params['pasteid']) &&
+            !empty($this->_params['pasteid'])
+        ) {
+            if (
+                array_key_exists('deletetoken', $this->_params) &&
+                is_string($this->_params['deletetoken']) &&
+                !empty($this->_params['deletetoken'])
+            ) {
                 $this->_operation = 'delete';
                 $this->_operation = 'delete';
             } elseif ($this->_operation !== 'create') {
             } elseif ($this->_operation !== 'create') {
                 $this->_operation = 'read';
                 $this->_operation = 'read';
             }
             }
-        } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) {
+        } elseif (
+            array_key_exists('jsonld', $this->_params) &&
+            is_string($this->_params['jsonld']) &&
+            !empty($this->_params['jsonld'])
+        ) {
             $this->_operation = 'jsonld';
             $this->_operation = 'jsonld';
-        } elseif (array_key_exists('link', $this->_params) && !empty($this->_params['link'])) {
+        } elseif (
+            array_key_exists('link', $this->_params) &&
+            is_string($this->_params['link']) &&
+            !empty($this->_params['link'])
+        ) {
             if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
             if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
                 $this->_operation = 'yourlsproxy';
                 $this->_operation = 'yourlsproxy';
             }
             }
@@ -180,15 +206,24 @@ class Request
      * Get data of paste or comment
      * Get data of paste or comment
      *
      *
      * @access public
      * @access public
+     * @param  int $sizelimit
+     * @throws JsonException
+     * @throws TranslatedException
      * @return array
      * @return array
      */
      */
-    public function getData()
+    public function getData(int $sizelimit)
     {
     {
+        if ($this->_bodyLength > $sizelimit) {
+            throw new TranslatedException([
+                'Document is limited to %s of encrypted data.',
+                Filter::formatHumanReadableSize($sizelimit)
+            ]);
+        }
         $data = [
         $data = [
             'adata' => $this->getParam('adata'),
             'adata' => $this->getParam('adata'),
         ];
         ];
         $required_keys = ['v', 'ct'];
         $required_keys = ['v', 'ct'];
-        $meta          = $this->getParam('meta');
+        $meta = $this->getParam('meta');
         if (empty($meta)) {
         if (empty($meta)) {
             $required_keys[] = 'pasteid';
             $required_keys[] = 'pasteid';
             $required_keys[] = 'parentid';
             $required_keys[] = 'parentid';
@@ -235,7 +270,8 @@ class Request
     public function getRequestUri()
     public function getRequestUri()
     {
     {
         $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
         $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
-        return empty($uri) ? '/' : parse_url($uri, PHP_URL_PATH);
+        $path = empty($uri) ? '' : parse_url($uri, PHP_URL_PATH);
+        return is_string($path) && $path !== '' ? $path : '/';
     }
     }
 
 
     /**
     /**

+ 4 - 1
lib/TemplateSwitcher.php

@@ -73,7 +73,10 @@ class TemplateSwitcher
      */
      */
     public static function getTemplate(): string
     public static function getTemplate(): string
     {
     {
-        if (array_key_exists('template', $_COOKIE)) {
+        if (
+            array_key_exists('template', $_COOKIE) &&
+            is_string($_COOKIE['template'])
+        ) {
             $template = basename($_COOKIE['template']);
             $template = basename($_COOKIE['template']);
             if (self::isTemplateAvailable($template)) {
             if (self::isTemplateAvailable($template)) {
                 return $template;
                 return $template;

+ 1 - 1
lib/View.php

@@ -49,7 +49,7 @@ class View
      */
      */
     public function draw($template)
     public function draw($template)
     {
     {
-        $dir  = PATH . 'tpl' . DIRECTORY_SEPARATOR;
+        $dir = PATH . 'tpl' . DIRECTORY_SEPARATOR;
         $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template;
         $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template;
         $path = $dir . $file . '.php';
         $path = $dir . $file . '.php';
         if (!is_file($path)) {
         if (!is_file($path)) {

+ 11 - 9
lib/Vizhash16x16.php

@@ -63,7 +63,7 @@ class Vizhash16x16
      */
      */
     public function __construct()
     public function __construct()
     {
     {
-        $this->width  = 16;
+        $this->width = 16;
         $this->height = 16;
         $this->height = 16;
     }
     }
 
 
@@ -110,10 +110,10 @@ class Vizhash16x16
 
 
         for ($i = 0; $i < 7; ++$i) {
         for ($i = 0; $i < 7; ++$i) {
             $action = $this->getInt();
             $action = $this->getInt();
-            $color  = imagecolorallocate($image, $r, $g, $b);
-            $r      = $r0      = (int) ($r0 + $this->getInt() / 25) % 256;
-            $g      = $g0      = (int) ($g0 + $this->getInt() / 25) % 256;
-            $b      = $b0      = (int) ($b0 + $this->getInt() / 25) % 256;
+            $color = imagecolorallocate($image, $r, $g, $b);
+            $r = $r0 = (int) ($r0 + $this->getInt() / 25) % 256;
+            $g = $g0 = (int) ($g0 + $this->getInt() / 25) % 256;
+            $b = $b0 = (int) ($b0 + $this->getInt() / 25) % 256;
             $this->drawshape($image, $action, $color);
             $this->drawshape($image, $action, $color);
         }
         }
 
 
@@ -123,7 +123,9 @@ class Vizhash16x16
         imagepng($image);
         imagepng($image);
         $imagedata = ob_get_contents();
         $imagedata = ob_get_contents();
         ob_end_clean();
         ob_end_clean();
-        imagedestroy($image);
+        if (version_compare(PHP_VERSION, '8.5', '<') && defined('imagedestroy')) {
+            imagedestroy($image);
+        }
 
 
         return $imagedata;
         return $imagedata;
     }
     }
@@ -180,10 +182,10 @@ class Vizhash16x16
     private function degrade($img, $direction, $color1, $color2)
     private function degrade($img, $direction, $color1, $color2)
     {
     {
         if ($direction === 'h') {
         if ($direction === 'h') {
-            $size    = imagesx($img);
+            $size = imagesx($img);
             $sizeinv = imagesy($img);
             $sizeinv = imagesy($img);
         } else {
         } else {
-            $size    = imagesy($img);
+            $size = imagesy($img);
             $sizeinv = imagesx($img);
             $sizeinv = imagesx($img);
         }
         }
         $diffs = [
         $diffs = [
@@ -228,7 +230,7 @@ class Vizhash16x16
                 break;
                 break;
             default:
             default:
                 $start = (int) ($this->getInt() * 360 / 256);
                 $start = (int) ($this->getInt() * 360 / 256);
-                $end   = (int) ($start + $this->getInt() * 180 / 256);
+                $end = (int) ($start + $this->getInt() * 180 / 256);
                 imagefilledarc($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $start, $end, $color, IMG_ARC_PIE);
                 imagefilledarc($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $start, $end, $color, IMG_ARC_PIE);
         }
         }
     }
     }

+ 5 - 5
tpl/bootstrap5.php

@@ -49,7 +49,7 @@ if ($MARKDOWN) :
 <?php
 <?php
 endif;
 endif;
 ?>
 ?>
-		<?php $this->_scriptTag('js/purify-3.4.12.js', 'defer'); ?>
+		<?php $this->_scriptTag('js/purify-3.4.17.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
 		<!-- icon -->
 		<!-- icon -->
@@ -158,12 +158,12 @@ endif;
 					<span class="navbar-toggler-icon"></span>
 					<span class="navbar-toggler-icon"></span>
 				</button>
 				</button>
 				<div id="navbar" class="collapse navbar-collapse">
 				<div id="navbar" class="collapse navbar-collapse">
-					<ul class="navbar-nav me-auto gap-2 align-items-lg-center align-items-stretch">
+					<ul class="navbar-nav me-auto gap-2 align-items-lg-center align-items-stretch flex-wrap">
 						<li id="loadingindicator" class="navbar-text hidden me-auto">
 						<li id="loadingindicator" class="navbar-text hidden me-auto">
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#clock" /></svg>
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#clock" /></svg>
 							<?php echo I18n::_('Loading…'), PHP_EOL; ?>
 							<?php echo I18n::_('Loading…'), PHP_EOL; ?>
 						</li>
 						</li>
-						<li class="nav-item d-flex flex-lg-row flex-column">
+						<li class="nav-item d-flex flex-lg-row flex-column hidden">
 							<button id="retrybutton" type="button" class="reloadlink hidden btn btn-primary d-flex justify-content-center align-items-center gap-1">
 							<button id="retrybutton" type="button" class="reloadlink hidden btn btn-primary d-flex justify-content-center align-items-center gap-1">
 								<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#repeat" /></svg> <?php echo I18n::_('Retry'), PHP_EOL; ?>
 								<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#repeat" /></svg> <?php echo I18n::_('Retry'), PHP_EOL; ?>
 							</button>
 							</button>
@@ -449,7 +449,7 @@ if (!empty($URLSHORTENER)) :
 								<?php if ($SHORTENBYDEFAULT) : ?>
 								<?php if ($SHORTENBYDEFAULT) : ?>
 								data-autoshorten="true"
 								data-autoshorten="true"
 								<?php endif; ?>
 								<?php endif; ?>
-								type="button" class="btn btn-primary btn-block d-flex justify-content-center align-items-center gap-1"
+								type="button" class="btn btn-primary btn-block d-flex justify-content-center align-items-center gap-1<?php echo $SHORTENBYDEFAULT ? ' hidden' : ''; ?>"
 						>
 						>
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#send" /></svg> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#send" /></svg> <?php echo I18n::_('Shorten URL'), PHP_EOL; ?>
 						</button>
 						</button>
@@ -469,7 +469,7 @@ endif;
 					<li role="presentation" class="nav-item me-1"><a class="nav-link active" role="tab" id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
 					<li role="presentation" class="nav-item me-1"><a class="nav-link active" role="tab" id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
 					<li role="presentation" class="nav-item me-1"><a class="nav-link" role="tab" id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
 					<li role="presentation" class="nav-item me-1"><a class="nav-link" role="tab" id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
 					<li role="presentation" class="nav-item ms-auto">
 					<li role="presentation" class="nav-item ms-auto">
-						<button id="sendbutton" type="button" tabindex="2" class="hidden btn btn-primary d-flex justify-content-center align-items-center gap-1">
+						<button id="sendbutton" type="button" tabindex="2" class="hidden btn btn-primary d-flex justify-content-center align-items-center gap-1" title="<?php echo I18n::_('Hit %s+s to create the document, storing it on %s', I18n::getCopyHotkey(), I18n::_($NAME)); ?>" aria-keyshortcuts="Control+S Meta+S">
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#cloud-upload" /></svg> <?php echo I18n::_('Create'), PHP_EOL; ?>
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#cloud-upload" /></svg> <?php echo I18n::_('Create'), PHP_EOL; ?>
 						</button>
 						</button>
 					</li>
 					</li>

+ 1 - 1
tst/AdministrationEmptyDirsTest.php

@@ -17,7 +17,7 @@ class AdministrationEmptyDirsTest extends TestCase
         mkdir($dataPath . DIRECTORY_SEPARATOR . 'empty' . DIRECTORY_SEPARATOR . 'nested', 0777, true);
         mkdir($dataPath . DIRECTORY_SEPARATOR . 'empty' . DIRECTORY_SEPARATOR . 'nested', 0777, true);
         file_put_contents($dataPath . DIRECTORY_SEPARATOR . 'keep.txt', 'keep');
         file_put_contents($dataPath . DIRECTORY_SEPARATOR . 'keep.txt', 'keep');
 
 
-        $options                         = parse_ini_file(CONF_SAMPLE, true);
+        $options = parse_ini_file(CONF_SAMPLE, true);
         $options['model_options']['dir'] = $dataPath;
         $options['model_options']['dir'] = $dataPath;
         Helper::createIniFile(
         Helper::createIniFile(
             $this->_path . DIRECTORY_SEPARATOR . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php',
             $this->_path . DIRECTORY_SEPARATOR . 'cfg' . DIRECTORY_SEPARATOR . 'conf.php',

+ 34 - 26
tst/Bootstrap.php

@@ -75,7 +75,14 @@ class Helper
      *
      *
      * @var string
      * @var string
      */
      */
-    private static $commentid = '5a52eebf11c4c94b';
+    private static $commentid = '33a5671d11b29d01';
+
+    /**
+     * example comment cipher text, its hash is the comment ID
+     *
+     * @var string
+     */
+    private static $commentCt = 'XNE9E3AGszrjQfV5bxxSuaflS05MViI3rYtG1Nph0Qc=';
 
 
     /**
     /**
      * example comment meta data
      * example comment meta data
@@ -112,9 +119,9 @@ class Helper
      */
      */
     public static function getPaste(array $meta = []): array
     public static function getPaste(array $meta = []): array
     {
     {
-        $example                 = self::$pasteV2;
+        $example = self::$pasteV2;
         $example['meta']['salt'] = ServerSalt::generate();
         $example['meta']['salt'] = ServerSalt::generate();
-        $example['meta']         = array_merge($example['meta'], $meta);
+        $example['meta'] = array_merge($example['meta'], $meta);
         return $example;
         return $example;
     }
     }
 
 
@@ -126,7 +133,7 @@ class Helper
      */
      */
     public static function getPastePost(array $meta = []): array
     public static function getPastePost(array $meta = []): array
     {
     {
-        $example         = self::getPaste($meta);
+        $example = self::getPaste($meta);
         $example['meta'] = ['expire' => $example['meta']['expire']];
         $example['meta'] = ['expire' => $example['meta']['expire']];
         return $example;
         return $example;
     }
     }
@@ -160,8 +167,9 @@ class Helper
      */
      */
     public static function getComment(array $meta = []): array
     public static function getComment(array $meta = []): array
     {
     {
-        $example            = self::$pasteV2;
-        $example['adata']   = $example['adata'][0];
+        $example = self::$pasteV2;
+        $example['adata'] = $example['adata'][0];
+        $example['ct'] = self::$commentCt;
         $example['pasteid'] = $example['parentid'] = self::getPasteId();
         $example['pasteid'] = $example['parentid'] = self::getPasteId();
         unset($example['meta']['expire']);
         unset($example['meta']['expire']);
         $example['meta'] = array_merge($example['meta'], self::$commentV2);
         $example['meta'] = array_merge($example['meta'], self::$commentV2);
@@ -351,7 +359,7 @@ class Helper
         }
         }
 
 
         $counter = 0;
         $counter = 0;
-        $file    = PATH . 'lib' . DIRECTORY_SEPARATOR . 'Configuration.php';
+        $file = PATH . 'lib' . DIRECTORY_SEPARATOR . 'Configuration.php';
         $content = preg_replace_callback(
         $content = preg_replace_callback(
             '#\'js/([a-z0-9.-]+.js)(\' +)=\> \'[^\']*\',#',
             '#\'js/([a-z0-9.-]+.js)(\' +)=\> \'[^\']*\',#',
             function ($matches) use (&$counter) {
             function ($matches) use (&$counter) {
@@ -385,11 +393,11 @@ class BucketStub extends Bucket
 
 
     public function __construct(ConnectionInterface $connection, $name, array $info = [], $client = null)
     public function __construct(ConnectionInterface $connection, $name, array $info = [], $client = null)
     {
     {
-        $this->_name       = $name;
-        $this->_info       = $info;
+        $this->_name = $name;
+        $this->_info = $info;
         $this->_connection = $connection;
         $this->_connection = $connection;
-        $this->_objects    = [];
-        $this->_client     = $client;
+        $this->_objects = [];
+        $this->_client = $client;
     }
     }
 
 
     public function acl()
     public function acl()
@@ -413,9 +421,9 @@ class BucketStub extends Bucket
             throw new BadMethodCallException('not supported by this stub');
             throw new BadMethodCallException('not supported by this stub');
         }
         }
 
 
-        $name                             = $options['name'];
-        $generation                       = '1';
-        $o                                = new StorageObjectStub($this->_connection, $name, $this, $generation, $options);
+        $name = $options['name'];
+        $generation = '1';
+        $o = new StorageObjectStub($this->_connection, $name, $this, $generation, $options);
         $this->_objects[$options['name']] = $o;
         $this->_objects[$options['name']] = $o;
         $o->setData($data);
         $o->setData($data);
     }
     }
@@ -548,12 +556,12 @@ class StorageObjectStub extends StorageObject
 
 
     public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = [], $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;
-        $this->_generation                      = $generation;
-        $this->_info                            = $info;
-        $this->_connection                      = $connection;
-        $timeCreated                            = new DateTime();
+        $this->_name = $name;
+        $this->_bucket = $bucket;
+        $this->_generation = $generation;
+        $this->_info = $info;
+        $this->_connection = $connection;
+        $timeCreated = new DateTime();
         $this->_info['metadata']['timeCreated'] = $timeCreated->format('Y-m-d\TH:i:s.u\Z');
         $this->_info['metadata']['timeCreated'] = $timeCreated->format('Y-m-d\TH:i:s.u\Z');
     }
     }
 
 
@@ -677,7 +685,7 @@ class StorageObjectStub extends StorageObject
 
 
     public function setData($data)
     public function setData($data)
     {
     {
-        $this->_data   = $data;
+        $this->_data = $data;
         $this->_exists = true;
         $this->_exists = true;
     }
     }
 }
 }
@@ -878,14 +886,14 @@ class ConnectionInterfaceStub implements ConnectionInterface
  */
  */
 class StorageClientStub extends StorageClient
 class StorageClientStub extends StorageClient
 {
 {
-    private $_config         = null;
-    private $_connection     = null;
+    private $_config = null;
+    private $_connection = null;
     private static $_buckets = [];
     private static $_buckets = [];
 
 
     public function __construct(array $config = [])
     public function __construct(array $config = [])
     {
     {
-        $this->_config     = $config;
-        $this->_connection =  new ConnectionInterfaceStub();
+        $this->_config = $config;
+        $this->_connection = new ConnectionInterfaceStub();
     }
     }
 
 
     public function bucket($name, $userProject = false, array $config = [])
     public function bucket($name, $userProject = false, array $config = [])
@@ -958,7 +966,7 @@ class StorageClientStub extends StorageClient
         if (array_key_exists($name, self::$_buckets)) {
         if (array_key_exists($name, self::$_buckets)) {
             throw new BadRequestException('already exists');
             throw new BadRequestException('already exists');
         }
         }
-        $b                     = new BucketStub($this->_connection, $name, [], $this);
+        $b = new BucketStub($this->_connection, $name, [], $this);
         self::$_buckets[$name] = $b;
         self::$_buckets[$name] = $b;
         return $b;
         return $b;
     }
     }

+ 20 - 20
tst/ConfigurationTest.php

@@ -15,10 +15,10 @@ class ConfigurationTest extends TestCase
     {
     {
         /* Setup Routine */
         /* Setup Routine */
         Helper::confBackup();
         Helper::confBackup();
-        $this->_minimalConfig                   = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]';
-        $this->_options                         = Configuration::getDefaults();
+        $this->_minimalConfig = '[main]' . PHP_EOL . '[model]' . PHP_EOL . '[model_options]';
+        $this->_options = Configuration::getDefaults();
         $this->_options['model_options']['dir'] = PATH . $this->_options['model_options']['dir'];
         $this->_options['model_options']['dir'] = PATH . $this->_options['model_options']['dir'];
-        $this->_path                            = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_cfg';
+        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_cfg';
         if (!is_dir($this->_path)) {
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
             mkdir($this->_path);
         }
         }
@@ -98,19 +98,19 @@ class ConfigurationTest extends TestCase
 
 
     public function testHandleWrongTypes()
     public function testHandleWrongTypes()
     {
     {
-        $original_options                                    = $this->_options;
+        $original_options = $this->_options;
         $original_options['main']['syntaxhighlightingtheme'] = 'foo';
         $original_options['main']['syntaxhighlightingtheme'] = 'foo';
-        $options                                             = $original_options;
-        $options['main']['discussion']                       = 'true';
-        $options['main']['opendiscussion']                   = 0;
-        $options['main']['password']                         = -1; // evaluates to TRUE
-        $options['main']['fileupload']                       = 'false';
-        $options['expire_options']['foo']                    = 'bar';
-        $options['formatter_options'][]                      = 'foo';
+        $options = $original_options;
+        $options['main']['discussion'] = 'true';
+        $options['main']['opendiscussion'] = 0;
+        $options['main']['password'] = -1; // evaluates to TRUE
+        $options['main']['fileupload'] = 'false';
+        $options['expire_options']['foo'] = 'bar';
+        $options['formatter_options'][] = 'foo';
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
-        $conf                                      = new Configuration;
+        $conf = new Configuration;
         $original_options['expire_options']['foo'] = intval('bar');
         $original_options['expire_options']['foo'] = intval('bar');
-        $original_options['formatter_options'][0]  = 'foo';
+        $original_options['formatter_options'][0] = 'foo';
         $this->assertEquals($original_options, $conf->get(), 'incorrect types are corrected');
         $this->assertEquals($original_options, $conf->get(), 'incorrect types are corrected');
     }
     }
 
 
@@ -120,23 +120,23 @@ class ConfigurationTest extends TestCase
         unset($options['expire_options']['1week']);
         unset($options['expire_options']['1week']);
         unset($options['expire_options']['1year']);
         unset($options['expire_options']['1year']);
         unset($options['expire_options']['never']);
         unset($options['expire_options']['never']);
-        $sri_key                         = array_key_first($options['sri']);
-        $valid_sri                       = $options['sri'][$sri_key];
-        $options['sri'][$sri_key]        = ''; // empty string should get replaced with default
+        $sri_key = array_key_first($options['sri']);
+        $valid_sri = $options['sri'][$sri_key];
+        $options['sri'][$sri_key] = ''; // empty string should get replaced with default
         $options['sri']['js/example.js'] = 'some invalid SRI hash';
         $options['sri']['js/example.js'] = 'some invalid SRI hash';
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
-        $conf                         = new Configuration;
+        $conf = new Configuration;
         // restore expected results
         // restore expected results
         $options['expire']['default'] = '5min';
         $options['expire']['default'] = '5min';
-        $options['sri'][$sri_key]     = $valid_sri;
+        $options['sri'][$sri_key] = $valid_sri;
         $this->assertEquals($options, $conf->get(), 'not overriding "missing" subkeys');
         $this->assertEquals($options, $conf->get(), 'not overriding "missing" subkeys');
     }
     }
 
 
     public function testConfigPath()
     public function testConfigPath()
     {
     {
         // setup
         // setup
-        $configFile              = $this->_path . DIRECTORY_SEPARATOR . 'conf.php';
-        $options                 = $this->_options;
+        $configFile = $this->_path . DIRECTORY_SEPARATOR . 'conf.php';
+        $options = $this->_options;
         $options['main']['name'] = 'OtherBin';
         $options['main']['name'] = 'OtherBin';
         Helper::createIniFile($configFile, $options);
         Helper::createIniFile($configFile, $options);
 
 

+ 149 - 122
tst/ControllerTest.php

@@ -17,8 +17,8 @@ class ControllerTest extends TestCase
     public function setUp(): void
     public function setUp(): void
     {
     {
         /* Setup Routine */
         /* Setup Routine */
-        $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
-        $this->_data  = new Filesystem(['dir' => $this->_path]);
+        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_data = new Filesystem(['dir' => $this->_path]);
         ServerSalt::setStore($this->_data);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();
         $this->reset();
@@ -34,13 +34,13 @@ class ControllerTest extends TestCase
 
 
     public function reset()
     public function reset()
     {
     {
-        $_POST   = [];
-        $_GET    = [];
+        $_POST = [];
+        $_GET = [];
         $_SERVER = [];
         $_SERVER = [];
         if ($this->_data->exists(Helper::getPasteId())) {
         if ($this->_data->exists(Helper::getPasteId())) {
             $this->_data->delete(Helper::getPasteId());
             $this->_data->delete(Helper::getPasteId());
         }
         }
-        $options                         = parse_ini_file(CONF_SAMPLE, true);
+        $options = parse_ini_file(CONF_SAMPLE, true);
         $options['model_options']['dir'] = $this->_path;
         $options['model_options']['dir'] = $this->_path;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
     }
     }
@@ -50,9 +50,9 @@ class ControllerTest extends TestCase
      */
      */
     public function testView()
     public function testView()
     {
     {
-        $_SERVER['HTTP_HOST']             = 'example.com';
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['HTTP_HOST'] = 'example.com';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -79,7 +79,7 @@ class ControllerTest extends TestCase
      */
      */
     public function testViewLanguageSelection()
     public function testViewLanguageSelection()
     {
     {
-        $options                              = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['main']['languageselection'] = true;
         $options['main']['languageselection'] = true;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $_COOKIE['lang'] = 'de';
         $_COOKIE['lang'] = 'de';
@@ -99,9 +99,9 @@ class ControllerTest extends TestCase
      */
      */
     public function testViewForceLanguageDefault()
     public function testViewForceLanguageDefault()
     {
     {
-        $options                              = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['main']['languageselection'] = false;
         $options['main']['languageselection'] = false;
-        $options['main']['languagedefault']   = 'fr';
+        $options['main']['languagedefault'] = 'fr';
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $_COOKIE['lang'] = 'de';
         $_COOKIE['lang'] = 'de';
         ob_start();
         ob_start();
@@ -120,9 +120,9 @@ class ControllerTest extends TestCase
      */
      */
     public function testViewForceRegionalLanguageDefault()
     public function testViewForceRegionalLanguageDefault()
     {
     {
-        $options                              = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['main']['languageselection'] = false;
         $options['main']['languageselection'] = false;
-        $options['main']['languagedefault']   = 'zh-tw';
+        $options['main']['languagedefault'] = 'zh-tw';
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $_COOKIE['lang'] = 'de';
         $_COOKIE['lang'] = 'de';
         ob_start();
         ob_start();
@@ -142,8 +142,8 @@ class ControllerTest extends TestCase
      */
      */
     public function testViewUrlShortener()
     public function testViewUrlShortener()
     {
     {
-        $shortener                       = 'https://shortener.example.com/api?link=';
-        $options                         = parse_ini_file(CONF, true);
+        $shortener = 'https://shortener.example.com/api?link=';
+        $options = parse_ini_file(CONF, true);
         $options['main']['urlshortener'] = $shortener;
         $options['main']['urlshortener'] = $shortener;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $_COOKIE['lang'] = 'de';
         $_COOKIE['lang'] = 'de';
@@ -171,9 +171,9 @@ class ControllerTest extends TestCase
      */
      */
     public function testNonDefaultConf()
     public function testNonDefaultConf()
     {
     {
-        $newConfig   = new class extends Configuration {};
+        $newConfig = new class extends Configuration {};
         $configValue = (new ReflectionClass(Controller::class))->getProperty('_conf');
         $configValue = (new ReflectionClass(Controller::class))->getProperty('_conf');
-        if (version_compare(PHP_VERSION, '8.1') < 0) {
+        if (version_compare(PHP_VERSION, '8.1', '<')) {
             // > This function has been DEPRECATED as of PHP 8.5.0. [...]
             // > This function has been DEPRECATED as of PHP 8.5.0. [...]
             // > As of PHP 8.1.0, calling this method has no effect; all properties are accessible by default.
             // > As of PHP 8.1.0, calling this method has no effect; all properties are accessible by default.
             // @see: https://www.php.net/manual/en/reflectionproperty.setaccessible.php
             // @see: https://www.php.net/manual/en/reflectionproperty.setaccessible.php
@@ -190,16 +190,16 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreate()
     public function testCreate()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -221,16 +221,16 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidTimelimit()
     public function testCreateInvalidTimelimit()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson(['expire' => 25]);
         $paste = Helper::getPasteJson(['expire' => 25]);
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         // Explicitly invoke TrafficLimiter to ensure limit applies to this test
         // Explicitly invoke TrafficLimiter to ensure limit applies to this test
         TrafficLimiter::canPass();
         TrafficLimiter::canPass();
         ob_start();
         ob_start();
@@ -253,15 +253,42 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidSize()
     public function testCreateInvalidSize()
     {
     {
-        $options                      = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['main']['sizelimit'] = 10;
         $options['main']['sizelimit'] = 10;
-        $options['traffic']['limit']  = 0;
+        $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        ob_start();
+        new Controller;
+        $content = ob_get_contents();
+        ob_end_clean();
+        $response = json_decode($content, true);
+        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data');
+    }
+
+    /**
+     * @runInSeparateProcess
+     */
+    public function testCreateOverSize()
+    {
+        $options                      = parse_ini_file(CONF, true);
+        $options['main']['sizelimit'] = 1000;
+        $options['traffic']['limit']  = 0;
+        Helper::createIniFile(CONF, $options);
+        $paste = Helper::getPastePost();
+        // add some extra authenticated data, could be a hypothetical future cryptographic hash, exceeding the sizelimit
+        $paste['adata'][] = str_repeat('0', $options['main']['sizelimit']);
+        $file  = Helper::createTempFile();
+        file_put_contents($file, json_encode($paste));
+        Request::setInputStream($file);
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['REQUEST_METHOD']        = 'POST';
         $_SERVER['REQUEST_METHOD']        = 'POST';
         $_SERVER['REMOTE_ADDR']           = '::1';
         $_SERVER['REMOTE_ADDR']           = '::1';
         ob_start();
         ob_start();
@@ -278,17 +305,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateProxyHeader()
     public function testCreateProxyHeader()
     {
     {
-        $options                      = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['header'] = 'X_FORWARDED_FOR';
         $options['traffic']['header'] = 'X_FORWARDED_FOR';
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
-        $_SERVER['HTTP_X_FORWARDED_FOR']  = '::2';
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = '::2';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -309,18 +336,18 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateDuplicateId()
     public function testCreateDuplicateId()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -335,17 +362,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateValidExpire()
     public function testCreateValidExpire()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        $time                             = time();
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        $time = time();
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -367,17 +394,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateValidExpireWithDiscussion()
     public function testCreateValidExpireWithDiscussion()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        $time                             = time();
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        $time = time();
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -400,16 +427,16 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidExpire()
     public function testCreateInvalidExpire()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPasteJson(['expire' => 'foo']);
         $paste = Helper::getPasteJson(['expire' => 'foo']);
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -430,17 +457,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidBurn()
     public function testCreateInvalidBurn()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
-        $paste             = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][3] = 'neither 1 nor 0';
         $paste['adata'][3] = 'neither 1 nor 0';
-        $file              = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, json_encode($paste));
         file_put_contents($file, json_encode($paste));
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -455,17 +482,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidOpenDiscussion()
     public function testCreateInvalidOpenDiscussion()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
-        $paste             = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][2] = 'neither 1 nor 0';
         $paste['adata'][2] = 'neither 1 nor 0';
-        $file              = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, json_encode($paste));
         file_put_contents($file, json_encode($paste));
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -484,12 +511,12 @@ class ControllerTest extends TestCase
     public function testCreateBrokenUpload()
     public function testCreateBrokenUpload()
     {
     {
         $paste = substr(Helper::getPasteJson(), 0, -10);
         $paste = substr(Helper::getPasteJson(), 0, -10);
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste does not exists before posting data');
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste does not exists before posting data');
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -506,12 +533,12 @@ class ControllerTest extends TestCase
     public function testCreateTooSoon()
     public function testCreateTooSoon()
     {
     {
         $paste = Helper::getPasteJson();
         $paste = Helper::getPasteJson();
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $paste);
         file_put_contents($file, $paste);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         ob_end_clean();
         ob_end_clean();
@@ -530,15 +557,15 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidFormat()
     public function testCreateInvalidFormat()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $file = Helper::createTempFile();
         $file = Helper::createTempFile();
         file_put_contents($file, '{"data":"","meta":{}}');
         file_put_contents($file, '{"data":"","meta":{}}');
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -553,17 +580,17 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateComment()
     public function testCreateComment()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $comment = Helper::getCommentJson();
         $comment = Helper::getCommentJson();
-        $file    = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $comment);
         file_put_contents($file, $comment);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        $paste                            = Helper::getPaste();
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -579,18 +606,18 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateInvalidComment()
     public function testCreateInvalidComment()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
-        $comment             = Helper::getCommentPost();
+        $comment = Helper::getCommentPost();
         $comment['parentid'] = 'foo';
         $comment['parentid'] = 'foo';
-        $file                = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, json_encode($comment));
         file_put_contents($file, json_encode($comment));
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        $paste                            = Helper::getPaste();
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -606,18 +633,18 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateCommentDiscussionDisabled()
     public function testCreateCommentDiscussionDisabled()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $comment = Helper::getCommentJson();
         $comment = Helper::getCommentJson();
-        $file    = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $comment);
         file_put_contents($file, $comment);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
-        $paste                            = Helper::getPaste();
-        $paste['adata'][2]                = 0;
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
+        $paste = Helper::getPaste();
+        $paste['adata'][2] = 0;
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -633,16 +660,16 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateCommentInvalidPaste()
     public function testCreateCommentInvalidPaste()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $comment = Helper::getCommentJson();
         $comment = Helper::getCommentJson();
-        $file    = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $comment);
         file_put_contents($file, $comment);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -657,28 +684,28 @@ class ControllerTest extends TestCase
      */
      */
     public function testCreateDuplicateComment()
     public function testCreateDuplicateComment()
     {
     {
-        $options                     = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['traffic']['limit'] = 0;
         $options['traffic']['limit'] = 0;
         Helper::createIniFile(CONF, $options);
         Helper::createIniFile(CONF, $options);
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         $comment = Helper::getComment();
         $comment = Helper::getComment();
-        $this->_data->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId(), $comment);
-        $this->assertTrue($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'comment exists before posting data');
+        $this->_data->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment);
+        $this->assertTrue($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists before posting data');
         $comment = Helper::getCommentJson();
         $comment = Helper::getCommentJson();
-        $file    = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, $comment);
         file_put_contents($file, $comment);
         Request::setInputStream($file);
         Request::setInputStream($file);
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
-        $_SERVER['REMOTE_ADDR']           = '::1';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
+        $_SERVER['REMOTE_ADDR'] = '::1';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
         ob_end_clean();
         ob_end_clean();
         $response = json_decode($content, true);
         $response = json_decode($content, true);
         $this->assertEquals(1, $response['status'], 'outputs error status');
         $this->assertEquals(1, $response['status'], 'outputs error status');
-        $this->assertTrue($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'paste exists after posting data');
+        $this->assertTrue($this->_data->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'paste exists after posting data');
     }
     }
 
 
     /**
     /**
@@ -686,8 +713,8 @@ class ControllerTest extends TestCase
      */
      */
     public function testReadInvalidId()
     public function testReadInvalidId()
     {
     {
-        $_SERVER['QUERY_STRING']          = 'foo';
-        $_GET['foo']                      = '';
+        $_SERVER['QUERY_STRING'] = 'foo';
+        $_GET['foo'] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -703,8 +730,8 @@ class ControllerTest extends TestCase
      */
      */
     public function testReadNonexisting()
     public function testReadNonexisting()
     {
     {
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -722,8 +749,8 @@ class ControllerTest extends TestCase
     {
     {
         $expiredPaste = Helper::getPaste(['expire_date' => 1344803344]);
         $expiredPaste = Helper::getPaste(['expire_date' => 1344803344]);
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -739,11 +766,11 @@ class ControllerTest extends TestCase
      */
      */
     public function testReadBurn()
     public function testReadBurn()
     {
     {
-        $paste             = Helper::getPaste();
+        $paste = Helper::getPaste();
         $paste['adata'][3] = 1;
         $paste['adata'][3] = 1;
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -771,8 +798,8 @@ class ControllerTest extends TestCase
     {
     {
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -796,12 +823,12 @@ class ControllerTest extends TestCase
      */
      */
     public function testReadBurnAfterReading()
     public function testReadBurnAfterReading()
     {
     {
-        $burnPaste             = Helper::getPaste();
+        $burnPaste = Helper::getPaste();
         $burnPaste['adata'][3] = 1;
         $burnPaste['adata'][3] = 1;
         $this->_data->create(Helper::getPasteId(), $burnPaste);
         $this->_data->create(Helper::getPasteId(), $burnPaste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -820,8 +847,8 @@ class ControllerTest extends TestCase
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $paste               = $this->_data->read(Helper::getPasteId());
-        $_GET['pasteid']     = Helper::getPasteId();
+        $paste = $this->_data->read(Helper::getPasteId());
+        $_GET['pasteid'] = Helper::getPasteId();
         $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']);
         $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $paste['meta']['salt']);
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -842,7 +869,7 @@ class ControllerTest extends TestCase
     {
     {
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
-        $_GET['pasteid']     = 'foo';
+        $_GET['pasteid'] = 'foo';
         $_GET['deletetoken'] = 'bar';
         $_GET['deletetoken'] = 'bar';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -861,7 +888,7 @@ class ControllerTest extends TestCase
      */
      */
     public function testDeleteInexistantId()
     public function testDeleteInexistantId()
     {
     {
-        $_GET['pasteid']     = Helper::getPasteId();
+        $_GET['pasteid'] = Helper::getPasteId();
         $_GET['deletetoken'] = 'bar';
         $_GET['deletetoken'] = 'bar';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -881,7 +908,7 @@ class ControllerTest extends TestCase
     {
     {
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
-        $_GET['pasteid']     = Helper::getPasteId();
+        $_GET['pasteid'] = Helper::getPasteId();
         $_GET['deletetoken'] = 'bar';
         $_GET['deletetoken'] = 'bar';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
@@ -903,15 +930,15 @@ class ControllerTest extends TestCase
         $paste = Helper::getPaste();
         $paste = Helper::getPaste();
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->_data->create(Helper::getPasteId(), $paste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $file  = Helper::createTempFile();
+        $file = Helper::createTempFile();
         file_put_contents($file, json_encode([
         file_put_contents($file, json_encode([
             'deletetoken' => 'burnafterreading',
             'deletetoken' => 'burnafterreading',
         ]));
         ]));
         Request::setInputStream($file);
         Request::setInputStream($file);
-        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
-        $_GET[Helper::getPasteId()]       = '';
+        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_GET[Helper::getPasteId()] = '';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
-        $_SERVER['REQUEST_METHOD']        = 'POST';
+        $_SERVER['REQUEST_METHOD'] = 'POST';
         ob_start();
         ob_start();
         new Controller;
         new Controller;
         $content = ob_get_contents();
         $content = ob_get_contents();
@@ -930,7 +957,7 @@ class ControllerTest extends TestCase
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste does not exist before being created');
         $this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste does not exist before being created');
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
         $this->_data->create(Helper::getPasteId(), $expiredPaste);
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
-        $_GET['pasteid']     = Helper::getPasteId();
+        $_GET['pasteid'] = Helper::getPasteId();
         $_GET['deletetoken'] = 'does not matter in this context, but has to be set';
         $_GET['deletetoken'] = 'does not matter in this context, but has to be set';
         ob_start();
         ob_start();
         new Controller;
         new Controller;

+ 2 - 2
tst/ControllerWithDbTest.php

@@ -25,7 +25,7 @@ class ControllerWithDbTest extends ControllerTest
             mkdir($this->_path);
             mkdir($this->_path);
         }
         }
         $this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3';
         $this->_options['dsn'] = 'sqlite:' . $this->_path . DIRECTORY_SEPARATOR . 'tst.sq3';
-        $this->_data           = new Database($this->_options);
+        $this->_data = new Database($this->_options);
         ServerSalt::setStore($this->_data);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();
         $this->reset();
@@ -35,7 +35,7 @@ class ControllerWithDbTest extends ControllerTest
     {
     {
         parent::reset();
         parent::reset();
         // but then inject a db config
         // but then inject a db config
-        $options          = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['model'] = [
         $options['model'] = [
             'class' => 'Database',
             'class' => 'Database',
         ];
         ];

+ 4 - 4
tst/ControllerWithGcsTest.php

@@ -17,9 +17,9 @@ class ControllerWithGcsTest extends ControllerTest
     public static function setUpBeforeClass(): void
     public static function setUpBeforeClass(): void
     {
     {
         $httpClient = new Client(['debug'=>false]);
         $httpClient = new Client(['debug'=>false]);
-        $handler    = HttpHandlerFactory::build($httpClient);
+        $handler = HttpHandlerFactory::build($httpClient);
 
 
-        $name     = 'pb-';
+        $name = 'pb-';
         $alphabet = 'abcdefghijklmnopqrstuvwxyz';
         $alphabet = 'abcdefghijklmnopqrstuvwxyz';
         for ($i = 0; $i < 29; ++$i) {
         for ($i = 0; $i < 29; ++$i) {
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
@@ -39,7 +39,7 @@ class ControllerWithGcsTest extends ControllerTest
             'bucket' => self::$_bucket->name(),
             'bucket' => self::$_bucket->name(),
             'prefix' => 'pastes',
             'prefix' => 'pastes',
         ];
         ];
-        $this->_data    = new GoogleCloudStorage($this->_options);
+        $this->_data = new GoogleCloudStorage($this->_options);
         ServerSalt::setStore($this->_data);
         ServerSalt::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         TrafficLimiter::setStore($this->_data);
         $this->reset();
         $this->reset();
@@ -49,7 +49,7 @@ class ControllerWithGcsTest extends ControllerTest
     {
     {
         parent::reset();
         parent::reset();
         // but then inject a db config
         // but then inject a db config
-        $options          = parse_ini_file(CONF, true);
+        $options = parse_ini_file(CONF, true);
         $options['model'] = [
         $options['model'] = [
             'class' => 'GoogleCloudStorage',
             'class' => 'GoogleCloudStorage',
         ];
         ];

+ 21 - 21
tst/Data/DatabaseTest.php

@@ -22,7 +22,7 @@ class DatabaseTest extends TestCase
     public function setUp(): void
     public function setUp(): void
     {
     {
         /* Setup Routine */
         /* Setup Routine */
-        $this->_path  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_model = new Database($this->_options);
         $this->_model = new Database($this->_options);
     }
     }
 
 
@@ -63,22 +63,22 @@ class DatabaseTest extends TestCase
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
 
 
         // storing comments
         // storing comments
-        $comment1             = Helper::getComment();
-        $comment2             = Helper::getComment();
-        $meta1                = $comment1['meta'];
-        $meta2                = $comment2['meta'];
+        $comment1 = Helper::getComment();
+        $comment2 = Helper::getComment();
+        $meta1 = $comment1['meta'];
+        $meta2 = $comment2['meta'];
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment 1 does not yet exist');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment 1 does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment1) !== false, 'store comment 1');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment1) !== false, 'store comment 1');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment 2 exists after storing it');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment 2 exists after storing it');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'comment 2 does not yet exist');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'comment 2 does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId(), $comment2) !== false, 'store comment 2');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId(), $comment2) !== false, 'store comment 2');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'comment 2 exists after storing it');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getPasteId()), 'comment 2 exists after storing it');
-        $comment1['id']       = Helper::getCommentId();
+        $comment1['id'] = Helper::getCommentId();
         $comment1['parentid'] = Helper::getPasteId();
         $comment1['parentid'] = Helper::getPasteId();
-        $comment1['meta']     = $meta1;
-        $comment2['id']       = Helper::getPasteId();
+        $comment1['meta'] = $meta1;
+        $comment2['id'] = Helper::getPasteId();
         $comment2['parentid'] = Helper::getPasteId();
         $comment2['parentid'] = Helper::getPasteId();
-        $comment2['meta']     = $meta2;
+        $comment2['meta'] = $meta2;
         $this->assertEquals(
         $this->assertEquals(
             [
             [
                 $comment1['meta']['created']        => $comment1,
                 $comment1['meta']['created']        => $comment1,
@@ -98,7 +98,7 @@ class DatabaseTest extends TestCase
     {
     {
         $error_log_setting = ini_get('error_log');
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
         $this->_model->delete(Helper::getPasteId());
-        $original                          = $paste                                = Helper::getPaste(['expire_date' => 1344803344]);
+        $original = $paste = Helper::getPaste(['expire_date' => 1344803344]);
         $paste['meta']['burnafterreading'] = $original['meta']['burnafterreading'] = true;
         $paste['meta']['burnafterreading'] = $original['meta']['burnafterreading'] = true;
         $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist');
         $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->create(Helper::getPasteId(), $paste), 'store new paste');
@@ -112,16 +112,16 @@ class DatabaseTest extends TestCase
     public function testCommentsWithSameTimestampRemainInInsertionOrder()
     public function testCommentsWithSameTimestampRemainInInsertionOrder()
     {
     {
         $pasteid = Helper::getPasteId();
         $pasteid = Helper::getPasteId();
-        $paste   = Helper::getPaste();
+        $paste = Helper::getPaste();
         $this->_model->delete($pasteid);
         $this->_model->delete($pasteid);
         $this->assertTrue($this->_model->create($pasteid, $paste));
         $this->assertTrue($this->_model->create($pasteid, $paste));
 
 
         $expectedIds = [];
         $expectedIds = [];
         for ($i = 1; $i <= 12; ++$i) {
         for ($i = 1; $i <= 12; ++$i) {
-            $comment                    = Helper::getComment();
+            $comment = Helper::getComment();
             $comment['meta']['created'] = 1735689600;
             $comment['meta']['created'] = 1735689600;
-            $commentid                  = sprintf('%016x', $i);
-            $expectedIds[]              = $commentid;
+            $commentid = sprintf('%016x', $i);
+            $expectedIds[] = $commentid;
             $this->assertTrue(
             $this->assertTrue(
                 $this->_model->createComment($pasteid, $pasteid, $commentid, $comment)
                 $this->_model->createComment($pasteid, $pasteid, $commentid, $comment)
             );
             );
@@ -145,9 +145,9 @@ class DatabaseTest extends TestCase
     {
     {
         $this->_model->delete(Helper::getPasteId());
         $this->_model->delete(Helper::getPasteId());
         $expired = Helper::getPaste(['expire_date' => 1344803344]);
         $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     = [];
+        $paste = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids = [];
         foreach ($keys as $key) {
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $ids[$key] = hash('fnv164', $key);
             $this->_model->delete($ids[$key]);
             $this->_model->delete($ids[$key]);
@@ -189,7 +189,7 @@ class DatabaseTest extends TestCase
     {
     {
         $error_log_setting = ini_get('error_log');
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
         $this->_model->delete(Helper::getPasteId());
-        $data    = Helper::getPaste();
+        $data = Helper::getPaste();
         $comment = Helper::getComment(['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->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->create(Helper::getPasteId(), $data), 'store new paste');
@@ -309,7 +309,7 @@ class DatabaseTest extends TestCase
         }
         }
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['dsn'] = 'sqlite:' . $path;
         $this->_options['tbl'] = 'foo_';
         $this->_options['tbl'] = 'foo_';
-        $db                    = new PDO(
+        $db = new PDO(
             $this->_options['dsn'],
             $this->_options['dsn'],
             $this->_options['usr'],
             $this->_options['usr'],
             $this->_options['pwd'],
             $this->_options['pwd'],
@@ -347,9 +347,9 @@ class DatabaseTest extends TestCase
 
 
     public function testOciClob()
     public function testOciClob()
     {
     {
-        $int    = (int) random_bytes(1);
+        $int = (int) random_bytes(1);
         $string = random_bytes(10);
         $string = random_bytes(10);
-        $clob   = fopen('php://memory', 'r+');
+        $clob = fopen('php://memory', 'r+');
         fwrite($clob, $string);
         fwrite($clob, $string);
         rewind($clob);
         rewind($clob);
         $this->assertEquals($int, Database::_sanitizeClob($int));
         $this->assertEquals($int, Database::_sanitizeClob($int));

+ 16 - 16
tst/Data/FilesystemTest.php

@@ -14,9 +14,9 @@ class FilesystemTest extends TestCase
     public function setUp(): void
     public function setUp(): void
     {
     {
         /* Setup Routine */
         /* Setup Routine */
-        $this->_path        = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
+        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
         $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
         $this->_invalidPath = $this->_path . DIRECTORY_SEPARATOR . 'bar';
-        $this->_model       = new Filesystem(['dir' => $this->_path]);
+        $this->_model = new Filesystem(['dir' => $this->_path]);
         if (!is_dir($this->_path)) {
         if (!is_dir($this->_path)) {
             mkdir($this->_path);
             mkdir($this->_path);
         }
         }
@@ -45,12 +45,12 @@ class FilesystemTest extends TestCase
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
 
 
         // storing comments
         // storing comments
-        $comment             = Helper::getComment();
+        $comment = Helper::getComment();
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'store comment');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'store comment');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
         $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store the same comment twice');
         $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store the same comment twice');
-        $comment['id']       = Helper::getCommentId();
+        $comment['id'] = Helper::getCommentId();
         $comment['parentid'] = Helper::getPasteId();
         $comment['parentid'] = Helper::getPasteId();
         $this->assertEquals(
         $this->assertEquals(
             [$comment['meta']['created'] => $comment],
             [$comment['meta']['created'] => $comment],
@@ -77,9 +77,9 @@ class FilesystemTest extends TestCase
 
 
     public function testCorruptCommentsAreIgnored()
     public function testCorruptCommentsAreIgnored()
     {
     {
-        $pasteid   = Helper::getPasteId();
+        $pasteid = Helper::getPasteId();
         $commentid = Helper::getCommentId();
         $commentid = Helper::getCommentId();
-        $comment   = Helper::getComment();
+        $comment = Helper::getComment();
         $this->assertTrue($this->_model->createComment($pasteid, $pasteid, $commentid, $comment));
         $this->assertTrue($this->_model->createComment($pasteid, $pasteid, $commentid, $comment));
 
 
         $discussionPath = $this->_path . DIRECTORY_SEPARATOR . substr($pasteid, 0, 2) .
         $discussionPath = $this->_path . DIRECTORY_SEPARATOR . substr($pasteid, 0, 2) .
@@ -117,9 +117,9 @@ class FilesystemTest extends TestCase
     {
     {
         mkdir($this->_path . DIRECTORY_SEPARATOR . '00', 0777, true);
         mkdir($this->_path . DIRECTORY_SEPARATOR . '00', 0777, true);
         $expired = Helper::getPaste(['expire_date' => 1344803344]);
         $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     = [];
+        $paste = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids = [];
         foreach ($keys as $key) {
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $ids[$key] = hash('fnv164', $key);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
@@ -161,7 +161,7 @@ class FilesystemTest extends TestCase
     {
     {
         $error_log_setting = ini_get('error_log');
         $error_log_setting = ini_get('error_log');
         $this->_model->delete(Helper::getPasteId());
         $this->_model->delete(Helper::getPasteId());
-        $data    = Helper::getPaste();
+        $data = Helper::getPaste();
         $comment = Helper::getComment(['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->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->create(Helper::getPasteId(), $data), 'store new paste');
@@ -176,12 +176,12 @@ class FilesystemTest extends TestCase
     public function testOldFilesGetConverted()
     public function testOldFilesGetConverted()
     {
     {
         // generate 10 (default purge batch size) pastes in the old format
         // generate 10 (default purge batch size) pastes in the old format
-        $paste     = Helper::getPaste();
-        $comment   = Helper::getComment();
+        $paste = Helper::getPaste();
+        $comment = Helper::getComment();
         $commentid = Helper::getCommentId();
         $commentid = Helper::getCommentId();
-        $ids       = [];
+        $ids = [];
         for ($i = 0, $max = 10; $i < $max; ++$i) {
         for ($i = 0, $max = 10; $i < $max; ++$i) {
-            $dataid     = Helper::getRandomId();
+            $dataid = Helper::getRandomId();
             $storagedir = $this->_path . DIRECTORY_SEPARATOR . substr($dataid, 0, 2) .
             $storagedir = $this->_path . DIRECTORY_SEPARATOR . substr($dataid, 0, 2) .
                 DIRECTORY_SEPARATOR . substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
                 DIRECTORY_SEPARATOR . substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
             $ids[$dataid] = $storagedir;
             $ids[$dataid] = $storagedir;
@@ -225,8 +225,8 @@ class FilesystemTest extends TestCase
             );
             );
             $this->assertFileDoesNotExist($storagedir . $dataid . '.' . $commentid . '.' . $dataid, "old format comment of $dataid got removed");
             $this->assertFileDoesNotExist($storagedir . $dataid . '.' . $commentid . '.' . $dataid, "old format comment of $dataid got removed");
             $this->assertTrue($this->_model->existsComment($dataid, $dataid, $commentid), "comment in paste $dataid exists");
             $this->assertTrue($this->_model->existsComment($dataid, $dataid, $commentid), "comment in paste $dataid exists");
-            $comment             = $comment;
-            $comment['id']       = $commentid;
+            $comment = $comment;
+            $comment['id'] = $commentid;
             $comment['parentid'] = $dataid;
             $comment['parentid'] = $dataid;
             $this->assertEquals($this->_model->readComments($dataid), [$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");
         }
         }

+ 9 - 9
tst/Data/GoogleCloudStorageTest.php

@@ -14,9 +14,9 @@ class GoogleCloudStorageTest extends TestCase
     public static function setUpBeforeClass(): void
     public static function setUpBeforeClass(): void
     {
     {
         $httpClient = new Client(['debug'=>false]);
         $httpClient = new Client(['debug'=>false]);
-        $handler    = HttpHandlerFactory::build($httpClient);
+        $handler = HttpHandlerFactory::build($httpClient);
 
 
-        $name     = 'pb-';
+        $name = 'pb-';
         $alphabet = 'abcdefghijklmnopqrstuvwxyz';
         $alphabet = 'abcdefghijklmnopqrstuvwxyz';
         for ($i = 0; $i < 29; ++$i) {
         for ($i = 0; $i < 29; ++$i) {
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
             $name .= $alphabet[rand(0, strlen($alphabet) - 1)];
@@ -59,12 +59,12 @@ class GoogleCloudStorageTest extends TestCase
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
         $this->assertEquals($paste, $this->_model->read(Helper::getPasteId()));
 
 
         // storing comments
         // storing comments
-        $comment             = Helper::getComment();
+        $comment = Helper::getComment();
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'store comment');
         $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'store comment');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it');
         $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store the same comment twice');
         $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store the same comment twice');
-        $comment['id']       = Helper::getCommentId();
+        $comment['id'] = Helper::getCommentId();
         $comment['parentid'] = Helper::getPasteId();
         $comment['parentid'] = Helper::getPasteId();
         $this->assertEquals(
         $this->assertEquals(
             [$comment['meta']['created'] => $comment],
             [$comment['meta']['created'] => $comment],
@@ -84,9 +84,9 @@ class GoogleCloudStorageTest extends TestCase
     public function testPurge()
     public function testPurge()
     {
     {
         $expired = Helper::getPaste(['expire_date' => 1344803344]);
         $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     = [];
+        $paste = Helper::getPaste(['expire_date' => time() + 3600]);
+        $keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'];
+        $ids = [];
         foreach ($keys as $key) {
         foreach ($keys as $key) {
             $ids[$key] = hash('fnv164', $key);
             $ids[$key] = hash('fnv164', $key);
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
             $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist");
@@ -123,7 +123,7 @@ class GoogleCloudStorageTest extends TestCase
     public function testCommentErrorDetection()
     public function testCommentErrorDetection()
     {
     {
         $this->_model->delete(Helper::getPasteId());
         $this->_model->delete(Helper::getPasteId());
-        $data    = Helper::getPaste();
+        $data = Helper::getPaste();
         $comment = Helper::getComment(['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->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->create(Helper::getPasteId(), $data), 'store new paste');
@@ -170,7 +170,7 @@ class GoogleCloudStorageTest extends TestCase
      */
      */
     public function testKeyValuePurgeTrafficLimiter()
     public function testKeyValuePurgeTrafficLimiter()
     {
     {
-        $salt   = bin2hex(random_bytes(256));
+        $salt = bin2hex(random_bytes(256));
         $client = hash_hmac('sha512', '127.0.0.1', $salt);
         $client = hash_hmac('sha512', '127.0.0.1', $salt);
         $expire = time();
         $expire = time();
         $this->_model->setValue(strval($expire), 'traffic_limiter', $client);
         $this->_model->setValue(strval($expire), 'traffic_limiter', $client);

+ 35 - 27
tst/FormatV2Test.php

@@ -7,7 +7,7 @@ class FormatV2Test extends TestCase
 {
 {
     public function testFormatV2ValidatorValidatesCorrectly()
     public function testFormatV2ValidatorValidatesCorrectly()
     {
     {
-        $paste   = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $comment = Helper::getCommentPost();
         $comment = Helper::getCommentPost();
         $this->assertTrue(FormatV2::isValid($paste), 'valid format');
         $this->assertTrue(FormatV2::isValid($paste), 'valid format');
         $this->assertTrue(FormatV2::isValid($comment, true), 'valid format');
         $this->assertTrue(FormatV2::isValid($comment, true), 'valid format');
@@ -15,57 +15,65 @@ class FormatV2Test extends TestCase
         $paste['adata'][0][0] = '$';
         $paste['adata'][0][0] = '$';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of iv');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of iv');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][1] = '$';
         $paste['adata'][0][1] = '$';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of salt');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of salt');
 
 
-        $paste       = Helper::getPastePost();
+        $paste = Helper::getPastePost();
+        $paste['adata'][] = 0;
+        $this->assertFalse(FormatV2::isValid($paste), 'invalid extra adata entry');
+
+        $paste = Helper::getPastePost();
+        $paste['adata'][] = 0;
+        $this->assertFalse(FormatV2::isValid($paste), 'invalid extra adata entry');
+
+        $paste = Helper::getPastePost();
         $paste['ct'] = '$';
         $paste['ct'] = '$';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of ct');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of ct');
 
 
-        $paste       = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['ct'] = 'bm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhCg==';
         $paste['ct'] = 'bm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhCg==';
         $this->assertFalse(FormatV2::isValid($paste), 'low ct entropy');
         $this->assertFalse(FormatV2::isValid($paste), 'low ct entropy');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][0] = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=';
         $paste['adata'][0][0] = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=';
         $this->assertFalse(FormatV2::isValid($paste), 'iv too long');
         $this->assertFalse(FormatV2::isValid($paste), 'iv too long');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][1] = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=';
         $paste['adata'][0][1] = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=';
         $this->assertFalse(FormatV2::isValid($paste), 'salt too long');
         $this->assertFalse(FormatV2::isValid($paste), 'salt too long');
 
 
-        $paste        = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['foo'] = 'bar';
         $paste['foo'] = 'bar';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid additional key');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid additional key');
         unset($paste['meta']);
         unset($paste['meta']);
         $this->assertFalse(FormatV2::isValid($paste), 'invalid missing key');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid missing key');
 
 
-        $paste      = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['v'] = 0.9;
         $paste['v'] = 0.9;
         $this->assertFalse(FormatV2::isValid($paste), 'unsupported version');
         $this->assertFalse(FormatV2::isValid($paste), 'unsupported version');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][2] = 1000;
         $paste['adata'][0][2] = 1000;
         $this->assertFalse(FormatV2::isValid($paste), 'not enough iterations');
         $this->assertFalse(FormatV2::isValid($paste), 'not enough iterations');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][3] = 127;
         $paste['adata'][0][3] = 127;
         $this->assertFalse(FormatV2::isValid($paste), 'invalid key size');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid key size');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][4] = 63;
         $paste['adata'][0][4] = 63;
         $this->assertFalse(FormatV2::isValid($paste), 'invalid tag length');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid tag length');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][5] = '!#@';
         $paste['adata'][0][5] = '!#@';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid algorithm');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid algorithm');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][6] = '!#@';
         $paste['adata'][0][6] = '!#@';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid mode');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid mode');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][7] = '!#@';
         $paste['adata'][0][7] = '!#@';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid compression');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid compression');
 
 
@@ -77,36 +85,36 @@ class FormatV2Test extends TestCase
     {
     {
         // malformed input must yield "false" (Invalid data) rather than a
         // malformed input must yield "false" (Invalid data) rather than a
         // fatal type error under strict_types
         // fatal type error under strict_types
-        $paste          = Helper::getPastePost();
-        $paste['v']     = 'abc';
+        $paste = Helper::getPastePost();
+        $paste['v'] = 'abc';
         $this->assertFalse(FormatV2::isValid($paste), 'non-numeric version');
         $this->assertFalse(FormatV2::isValid($paste), 'non-numeric version');
 
 
-        $paste          = Helper::getPastePost();
-        $paste['v']     = [];
+        $paste = Helper::getPastePost();
+        $paste['v'] = [];
         $this->assertFalse(FormatV2::isValid($paste), 'array version');
         $this->assertFalse(FormatV2::isValid($paste), 'array version');
 
 
-        $paste          = Helper::getPastePost();
-        $paste['ct']    = ['not', 'a', 'string'];
+        $paste = Helper::getPastePost();
+        $paste['ct'] = ['not', 'a', 'string'];
         $this->assertFalse(FormatV2::isValid($paste), 'non-string ciphertext');
         $this->assertFalse(FormatV2::isValid($paste), 'non-string ciphertext');
 
 
-        $paste          = Helper::getPastePost();
-        $paste['meta']  = 'not-an-array';
+        $paste = Helper::getPastePost();
+        $paste['meta'] = 'not-an-array';
         $this->assertFalse(FormatV2::isValid($paste), 'non-array meta');
         $this->assertFalse(FormatV2::isValid($paste), 'non-array meta');
 
 
-        $paste             = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0] = 'not-an-array';
         $paste['adata'][0] = 'not-an-array';
         $this->assertFalse(FormatV2::isValid($paste), 'non-array cipher parameters');
         $this->assertFalse(FormatV2::isValid($paste), 'non-array cipher parameters');
 
 
-        $paste             = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0] = [];
         $paste['adata'][0] = [];
         $this->assertFalse(FormatV2::isValid($paste), 'empty cipher parameters');
         $this->assertFalse(FormatV2::isValid($paste), 'empty cipher parameters');
 
 
-        $paste                = Helper::getPastePost();
+        $paste = Helper::getPastePost();
         $paste['adata'][0][0] = [];
         $paste['adata'][0][0] = [];
         $this->assertFalse(FormatV2::isValid($paste), 'non-string iv');
         $this->assertFalse(FormatV2::isValid($paste), 'non-string iv');
 
 
-        $comment           = Helper::getCommentPost();
-        $comment['ct']     = 42;
+        $comment = Helper::getCommentPost();
+        $comment['ct'] = 42;
         $this->assertFalse(FormatV2::isValid($comment, true), 'non-string comment ciphertext');
         $this->assertFalse(FormatV2::isValid($comment, true), 'non-string comment ciphertext');
     }
     }
 }
 }

+ 42 - 14
tst/I18nTest.php

@@ -44,7 +44,7 @@ class I18nTest extends TestCase
     public function testTranslationFallback()
     public function testTranslationFallback()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
-        $messageId                       = 'It does not matter if the message ID exists';
+        $messageId = 'It does not matter if the message ID exists';
         I18n::loadTranslations();
         I18n::loadTranslations();
         $this->assertEquals($messageId, I18n::_($messageId), 'fallback to en');
         $this->assertEquals($messageId, I18n::_($messageId), 'fallback to en');
         I18n::getLanguageLabels();
         I18n::getLanguageLabels();
@@ -80,6 +80,14 @@ class I18nTest extends TestCase
         $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in French');
         $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in French');
     }
     }
 
 
+    public function testEqualQualityPreservesBrowserOrder()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr;q=0.8,de;q=0.8';
+        I18n::loadTranslations();
+
+        $this->assertEquals('fr', I18n::getLanguage());
+    }
+
     public function testBulgarianMonthExpirationTranslation()
     public function testBulgarianMonthExpirationTranslation()
     {
     {
         $_COOKIE['lang'] = 'bg';
         $_COOKIE['lang'] = 'bg';
@@ -95,6 +103,20 @@ class I18nTest extends TestCase
         );
         );
     }
     }
 
 
+    public function testBrowserLanguageLtDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'lt;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('lt', I18n::getLanguage(), 'browser language lt');
+        $this->assertEquals('1 sekundę', I18n::_('%d seconds', 1), '1 second in Lithuanian');
+        $this->assertEquals('2 sekundės', I18n::_('%d seconds', 2), '2 seconds in Lithuanian');
+        $this->assertEquals('10 sekundžių', I18n::_('%d seconds', 10), '10 seconds in Lithuanian');
+        $this->assertEquals('22 sekundės', I18n::_('%d seconds', 22), '22 seconds in Lithuanian');
+        $this->assertEquals('110 sekundžių', I18n::_('%d seconds', 110), '110 seconds in Lithuanian');
+        $this->assertEquals('111 sekundžių', I18n::_('%d seconds', 111), '111 seconds in Lithuanian');
+        $this->assertEquals('120 sekundžių', I18n::_('%d seconds', 120), '120 seconds in Lithuanian');
+    }
+
     public function testBrowserLanguageNoDetection()
     public function testBrowserLanguageNoDetection()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'no;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'no;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
@@ -177,14 +199,14 @@ class I18nTest extends TestCase
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'sl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'sl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         I18n::loadTranslations();
         I18n::loadTranslations();
         $this->assertEquals('sl', I18n::getLanguage(), 'browser language sl');
         $this->assertEquals('sl', I18n::getLanguage(), 'browser language sl');
-        $this->assertEquals('0 ura',  I18n::_('%d hours', 0), '0 hours in Slovene');
-        $this->assertEquals('1 uri',  I18n::_('%d hours', 1), '1 hour in Slovene');
-        $this->assertEquals('2 ure', I18n::_('%d hours', 2), '2 hours in Slovene');
-        $this->assertEquals('3 ur',  I18n::_('%d hours', 3), '3 hours in Slovene');
-        $this->assertEquals('11 ura',  I18n::_('%d hours', 11), '11 hours in Slovene');
-        $this->assertEquals('101 uri',  I18n::_('%d hours', 101), '101 hours in Slovene');
-        $this->assertEquals('102 ure', I18n::_('%d hours', 102), '102 hours in Slovene');
-        $this->assertEquals('104 ur',  I18n::_('%d hours', 104), '104 hours in Slovene');
+        $this->assertEquals('0 ur',  I18n::_('%d hours', 0), '0 hours in Slovene');
+        $this->assertEquals('1 ura',  I18n::_('%d hours', 1), '1 hour in Slovene');
+        $this->assertEquals('2 uri', I18n::_('%d hours', 2), '2 hours in Slovene');
+        $this->assertEquals('3 ure',  I18n::_('%d hours', 3), '3 hours in Slovene');
+        $this->assertEquals('11 ur',  I18n::_('%d hours', 11), '11 hours in Slovene');
+        $this->assertEquals('101 ura',  I18n::_('%d hours', 101), '101 hours in Slovene');
+        $this->assertEquals('102 uri', I18n::_('%d hours', 102), '102 hours in Slovene');
+        $this->assertEquals('104 ure',  I18n::_('%d hours', 104), '104 hours in Slovene');
     }
     }
 
 
     public function testBrowserLanguageCsDetection()
     public function testBrowserLanguageCsDetection()
@@ -198,6 +220,12 @@ class I18nTest extends TestCase
         $this->assertEquals('14 minut',  I18n::_('%d minutes', 14), '14 minutes in Czech');
         $this->assertEquals('14 minut',  I18n::_('%d minutes', 14), '14 minutes in Czech');
     }
     }
 
 
+    public function testInvalidBrowserLanguageQualityIsIgnored()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'de;q=2,en;q=0.8';
+        $this->assertSame(['0.8' => ['en']], I18n::getBrowserLanguages());
+    }
+
     public function testBrowserLanguageAnyDetection()
     public function testBrowserLanguageAnyDetection()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '*';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '*';
@@ -216,7 +244,7 @@ class I18nTest extends TestCase
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         I18n::loadTranslations();
         I18n::loadTranslations();
-        $input  = '&<>"\'/`=';
+        $input = '&<>"\'/`=';
         $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
         $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
         $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities');
         $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities');
         $this->assertEquals('<a>some ' . $result . ' + 1</a>', I18n::_('<a>some %s + %d</a>', $input, 1), 'encodes parameters in translations');
         $this->assertEquals('<a>some ' . $result . ' + 1</a>', I18n::_('<a>some %s + %d</a>', $input, 1), 'encodes parameters in translations');
@@ -299,7 +327,7 @@ class I18nTest extends TestCase
     public function testMessageIdsExistInAllLanguages()
     public function testMessageIdsExistInAllLanguages()
     {
     {
         $messageIds = [];
         $messageIds = [];
-        $languages  = [];
+        $languages = [];
         foreach (new DirectoryIterator(PATH . 'i18n') as $file) {
         foreach (new DirectoryIterator(PATH . 'i18n') as $file) {
             $fileNameLength = strlen($file->getFilename());
             $fileNameLength = strlen($file->getFilename());
             if ($fileNameLength === 7) {       // xx.json
             if ($fileNameLength === 7) {       // xx.json
@@ -311,9 +339,9 @@ class I18nTest extends TestCase
             } else {
             } else {
                 continue;
                 continue;
             }
             }
-            $languageJson         = file_get_contents($file->getPathname());
-            $languageMessageIds   = array_keys(Json::decode($languageJson));
-            $messageIds           = array_unique(array_merge($messageIds, $languageMessageIds));
+            $languageJson = file_get_contents($file->getPathname());
+            $languageMessageIds = array_keys(Json::decode($languageJson));
+            $messageIds = array_unique(array_merge($messageIds, $languageMessageIds));
             $languages[$language] = $languageMessageIds;
             $languages[$language] = $languageMessageIds;
         }
         }
         foreach ($messageIds as $messageId) {
         foreach ($messageIds as $messageId) {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است