فهرست منبع

Merge branch 'master' into js/removeJQuery

# Conflicts:
#	js/privatebin.js
#	js/test/CopyToClipboard.js
#	lib/Configuration.php
rugk 1 ماه پیش
والد
کامیت
55639c70fb
56فایلهای تغییر یافته به همراه489 افزوده شده و 320 حذف شده
  1. 5 3
      .github/workflows/codacy-analysis.yml
  2. 5 3
      .github/workflows/codeql-analysis.yml
  3. 3 2
      .github/workflows/eslint.yml
  4. 3 2
      .github/workflows/release.yml
  5. 5 3
      .github/workflows/snyk-scan.yml
  6. 10 2
      .github/workflows/tests.yml
  7. 2 0
      CHANGELOG.md
  8. 0 20
      css/bootstrap/privatebin.css
  9. 0 28
      css/bootstrap5/privatebin.css
  10. 2 1
      i18n/ar.json
  11. 2 1
      i18n/bg.json
  12. 2 1
      i18n/ca.json
  13. 2 1
      i18n/co.json
  14. 2 1
      i18n/cs.json
  15. 2 1
      i18n/de.json
  16. 2 1
      i18n/el.json
  17. 2 1
      i18n/en.json
  18. 2 1
      i18n/es.json
  19. 2 1
      i18n/et.json
  20. 2 1
      i18n/fa.json
  21. 2 1
      i18n/fi.json
  22. 2 1
      i18n/fr.json
  23. 12 11
      i18n/he.json
  24. 2 1
      i18n/hi.json
  25. 2 1
      i18n/hu.json
  26. 2 1
      i18n/id.json
  27. 2 1
      i18n/it.json
  28. 2 1
      i18n/ja.json
  29. 2 1
      i18n/jbo.json
  30. 2 1
      i18n/ko.json
  31. 2 1
      i18n/ku.json
  32. 2 1
      i18n/la.json
  33. 2 1
      i18n/lt.json
  34. 2 1
      i18n/nl.json
  35. 2 1
      i18n/no.json
  36. 2 1
      i18n/oc.json
  37. 2 1
      i18n/pl.json
  38. 2 1
      i18n/pt.json
  39. 2 1
      i18n/ro.json
  40. 16 15
      i18n/ru.json
  41. 2 1
      i18n/sk.json
  42. 2 1
      i18n/sl.json
  43. 2 1
      i18n/sv.json
  44. 2 1
      i18n/th.json
  45. 2 1
      i18n/tr.json
  46. 6 5
      i18n/uk.json
  47. 2 1
      i18n/zh.json
  48. 272 124
      js/package-lock.json
  49. 1 1
      js/package.json
  50. 12 29
      js/privatebin.js
  51. 35 26
      js/test/CopyToClipboard.js
  52. 1 1
      lib/Proxy/YourlsProxy.php
  53. 10 5
      tpl/bootstrap.php
  54. 6 5
      tpl/bootstrap5.php
  55. 5 0
      tst/Bootstrap.php
  56. 10 0
      tst/YourlsProxyTest.php

+ 5 - 3
.github/workflows/codacy-analysis.yml

@@ -17,18 +17,20 @@ on:
   schedule:
     - cron: '45 16 * * 1'
 
-permissions:
-  contents: read
-  security-events: write
+permissions: {}
 
 jobs:
   codacy-security-scan:
     name: Codacy Security Scan
+    permissions:
+      security-events: write
     runs-on: ubuntu-latest
     steps:
       # Checkout the repository to the GitHub Actions runner
       - name: Checkout code
         uses: actions/checkout@v6
+        with:
+          persist-credentials: false
 
       - name: Remove folders causing errors in report
         run: rm -rf doc img *.md

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

@@ -18,13 +18,13 @@ on:
   schedule:
     - cron: '28 22 * * 5'
 
-permissions:
-  contents: read
-  security-events: write
+permissions: {}
 
 jobs:
   analyze:
     name: Analyze
+    permissions:
+      security-events: write
     runs-on: ubuntu-latest
 
     strategy:
@@ -38,6 +38,8 @@ jobs:
     steps:
     - name: Checkout repository
       uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL

+ 3 - 2
.github/workflows/eslint.yml

@@ -12,8 +12,7 @@ on:
       - 'js/package-lock.json'
       - .github/workflows/eslint.yml
 
-permissions:
-  contents: read
+permissions: {}
 
 jobs:
   eslint:
@@ -21,6 +20,8 @@ jobs:
     steps:
       - name: Checkout repository
         uses: actions/checkout@v6
+        with:
+          persist-credentials: false
 
       - name: Set up Node.js
         uses: actions/setup-node@v6

+ 3 - 2
.github/workflows/release.yml

@@ -15,6 +15,7 @@ jobs:
       - name: Fetch changelog from tag
         uses: actions/checkout@v6
         with:
+          persist-credentials: false
           sparse-checkout: CHANGELOG.md
           sparse-checkout-cone-mode: false
 
@@ -32,8 +33,8 @@ jobs:
     steps:
       - name: Collect artifacts
         run: |
-          wget -q https://github.com/PrivateBin/PrivateBin/archive/refs/tags/${GITHUB_REF_NAME}.tar.gz
-          wget -q https://github.com/PrivateBin/PrivateBin/archive/refs/tags/${GITHUB_REF_NAME}.zip
+          wget -q "https://github.com/PrivateBin/PrivateBin/archive/refs/tags/${GITHUB_REF_NAME}.tar.gz"
+          wget -q "https://github.com/PrivateBin/PrivateBin/archive/refs/tags/${GITHUB_REF_NAME}.zip"
 
       - name: Generate hashes
         shell: bash

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

@@ -9,13 +9,13 @@ on:
   pull_request:
     branches: [ master ]
 
-permissions:
-  contents: read
-  security-events: write
+permissions: {}
 
 jobs:
   # https://github.com/snyk/actions/tree/master/php
   snyk-php:
+    permissions:
+      security-events: write
     runs-on: ubuntu-latest
     if: |
       github.repository == 'PrivateBin/PrivateBin' && (
@@ -25,6 +25,8 @@ jobs:
       github.event.pull_request.author_association == 'OWNER' )
     steps:
       - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
       - name: Run Snyk to check for vulnerabilities
         uses: snyk/actions/php@master
         continue-on-error: true # To make sure that SARIF upload gets called

+ 10 - 2
.github/workflows/tests.yml

@@ -15,6 +15,8 @@ jobs:
     steps:
     - name: Checkout
       uses: actions/checkout@v6
+      with:
+        persist-credentials: false
     - name: Validate composer.json and composer.lock
       run: composer validate
     - name: Install dependencies
@@ -42,6 +44,8 @@ jobs:
     # let's get started!
     - name: Checkout
       uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
     # cache PHP extensions
     - name: Setup cache environment
@@ -121,7 +125,7 @@ jobs:
     name: PHP configuration combination unit tests
     runs-on: ubuntu-latest
     env:
-      php-version: '8.4'
+      php-version: '8.5'
       extensions: gd, sqlite3
       extensions-cache-key-name: phpextensions
 
@@ -130,6 +134,8 @@ jobs:
     # let's get started!
     - name: Checkout
       uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
     # cache PHP extensions
     - name: Setup cache environment
@@ -214,11 +220,13 @@ jobs:
 
     - name: Checkout
       uses: actions/checkout@v6
+      with:
+        persist-credentials: false
 
     - name: Setup Node
       uses: actions/setup-node@v6
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'npm'
         cache-dependency-path: 'js/package-lock.json'
 

+ 2 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 ## 2.0.5 (not yet released)
 * FIXED: State corruption after "Remove attachment" (#1824)
+* FIXED: Copy button is hidden if the document is made as markdown (#1703)
+* FIXED: Shortened URLs from YOURLS received but failed to parse (#1844)
 
 ## 2.0.4 (2026-05-03)
 * ADDED: Translations for Swedish & Persian

+ 0 - 20
css/bootstrap/privatebin.css

@@ -143,26 +143,6 @@ html[dir="rtl"] #deletelink, html[dir="rtl"] #qrcodemodalClose {
 	padding-right: 30px;
 }
 
-#prettyMessageCopyBtn {
-	position: absolute;
-	top: 8px;
-	right: 25px;
-	left: auto;
-	padding: 0;
-	background: none;
-	border: none;
-	z-index: 1;
-}
-
-html[dir="rtl"] #prettyMessageCopyBtn {
-	left: 25px;
-	right: auto;
-}
-
-#copySuccessIcon {
-	display: none;
-}
-
 #copyShortcutHint {
 	margin-bottom: 5px;
 }

+ 0 - 28
css/bootstrap5/privatebin.css

@@ -57,34 +57,6 @@ html[dir="rtl"] #deletelink, html[dir="rtl"] #qrcodemodalClose {
 	padding-right: 30px;
 }
 
-#prettyMessageCopyBtn {
-	position: absolute;
-	top: 8px;
-	right: 8px;
-	left: auto;
-	width: 20px;
-	height: 20px;
-	padding: 0;
-	background: none;
-	border: none;
-	z-index: 1;
-}
-
-html[dir="rtl"] #prettyMessageCopyBtn {
-	left: 8px;
-	right: auto;
-}
-
-#prettyMessageCopyBtn svg {
-	width: 100%;
-	height: 100%;
-	vertical-align: baseline;
-}
-
-#copySuccessIcon {
-	display: none;
-}
-
 #sendbutton svg {
 	transform: translateY(1.5px);
 }

+ 2 - 1
i18n/ar.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "مفتاح التبويب يعمل كشخصية (انقر <kbd>Ctrl</kbd>+<kbd>m</kbd> أو <kbd>Esc</kbd> للتبديل)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "السمة"
+    "Theme": "السمة",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/bg.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/ca.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/co.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "U tastu di tabulazione ghjova cum’è un caratteru (Appughjate nant’à <kbd>Ctrl</kbd>+<kbd>m</kbd> o <kbd>Scapp</kbd> per scambià)",
     "Show password": "Affissà a parolla d’intesa",
     "Hide password": "Piattà a parolla d’intesa",
-    "Theme": "Tema"
+    "Theme": "Tema",
+    "Copy": "Cupià"
 }

+ 2 - 1
i18n/cs.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulátor funguje jako znak (pro přepnutí stiskněte <kbd>Ctrl</kbd>+<kbd>m</kbd> nebo <kbd>Esc</kbd>)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Vzhled"
+    "Theme": "Vzhled",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/de.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortaste als Zeichen interpretieren (Umschalten durch <kbd>Strg</kbd>+<kbd>m</kbd> oder <kbd>Esc</kbd>)",
     "Show password": "Passwort anzeigen",
     "Hide password": "Passwort verbergen",
-    "Theme": "Design"
+    "Theme": "Design",
+    "Copy": "Kopieren"
 }

+ 2 - 1
i18n/el.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/en.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/es.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "La tecla de tabulación funciona como carácter (presione <kbd>Ctrl</kbd>+<kbd>m</kbd> o <kbd>Esc</kbd> para alternar)",
     "Show password": "Mostrar contraseña",
     "Hide password": "Ocultar contraseña",
-    "Theme": "Tema"
+    "Theme": "Tema",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/et.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Teema"
+    "Theme": "Teema",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/fa.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "کلید Tab به‌عنوان کاراکتر عمل می‌کند (برای تغییر وضعیت <kbd>Ctrl</kbd>+<kbd>m</kbd> یا <kbd>Esc</kbd> را فشار دهید)",
     "Show password": "نمایش رمز عبور",
     "Hide password": "مخفی‌کردن رمز عبور",
-    "Theme": "تم"
+    "Theme": "تم",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/fi.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulaattori toimii merkkinä (Paina <kbd>Ctrl</kbd>+<kbd>m</kbd> tai <kbd>Esc</kbd> vaihtaaksesi)",
     "Show password": "Näytä salasana",
     "Hide password": "Piilota salasana",
-    "Theme": "Teema"
+    "Theme": "Teema",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/fr.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "La touche de tabulation sert de caractère (Presser <kbd>Ctrl</kbd>+<kbd>m</kbd> ou <kbd>Esc</kbd> pour basculer)",
     "Show password": "Afficher le mot de passe",
     "Hide password": "Cacher le mot de passe",
-    "Theme": "Thème"
+    "Theme": "Thème",
+    "Copy": "Copier"
 }

+ 12 - 11
i18n/he.json

@@ -156,14 +156,14 @@
     "Could not create document: %s": "לא ניתן ליצור הדבקה: %s",
     "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "לא ניתן לפענח הדבקה: מפתח הפענוח חסר בכתובת (השתמשת במערכת הפנייה או מקצר כתובות שחותכים חלק מהכתובת?)",
     "B": "ב׳",
-    "kB": "kB",
-    "MB": "MB",
-    "GB": "GB",
-    "TB": "TB",
-    "PB": "PB",
-    "EB": "EB",
-    "ZB": "ZB",
-    "YB": "YB",
+    "kB": "ק\"ב",
+    "MB": "מ\"ב",
+    "GB": "ג\"ב",
+    "TB": "ט\"ב",
+    "PB": "פ\"ב",
+    "EB": "א\"ב",
+    "ZB": "ז\"ב",
+    "YB": "י\"ב",
     "Format": "פורמט",
     "Plain Text": "טקסט פשוט",
     "Source Code": "קוד מקור",
@@ -229,7 +229,8 @@
     "Link copied to clipboard": "הקישור הועתק ללוח",
     "Document text": "הדבק טקסט",
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "משטח ה-tab פועל כמקש תו (לחץ על <kbd>Ctrl</kbd>+<kbd>m</kbd> או <kbd>Esc</kbd> להחלפה)",
-    "Show password": "Show password",
-    "Hide password": "Hide password",
-    "Theme": "נושא"
+    "Show password": "הצג סיסמה",
+    "Hide password": "הסתר סיסמה",
+    "Theme": "נושא",
+    "Copy": "העתק"
 }

+ 2 - 1
i18n/hi.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/hu.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "A tabulátor karakternek használható (nyomd le a <kbd>Ctrl</kbd>+<kbd>m</kbd> vagy az <kbd>Esc</kbd> to billentyűket ennek megszüntetéséhez).",
     "Show password": "Jelszó megjelenítése",
     "Hide password": "Jelszó elrejtése",
-    "Theme": "Téma"
+    "Theme": "Téma",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/id.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/it.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Il tasto Tabulatore serve come carattere (basta <kbd>Ctrl</kbd>+<kbd>m</kbd> o <kbd>Esc</kbd> per attivare/disattivare)",
     "Show password": "Mostra password",
     "Hide password": "Nascondi password",
-    "Theme": "Tema"
+    "Theme": "Tema",
+    "Copy": "Copiare"
 }

+ 2 - 1
i18n/ja.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/jbo.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "jvinu"
+    "Theme": "jvinu",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/ko.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/ku.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/la.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/lt.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabuliatoriaus klavišas tarnauja kaip simbolis (Paspauskite <kbd>Ctrl</kbd>+<kbd>m</kbd> arba <kbd>Esc</kbd> norėdami perjungti)",
     "Show password": "Rodyti slaptažodį",
     "Hide password": "Slėpti slaptažodį",
-    "Theme": "Apipavidalinimas"
+    "Theme": "Apipavidalinimas",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/nl.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortoets dient als teken (gebruik <kbd>Ctrl</kbd>+<kbd>m</kbd> of <kbd>Esc</kbd> om te schakelen)",
     "Show password": "Toon wachtwoord",
     "Hide password": "Verberg wachtwoord",
-    "Theme": "Thema"
+    "Theme": "Thema",
+    "Copy": "Kopieëren"
 }

+ 2 - 1
i18n/no.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/oc.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/pl.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Klawisz Tabulatora służy jako znak (użyj <kbd>Ctrl</kbd>+<kbd>m</kbd> lub <kbd>Esc</kbd>, aby przełączyć tryb)",
     "Show password": "Pokaż hasło",
     "Hide password": "Ukryj hasło",
-    "Theme": "Motyw"
+    "Theme": "Motyw",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/pt.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/ro.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 16 - 15
i18n/ru.json

@@ -3,7 +3,7 @@
     "%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s это минималистичный Open Source проект для создания заметок, где сервер не знает ничего о сохраняемых данных. Данные шифруются/расшифровываются %sв браузере%s с использованием 256 битного шифрования AES.",
     "More information on the <a href=\"https://privatebin.info/\">project page</a>.": "Подробнее можно узнать на <a href=\"https://privatebin.info/\">сайте проекта</a>.",
     "Because ignorance is bliss": "Потому что неведение - благо",
-    "Document does not exist, has expired or has been deleted.": "Запись не существует, просрочена или была удалена.",
+    "Document does not exist, has expired or has been deleted.": "Документ не существует, его срок действия истек или он был удален.",
     "%s requires php %s or above to work. Sorry.": "Для работы %s требуется php %s или выше. Извините.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s необходимо наличие секции [%s] в конфигурационном файле.",
     "Please wait %d seconds between each post.": [
@@ -14,13 +14,13 @@
         "Пожалуйста, ожидайте %d секунд между каждыми записями.",
         "Пожалуйста, ожидайте %d секунд между каждыми записями."
     ],
-    "Document is limited to %s of encrypted data.": "Размер записи ограничен %s зашифрованных данных.",
+    "Document is limited to %s of encrypted data.": "Размер документа ограничен %s зашифрованных данных.",
     "Invalid data.": "Неверные данные.",
     "You are unlucky. Try again.": "Вам не повезло. Попробуйте еще раз.",
     "Error saving comment. Sorry.": "Ошибка при сохранении комментария. Извините.",
-    "Error saving document. Sorry.": "Ошибка при сохранении записи. Извините.",
-    "Invalid document ID.": "Неверный ID записи.",
-    "Document is not of burn-after-reading type.": "Тип записи не \"Удалить после прочтения\".",
+    "Error saving document. Sorry.": "Ошибка сохранения документа. Извините.",
+    "Invalid document ID.": "Неверный ID документа.",
+    "Document is not of burn-after-reading type.": "Документ не относится к типу \"Удалить после прочтения\".",
     "Wrong deletion token. Document was not deleted.": "Неверный ключ удаления записи. Запись не удалена.",
     "Document was properly deleted.": "Запись была успешно удалена.",
     "JavaScript is required for %s to work. Sorry for the inconvenience.": "Для работы %s требуется включенный JavaScript. Приносим извинения за неудобства.",
@@ -210,26 +210,27 @@
     "Encrypted note on %s": "Зашифрованная запись на %s",
     "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Посетите эту ссылку чтобы просмотреть запись. Передача ссылки кому либо позволит им получить доступ к записи тоже.",
     "URL shortener may expose your decrypt key in URL.": "Сервис сокращения ссылок может получить ваш ключ расшифровки из ссылки.",
-    "URL shortener is enabled by default.": "URL shortener is enabled by default.",
+    "URL shortener is enabled by default.": "Сокращатель URL-адресов включен по умолчанию.",
     "Save document": "Сохранить запись",
     "Your IP is not authorized to create documents.": "Вашему IP адресу не разрешено создавать записи.",
     "Trying to shorten a URL that isn't pointing at our instance.": "Попытка сократить URL, который указывает не на наш сервер.",
     "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Ошибка обращения к YOURLS. Возможно в конфигурации допущена ошибка, например неверный или отсутствующий параметр \"apiurl\" или \"signature\".",
     "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Ошибка разбора ответа от YOURLS.",
-    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.",
+    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Ошибка прокси: неверный ответ. Это может быть проблемой конфигурации, например из-за неверных или отсутствующих ключей, либо временным сбоем.",
     "This secret message can only be displayed once. Would you like to see it now?": "Записи, удаляемые после прочтения, могут быть отображены после загрузки только один раз. Вы хотите открыть её сейчас?",
     "Yes, see it": "Да, загрузить",
     "Dark Mode": "Тёмная",
-    "Error compressing document, due to missing WebAssembly support.": "Error compressing document, due to missing WebAssembly support.",
-    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.",
-    "Start over": "Start over",
+    "Error compressing document, due to missing WebAssembly support.": "Ошибка сжатия документа из-за отсутствия поддержки WebAssembly.",
+    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Ошибка распаковки документа, ваш браузер не поддерживает WebAssembly. Пожалуйста, используйте другой браузер для просмотра этого документа.",
+    "Start over": "Начать заново",
     "Document copied to clipboard": "Запись скопирована в буфер обмена",
     "To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>": "Чтобы скопировать запись нажмите на кнопку копирования или используйте комбинацию клавиш <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>",
     "Copy link": "Скопировать ссылку",
     "Link copied to clipboard": "Ссылка скопирована в буфер обмена",
-    "Document text": "Document text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
-    "Show password": "Show password",
-    "Hide password": "Hide password",
-    "Theme": "Тема"
+    "Document text": "Текст документа",
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Клавиша Tab работает как символ (Нажмите <kbd>Ctrl</kbd>+<kbd>M</kbd> или <kbd>Esc</kbd> для переключения)",
+    "Show password": "Показать пароль",
+    "Hide password": "Скрыть пароль",
+    "Theme": "Тема",
+    "Copy": "Скопировать"
 }

+ 2 - 1
i18n/sk.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/sl.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatorska tipka služi kot znak (za preklop pritisnite <kbd>Ctrl</kbd>+<kbd>m</kbd> ali <kbd>Esc</kbd>)",
     "Show password": "Pokaži geslo",
     "Hide password": "Skrij geslo",
-    "Theme": "Tema"
+    "Theme": "Tema",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/sv.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabb-tangenten fungerar som tecken (Tryck <kbd>Ctrl</kbd>+<kbd>m</kbd> eller <kbd>Esc</kbd> för att växla)",
     "Show password": "Visa lösenord",
     "Hide password": "Dölj lösenord",
-    "Theme": "Tema"
+    "Theme": "Tema",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/th.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "ปุ่ม Tabulator ใช้เป็นอักขระ (กด <kbd>Ctrl</kbd>+<kbd>m</kbd> หรือ <kbd>Esc</kbd> เพื่อสลับ)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "ธีม"
+    "Theme": "ธีม",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/tr.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
     "Show password": "Show password",
     "Hide password": "Hide password",
-    "Theme": "Theme"
+    "Theme": "Theme",
+    "Copy": "Copy"
 }

+ 6 - 5
i18n/uk.json

@@ -5,7 +5,7 @@
     "Because ignorance is bliss": "Менше знаєш — краще спиш",
     "Document does not exist, has expired or has been deleted.": "Документ не існує. Можливо, його протерміновано чи видалено.",
     "%s requires php %s or above to work. Sorry.": "Для роботи %s потрібен php %s чи вище. Вибачте.",
-    "%s requires configuration section [%s] to be present in configuration file.": "%s потрібна секція [%s] в конфігураційному файлі.",
+    "%s requires configuration section [%s] to be present in configuration file.": "%s потрібна секція [%s] у файлі налаштувань.",
     "Please wait %d seconds between each post.": [
         "Будь ласка, зачекайте %d секунду між створеннями.",
         "Будь ласка, зачекайте %d секунди між створеннями.",
@@ -36,8 +36,8 @@
     "Discussion": "Обговорення",
     "Toggle navigation": "Перемкнути навігацію",
     "%d seconds": [
-        "%d секунду",
-        "%d секунди",
+        "%d секунд",
+        "%d секунд",
         "%d секунд",
         "%d секунд",
         "%d секунд",
@@ -213,7 +213,7 @@
     "URL shortener is enabled by default.": "Скорочення посилань типово увімкнено.",
     "Save document": "Зберегти документ",
     "Your IP is not authorized to create documents.": "Вашій IP-адресі не дозволено створювати документи.",
-    "Trying to shorten a URL that isn't pointing at our instance.": "Спроба скоротити URL, який не вказує на наш сервер.",
+    "Trying to shorten a URL that isn't pointing at our instance.": "Спроба скоротити URL, який не вказує на наш екземпляр.",
     "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Помилка проксі: не вказано URL-адреси проксі. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук.",
     "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Помилка проксі: не вдається розпізнати відповідь проксі. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук",
     "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Помилка проксі: хибна відповідь. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук.",
@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Клавіша Tab вводить символ табуляції (перемикається клавішами <kbd>Ctrl</kbd>+<kbd>m</kbd> чи <kbd>Esc</kbd>)",
     "Show password": "Показати пароль",
     "Hide password": "Сховати пароль",
-    "Theme": "Тема"
+    "Theme": "Тема",
+    "Copy": "Copy"
 }

+ 2 - 1
i18n/zh.json

@@ -231,5 +231,6 @@
     "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tab 键可作为字符(按 <kbd>Ctrl</kbd>+<kbd>m</kbd> 或 <kbd>Esc</kbd> 切换开关)",
     "Show password": "显示密码",
     "Hide password": "隐藏密码",
-    "Theme": "主题"
+    "Theme": "主题",
+    "Copy": "复制"
 }

+ 272 - 124
js/package-lock.json

@@ -17,7 +17,7 @@
         "jsverify": "^0.8.3",
         "mime-types": "^3.0.2",
         "mocha": "^11.7.5",
-        "nyc": "^17.1.0"
+        "nyc": "^18.0.0"
       }
     },
     "node_modules/@asamuzakjp/css-color": {
@@ -870,6 +870,7 @@
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
       "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "clean-stack": "^2.0.0",
         "indent-string": "^4.0.0"
@@ -937,7 +938,8 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
       "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/argparse": {
       "version": "2.0.1",
@@ -1115,6 +1117,7 @@
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
       "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -1695,12 +1698,6 @@
         }
       ]
     },
-    "node_modules/fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
-    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1932,27 +1929,11 @@
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
       "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
-    "node_modules/inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
-      "dev": true,
-      "dependencies": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
-    },
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2042,6 +2023,7 @@
       "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
       "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -2090,20 +2072,20 @@
       }
     },
     "node_modules/istanbul-lib-processinfo": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
-      "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.1.tgz",
+      "integrity": "sha512-s3mX05h5wGZeScG6XnOanygPh4SJu5ujMc9YbvpnLGXWy1cRiGbp0NdVcjHxgoZt3WfQppfBsa0y+gWdYJ2pGQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "archy": "^1.0.0",
         "cross-spawn": "^7.0.3",
         "istanbul-lib-coverage": "^3.2.0",
         "p-map": "^3.0.0",
-        "rimraf": "^3.0.0",
-        "uuid": "^8.3.2"
+        "rimraf": "^6.1.3"
       },
       "engines": {
-        "node": ">=8"
+        "node": "20 || >=22"
       }
     },
     "node_modules/istanbul-lib-report": {
@@ -2562,10 +2544,11 @@
       "dev": true
     },
     "node_modules/nyc": {
-      "version": "17.1.0",
-      "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz",
-      "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==",
+      "version": "18.0.0",
+      "resolved": "https://registry.npmjs.org/nyc/-/nyc-18.0.0.tgz",
+      "integrity": "sha512-G5UyHinFkB1BxqGTrmZdB6uIYH0+v7ZnVssuflUDi+J+RhKWyAhRT1RCehBSI6jLFLuUUgFDyLt49mUtdO1XeQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/load-nyc-config": "^1.0.0",
         "@istanbuljs/schema": "^0.1.2",
@@ -2576,11 +2559,11 @@
         "find-up": "^4.1.0",
         "foreground-child": "^3.3.0",
         "get-package-type": "^0.1.0",
-        "glob": "^7.1.6",
+        "glob": "^13.0.6",
         "istanbul-lib-coverage": "^3.0.0",
         "istanbul-lib-hook": "^3.0.0",
         "istanbul-lib-instrument": "^6.0.2",
-        "istanbul-lib-processinfo": "^2.0.2",
+        "istanbul-lib-processinfo": "^3.0.0",
         "istanbul-lib-report": "^3.0.0",
         "istanbul-lib-source-maps": "^4.0.0",
         "istanbul-reports": "^3.0.2",
@@ -2589,17 +2572,17 @@
         "p-map": "^3.0.0",
         "process-on-spawn": "^1.0.0",
         "resolve-from": "^5.0.0",
-        "rimraf": "^3.0.0",
+        "rimraf": "^6.1.3",
         "signal-exit": "^3.0.2",
-        "spawn-wrap": "^2.0.0",
-        "test-exclude": "^6.0.0",
+        "spawn-wrap": "^3.0.0",
+        "test-exclude": "^8.0.0",
         "yargs": "^15.0.2"
       },
       "bin": {
         "nyc": "bin/nyc.js"
       },
       "engines": {
-        "node": ">=18"
+        "node": "20 || >=22"
       }
     },
     "node_modules/nyc/node_modules/ansi-regex": {
@@ -2611,6 +2594,29 @@
         "node": ">=8"
       }
     },
+    "node_modules/nyc/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/nyc/node_modules/brace-expansion": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+      "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
     "node_modules/nyc/node_modules/cliui": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -2642,21 +2648,18 @@
       }
     },
     "node_modules/nyc/node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "version": "13.0.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+      "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
       },
       "engines": {
-        "node": "*"
+        "node": "18 || 20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -2674,6 +2677,32 @@
         "node": ">=8"
       }
     },
+    "node_modules/nyc/node_modules/lru-cache": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+      "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/nyc/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/nyc/node_modules/p-limit": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -2701,6 +2730,23 @@
         "node": ">=8"
       }
     },
+    "node_modules/nyc/node_modules/path-scurry": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+      "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/nyc/node_modules/resolve-from": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -2797,15 +2843,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
     "node_modules/optionator": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2858,6 +2895,7 @@
       "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
       "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "aggregate-error": "^3.0.0"
       },
@@ -2928,15 +2966,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -3165,37 +3194,104 @@
       }
     },
     "node_modules/rimraf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "version": "6.1.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
+      "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "glob": "^7.1.3"
+        "glob": "^13.0.3",
+        "package-json-from-dist": "^1.0.1"
       },
       "bin": {
-        "rimraf": "bin.js"
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/rimraf/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/rimraf/node_modules/brace-expansion": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+      "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
     "node_modules/rimraf/node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "version": "13.0.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+      "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
       },
       "engines": {
-        "node": "*"
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/lru-cache": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+      "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/rimraf/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/path-scurry": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+      "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -3315,15 +3411,17 @@
       }
     },
     "node_modules/spawn-wrap": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
-      "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-3.0.0.tgz",
+      "integrity": "sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
+        "cross-spawn": "^7.0.6",
         "foreground-child": "^2.0.0",
         "is-windows": "^1.0.2",
         "make-dir": "^3.0.0",
-        "rimraf": "^3.0.0",
+        "rimraf": "^6.1.3",
         "signal-exit": "^3.0.2",
         "which": "^2.0.1"
       },
@@ -3336,6 +3434,7 @@
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
       "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "cross-spawn": "^7.0.0",
         "signal-exit": "^3.0.2"
@@ -3348,7 +3447,8 @@
       "version": "3.0.7",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/sprintf-js": {
       "version": "1.0.3",
@@ -3492,35 +3592,99 @@
       "dev": true
     },
     "node_modules/test-exclude": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
-      "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz",
+      "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/schema": "^0.1.2",
-        "glob": "^7.1.4",
-        "minimatch": "^3.0.4"
+        "glob": "^13.0.6",
+        "minimatch": "^10.2.2"
       },
       "engines": {
-        "node": ">=8"
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/test-exclude/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/test-exclude/node_modules/brace-expansion": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+      "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
       }
     },
     "node_modules/test-exclude/node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "version": "13.0.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+      "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
       },
       "engines": {
-        "node": "*"
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/test-exclude/node_modules/lru-cache": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+      "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/test-exclude/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/test-exclude/node_modules/path-scurry": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+      "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -3658,16 +3822,6 @@
         "punycode": "^2.1.0"
       }
     },
-    "node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "deprecated": "uuid@10 and below is no longer supported.  For ESM codebases, update to uuid@latest.  For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
-      "dev": true,
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/w3c-xmlserializer": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -3861,12 +4015,6 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true
-    },
     "node_modules/write-file-atomic": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",

+ 1 - 1
js/package.json

@@ -15,7 +15,7 @@
     "jsverify": "^0.8.3",
     "mime-types": "^3.0.2",
     "mocha": "^11.7.5",
-    "nyc": "^17.1.0"
+    "nyc": "^18.0.0"
   },
   "scripts": {
     "lint": "eslint",

+ 12 - 29
js/privatebin.js

@@ -2219,6 +2219,10 @@ window.PrivateBin = (function () {
             loadconfirmClose.removeEventListener('click', Controller.newPaste);
             loadconfirmClose.addEventListener('click', Controller.newPaste);
 
+            $loadconfirmmodal.on('shown.bs.modal', () => {
+                $loadconfirmOpenNow.trigger('focus');
+            });
+
             if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip && typeof bootstrap.Modal === 'function') {
                 (new bootstrap.Modal(loadconfirmmodal)).show();
             } else {
@@ -4155,6 +4159,10 @@ window.PrivateBin = (function () {
                     triggerEmailSend(emailBody);
                 }
 
+                emailconfirmmodal.addEventListener('shown.bs.modal', () => {
+                    emailconfirmTimezoneUtc.focus();
+                });
+
                 emailconfirmTimezoneCurrent.removeEventListener('click', sendEmailAndHideModal);
                 emailconfirmTimezoneCurrent.addEventListener('click', sendEmailAndHideModal);
                 emailconfirmTimezoneUtc.removeEventListener('click', sendEmailAndHideModal);
@@ -5545,8 +5553,6 @@ window.PrivateBin = (function () {
 
         let copyButton,
             copyLinkButton,
-            copyIcon,
-            successIcon,
             shortcutHint,
             url;
 
@@ -5564,7 +5570,6 @@ window.PrivateBin = (function () {
                 const text = PasteViewer.getText();
                 saveToClipboard(text);
 
-                toggleSuccessIcon();
                 showAlertMessage('Document copied to clipboard');
             });
         }
@@ -5648,23 +5653,6 @@ window.PrivateBin = (function () {
             Alert.showStatus(message);
         }
 
-        /**
-         * Toogle success icon after copy
-         *
-         * @name CopyToClipboard.toggleSuccessIcon
-         * @private
-         * @function
-         */
-        function toggleSuccessIcon() {
-            copyIcon.style.display = 'none';
-            successIcon.style.display = 'block';
-
-            setTimeout(function () {
-                copyIcon.style.display = 'block';
-                successIcon.style.display = 'none';
-            }, 1000);
-        }
-
         /**
          * Show keyboard shortcut hint
          *
@@ -5672,10 +5660,7 @@ window.PrivateBin = (function () {
          * @function
          */
         me.showKeyboardShortcutHint = function () {
-            I18n._(
-                shortcutHint,
-                'To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>'
-            );
+            shortcutHint.classList.remove('hidden');
         };
 
         /**
@@ -5685,7 +5670,7 @@ window.PrivateBin = (function () {
          * @function
          */
         me.hideKeyboardShortcutHint = function () {
-            shortcutHint.innerHTML = '';
+            shortcutHint.classList.add('hidden');
         };
 
         /**
@@ -5706,11 +5691,9 @@ window.PrivateBin = (function () {
          * @function
          */
         me.init = function () {
-            copyButton = document.getElementById('prettyMessageCopyBtn');
+            copyButton = document.getElementById('copyShortcutHintBtn');
             copyLinkButton = document.getElementById('copyLink');
-            copyIcon = document.getElementById('copyIcon');
-            successIcon = document.getElementById('copySuccessIcon');
-            shortcutHint = document.getElementById('copyShortcutHintText');
+            shortcutHint = document.getElementById('copyShortcutHint');
 
             handleCopyButtonClick();
             handleCopyLinkButtonClick();

+ 35 - 26
js/test/CopyToClipboard.js

@@ -18,12 +18,15 @@ describe('CopyToClipboard', function () {
                     document.body.innerHTML = (
                         '<div id="status" class="hidden"></div>' +
                         '<div id="errormessage" class="hidden"></div>' +
-                        '<div id="placeholder" class="hidden">+++ no document text ' +
-                        '+++</div><div id="prettymessage" class="hidden">' +
-                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
-                        '<svg id="copySuccessIcon"></svg></button><pre ' +
-                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                        '</div><div id="plaintext" class="hidden"></div>'
+                        '<div id="placeholder"></div>' +
+					'<div id="attachmentPreview" class="hidden"></div>' +
+                    '<h5 id="copyShortcutHint" class="hidden">' +
+						'<small id="copyShortcutHintText"></small>' +
+                        '<button type="button" id="copyShortcutHintBtn"></button>' +
+					'</h5>' +
+                    '<div id="prettymessage" class="hidden">' +
+                        '<pre id="prettyprint"></pre>' +
+                        '</div>'
                     );
 
                     PrivateBin.Alert.init();
@@ -34,7 +37,7 @@ describe('CopyToClipboard', function () {
 
                     PrivateBin.CopyToClipboard.init();
 
-                    document.getElementById('prettyMessageCopyBtn').click();
+                    document.getElementById('copyShortcutHintBtn').click();
 
                     const savedToClipboardText = await navigator.clipboard.readText();
 
@@ -57,12 +60,15 @@ describe('CopyToClipboard', function () {
                     document.body.innerHTML = (
                         '<div id="status" class="hidden"></div>' +
                         '<div id="errormessage" class="hidden"></div>' +
-                        '<div id="placeholder">+++ no document text ' +
-                        '+++</div><div id="prettymessage" class="hidden">' +
-                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
-                        '<svg id="copySuccessIcon"></svg></button><pre ' +
-                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                        '</div><div id="plaintext" class="hidden"></div>'
+                        '<div id="placeholder"></div> ' +
+                        '<div id="attachmentPreview" class="hidden"></div>' +
+                    '<h5 id="copyShortcutHint" class="hidden">' +
+                        '<small id="copyShortcutHintText"></small>' +
+						'<button type="button" id="copyShortcutHintBtn"></button>' +
+					'</h5>' +
+                    '<div id="prettymessage" class="hidden">' +
+                        '<pre id="prettyprint"></pre>' +
+                        '</div>'
                     );
 
                     PrivateBin.Alert.init();
@@ -131,34 +137,37 @@ describe('CopyToClipboard', function () {
     });
 
     describe('Keyboard shortcut hint', function () {
-        it('shows hint', () => {
-            fc.assert(fc.property(fc.string(),
-                function (text) {
-                    document.body.innerHTML = '<small id="copyShortcutHintText"></small>';
+        jsc.property('Show hint', function () {
+                var clean = jsdom();
+
+                $('body').html('<small id="copyShortcutHintText"></small>');
 
                     PrivateBin.CopyToClipboard.init();
                     PrivateBin.CopyToClipboard.showKeyboardShortcutHint();
 
-                    const keyboardShortcutHint = document.getElementById('copyShortcutHintText').textContent;
+                const keyboardShortcutHint = $('#copyShortcutHintText').text();
+
+                clean();
 
-                    return keyboardShortcutHint.length > 0;
+                    return !hasHidden;
                 }
             ));
         });
     });
 
-    it('Hide hint', () => {
-        fc.assert(fc.property(
-            fc.string({minLength: 1}),
-            function (text) {
-                document.body.innerHTML = '<small id="copyShortcutHintText">' + text + '</small>';
+        jsc.property('Hide hint', function () {
+                var clean = jsdom();
+
+                $('body').html('<small id="copyShortcutHintText">' + text + '</small>');
 
                 PrivateBin.CopyToClipboard.init();
                 PrivateBin.CopyToClipboard.hideKeyboardShortcutHint();
 
-                const keyboardShortcutHint = document.getElementById('copyShortcutHintText').textContent;
+                const keyboardShortcutHint = $('#copyShortcutHintText').text();
+
+                clean();
 
-                return keyboardShortcutHint.length === 0;
+                return hasHidden;
             }
         ));
     });

+ 1 - 1
lib/Proxy/YourlsProxy.php

@@ -65,7 +65,7 @@ class YourlsProxy extends AbstractProxy
      */
     protected function _extractShortUrl(array $data): ?string
     {
-        if (($data['statusCode'] ?? 0) === 200) {
+        if ((int) ($data['statusCode'] ?? 0) === 200) {
             return $data['shorturl'] ?? 0;
         }
         return null;

+ 10 - 5
tpl/bootstrap.php

@@ -622,12 +622,17 @@ endif;
 				<article class="row">
 					<div id="placeholder" class="col-md-12 hidden"><?php echo I18n::_('+++ no document text +++'); ?></div>
 					<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>
-					<h5 id="copyShortcutHint" class="col-md-12"><small id="copyShortcutHintText"></small></h5>
+					<h5 id="copyShortcutHint" class="media col-md-12 hidden" style="margin-top: 0;">
+						<div class="media-body media-middle">
+							<small id="copyShortcutHintText" class="hidden-xs">
+								<?php echo I18n::_("To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>") ?>
+							</small>
+						</div>
+						<div class="media-right media-middle">
+							<button type="button" id="copyShortcutHintBtn" class="btn btn-default"><?php echo I18n::_('Copy'); ?></button>
+						</div>
+					</h5>
 					<div id="prettymessage" class="col-md-12 hidden">
-						<button id="prettyMessageCopyBtn">
-							<span id="copyIcon" class="glyphicon glyphicon-duplicate" aria-hidden="true"></span>
-							<span id="copySuccessIcon" class="glyphicon glyphicon-ok text-success" aria-hidden="true"></span>
-						</button>
 						<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
 					</div>
 					<div id="plaintext" class="col-md-12 hidden"></div>

+ 6 - 5
tpl/bootstrap5.php

@@ -480,12 +480,13 @@ endif;
 				<article>
 					<div id="placeholder" class="col-md-12 hidden"><?php echo I18n::_('+++ no document text +++'); ?></div>
 					<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>
-					<h6 id="copyShortcutHint" class="col-md-12"><small id="copyShortcutHintText"></small></h6>
+					<h6 id="copyShortcutHint" class="col-md-12 nav justify-content-between align-items-center mb-2 hidden">
+						<small id="copyShortcutHintText" class="d-none d-md-inline">
+							<?php echo I18n::_("To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>") ?>
+						</small>
+						<button type="button" id="copyShortcutHintBtn" class="btn btn-secondary ms-auto"><?php echo I18n::_('Copy'); ?></button>
+					</h6>
 					<div id="prettymessage" class="card col-md-12 hidden">
-						<button type="button" id="prettyMessageCopyBtn" class="text-secondary opacity-05-1-hover">
-							<svg id="copyIcon" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#copy" /></svg>
-							<svg id="copySuccessIcon" class="text-success" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#check" /></svg>
-						</button>
 						<pre id="prettyprint" class="card-body col-md-12 prettyprint linenums:1"></pre>
 					</div>
 					<div id="plaintext" class="col-md-12 hidden"></div>

+ 5 - 0
tst/Bootstrap.php

@@ -792,6 +792,11 @@ class ConnectionInterfaceStub implements ConnectionInterface
         throw new BadMethodCallException('not supported by this stub');
     }
 
+    public function headObject(array $args = array()): array
+    {
+        throw new BadMethodCallException('not supported by this stub');
+    }
+
     public function listObjects(array $args = array())
     {
         throw new BadMethodCallException('not supported by this stub');

+ 10 - 0
tst/YourlsProxyTest.php

@@ -51,6 +51,16 @@ class YourlsProxyTest extends TestCase
         $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
     }
 
+    public function testYourlsProxyWithStringStatusCode(): void
+    {
+        // YOURLS API returns statusCode as a string "200", not integer 200
+        file_put_contents($this->_mock_yourls_service, '{"shorturl":"https:\/\/example.com\/1","statusCode":"200"}');
+
+        $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
+        $this->assertFalse($yourls->isError());
+        $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
+    }
+
     /**
      * @dataProvider providerInvalidUrl
      */