Просмотр исходного кода

Merge pull request #1711 from PrivateBin/1.7-backport-2.0.3

backport security fixes for 1.7
El RIDO 8 месяцев назад
Родитель
Сommit
0618a9dd74
57 измененных файлов с 1012 добавлено и 435 удалено
  1. 9 5
      .github/workflows/codeql-analysis.yml
  2. 5 1
      .github/workflows/release.yml
  3. 13 3
      .github/workflows/snyk-scan.yml
  4. 1 0
      .github/workflows/test-results.yml
  5. 101 7
      .github/workflows/tests.yml
  6. 1 0
      .gitignore
  7. 2 4
      .scrutinizer.yml
  8. 2 6
      BADGES.md
  9. 10 0
      CHANGELOG.md
  10. 3 2
      bin/configuration-test-generator
  11. 4 0
      codacy-analysis.yml
  12. 1 1
      composer.json
  13. 104 48
      composer.lock
  14. 0 4
      css/bootstrap5/bootstrap-5.3.3.css
  15. 4 0
      css/bootstrap5/bootstrap-5.3.8.css
  16. 0 4
      css/bootstrap5/bootstrap.rtl-5.3.3.css
  17. 4 0
      css/bootstrap5/bootstrap.rtl-5.3.8.css
  18. 14 6
      doc/Installation.md
  19. 3 3
      doc/README.md
  20. 1 1
      doc/Running Unit Tests.md
  21. 54 50
      js/base-x-5.0.1.js
  22. 0 5
      js/bootstrap-5.3.3.js
  23. 5 0
      js/bootstrap-5.3.8.js
  24. 2 2
      js/common.js
  25. 1 0
      js/kjua-0.10.0.js
  26. 0 1
      js/kjua-0.9.0.js
  27. 7 2
      js/legacy.js
  28. 58 55
      js/privatebin.js
  29. 0 1
      js/purify-3.2.6.js
  30. 1 0
      js/purify-3.3.0.js
  31. 2 1
      js/test/Prompt.js
  32. 6 6
      lib/Configuration.php
  33. 8 4
      lib/Controller.php
  34. 6 0
      lib/Data/Filesystem.php
  35. 4 10
      lib/Persistence/TrafficLimiter.php
  36. 14 34
      lib/TemplateSwitcher.php
  37. 8 28
      lib/View.php
  38. 4 4
      tpl/bootstrap.php
  39. 6 6
      tpl/bootstrap5.php
  40. 3 0
      tst/Bootstrap.php
  41. 5 13
      tst/ViewTest.php
  42. 45 51
      vendor/composer/ClassLoader.php
  43. 1 0
      vendor/composer/autoload_classmap.php
  44. 1 0
      vendor/composer/autoload_static.php
  45. 9 9
      vendor/composer/installed.php
  46. 3 2
      vendor/mlocati/ip-lib/src/Address/AddressInterface.php
  47. 14 16
      vendor/mlocati/ip-lib/src/Address/IPv4.php
  48. 15 17
      vendor/mlocati/ip-lib/src/Address/IPv6.php
  49. 15 5
      vendor/mlocati/ip-lib/src/Range/AbstractRange.php
  50. 16 1
      vendor/mlocati/ip-lib/src/Range/Pattern.php
  51. 14 4
      vendor/mlocati/ip-lib/src/Range/RangeInterface.php
  52. 10 0
      vendor/mlocati/ip-lib/src/Range/Single.php
  53. 16 1
      vendor/mlocati/ip-lib/src/Range/Subnet.php
  54. 11 11
      vendor/mlocati/ip-lib/src/Range/Type.php
  55. 112 0
      vendor/mlocati/ip-lib/src/Service/BinaryMath.php
  56. 253 0
      vendor/mlocati/ip-lib/src/Service/NumberInChunks.php
  57. 1 1
      vendor/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php

+ 9 - 5
.github/workflows/codeql-analysis.yml

@@ -18,6 +18,10 @@ on:
   schedule:
     - cron: '28 22 * * 5'
 
+permissions:
+  contents: read
+  security-events: write
+
 jobs:
   analyze:
     name: Analyze
@@ -26,18 +30,18 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        language: [ 'javascript' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+        language: [ 'actions', 'javascript' ]
+        # CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
         # Learn more:
         # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v3
+      uses: github/codeql-action/init@v4
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,4 +50,4 @@ jobs:
         # queries: ./path/to/local/query, your-org/your-repo/queries@main
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v3
+      uses: github/codeql-action/analyze@v4

+ 5 - 1
.github/workflows/release.yml

@@ -4,12 +4,16 @@ on:
   push:
     tags: '[0-9]+.[0-9]?[0-9]?[0-9]?.?[0-9]+'
 
+permissions: {}
+
 jobs:
   draft:
+    permissions:
+      contents: write
     runs-on: ubuntu-latest
     steps:
       - name: Fetch changelog from tag
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           sparse-checkout: CHANGELOG.md
           sparse-checkout-cone-mode: false

+ 13 - 3
.github/workflows/snyk-scan.yml

@@ -8,13 +8,23 @@ on:
     branches: [ master ]
   pull_request:
     branches: [ master ]
+
+permissions:
+  contents: read
+  security-events: write
+
 jobs:
   # https://github.com/snyk/actions/tree/master/php
   snyk-php:
     runs-on: ubuntu-latest
-    if: ${{ github.repository == 'PrivateBin/PrivateBin' }}
+    if: |
+      github.repository == 'PrivateBin/PrivateBin' && (
+      github.event.pull_request.author_association == 'COLLABORATOR' ||
+      github.event.pull_request.author_association == 'CONTRIBUTOR' ||
+      github.event.pull_request.author_association == 'MEMBER' ||
+      github.event.pull_request.author_association == 'OWNER' )
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
       - name: Install Google Cloud Storage
         run: composer require --no-update google/cloud-storage && composer update --no-dev
       - name: Run Snyk to check for vulnerabilities
@@ -25,6 +35,6 @@ jobs:
         with:
           args: --sarif-file-output=snyk.sarif
       - name: Upload result to GitHub Code Scanning
-        uses: github/codeql-action/upload-sarif@v3
+        uses: github/codeql-action/upload-sarif@v4
         with:
           sarif_file: snyk.sarif

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

@@ -5,6 +5,7 @@ on:
     workflows: ["Tests"]
     types:
       - completed
+
 permissions: {}
 
 jobs:

+ 101 - 7
.github/workflows/tests.yml

@@ -1,15 +1,18 @@
 name: Tests
+
 on:
   push:
   workflow_dispatch:
 
+permissions: {}
+
 jobs:
 
   Composer:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
     - name: Validate composer.json and composer.lock
       run: composer validate
     - name: Install dependencies
@@ -36,7 +39,7 @@ jobs:
 
     # let's get started!
     - name: Checkout
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
 
     # cache PHP extensions
     - name: Setup cache environment
@@ -106,20 +109,111 @@ jobs:
 
     - name: Upload Test Results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: Test Results (PHP ${{ matrix.php-versions }})
         path: tst/results.xml
 
+  PHPunitConfigCombinations:
+    name: PHP configuration combination unit tests
+    runs-on: ubuntu-latest
+    env:
+      php-version: '8.4'
+      extensions: gd, sqlite3
+      extensions-cache-key-name: phpextensions
+
+    steps:
+
+    # let's get started!
+    - name: Checkout
+      uses: actions/checkout@v5
+
+    # cache PHP extensions
+    - name: Setup cache environment
+      id: extcache
+      uses: shivammathur/cache-extensions@v1
+      with:
+        php-version: ${{ env.php-version }}
+        extensions: ${{ env.extensions }}
+        key: ${{ runner.os }}-${{ env.extensions-cache-key }}
+
+    - name: Cache extensions
+      uses: actions/cache@v4
+      with:
+        path: ${{ steps.extcache.outputs.dir }}
+        key: ${{ steps.extcache.outputs.key }}
+        restore-keys: ${{ runner.os }}-${{ env.extensions-cache-key }}
+
+    - name: Setup PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: ${{ env.php-version }}
+        extensions: ${{ env.extensions }}
+
+    # Setup GitHub CI PHP problem matchers
+    # https://github.com/shivammathur/setup-php#problem-matchers
+    - name: Setup problem matchers for PHP
+      run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
+
+    - name: Setup problem matchers for PHPUnit
+      run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
+
+    # composer cache
+    - name: Remove composer lock
+      run: rm composer.lock
+
+    - name: Get composer cache directory
+      id: composer-cache
+      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+    # http://man7.org/linux/man-pages/man1/date.1.html
+    # https://github.com/actions/cache#creating-a-cache-key
+    - name: Get Date
+      id: get-date
+      run: echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
+      shell: bash
+
+    - name: Cache dependencies
+      uses: actions/cache@v4
+      with:
+        path: "${{ steps.composer-cache.outputs.dir }}"
+        key: "${{ runner.os }}-composer-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/composer.json') }}"
+        restore-keys: "${{ runner.os }}-composer-${{ steps.get-date.outputs.date }}-"
+
+    # composer installation
+    - name: Unset platform requirement
+      run: composer config --unset platform
+
+    - name: Setup PHPunit
+      run: composer install -n
+
+    - name: Install Google Cloud Storage
+      run: composer require google/cloud-storage
+
+    # testing
+    - name: Generate configuration combination unit tests
+      run: bin/configuration-test-generator
+
+    - name: Run unit tests
+      run: ../vendor/bin/phpunit --no-coverage --log-junit results.xml ConfigurationCombinationsTest.php
+      working-directory: tst
+
+    - name: Upload Test Results
+      if: always()
+      uses: actions/upload-artifact@v5
+      with:
+        name: Test Results
+        path: tst/results.xml
+
   Mocha:
     runs-on: ubuntu-latest
     steps:
 
     - name: Checkout
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
 
     - name: Setup Node
-      uses: actions/setup-node@v4
+      uses: actions/setup-node@v6
       with:
         node-version: '18'
         cache: 'npm'
@@ -138,7 +232,7 @@ jobs:
 
     - name: Upload Test Results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: Test Results (Mocha)
         path: js/mocha-results.xml
@@ -148,7 +242,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Upload
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: Event File
         path: "${{ github.event_path }}"

+ 1 - 0
.gitignore

@@ -25,6 +25,7 @@ vendor/**/tst
 vendor/**/tests
 vendor/**/build_phar.php
 !vendor/**/*.php
+vendor/bin/**
 
 # Ignore local node modules, unit testing logs, api docs and IDE project files
 js/node_modules/

+ 2 - 4
.scrutinizer.yml

@@ -15,14 +15,12 @@ coding_style:
                 additive: false
                 concatenation: true
 build:
-    image: default-bionic
     environment:
         php:
-            version: 7.4
+            version: 8.2
     tests:
         override:
-            -
-                command: 'composer require google/cloud-storage && cd tst && ../vendor/bin/phpunit'
+            -   command: 'composer require google/cloud-storage && cd tst && XDEBUG_MODE=coverage ../vendor/bin/phpunit'
                 coverage:
                     file: 'tst/log/coverage-clover.xml'
                     format: 'clover'

+ 2 - 6
BADGES.md

@@ -1,11 +1,7 @@
 # Badges
 
-[![Build Status](https://travis-ci.org/PrivateBin/PrivateBin.svg?branch=master)](https://travis-ci.org/PrivateBin/PrivateBin) [![Build Status](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/build.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/build-status/master)  
-[![Codacy Badge](https://api.codacy.com/project/badge/Grade/094500f62abf4c9aa0c8a8a4520e4789)](https://www.codacy.com/app/PrivateBin/PrivateBin)
-[![Code Climate](https://codeclimate.com/github/PrivateBin/PrivateBin/badges/gpa.svg)](https://codeclimate.com/github/PrivateBin/PrivateBin)
+[![Build Status](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/build.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/build-status/master) [![Code Climate](https://codeclimate.com/github/PrivateBin/PrivateBin/badges/gpa.svg)](https://codeclimate.com/github/PrivateBin/PrivateBin)
 [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/?branch=master)
-[![SensioLabsInsight](https://insight.sensiolabs.com/projects/57c9e74e-c6f9-4de6-a876-df66ec2ea1ff/mini.png)](https://insight.sensiolabs.com/projects/57c9e74e-c6f9-4de6-a876-df66ec2ea1ff)  
-[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/094500f62abf4c9aa0c8a8a4520e4789)](https://www.codacy.com/app/PrivateBin/PrivateBin)
-[![Test Coverage](https://codeclimate.com/github/PrivateBin/PrivateBin/badges/coverage.svg)](https://codeclimate.com/github/PrivateBin/PrivateBin/coverage) [![Code Coverage](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/?branch=master)
+[![Code Coverage](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PrivateBin/PrivateBin/?branch=master)
 
 [![BrowserStack - cross browser testing](img/browserstack.svg)](https://www.browserstack.com/)

+ 10 - 0
CHANGELOG.md

@@ -1,5 +1,15 @@
 # PrivateBin version history
 
+## 1.7.9 (not yet released)
+* CHANGED: Upgrading libraries to: base-x 5.0.1, bootstrap 5.3.8, DOMpurify 3.2.7, ip-lib 1.21.0 & kjua 0.10.0
+* CHANGED: Refactored jQuery DOM element creation into plain JavaScript
+* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching
+* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users
+* FIXED: Sanitize file name in attachment size hint
+* FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585)
+* FIXED: traffic limiter not working when using Filesystem storage and PHP opcache
+* FIXED: Configuration combinations test errors
+
 ## 1.7.8 (2025-06-30)
 * FIXED: Duplicate attachment for every comment (#1577)
 * FIXED: Attachments with empty file names (#1577)

+ 3 - 2
bin/configuration-test-generator

@@ -536,7 +536,8 @@ EOT;
                 break;
             case 'Delete':
                 $code .= PHP_EOL . <<<'EOT'
-        $this->_model->create(Helper::getPasteId(), Helper::getPaste());
+        $paste = Helper::getPaste();
+        $this->_model->create(Helper::getPasteId(), $paste);
         $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
         $_GET['pasteid'] = Helper::getPasteId();
         $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $this->_model->read(Helper::getPasteId())['meta']['salt']);
@@ -574,7 +575,7 @@ EOT;
                 $code .= <<<'EOT'
 
         $this->assertMatchesRegularExpression(
-            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
+            '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*(<button|<\/div>)#s',
             $content,
             'outputs deleted status correctly'
         );

+ 4 - 0
codacy-analysis.yml

@@ -17,6 +17,10 @@ on:
   schedule:
     - cron: '45 16 * * 1'
 
+permissions:
+  contents: read
+  security-events: write
+
 jobs:
   codacy-security-scan:
     name: Codacy Security Scan

+ 1 - 1
composer.json

@@ -26,7 +26,7 @@
 	"require" : {
 		"php": "^7.3 || ^8.0",
 		"jdenticon/jdenticon": "1.0.2",
-		"mlocati/ip-lib": "1.20.0",
+		"mlocati/ip-lib": "1.21.0",
 		"symfony/polyfill-php80": "1.31.0",
 		"yzalis/identicon": "2.0.0"
 	},

+ 104 - 48
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "6c7e6dea19e8bfd5641b220cb68c4b65",
+    "content-hash": "a6501ecd3d80fece24f1c78e49df7217",
     "packages": [
         {
             "name": "jdenticon/jdenticon",
@@ -57,16 +57,16 @@
         },
         {
             "name": "mlocati/ip-lib",
-            "version": "1.20.0",
+            "version": "1.21.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mlocati/ip-lib.git",
-                "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4"
+                "reference": "b5d38cdcbfc1516604d821a1f3f4a1638f327267"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/fd45fc3bf08ed6c7e665e2e70562082ac954afd4",
-                "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4",
+                "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/b5d38cdcbfc1516604d821a1f3f4a1638f327267",
+                "reference": "b5d38cdcbfc1516604d821a1f3f4a1638f327267",
                 "shasum": ""
             },
             "require": {
@@ -112,7 +112,7 @@
             ],
             "support": {
                 "issues": "https://github.com/mlocati/ip-lib/issues",
-                "source": "https://github.com/mlocati/ip-lib/tree/1.20.0"
+                "source": "https://github.com/mlocati/ip-lib/tree/1.21.0"
             },
             "funding": [
                 {
@@ -124,7 +124,7 @@
                     "type": "other"
                 }
             ],
-            "time": "2025-02-04T17:30:58+00:00"
+            "time": "2025-09-24T13:58:50+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
@@ -337,16 +337,16 @@
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.13.0",
+            "version": "1.13.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
+                "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
-                "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+                "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
                 "shasum": ""
             },
             "require": {
@@ -385,7 +385,7 @@
             ],
             "support": {
                 "issues": "https://github.com/myclabs/DeepCopy/issues",
-                "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
             },
             "funding": [
                 {
@@ -393,7 +393,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2025-02-12T12:17:51+00:00"
+            "time": "2025-08-01T08:46:24+00:00"
         },
         {
             "name": "nikic/php-parser",
@@ -890,16 +890,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.6.22",
+            "version": "9.6.29",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c"
+                "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
-                "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
+                "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
                 "shasum": ""
             },
             "require": {
@@ -910,7 +910,7 @@
                 "ext-mbstring": "*",
                 "ext-xml": "*",
                 "ext-xmlwriter": "*",
-                "myclabs/deep-copy": "^1.12.1",
+                "myclabs/deep-copy": "^1.13.4",
                 "phar-io/manifest": "^2.0.4",
                 "phar-io/version": "^3.2.1",
                 "php": ">=7.3",
@@ -921,11 +921,11 @@
                 "phpunit/php-timer": "^5.0.3",
                 "sebastian/cli-parser": "^1.0.2",
                 "sebastian/code-unit": "^1.0.8",
-                "sebastian/comparator": "^4.0.8",
+                "sebastian/comparator": "^4.0.9",
                 "sebastian/diff": "^4.0.6",
                 "sebastian/environment": "^5.1.5",
-                "sebastian/exporter": "^4.0.6",
-                "sebastian/global-state": "^5.0.7",
+                "sebastian/exporter": "^4.0.8",
+                "sebastian/global-state": "^5.0.8",
                 "sebastian/object-enumerator": "^4.0.4",
                 "sebastian/resource-operations": "^3.0.4",
                 "sebastian/type": "^3.2.1",
@@ -973,7 +973,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29"
             },
             "funding": [
                 {
@@ -984,12 +984,20 @@
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
                 },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
                 {
                     "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-05T13:48:26+00:00"
+            "time": "2025-09-24T06:29:11+00:00"
         },
         {
             "name": "sebastian/cli-parser",
@@ -1160,16 +1168,16 @@
         },
         {
             "name": "sebastian/comparator",
-            "version": "4.0.8",
+            "version": "4.0.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+                "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
-                "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
+                "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
                 "shasum": ""
             },
             "require": {
@@ -1222,15 +1230,27 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/comparator/issues",
-                "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+                "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9"
             },
             "funding": [
                 {
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2022-09-14T12:41:17+00:00"
+            "time": "2025-08-10T06:51:50+00:00"
         },
         {
             "name": "sebastian/complexity",
@@ -1420,16 +1440,16 @@
         },
         {
             "name": "sebastian/exporter",
-            "version": "4.0.6",
+            "version": "4.0.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+                "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
-                "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
+                "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
                 "shasum": ""
             },
             "require": {
@@ -1485,28 +1505,40 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/exporter/issues",
-                "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+                "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8"
             },
             "funding": [
                 {
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2024-03-02T06:33:00+00:00"
+            "time": "2025-09-24T06:03:27+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "5.0.7",
+            "version": "5.0.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+                "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
-                "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+                "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
                 "shasum": ""
             },
             "require": {
@@ -1549,15 +1581,27 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/global-state/issues",
-                "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+                "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
             },
             "funding": [
                 {
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2024-03-02T06:35:11+00:00"
+            "time": "2025-08-10T07:10:35+00:00"
         },
         {
             "name": "sebastian/lines-of-code",
@@ -1730,16 +1774,16 @@
         },
         {
             "name": "sebastian/recursion-context",
-            "version": "4.0.5",
+            "version": "4.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+                "reference": "539c6691e0623af6dc6f9c20384c120f963465a0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
-                "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0",
+                "reference": "539c6691e0623af6dc6f9c20384c120f963465a0",
                 "shasum": ""
             },
             "require": {
@@ -1781,15 +1825,27 @@
             "homepage": "https://github.com/sebastianbergmann/recursion-context",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
-                "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6"
             },
             "funding": [
                 {
                     "url": "https://github.com/sebastianbergmann",
                     "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2023-02-03T06:07:39+00:00"
+            "time": "2025-08-10T06:57:39+00:00"
         },
         {
             "name": "sebastian/resource-operations",
@@ -2017,5 +2073,5 @@
     "platform-overrides": {
         "php": "7.3"
     },
-    "plugin-api-version": "2.3.0"
+    "plugin-api-version": "2.6.0"
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 4
css/bootstrap5/bootstrap-5.3.3.css


Разница между файлами не показана из-за своего большого размера
+ 4 - 0
css/bootstrap5/bootstrap-5.3.8.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 4
css/bootstrap5/bootstrap.rtl-5.3.3.css


Разница между файлами не показана из-за своего большого размера
+ 4 - 0
css/bootstrap5/bootstrap.rtl-5.3.8.css


+ 14 - 6
doc/Installation.md

@@ -22,12 +22,20 @@ for more information.
 ### Minimal Requirements
 
 - PHP version 7.3 or above
-- GD extension (when using identicon or vizhash icons, jdenticon works without it)
-- zlib extension
-- some disk space or a database supported by [PDO](https://php.net/manual/book.pdo.php)
-- ability to create files and folders in the installation directory and the PATH
-  defined in index.php
-- A web browser with JavaScript and (optional) WebAssembly support
+- PHP with zlib extension
+- some disk space and the capability to create files and folders in the
+  installation directory and the `PATH` defined in index.php
+- A web browser with JavaScript support enabled
+
+### Optional Requirements
+
+- PHP with GD extension (when using identicon or vizhash icons, jdenticon works
+  without it) and OPcache (for better performance)
+- a database supported by [PHP PDO](https://php.net/manual/book.pdo.php) and the
+  PHP PDO extension (when using database storage)
+- a Ceph cluster with Rados gateway or AWS S3 storage (when using S3 storage)
+- Google Cloud Storage (when using GCP storage)
+- A web browser with WebAssembly support
 
 ## Hardening and Security
 

+ 3 - 3
doc/README.md

@@ -4,7 +4,7 @@
 
 Please have a look at these questions *before* opening an issue in this repo.
 
-## [Installation guide](https://github.com/PrivateBin/PrivateBin/blob/master/doc/Installation.md#installation)
+## [Installation guide](Installation.md#installation)
 
 Minimal requirements, hardening and securing your installation and initial
 configuration.
@@ -26,12 +26,12 @@ How to help translate PrivateBin and technical background on it's implementation
 
 Know how for participating in PrivateBins development.
 
-### [Generating Source Code Documentation](https://github.com/PrivateBin/PrivateBin/blob/master/doc/Generating%20Source%20Code%20Documentation.md#generating-source-code-documentation)
+### [Generating Source Code Documentation](Generating%20Source%20Code%20Documentation.md#generating-source-code-documentation)
 
 How to generate the source code API documentation, as found on the project
 website for [PHP](https://privatebin.info/codedoc/) and [JS](https://privatebin.info/jsdoc/)
 
-### [Running Unit Tests](https://github.com/PrivateBin/PrivateBin/blob/master/doc/Running Unit Tests.md#running-all-unit-tests)
+### [Running Unit Tests](Running%20Unit%20Tests.md#running-all-unit-tests)
 
 How to run the PHP & JS unit tests, including a brief introduction to property
 based unit testing.

+ 1 - 1
doc/Running Unit Tests.md

@@ -18,7 +18,7 @@ The parameters in detail:
   an accidentally destructive test case in it.
 - `--read-only` - This image supports running in read-only mode. Only /tmp
   may be written into.
-- `-rm` - Remove the container after the run. This saves you doing a cleanup
+- `--rm` - Remove the container after the run. This saves you doing a cleanup
   on your docker environment, if you run the image frequently.
 
 You can also run just the php and javascript test suites instead of both:

+ 54 - 50
js/base-x-4.0.0.js → js/base-x-5.0.1.js

@@ -7,47 +7,47 @@
 (function(){
 this.baseX = function base (ALPHABET) {
   if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') }
-  var BASE_MAP = new Uint8Array(256)
-  for (var j = 0; j < BASE_MAP.length; j++) {
+  const BASE_MAP = new Uint8Array(256)
+  for (let j = 0; j < BASE_MAP.length; j++) {
     BASE_MAP[j] = 255
   }
-  for (var i = 0; i < ALPHABET.length; i++) {
-    var x = ALPHABET.charAt(i)
-    var xc = x.charCodeAt(0)
+  for (let i = 0; i < ALPHABET.length; i++) {
+    const x = ALPHABET.charAt(i)
+    const xc = x.charCodeAt(0)
     if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') }
     BASE_MAP[xc] = i
   }
-  var BASE = ALPHABET.length
-  var LEADER = ALPHABET.charAt(0)
-  var FACTOR = Math.log(BASE) / Math.log(256) // log(BASE) / log(256), rounded up
-  var iFACTOR = Math.log(256) / Math.log(BASE) // log(256) / log(BASE), rounded up
+  const BASE = ALPHABET.length
+  const LEADER = ALPHABET.charAt(0)
+  const FACTOR = Math.log(BASE) / Math.log(256) // log(BASE) / log(256), rounded up
+  const iFACTOR = Math.log(256) / Math.log(BASE) // log(256) / log(BASE), rounded up
   function encode (source) {
-    if (source instanceof Uint8Array) {
-    } else if (ArrayBuffer.isView(source)) {
+    // eslint-disable-next-line no-empty
+    if (source instanceof Uint8Array) { } else if (ArrayBuffer.isView(source)) {
       source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength)
     } else if (Array.isArray(source)) {
       source = Uint8Array.from(source)
     }
     if (!(source instanceof Uint8Array)) { throw new TypeError('Expected Uint8Array') }
     if (source.length === 0) { return '' }
-        // Skip & count leading zeroes.
-    var zeroes = 0
-    var length = 0
-    var pbegin = 0
-    var pend = source.length
+    // Skip & count leading zeroes.
+    let zeroes = 0
+    let length = 0
+    let pbegin = 0
+    const pend = source.length
     while (pbegin !== pend && source[pbegin] === 0) {
       pbegin++
       zeroes++
     }
-        // Allocate enough space in big-endian base58 representation.
-    var size = ((pend - pbegin) * iFACTOR + 1) >>> 0
-    var b58 = new Uint8Array(size)
-        // Process the bytes.
+    // Allocate enough space in big-endian base58 representation.
+    const size = ((pend - pbegin) * iFACTOR + 1) >>> 0
+    const b58 = new Uint8Array(size)
+    // Process the bytes.
     while (pbegin !== pend) {
-      var carry = source[pbegin]
-            // Apply "b58 = b58 * 256 + ch".
-      var i = 0
-      for (var it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
+      let carry = source[pbegin]
+      // Apply "b58 = b58 * 256 + ch".
+      let i = 0
+      for (let it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
         carry += (256 * b58[it1]) >>> 0
         b58[it1] = (carry % BASE) >>> 0
         carry = (carry / BASE) >>> 0
@@ -56,38 +56,42 @@ this.baseX = function base (ALPHABET) {
       length = i
       pbegin++
     }
-        // Skip leading zeroes in base58 result.
-    var it2 = size - length
+    // Skip leading zeroes in base58 result.
+    let it2 = size - length
     while (it2 !== size && b58[it2] === 0) {
       it2++
     }
-        // Translate the result into a string.
-    var str = LEADER.repeat(zeroes)
+    // Translate the result into a string.
+    let str = LEADER.repeat(zeroes)
     for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]) }
     return str
   }
   function decodeUnsafe (source) {
     if (typeof source !== 'string') { throw new TypeError('Expected String') }
     if (source.length === 0) { return new Uint8Array() }
-    var psz = 0
-        // Skip and count leading '1's.
-    var zeroes = 0
-    var length = 0
+    let psz = 0
+    // Skip and count leading '1's.
+    let zeroes = 0
+    let length = 0
     while (source[psz] === LEADER) {
       zeroes++
       psz++
     }
-        // Allocate enough space in big-endian base256 representation.
-    var size = (((source.length - psz) * FACTOR) + 1) >>> 0 // log(58) / log(256), rounded up.
-    var b256 = new Uint8Array(size)
-        // Process the characters.
-    while (source[psz]) {
-            // Decode character
-      var carry = BASE_MAP[source.charCodeAt(psz)]
-            // Invalid character
+    // Allocate enough space in big-endian base256 representation.
+    const size = (((source.length - psz) * FACTOR) + 1) >>> 0 // log(58) / log(256), rounded up.
+    const b256 = new Uint8Array(size)
+    // Process the characters.
+    while (psz < source.length) {
+      // Find code of next character
+      const charCode = source.charCodeAt(psz)
+      // Base map can not be indexed using char code
+      if (charCode > 255) { return }
+      // Decode character
+      let carry = BASE_MAP[charCode]
+      // Invalid character
       if (carry === 255) { return }
-      var i = 0
-      for (var it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
+      let i = 0
+      for (let it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
         carry += (BASE * b256[it3]) >>> 0
         b256[it3] = (carry % 256) >>> 0
         carry = (carry / 256) >>> 0
@@ -96,27 +100,27 @@ this.baseX = function base (ALPHABET) {
       length = i
       psz++
     }
-        // Skip leading zeroes in b256.
-    var it4 = size - length
+    // Skip leading zeroes in b256.
+    let it4 = size - length
     while (it4 !== size && b256[it4] === 0) {
       it4++
     }
-    var vch = new Uint8Array(zeroes + (size - it4))
-    var j = zeroes
+    const vch = new Uint8Array(zeroes + (size - it4))
+    let j = zeroes
     while (it4 !== size) {
       vch[j++] = b256[it4++]
     }
     return vch
   }
   function decode (string) {
-    var buffer = decodeUnsafe(string)
+    const buffer = decodeUnsafe(string)
     if (buffer) { return buffer }
     throw new Error('Non-base' + BASE + ' character')
   }
   return {
-    encode: encode,
-    decodeUnsafe: decodeUnsafe,
-    decode: decode
+    encode,
+    decodeUnsafe,
+    decode
   }
 }
 }).call(this);

Разница между файлами не показана из-за своего большого размера
+ 0 - 5
js/bootstrap-5.3.3.js


Разница между файлами не показана из-за своего большого размера
+ 5 - 0
js/bootstrap-5.3.8.js


+ 2 - 2
js/common.js

@@ -16,8 +16,8 @@ require('./prettify');
 global.prettyPrint = window.PR.prettyPrint;
 global.prettyPrintOne = window.PR.prettyPrintOne;
 global.showdown = require('./showdown-2.1.0');
-global.DOMPurify = require('./purify-3.2.6');
-global.baseX = require('./base-x-4.0.0').baseX;
+global.DOMPurify = require('./purify-3.3.0');
+global.baseX = require('./base-x-5.0.1').baseX;
 global.Legacy = require('./legacy').Legacy;
 require('./bootstrap-3.4.1');
 require('./privatebin');

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
js/kjua-0.10.0.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
js/kjua-0.9.0.js


+ 7 - 2
js/legacy.js

@@ -106,8 +106,8 @@
                 return window.isSecureContext;
             }
 
-            // HTTP is obviously insecure
-            if (window.location.protocol !== 'http:') {
+            // HTTPS is considered secure
+            if (window.location.protocol === 'https:') {
                 return true;
             }
 
@@ -246,6 +246,11 @@
          */
         me.init = function()
         {
+            // prevent early init
+            if (typeof document === 'undefined' || typeof navigator === 'undefined' || typeof window === 'undefined') {
+                return;
+            }
+
             // prevent bots from viewing a paste and potentially deleting data
             // when burn-after-reading is set
             if (isBadBot()) {

+ 58 - 55
js/privatebin.js

@@ -10,7 +10,7 @@
  * @namespace
  */
 
-// global Base64, DOMPurify, FileReader, RawDeflate, history, navigator, prettyPrint, prettyPrintOne, showdown, kjua
+// global Base64, DOMPurify, FileReader, baseX, bootstrap, RawDeflate, history, navigator, prettyPrint, prettyPrintOne, showdown, kjua
 
 jQuery.fn.draghover = function() {
     'use strict';
@@ -975,7 +975,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          *
          * @private
          */
-        let base58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
+        const base58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
 
         /**
          * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString
@@ -3028,7 +3028,8 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                 attachmentLink.attr('download', fileName);
 
                 const fileSize = Helper.formatBytes(decodedData.length);
-                template.append(`(${fileName}, ${fileSize})`);
+                const fileInfo = document.createTextNode(` (${fileName}, ${fileSize})`);
+                template[0].appendChild(fileInfo);
             }
 
             // sanitize SVG preview
@@ -3121,10 +3122,15 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          * @name AttachmentViewer.printDragAndDropFileNames
          * @private
          * @function
-         * @param {array} fileNames
+         * @param {string[]} fileNames
          */
         function printDragAndDropFileNames(fileNames) {
-            $dragAndDropFileNames.html(fileNames.join("<br>"));
+            $dragAndDropFileNames.empty();
+            fileNames.forEach(fileName => {
+                const name = document.createTextNode(fileName);
+                $dragAndDropFileNames[0].appendChild(name);
+                $dragAndDropFileNames[0].appendChild(document.createElement('br'));
+            });
         }
 
         /**
@@ -3323,44 +3329,38 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             const alreadyIncludesCurrentAttachment = $targetElement.find(`[src='${blobUrl}']`).length > 0;
 
             if (blobUrl && !alreadyIncludesCurrentAttachment) {
-                if (mimeType.match(/^image\//i)) {
-                    $targetElement.append(
-                        $(document.createElement('img'))
-                            .attr('src', blobUrl)
-                            .attr('class', 'img-thumbnail')
-                    );
-                } else if (mimeType.match(/^video\//i)) {
-                    $targetElement.append(
-                        $(document.createElement('video'))
-                            .attr('controls', 'true')
-                            .attr('autoplay', 'true')
-                            .attr('class', 'img-thumbnail')
-
-                            .append($(document.createElement('source'))
-                            .attr('type', mimeType)
-                            .attr('src', blobUrl))
-                    );
-                } else if (mimeType.match(/^audio\//i)) {
-                    $targetElement.append(
-                        $(document.createElement('audio'))
-                            .attr('controls', 'true')
-                            .attr('autoplay', 'true')
-
-                            .append($(document.createElement('source'))
-                            .attr('type', mimeType)
-                            .attr('src', blobUrl))
-                    );
-                } else if (mimeType.match(/\/pdf/i)) {
+                if (mimeType.toLowerCase().startsWith('image/')) {
+                    const image = document.createElement('img');
+                    image.setAttribute('src', blobUrl);
+                    image.setAttribute('class', 'img-thumbnail');
+                    $targetElement[0].appendChild(image);
+                } else if (mimeType.toLowerCase().startsWith('video/')) {
+                    const video = document.createElement('video');
+                    video.setAttribute('controls', 'true');
+                    video.setAttribute('autoplay', 'true');
+                    video.setAttribute('class', 'img-thumbnail');
+                    const source = document.createElement('source');
+                    source.setAttribute('type', mimeType);
+                    source.setAttribute('src', blobUrl);
+                    video.appendChild(source);
+                    $targetElement[0].appendChild(video);
+                } else if (mimeType.toLowerCase().startsWith('audio/')) {
+                    const audio = document.createElement('audio');
+                    audio.setAttribute('controls', 'true');
+                    audio.setAttribute('autoplay', 'true');
+                    const source = document.createElement('source');
+                    source.setAttribute('type', mimeType);
+                    source.setAttribute('src', blobUrl);
+                    audio.appendChild(source);
+                    $targetElement[0].appendChild(audio);
+                } else if (mimeType.toLowerCase().endsWith('/pdf')) {
+                    const embed = document.createElement('embed');
+                    embed.setAttribute('src', blobUrl);
+                    embed.setAttribute('type', 'application/pdf');
+                    embed.setAttribute('class', 'pdfPreview');
                     // Fallback for browsers, that don't support the vh unit
-                    const clientHeight = $(window).height();
-
-                    $targetElement.append(
-                        $(document.createElement('embed'))
-                            .attr('src', blobUrl)
-                            .attr('type', 'application/pdf')
-                            .attr('class', 'pdfPreview')
-                            .css('height', clientHeight)
-                    );
+                    embed.style.height = window.innerHeight + 'px';
+                    $targetElement[0].appendChild(embed);
                 }
             }
         };
@@ -3638,8 +3638,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             if (nickname.length > 0) {
                 $commentEntry.find('span.nickname').text(nickname);
             } else {
-                $commentEntry.find('span.nickname').html('<i></i>');
-                I18n._($commentEntry.find('span.nickname i'), 'Anonymous');
+                const anonCommenter = document.createElement('em');
+                anonCommenter.textContent = I18n._('Anonymous');
+                $commentEntry.find('span.nickname')[0].innerHTML = '';
+                $commentEntry.find('span.nickname')[0].appendChild(anonCommenter);
             }
 
             // set date
@@ -3652,14 +3654,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             // if an avatar is available, display it
             const icon = comment.getIcon();
             if (icon) {
-                $commentEntry.find('span.nickname')
-                             .before(
-                                '<img src="' + icon + '" class="vizhash" /> '
-                             );
-                $(document).on('languageLoaded', function () {
-                    $commentEntry.find('img.vizhash')
-                                 .prop('title', I18n._('Avatar generated from IP address'));
-                });
+                const image = document.createElement('img');
+                image.setAttribute('src', icon);
+                image.setAttribute('class', 'vizhash');
+                $commentEntry.find('span.nickname').prepend(' ').prepend(image);
             }
 
             // starting point (default value/fallback)
@@ -5268,22 +5266,23 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                 cipherMessage['attachment'] = attachments.map(attachment => attachment[0]);
                 cipherMessage['attachment_name'] = attachments.map(attachment => attachment[1]);
 
-                cipherMessage['attachment'] = await Promise.all(cipherMessage['attachment'].map(async (attachment) => {
+                cipherMessage['attachment'] = await Promise.all(cipherMessage['attachment'].map(async (attachment, i) => {
                     // we need to retrieve data from blob if browser already parsed it in memory
                     if (typeof attachment === 'string' && attachment.startsWith('blob:')) {
                         Alert.showStatus(
                             [
                                 'Retrieving cloned file \'%s\' from memory...',
-                                attachment[1]
+                                cipherMessage['attachment_name'][i]
                             ],
                             'copy'
                         );
                         try {
                             const blobData = await $.ajax({
                                 type: 'GET',
-                                url: `${attachment}`,
+                                url: attachment,
                                 processData: false,
                                 timeout: 10000,
+                                dataType: 'binary',
                                 xhrFields: {
                                     withCredentials: false,
                                     responseType: 'blob'
@@ -5493,6 +5492,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                         plaintexts[i][1]
                     );
                 }
+                $(document).on('languageLoaded', function () {
+                    $('#commentcontainer').find('img.vizhash')
+                        .prop('title', I18n._('Avatar generated from IP address'));
+                });
             });
         }
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 1
js/purify-3.2.6.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
js/purify-3.3.0.js


+ 2 - 1
js/test/Prompt.js

@@ -24,6 +24,8 @@ describe('Prompt', function () {
                 );
                 $.PrivateBin.Model.reset();
                 $.PrivateBin.Model.init();
+                // eslint-disable-next-line global-require
+                global.bootstrap = require('../bootstrap-5.3.8');
                 $.PrivateBin.Prompt.init();
                 $.PrivateBin.Prompt.requestPassword();
                 $('#passworddecrypt').val(password);
@@ -39,4 +41,3 @@ describe('Prompt', function () {
         );
     });
 });
-

+ 6 - 6
lib/Configuration.php

@@ -110,17 +110,17 @@ class Configuration
         ),
         // update this array when adding/changing/removing js files
         'sri' => array(
-            'js/base-x-4.0.0.js'     => 'sha512-nNPg5IGCwwrveZ8cA/yMGr5HiRS5Ps2H+s0J/mKTPjCPWUgFGGw7M5nqdnPD3VsRwCVysUh3Y8OWjeSKGkEQJQ==',
+            'js/base-x-5.0.1.js'     => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==',
             'js/base64-1.7.js'       => 'sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==',
             'js/bootstrap-3.4.1.js'  => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
-            'js/bootstrap-5.3.3.js'  => 'sha512-in2rcOpLTdJ7/pw5qjF4LWHFRtgoBDxXCy49H4YGOcVdGiPaQucGIbOqxt1JvmpvOpq3J/C7VTa0FlioakB2gQ==',
+            'js/bootstrap-5.3.8.js'  => 'sha512-BkZvJ5rZ3zbDCod5seWHpRGg+PRd6ZgE8Nua/OMtcxqm8Wtg0PqwhUUXK5bqvl3oclMt5O+3zjRVX0L+L2j7fA==',
             'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
             'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
-            'js/kjua-0.9.0.js'       => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==',
-            'js/legacy.js'           => 'sha512-UxW/TOZKon83n6dk/09GsYKIyeO5LeBHokxyIq+r7KFS5KMBeIB/EM7NrkVYIezwZBaovnyNtY2d9tKFicRlXg==',
+            'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
+            'js/legacy.js'           => 'sha512-iP69buypAHBJOgt7AyDcfaelVxBES9/k3dVfd6hPxTRizVRH2dijEpMWCt1D8OH4FNgytKsDI/J7+9y7IgXPaA==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-i0l0rh+NCY8Oeg9SxzQREHin6egXJ6sdIC84RTEsBpBNhYGObv8QEdRng1dMERZmw4olVeXx2ZCkusTyT1G+SA==',
-            'js/purify-3.2.6.js'     => 'sha512-zqwL4OoBLFx89QPewkz4Lz5CSA2ktU+f31fuECkF0iK3Id5qd3Zpq5dMby8KwHjIEpsUgOqwF58cnmcaNem0EA==',
+            'js/privatebin.js'       => 'sha512-ehY9hXpKGr1xhUaZbD6n2IyfyUvTVHDtcobY/azlnuOnXmdwgH4dHSGUSNxscyeMxHAktS/9K8UTYyCka4Cnlg==',
+            'js/purify-3.3.0.js'     => 'sha512-lsHD5zxs4lu/NDzaaibe27Vd2t7Cy9JQ3qDHUvDfb4oZvKoWDNEhwUY+4bT3R68cGgpgCYp8U1x2ifeVxqurdQ==',
             'js/rawinflate-0.3.js'   => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.1-1.js'     => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',

+ 8 - 4
lib/Controller.php

@@ -210,13 +210,17 @@ class Controller
     {
         $templates = $this->_conf->getKey('availabletemplates');
         $template  = $this->_conf->getKey('template');
+        if (!in_array($template, $templates, true)) {
+            $templates[] = $template;
+        }
         TemplateSwitcher::setAvailableTemplates($templates);
         TemplateSwitcher::setTemplateFallback($template);
 
-        // force default template, if template selection is disabled and a default is set
-        if (!$this->_conf->getKey('templateselection') && !empty($template)) {
-            $_COOKIE['template'] = $template;
-            setcookie('template', $template, array('SameSite' => 'Lax', 'Secure' => true));
+        // force default template, if template selection is disabled
+        if (!$this->_conf->getKey('templateselection') && array_key_exists('template', $_COOKIE)) {
+            unset($_COOKIE['template']); // ensure value is not re-used in template switcher
+            $expiredInAllTimezones = time() - 86400;
+            setcookie('template', '', array('expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true));
         }
     }
 

+ 6 - 0
lib/Data/Filesystem.php

@@ -274,6 +274,9 @@ class Filesystem extends AbstractData
     {
         switch ($namespace) {
             case 'purge_limiter':
+                if (function_exists('opcache_invalidate')) {
+                    opcache_invalidate($this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php');
+                }
                 return $this->_storeString(
                     $this->_path . DIRECTORY_SEPARATOR . 'purge_limiter.php',
                     '<?php' . PHP_EOL . '$GLOBALS[\'purge_limiter\'] = ' . var_export($value, true) . ';'
@@ -285,6 +288,9 @@ class Filesystem extends AbstractData
                 );
             case 'traffic_limiter':
                 $this->_last_cache[$key] = $value;
+                if (function_exists('opcache_invalidate')) {
+                    opcache_invalidate($this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php');
+                }
                 return $this->_storeString(
                     $this->_path . DIRECTORY_SEPARATOR . 'traffic_limiter.php',
                     '<?php' . PHP_EOL . '$GLOBALS[\'traffic_limiter\'] = ' . var_export($this->_last_cache, true) . ';'

+ 4 - 10
lib/Persistence/TrafficLimiter.php

@@ -204,16 +204,10 @@ class TrafficLimiter extends AbstractPersistence
         $now  = time();
         $tl   = (int) self::$_store->getValue('traffic_limiter', $hash);
         self::$_store->purgeValues('traffic_limiter', $now - self::$_limit);
-        if ($tl > 0 && ($tl + self::$_limit >= $now)) {
-            $result = false;
-        } else {
-            $tl     = time();
-            $result = true;
-        }
-        if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) {
-            error_log('failed to store the traffic limiter, it probably contains outdated information');
-        }
-        if ($result) {
+        if ($tl === 0 || ($tl + self::$_limit) < $now) {
+            if (!self::$_store->setValue((string) $now, 'traffic_limiter', $hash)) {
+                error_log('failed to store the traffic limiter, it probably contains outdated information');
+            }
             return true;
         }
         throw new Exception(I18n::_(

+ 14 - 34
lib/TemplateSwitcher.php

@@ -59,16 +59,13 @@ class TemplateSwitcher
     {
         if (self::isTemplateAvailable($template)) {
             self::$_templateFallback = $template;
-
-            if (!in_array($template, self::getAvailableTemplates())) {
-                // Add custom template to the available templates list
-                self::$_availableTemplates[] = $template;
-            }
+        } else {
+            error_log('failed to set "' . $template . '" as a fallback, it needs to be added to the list of `availabletemplates` in the configuration file');
         }
     }
 
     /**
-     * get currently loaded template
+     * get user selected template or fallback
      *
      * @access public
      * @static
@@ -76,8 +73,13 @@ class TemplateSwitcher
      */
     public static function getTemplate(): string
     {
-        $selectedTemplate = self::getSelectedByUserTemplate();
-        return $selectedTemplate ?? self::$_templateFallback;
+        if (array_key_exists('template', $_COOKIE)) {
+            $template = basename($_COOKIE['template']);
+            if (self::isTemplateAvailable($template)) {
+                return $template;
+            }
+        }
+        return self::$_templateFallback;
     }
 
     /**
@@ -101,32 +103,10 @@ class TemplateSwitcher
      */
     public static function isTemplateAvailable(string $template): bool
     {
-        $available = in_array($template, self::getAvailableTemplates());
-
-        if (!$available && !View::isBootstrapTemplate($template)) {
-            $path      = View::getTemplateFilePath($template);
-            $available = file_exists($path);
-        }
-
-        return $available;
-    }
-
-    /**
-     * get the template selected by user
-     *
-     * @access private
-     * @static
-     * @return string|null
-     */
-    private static function getSelectedByUserTemplate(): ?string
-    {
-        $selectedTemplate    = null;
-        $templateCookieValue = $_COOKIE['template'] ?? '';
-
-        if (self::isTemplateAvailable($templateCookieValue)) {
-            $selectedTemplate = $templateCookieValue;
+        if (in_array($template, self::getAvailableTemplates(), true)) {
+            return true;
         }
-
-        return $selectedTemplate;
+        error_log('template "' . $template . '" is not in the list of `availabletemplates` in the configuration file');
+        return false;
     }
 }

+ 8 - 28
lib/View.php

@@ -49,39 +49,19 @@ class View
      */
     public function draw($template)
     {
-        $path = self::getTemplateFilePath($template);
-        if (!file_exists($path)) {
-            throw new Exception('Template ' . $template . ' not found!', 80);
+        $dir  = PATH . 'tpl' . DIRECTORY_SEPARATOR;
+        $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template;
+        $path = $dir . $file . '.php';
+        if (!is_file($path)) {
+            throw new Exception('Template ' . $template . ' not found in file ' . $path . '!', 80);
+        }
+        if (!in_array($path, glob($dir . '*.php', GLOB_NOSORT | GLOB_ERR), true)) {
+            throw new Exception('Template ' . $file . '.php not found in ' . $dir . '!', 81);
         }
         extract($this->_variables);
         include $path;
     }
 
-    /**
-     * Get template file path
-     *
-     * @access public
-     * @param  string $template
-     * @return string
-     */
-    public static function getTemplateFilePath(string $template): string
-    {
-        $file = self::isBootstrapTemplate($template) ? 'bootstrap' : $template;
-        return PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php';
-    }
-
-    /**
-     * Is the template a variation of the bootstrap template
-     *
-     * @access public
-     * @param  string $template
-     * @return bool
-     */
-    public static function isBootstrapTemplate(string $template): bool
-    {
-        return substr($template, 0, 10) === 'bootstrap-';
-    }
-
     /**
      * echo script tag incl. SRI hash for given script file
      *

+ 4 - 4
tpl/bootstrap.php

@@ -46,7 +46,7 @@ endif;
 <?php
 if ($QRCODE) :
 ?>
-		<?php $this->_scriptTag('js/kjua-0.9.0.js', 'async'); ?>
+		<?php $this->_scriptTag('js/kjua-0.10.0.js', 'async'); ?>
 <?php
 endif;
 if ($ZEROBINCOMPATIBILITY) :
@@ -56,7 +56,7 @@ if ($ZEROBINCOMPATIBILITY) :
 endif;
 ?>
 		<?php $this->_scriptTag('js/zlib-1.3.1-1.js', 'async'); ?>
-		<?php $this->_scriptTag('js/base-x-4.0.0.js', 'defer'); ?>
+		<?php $this->_scriptTag('js/base-x-5.0.1.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/rawinflate-0.3.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/bootstrap-3.4.1.js', 'defer'); ?>
 <?php
@@ -71,8 +71,8 @@ if ($MARKDOWN) :
 <?php
 endif;
 ?>
-		<?php $this->_scriptTag('js/purify-3.2.6.js', 'async'); ?>
-		<?php $this->_scriptTag('js/legacy.js', 'async'); ?>
+		<?php $this->_scriptTag('js/purify-3.3.0.js', 'defer'); ?>
+		<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />

+ 6 - 6
tpl/bootstrap5.php

@@ -10,7 +10,7 @@ use PrivateBin\I18n;
 		<meta name="robots" content="noindex" />
 		<meta name="google" content="notranslate">
 		<title><?php echo I18n::_($NAME); ?></title>
-		<link type="text/css" rel="stylesheet" href="css/bootstrap5/bootstrap<?php echo I18n::isRtl() ? '.rtl' : ''; ?>-5.3.3.css" />
+		<link type="text/css" rel="stylesheet" href="css/bootstrap5/bootstrap<?php echo I18n::isRtl() ? '.rtl' : ''; ?>-5.3.8.css" />
 		<link type="text/css" rel="stylesheet" href="css/bootstrap5/privatebin.css?<?php echo rawurlencode($VERSION); ?>" />
 <?php
 if ($SYNTAXHIGHLIGHTING) :
@@ -29,7 +29,7 @@ endif;
 <?php
 if ($QRCODE) :
 ?>
-		<?php $this->_scriptTag('js/kjua-0.9.0.js', 'async'); ?>
+		<?php $this->_scriptTag('js/kjua-0.10.0.js', 'async'); ?>
 <?php
 endif;
 if ($ZEROBINCOMPATIBILITY) :
@@ -39,9 +39,9 @@ if ($ZEROBINCOMPATIBILITY) :
 endif;
 ?>
 		<?php $this->_scriptTag('js/zlib-1.3.1-1.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/base-x-4.0.0.js', 'defer'); ?>
+		<?php $this->_scriptTag('js/base-x-5.0.1.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/rawinflate-0.3.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/bootstrap-5.3.3.js', 'async'); ?>
+		<?php $this->_scriptTag('js/bootstrap-5.3.8.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/dark-mode-switch.js', 'defer'); ?>
 <?php
 if ($SYNTAXHIGHLIGHTING) :
@@ -55,8 +55,8 @@ if ($MARKDOWN) :
 <?php
 endif;
 ?>
-		<?php $this->_scriptTag('js/purify-3.2.6.js', 'defer'); ?>
-		<?php $this->_scriptTag('js/legacy.js', 'async'); ?>
+		<?php $this->_scriptTag('js/purify-3.3.0.js', 'defer'); ?>
+		<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
 		<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />

+ 3 - 0
tst/Bootstrap.php

@@ -6,7 +6,9 @@ use Google\Cloud\Storage\Bucket;
 use Google\Cloud\Storage\Connection\ConnectionInterface;
 use Google\Cloud\Storage\StorageClient;
 use Google\Cloud\Storage\StorageObject;
+use PrivateBin\Configuration;
 use PrivateBin\Persistence\ServerSalt;
+use PrivateBin\TemplateSwitcher;
 
 error_reporting(E_ALL | E_STRICT);
 
@@ -26,6 +28,7 @@ if (!defined('CONF_SAMPLE')) {
 
 require PATH . 'vendor/autoload.php';
 Helper::updateSubresourceIntegrity();
+TemplateSwitcher::setAvailableTemplates(Configuration::getDefaults()['main']['availabletemplates']);
 
 /**
  * Class Helper provides unit tests pastes and comments of various formats

+ 5 - 13
tst/ViewTest.php

@@ -143,19 +143,11 @@ class ViewTest extends TestCase
         $test->draw('123456789 does not exist!');
     }
 
-    public function testTemplateFilePath()
+    public function testInvalidTemplate()
     {
-        $template     = 'bootstrap';
-        $templatePath = PATH . 'tpl' . DIRECTORY_SEPARATOR . $template . '.php';
-        $path         = View::getTemplateFilePath($template);
-        $this->assertEquals($templatePath, $path, 'Template file path');
-    }
-
-    public function testIsBootstrapTemplate()
-    {
-        $bootstrapTemplate    = 'bootstrap-dark';
-        $nonBootstrapTemplate = 'page';
-        $this->assertTrue(View::isBootstrapTemplate($bootstrapTemplate), 'Is bootstrap template');
-        $this->assertFalse(View::isBootstrapTemplate($nonBootstrapTemplate), 'Is not bootstrap template');
+        $test = new View;
+        $this->expectException(Exception::class);
+        $this->expectExceptionCode(81);
+        $test->draw('../index');
     }
 }

+ 45 - 51
vendor/composer/ClassLoader.php

@@ -45,35 +45,34 @@ class ClassLoader
     /** @var \Closure(string):void */
     private static $includeFile;
 
-    /** @var ?string */
+    /** @var string|null */
     private $vendorDir;
 
     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();
 
     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();
 
@@ -81,8 +80,7 @@ class ClassLoader
     private $useIncludePath = false;
 
     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();
 
@@ -90,21 +88,20 @@ class ClassLoader
     private $classMapAuthoritative = false;
 
     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();
 
-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;
 
     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();
 
     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
@@ -113,7 +110,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -125,8 +122,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -134,8 +130,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -143,8 +138,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -152,8 +146,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -161,8 +154,7 @@ class ClassLoader
     }
 
     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -179,24 +171,25 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }
 
@@ -205,19 +198,19 @@ class ClassLoader
 
         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;
 
             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -226,9 +219,9 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws \InvalidArgumentException
      *
@@ -236,17 +229,18 @@ class ClassLoader
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -256,18 +250,18 @@ class ClassLoader
                 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -276,8 +270,8 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -294,8 +288,8 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws \InvalidArgumentException
      *
@@ -481,9 +475,9 @@ class ClassLoader
     }
 
     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {

+ 1 - 0
vendor/composer/autoload_classmap.php

@@ -22,6 +22,7 @@ return array(
     'IPLib\\Range\\Subnet' => $vendorDir . '/mlocati/ip-lib/src/Range/Subnet.php',
     'IPLib\\Range\\Type' => $vendorDir . '/mlocati/ip-lib/src/Range/Type.php',
     'IPLib\\Service\\BinaryMath' => $vendorDir . '/mlocati/ip-lib/src/Service/BinaryMath.php',
+    'IPLib\\Service\\NumberInChunks' => $vendorDir . '/mlocati/ip-lib/src/Service/NumberInChunks.php',
     'IPLib\\Service\\RangesFromBoundaryCalculator' => $vendorDir . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php',
     'IPLib\\Service\\UnsignedIntegerMath' => $vendorDir . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php',
     'Identicon\\Generator\\BaseGenerator' => $vendorDir . '/yzalis/identicon/src/Identicon/Generator/BaseGenerator.php',

+ 1 - 0
vendor/composer/autoload_static.php

@@ -70,6 +70,7 @@ class ComposerStaticInitDontChange
         'IPLib\\Range\\Subnet' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Subnet.php',
         'IPLib\\Range\\Type' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Type.php',
         'IPLib\\Service\\BinaryMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/BinaryMath.php',
+        'IPLib\\Service\\NumberInChunks' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/NumberInChunks.php',
         'IPLib\\Service\\RangesFromBoundaryCalculator' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php',
         'IPLib\\Service\\UnsignedIntegerMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php',
         'Identicon\\Generator\\BaseGenerator' => __DIR__ . '/..' . '/yzalis/identicon/src/Identicon/Generator/BaseGenerator.php',

+ 9 - 9
vendor/composer/installed.php

@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'privatebin/privatebin',
-        'pretty_version' => 'dev-master',
-        'version' => 'dev-master',
-        'reference' => '7825471d70c39baf6042c52a453c8fe705d9ed75',
+        'pretty_version' => 'dev-1.7-backport-2.0.3',
+        'version' => 'dev-1.7-backport-2.0.3',
+        'reference' => 'eb203e2d2505fd167de6f4c714562fad611b1ffa',
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -20,18 +20,18 @@
             'dev_requirement' => false,
         ),
         'mlocati/ip-lib' => array(
-            'pretty_version' => '1.20.0',
-            'version' => '1.20.0.0',
-            'reference' => 'fd45fc3bf08ed6c7e665e2e70562082ac954afd4',
+            'pretty_version' => '1.21.0',
+            'version' => '1.21.0.0',
+            'reference' => 'b5d38cdcbfc1516604d821a1f3f4a1638f327267',
             'type' => 'library',
             'install_path' => __DIR__ . '/../mlocati/ip-lib',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'privatebin/privatebin' => array(
-            'pretty_version' => 'dev-master',
-            'version' => 'dev-master',
-            'reference' => '7825471d70c39baf6042c52a453c8fe705d9ed75',
+            'pretty_version' => 'dev-1.7-backport-2.0.3',
+            'version' => 'dev-1.7-backport-2.0.3',
+            'reference' => 'eb203e2d2505fd167de6f4c714562fad611b1ffa',
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

+ 3 - 2
vendor/mlocati/ip-lib/src/Address/AddressInterface.php

@@ -110,11 +110,12 @@ interface AddressInterface
     /**
      * Get the address at a certain distance from this address.
      *
-     * @param int $n the distance of the address (can be negative)
+     * @param int|numeric-string $n the distance of the address (can be negative)
      *
-     * @return \IPLib\Address\AddressInterface|null return NULL if $n is not an integer or if the final address would be invalid
+     * @return \IPLib\Address\AddressInterface|null return NULL if $n is not an integer or NULL if $n is neither an integer nor a string containing a valid integer, or if the final address would be invalid
      *
      * @since 1.15.0
+     * @since 1.21.0 $n can also be a numeric string
      *
      * @example passing 1 to the address 127.0.0.1 will result in 127.0.0.2
      * @example passing -1 to the address 127.0.0.1 will result in 127.0.0.0

+ 14 - 16
vendor/mlocati/ip-lib/src/Address/IPv4.php

@@ -6,6 +6,8 @@ use IPLib\ParseStringFlag;
 use IPLib\Range\RangeInterface;
 use IPLib\Range\Subnet;
 use IPLib\Range\Type as RangeType;
+use IPLib\Service\BinaryMath;
+use IPLib\Service\NumberInChunks;
 
 /**
  * An IPv4 address.
@@ -456,28 +458,24 @@ class IPv4 implements AddressInterface
      */
     public function getAddressAtOffset($n)
     {
-        if (!is_int($n)) {
+        if (is_int($n)) {
+            $thatChunks = NumberInChunks::fromInteger($n, NumberInChunks::CHUNKSIZE_BYTES);
+        } elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
+            $thatChunks = NumberInChunks::fromNumericString($s, NumberInChunks::CHUNKSIZE_BYTES);
+        } else {
             return null;
         }
-
-        $boundary = 256;
-        $mod = $n;
-        $bytes = $this->getBytes();
-        for ($i = count($bytes) - 1; $i >= 0; $i--) {
-            $tmp = ($bytes[$i] + $mod) % $boundary;
-            $mod = (int) floor(($bytes[$i] + $mod) / $boundary);
-            if ($tmp < 0) {
-                $tmp += $boundary;
-            }
-
-            $bytes[$i] = $tmp;
+        $myBytes = $this->getBytes();
+        while (isset($myBytes[1]) && $myBytes[0] === 0) {
+            array_shift($myBytes);
         }
-
-        if ($mod !== 0) {
+        $myChunks = new NumberInChunks(false, $myBytes, NumberInChunks::CHUNKSIZE_BYTES);
+        $result = $myChunks->add($thatChunks);
+        if ($result->negative || count($result->chunks) > 4) {
             return null;
         }
 
-        return static::fromBytes($bytes);
+        return static::fromBytes(array_pad($result->chunks, -4, 0));
     }
 
     /**

+ 15 - 17
vendor/mlocati/ip-lib/src/Address/IPv6.php

@@ -6,6 +6,8 @@ use IPLib\ParseStringFlag;
 use IPLib\Range\RangeInterface;
 use IPLib\Range\Subnet;
 use IPLib\Range\Type as RangeType;
+use IPLib\Service\BinaryMath;
+use IPLib\Service\NumberInChunks;
 
 /**
  * An IPv6 address.
@@ -549,28 +551,24 @@ class IPv6 implements AddressInterface
      */
     public function getAddressAtOffset($n)
     {
-        if (!is_int($n)) {
+        if (is_int($n)) {
+            $thatChunks = NumberInChunks::fromInteger($n, NumberInChunks::CHUNKSIZE_WORDS);
+        } elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
+            $thatChunks = NumberInChunks::fromNumericString($s, NumberInChunks::CHUNKSIZE_WORDS);
+        } else {
             return null;
         }
-
-        $boundary = 0x10000;
-        $mod = $n;
-        $words = $this->getWords();
-        for ($i = count($words) - 1; $i >= 0; $i--) {
-            $tmp = ($words[$i] + $mod) % $boundary;
-            $mod = (int) floor(($words[$i] + $mod) / $boundary);
-            if ($tmp < 0) {
-                $tmp += $boundary;
-            }
-
-            $words[$i] = $tmp;
+        $myWords = $this->getWords();
+        while (isset($myWords[1]) && $myWords[0] === 0) {
+            array_shift($myWords);
         }
-
-        if ($mod !== 0) {
+        $myChunks = new NumberInChunks(false, $myWords, NumberInChunks::CHUNKSIZE_WORDS);
+        $result = $myChunks->add($thatChunks);
+        if ($result->negative || count($result->chunks) > 8) {
             return null;
         }
 
-        return static::fromWords($words);
+        return static::fromWords(array_pad($result->chunks, -8, 0));
     }
 
     /**
@@ -645,7 +643,7 @@ class IPv6 implements AddressInterface
         }
         $myWords = $this->getWords();
         $otherWords = $other->getWords();
-        $sum = array_fill(0, 7, 0);
+        $sum = array_fill(0, 8, 0);
         $carry = 0;
         for ($index = 7; $index >= 0; $index--) {
             $word = $myWords[$index] + $otherWords[$index] + $carry;

+ 15 - 5
vendor/mlocati/ip-lib/src/Range/AbstractRange.php

@@ -7,6 +7,7 @@ use IPLib\Address\IPv4;
 use IPLib\Address\IPv6;
 use IPLib\Address\Type as AddressType;
 use IPLib\Factory;
+use IPLib\Service\BinaryMath;
 use OutOfBoundsException;
 
 /**
@@ -59,17 +60,26 @@ abstract class AbstractRange implements RangeInterface
      */
     public function getAddressAtOffset($n)
     {
-        if (!is_int($n)) {
+        if (is_int($n)) {
+            $positive = $n >= 0;
+            if ($positive === false) {
+                $nPlus1 = $n + 1;
+            }
+        } elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
+            $n = $s;
+            $positive = $n[0] !== '-';
+            if ($positive === false) {
+                $nPlus1 = BinaryMath::getInstance()->add1ToIntegerString($n);
+            }
+        } else {
             return null;
         }
-
-        $address = null;
-        if ($n >= 0) {
+        if ($positive) {
             $start = Factory::parseAddressString($this->getComparableStartString());
             $address = $start->getAddressAtOffset($n);
         } else {
             $end = Factory::parseAddressString($this->getComparableEndString());
-            $address = $end->getAddressAtOffset($n + 1);
+            $address = $end->getAddressAtOffset($nPlus1);
         }
 
         if ($address === null) {

+ 16 - 1
vendor/mlocati/ip-lib/src/Range/Pattern.php

@@ -7,6 +7,7 @@ use IPLib\Address\IPv4;
 use IPLib\Address\IPv6;
 use IPLib\Address\Type as AddressType;
 use IPLib\ParseStringFlag;
+use IPLib\Service\BinaryMath;
 
 /**
  * Represents an address range in pattern format (only ending asterisks are supported).
@@ -304,7 +305,21 @@ class Pattern extends AbstractRange
         $maxPrefix = $fromAddress::getNumberOfBits();
         $prefix = $this->getNetworkPrefix();
 
-        return pow(2, ($maxPrefix - $prefix));
+        return pow(2, $maxPrefix - $prefix);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @see \IPLib\Range\RangeInterface::getExactSize()
+     */
+    public function getExactSize()
+    {
+        $fromAddress = $this->fromAddress;
+        $maxPrefix = $fromAddress::getNumberOfBits();
+        $prefix = $this->getNetworkPrefix();
+
+        return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix);
     }
 
     /**

+ 14 - 4
vendor/mlocati/ip-lib/src/Range/RangeInterface.php

@@ -46,11 +46,12 @@ interface RangeInterface
     /**
      * Get the address at a certain offset of this range.
      *
-     * @param int $n the offset of the address (support negative offset)
+     * @param int|numeric-string $n the offset of the address (support negative offset)
      *
-     * @return \IPLib\Address\AddressInterface|null return NULL if $n is not an integer or if the offset out of range
+     * @return \IPLib\Address\AddressInterface|null return NULL if $n is neither an integer nor a string containing a valid integer, or if the offset out of range
      *
      * @since 1.15.0
+     * @since 1.21.0 $n can also be a numeric string
      *
      * @example passing 256 to the range 127.0.0.0/16 will result in 127.0.1.0
      * @example passing -1 to the range 127.0.1.0/16 will result in 127.0.255.255
@@ -150,14 +151,23 @@ interface RangeInterface
     public function getReverseDNSLookupName();
 
     /**
-     * Get the count of addresses this IP range contains.
+     * Get the count of addresses contained in this IP range (possibly approximated).
      *
-     * @return int|float Return float as for huge IPv6 networks, int is not enough
+     * @return int|float If the number of addresses exceeds PHP_INT_MAX a float containing an approximation will be returned
      *
      * @since 1.16.0
      */
     public function getSize();
 
+    /**
+     * Get the exact count of addresses contained in this IP range.
+     *
+     * @return int|numeric-string If the number of addresses exceeds PHP_INT_MAX a string containing the exact number of addresses will be returned
+     *
+     * @since 1.21.0
+     */
+    public function getExactSize();
+
     /**
      * Get the "network prefix", that is how many bits of the address are dedicated to the network portion.
      *

+ 10 - 0
vendor/mlocati/ip-lib/src/Range/Single.php

@@ -237,6 +237,16 @@ class Single extends AbstractRange
         return 1;
     }
 
+    /**
+     * {@inheritdoc}
+     *
+     * @see \IPLib\Range\RangeInterface::getExactSize()
+     */
+    public function getExactSize()
+    {
+        return 1;
+    }
+
     /**
      * {@inheritdoc}
      *

+ 16 - 1
vendor/mlocati/ip-lib/src/Range/Subnet.php

@@ -7,6 +7,7 @@ use IPLib\Address\IPv4;
 use IPLib\Address\Type as AddressType;
 use IPLib\Factory;
 use IPLib\ParseStringFlag;
+use IPLib\Service\BinaryMath;
 
 /**
  * Represents an address range in subnet format (eg CIDR).
@@ -348,6 +349,20 @@ class Subnet extends AbstractRange
         $maxPrefix = $fromAddress::getNumberOfBits();
         $prefix = $this->getNetworkPrefix();
 
-        return pow(2, ($maxPrefix - $prefix));
+        return pow(2, $maxPrefix - $prefix);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @see \IPLib\Range\RangeInterface::getExactSize()
+     */
+    public function getExactSize()
+    {
+        $fromAddress = $this->fromAddress;
+        $maxPrefix = $fromAddress::getNumberOfBits();
+        $prefix = $this->getNetworkPrefix();
+
+        return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix);
     }
 }

+ 11 - 11
vendor/mlocati/ip-lib/src/Range/Type.php

@@ -120,29 +120,29 @@ class Type
             case static::T_UNSPECIFIED:
                 return 'Unspecified/unknown address';
             case static::T_RESERVED:
-                 return 'Reserved/internal use only';
+                return 'Reserved/internal use only';
             case static::T_THISNETWORK:
-                 return 'Refer to source hosts on "this" network';
+                return 'Refer to source hosts on "this" network';
             case static::T_LOOPBACK:
-                 return 'Internet host loopback address';
+                return 'Internet host loopback address';
             case static::T_ANYCASTRELAY:
-                 return 'Relay anycast address';
+                return 'Relay anycast address';
             case static::T_LIMITEDBROADCAST:
-                 return '"Limited broadcast" destination address';
+                return '"Limited broadcast" destination address';
             case static::T_MULTICAST:
-                 return 'Multicast address assignments - Indentify a group of interfaces';
+                return 'Multicast address assignments - Indentify a group of interfaces';
             case static::T_LINKLOCAL:
-                 return '"Link local" address, allocated for communication between hosts on a single link';
+                return '"Link local" address, allocated for communication between hosts on a single link';
             case static::T_LINKLOCAL_UNICAST:
                 return 'Link local unicast / Linked-scoped unicast';
             case static::T_DISCARDONLY:
-                 return 'Discard only';
+                return 'Discard only';
             case static::T_DISCARD:
-                 return 'Discard';
+                return 'Discard';
             case static::T_PRIVATENETWORK:
-                 return 'For use in private networks';
+                return 'For use in private networks';
             case static::T_PUBLIC:
-                 return 'Public address';
+                return 'Public address';
             case static::T_CGNAT:
                 return 'Carrier-grade NAT';
             default:

+ 112 - 0
vendor/mlocati/ip-lib/src/Service/BinaryMath.php

@@ -9,6 +9,20 @@ namespace IPLib\Service;
  */
 class BinaryMath
 {
+    private static $instance;
+
+    /**
+     * @return \IPLib\Service\BinaryMath
+     */
+    public static function getInstance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
     /**
      * Trim the leading zeroes from a non-negative integer represented in binary form.
      *
@@ -97,6 +111,104 @@ class BinaryMath
         return $result;
     }
 
+    /**
+     * Compute 2 raised to the given exponent.
+     *
+     * If the result fits into a native PHP integer, an int is returned.
+     * If the result exceeds PHP_INT_MAX, a string containing the exact decimal representation is returned.
+     *
+     * @param int $exponent The non-negative exponent
+     *
+     * @return int|string
+     */
+    public function pow2string($exponent)
+    {
+        if ($exponent < PHP_INT_SIZE * 8 - 1) {
+            return 1 << $exponent;
+        }
+        $digits = array(1);
+        for ($i = 0; $i < $exponent; $i++) {
+            $carry = 0;
+            foreach ($digits as $index => $digit) {
+                $product = $digit * 2 + $carry;
+                $digits[$index] = $product % 10;
+                $carry = (int) ($product / 10);
+            }
+            if ($carry !== 0) {
+                $digits[] = $carry;
+            }
+        }
+
+        return implode('', array_reverse($digits));
+    }
+
+    /**
+     * @param numeric-string|mixed $value
+     *
+     * @return string empty string if $value is not a valid numeric string
+     */
+    public function normalizeIntegerString($value)
+    {
+        if (!is_string($value) || $value === '') {
+            return '';
+        }
+        $sign = $value[0];
+        if ($sign === '-' || $sign === '+') {
+            $value = substr($value, 1);
+        }
+        $matches = null;
+        if (!preg_match('/^0*([0-9]+)$/', $value, $matches)) {
+            return '';
+        }
+
+        return ($sign === '-' && $matches[1] !== '0' ? $sign : '') . $matches[1];
+    }
+
+    /**
+     * @param numeric-string $value a string that has been normalized with normalizeIntegerString()
+     *
+     * @return string
+     */
+    public function add1ToIntegerString($value)
+    {
+        if ($value[0] === '-') {
+            if ($value === '-1') {
+                return '0';
+            }
+            $digits = str_split(substr($value, 1));
+            $i = count($digits) - 1;
+            while ($i >= 0) {
+                if ($digits[$i] !== '0') {
+                    $digits[$i] = (string) ((int) $digits[$i] - 1);
+                    break;
+                }
+                $digits[$i] = '9';
+                $i--;
+            }
+            $imploded = implode('', $digits);
+            if ($imploded[0] === '0') {
+                $imploded = substr($imploded, 1);
+            }
+
+            return '-' . $imploded;
+        }
+        $digits = str_split($value);
+        $carry = 1;
+        for ($i = count($digits) - 1; $i >= 0; $i--) {
+            $sum = (int) $digits[$i] + $carry;
+            $digits[$i] = (string) ($sum % 10);
+            $carry = (int) ($sum / 10);
+            if ($carry === 0) {
+                break;
+            }
+            if ($i === 0) {
+                array_unshift($digits, (string) $carry);
+            }
+        }
+
+        return implode('', $digits);
+    }
+
     /**
      * Zero-padding of two non-negative integers represented in binary form, so that they have the same length.
      *

+ 253 - 0
vendor/mlocati/ip-lib/src/Service/NumberInChunks.php

@@ -0,0 +1,253 @@
+<?php
+
+namespace IPLib\Service;
+
+use InvalidArgumentException;
+
+/**
+ * @internal
+ *
+ * @readonly
+ */
+class NumberInChunks
+{
+    const CHUNKSIZE_BYTES = 8;
+
+    const CHUNKSIZE_WORDS = 16;
+
+    /**
+     * @var bool
+     */
+    public $negative;
+
+    /**
+     * @var int[]
+     */
+    public $chunks;
+
+    /**
+     * @var int
+     */
+    public $chunkSize;
+
+    /**
+     * @param bool $negative
+     * @param int[] $chunks
+     * @param int $chunkSize
+     */
+    public function __construct($negative, array $chunks, $chunkSize)
+    {
+        $this->negative = $negative;
+        $this->chunks = $chunks;
+        $this->chunkSize = $chunkSize;
+    }
+
+    /**
+     * @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
+     *
+     * @return \IPLib\Service\NumberInChunks
+     */
+    public function negate()
+    {
+        return new self($this->chunks === array(0) ? false : !$this->negative, $this->chunks, $this->chunkSize);
+    }
+
+    /**
+     * @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
+     *
+     * @return \IPLib\Service\NumberInChunks
+     */
+    public function add(NumberInChunks $that)
+    {
+        if ($this->chunkSize !== $that->chunkSize) {
+            throw new InvalidArgumentException('Incompatible chunk size');
+        }
+        if ($this->negative === $that->negative) {
+            return new self($this->negative, self::addChunks($this->chunks, $that->chunks, $this->chunkSize), $this->chunkSize);
+        }
+        if ($that->negative) {
+            list($negative, $chunks) = self::substractChunks($this->chunks, $that->chunks, $this->chunkSize);
+        } else {
+            list($negative, $chunks) = self::substractChunks($that->chunks, $this->chunks, $this->chunkSize);
+        }
+
+        return new self($negative, $chunks, $this->chunkSize);
+    }
+
+    /**
+     * @param int $int
+     * @param int $chunkSize
+     *
+     * @return \IPLib\Service\NumberInChunks
+     */
+    public static function fromInteger($int, $chunkSize)
+    {
+        if ($int === 0) {
+            return new self(false, array(0), $chunkSize);
+        }
+        $negative = $int < 0;
+        if ($negative) {
+            $positiveInt = -$int;
+            if (is_float($positiveInt)) { // -PHP_INT_MIN is bigger than PHP_INT_MAX
+                return self::fromNumericString((string) $int, $chunkSize);
+            }
+            $int = $positiveInt;
+        }
+        $bitMask = (1 << $chunkSize) - 1;
+        $chunks = array();
+        while ($int !== 0) {
+            $chunks[] = $int & $bitMask;
+            $int >>= $chunkSize;
+        }
+
+        return new self($negative, array_reverse($chunks), $chunkSize);
+    }
+
+    /**
+     * @param string $numericString a string normalized with BinaryMath::normalizeIntegerString()
+     * @param int $chunkSize
+     *
+     * @return \IPLib\Service\NumberInChunks
+     */
+    public static function fromNumericString($numericString, $chunkSize)
+    {
+        if ($numericString === '0') {
+            return new self(false, array(0), $chunkSize);
+        }
+        $negative = $numericString[0] === '-';
+        if ($negative) {
+            $numericString = substr($numericString, 1);
+        }
+        $chunks = array();
+        while ($numericString !== '0') {
+            $chunks[] = self::modulo($numericString, $chunkSize);
+            $numericString = self::divide($numericString, $chunkSize);
+        }
+
+        return new self($negative, array_reverse($chunks), $chunkSize);
+    }
+
+    /**
+     * @param string $numericString
+     * @param int $chunkSize
+     *
+     * @return int
+     */
+    private static function modulo($numericString, $chunkSize)
+    {
+        $divisor = 1 << $chunkSize;
+        $carry = 0;
+        $len = strlen($numericString);
+        for ($i = 0; $i < $len; $i++) {
+            $digit = (int) $numericString[$i];
+            $carry = ($carry * 10 + $digit) % $divisor;
+        }
+
+        return $carry;
+    }
+
+    /**
+     * @param string $numericString
+     * @param int $chunkSize
+     *
+     * @return string
+     */
+    private static function divide($numericString, $chunkSize)
+    {
+        $divisor = 1 << $chunkSize;
+        $quotient = '';
+        $carry = 0;
+        $len = strlen($numericString);
+        for ($i = 0; $i < $len; $i++) {
+            $digit = (int) $numericString[$i];
+            $value = $carry * 10 + $digit;
+            $quotient .= (string) ($value >> $chunkSize);
+            $carry = $value % $divisor;
+        }
+
+        return ltrim($quotient, '0') ?: '0';
+    }
+
+    /**
+     * @param int[] $addend1
+     * @param int[] $addend2
+     * @param int $chunkSize
+     *
+     * @return int[]
+     */
+    private static function addChunks(array $addend1, array $addend2, $chunkSize)
+    {
+        $divisor = 1 << $chunkSize;
+        $result = array();
+        $carry = 0;
+        while ($addend1 !== array() || $addend2 !== array()) {
+            $sum = $carry + (array_pop($addend1) ?: 0) + (array_pop($addend2) ?: 0);
+            $result[] = $sum % $divisor;
+            $carry = $sum >> $chunkSize;
+        }
+        if ($carry !== 0) {
+            $result[] = $carry;
+        }
+
+        return array_reverse($result);
+    }
+
+    /**
+     * @param int[] $minuend
+     * @param int[] $subtrahend
+     * @param int $chunkSize
+     *
+     * @return array
+     */
+    private static function substractChunks(array $minuend, array $subtrahend, $chunkSize)
+    {
+        $minuendCount = count($minuend);
+        $subtrahendCount = count($subtrahend);
+        if ($minuendCount > $subtrahendCount) {
+            $count = $minuendCount;
+            $negative = false;
+        } elseif ($minuendCount < $subtrahendCount) {
+            $count = $subtrahendCount;
+            $negative = true;
+        } else {
+            $count = $minuendCount;
+            $negative = false;
+            for ($i = 0; $i < $count; $i++) {
+                $delta = $minuend[$i] - $subtrahend[$i];
+                if ($delta === 0) {
+                    continue;
+                }
+                if ($delta < 0) {
+                    $negative = true;
+                }
+                break;
+            }
+        }
+        if ($negative) {
+            list($minuend, $subtrahend) = array($subtrahend, $minuend);
+        }
+        $subtrahend = array_pad($subtrahend, -$count, 0);
+        $borrowValue = 1 << $chunkSize;
+        $result = array();
+        $borrow = 0;
+        for ($i = $count - 1; $i >= 0; $i--) {
+            $value = $minuend[$i] - $subtrahend[$i] - $borrow;
+            if ($value < 0) {
+                $value += $borrowValue;
+                $borrow = 1;
+            } else {
+                $borrow = 0;
+            }
+            $result[] = $value;
+        }
+        while (isset($result[1])) {
+            $value = array_pop($result);
+            if ($value !== 0) {
+                $result[] = $value;
+                break;
+            }
+        }
+
+        return array($negative, array_reverse($result));
+    }
+}

+ 1 - 1
vendor/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php

@@ -50,7 +50,7 @@ class RangesFromBoundaryCalculator
      */
     public function __construct($numBits)
     {
-        $this->math = new BinaryMath();
+        $this->math = BinaryMath::getInstance();
         $this->setNumBits($numBits);
     }
 

Некоторые файлы не были показаны из-за большого количества измененных файлов