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

Merge branch 'master' into purify-3.3.0

El RIDO 9 месяцев назад
Родитель
Сommit
43cf8b53ac
7 измененных файлов с 202 добавлено и 163 удалено
  1. 4 4
      .github/workflows/tests.yml
  2. 2 0
      CHANGELOG.md
  3. 108 108
      i18n/uk.json
  4. 2 2
      i18n/zh.json
  5. 44 48
      js/privatebin.js
  6. 41 0
      js/test/AttachmentViewer.js
  7. 1 1
      lib/Configuration.php

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

@@ -112,7 +112,7 @@ 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
@@ -203,7 +203,7 @@ jobs:
 
     - name: Upload Test Results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: Test Results
         path: tst/results.xml
@@ -235,7 +235,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
@@ -245,7 +245,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 }}"

+ 2 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 ## 2.0.2 (not yet released)
 * CHANGED: Upgrading libraries to: DOMpurify 3.3.0
+* CHANGED: Refactored jQuery DOM element creation into plain JavaScript
+* FIXED: Sanitize file name in attachment size hint
 
 ## 2.0.1 (2025-10-12)
 * ADDED: Auto shorten URLs with config option `shortenbydefault` (#1627)

+ 108 - 108
i18n/uk.json

@@ -1,10 +1,10 @@
 {
     "PrivateBin": "PrivateBin",
-    "%s is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted %sin the browser%s using 256 bits AES.": "%s це мінімалістичний 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.": "Допис не існує, протермінований чи був видалений.",
-    "%s requires php %s or above to work. Sorry.": "Для роботи %s потрібен php %s и вище. Вибачте.",
+    "%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 — мінімалістична вільна програма для створення нотаток, сервер якої не знає нічого про дані, що зберігаються. Дані шифруються/розшифровуються %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.": "Документ не існує. Можливо, його протерміновано чи видалено.",
+    "%s requires php %s or above to work. Sorry.": "Для роботи %s потрібен php %s чи вище. Вибачте.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s потрібна секція [%s] в конфігураційному файлі.",
     "Please wait %d seconds between each post.": [
         "Будь ласка, зачекайте %d секунду між створеннями.",
@@ -14,18 +14,18 @@
         "Будь ласка, зачекайте %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.": "Тип допису не \"Знищити після прочитання\".",
-    "Wrong deletion token. Document was not deleted.": "Неправильний жетон вилучення допису. Допис не вилучено.",
-    "Document was properly deleted.": "Допис був вилучений повністю.",
+    "Error saving comment. Sorry.": "Не вдалося зберегти коментар. Вибачте.",
+    "Error saving document. Sorry.": "Не вдалося зберегти документ. Вибачте.",
+    "Invalid document ID.": "Хибний ідентифікатор документа.",
+    "Document is not of burn-after-reading type.": "Тип документа не «Знищити після прочитання».",
+    "Wrong deletion token. Document was not deleted.": "Хибний токен видалення. Документ не видалено.",
+    "Document was properly deleted.": "Документ видалено повністю.",
     "JavaScript is required for %s to work. Sorry for the inconvenience.": "Для роботи %s потрібен увімкнутий JavaScript. Вибачте.",
     "%s requires a modern browser to work.": "Для роботи %s потрібен більш сучасний браузер.",
-    "New": "Новий допис",
+    "New": "Новий документ",
     "Create": "Створити",
     "Clone": "Дублювати",
     "Raw text": "Початковий текст",
@@ -36,8 +36,8 @@
     "Discussion": "Обговорення",
     "Toggle navigation": "Перемкнути навігацію",
     "%d seconds": [
-        "%d секунд",
-        "%d секунд",
+        "%d секунду",
+        "%d секунди",
         "%d секунд",
         "%d секунд",
         "%d секунд",
@@ -92,144 +92,144 @@
         "%d років"
     ],
     "Never": "Ніколи",
-    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Примітка: Це тестовий сервіс: Дані можуть бути вилучені в будь який момент. Кошенята помруть, якщо ви будете зловживати сервісом.",
+    "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Примітка: це тестовий сервіс, і дані можуть бути видалені в будь-який момент. Будете зловживати — пси вам марша зіграють.",
     "This document will expire in %d seconds.": [
-        "Документ буде вилучений через %d секунду.",
-        "Документ буде вилучений через %d секунди.",
-        "Документ буде вилучений через %d секунд.",
-        "Документ буде вилучений через %d секунд.",
-        "Документ буде вилучений через %d секунд.",
-        "Документ буде вилучений через %d секунд."
+        "Документ буде видалено через %d секунду.",
+        "Документ буде видалено через %d секунди.",
+        "Документ буде видалено через %d секунд.",
+        "Документ буде видалено через %d секунд.",
+        "Документ буде видалено через %d секунд.",
+        "Документ буде видалено через %d секунд."
     ],
     "This document will expire in %d minutes.": [
-        "Документ буде вилучений через %d хвилину.",
-        "Документ буде вилучений через %d хвилини.",
-        "Документ буде вилучений через %d хвилин.",
-        "Документ буде вилучений через %d хвилин.",
-        "Документ буде вилучений через %d хвилин.",
-        "Документ буде вилучений через %d хвилин."
+        "Документ буде видалено через %d хвилину.",
+        "Документ буде видалено через %d хвилини.",
+        "Документ буде видалено через %d хвилин.",
+        "Документ буде видалено через %d хвилин.",
+        "Документ буде видалено через %d хвилин.",
+        "Документ буде видалено через %d хвилин."
     ],
     "This document will expire in %d hours.": [
-        "Документ буде вилучений через %d годину.",
-        "Документ буде вилучений через %d години.",
-        "Документ буде вилучений через %d годин.",
-        "Документ буде вилучений через %d годин.",
-        "Документ буде вилучений через %d годин.",
-        "Документ буде вилучений через %d годин."
+        "Документ буде видалено через %d годину.",
+        "Документ буде видалено через %d години.",
+        "Документ буде видалено через %d годин.",
+        "Документ буде видалено через %d годин.",
+        "Документ буде видалено через %d годин.",
+        "Документ буде видалено через %d годин."
     ],
     "This document will expire in %d days.": [
-        "Документ буде вилучений через %d день.",
-        "Документ буде вилучений через %d дні.",
-        "Документ буде вилучений через %d днів.",
-        "Документ буде вилучений через %d днів.",
-        "Документ буде вилучений через %d днів.",
-        "Документ буде вилучений через %d днів."
+        "Документ буде видалено через %d день.",
+        "Документ буде видалено через %d дні.",
+        "Документ буде видалено через %d днів.",
+        "Документ буде видалено через %d днів.",
+        "Документ буде видалено через %d днів.",
+        "Документ буде видалено через %d днів."
     ],
     "This document will expire in %d months.": [
-        "Документ буде вилучений через %d місяць.",
-        "Документ буде вилучений через %d місяці.",
-        "Документ буде вилучений через %d місяців.",
-        "Документ буде вилучений через %d місяців.",
-        "Документ буде вилучений через %d місяців.",
-        "Документ буде вилучений через %d місяців."
+        "Документ буде видалено через %d місяць.",
+        "Документ буде видалено через %d місяці.",
+        "Документ буде видалено через %d місяців.",
+        "Документ буде видалено через %d місяців.",
+        "Документ буде видалено через %d місяців.",
+        "Документ буде видалено через %d місяців."
     ],
-    "Please enter the password for this document:": "Будь ласка, введіть пароль від допису:",
-    "Could not decrypt data (Wrong key?)": "Неможливо розшифрувати дані (можливо, невірний ключ?)",
-    "Could not delete the document, it was not stored in burn after reading mode.": "Неможливо вилучити допис, він не був збережений в режимі знищити після прочитання.",
-    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "ЛИШЕ ДЛЯ ВАШИХ ОЧЕЙ. Не закривайте це вікно, це повідомлення не може бути показано знову.",
-    "Could not decrypt comment; Wrong key?": "Неможливо розшифрувати коментар; Неправильний ключ?",
+    "Please enter the password for this document:": "Введіть пароль від документа:",
+    "Could not decrypt data (Wrong key?)": "Неможливо розшифрувати дані (хибний ключ?)",
+    "Could not delete the document, it was not stored in burn after reading mode.": "Неможливо видалити документ, його не збережено в режимі «Знищити після прочитання».",
+    "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "ЛИШЕ ДЛЯ ВАС. Не закривайте вікно, це повідомлення одноразове.",
+    "Could not decrypt comment; Wrong key?": "Неможливо розшифрувати коментар. Хибний ключ?",
     "Reply": "Відповісти",
     "Anonymous": "Анонім",
-    "Avatar generated from IP address": "Аватар зґенерований з IP-адреси",
+    "Avatar generated from IP address": "Аватар зґенеровано з IP-адреси",
     "Add comment": "Додати коментар",
     "Optional nickname…": "Необов’язкове прізвисько…",
-    "Post comment": "Відправити коментар",
-    "Sending comment…": "Відправка коментаря…",
-    "Comment posted.": "Коментар опублікований.",
+    "Post comment": "Надіслати коментар",
+    "Sending comment…": "Надсилання коментаря…",
+    "Comment posted.": "Коментар опубліковано.",
     "Could not refresh display: %s": "Не вдалося оновити екран: %s",
     "unknown status": "невідома причина",
     "server error or not responding": "помилка на сервері чи немає відповіді",
     "Could not post comment: %s": "Не вдалося опублікувати коментар: %s",
-    "Sending document…": "Відправка допису…",
-    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span>": "Посилання на допис <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Тисніть <kbd>Ctrl</kbd>+<kbd>c</kbd>, щоб скопіювати посилання)</span>",
-    "Delete data": "Видалити допис",
-    "Could not create document: %s": "Не вдалося опублікувати допис: %s",
-    "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Неможливо розшифрувати запис: Ключ дешифрування відсутній в посиланні (Можливо, ви використовуєте скорочувач посилань, що видаляє частину посилання?)",
-    "B": "байт",
-    "kB": "кбайт",
-    "MB": "Мбайт",
-    "GB": "Гбайт",
-    "TB": "Тбайт",
-    "PB": "Пбайт",
-    "EB": "Ебайт",
-    "ZB": "Збайт",
-    "YB": "Йбайт",
+    "Sending document…": "Надсилання документа…",
+    "Your document is <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span>": "Документ: <a id=\"pasteurl\" href=\"%s\">%s</a> <span id=\"copyhint\">(копіювати: <kbd>Ctrl</kbd>+<kbd>c</kbd>)</span>",
+    "Delete data": "Видалити дані",
+    "Could not create document: %s": "Не вдалося опублікувати документ: %s",
+    "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Не вдається розшифрувати документ: ключ дешифрування відсутній у посиланні (можливо, ваш сервіс переспрямування чи скорочення посилань видаляє частину посилання?)",
+    "B": "Б",
+    "kB": "кБ",
+    "MB": "МБ",
+    "GB": "ГБ",
+    "TB": "ТБ",
+    "PB": "ПБ",
+    "EB": "ЕБ",
+    "ZB": "ЗБ",
+    "YB": "ЙБ",
     "Format": "Формат",
     "Plain Text": "Звичайний текст",
     "Source Code": "Вихідний код",
     "Markdown": "Мова розмітки",
-    "Download attachment": "Звантажити прикріплений файл",
-    "Cloned: '%s'": "Дубльовано: '%s'",
-    "The cloned file '%s' was attached to this document.": "Дублікат файлу '%s' був прикріплений до цього запису.",
-    "Attach a file": "Прикріпити файл",
-    "alternatively drag & drop a file or paste an image from the clipboard": "також можна перенести файл у вікно переглядача чи вставити зображення з буфера",
-    "File too large, to display a preview. Please download the attachment.": "Файл завеликий для відображення передогляду. Будь ласка, звантажте прикріплений файл.",
+    "Download attachment": "Завантажити вкладення",
+    "Cloned: '%s'": "Дубльовано: «%s»",
+    "The cloned file '%s' was attached to this document.": "Дублікат файлу «%s» вкладено до цього документа.",
+    "Attach a file": "Вкласти файл",
+    "alternatively drag & drop a file or paste an image from the clipboard": "також можна перенести файл у вікно браузера чи вставити зображення з буфера",
+    "File too large, to display a preview. Please download the attachment.": "Файл завеликий для передогляду. Будь ласка, завантажте вкладення.",
     "Remove attachment": "Видалити вкладення",
-    "Your browser does not support uploading encrypted files. Please use a newer browser.": "Ваш переглядач не підтримує відправлення зашифрованих файлів. Використовуйте сучасніший переглядач.",
-    "Invalid attachment.": "Невідоме вкладення.",
+    "Your browser does not support uploading encrypted files. Please use a newer browser.": "Ваш браузер не підтримує надсилання зашифрованих файлів. Використовуйте сучасніший браузер.",
+    "Invalid attachment.": "Непідтримуване вкладення.",
     "Options": "Опції",
     "Shorten URL": "Коротке посилання",
     "Editor": "Редактор",
     "Preview": "Передогляд",
-    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "Змінна PATH необхідна %s в конці \"%s\". Будь ласка, оновіть змінну PATH у вашому index.php.",
+    "%s requires the PATH to end in a \"%s\". Please update the PATH in your index.php.": "Для роботи %s у кінці змінної PATH має бути «%s». Будь ласка, оновіть змінну PATH у вашому index.php.",
     "Decrypt": "Розшифрувати",
     "Enter password": "Введіть пароль",
     "Loading…": "Завантаження…",
-    "Decrypting document…": "Розшифровування допису…",
-    "Preparing new document…": "Приготування нового допису…",
+    "Decrypting document…": "Розшифрування документа…",
+    "Preparing new document…": "Підготовка нового документа…",
     "In case this message never disappears please have a look at <a href=\"%s\">this FAQ for information to troubleshoot</a>.": "Якщо це повідомлення не зникатиме тривалий час, подивіться <a href=\"%s\">цей FAQ з інформацією про можливе вирішення проблеми</a>.",
-    "+++ no document text +++": "+++ у дописі немає тексту +++",
-    "Could not get document data: %s": "Не вдалося отримати дані допису: %s",
+    "+++ no document text +++": "+++ документ без тексту +++",
+    "Could not get document data: %s": "Не вдалося отримати дані документа: %s",
     "QR code": "QR код",
-    "This website is using an insecure HTTP connection! Please use it only for testing.": "Цей сайт використовує незахищене HTTP підключення! Будь ласка, використовуйте його лише для тестування.",
-    "For more information <a href=\"%s\">see this FAQ entry</a>.": "Для подробиць <a href=\"%s\">дивіться інформацію в FAQ</a>.",
-    "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.": "Ваш переглядач вимагає підключення HTTPS для підтримки  WebCrypto API. Спробуйте <a href=\"%s\">перемкнутися на HTTPS</a>.",
-    "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "Ваш переглядач не підтримує WebAssembly, що використовується для стиснення zlib. Ви можете створювати нестиснені документи, але не зможете читати стиснені.",
-    "waiting on user to provide a password": "очікування користувача для вводу паролю",
+    "This website is using an insecure HTTP connection! Please use it only for testing.": "Сайт використовує незахищене HTTP-з'єднання! Користуйтесь лише для тестування.",
+    "For more information <a href=\"%s\">see this FAQ entry</a>.": "Докладніше — <a href=\"%s\">в FAQ</a>.",
+    "Your browser may require an HTTPS connection to support the WebCrypto API. Try <a href=\"%s\">switching to HTTPS</a>.": "WebCrypto API може не працювати у вашому браузері без HTTPS-з'єднання. Спробуйте <a href=\"%s\">перемкнутися на HTTPS</a>.",
+    "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "Ваш браузер не підтримує WebAssembly, без якого zlib-стиснення не працює. Ви можете створювати нестиснені документи, але не можете читати стиснені.",
+    "waiting on user to provide a password": "очікування користувача для вводу пароля",
     "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.": "Не вдалося розшифрувати дані. Може, ви ввели неправильний пароль? Спробуйте знову за допомогою верхньої кнопки.",
     "Retry": "Спробуйте ще раз",
-    "Showing raw text…": "Відображається неформатований текст…",
+    "Showing raw text…": "Показано неформатований текст…",
     "Notice:": "Зверніть увагу:",
     "This link will expire after %s.": "Термін дії цього посилання сплине через %s.",
-    "This link can only be accessed once, do not use back or refresh button in your browser.": "Дане посилання доступна тільки один раз, не натискайте кнопку назад та не обновляйте сторінку браузера.",
+    "This link can only be accessed once, do not use back or refresh button in your browser.": "Посилання доступне тільки один раз, не натискайте кнопку «Назад» і не оновлюйте сторінку браузера.",
     "Link:": "Посилання:",
     "Recipient may become aware of your timezone, convert time to UTC?": "Отримувач дізнається ваш часовий пояс, перетворити час в UTC?",
     "Use Current Timezone": "Використовувати поточний часовий пояс",
     "Convert To UTC": "Конвертувати в UTC",
     "Close": "Закрити",
-    "Encrypted note on %s": "Зашифрована нотатка на %s",
+    "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.",
-    "URL shortener is enabled by default.": "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, який не вказує на наш екземпляр.",
-    "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.",
+    "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, який не вказує на наш сервер.",
+    "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Помилка проксі: не вказано URL-адреси проксі. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук.",
+    "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Помилка проксі: не вдається розпізнати відповідь проксі. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук",
+    "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Помилка проксі: хибна відповідь. Ймовірно, проблема в налаштуваннях: ключі конфігурації можуть бути відсутні або містити одрук.",
     "This secret message can only be displayed once. Would you like to see it now?": "Це таємне повідомлення можна надіслати лише один раз. Хочете переглянути його зараз?",
     "Yes, see it": "Так, побачити",
     "Dark Mode": "Темний режим",
-    "Error compressing document, due to missing WebAssembly support.": "Помилка при стисканні допису, через відсутність підтримки WebAssembly сервера.",
-    "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Помилка при розпакуванні допису, бо ваш браузер не підтримує WebAssembly. Будь ласка, відкрийте в іншому браузері для перегляду цього допису.",
+    "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": "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>": "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>",
-    "Copy link": "Copy link",
-    "Link copied to clipboard": "Link copied to clipboard",
-    "Document text": "Document text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
-    "Show password": "Show password",
-    "Hide password": "Hide password",
-    "Theme": "Theme"
+    "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": "Текст документа",
+    "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": "Тема"
 }

+ 2 - 2
i18n/zh.json

@@ -229,7 +229,7 @@
     "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",
+    "Show password": "显示密码",
+    "Hide password": "隐藏密码",
     "Theme": "主题"
 }

+ 44 - 48
js/privatebin.js

@@ -2992,7 +2992,8 @@ jQuery.PrivateBin = (function($) {
                 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
@@ -3287,44 +3288,38 @@ jQuery.PrivateBin = (function($) {
             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);
                 }
             }
         };
@@ -3603,8 +3598,9 @@ jQuery.PrivateBin = (function($) {
             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].appendChild(anonCommenter);
             }
 
             // set date
@@ -3617,14 +3613,10 @@ jQuery.PrivateBin = (function($) {
             // 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(image);
             }
 
             // starting point (default value/fallback)
@@ -5418,6 +5410,10 @@ jQuery.PrivateBin = (function($) {
                         plaintexts[i][1]
                     );
                 }
+                $(document).on('languageLoaded', function () {
+                    $('#commentcontainer').find('img.vizhash')
+                        .prop('title', I18n._('Avatar generated from IP address'));
+                });
             });
         }
 

+ 41 - 0
js/test/AttachmentViewer.js

@@ -129,5 +129,46 @@ describe('AttachmentViewer', function () {
                 return results.every(element => element);
             }
         );
+
+        it(
+            'sanitizes file names in attachments',
+            function() {
+                const clean = jsdom();
+                $('body').html(
+                    '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
+                    '<div id="attachment" class="hidden"></div>' +
+                    '<div id="templates">' +
+                        '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
+                            '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
+                            '<a class="alert-link">Download attachment</a>' +
+                        '</div>' +
+                    '</div>'
+                );
+                // mock createObjectURL for jsDOM
+                if (typeof window.URL.createObjectURL === 'undefined') {
+                    Object.defineProperty(
+                        window.URL,
+                        'createObjectURL',
+                        {value: function(blob) {
+                            return 'blob:' + location.origin + '/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed';
+                        }}
+                    )
+                }
+                $.PrivateBin.AttachmentViewer.init();
+                $.PrivateBin.Model.init();
+                global.atob = common.atob;
+
+                const maliciousFileNames = [
+                    '<script>alert("☹️");//<a',
+                    '"><meta http-equiv="refresh" content="0;url=http://example.com/">.txt',
+                ];
+                for (const filename of maliciousFileNames) {
+                    $.PrivateBin.AttachmentViewer.setAttachment('data:;base64,', filename);
+                    assert.ok(!$('body').html().includes(filename));
+                }
+                clean();
+            }
+        );
+
     });
 });

+ 1 - 1
lib/Configuration.php

@@ -121,7 +121,7 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-rGXYUpIqbFoHAgBXZ0UlJBdNAIMOC9EQ67MG0X46D5uRB8LvwzgKirbSQRGdYfk8I2jsUcm+tvHXYboUnC6DUg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-2F02E/UQoQyCNk0FvwaOLD9MvDhtuYqTtGKdqwsbDjY4O0jMZjn/EtiP2wvS0uxYojkxeUitF0HWb+RDFUwQXg==',
+            'js/privatebin.js'       => 'sha512-7b9PUe3bhiZAjMlNMuRMLkiiTx4CpIlh/L/XAfj+zmQ0DUFXIVkdliOLEZixXliYc3N97OOZEJPpRA7VYMLyjw==',
             'js/purify-3.3.0.js'     => 'sha512-lsHD5zxs4lu/NDzaaibe27Vd2t7Cy9JQ3qDHUvDfb4oZvKoWDNEhwUY+4bT3R68cGgpgCYp8U1x2ifeVxqurdQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.1-1.js'     => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',