From 5018c963f986be85d7d00ee7755d4bc4d550eb82 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 28 Oct 2025 16:53:07 +0100 Subject: [PATCH 01/80] chore: prepare for next release --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1384fd78..6c71c2d1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all coverage coverage-js coverage-php doc doc-js doc-php increment sign test test-js test-php help CURRENT_VERSION = 2.0.2 -VERSION ?= 2.0.2 +VERSION ?= 2.0.3 VERSION_FILES = README.md SECURITY.md doc/Installation.md js/package.json lib/Controller.php Makefile REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g") REGEX_VERSION := $(shell echo $(VERSION) | sed "s/\./\\\./g") From 9d7508f44fac95dfadf4aad4fb3d3be128633336 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 28 Oct 2025 16:54:42 +0100 Subject: [PATCH 02/80] chore: prepare for next release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6897e35d..6c368725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # PrivateBin version history +## 2.0.3 (not yet released) + ## 2.0.2 (2025-10-28) * CHANGED: Upgrading libraries to: DOMpurify 3.3.0 * CHANGED: Refactored jQuery DOM element creation into plain JavaScript From 207c79057b753b2cba35d7ebdbaab188f329144c Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 30 Oct 2025 13:18:30 +0100 Subject: [PATCH 03/80] Add GitHub Copilot instructions As per discussion in https://github.com/orgs/PrivateBin/discussions/1696 here are some instructions generated by Copilot itself. I used this prompt (in chat not agent mode, but well... should be enough) as suggested by GitHub: https://docs.github.com/copilot/how-tos/configure-custom-instructions/add-repository-instructions#anweisen-von-copilot-programmier-agent-zum-generieren-einer-copilot-instructionsmd-datei Also added the feedback from the discussion. --- .github/copilot-instructions.md | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..00dd1928 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,118 @@ +# Copilot Coding Agent Onboarding for PrivateBin + +## Repository Overview + +**PrivateBin** is a minimalist, open-source online pastebin where the server has zero knowledge of the pasted data. All data is encrypted/decrypted in the browser via 256-bit AES (Galois Counter Mode). The project is a refactored fork of ZeroBin focused on extensibility and additional features. + +- **Main Use Case:** Secure, ephemeral sharing of text/code, with encryption happening exclusively client-side. +- **Languages:** PHP (~48%), JavaScript (~35%), CSS (~17%), with some legacy browser support (see `legacy.js`). +- **Type:** Web application (pastebin), with both server (PHP) and client (JavaScript) components. + +## Build & Validation Instructions + +### Prerequisites + +- **PHP:** 7.0+ (recommended: latest stable 7.x or 8.x) +- **Composer:** For dependency management (`composer install`) +- **Node.js & npm:** Only required for running JavaScript unit tests. **Main JS logic must remain browser-compatible!** +- **Recommended Environment:** Unix-like OS (Linux, macOS) with Apache or Nginx for full demo. + +### Bootstrap & Install + +1. **Clone the repository** and enter its root directory. +2. **Install PHP dependencies:** + ```sh + composer install + ``` + - Always run this before building or testing PHP code. + - If you receive permission errors, verify `vendor/` is writable. + +3. **Install JS dependencies (for test only):** + ```sh + npm install + ``` + - Only required for running JS tests. Not needed for building or running the app. + +### Build + +- **No explicit build step** for PHP. The web app is served directly from source. +- **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES5+ syntax except in `legacy.js` (ES3). + +### Run + +- **PHP Server Mode:** Use Apache/Nginx with PHP, pointing the web root to the repo root or the `public/` directory (if present). +- **Demo:** Open `index.php` in a browser (via your web server). + +### Test + +- **PHP Unit Tests:** + ```sh + vendor/bin/phpunit + ``` + - Always run after code changes to backend logic. + - If `vendor/bin/phpunit` does not exist, ensure `composer install` completed without errors. + +- **JavaScript Unit Tests:** + ```sh + npm test + ``` + - Runs Jasmine-based tests in Node.js context. + - Note: **Production JS must not use Node-only APIs.** Test code may use Node.js features, but main JS logic must remain browser-compatible. + - If you encounter `ReferenceError` for browser features, ensure only test code uses Node.js APIs. + +### Lint + +- **PHP:** Run (if `phpcs.xml` or similar config exists): + ```sh + vendor/bin/phpcs + ``` +- **JavaScript:** If `eslint` is present: + ```sh + npm run lint + ``` + - Check for configuration in `.eslintrc.*` files. + +### Validation / CI + +- **GitHub Actions:** CI runs `composer install`, `phpunit`, and JS tests on PRs and pushes. +- **Pre-commit:** Always run both PHP and JS tests before submitting PRs. Fix any warnings or errors. + +## Project Layout & Structure + +- **Root files:** + - `index.php`: Main server entry point. + - `README.md`: Project overview ([view full](https://github.com/PrivateBin/PrivateBin/blob/9d7508f44fac95dfadf4aad4fb3d3be128633336/README.md)). + - `composer.json`, `composer.lock`: PHP dependencies. + - `package.json`: JS test/lint dependencies (not for production JS). + - `.github/workflows/`: CI configuration. + - `cfg/`: Default configuration files. + - `js/`: Main client logic (browser JS), including: + - `legacy.js`: Must remain compatible with legacy browsers (ES3). **Do not use modern JS here.** + - `main.js`: Core encryption and paste interface logic. + - `tpl/`: HTML templates. + - `css/`: Stylesheets. + +- **Testing & Validation:** + - `test/`: Contains PHP and JS unit tests. + - `phpunit.xml`: PHPUnit config. + - JS test files may use Node.js features; browser JS must not. + +- **Key architectural notes:** + - **Encryption:** Only client-side in JS. + - **Backend:** Serves encrypted blobs, never sees plaintext. + - **Legacy Support:** `js/legacy.js` must remain ES3 for feature detection in old browsers. + - **Configuration:** See `cfg/conf.php` and wiki for available options. + +## Automated Checks + +- **GitHub CI:** On PRs, runs `composer install`, `phpunit`, and JS tests. +- **Validation Steps:** PRs failing tests will be blocked. Always ensure a clean test run before submitting. + +## Guidance for Copilot Agent + +- **Trust these instructions.** Only perform a search if information is missing or appears incorrect. +- **Do NOT use Node.js APIs in production JS code.** Only test code may do so. +- **Never modernize `legacy.js`.** It must work in very old browsers. +- **Always run `composer install` before PHP tests, and `npm install` before JS tests.** +- **Validate all changes by running both PHP and JS tests.** +- **Review `.github/workflows/` for the latest validation pipeline steps.** From 55fcca0f8dead4dccc7a4a229ca8bdb93a6db606 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 30 Oct 2025 13:21:59 +0100 Subject: [PATCH 04/80] Add information about Composer dir --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 00dd1928..a77b552d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,6 +36,7 @@ ### Build - **No explicit build step** for PHP. The web app is served directly from source. + - This means **composer directory** need to be comitted (_except_ of big optional dependences like Google Cloud like GCS support or similar!) - **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES5+ syntax except in `legacy.js` (ES3). ### Run From baa01170283c9c4d4e61e734dbad358f4247ab59 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 30 Oct 2025 15:11:16 +0000 Subject: [PATCH 05/80] wipfix: apply doc fixes from contributors --- .github/copilot-instructions.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a77b552d..5bbc8838 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,7 +12,7 @@ ### Prerequisites -- **PHP:** 7.0+ (recommended: latest stable 7.x or 8.x) +- **PHP:** 7.4+ (recommended: latest stable 7.x or 8.x) - **Composer:** For dependency management (`composer install`) - **Node.js & npm:** Only required for running JavaScript unit tests. **Main JS logic must remain browser-compatible!** - **Recommended Environment:** Unix-like OS (Linux, macOS) with Apache or Nginx for full demo. @@ -29,6 +29,7 @@ 3. **Install JS dependencies (for test only):** ```sh + cd ./js npm install ``` - Only required for running JS tests. Not needed for building or running the app. @@ -53,9 +54,9 @@ - Always run after code changes to backend logic. - If `vendor/bin/phpunit` does not exist, ensure `composer install` completed without errors. -- **JavaScript Unit Tests:** ```sh - npm test + cd ./js + npm run test ``` - Runs Jasmine-based tests in Node.js context. - Note: **Production JS must not use Node-only APIs.** Test code may use Node.js features, but main JS logic must remain browser-compatible. @@ -81,28 +82,27 @@ ## Project Layout & Structure - **Root files:** - - `index.php`: Main server entry point. - - `README.md`: Project overview ([view full](https://github.com/PrivateBin/PrivateBin/blob/9d7508f44fac95dfadf4aad4fb3d3be128633336/README.md)). + - `README.md`: Project overview ([view full](../README.md)). - `composer.json`, `composer.lock`: PHP dependencies. - `package.json`: JS test/lint dependencies (not for production JS). - `.github/workflows/`: CI configuration. - `cfg/`: Default configuration files. - `js/`: Main client logic (browser JS), including: - `legacy.js`: Must remain compatible with legacy browsers (ES3). **Do not use modern JS here.** - - `main.js`: Core encryption and paste interface logic. + - `privatebin.js`: Core encryption and paste interface logic. - `tpl/`: HTML templates. - `css/`: Stylesheets. - **Testing & Validation:** - - `test/`: Contains PHP and JS unit tests. + - `tst/`: Contains PHP unit tests. + - `js/test`: Contains JS unit tests. - `phpunit.xml`: PHPUnit config. - JS test files may use Node.js features; browser JS must not. -- **Key architectural notes:** - - **Encryption:** Only client-side in JS. + - **Encryption:** Only client-side in JS with WebCrypto. - **Backend:** Serves encrypted blobs, never sees plaintext. - **Legacy Support:** `js/legacy.js` must remain ES3 for feature detection in old browsers. - - **Configuration:** See `cfg/conf.php` and wiki for available options. + - **Configuration:** See `cfg/conf.php` and [wiki](https://github.com/PrivateBin/PrivateBin/wiki/Configuration) for available options. ## Automated Checks From cadfe65bfa231af62ab16ee8418987dc7098585e Mon Sep 17 00:00:00 2001 From: PrivateBin Translator Bot <72346835+privatebin-translator@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:52:40 +0100 Subject: [PATCH 06/80] New translations en.json (Finnish) --- i18n/fi.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/fi.json b/i18n/fi.json index bb7bb44b..304c64ab 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -210,13 +210,13 @@ "Encrypted note on %s": "Salattu viesti %sissä", "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Vieraile tässä linkissä nähdäksesi viestin. URL:n antaminen kenellekään antaa heidänkin päästä katsomaan viestiä.", "URL shortener may expose your decrypt key in URL.": "URL-lyhentäjä voi paljastaa purkuavaimesi URL:ssä.", - "URL shortener is enabled by default.": "URL shortener is enabled by default.", + "URL shortener is enabled by default.": "URL-lyhennys on oletuksena käytössä.", "Save document": "Tallenna asiakirja", "Your IP is not authorized to create documents.": "IP:llesi ei ole annettu oikeutta luoda pasteja.", "Trying to shorten a URL that isn't pointing at our instance.": "Yritetään lyhentää URL-osoite, joka ei osoita meidän instanssiiin.", "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Virhe kutsuttaessa YOURLS. Luultavasti asetusongelma kuten väärä tai puuttuuva \"apiurl\" tai \"signature\".", "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Virhe jäsennettäessä YOURLS-vastausta.", - "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.": "Välityspalvelinvirhe: Virheellinen vastaus. Tämä voi olla asetusongelma, kuten väärä tai puuttuva asetus, tai väliaikainen katkos.", "This secret message can only be displayed once. Would you like to see it now?": "Tämä salainen viesti voidaan näyttää vain kerran. Haluatko nähdä sen nyt?", "Yes, see it": "Kyllä, näet sen", "Dark Mode": "Tumma tila", @@ -229,7 +229,7 @@ "Link copied to clipboard": "Linkki kopioitu leikepöydälle", "Document text": "Liitä teksti", "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)": "Tabulaattori toimii merkkinä (Paina Ctrl+m tai Esc vaihtaaksesi)", - "Show password": "Show password", - "Hide password": "Hide password", + "Show password": "Näytä salasana", + "Hide password": "Piilota salasana", "Theme": "Teema" } From 46599af4f0bd319a86bc0e0058c09e3175cfc090 Mon Sep 17 00:00:00 2001 From: rugk Date: Sun, 2 Nov 2025 17:47:12 +0100 Subject: [PATCH 07/80] Add note about jQuery dropping Co-authored-by: El RIDO --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5bbc8838..4b25cfca 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,7 +38,7 @@ - **No explicit build step** for PHP. The web app is served directly from source. - This means **composer directory** need to be comitted (_except_ of big optional dependences like Google Cloud like GCS support or similar!) -- **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES5+ syntax except in `legacy.js` (ES3). +- **For JavaScript:** There is no webpack/bundler step for release; browser JS is written in compatible ES6+ syntax, except in `legacy.js` (which must be designed to run cleanly even on ancient IE4 or Netscape to display the error message that a browser upgrade is necessary). We are trying to avoid jQuery in any new code and would like to eventually drop use of jQuery. We are considering modularizing the JS logic, but need to ensure to do so in a way that will work both in the browser as well as for node JS driven unit tests. ### Run From 4f64ad3b12520cdb8fd7d298aedd24c025f6afb0 Mon Sep 17 00:00:00 2001 From: rugk Date: Sun, 2 Nov 2025 17:49:26 +0100 Subject: [PATCH 08/80] docs: remove wrong public dir advise Co-authored-by: El RIDO --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4b25cfca..b744e78f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,7 +42,7 @@ ### Run -- **PHP Server Mode:** Use Apache/Nginx with PHP, pointing the web root to the repo root or the `public/` directory (if present). +- **PHP Server Mode:** Use Apache/Nginx with PHP, pointing the web root to the repo root. - **Demo:** Open `index.php` in a browser (via your web server). ### Test From feeac849c481a82322122aec366e9e5c6edab969 Mon Sep 17 00:00:00 2001 From: HariZalanPrivateBin Date: Wed, 5 Nov 2025 06:29:42 +0100 Subject: [PATCH 09/80] Update hu.json My own translations currently used at privbin.harizalan.hu, enhanced at various points --- i18n/hu.json | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/i18n/hu.json b/i18n/hu.json index d284ee18..203d4cf0 100644 --- a/i18n/hu.json +++ b/i18n/hu.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.": "A %s egy minimalista, nyílt forráskódú adattároló szoftver, ahol a szerver semmilyen információt nem tárol a feltett adatról. Azt ugyanis a %sböngésződ%s segítségével titkosítja és oldja fel 256 bit hosszú titkosítási kulcsú AES-t használva.", + "%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.": "A %s minimalista, nyílt forráskódú adattároló szoftver, ahol a szerver semmilyen információt nem tárol a rábízott adatról. Azt ugyanis a %sböngésződ%s segítségével titkosítja és oldja fel 256 bit hosszú titkosítási kulcsú AES-t használva.", "More information on the project page.": "További információt a projekt oldalán találsz.", "Because ignorance is bliss": "A titok egyfajta hatalom.", "Document does not exist, has expired or has been deleted.": "A bejegyzés nem létezik, lejárt vagy törölve lett.", - "%s requires php %s or above to work. Sorry.": "Bocs, de a %s működéséhez %s vagy ezt meghaladó verziójú php-s környezet szükséges.", + "%s requires php %s or above to work. Sorry.": "Sajnáljuk, de a %s működéséhez %s vagy ezt meghaladó verziójú php-s környezet szükséges.", "%s requires configuration section [%s] to be present in configuration file.": "A %s megfelelő működéséhez a konfigurációs fájlban a [%s] résznek léteznie kell.", "Please wait %d seconds between each post.": [ "Kérlek várj %d másodpercet két beküldés között.", @@ -92,7 +92,7 @@ "%d év" ], "Never": "Soha", - "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Megjegyzés: ez egy teszt szolgáltatás, az adatok bármikor törlődhetnek. Ha visszaélsz vele, kiscicák bánhatják! :)", + "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Megjegyzés: ezen szolgáltatás egy teszt, az adatok bármikor törlődhetnek. Ha visszaélsz vele, kiscicák bánhatják.", "This document will expire in %d seconds.": [ "Ez a bejegyzés %d másodperc múlva megsemmisül.", "Ez a bejegyzés %d másodperc múlva megsemmisül.", @@ -142,7 +142,7 @@ "Anonymous": "Névtelen", "Avatar generated from IP address": "Avatar (az IP cím alapján generáljuk)", "Add comment": "Hozzászólok", - "Optional nickname…": "Becenév (már ha meg akarod adni)", + "Optional nickname…": "Becenév (amennyiben meg akarod adni)", "Post comment": "Beküld", "Sending comment…": "Beküldés alatt...", "Comment posted.": "A hozzászólás beküldve.", @@ -154,7 +154,7 @@ "Your document is %s (Hit Ctrl+c to copy)": "A bejegyzésed a %s címen elérhető. Ctrl+c-vel tudod vágólapra másolni.", "Delete data": "Adat törlése", "Could not create document: %s": "Nem tudtuk létrehozni a bejegyzést: %s", - "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Nem tudjuk visszafejteni a bejegyzést: a dekódoláshoz szükséges kulcs hiányzik a címből. Talán URL rövidítőt használtál ami kivágta azt belőle?", + "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Nem tudjuk visszafejteni a bejegyzést: a dekódoláshoz szükséges kulcs hiányzik a címből. Talán URL rövidítőt használtál, amely azt kivágta belőle?", "B": "B", "kB": "kB", "MB": "MB", @@ -190,13 +190,13 @@ "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Abban az esetben, ha ez az üzenet mindig látható lenne, látogass el a Gyakran Ismételt Kérdések szekcióba a megoldásához.", "+++ no document text +++": "+++ nincs beillesztett szöveg +++", "Could not get document data: %s": "Az adat megszerzése nem sikerült: %s", - "QR code": "QR kód", - "This website is using an insecure HTTP connection! Please use it only for testing.": "Ez a weboldal nem biztonságos HTTP kapcsolatot használ! Emiatt csak teszt célokra ajánljuk.", + "QR code": "QR-kód", + "This website is using an insecure HTTP connection! Please use it only for testing.": "Ez a weboldal nem biztonságos (HTTP) kapcsolatot használ! Emiatt csak teszt céljára ajánljuk.", "For more information see this FAQ entry.": "További információ ebben a GyIK bejegyzésben található (angolul).", "Your browser may require an HTTPS connection to support the WebCrypto API. Try switching to HTTPS.": "A WebCrypto API használatához a böngésződ számára esetleg HTTPS kapcsolat szükséges. Ezért próbálj meg HTTPS-re váltani.", - "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "A böngésződ nem támogatja a WebAssemblyt, ami a zlib tömörítéshez kell. Létre tudsz hozni tömörítetlen dokumentumokat, de tömörítetteket nem tudsz olvasni.", - "waiting on user to provide a password": "Várakozás a felhasználóra jelszó megadása okán", - "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.": "Nem lehetett visszafejteni az adatot. Rossz jelszót ütöttél be? Ismételd meg a fent található gombbal.", + "Your browser doesn't support WebAssembly, used for zlib compression. You can create uncompressed documents, but can't read compressed ones.": "A böngésződ nem támogatja a WebAssemblyt, amely zlib-tömörítéshez szükséges. Létrehozhatsz tömörítetlen bejegyzést, de tömörítetteket nem tudsz elolvasni.", + "waiting on user to provide a password": "Várakozás jelszó megadására", + "Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.": "Nem sikerült visszafejteni az adatot. Rossz jelszót ütöttél be? Ismételd meg a fentebbi gombbal.", "Retry": "Újrapróbálkozás", "Showing raw text…": "Nyers szöveg mutatása…", "Notice:": "Megjegyzés:", @@ -209,27 +209,27 @@ "Close": "Bezárás", "Encrypted note on %s": "Titkosított jegyzet a %s", "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Látogasd meg ezt a hivatkozást a bejegyzés megtekintéséhez. Ha mások számára is megadod ezt a linket, azzal hozzáférnek ők is.", - "URL shortener may expose your decrypt key in URL.": "URL shortener may expose your decrypt key in URL.", - "URL shortener is enabled by default.": "URL shortener is enabled by default.", - "Save document": "Save document", + "URL shortener may expose your decrypt key in URL.": "Az URL-rövidítő kiszolgáltathatja dekódolókulcsod.", + "URL shortener is enabled by default.": "Az URL-rövidítő alapértelmezetten engedélyezett.", + "Save document": "Bejegyzés mentése", "Your IP is not authorized to create documents.": "Your IP is not authorized to create documents.", "Trying to shorten a URL that isn't pointing at our instance.": "Trying to shorten a URL that isn't pointing at our instance.", "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.", "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.", "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.", - "This secret message can only be displayed once. Would you like to see it now?": "This secret message can only be displayed once. Would you like to see it now?", - "Yes, see it": "Yes, see it", + "This secret message can only be displayed once. Would you like to see it now?": "Ez az üzenet csak egyszer jeleníthető meg. Szeretnéd megnézni?", + "Yes, see it": "Igen", "Dark Mode": "Sötét mód", - "Error compressing document, due to missing WebAssembly support.": "Error compressing document, due to missing WebAssembly support.", + "Error compressing document, due to missing WebAssembly support.": "WebAssembly-támogatás hiánya miatt nem tömöríthetjük a dokumentumot.", "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.", - "Start over": "Start over", - "Document copied to clipboard": "Document copied to clipboard", - "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c": "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c", - "Copy link": "Copy link", - "Link copied to clipboard": "Link copied to clipboard", - "Document text": "Document text", - "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)": "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)", - "Show password": "Show password", - "Hide password": "Hide password", - "Theme": "Theme" + "Start over": "Újrakezdés", + "Document copied to clipboard": "Bejegyzés másolva", + "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c": "Másoláshoz használd a Ctrl+c/Cmd+c billentyűkombinációt", + "Copy link": "Link másolása", + "Link copied to clipboard": "Link másolva", + "Document text": "Bejegyzés szövege", + "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)": "A tabulátor karakternek használható (nyomd le a Ctrl+m vagy az Esc to billentyűket ennek megszüntetéséhez).", + "Show password": "Jelszó megjelenítése", + "Hide password": "Jelszó elrejtése", + "Theme": "Téma" } From a7b253a43a47f5e25eb805857117caf234ad19d7 Mon Sep 17 00:00:00 2001 From: Ribas160 Date: Wed, 5 Nov 2025 17:33:08 +0200 Subject: [PATCH 10/80] fix: error fetching attachments from blob --- CHANGELOG.md | 1 + js/privatebin.js | 7 ++++--- lib/Configuration.php | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c368725..f39aec24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # PrivateBin version history ## 2.0.3 (not yet released) +* FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585) ## 2.0.2 (2025-10-28) * CHANGED: Upgrading libraries to: DOMpurify 3.3.0 diff --git a/js/privatebin.js b/js/privatebin.js index 9bbc39bc..b675fddd 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -5214,22 +5214,23 @@ jQuery.PrivateBin = (function($) { cipherMessage['attachment'] = attachments.map(attachment => attachment[0]); cipherMessage['attachment_name'] = attachments.map(attachment => attachment[1]); - cipherMessage['attachment'] = await Promise.all(cipherMessage['attachment'].map(async (attachment) => { + cipherMessage['attachment'] = await Promise.all(cipherMessage['attachment'].map(async (attachment, i) => { // we need to retrieve data from blob if browser already parsed it in memory if (typeof attachment === 'string' && attachment.startsWith('blob:')) { Alert.showStatus( [ 'Retrieving cloned file \'%s\' from memory...', - attachment[1] + cipherMessage['attachment_name'][i] ], 'copy' ); try { const blobData = await $.ajax({ type: 'GET', - url: `${attachment}`, + url: attachment, processData: false, timeout: 10000, + dataType: 'binary', xhrFields: { withCredentials: false, responseType: 'blob' diff --git a/lib/Configuration.php b/lib/Configuration.php index 6ad8c546..b647877b 100644 --- a/lib/Configuration.php +++ b/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-C9Mc6qgEHhaMKC+VzN7Hp8C77HVm8cD5N/AMlP6qkaYj/QLZ0HdtYfOMWrXNn9i83MbqkRD//DnM7bHHEixzIg==', + 'js/privatebin.js' => 'sha512-D7wsi3HMNnYlVtwljJLWI5VSxAydg6E+6OfggGcZ8xV6kSpVzy7RHzefKBXcVIfGgwBU5bjDjtIrUQ3eNJQpGQ==', '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==', From 697753ab9167bd73a6d5aab7cf770a9a8c7cd09c Mon Sep 17 00:00:00 2001 From: PrivateBin Translator Bot <72346835+privatebin-translator@users.noreply.github.com> Date: Sat, 8 Nov 2025 06:00:28 +0100 Subject: [PATCH 11/80] New Crowdin updates (#1706) * New translations en.json (Lithuanian) --- i18n/lt.json | 72 ++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/i18n/lt.json b/i18n/lt.json index e611d979..d2da30ab 100644 --- a/i18n/lt.json +++ b/i18n/lt.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 yra minimalistinis, atvirojo kodo internetinis įdėjimų dėklas, kurį naudojant, serveris nieko nenutuokia apie įdėtus duomenis. Duomenys yra šifruojami/iššifruojami %snaršyklėje%s naudojant 256 bitų AES.", "More information on the project page.": "Daugiau informacijos rasite projekto puslapyje.", "Because ignorance is bliss": "Nes nežinojimas yra palaima", - "Document does not exist, has expired or has been deleted.": "Įdėjimo nėra, jis nebegalioja arba buvo ištrintas.", + "Document does not exist, has expired or has been deleted.": "Dokumento nėra, jis nebegalioja arba buvo ištrintas.", "%s requires php %s or above to work. Sorry.": "%s savo darbui reikalauja php %s arba naujesnės versijos. Apgailestaujame.", "%s requires configuration section [%s] to be present in configuration file.": "%s reikalauja, kad konfigūracijos faile būtų [%s] konfigūracijos sekcija.", "Please wait %d seconds between each post.": [ @@ -14,15 +14,15 @@ "Tarp kiekvieno įrašo palaukite %d sekundžių.", "Tarp kiekvieno įrašo palaukite %d sekundžių." ], - "Document is limited to %s of encrypted data.": "Įdėjimas yra apribotas iki %s šifruotų duomenų.", + "Document is limited to %s of encrypted data.": "Dokumentas yra apribotas iki %s šifruotų duomenų.", "Invalid data.": "Neteisingi duomenys.", "You are unlucky. Try again.": "Jums nesiseka. Bandykite dar kartą.", "Error saving comment. Sorry.": "Klaida įrašant komentarą. Apgailestaujame.", - "Error saving document. Sorry.": "Klaida įrašant įdėjimą. Apgailestaujame.", - "Invalid document ID.": "Neteisingas įdėjimo ID.", - "Document is not of burn-after-reading type.": "Įdėjimo tipas nėra „Perskaičius sudeginti“.", - "Wrong deletion token. Document was not deleted.": "Neteisingas ištrynimo prieigos raktas. Įdėjimas nebuvo ištrintas.", - "Document was properly deleted.": "Įdėjimas buvo tinkamai ištrintas.", + "Error saving document. Sorry.": "Klaida įrašant dokumentą. Apgailestaujame.", + "Invalid document ID.": "Neteisingas dokumento ID.", + "Document is not of burn-after-reading type.": "Dokumento tipas nėra „Perskaičius sudeginti“.", + "Wrong deletion token. Document was not deleted.": "Neteisingas ištrynimo prieigos raktas. Dokumentas nebuvo ištrintas.", + "Document was properly deleted.": "Dokumentas buvo tinkamai ištrintas.", "JavaScript is required for %s to work. Sorry for the inconvenience.": "%s darbui reikalinga JavaScript. Atsiprašome už nepatogumus.", "%s requires a modern browser to work.": "%s savo darbui reikalauja šiuolaikinės naršyklės.", "New": "Naujas", @@ -133,9 +133,9 @@ "Šis dokumentas nustos galioti po %d mėnesių.", "Šis dokumentas nustos galioti po %d mėnesių." ], - "Please enter the password for this document:": "Įveskite šio įdėjimo slaptažodį:", + "Please enter the password for this document:": "Įveskite šio dokumento slaptažodį:", "Could not decrypt data (Wrong key?)": "Nepavyko iššifruoti duomenų (Neteisingas raktas?)", - "Could not delete the document, it was not stored in burn after reading mode.": "Nepavyko ištrinti įdėjimo, jis nebuvo saugomas „Perskaičius sudeginti“ veiksenoje.", + "Could not delete the document, it was not stored in burn after reading mode.": "Nepavyko ištrinti dokumento, jis nebuvo saugomas „Perskaičius sudeginti“ veiksenoje.", "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": "SKIRTA TIK JŪSŲ AKIMS. Neužverkite šio lango, šis pranešimas negalės būti rodomas dar kartą.", "Could not decrypt comment; Wrong key?": "Nepavyko iššifruoti komentaro; Neteisingas raktas?", "Reply": "Atsakyti", @@ -150,11 +150,11 @@ "unknown status": "nežinoma būsena", "server error or not responding": "serverio klaida arba jis neatsako", "Could not post comment: %s": "Nepavyko paskelbti komentaro: %s", - "Sending document…": "Siunčiamas įdėjimas…", - "Your document is %s (Hit Ctrl+c to copy)": "Jūsų įdėjimas yra %s (Paspauskite Vald+c norėdami nukopijuoti)", + "Sending document…": "Siunčiamas dokumentas…", + "Your document is %s (Hit Ctrl+c to copy)": "Jūsų dokumentas yra %s (Paspauskite Ctrl+c norėdami nukopijuoti)", "Delete data": "Ištrinti duomenis", - "Could not create document: %s": "Nepavyko sukurti įdėjimo: %s", - "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Nepavyksta iššifruoti įdėjimo: URL adrese trūksta iššifravimo rakto (Ar naudojote peradresavimo ar URL trumpinimo įrankį, kuris pašalina URL dalį?)", + "Could not create document: %s": "Nepavyko sukurti dokumento: %s", + "Cannot decrypt document: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Nepavyksta iššifruoti dokumento: URL adrese trūksta iššifravimo rakto (Ar naudojote peradresavimo ar URL trumpinimo įrankį, kuris pašalina URL dalį?)", "B": "B", "kB": "kB", "MB": "MB", @@ -170,7 +170,7 @@ "Markdown": "„Markdown“", "Download attachment": "Atsisiųsti priedą", "Cloned: '%s'": "Dubliuota: „%s“", - "The cloned file '%s' was attached to this document.": "Dubliuotas failas „%s“ buvo pridėtas į šį įdėjimą.", + "The cloned file '%s' was attached to this document.": "Dubliuotas failas „%s“ buvo pridėtas į šį dokumentą.", "Attach a file": "Pridėti failą", "alternatively drag & drop a file or paste an image from the clipboard": "arba kitaip - tempkite failą arba įdėkite paveikslą iš iškarpinės", "File too large, to display a preview. Please download the attachment.": "Failas per didelis, kad būtų rodoma peržiūra. Atsisiųskite priedą.", @@ -185,11 +185,11 @@ "Decrypt": "Iššifruoti", "Enter password": "Įveskite slaptažodį", "Loading…": "Įkeliama…", - "Decrypting document…": "Iššifruojamas įdėjimas…", - "Preparing new document…": "Ruošiamas naujas įdėjimas…", + "Decrypting document…": "Iššifruojamas dokumentas…", + "Preparing new document…": "Ruošiamas naujas dokumentas…", "In case this message never disappears please have a look at this FAQ for information to troubleshoot.": "Jeigu šis pranešimas niekada nedingsta, pasižiūrėkite šį DUK skyrių, kuriame yra informacija apie nesklandumų šalinimą.", - "+++ no document text +++": "+++ nėra įdėjimo teksto +++", - "Could not get document data: %s": "Nepavyko gauti įdėjimo duomenų: %s", + "+++ no document text +++": "+++ nėra dokumento teksto +++", + "Could not get document data: %s": "Nepavyko gauti dokumento duomenų: %s", "QR code": "QR kodas", "This website is using an insecure HTTP connection! Please use it only for testing.": "Ši internetinė svetainė naudoja nesaugų HTTP ryšį! Naudokite ją tik bandymams.", "For more information see this FAQ entry.": "Išsamesnei informacijai, žiūrėkite šį DUK įrašą.", @@ -210,26 +210,26 @@ "Encrypted note on %s": "Šifruoti užrašai ties %s", "Visit this link to see the note. Giving the URL to anyone allows them to access the note, too.": "Norėdami matyti užrašus, aplankykite šį tinklalapį. Pasidalinus šiuo URL adresu su kitais žmonėmis, jiems taip pat bus leidžiama prieiga prie šių užrašų.", "URL shortener may expose your decrypt key in URL.": "URL trumpinimo įrankis gali atskleisti URL adrese jūsų iššifravimo raktą.", - "URL shortener is enabled by default.": "URL shortener is enabled by default.", - "Save document": "Įrašyti įdėjimą", - "Your IP is not authorized to create documents.": "Jūsų IP adresas neturi įgaliojimų kurti įdėjimų.", + "URL shortener is enabled by default.": "URL trumpinimo įrankis pagal numatymą įjungtas.", + "Save document": "Įrašyti dokumentą", + "Your IP is not authorized to create documents.": "Jūsų IP adresas neturi įgaliojimų kurti dokumentus.", "Trying to shorten a URL that isn't pointing at our instance.": "Bandoma sutrumpinti URL adresą, kuris nenurodo į mūsų egzempliorių.", - "Proxy error: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Klaida iškviečiant YOURLS. Tikriausiai, konfigūracijos klaida, pavyzdžiui, neteisingi „apiurl“ ar „signature“, arba jų nėra.", - "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Klaida nagrinėjant YOURLS atsaką.", - "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: Proxy URL is empty. This can be a configuration issue, like wrong or missing config keys.": "Įgaliotojo serverio klaida: Įgaliotojo serverio URL yra tuščias. Tai gali būti konfigūracijos sukelta problema, pavyzdžiui, neteisingi arba trūkstami konfigūracijos raktai.", + "Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.": "Įgaliotojo serverio klaida: Klaida nagrinėjant įgaliotojo serverio atsaką. Tai gali būti konfigūracijos sukelta problema, pavyzdžiui, neteisingi arba trūkstami konfigūracijos raktai.", + "Proxy error: Bad response. This can be a configuration issue, like wrong or missing config keys or a temporary outage.": "Įgaliotojo serverio klaida: Blogas atsakas. Tai gali būti konfigūracijos sukelta problema, pavyzdžiui, neteisingi arba trūkstami konfigūracijos raktai, arba laikinas avarinis atjungimas.", "This secret message can only be displayed once. Would you like to see it now?": "Ši slapta žinutė gali būti parodyta tik vieną kartą. Ar norėtumėte ją dabar pamatyti?", "Yes, see it": "Taip, pamatyti", "Dark Mode": "Tamsi veiksena", - "Error compressing document, due to missing WebAssembly support.": "Klaida glaudinant įdėjimą, nes trūksta WebAssembly palaikymo.", - "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Klaida išglaudinant įdėjimą, jūsų naršyklė nepalaiko WebAssembly. Norėdami peržiūrėti šį įdėjimą, naudokite kitą naršyklę.", - "Start over": "Start over", - "Document copied to clipboard": "Document copied to clipboard", - "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c": "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c", - "Copy link": "Copy link", - "Link copied to clipboard": "Link copied to clipboard", - "Document text": "Document text", - "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)": "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)", - "Show password": "Show password", - "Hide password": "Hide password", - "Theme": "Theme" + "Error compressing document, due to missing WebAssembly support.": "Klaida glaudinant dokumentą, nes trūksta „WebAssembly“ palaikymo.", + "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Klaida išglaudinant dokumentą, jūsų naršyklė nepalaiko „WebAssembly“. Norėdami peržiūrėti šį dokumentą, naudokite kitą naršyklę.", + "Start over": "Pradėti iš naujo", + "Document copied to clipboard": "Dokumentas nukopijuotas į iškarpinę", + "To copy document press on the copy button or use the clipboard shortcut Ctrl+c/Cmd+c": "Norėdami nukopijuoti dokumentą paspauskite kopijavimo mygtuką arba naudokite iškarpinės sparčiuosius klavišus Ctrl+c/Cmd+c", + "Copy link": "Kopijuoti nuorodą", + "Link copied to clipboard": "Nuoroda nukopijuota į iškarpinę", + "Document text": "Dokumento tekstas", + "Tabulator key serves as character (Hit Ctrl+m or Esc to toggle)": "Tabuliatoriaus klavišas tarnauja kaip simbolis (Paspauskite Ctrl+m arba Esc norėdami perjungti)", + "Show password": "Rodyti slaptažodį", + "Hide password": "Slėpti slaptažodį", + "Theme": "Apipavidalinimas" } From ec178e0c38fe2d9a8f3416e153730764d7e618ab Mon Sep 17 00:00:00 2001 From: Calvin Bui <3604363+calvinbui@users.noreply.github.com> Date: Sun, 9 Nov 2025 10:00:48 +1100 Subject: [PATCH 12/80] Fix typo in Shlink config docs --- cfg/conf.sample.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php index 8fb5288b..b2456a4e 100644 --- a/cfg/conf.sample.php +++ b/cfg/conf.sample.php @@ -280,7 +280,7 @@ dir = PATH "data" ; "urlshortener" needs to point to the base URL of your PrivateBin ; instance with "?shortenviashlink&link=" appended. For example: ; urlshortener = "${basepath}?shortenviashlink&link=" -; This URL will in turn call YOURLS on the server side, using the URL from +; This URL will in turn call Shlink on the server side, using the URL from ; "apiurl" and the API Key from the "apikey" parameters below. ; apiurl = "https://shlink.example.com/rest/v3/short-urls" ; apikey = "your_api_key" From 5f6c2beb3b5c9fd4416422f7d336438b8fb7c47f Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 10 Nov 2025 12:00:29 +0100 Subject: [PATCH 13/80] Unit test on escaping the template directory --- tst/TemplateSwitcherTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tst/TemplateSwitcherTest.php b/tst/TemplateSwitcherTest.php index 16aec6b0..7f4ef8d8 100644 --- a/tst/TemplateSwitcherTest.php +++ b/tst/TemplateSwitcherTest.php @@ -41,6 +41,7 @@ class TemplateSwitcherTest extends TestCase $defaultTemplateFallback = 'bootstrap5'; $customTemplate = 'bootstrap-dark'; $customWrongTemplate = 'bootstrap-wrong'; + $escapeTemplateDirectory = '../index'; TemplateSwitcher::setTemplateFallback($defaultTemplateFallback); @@ -49,6 +50,9 @@ class TemplateSwitcherTest extends TestCase $_COOKIE['template'] = $customTemplate; $this->assertEquals($customTemplate, TemplateSwitcher::getTemplate(), 'Custom template'); + + $_COOKIE['template'] = $escapeTemplateDirectory; + $this->assertEquals($defaultTemplateFallback, TemplateSwitcher::getTemplate(), 'Fallback on escaping template directory'); } public function testGetAvailableTemplates() From 13949349af48cf6044e66b9aaf3ae1adfe87bf5b Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 10 Nov 2025 12:22:29 +0100 Subject: [PATCH 14/80] improve readability of logic --- lib/TemplateSwitcher.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/TemplateSwitcher.php b/lib/TemplateSwitcher.php index 22c65b08..ad509759 100644 --- a/lib/TemplateSwitcher.php +++ b/lib/TemplateSwitcher.php @@ -101,14 +101,13 @@ class TemplateSwitcher */ public static function isTemplateAvailable(string $template): bool { - $available = in_array($template, self::getAvailableTemplates()); - - if (!$available && !View::isBootstrapTemplate($template)) { - $path = View::getTemplateFilePath($template); - $available = file_exists($path); + if (in_array($template, self::getAvailableTemplates())) { + return true; } - - return $available; + if (View::isBootstrapTemplate($template)) { + return false; + } + return file_exists(View::getTemplateFilePath($template)); } /** @@ -120,13 +119,10 @@ class TemplateSwitcher */ private static function getSelectedByUserTemplate(): ?string { - $selectedTemplate = null; $templateCookieValue = $_COOKIE['template'] ?? ''; - if (self::isTemplateAvailable($templateCookieValue)) { - $selectedTemplate = $templateCookieValue; + return $templateCookieValue; } - - return $selectedTemplate; + return null; } } From 17ff44037aba4669f310395afb9375e220ca3f01 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 10 Nov 2025 12:23:50 +0100 Subject: [PATCH 15/80] prevent use of paths in template names, only file names inside tpl directory are allowed --- lib/View.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/View.php b/lib/View.php index dd15e321..65830323 100644 --- a/lib/View.php +++ b/lib/View.php @@ -66,7 +66,7 @@ class View */ public static function getTemplateFilePath(string $template): string { - $file = self::isBootstrapTemplate($template) ? 'bootstrap' : $template; + $file = self::isBootstrapTemplate($template) ? 'bootstrap' : basename($template); return PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php'; } From a479d7540533d1e8bac78447548b9c0c65753023 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 10 Nov 2025 12:25:19 +0100 Subject: [PATCH 16/80] belt and braces: reset the template cookie, if function is not enabled --- lib/Controller.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Controller.php b/lib/Controller.php index 3d316687..188524d5 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -434,11 +434,15 @@ class Controller setcookie('lang', $languageselection, array('SameSite' => 'Lax', 'Secure' => true)); } - // set template cookie if that functionality was enabled + // set template cookie if that functionality was enabled, otherwise delete any existing cookie $templateselection = ''; if ($this->_conf->getKey('templateselection')) { $templateselection = TemplateSwitcher::getTemplate(); setcookie('template', $templateselection, array('SameSite' => 'Lax', 'Secure' => true)); + } elseif (array_key_exists('template', $_COOKIE)) { + unset($_COOKIE['template']); // ensure value is not re-used in template switcher + $expiredInAllTimezones = time() - 86400; + setcookie('template', '', array('expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true)); } // strip policies that are unsupported in meta tag From 14b68af5284b3888bf274d6433cb0b9f6ee37d2e Mon Sep 17 00:00:00 2001 From: Ribas160 Date: Mon, 10 Nov 2025 17:59:18 +0200 Subject: [PATCH 17/80] Insert drag and drop file names as a text, not html --- js/privatebin.js | 7 +++++-- lib/Configuration.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 9bbc39bc..ffb02d1b 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -3086,10 +3086,13 @@ jQuery.PrivateBin = (function($) { * @name AttachmentViewer.printDragAndDropFileNames * @private * @function - * @param {array} fileNames + * @param {string[]} fileNames */ function printDragAndDropFileNames(fileNames) { - $dragAndDropFileNames.html(fileNames.join('
')); + $dragAndDropFileNames.empty(); + fileNames.forEach(fileName => { + $('
').text(fileName).appendTo($dragAndDropFileNames); + }); } /** diff --git a/lib/Configuration.php b/lib/Configuration.php index 6ad8c546..b1655ba4 100644 --- a/lib/Configuration.php +++ b/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-C9Mc6qgEHhaMKC+VzN7Hp8C77HVm8cD5N/AMlP6qkaYj/QLZ0HdtYfOMWrXNn9i83MbqkRD//DnM7bHHEixzIg==', + 'js/privatebin.js' => 'sha512-L2R5jtnyDjqMnUPKNjjoal2LO5fl/OcLtChaj6pQKkmbK97HUt9EcbuSpbnnyjPDhZCtG0CF4wkCQ4xAk8x2Ag==', '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==', From dae5f7fd616ffeee3c88ed7d1441337dda5ff6a8 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 10 Nov 2025 17:27:11 +0100 Subject: [PATCH 18/80] partially revert #1559 Instead of automatically adding custom templates, we log an error if that template is missing in the available templates. Still mitigates arbitrary file inclusion, as the string is now checked against a fixed allow list. --- lib/TemplateSwitcher.php | 20 +++++++------------- lib/View.php | 28 ++-------------------------- tst/Bootstrap.php | 3 +++ tst/ViewTest.php | 16 ---------------- 4 files changed, 12 insertions(+), 55 deletions(-) diff --git a/lib/TemplateSwitcher.php b/lib/TemplateSwitcher.php index ad509759..142a9235 100644 --- a/lib/TemplateSwitcher.php +++ b/lib/TemplateSwitcher.php @@ -59,11 +59,8 @@ class TemplateSwitcher { if (self::isTemplateAvailable($template)) { self::$_templateFallback = $template; - - if (!in_array($template, self::getAvailableTemplates())) { - // Add custom template to the available templates list - self::$_availableTemplates[] = $template; - } + } else { + error_log('failed to set "' . $template . '" as a fallback, it needs to be added to the list of `availabletemplates` in the configuration file'); } } @@ -101,13 +98,11 @@ class TemplateSwitcher */ public static function isTemplateAvailable(string $template): bool { - if (in_array($template, self::getAvailableTemplates())) { + if (in_array($template, self::getAvailableTemplates(), true)) { return true; } - if (View::isBootstrapTemplate($template)) { - return false; - } - return file_exists(View::getTemplateFilePath($template)); + error_log('template "' . $template . '" is not in the list of `availabletemplates` in the configuration file'); + return false; } /** @@ -119,9 +114,8 @@ class TemplateSwitcher */ private static function getSelectedByUserTemplate(): ?string { - $templateCookieValue = $_COOKIE['template'] ?? ''; - if (self::isTemplateAvailable($templateCookieValue)) { - return $templateCookieValue; + if (array_key_exists('template', $_COOKIE) && self::isTemplateAvailable($_COOKIE['template'])) { + return $_COOKIE['template']; } return null; } diff --git a/lib/View.php b/lib/View.php index 65830323..666a03f7 100644 --- a/lib/View.php +++ b/lib/View.php @@ -49,7 +49,8 @@ class View */ public function draw($template) { - $path = self::getTemplateFilePath($template); + $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template; + $path = PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php'; if (!file_exists($path)) { throw new Exception('Template ' . $template . ' not found!', 80); } @@ -57,31 +58,6 @@ class View include $path; } - /** - * Get template file path - * - * @access public - * @param string $template - * @return string - */ - public static function getTemplateFilePath(string $template): string - { - $file = self::isBootstrapTemplate($template) ? 'bootstrap' : basename($template); - return PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php'; - } - - /** - * Is the template a variation of the bootstrap template - * - * @access public - * @param string $template - * @return bool - */ - public static function isBootstrapTemplate(string $template): bool - { - return substr($template, 0, 10) === 'bootstrap-'; - } - /** * echo script tag incl. SRI hash for given script file * diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php index 5c5ceb6f..e905312c 100644 --- a/tst/Bootstrap.php +++ b/tst/Bootstrap.php @@ -6,7 +6,9 @@ use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\Connection\ConnectionInterface; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Storage\StorageObject; +use PrivateBin\Configuration; use PrivateBin\Persistence\ServerSalt; +use PrivateBin\TemplateSwitcher; error_reporting(E_ALL | E_STRICT); @@ -26,6 +28,7 @@ if (!defined('CONF_SAMPLE')) { require PATH . 'vendor/autoload.php'; Helper::updateSubresourceIntegrity(); +TemplateSwitcher::setAvailableTemplates(Configuration::getDefaults()['main']['availabletemplates']); /** * Class Helper provides unit tests pastes and comments of various formats diff --git a/tst/ViewTest.php b/tst/ViewTest.php index fcb0bdac..fb261f6c 100644 --- a/tst/ViewTest.php +++ b/tst/ViewTest.php @@ -141,20 +141,4 @@ class ViewTest extends TestCase $this->expectExceptionCode(80); $test->draw('123456789 does not exist!'); } - - public function testTemplateFilePath() - { - $template = 'bootstrap'; - $templatePath = PATH . 'tpl' . DIRECTORY_SEPARATOR . $template . '.php'; - $path = View::getTemplateFilePath($template); - $this->assertEquals($templatePath, $path, 'Template file path'); - } - - public function testIsBootstrapTemplate() - { - $bootstrapTemplate = 'bootstrap-dark'; - $nonBootstrapTemplate = 'bootstrap5'; - $this->assertTrue(View::isBootstrapTemplate($bootstrapTemplate), 'Is bootstrap template'); - $this->assertFalse(View::isBootstrapTemplate($nonBootstrapTemplate), 'Is not bootstrap template'); - } } From f2164353c31cf9491d917bbf2a2ea5712fcb6cc9 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 09:34:54 +0100 Subject: [PATCH 19/80] use realpath and validate tpl directory contents to ensure only php files inside the tpl dir can get used as templates --- lib/View.php | 17 +++++++++++++---- tst/ViewTest.php | 9 +++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/View.php b/lib/View.php index 666a03f7..9a83e6e4 100644 --- a/lib/View.php +++ b/lib/View.php @@ -12,6 +12,7 @@ namespace PrivateBin; use Exception; +use GlobIterator; /** * View @@ -49,13 +50,21 @@ class View */ public function draw($template) { + $dir = PATH . 'tpl' . DIRECTORY_SEPARATOR; $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template; - $path = PATH . 'tpl' . DIRECTORY_SEPARATOR . $file . '.php'; - if (!file_exists($path)) { + $path = realpath($dir . $file . '.php'); + if ($path === false) { throw new Exception('Template ' . $template . ' not found!', 80); } - extract($this->_variables); - include $path; + foreach (new GlobIterator($dir . '*.php') as $tplFile) { + if ($tplFile->getRealPath() === $path) { + $templatesInPath = new GlobIterator(PATH . 'tpl' . DIRECTORY_SEPARATOR . '*.php'); + extract($this->_variables); + include $path; + return; + } + } + throw new Exception('Template ' . $file . '.php not found in ' . $dir . '!', 81); } /** diff --git a/tst/ViewTest.php b/tst/ViewTest.php index fb261f6c..b3d4aa37 100644 --- a/tst/ViewTest.php +++ b/tst/ViewTest.php @@ -141,4 +141,13 @@ class ViewTest extends TestCase $this->expectExceptionCode(80); $test->draw('123456789 does not exist!'); } + + public function testInvalidTemplate() + { + $test = new View; + $this->expectException(Exception::class); + $this->expectExceptionCode(81); + $test->draw('../index'); + } + } From be6a3702fca101456788521d71c7b7d10394c449 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 09:43:41 +0100 Subject: [PATCH 20/80] simplify logic and improve readability function was only used in one place and only indirectly tested, so it could be inlined, which also makes the test for null and the extra variable allocation unnecessary --- lib/TemplateSwitcher.php | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/TemplateSwitcher.php b/lib/TemplateSwitcher.php index 142a9235..abfdffc0 100644 --- a/lib/TemplateSwitcher.php +++ b/lib/TemplateSwitcher.php @@ -65,7 +65,7 @@ class TemplateSwitcher } /** - * get currently loaded template + * get user selected template or fallback * * @access public * @static @@ -73,8 +73,10 @@ class TemplateSwitcher */ public static function getTemplate(): string { - $selectedTemplate = self::getSelectedByUserTemplate(); - return $selectedTemplate ?? self::$_templateFallback; + if (array_key_exists('template', $_COOKIE) && self::isTemplateAvailable($_COOKIE['template'])) { + return $_COOKIE['template']; + } + return self::$_templateFallback; } /** @@ -104,19 +106,4 @@ class TemplateSwitcher error_log('template "' . $template . '" is not in the list of `availabletemplates` in the configuration file'); return false; } - - /** - * get the template selected by user - * - * @access private - * @static - * @return string|null - */ - private static function getSelectedByUserTemplate(): ?string - { - if (array_key_exists('template', $_COOKIE) && self::isTemplateAvailable($_COOKIE['template'])) { - return $_COOKIE['template']; - } - return null; - } } From ea73300e155780aaf7a2ce3b710f1aa5b763c8bf Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 09:45:51 +0100 Subject: [PATCH 21/80] don't always set the cookie, having to unset it later but still unset it, if it currently should not be in use (templateselection = false) --- lib/Controller.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/Controller.php b/lib/Controller.php index 188524d5..08192d4d 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -219,10 +219,11 @@ class Controller TemplateSwitcher::setAvailableTemplates($templates); TemplateSwitcher::setTemplateFallback($template); - // force default template, if template selection is disabled and a default is set - if (!$this->_conf->getKey('templateselection') && !empty($template)) { - $_COOKIE['template'] = $template; - setcookie('template', $template, array('SameSite' => 'Lax', 'Secure' => true)); + // force default template, if template selection is disabled + if (!$this->_conf->getKey('templateselection') && array_key_exists('template', $_COOKIE)) { + unset($_COOKIE['template']); // ensure value is not re-used in template switcher + $expiredInAllTimezones = time() - 86400; + setcookie('template', '', array('expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true)); } } @@ -434,15 +435,11 @@ class Controller setcookie('lang', $languageselection, array('SameSite' => 'Lax', 'Secure' => true)); } - // set template cookie if that functionality was enabled, otherwise delete any existing cookie + // set template cookie if that functionality was enabled $templateselection = ''; if ($this->_conf->getKey('templateselection')) { $templateselection = TemplateSwitcher::getTemplate(); setcookie('template', $templateselection, array('SameSite' => 'Lax', 'Secure' => true)); - } elseif (array_key_exists('template', $_COOKIE)) { - unset($_COOKIE['template']); // ensure value is not re-used in template switcher - $expiredInAllTimezones = time() - 86400; - setcookie('template', '', array('expires' => $expiredInAllTimezones, 'SameSite' => 'Lax', 'Secure' => true)); } // strip policies that are unsupported in meta tag From 94a854faca5b997783b935ae6194fa13dbea6ce0 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 10:59:55 +0100 Subject: [PATCH 22/80] do add the configured template to the available ones, if missing --- lib/Controller.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Controller.php b/lib/Controller.php index 08192d4d..1d726da5 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -216,6 +216,9 @@ class Controller { $templates = $this->_conf->getKey('availabletemplates'); $template = $this->_conf->getKey('template'); + if (!in_array($template, $templates, true)) { + $templates[] = $template; + } TemplateSwitcher::setAvailableTemplates($templates); TemplateSwitcher::setTemplateFallback($template); From 51bb637411bf03e5f907f827b13f9fdc28c4bc42 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 11:00:19 +0100 Subject: [PATCH 23/80] document the change --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c368725..2bd8f43d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # PrivateBin version history ## 2.0.3 (not yet released) +* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching ## 2.0.2 (2025-10-28) * CHANGED: Upgrading libraries to: DOMpurify 3.3.0 @@ -44,7 +45,7 @@ * FIXED: Page template scripts loading order (#1579) ## 1.7.7 (2025-06-28) -* ADDED: Switching templates using the web ui (#1501) +* ADDED: Switching templates using the web UI (#1501) * ADDED: Show file name and size on download page (#603) * CHANGED: Passing large data structures by reference to reduce memory consumption (#858) * CHANGED: Removed use of ctype functions and polyfill library for ctype From a371f5cab51c8a03eea898a7ed7f8773564fd066 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 12:49:37 +0100 Subject: [PATCH 24/80] remove dead code --- lib/View.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/View.php b/lib/View.php index 9a83e6e4..f1c192cc 100644 --- a/lib/View.php +++ b/lib/View.php @@ -58,7 +58,6 @@ class View } foreach (new GlobIterator($dir . '*.php') as $tplFile) { if ($tplFile->getRealPath() === $path) { - $templatesInPath = new GlobIterator(PATH . 'tpl' . DIRECTORY_SEPARATOR . '*.php'); extract($this->_variables); include $path; return; From 9c71fbcc705d8f7d2d326582a555872aefdf720d Mon Sep 17 00:00:00 2001 From: Ribas160 Date: Tue, 11 Nov 2025 17:45:27 +0200 Subject: [PATCH 25/80] Use pure JavaScript to create a div element --- js/privatebin.js | 4 +++- lib/Configuration.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index ffb02d1b..4071399d 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -3091,7 +3091,9 @@ jQuery.PrivateBin = (function($) { function printDragAndDropFileNames(fileNames) { $dragAndDropFileNames.empty(); fileNames.forEach(fileName => { - $('
').text(fileName).appendTo($dragAndDropFileNames); + const div = document.createElement('div'); + div.textContent = fileName; + $(div).appendTo($dragAndDropFileNames); }); } diff --git a/lib/Configuration.php b/lib/Configuration.php index b1655ba4..a4909b37 100644 --- a/lib/Configuration.php +++ b/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-L2R5jtnyDjqMnUPKNjjoal2LO5fl/OcLtChaj6pQKkmbK97HUt9EcbuSpbnnyjPDhZCtG0CF4wkCQ4xAk8x2Ag==', + 'js/privatebin.js' => 'sha512-9z0y4LGbucj4HvTPYdPewBnijDUsPsDz8zDoQdjn2+pUw6P1OzhLe0EMtySeks4tp2AyuB0mQo1JBgJgKJxAOA==', '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==', From f456fb576eef76031659d077d90c3a5600b0409b Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 17:52:48 +0100 Subject: [PATCH 26/80] ensure template cookie cannot be a path --- lib/TemplateSwitcher.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/TemplateSwitcher.php b/lib/TemplateSwitcher.php index abfdffc0..fa9fdb18 100644 --- a/lib/TemplateSwitcher.php +++ b/lib/TemplateSwitcher.php @@ -73,8 +73,11 @@ class TemplateSwitcher */ public static function getTemplate(): string { - if (array_key_exists('template', $_COOKIE) && self::isTemplateAvailable($_COOKIE['template'])) { - return $_COOKIE['template']; + if (array_key_exists('template', $_COOKIE)) { + $template = basename($_COOKIE['template']); + if (self::isTemplateAvailable($template)) { + return $template; + } } return self::$_templateFallback; } From c35fc4f7904fd5f386c1f2d9e0b806217b6922a1 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 17:53:50 +0100 Subject: [PATCH 27/80] use more straight forward in_array check kudos @Ribas160 for the suggestion --- lib/View.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/View.php b/lib/View.php index f1c192cc..4aebfe43 100644 --- a/lib/View.php +++ b/lib/View.php @@ -52,18 +52,15 @@ class View { $dir = PATH . 'tpl' . DIRECTORY_SEPARATOR; $file = substr($template, 0, 10) === 'bootstrap-' ? 'bootstrap' : $template; - $path = realpath($dir . $file . '.php'); - if ($path === false) { - throw new Exception('Template ' . $template . ' not found!', 80); + $path = $dir . $file . '.php'; + if (!is_file($path)) { + throw new Exception('Template ' . $template . ' not found in file ' . $path . '!', 80); } - foreach (new GlobIterator($dir . '*.php') as $tplFile) { - if ($tplFile->getRealPath() === $path) { - extract($this->_variables); - include $path; - return; - } + if (!in_array($path, glob($dir . '*.php', GLOB_NOSORT | GLOB_ERR), true)) { + throw new Exception('Template ' . $file . '.php not found in ' . $dir . '!', 81); } - throw new Exception('Template ' . $file . '.php not found in ' . $dir . '!', 81); + extract($this->_variables); + include $path; } /** From 2e11b13464cf568cd5405179a58c5ca3635aaa80 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 11 Nov 2025 17:56:49 +0100 Subject: [PATCH 28/80] remove dead code --- lib/View.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/View.php b/lib/View.php index 4aebfe43..514ac2b1 100644 --- a/lib/View.php +++ b/lib/View.php @@ -12,7 +12,6 @@ namespace PrivateBin; use Exception; -use GlobIterator; /** * View From ff5aee85b47be07881399214c457816288500d7b Mon Sep 17 00:00:00 2001 From: Mikhail Romanov <42250412+Ribas160@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:05:32 +0200 Subject: [PATCH 29/80] Insert file names as break-separated text nodes Co-authored-by: El RIDO --- js/privatebin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 4071399d..d798f908 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -3091,9 +3091,9 @@ jQuery.PrivateBin = (function($) { function printDragAndDropFileNames(fileNames) { $dragAndDropFileNames.empty(); fileNames.forEach(fileName => { - const div = document.createElement('div'); - div.textContent = fileName; - $(div).appendTo($dragAndDropFileNames); + const name = document.createTextNode(fileName); + $dragAndDropFileNames[0].appendChild(name); + $dragAndDropFileNames[0].appendChild(document.createElement('br')); }); } From 08b324431426e053e8c5c35e9f660f60e13f0d9a Mon Sep 17 00:00:00 2001 From: Ribas160 Date: Tue, 11 Nov 2025 20:13:10 +0200 Subject: [PATCH 30/80] privatebin.js SRI and CHANGELOG.md updated --- CHANGELOG.md | 1 + lib/Configuration.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c368725..f1707038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # PrivateBin version history ## 2.0.3 (not yet released) +* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users ## 2.0.2 (2025-10-28) * CHANGED: Upgrading libraries to: DOMpurify 3.3.0 diff --git a/lib/Configuration.php b/lib/Configuration.php index a4909b37..ee72c43b 100644 --- a/lib/Configuration.php +++ b/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-9z0y4LGbucj4HvTPYdPewBnijDUsPsDz8zDoQdjn2+pUw6P1OzhLe0EMtySeks4tp2AyuB0mQo1JBgJgKJxAOA==', + 'js/privatebin.js' => 'sha512-D2cmzY2Ol+RvUvN7g6gGCYRE3CIksHOg0B/ejbYgplDA2c3CHG1l81nvChTgXWCP4+uK2N5fMoRKzwUMjEjnSA==', '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==', From 42df559d3c91a8ef34f2f389481f7a98acfbf6c9 Mon Sep 17 00:00:00 2001 From: rugk Date: Tue, 11 Nov 2025 19:52:12 +0100 Subject: [PATCH 31/80] docs: update how to open Co-authored-by: El RIDO --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b744e78f..87daf532 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -43,7 +43,7 @@ ### Run - **PHP Server Mode:** Use Apache/Nginx with PHP, pointing the web root to the repo root. -- **Demo:** Open `index.php` in a browser (via your web server). +- **Demo:** Open the root directory served by the web server in a browser. This should call the index.php in the repositories root directory. ### Test From f9630e7e6cae802d13975241adf96ddfa439dfaa Mon Sep 17 00:00:00 2001 From: rugk Date: Tue, 11 Nov 2025 20:02:36 +0100 Subject: [PATCH 32/80] Add PHP composer global installation for DevContainer Updated composer commands to require google/cloud-storage globally and optimized autoloader during update. This helps with https://github.com/PrivateBin/PrivateBin/issues/1641 (but technically not solve it). --- .devcontainer/postCreateCommand.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index a59fbfbf..67ee2279 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -8,8 +8,7 @@ ln -s ./conf.sample.php cfg/conf.php composer install --no-dev --optimize-autoloader # for PHP unit testing -# composer require google/cloud-storage -# composer install --optimize-autoloader +composer require --global google/cloud-storage sudo chmod a+x "$(pwd)" && sudo rm -rf /var/www/html && sudo ln -s "$(pwd)" /var/www/html From f48544978b8acb86abb30ba270911321bd1a5fb1 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 12 Nov 2025 07:57:30 +0100 Subject: [PATCH 33/80] apply StyleCI suggestion --- tst/ViewTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tst/ViewTest.php b/tst/ViewTest.php index b3d4aa37..64ec6503 100644 --- a/tst/ViewTest.php +++ b/tst/ViewTest.php @@ -149,5 +149,4 @@ class ViewTest extends TestCase $this->expectExceptionCode(81); $test->draw('../index'); } - } From 19ca6d3dab89a8282cdb9dcad07a8b49b4492c03 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 12 Nov 2025 08:00:50 +0100 Subject: [PATCH 34/80] incrementing version --- Makefile | 2 +- README.md | 2 +- SECURITY.md | 4 ++-- doc/Installation.md | 2 +- js/package.json | 2 +- lib/Controller.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 6c71c2d1..e74693f7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all coverage coverage-js coverage-php doc doc-js doc-php increment sign test test-js test-php help -CURRENT_VERSION = 2.0.2 +CURRENT_VERSION = 2.0.3 VERSION ?= 2.0.3 VERSION_FILES = README.md SECURITY.md doc/Installation.md js/package.json lib/Controller.php Makefile REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g") diff --git a/README.md b/README.md index e3bf3346..9ea3adf4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![PrivateBin](https://cdn.rawgit.com/PrivateBin/assets/master/images/preview/logoSmall.png)](https://privatebin.info/) -*Current version: 2.0.2* +*Current version: 2.0.3* **PrivateBin** is a minimalist, open source online [pastebin](https://en.wikipedia.org/wiki/Pastebin) diff --git a/SECURITY.md b/SECURITY.md index 9e05fa43..5b37c50d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 2.0.2 | :heavy_check_mark: | -| < 2.0.2 | :x: | +| 2.0.3 | :heavy_check_mark: | +| < 2.0.3 | :x: | ## Reporting a Vulnerability diff --git a/doc/Installation.md b/doc/Installation.md index 355b2f2d..43bb5a43 100644 --- a/doc/Installation.md +++ b/doc/Installation.md @@ -203,7 +203,7 @@ CREATE INDEX parent ON prefix_comment(pasteid); CREATE TABLE prefix_config ( id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id) ); -INSERT INTO prefix_config VALUES('VERSION', '2.0.2'); +INSERT INTO prefix_config VALUES('VERSION', '2.0.3'); ``` In **PostgreSQL**, the `data`, `attachment`, `nickname` and `vizhash` columns diff --git a/js/package.json b/js/package.json index c9af5ad2..8e1bb434 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "privatebin", - "version": "2.0.2", + "version": "2.0.3", "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of stored data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).", "main": "privatebin.js", "directories": { diff --git a/lib/Controller.php b/lib/Controller.php index 1d726da5..b4a12706 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -30,7 +30,7 @@ class Controller * * @const string */ - const VERSION = '2.0.2'; + const VERSION = '2.0.3'; /** * minimal required PHP version From d32ac29925066c668241a165264c76de051398e3 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 12 Nov 2025 08:51:05 +0100 Subject: [PATCH 35/80] chore: prepare for next release --- CHANGELOG.md | 4 +++- Makefile | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dbfe0df..2e95aec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # PrivateBin version history -## 2.0.3 (not yet released) +## 2.0.4 (not yet released) + +## 2.0.3 (2025-11-12) * FIXED: Prevent arbitrary PHP file inclusion when enabling template switching * FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users * FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585) diff --git a/Makefile b/Makefile index e74693f7..86165d7c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all coverage coverage-js coverage-php doc doc-js doc-php increment sign test test-js test-php help CURRENT_VERSION = 2.0.3 -VERSION ?= 2.0.3 +VERSION ?= 2.0.4 VERSION_FILES = README.md SECURITY.md doc/Installation.md js/package.json lib/Controller.php Makefile REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g") REGEX_VERSION := $(shell echo $(VERSION) | sed "s/\./\\\./g") From f4216b34dfc63433f2c5a57889b75a806f3c59dc Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 12 Nov 2025 19:36:05 +0100 Subject: [PATCH 36/80] Apply suggestions from code review Co-authored-by: El RIDO --- .github/copilot-instructions.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 87daf532..e004b740 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,7 +22,7 @@ 1. **Clone the repository** and enter its root directory. 2. **Install PHP dependencies:** ```sh - composer install + composer require --global google/cloud-storage phpunit/phpunit ``` - Always run this before building or testing PHP code. - If you receive permission errors, verify `vendor/` is writable. @@ -58,7 +58,7 @@ cd ./js npm run test ``` - - Runs Jasmine-based tests in Node.js context. + - Runs Mocha-based tests in Node.js context. Tests are implemented in BDD-Style or using jsVerify fixtures for property-based tests. - Note: **Production JS must not use Node-only APIs.** Test code may use Node.js features, but main JS logic must remain browser-compatible. - If you encounter `ReferenceError` for browser features, ensure only test code uses Node.js APIs. @@ -76,7 +76,7 @@ ### Validation / CI -- **GitHub Actions:** CI runs `composer install`, `phpunit`, and JS tests on PRs and pushes. +- **GitHub Actions:** CI runs `composer install`, `phpunit`, and `mocha` tests on PRs and pushes, as well as external tools such as style checkers and linters. - **Pre-commit:** Always run both PHP and JS tests before submitting PRs. Fix any warnings or errors. ## Project Layout & Structure @@ -84,10 +84,10 @@ - **Root files:** - `README.md`: Project overview ([view full](../README.md)). - `composer.json`, `composer.lock`: PHP dependencies. - - `package.json`: JS test/lint dependencies (not for production JS). - `.github/workflows/`: CI configuration. - `cfg/`: Default configuration files. - `js/`: Main client logic (browser JS), including: + - `package.json`: JS test/lint dependencies (not for production JS). - `legacy.js`: Must remain compatible with legacy browsers (ES3). **Do not use modern JS here.** - `privatebin.js`: Core encryption and paste interface logic. - `tpl/`: HTML templates. @@ -99,10 +99,10 @@ - `phpunit.xml`: PHPUnit config. - JS test files may use Node.js features; browser JS must not. - - **Encryption:** Only client-side in JS with WebCrypto. - - **Backend:** Serves encrypted blobs, never sees plaintext. - - **Legacy Support:** `js/legacy.js` must remain ES3 for feature detection in old browsers. - - **Configuration:** See `cfg/conf.php` and [wiki](https://github.com/PrivateBin/PrivateBin/wiki/Configuration) for available options. + - **Encryption:** Only client-side in JS using the browsers WebCrypto API. + - **Backend:** Serves encrypted blobs (as base64 encoded strings) and plaintext meta data in JSON format. APIs are designed for WORM (write once, read many) usage. Once stored content is never updated, only deleted, if delete token is sent, has expired as per meta data or immediately upon reading for the first time, if meta data was set to burn-after-reading. + - **Legacy Support:** `js/legacy.js` must remain compatible with IE4 and Netscape for feature detection of ancient browsers. + - **Configuration:** See `cfg/conf.sample.php` and the [wiki](https://github.com/PrivateBin/PrivateBin/wiki/Configuration) for available options. All option defaults are defined in `lib/Configuration.php` ## Automated Checks From 3ede063e0a50a3f2436b9d3dbcd891203f0a686a Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 12 Nov 2025 20:42:05 +0100 Subject: [PATCH 37/80] Syncronize changelog with GitHub release doc (for CVE IDs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aka adding the CVE ID's. BTW GitHub will make the CVEs clickable automatically when published. As for the GitHub's own ID well yeah… I just kept it synchronous now. (Maybe it's not _that_ relevant to mention all that IDs.) --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e95aec9..bf6852cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,14 @@ ## 2.0.4 (not yet released) ## 2.0.3 (2025-11-12) -* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching -* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users +* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching (CVE-2025-64714) +* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users (CVE-2025-64711) * FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585) ## 2.0.2 (2025-10-28) * 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 +* FIXED: Sanitize file name in attachment size hint (CVE-2025-62796 / https://github.com/PrivateBin/PrivateBin/security/advisories/GHSA-867c-p784-5q6g) * FIXED: PHP OPcache module is optional again (#1679) * FIXED: bootstrap template password peek input group display From 4cdc6871e726faec4ae37fe48a84568ef2d5ad6d Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 10:35:49 +0100 Subject: [PATCH 38/80] docs: use CVE website link Co-authored-by: El RIDO --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6852cf..3dfaebad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ## 2.0.2 (2025-10-28) * 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 (CVE-2025-62796 / https://github.com/PrivateBin/PrivateBin/security/advisories/GHSA-867c-p784-5q6g) +* FIXED: Sanitize file name in attachment size hint ([CVE-2025-62796](https://privatebin.info/reports/vulnerability-2025-10-28.html)) * FIXED: PHP OPcache module is optional again (#1679) * FIXED: bootstrap template password peek input group display From 2c4dd2594cc47ca0b5cd719b963304ec48aecba2 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 10:52:08 +0000 Subject: [PATCH 39/80] fix: do not encode source JSON translation string resulting in wrong display of special characters like ' Fixes #1712 Disclosure: Coded with help of Copiot. (description wrtten by me) So this does indeed loosen the encoding a bit. However, IMHO, it was neither better before though. You could always bypass the encoding for `args{0]` when you just include `')) { - continue; - } - } elseif (is_int($args[$i])) { + for ($i = 1; $i < $argsCount; ++$i) { + if (is_int($args[$i])) { continue; } $args[$i] = self::encode($args[$i]); diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 9e196103..118fe23b 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -182,7 +182,20 @@ class I18nTest extends TestCase $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false); $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities'); $this->assertEquals('some ' . $result . ' + 1', I18n::_('some %s + %d', $input, 1), 'encodes parameters in translations'); - $this->assertEquals($result . $result, I18n::_($input . '%s', $input), 'encodes message ID as well, when no link'); + // Message ID should NOT be encoded (it comes from trusted source), only the parameter should be + $this->assertEquals($input . $result, I18n::_($input . '%s', $input), 'encodes only parameters, not message ID'); + } + + public function testFrenchApostropheInMessage() + { + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; + I18n::loadTranslations(); + // The French translation should not have the apostrophe encoded + // Original: "Le document n'existe pas, a expiré, ou a été supprimé." + // Should NOT become: "Le document n'existe pas, a expiré, ou a été supprimé." + $message = I18n::_('Document does not exist, has expired or has been deleted.'); + $this->assertFalse(strpos($message, ''') !== false, 'French apostrophe should not be encoded in translation message'); + $this->assertTrue(strpos($message, "n'existe") !== false, 'French apostrophe should be present as literal character'); } public function testFallbackAlwaysPresent() From 38a722d2f5516085783583dee8f9c99e2ff2695a Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:19:49 +0000 Subject: [PATCH 40/80] test: make sure to unset HTTP_ACCEPT_LANGUAGE at test teardown --- tst/I18nTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 118fe23b..0374c897 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -37,6 +37,7 @@ class I18nTest extends TestCase public function tearDown(): void { unset($_COOKIE['lang'], $_SERVER['HTTP_ACCEPT_LANGUAGE']); + unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); } public function testTranslationFallback() @@ -186,7 +187,7 @@ class I18nTest extends TestCase $this->assertEquals($input . $result, I18n::_($input . '%s', $input), 'encodes only parameters, not message ID'); } - public function testFrenchApostropheInMessage() + public function testApostropheEncodngInMessage() { $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; I18n::loadTranslations(); From e6762646168c69f32ea4ec1de5ec96d88f221935 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:28:03 +0000 Subject: [PATCH 41/80] test: make I18nTest actually reload English translations again --- tst/I18nTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 0374c897..02d8cd3f 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -37,7 +37,7 @@ class I18nTest extends TestCase public function tearDown(): void { unset($_COOKIE['lang'], $_SERVER['HTTP_ACCEPT_LANGUAGE']); - unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); + I18n::loadTranslations(); } public function testTranslationFallback() From 72d4c7aa2b91b64ae1c2af923535e5a6a02f5642 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:33:31 +0000 Subject: [PATCH 42/80] style: clarify comments --- tst/I18nTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 02d8cd3f..d707d724 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -191,9 +191,8 @@ class I18nTest extends TestCase { $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; I18n::loadTranslations(); - // The French translation should not have the apostrophe encoded - // Original: "Le document n'existe pas, a expiré, ou a été supprimé." - // Should NOT become: "Le document n'existe pas, a expiré, ou a été supprimé." + // For example, the French translation should not have the apostrophe encoded + // See https://github.com/PrivateBin/PrivateBin/issues/1712 $message = I18n::_('Document does not exist, has expired or has been deleted.'); $this->assertFalse(strpos($message, ''') !== false, 'French apostrophe should not be encoded in translation message'); $this->assertTrue(strpos($message, "n'existe") !== false, 'French apostrophe should be present as literal character'); From 318a37d352a36ebaa7d145e3bc6d5064862fd47e Mon Sep 17 00:00:00 2001 From: El RIDO Date: Thu, 13 Nov 2025 15:38:22 +0100 Subject: [PATCH 43/80] document changes - forward ported 1.7.9 release changes - linked all vulnerability reports - unified heading formats (dropped colons) --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfaebad..6b7ad93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,19 @@ ## 2.0.4 (not yet released) +## 1.7.9 (2025-11-13) +* CHANGED: Upgrading libraries to: base-x 5.0.1, bootstrap 5.3.8, DOMpurify 3.2.7, ip-lib 1.21.0 & kjua 0.10.0 +* CHANGED: Refactored jQuery DOM element creation into plain JavaScript +* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching ([CVE-2025-64714](https://privatebin.info/reports/vulnerability-2025-11-12-templates.html)) +* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users ([CVE-2025-64711](https://privatebin.info/reports/vulnerability-2025-11-12-drag-drop.html)) +* FIXED: Sanitize file name in attachment size hint ([CVE-2025-62796](https://privatebin.info/reports/vulnerability-2025-10-28.html)) +* FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585) +* FIXED: traffic limiter not working when using Filesystem storage and PHP opcache +* FIXED: Configuration combinations test errors + ## 2.0.3 (2025-11-12) -* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching (CVE-2025-64714) -* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users (CVE-2025-64711) +* FIXED: Prevent arbitrary PHP file inclusion when enabling template switching ([CVE-2025-64714](https://privatebin.info/reports/vulnerability-2025-11-12-templates.html)) +* FIXED: Malicious filename can be used for self-XSS / HTML injection locally for users ([CVE-2025-64711](https://privatebin.info/reports/vulnerability-2025-11-12-drag-drop.html)) * FIXED: Unable to create a new paste from the cloned one when a JSON file attached (#1585) ## 2.0.2 (2025-10-28) @@ -87,7 +97,7 @@ * FIXED: Reset password input field on creation of new paste (#1194) * FIXED: Allow database schema upgrade to skip versions (#1343) * FIXED: `bootstrap5` dark mode toggle unset on dark browser preference (#1340) -* FIXED: Prevent bypassing YOURLS proxy URL filter, allowing to shorten non-self URLs +* FIXED: Prevent bypassing YOURLS proxy URL filter, allowing to shorten non-self URLs ([CVE-2024-39899](https://privatebin.info/reports/vulnerability-2024-07-09.html)) ## 1.7.3 (2024-05-13) * CHANGED: Various tweaks of the `bootstrap5` template, suggested by the community @@ -167,7 +177,7 @@ * ADDED: Oracle database support (#868) * ADDED: Configuration option to limit paste creation and commenting to certain IPs (#883) * ADDED: Set CSP also as meta tag, to deal with misconfigured webservers mangling the HTTP header -* ADDED: Sanitize SVG preview, preventing script execution in instance context +* ADDED: Sanitize SVG preview, preventing script execution in instance context ([CVE-2022-24833](https://privatebin.info/reports/vulnerability-2022-04-09.html)) * CHANGED: Language selection cookie only transmitted over HTTPS (#472) * CHANGED: Upgrading libraries to: base-x 4.0.0, bootstrap 3.4.1 (JS), DOMpurify 2.3.6, ip-lib 1.18.0, jQuery 3.6.0, random_compat 2.0.21, Showdown 2.0.3 & zlib 1.2.12 * CHANGED: Removed automatic `.ini` configuration file migration (#808) @@ -219,12 +229,12 @@ * ADDED: Option to send a mail with the link, when creating a paste (#398) * ADDED: Add support for CONFIG_PATH environment variable (#552) * CHANGED: Upgrading libraries to: base-x 3.0.7, DOMpurify 2.0.7 & Showdown 1.9.1 -* FIXED: HTML injection via unescaped attachment filename (#554) +* FIXED: HTML injection via unescaped attachment filename (#554) ([CVE-2020-5223](https://privatebin.info/reports/vulnerability-2020-01-11.html)) * FIXED: Password disabling option (#527) ## 1.2.2 (2020-01-11) * CHANGED: Upgrading libraries to: bootstrap 3.4.1 (CSS), DOMpurify 2.0.7, jQuery 3.4.1, kjua 0.6.0, Showdown 1.9.1 & SJCL 1.0.8 -* FIXED: HTML injection via unescaped attachment filename (#554) +* FIXED: HTML injection via unescaped attachment filename (#554) ([CVE-2020-5223](https://privatebin.info/reports/vulnerability-2020-01-11.html)) ## 1.3.1 (2019-09-22) * ADDED: Translation for Bulgarian (#455) @@ -268,7 +278,7 @@ * CHANGED: Added some missing Russian translations (#348) * CHANGED: Minor PHP refactoring: Rename PrivateBin class to Controller, improved logic of some persistence classes (#342) * CHANGED: Upgrading DOMpurify library to 1.0.7 -* FIXED: Ensure legacy browsers without webcrypto support can't create paste keys with insufficient entropy (#346) +* FIXED: Ensure legacy browsers without webcrypto support can't create paste keys with [insufficient entropy](https://privatebin.info/reports/vulnerability-2018-08-11.html) (#346) * FIXED: Re-add support for old browsers (Firefox<21, Chrome<31, Safari<7, IE<11), broken in 1.2, will be removed again in 1.3 ## 1.2 (2018-07-22) @@ -287,7 +297,7 @@ * FIXED: To counteract regressions introduced by the refactoring, we finally introduced property based unit testing for the JavaScript code, this caught several regressions, but also some very old bugs not found so far (#32) ## 1.1.1 (2017-10-06) -* CHANGED: Switched to `.php` file extension for configuration file, to avoid leaking configuration data in unprotected installation. +* CHANGED: Switched to `.php` file extension for configuration file, to avoid [leaking configuration data](https://privatebin.info/reports/vulnerability-2017-09-29.html) in unprotected installation. ## 1.1 (2016-12-26) * ADDED: Translations for Italian and Russian @@ -328,7 +338,7 @@ * FIXED: Removed unused code detected with the help of various code review tools * FIXED: Table format for PostgreSQL, making it possible to use PostgreSQL as backend in addition to MySQL, SQLite and flat files -## 0.22 (2015-11-09): +## 0.22 (2015-11-09) * ADDED: Tab character input support * ADDED: Dark bootstrap theme * ADDED: Option to hide clone button on expiring pastes @@ -344,13 +354,13 @@ * CHANGED: Database structure to store attachments, allowing larger attachments to be stored (depending on maximum BLOB size of database backend) * CHANGED: Refactored data model, traffic limiting & request handling -## 0.21.1 (2015-09-21): +## 0.21.1 (2015-09-21) * FIXING: lost meta data when using DB model instead of flat files * FIXING: mobile navbar getting triggered on load * CHANGED: database table "paste" gets automatically extended with a "meta" column * CHANGED: navbar of "bootstrap" template now spans full width of view port on large screens -## 0.21 (2015-09-19): +## 0.21 (2015-09-19) * ADDED: Translations for German, French and Polish, language selection menu (optional) * ADDED: File upload and image display support (optional) * ADDED: Markdown format support @@ -368,7 +378,7 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * [Translation](https://github.com/PrivateBin/PrivateBin/wiki/Translation) * [Templates](https://github.com/PrivateBin/PrivateBin/wiki/Templates) -## 0.20 (2015-09-03): +## 0.20 (2015-09-03) * ADDED: Password protected pastes (optional) * ADDED: configuration options for highlighting, password, discussions, expiration times, rate limiting * ADDED: JSON-only retrieval of paste incl. discussion, used to be able to refresh paste when posting a comment @@ -379,11 +389,11 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * updated JS libraries: jquery to 1.11.3, sjcl to 1.0.2, base64.js to 2.1.9, deflate to 0.5, inflate to 0.3 and prettify to latest * generally improved documentation, both inline phpdoc / JSdoc source code documentation, as well as Wiki pages on installation, configuration, development and JSON-API -## Alpha 0.19 (2013-07-05): +## Alpha 0.19 (2013-07-05) * Corrected XSS security flaw which affected IE<10. Other browsers were not affected. * Corrected spacing display in IE<10. -## Alpha 0.18 (2013-02-24): +## Alpha 0.18 (2013-02-24) * ADDED: The resulting URL is automatically selected after pressing "Send". You just have to press CTRL+C. * ADDED: Automatic syntax highlighting for 53 languages using highlight.js * ADDED: "5 minutes" and "1 week" expirations. @@ -397,32 +407,32 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * ADDED: Added version to js/css assets URLs in order to prevent some abusive caches to serve an obsolete version of these files when ZeroBin is upgraded. * "Burn after reading" option has been moved out of Expiration combo to a separate checkbox. Reason is: You can prevent a read-once paste to be available ad vitam eternam on the net. -## Alpha 0.17 (2013-02-23): +## Alpha 0.17 (2013-02-23) * ADDED: Deletion URL. * small refactoring. * improved regex checks. * larger server alt on installation. -## Alpha 0.16: +## Alpha 0.16 * FIXED minor php warnings. * FIXED: zerobin.js reformated and properly commented. * FIXED: Directory structure re-organized. * CHANGED: URL shortening button was removed. (It was bad for privacy.) -## Alpha 0.15 (2012-04-20): +## Alpha 0.15 (2012-04-20) * FIXED: 2 minor corrections to avoid notices in php log. * FIXED: Sources converted to UTF-8. -## Alpha 0.14 (2012-04-20): +## Alpha 0.14 (2012-04-20) * ADDED: GD presence is checked. * CHANGED: Traffic limiter data files moved to data/ (→easier rights management) * ADDED: "Burn after reading" implemented. Opening the URL will display the paste and immediately destroy it on server. -## Alpha 0.13 (2012-04-18): +## Alpha 0.13 (2012-04-18) * FIXED: ''imageantialias()'' call removed because it's not really usefull and can be a problem on most hosts (if GD is not compiled in php). * FIXED: $error not properly initialized in index.php -## Alpha 0.12 (2012-04-18): +## Alpha 0.12 (2012-04-18) ## DISCUSSIONS ! Now you can enable discussions on your pastes. Of course, posted comments and nickname are also encrypted and the server cannot see them. * This feature implies a change in storage format. You will have to delete all previous pastes in your ZeroBin. * Added [[php:vizhash_gd|Vizhash]] as avatars, so you can match posters IP addresses without revealing them. (Same image = same IP). Of course the IP address cannot be deduced from the Vizhash. @@ -430,17 +440,17 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * Explicit tags were added to CSS and jQuery selectors (eg. div#aaa instead of #aaa) to speed up browser. * Better cleaning of the URL (to make sure the key is not broken by some stupid redirection service) -## Alpha 0.11 (2012-04-12): +## Alpha 0.11 (2012-04-12) * Automatically ignore parameters (such as &utm_source=...) added //after// the anchor by some stupid Web 2.0 services. * First public release. -## Alpha 0.10 (2012-04-12): +## Alpha 0.10 (2012-04-12) * IE9 does not seem to correctly support ''pre-wrap'' either. Special handling mode activated for all version of IE<10. (Note: ALL other browsers correctly support this feature.) -## Alpha 0.9 (2012-04-11): +## Alpha 0.9 (2012-04-11) * Oh bummer... IE 8 is as shitty as IE6/7: Its does not seem to support ''white-space:pre-wrap'' correctly. I had to activate the special handling mode. I still have to test IE 9. -## Alpha 0.8 (2012-04-11): +## Alpha 0.8 (2012-04-11) * Source code not published yet. * Interface completely redesigned. Icons added. * Now properly supports IE6/7 (ugly display, but it works. "Clone" button is disabled though.) From d78c33438d3daabecd48e9509c03b28ad8211e9a Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 14 Nov 2025 10:04:41 +0100 Subject: [PATCH 44/80] refactor JSON response processing - avoid translating JSON error messages twice - separation of concerns, JSON response preparation should not mix handling errors and results, provide two functions instead of one - callers of JSON error method are responsible for translation of errors --- CHANGELOG.md | 1 + lib/Controller.php | 84 +++++++++++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfaebad..ee0b59dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # PrivateBin version history ## 2.0.4 (not yet released) +* CHANGED: Deduplicate JSON error message translations. ## 2.0.3 (2025-11-12) * FIXED: Prevent arbitrary PHP file inclusion when enabling template switching (CVE-2025-64714) diff --git a/lib/Controller.php b/lib/Controller.php index b4a12706..def5fae2 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -271,7 +271,8 @@ class Controller try { TrafficLimiter::canPass(); } catch (Exception $e) { - $this->_return_message(1, $e->getMessage()); + // traffic limiter exceptions come translated + $this->_json_error($e->getMessage()); return; } @@ -281,14 +282,13 @@ class Controller array_key_exists('parentid', $data) && !empty($data['parentid']); if (!FormatV2::isValid($data, $isComment)) { - $this->_return_message(1, I18n::_('Invalid data.')); + $this->_json_error(I18n::_('Invalid data.')); return; } $sizelimit = $this->_conf->getKey('sizelimit'); // Ensure content is not too big. if (strlen($data['ct']) > $sizelimit) { - $this->_return_message( - 1, + $this->_json_error( I18n::_( 'Document is limited to %s of encrypted data.', Filter::formatHumanReadableSize($sizelimit) @@ -306,12 +306,13 @@ class Controller $comment->setData($data); $comment->store(); } catch (Exception $e) { - $this->_return_message(1, $e->getMessage()); + // comment exceptions need translation + $this->_json_error(I18n::_($e->getMessage())); return; } - $this->_return_message(0, $comment->getId()); + $this->_json_result($comment->getId()); } else { - $this->_return_message(1, I18n::_('Invalid data.')); + $this->_json_error(I18n::_('Invalid data.')); } } // The user posts a standard paste. @@ -329,10 +330,11 @@ class Controller $paste->setData($data); $paste->store(); } catch (Exception $e) { - $this->_return_message(1, $e->getMessage()); + // paste exceptions need translation + $this->_json_error(I18n::_($e->getMessage())); return; } - $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken())); + $this->_json_result($paste->getId(), array('deletetoken' => $paste->getDeleteToken())); } } @@ -367,9 +369,9 @@ class Controller } if ($this->_request->isJsonApiCall()) { if (empty($this->_error)) { - $this->_return_message(0, $dataid); + $this->_json_result($dataid); } else { - $this->_return_message(1, $this->_error); + $this->_json_error(I18n::_($this->_error)); } } } @@ -393,12 +395,13 @@ class Controller if (array_key_exists('salt', $data['meta'])) { unset($data['meta']['salt']); } - $this->_return_message(0, $dataid, (array) $data); + $this->_json_result($dataid, (array) $data); } else { - $this->_return_message(1, self::GENERIC_ERROR); + $this->_json_error(I18n::_(self::GENERIC_ERROR)); } } catch (Exception $e) { - $this->_return_message(1, $e->getMessage()); + // paste exceptions need translation + $this->_json_error(I18n::_($e->getMessage())); } } @@ -537,6 +540,38 @@ class Controller echo $content; } + /** + * prepares JSON encoded error message + * + * @access private + * @param string $error + */ + private function _json_error($error) + { + $result = array( + 'status' => 1, + 'message' => $error + ); + $this->_json = Json::encode($result); + } + + /** + * prepares JSON encoded result message + * + * @access private + * @param string $dataid + * @param array $other + */ + private function _json_result($dataid, $other = array()) + { + $result = array( + 'status' => 0, + 'id' => $dataid, + 'url' => $this->_urlBase . '?' . $dataid + ) + $other; + $this->_json = Json::encode($result); + } + /** * Proxies a link using the specified proxy class, and updates the status or error with the response. * @@ -551,25 +586,4 @@ class Controller $this->_status = $proxy->getUrl(); } } - - /** - * prepares JSON encoded status message - * - * @access private - * @param int $status - * @param string $message - * @param array $other - */ - private function _return_message($status, $message, $other = array()) - { - $result = array('status' => $status); - if ($status) { - $result['message'] = I18n::_($message); - } else { - $result['id'] = $message; - $result['url'] = $this->_urlBase . '?' . $message; - } - $result += $other; - $this->_json = Json::encode($result); - } } From e26bcfa733c3ed7d1efc183bc1699137f8165a6b Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 14 Nov 2025 10:21:57 +0100 Subject: [PATCH 45/80] apply StyleCI recommendation --- lib/Controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Controller.php b/lib/Controller.php index def5fae2..bb161907 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -550,7 +550,7 @@ class Controller { $result = array( 'status' => 1, - 'message' => $error + 'message' => $error, ); $this->_json = Json::encode($result); } @@ -567,7 +567,7 @@ class Controller $result = array( 'status' => 0, 'id' => $dataid, - 'url' => $this->_urlBase . '?' . $dataid + 'url' => $this->_urlBase . '?' . $dataid, ) + $other; $this->_json = Json::encode($result); } From f4f655966cd2b82fd945fb27a4360b4b74316067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:37:12 +0000 Subject: [PATCH 46/80] Bump js-yaml from 4.1.0 to 4.1.1 in /js Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- js/package-lock.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/js/package-lock.json b/js/package-lock.json index 80b713d3..c06d5bf8 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "privatebin", - "version": "2.0.0", + "version": "2.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "privatebin", - "version": "2.0.0", + "version": "2.0.3", "license": "zlib-acknowledgement", "devDependencies": { "@peculiar/webcrypto": "^1.5.0", @@ -1275,11 +1275,10 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2785,9 +2784,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" From 3e6f1733f95c423fd51bb9113b31de1e01050f60 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 17 Nov 2025 17:28:13 +0100 Subject: [PATCH 47/80] refactored exceptions in controller - added missing exception doc blocks - introduced exception type that translates message during construction - catch explicit exception types where possible --- lib/Configuration.php | 11 +++++--- lib/Controller.php | 25 ++++++++++--------- lib/Model/AbstractModel.php | 26 ++++++++++++++----- lib/Model/Comment.php | 17 ++++++------- lib/Model/Paste.php | 28 ++++++++++----------- lib/Persistence/TrafficLimiter.php | 9 +++---- lib/TranslatedException.php | 36 +++++++++++++++++++++++++++ vendor/composer/autoload_classmap.php | 1 + vendor/composer/autoload_static.php | 1 + vendor/composer/installed.php | 4 +-- 10 files changed, 105 insertions(+), 53 deletions(-) create mode 100644 lib/TranslatedException.php diff --git a/lib/Configuration.php b/lib/Configuration.php index 2cccc342..c4651f8b 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -12,6 +12,7 @@ namespace PrivateBin; use Exception; +use PrivateBin\TranslatedException; /** * Configuration @@ -131,7 +132,7 @@ class Configuration /** * parse configuration file and ensure default configuration values are present * - * @throws Exception + * @throws TranslatedException */ public function __construct() { @@ -148,7 +149,9 @@ class Configuration $config = parse_ini_file($configFile, true); foreach (array('main', 'model', 'model_options') as $section) { if (!array_key_exists($section, $config)) { - throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 2); + $name = array_key_exists('main', $config) && array_key_exists('name', $config['main']) ? + $config['main']['name'] : self::getDefaults()['main']['name']; + throw new TranslatedException(array('%s requires configuration section [%s] to be present in configuration file.', I18n::_($name), $section), 2); } } break; @@ -304,13 +307,13 @@ class Configuration * get a section from the configuration, must exist * * @param string $section - * @throws Exception + * @throws TranslatedException * @return mixed */ public function getSection($section) { if (!array_key_exists($section, $this->_configuration)) { - throw new Exception(I18n::_('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3); + throw new TranslatedException(array('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3); } return $this->_configuration[$section]; } diff --git a/lib/Controller.php b/lib/Controller.php index bb161907..2e056db6 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -17,6 +17,7 @@ use PrivateBin\Persistence\TrafficLimiter; use PrivateBin\Proxy\AbstractProxy; use PrivateBin\Proxy\ShlinkProxy; use PrivateBin\Proxy\YourlsProxy; +use PrivateBin\TranslatedException; /** * Controller @@ -195,6 +196,7 @@ class Controller * Set default language * * @access private + * @throws Exception */ private function _setDefaultLanguage() { @@ -211,6 +213,7 @@ class Controller * Set default template * * @access private + * @throws Exception */ private function _setDefaultTemplate() { @@ -260,6 +263,7 @@ class Controller * pasteid (optional) = in discussions, which paste this comment belongs to. * * @access private + * @throws Exception * @return string */ private function _create() @@ -270,8 +274,7 @@ class Controller TrafficLimiter::setStore($this->_model->getStore()); try { TrafficLimiter::canPass(); - } catch (Exception $e) { - // traffic limiter exceptions come translated + } catch (TranslatedException $e) { $this->_json_error($e->getMessage()); return; } @@ -305,9 +308,8 @@ class Controller $comment = $paste->getComment($data['parentid']); $comment->setData($data); $comment->store(); - } catch (Exception $e) { - // comment exceptions need translation - $this->_json_error(I18n::_($e->getMessage())); + } catch (TranslatedException $e) { + $this->_json_error($e->getMessage()); return; } $this->_json_result($comment->getId()); @@ -319,7 +321,7 @@ class Controller else { try { $this->_model->purge(); - } catch (Exception $e) { + } catch (Exception $e) { // JSON error!!! error_log('Error purging documents: ' . $e->getMessage() . PHP_EOL . 'Use the administration scripts statistics to find ' . 'damaged paste IDs and either delete them or restore them ' . @@ -329,9 +331,8 @@ class Controller try { $paste->setData($data); $paste->store(); - } catch (Exception $e) { - // paste exceptions need translation - $this->_json_error(I18n::_($e->getMessage())); + } catch (TranslatedException $e) { + $this->_json_error($e->getMessage()); return; } $this->_json_result($paste->getId(), array('deletetoken' => $paste->getDeleteToken())); @@ -399,9 +400,8 @@ class Controller } else { $this->_json_error(I18n::_(self::GENERIC_ERROR)); } - } catch (Exception $e) { - // paste exceptions need translation - $this->_json_error(I18n::_($e->getMessage())); + } catch (TranslatedException $e) { + $this->_json_error($e->getMessage()); } } @@ -409,6 +409,7 @@ class Controller * Display frontend. * * @access private + * @throws Exception */ private function _view() { diff --git a/lib/Model/AbstractModel.php b/lib/Model/AbstractModel.php index d7b5102f..5ff4ab1b 100644 --- a/lib/Model/AbstractModel.php +++ b/lib/Model/AbstractModel.php @@ -11,9 +11,9 @@ namespace PrivateBin\Model; -use Exception; use PrivateBin\Configuration; use PrivateBin\Data\AbstractData; +use PrivateBin\TranslatedException; /** * AbstractModel @@ -22,6 +22,20 @@ use PrivateBin\Data\AbstractData; */ abstract class AbstractModel { + /** + * show the same error message if the data is invalid + * + * @const string + */ + const INVALID_DATA_ERROR = 'Invalid data.'; + + /** + * show the same error message if the document ID already exists + * + * @const string + */ + const COLLISION_ERROR = 'You are unlucky. Try again.'; + /** * Instance ID. * @@ -83,12 +97,12 @@ abstract class AbstractModel * * @access public * @param string $id - * @throws Exception + * @throws TranslatedException */ public function setId($id) { if (!self::isValidId($id)) { - throw new Exception('Invalid document ID.', 60); + throw new TranslatedException('Invalid document ID.', 60); } $this->_id = $id; } @@ -98,7 +112,7 @@ abstract class AbstractModel * * @access public * @param array $data - * @throws Exception + * @throws TranslatedException */ public function setData(array &$data) { @@ -125,7 +139,7 @@ abstract class AbstractModel * Store the instance's data. * * @access public - * @throws Exception + * @throws TranslatedException */ abstract public function store(); @@ -163,7 +177,7 @@ abstract class AbstractModel * * @access protected * @param array $data - * @throws Exception + * @throws TranslatedException */ protected function _validate(array &$data) { diff --git a/lib/Model/Comment.php b/lib/Model/Comment.php index 3c0b2b20..3a9d6336 100644 --- a/lib/Model/Comment.php +++ b/lib/Model/Comment.php @@ -11,10 +11,10 @@ namespace PrivateBin\Model; -use Exception; use Identicon\Identicon; use Jdenticon\Identicon as Jdenticon; use PrivateBin\Persistence\TrafficLimiter; +use PrivateBin\TranslatedException; use PrivateBin\Vizhash16x16; /** @@ -36,24 +36,24 @@ class Comment extends AbstractModel * Store the comment's data. * * @access public - * @throws Exception + * @throws TranslatedException */ public function store() { // Make sure paste exists. $pasteid = $this->getPaste()->getId(); if (!$this->getPaste()->exists()) { - throw new Exception('Invalid data.', 67); + throw new TranslatedException(self::INVALID_DATA_ERROR, 67); } // Make sure the discussion is opened in this paste and allowed in the configuration. if (!$this->getPaste()->isOpendiscussion() || !$this->_conf->getKey('discussion')) { - throw new Exception('Invalid data.', 68); + throw new TranslatedException(self::INVALID_DATA_ERROR, 68); } // Check for improbable collision. if ($this->exists()) { - throw new Exception('You are unlucky. Try again.', 69); + throw new TranslatedException(self::COLLISION_ERROR, 69); } $this->_data['meta']['created'] = time(); @@ -67,7 +67,7 @@ class Comment extends AbstractModel $this->_data ) === false ) { - throw new Exception('Error saving comment. Sorry.', 70); + throw new TranslatedException('Error saving comment. Sorry.', 70); } } @@ -91,7 +91,6 @@ class Comment extends AbstractModel * * @access public * @param Paste $paste - * @throws Exception */ public function setPaste(Paste &$paste) { @@ -115,12 +114,12 @@ class Comment extends AbstractModel * * @access public * @param string $id - * @throws Exception + * @throws TranslatedException */ public function setParentId($id) { if (!self::isValidId($id)) { - throw new Exception('Invalid document ID.', 65); + throw new TranslatedException('Invalid document ID.', 65); } $this->_data['parentid'] = $id; } diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index a42ab35e..0d52b2a3 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -11,9 +11,9 @@ namespace PrivateBin\Model; -use Exception; use PrivateBin\Controller; use PrivateBin\Persistence\ServerSalt; +use PrivateBin\TranslatedException; /** * Paste @@ -47,14 +47,14 @@ class Paste extends AbstractModel * Get paste data. * * @access public - * @throws Exception + * @throws TranslatedException * @return array */ public function get() { $data = $this->_store->read($this->getId()); if ($data === false) { - throw new Exception(Controller::GENERIC_ERROR, 64); + throw new TranslatedException(Controller::GENERIC_ERROR, 64); } // check if paste has expired and delete it if necessary. @@ -62,7 +62,7 @@ class Paste extends AbstractModel $now = time(); if ($data['meta']['expire_date'] < $now) { $this->delete(); - throw new Exception(Controller::GENERIC_ERROR, 63); + throw new TranslatedException(Controller::GENERIC_ERROR, 63); } // We kindly provide the remaining time before expiration (in seconds) $data['meta']['time_to_live'] = $data['meta']['expire_date'] - $now; @@ -93,13 +93,13 @@ class Paste extends AbstractModel * Store the paste's data. * * @access public - * @throws Exception + * @throws TranslatedException */ public function store() { // Check for improbable collision. if ($this->exists()) { - throw new Exception('You are unlucky. Try again.', 75); + throw new TranslatedException(self::COLLISION_ERROR, 75); } $this->_data['meta']['salt'] = ServerSalt::generate(); @@ -111,7 +111,7 @@ class Paste extends AbstractModel $this->_data ) === false ) { - throw new Exception('Error saving document. Sorry.', 76); + throw new TranslatedException('Error saving document. Sorry.', 76); } } @@ -119,7 +119,6 @@ class Paste extends AbstractModel * Delete the paste. * * @access public - * @throws Exception */ public function delete() { @@ -143,13 +142,13 @@ class Paste extends AbstractModel * @access public * @param string $parentId * @param string $commentId - * @throws Exception + * @throws TranslatedException * @return Comment */ public function getComment($parentId, $commentId = '') { if (!$this->exists()) { - throw new Exception('Invalid data.', 62); + throw new TranslatedException(self::INVALID_DATA_ERROR, 62); } $comment = new Comment($this->_conf, $this->_store); $comment->setPaste($this); @@ -201,7 +200,6 @@ class Paste extends AbstractModel * Check if paste has discussions enabled. * * @access public - * @throws Exception * @return bool */ public function isOpendiscussion() @@ -240,13 +238,13 @@ class Paste extends AbstractModel * * @access protected * @param array $data - * @throws Exception + * @throws TranslatedException */ protected function _validate(array &$data) { // reject invalid or disabled formatters if (!array_key_exists($data['adata'][self::ADATA_FORMATTER], $this->_conf->getSection('formatter_options'))) { - throw new Exception('Invalid data.', 75); + throw new TranslatedException(self::INVALID_DATA_ERROR, 75); } // discussion requested, but disabled in config or burn after reading requested as well, or invalid integer @@ -257,7 +255,7 @@ class Paste extends AbstractModel )) || ($data['adata'][self::ADATA_OPEN_DISCUSSION] !== 0 && $data['adata'][self::ADATA_OPEN_DISCUSSION] !== 1) ) { - throw new Exception('Invalid data.', 74); + throw new TranslatedException(self::INVALID_DATA_ERROR, 74); } // reject invalid burn after reading @@ -265,7 +263,7 @@ class Paste extends AbstractModel $data['adata'][self::ADATA_BURN_AFTER_READING] !== 0 && $data['adata'][self::ADATA_BURN_AFTER_READING] !== 1 ) { - throw new Exception('Invalid data.', 73); + throw new TranslatedException(self::INVALID_DATA_ERROR, 73); } } } diff --git a/lib/Persistence/TrafficLimiter.php b/lib/Persistence/TrafficLimiter.php index 60977f5d..c94bfdb2 100644 --- a/lib/Persistence/TrafficLimiter.php +++ b/lib/Persistence/TrafficLimiter.php @@ -12,11 +12,10 @@ namespace PrivateBin\Persistence; -use Exception; use IPLib\Factory; use IPLib\ParseStringFlag; use PrivateBin\Configuration; -use PrivateBin\I18n; +use PrivateBin\TranslatedException; /** * TrafficLimiter @@ -167,7 +166,7 @@ class TrafficLimiter extends AbstractPersistence * * @access public * @static - * @throws Exception + * @throws TranslatedException * @return true */ public static function canPass() @@ -181,7 +180,7 @@ class TrafficLimiter extends AbstractPersistence return true; } } - throw new Exception(I18n::_('Your IP is not authorized to create documents.')); + throw new TranslatedException('Your IP is not authorized to create documents.'); } // disable limits if set to less then 1 @@ -210,7 +209,7 @@ class TrafficLimiter extends AbstractPersistence } return true; } - throw new Exception(I18n::_( + throw new TranslatedException(array( 'Please wait %d seconds between each post.', self::$_limit )); diff --git a/lib/TranslatedException.php b/lib/TranslatedException.php new file mode 100644 index 00000000..bbadec43 --- /dev/null +++ b/lib/TranslatedException.php @@ -0,0 +1,36 @@ + $baseDir . '/lib/Proxy/YourlsProxy.php', 'PrivateBin\\Request' => $baseDir . '/lib/Request.php', 'PrivateBin\\TemplateSwitcher' => $baseDir . '/lib/TemplateSwitcher.php', + 'PrivateBin\\TranslatedException' => $baseDir . '/lib/TranslatedException.php', 'PrivateBin\\View' => $baseDir . '/lib/View.php', 'PrivateBin\\Vizhash16x16' => $baseDir . '/lib/Vizhash16x16.php', 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index e9194d01..aa7dd9e8 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -139,6 +139,7 @@ class ComposerStaticInitDontChange 'PrivateBin\\Proxy\\YourlsProxy' => __DIR__ . '/../..' . '/lib/Proxy/YourlsProxy.php', 'PrivateBin\\Request' => __DIR__ . '/../..' . '/lib/Request.php', 'PrivateBin\\TemplateSwitcher' => __DIR__ . '/../..' . '/lib/TemplateSwitcher.php', + 'PrivateBin\\TranslatedException' => __DIR__ . '/../..' . '/lib/TranslatedException.php', 'PrivateBin\\View' => __DIR__ . '/../..' . '/lib/View.php', 'PrivateBin\\Vizhash16x16' => __DIR__ . '/../..' . '/lib/Vizhash16x16.php', 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 56dccd55..c9befe93 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'privatebin/privatebin', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '06496a1b0e975b79c5a7abc0bd54b492ca264640', + 'reference' => 'a051c4bd6b71fd56f0d7fc235e815dafc8eb54ea', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -31,7 +31,7 @@ 'privatebin/privatebin' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '06496a1b0e975b79c5a7abc0bd54b492ca264640', + 'reference' => 'a051c4bd6b71fd56f0d7fc235e815dafc8eb54ea', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), From 3a23117ebfcfaf6fddfda82f73789df89e2e6adf Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 19 Nov 2025 09:36:08 +0100 Subject: [PATCH 48/80] Refactored translation of exception messages --- CHANGELOG.md | 4 +- composer.lock | 24 ++++----- lib/Configuration.php | 2 +- lib/Controller.php | 28 +++++------ lib/Data/Database.php | 55 ++++++++++++++------- lib/Data/Filesystem.php | 20 ++++---- lib/Data/GoogleCloudStorage.php | 8 ++- lib/Data/S3Storage.php | 11 +++-- lib/Exception/JsonException.php | 37 ++++++++++++++ lib/{ => Exception}/TranslatedException.php | 2 +- lib/I18n.php | 2 + lib/Json.php | 21 +++----- lib/Model/AbstractModel.php | 2 +- lib/Model/Comment.php | 2 +- lib/Model/Paste.php | 2 +- lib/Persistence/TrafficLimiter.php | 2 +- lib/Proxy/AbstractProxy.php | 4 +- lib/Proxy/ShlinkProxy.php | 18 ++++--- lib/Request.php | 4 +- tpl/bootstrap.php | 19 ++++--- tpl/bootstrap5.php | 19 ++++--- vendor/composer/autoload_classmap.php | 3 +- vendor/composer/autoload_static.php | 3 +- vendor/composer/installed.php | 4 +- 24 files changed, 186 insertions(+), 110 deletions(-) create mode 100644 lib/Exception/JsonException.php rename lib/{ => Exception}/TranslatedException.php (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7977c0d..8002155b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # PrivateBin version history ## 2.0.4 (not yet released) -* CHANGED: Deduplicate JSON error message translations. +* CHANGED: Deduplicate JSON error message translations +* CHANGED: Refactored translation of exception messages +* FIXED: Some exceptions not getting translated ## 1.7.9 (2025-11-13) * CHANGED: Upgrading libraries to: base-x 5.0.1, bootstrap 5.3.8, DOMpurify 3.2.7, ip-lib 1.21.0 & kjua 0.10.0 diff --git a/composer.lock b/composer.lock index dac377f9..a7a66ba0 100644 --- a/composer.lock +++ b/composer.lock @@ -397,16 +397,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -449,9 +449,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/manifest", @@ -2014,16 +2014,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", "shasum": "" }, "require": { @@ -2052,7 +2052,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.0" }, "funding": [ { @@ -2060,7 +2060,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-13T13:44:09+00:00" } ], "aliases": [], diff --git a/lib/Configuration.php b/lib/Configuration.php index c4651f8b..7836c3ec 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -12,7 +12,7 @@ namespace PrivateBin; use Exception; -use PrivateBin\TranslatedException; +use PrivateBin\Exception\TranslatedException; /** * Configuration diff --git a/lib/Controller.php b/lib/Controller.php index 2e056db6..056647de 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -12,12 +12,13 @@ namespace PrivateBin; use Exception; +use PrivateBin\Exception\JsonException; +use PrivateBin\Exception\TranslatedException; use PrivateBin\Persistence\ServerSalt; use PrivateBin\Persistence\TrafficLimiter; use PrivateBin\Proxy\AbstractProxy; use PrivateBin\Proxy\ShlinkProxy; use PrivateBin\Proxy\YourlsProxy; -use PrivateBin\TranslatedException; /** * Controller @@ -308,11 +309,10 @@ class Controller $comment = $paste->getComment($data['parentid']); $comment->setData($data); $comment->store(); - } catch (TranslatedException $e) { + $this->_json_result($comment->getId()); + } catch (Exception $e) { $this->_json_error($e->getMessage()); - return; } - $this->_json_result($comment->getId()); } else { $this->_json_error(I18n::_('Invalid data.')); } @@ -321,21 +321,13 @@ class Controller else { try { $this->_model->purge(); - } catch (Exception $e) { // JSON error!!! - error_log('Error purging documents: ' . $e->getMessage() . PHP_EOL . - 'Use the administration scripts statistics to find ' . - 'damaged paste IDs and either delete them or restore them ' . - 'from backup.'); - } - $paste = $this->_model->getPaste(); - try { + $paste = $this->_model->getPaste(); $paste->setData($data); $paste->store(); - } catch (TranslatedException $e) { + $this->_json_result($paste->getId(), array('deletetoken' => $paste->getDeleteToken())); + } catch (Exception $e) { $this->_json_error($e->getMessage()); - return; } - $this->_json_result($paste->getId(), array('deletetoken' => $paste->getDeleteToken())); } } @@ -365,7 +357,7 @@ class Controller } else { $this->_error = self::GENERIC_ERROR; } - } catch (Exception $e) { + } catch (TranslatedException $e) { $this->_error = $e->getMessage(); } if ($this->_request->isJsonApiCall()) { @@ -470,7 +462,7 @@ class Controller } $page->assign('BASEPATH', I18n::_($this->_conf->getKey('basepath'))); $page->assign('STATUS', I18n::_($this->_status)); - $page->assign('ISDELETED', I18n::_(json_encode($this->_is_deleted))); + $page->assign('ISDELETED', $this->_is_deleted); $page->assign('VERSION', self::VERSION); $page->assign('DISCUSSION', $this->_conf->getKey('discussion')); $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion')); @@ -546,6 +538,7 @@ class Controller * * @access private * @param string $error + * @throws JsonException */ private function _json_error($error) { @@ -562,6 +555,7 @@ class Controller * @access private * @param string $dataid * @param array $other + * @throws JsonException */ private function _json_result($dataid, $other = array()) { diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 0fd42dcc..108278a5 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -15,6 +15,7 @@ use Exception; use PDO; use PDOException; use PrivateBin\Controller; +use PrivateBin\Exception\JsonException; use PrivateBin\Json; /** @@ -179,18 +180,24 @@ class Database extends AbstractData 'SELECT * FROM "' . $this->_sanitizeIdentifier('paste') . '" WHERE "dataid" = ?', array($pasteid), true ); - } catch (Exception $e) { + } catch (PDOException $e) { $row = false; } if ($row === false) { return false; } // create array - $paste = Json::decode($row['data']); + try { + $paste = Json::decode($row['data']); + } catch (JsonException $e) { + error_log('Error while reading a paste from the database: ' . $e->getMessage()); + $paste = array(); + } try { $paste['meta'] = Json::decode($row['meta']); - } catch (Exception $e) { + } catch (JsonException $e) { + error_log('Error while reading a paste from the database: ' . $e->getMessage()); $paste['meta'] = array(); } $expire_date = (int) $row['expiredate']; @@ -233,7 +240,7 @@ class Database extends AbstractData 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . '" WHERE "dataid" = ?', array($pasteid), true ); - } catch (Exception $e) { + } catch (PDOException $e) { return false; } return (bool) $row; @@ -253,7 +260,7 @@ class Database extends AbstractData { try { $data = Json::encode($comment); - } catch (Exception $e) { + } catch (JsonException $e) { error_log('Error while attempting to insert a comment into the database: ' . $e->getMessage()); return false; } @@ -274,7 +281,7 @@ class Database extends AbstractData $meta['created'], ) ); - } catch (Exception $e) { + } catch (PDOException $e) { error_log('Error while attempting to insert a comment into the database: ' . $e->getMessage()); return false; } @@ -298,8 +305,14 @@ class Database extends AbstractData $comments = array(); if (count($rows)) { foreach ($rows as $row) { + try { + $data = Json::decode($row['data']); + } catch (JsonException $e) { + error_log('Error while reading a comment from the database: ' . $e->getMessage()); + $data = array(); + } $i = $this->getOpenSlot($comments, (int) $row['postdate']); - $comments[$i] = Json::decode($row['data']); + $comments[$i] = $data; $comments[$i]['id'] = $row['dataid']; $comments[$i]['parentid'] = $row['parentid']; $comments[$i]['meta'] = array('created' => (int) $row['postdate']); @@ -329,7 +342,7 @@ class Database extends AbstractData '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?', array($pasteid, $parentid, $commentid), true ); - } catch (Exception $e) { + } catch (PDOException $e) { return false; } } @@ -349,7 +362,8 @@ class Database extends AbstractData $this->_last_cache[$key] = $value; try { $value = Json::encode($this->_last_cache); - } catch (Exception $e) { + } catch (JsonException $e) { + error_log('Error encoding JSON for table "config", row "traffic_limiter": ' . $e->getMessage()); return false; } } @@ -393,7 +407,8 @@ class Database extends AbstractData if ($value && $namespace === 'traffic_limiter') { try { $this->_last_cache = Json::decode($value); - } catch (Exception $e) { + } catch (JsonException $e) { + error_log('Error decoding JSON from table "config", row "traffic_limiter": ' . $e->getMessage()); $this->_last_cache = array(); } if (array_key_exists($key, $this->_last_cache)) { @@ -412,13 +427,18 @@ class Database extends AbstractData */ protected function _getExpiredPastes($batchsize) { - $statement = $this->_db->prepare( - 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . - '" WHERE "expiredate" < ? AND "expiredate" != ? ' . - ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?') - ); - $statement->execute(array(time(), 0, $batchsize)); - return $statement->fetchAll(PDO::FETCH_COLUMN, 0); + try { + $statement = $this->_db->prepare( + 'SELECT "dataid" FROM "' . $this->_sanitizeIdentifier('paste') . + '" WHERE "expiredate" < ? AND "expiredate" != ? ' . + ($this->_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?') + ); + $statement->execute(array(time(), 0, $batchsize)); + return $statement->fetchAll(PDO::FETCH_COLUMN, 0); + } catch (PDOException $e) { + error_log('Error while attempting to find expired pastes in the database: ' . $e->getMessage()); + return array(); + } } /** @@ -552,6 +572,7 @@ class Database extends AbstractData '" WHERE "id" = ?', array($key), true ); } catch (PDOException $e) { + error_log('Error while attempting to fetch configuration key "' . $key . '" in the database: ' . $e->getMessage()); return ''; } return $row ? $row['value'] : ''; diff --git a/lib/Data/Filesystem.php b/lib/Data/Filesystem.php index e4377a9f..751980ab 100644 --- a/lib/Data/Filesystem.php +++ b/lib/Data/Filesystem.php @@ -11,8 +11,8 @@ namespace PrivateBin\Data; -use Exception; use GlobIterator; +use PrivateBin\Exception\JsonException; use PrivateBin\Json; /** @@ -104,13 +104,10 @@ class Filesystem extends AbstractData */ public function read($pasteid) { - if ( - !$this->exists($pasteid) || - !$paste = $this->_get($this->_dataid2path($pasteid) . $pasteid . '.php') - ) { - return false; + if ($this->exists($pasteid)) { + return $this->_get($this->_dataid2path($pasteid) . $pasteid . '.php'); } - return $paste; + return false; } /** @@ -346,7 +343,12 @@ class Filesystem extends AbstractData file_get_contents($filename), strlen(self::PROTECTION_LINE . PHP_EOL) ); - return Json::decode($data); + try { + return Json::decode($data); + } catch (JsonException $e) { + error_log('Error decoding JSON from "' . $filename . '": ' . $e->getMessage()); + return false; + } } /** @@ -450,7 +452,7 @@ class Filesystem extends AbstractData $filename, self::PROTECTION_LINE . PHP_EOL . Json::encode($data) ); - } catch (Exception $e) { + } catch (JsonException $e) { error_log('Error while trying to store data to the filesystem at path "' . $filename . '": ' . $e->getMessage()); return false; } diff --git a/lib/Data/GoogleCloudStorage.php b/lib/Data/GoogleCloudStorage.php index 9f64abe5..2ebfb7fe 100644 --- a/lib/Data/GoogleCloudStorage.php +++ b/lib/Data/GoogleCloudStorage.php @@ -15,6 +15,7 @@ use Exception; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\StorageClient; +use PrivateBin\Exception\JsonException; use PrivateBin\Json; class GoogleCloudStorage extends AbstractData @@ -219,7 +220,12 @@ class GoogleCloudStorage extends AbstractData try { foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) { $data = $this->_bucket->object($key->name())->downloadAsString(); - $comment = Json::decode($data); + try { + $comment = Json::decode($data); + } catch (JsonException $e) { + error_log('failed to read comment from ' . $key->name() . ', ' . $e->getMessage()); + $comment = array(); + } $comment['id'] = basename($key->name()); $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']); $comments[$slot] = $comment; diff --git a/lib/Data/S3Storage.php b/lib/Data/S3Storage.php index 526a9ec2..575f90f4 100644 --- a/lib/Data/S3Storage.php +++ b/lib/Data/S3Storage.php @@ -37,6 +37,7 @@ namespace PrivateBin\Data; use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; +use PrivateBin\Exception\JsonException; use PrivateBin\Json; class S3Storage extends AbstractData @@ -177,12 +178,14 @@ class S3Storage extends AbstractData 'ContentType' => 'application/json', 'Metadata' => $metadata, )); + return true; } catch (S3Exception $e) { error_log('failed to upload ' . $key . ' to ' . $this->_bucket . ', ' . trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); - return false; + } catch (JsonException $e) { + error_log('failed to JSON encode ' . $key . ', ' . $e->getMessage()); } - return true; + return false; } /** @@ -212,8 +215,10 @@ class S3Storage extends AbstractData } catch (S3Exception $e) { error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket . ', ' . trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); - return false; + } catch (JsonException $e) { + error_log('failed to JSON decode ' . $key . ', ' . $e->getMessage()); } + return false; } /** diff --git a/lib/Exception/JsonException.php b/lib/Exception/JsonException.php new file mode 100644 index 00000000..dc844a2d --- /dev/null +++ b/lib/Exception/JsonException.php @@ -0,0 +1,37 @@ +_error = 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.'; $this->logErrorWithClassName('Error calling proxy: ' . $e->getMessage()); return; diff --git a/lib/Proxy/ShlinkProxy.php b/lib/Proxy/ShlinkProxy.php index ee4507cf..639a957c 100644 --- a/lib/Proxy/ShlinkProxy.php +++ b/lib/Proxy/ShlinkProxy.php @@ -12,6 +12,7 @@ namespace PrivateBin\Proxy; use PrivateBin\Configuration; +use PrivateBin\Exception\JsonException; use PrivateBin\Json; /** @@ -48,12 +49,17 @@ class ShlinkProxy extends AbstractProxy 'longUrl' => $link, ); - return array( - 'method' => 'POST', - 'header' => "Content-Type: application/json\r\n" . - 'X-Api-Key: ' . $shlink_api_key . "\r\n", - 'content' => Json::encode($body), - ); + try { + return array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n" . + 'X-Api-Key: ' . $shlink_api_key . "\r\n", + 'content' => Json::encode($body), + ); + } catch (JsonException $e) { + error_log('[' . get_class($this) . '] Error encoding body: ' . $e->getMessage()); + return array(); + } } /** diff --git a/lib/Request.php b/lib/Request.php index 24a083be..fb6fb37c 100644 --- a/lib/Request.php +++ b/lib/Request.php @@ -11,7 +11,7 @@ namespace PrivateBin; -use Exception; +use PrivateBin\Exception\JsonException; use PrivateBin\Model\Paste; /** @@ -113,7 +113,7 @@ class Request try { $data = file_get_contents(self::$_inputStream); $this->_params = Json::decode($data); - } catch (Exception $e) { + } catch (JsonException $e) { // ignore error, $this->_params will remain empty } break; diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index e842065d..b219f713 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -513,16 +513,19 @@ if ($FILEUPLOAD) : -