Explorar el Código

Merge pull request #1501 from Ribas160/allow_change_template

Added the ability to web users to change the theme
El RIDO hace 1 año
padre
commit
c32c74a630
Se han modificado 48 ficheros con 430 adiciones y 47 borrados
  1. 1 0
      CHANGELOG.md
  2. 1 1
      CREDITS.md
  3. 20 4
      cfg/conf.sample.php
  4. 2 1
      i18n/ar.json
  5. 2 1
      i18n/bg.json
  6. 2 1
      i18n/ca.json
  7. 2 1
      i18n/co.json
  8. 2 1
      i18n/cs.json
  9. 2 1
      i18n/de.json
  10. 2 1
      i18n/el.json
  11. 2 1
      i18n/en.json
  12. 2 1
      i18n/es.json
  13. 2 1
      i18n/et.json
  14. 2 1
      i18n/fi.json
  15. 2 1
      i18n/fr.json
  16. 2 1
      i18n/he.json
  17. 2 1
      i18n/hi.json
  18. 2 1
      i18n/hu.json
  19. 2 1
      i18n/id.json
  20. 2 1
      i18n/it.json
  21. 2 1
      i18n/ja.json
  22. 2 1
      i18n/jbo.json
  23. 2 1
      i18n/ko.json
  24. 2 1
      i18n/ku.json
  25. 2 1
      i18n/la.json
  26. 2 1
      i18n/lt.json
  27. 2 1
      i18n/nl.json
  28. 2 1
      i18n/no.json
  29. 2 1
      i18n/oc.json
  30. 2 1
      i18n/pl.json
  31. 2 1
      i18n/pt.json
  32. 2 1
      i18n/ro.json
  33. 2 1
      i18n/ru.json
  34. 2 1
      i18n/sk.json
  35. 2 1
      i18n/sl.json
  36. 2 1
      i18n/sv.json
  37. 2 1
      i18n/th.json
  38. 2 1
      i18n/tr.json
  39. 2 1
      i18n/uk.json
  40. 2 1
      i18n/zh.json
  41. 26 2
      js/privatebin.js
  42. 12 1
      lib/Configuration.php
  43. 44 2
      lib/Controller.php
  44. 120 0
      lib/TemplateSwitcher.php
  45. 22 0
      tpl/bootstrap.php
  46. 24 0
      tpl/bootstrap5.php
  47. 21 0
      tpl/page.php
  48. 65 0
      tst/TemplateSwitcherTest.php

+ 1 - 0
CHANGELOG.md

@@ -1,6 +1,7 @@
 # PrivateBin version history
 
 ## 1.7.7 (not yet released)
+* ADDED: Switching templates using the web ui (#13)
 * CHANGED: Upgrading libraries to: ip-lib 1.20.0
 
 ## 1.7.6 (2025-02-01)

+ 1 - 1
CREDITS.md

@@ -33,7 +33,7 @@
 * Mounir Idrassi & J. Mozdzen - secure YOURLS integration
 * Felipe Nakandakari - enabled AWS SDK to use default credential provider chain in the S3 Storage backend
 * Aaron Sherber - cache control headers for API calls & use of `shortenviayourls` in query parameters
-* Mikhail Romanov - copying to clipboard, UI/UX improvements
+* Mikhail Romanov - copying to clipboard, UI/UX improvements, templates switching
 
 ## Translations
 * Hexalyse - French

+ 20 - 4
cfg/conf.sample.php

@@ -42,13 +42,18 @@ defaultformatter = "plaintext"
 ; size limit per paste or comment in bytes, defaults to 10 Mebibytes
 sizelimit = 10485760
 
-; template to include, default is "bootstrap" (tpl/bootstrap.php), also
+; by default PrivateBin use "bootstrap" template (tpl/bootstrap.php).
+; Optionally you can enable the template selection menu, which uses
+; a session cookie to store the choice until the browser is closed.
+templateselection = false
+
+; set the template your installs defaults to, defaults to "bootstrap" (tpl/bootstrap.php), also
 ; available are "page" (tpl/page.php), the classic ZeroBin style and several
 ; bootstrap variants: "bootstrap-dark", "bootstrap-compact", "bootstrap-page",
-; which can be combined with "-dark" and "-compact" for "bootstrap-dark-page"
-; and finally "bootstrap-compact-page" - previews at:
+; which can be combined with "-dark" and "-compact" for "bootstrap-dark-page",
+; "bootstrap-compact-page" and finally "bootstrap5" (tpl/bootstrap5.php) - previews at:
 ; https://privatebin.info/screenshots.html
-template = "bootstrap"
+; template = "bootstrap"
 
 ; (optional) info text to display
 ; use single, instead of double quotes for HTML attributes
@@ -146,6 +151,17 @@ plaintext = "Plain Text"
 syntaxhighlighting = "Source Code"
 markdown = "Markdown"
 
+[available_templates]
+; List of available for selection templates when "templateselection" option is enabled
+bootstrap
+bootstrap-page
+bootstrap-dark
+bootstrap-dark-page
+bootstrap-compact
+bootstrap-compact-page
+bootstrap5
+page
+
 [traffic]
 ; time limit between calls from the same IP address in seconds
 ; Set this to 0 to disable rate limiting.

+ 2 - 1
i18n/ar.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/bg.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ca.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/co.json

@@ -226,5 +226,6 @@
     "Copy link": "Cupià u liame",
     "Link copied to clipboard": "U liame hè statu cupiatu in u preme’papei",
     "Paste text": "Testu di l’appiccicu",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "U tastu di tabulazione ghjova cum’è un caratteru (Appughjate nant’à <kbd>Ctrl</kbd>+<kbd>m</kbd> o <kbd>Scapp</kbd> per scambià)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "U tastu di tabulazione ghjova cum’è un caratteru (Appughjate nant’à <kbd>Ctrl</kbd>+<kbd>m</kbd> o <kbd>Scapp</kbd> per scambià)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/cs.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/de.json

@@ -226,5 +226,6 @@
     "Copy link": "Verknüpfung kopieren",
     "Link copied to clipboard": "Verknüpfung wurde in die Zwischenablage kopiert.",
     "Paste text": "Text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortaste als Zeichen interpretieren (Umschalten durch <kbd>Strg</kbd>+<kbd>m</kbd> oder <kbd>Esc</kbd>)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortaste als Zeichen interpretieren (Umschalten durch <kbd>Strg</kbd>+<kbd>m</kbd> oder <kbd>Esc</kbd>)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/el.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/en.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/es.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/et.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/fi.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/fr.json

@@ -226,5 +226,6 @@
     "Copy link": "Copier le lien",
     "Link copied to clipboard": "Lien copié dans le presse-papier",
     "Paste text": "Texte du paste",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "La touche de tabulation sert de caractère (Presser <kbd>Ctrl</kbd>+<kbd>m</kbd> ou <kbd>Esc</kbd> pour basculer)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "La touche de tabulation sert de caractère (Presser <kbd>Ctrl</kbd>+<kbd>m</kbd> ou <kbd>Esc</kbd> pour basculer)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/he.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/hi.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/hu.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/id.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/it.json

@@ -226,5 +226,6 @@
     "Copy link": "Copia il link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Testo del messaggio",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ja.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/jbo.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ko.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ku.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/la.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/lt.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/nl.json

@@ -226,5 +226,6 @@
     "Copy link": "Kopieer link",
     "Link copied to clipboard": "Link gekopieerd naar klembord",
     "Paste text": "Tekst plakken",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortoets dient als teken (gebruik <kbd>Ctrl</kbd>+<kbd>m</kbd> of <kbd>Esc</kbd> om te schakelen)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulatortoets dient als teken (gebruik <kbd>Ctrl</kbd>+<kbd>m</kbd> of <kbd>Esc</kbd> om te schakelen)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/no.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/oc.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/pl.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/pt.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ro.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/ru.json

@@ -226,5 +226,6 @@
     "Copy link": "Скопировать ссылку",
     "Link copied to clipboard": "Ссылка скопирована в буфер обмена",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Тема"
 }

+ 2 - 1
i18n/sk.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/sl.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/sv.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/th.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/tr.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/uk.json

@@ -226,5 +226,6 @@
     "Copy link": "Copy link",
     "Link copied to clipboard": "Link copied to clipboard",
     "Paste text": "Paste text",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)",
+    "Theme": "Theme"
 }

+ 2 - 1
i18n/zh.json

@@ -226,5 +226,6 @@
     "Copy link": "复制链接",
     "Link copied to clipboard": "链接已复制到剪贴板",
     "Paste text": "粘贴文本",
-    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tab 键可作为字符(按 <kbd>Ctrl</kbd>+<kbd>m</kbd> 或 <kbd>Esc</kbd> 切换开关)"
+    "Tabulator key serves as character (Hit <kbd>Ctrl</kbd>+<kbd>m</kbd> or <kbd>Esc</kbd> to toggle)": "Tab 键可作为字符(按 <kbd>Ctrl</kbd>+<kbd>m</kbd> 或 <kbd>Esc</kbd> 切换开关)",
+    "Theme": "Theme"
 }

+ 26 - 2
js/privatebin.js

@@ -3932,7 +3932,26 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          */
         function setLanguage(event)
         {
-            document.cookie = 'lang=' + $(event.target).data('lang') + '; SameSite=Lax; Secure';
+            let lang = $(event.target).data('lang') || event.target.value;
+
+            document.cookie = 'lang=' + lang + '; SameSite=Lax; Secure';
+            window.location.reload();
+            event.preventDefault();
+        }
+
+        /**
+         * save the template in a cookie and reloads the page
+         *
+         * @name TopNav.setTemplate
+         * @private
+         * @function
+         * @param {Event} event
+         */
+        function setTemplate(event)
+        {
+            let template = $(event.target).data('template') || event.target.value;
+
+            document.cookie = 'template=' + template + '; SameSite=Lax; Secure';
             window.location.reload();
             event.preventDefault();
         }
@@ -4624,7 +4643,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             // bootstrap template drop down
             $('#language ul.dropdown-menu li a').click(setLanguage);
             // page template drop down
-            $('#language select option').click(setLanguage);
+            $('#language select').change(setLanguage);
+
+            // bootstrap template drop down
+            $('#template ul.dropdown-menu li a').click(setTemplate);
+            // page template drop down
+            $('#template select').change(setTemplate);
 
             // bind events
             $burnAfterReading.change(changeBurnAfterReading);

+ 12 - 1
lib/Configuration.php

@@ -45,6 +45,7 @@ class Configuration
             'defaultformatter'         => 'plaintext',
             'syntaxhighlightingtheme'  => '',
             'sizelimit'                => 10485760,
+            'templateselection'        => false,
             'template'                 => 'bootstrap',
             'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
             'notice'                   => '',
@@ -77,6 +78,16 @@ class Configuration
             'syntaxhighlighting' => 'Source Code',
             'markdown'           => 'Markdown',
         ),
+        'available_templates' => array(
+            'bootstrap5',
+            'bootstrap',
+            'bootstrap-page',
+            'bootstrap-dark',
+            'bootstrap-dark-page',
+            'bootstrap-compact',
+            'bootstrap-compact-page',
+            'page',
+        ),
         'traffic' => array(
             'limit'     => 10,
             'header'    => '',
@@ -108,7 +119,7 @@ class Configuration
             'js/kjua-0.9.0.js'       => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==',
             'js/legacy.js'           => 'sha512-UxW/TOZKon83n6dk/09GsYKIyeO5LeBHokxyIq+r7KFS5KMBeIB/EM7NrkVYIezwZBaovnyNtY2d9tKFicRlXg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-dTFOmnYqID3wEeDa5hjCSP0pKvBRygjxagW4PyAo4NzhCU4iLRjVzskfG6YScry28zmG40GlAwyfYsm5A3+loQ==',
+            'js/privatebin.js'       => 'sha512-yN0ZaVFrXpiYaeFRcofdCrlHVcw4ZJAXU3K1l6H9rsoY//iLKegX0GdWxRCFWzhd1xO4eDJKCT7qyaA1Vt65bg==',
             'js/purify-3.2.4.js'     => 'sha512-Mu9BqoHURMeycg6AgqTpokUv9guq88pajfaFqz53fx1OxohyROkydXPLEIbdKCQ7EdDs9hgcrYeZ9zTiPQQ4CA==',
             'js/rawinflate-0.3.js'   => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',

+ 44 - 2
lib/Controller.php

@@ -179,7 +179,19 @@ class Controller
         $this->_request = new Request;
         $this->_urlBase = $this->_request->getRequestUri();
 
-        // set default language
+        $this->_setDefaultLanguage();
+        $this->_setDefaultTemplate();
+    }
+
+    /**
+     * Set default language
+     *
+     * @access private
+     */
+    private function _setDefaultLanguage()
+    {
+        $this->_conf = new Configuration;
+
         $lang = $this->_conf->getKey('languagedefault');
         I18n::setLanguageFallback($lang);
         // force default language, if language selection is disabled and a default is set
@@ -189,6 +201,27 @@ class Controller
         }
     }
 
+    /**
+     * Set default template
+     *
+     * @access private
+     */
+    private function _setDefaultTemplate()
+    {
+        $this->_conf = new Configuration;
+
+        $templates = $this->_conf->getSection('available_templates');
+        $template  = $this->_conf->getKey('template');
+        TemplateSwitcher::setAvailableTemplates($templates);
+        TemplateSwitcher::setTemplateFallback($template);
+
+        // force default template, if template selection is disabled and a default is set
+        if (!$this->_conf->getKey('templateselection') && !empty($template)) {
+            $_COOKIE['template'] = $template;
+            setcookie('template', $template, array('SameSite' => 'Lax', 'Secure' => true));
+        }
+    }
+
     /**
      * Turn off browser caching
      *
@@ -400,6 +433,13 @@ class Controller
             setcookie('lang', $languageselection, array('SameSite' => 'Lax', 'Secure' => true));
         }
 
+        // 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));
+        }
+
         // strip policies that are unsupported in meta tag
         $metacspheader = str_replace(
             array(
@@ -438,6 +478,8 @@ class Controller
         $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
         $page->assign('LANGUAGESELECTION', $languageselection);
         $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
+        $page->assign('TEMPLATESELECTION', $templateselection);
+        $page->assign('TEMPLATES', TemplateSwitcher::getAvailableTemplates());
         $page->assign('EXPIRE', $expire);
         $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
         $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
@@ -447,7 +489,7 @@ class Controller
         $page->assign('HTTPSLINK', 'https://' . $this->_request->getHost() . $this->_request->getRequestUri());
         $page->assign('COMPRESSION', $this->_conf->getKey('compression'));
         $page->assign('SRI', $this->_conf->getSection('sri'));
-        $page->draw($this->_conf->getKey('template'));
+        $page->draw(TemplateSwitcher::getTemplate());
     }
 
     /**

+ 120 - 0
lib/TemplateSwitcher.php

@@ -0,0 +1,120 @@
+<?php declare(strict_types=1);
+/**
+ * PrivateBin
+ *
+ * a zero-knowledge paste bin
+ *
+ * @link      https://github.com/PrivateBin/PrivateBin
+ * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
+ * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
+ */
+
+namespace PrivateBin;
+
+/**
+ * TemplateSwitcher
+ *
+ * Provides tool to change application template
+ */
+class TemplateSwitcher
+{
+    /**
+     * template fallback
+     *
+     * @access protected
+     * @static
+     * @var    string
+     */
+    protected static $_templateFallback;
+
+    /**
+     * available templates
+     *
+     * @access protected
+     * @static
+     * @var    array
+     */
+    protected static $_availableTemplates = array();
+
+    /**
+     * set available templates
+     *
+     * @access public
+     * @static
+     * @param  array $templates
+     */
+    public static function setAvailableTemplates(array $templates)
+    {
+        self::$_availableTemplates = $templates;
+    }
+
+    /**
+     * set the default template
+     *
+     * @access public
+     * @static
+     * @param  string $template
+     */
+    public static function setTemplateFallback(string $template)
+    {
+        if (self::isTemplateAvailable($template)) {
+            self::$_templateFallback = $template;
+        }
+    }
+
+    /**
+     * get currently loaded template
+     *
+     * @access public
+     * @static
+     * @return string
+     */
+    public static function getTemplate(): string
+    {
+        $selectedTemplate = self::getSelectedByUserTemplate();
+        return $selectedTemplate ?? self::$_templateFallback;
+    }
+
+    /**
+     * get list of available templates
+     *
+     * @access public
+     * @static
+     * @return array
+     */
+    public static function getAvailableTemplates(): array
+    {
+        return self::$_availableTemplates;
+    }
+
+    /**
+     * check if the provided template is available
+     *
+     * @access public
+     * @static
+     * @return bool
+     */
+    public static function isTemplateAvailable(string $template): bool
+    {
+        return in_array($template, self::getAvailableTemplates());
+    }
+
+    /**
+     * get the template selected by user
+     *
+     * @access private
+     * @static
+     * @return string|null
+     */
+    private static function getSelectedByUserTemplate(): ?string
+    {
+        $selectedTemplate    = null;
+        $templateCookieValue = $_COOKIE['template'] ?? '';
+
+        if (self::isTemplateAvailable($templateCookieValue)) {
+            $selectedTemplate = $templateCookieValue;
+        }
+
+        return $selectedTemplate;
+    }
+}

+ 22 - 0
tpl/bootstrap.php

@@ -457,6 +457,28 @@ if (!empty($LANGUAGESELECTION)) :
 					</li>
 <?php
 endif;
+?>
+<?php
+if (!empty($TEMPLATESELECTION)) :
+?>
+					<li id="template" class="dropdown">
+						<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><?php echo I18n::_('Theme'); ?>: <?php echo $TEMPLATESELECTION; ?> <span class="caret"></span></a>
+						<ul class="dropdown-menu dropdown-menu-right">
+<?php
+    foreach ($TEMPLATES as $value) :
+?>
+							<li>
+								<a href="#" data-template="<?php echo $value; ?>">
+									<?php echo $value; ?>
+								</a>
+							</li>
+<?php
+    endforeach;
+?>
+						</ul>
+					</li>
+<?php
+endif;
 ?>
 				</ul>
 			</div>

+ 24 - 0
tpl/bootstrap5.php

@@ -323,6 +323,30 @@ if (!empty($LANGUAGESELECTION)) :
 						</li>
 <?php
 endif;
+?>
+<?php
+if (!empty($TEMPLATESELECTION)) :
+?>
+						<li id="template" class="nav-item dropdown">
+							<a href="#" class="nav-link dropdown-toggle d-flex align-items-center gap-1" data-bs-toggle="dropdown" role="button" aria-expanded="false">
+								<?php echo I18n::_('Theme'); ?>: <?php echo $TEMPLATESELECTION, PHP_EOL; ?>
+							</a>
+							<ul class="dropdown-menu dropdown-menu-end" role="menu">
+<?php
+    foreach ($TEMPLATES as $value) :
+?>
+								<li>
+									<a href="#" class="dropdown-item" data-template="<?php echo $value; ?>">
+										<?php echo $value; ?>
+									</a>
+								</li>
+<?php
+    endforeach;
+?>
+							</ul>
+						</li>
+<?php
+endif;
 ?>
 					</ul>
 				</div>

+ 21 - 0
tpl/page.php

@@ -222,6 +222,27 @@ if (!empty($LANGUAGESELECTION)):
 					</div>
 <?php
 endif;
+?>
+<?php
+if (!empty($TEMPLATESELECTION)):
+?>
+					<div id="template" class="button"><?php echo I18n::_('Theme'); ?>:
+						<select name="template">
+<?php
+    foreach ($TEMPLATES as $value):
+?>
+							<option data-template="<?php echo $value; ?>" value="<?php echo $value; ?>"<?php
+        if ($value == $TEMPLATESELECTION):
+?> selected="selected"<?php
+        endif;
+?>><?php echo $value; ?></option>
+<?php
+    endforeach;
+?>
+						</select>
+					</div>
+<?php
+endif;
 ?>
 				</div>
 <?php

+ 65 - 0
tst/TemplateSwitcherTest.php

@@ -0,0 +1,65 @@
+<?php declare(strict_types=1);
+
+use PHPUnit\Framework\TestCase;
+use PrivateBin\Configuration;
+use PrivateBin\TemplateSwitcher;
+
+class TemplateSwitcherTest extends TestCase
+{
+    public function testSetTemplateFallback()
+    {
+        $conf = new Configuration;
+
+        $existingTemplateFallback = 'bootstrap-dark';
+        $wrongTemplateFallback    = 'bootstrap-wrong';
+
+        TemplateSwitcher::setAvailableTemplates($conf->getSection('available_templates'));
+        TemplateSwitcher::setTemplateFallback($existingTemplateFallback);
+        $this->assertEquals($existingTemplateFallback, TemplateSwitcher::getTemplate(), 'Correct template fallback');
+
+        TemplateSwitcher::setTemplateFallback($wrongTemplateFallback);
+        $this->assertEquals($existingTemplateFallback, TemplateSwitcher::getTemplate(), 'Wrong template fallback');
+    }
+
+    public function testSetAvailableTemplates()
+    {
+        $conf               = new Configuration;
+        $availableTemplates = $conf->getSection('available_templates');
+
+        TemplateSwitcher::setAvailableTemplates($availableTemplates);
+        $this->assertEquals($availableTemplates, TemplateSwitcher::getAvailableTemplates(), 'Set available templates');
+    }
+
+    public function testGetTemplate()
+    {
+        $defaultTemplateFallback = 'bootstrap';
+        $customTemplate          = 'bootstrap-dark';
+        $customWrongTemplate     = 'bootstrap-wrong';
+
+        TemplateSwitcher::setTemplateFallback($defaultTemplateFallback);
+
+        $_COOKIE['template'] = $customWrongTemplate;
+        $this->assertEquals($defaultTemplateFallback, TemplateSwitcher::getTemplate(), 'Custom wrong template');
+
+        $_COOKIE['template'] = $customTemplate;
+        $this->assertEquals($customTemplate, TemplateSwitcher::getTemplate(), 'Custom template');
+    }
+
+    public function testGetAvailableTemplates()
+    {
+        $this->assertNotEmpty(TemplateSwitcher::getAvailableTemplates(), 'Get available templates');
+    }
+
+    public function testIsTemplateAvailable()
+    {
+        $conf = new Configuration;
+
+        $existingTemplate    = 'bootstrap';
+        $nonExistentTemplate = 'bootstrap-wrong';
+
+        TemplateSwitcher::setAvailableTemplates($conf->getSection('available_templates'));
+
+        $this->assertTrue(TemplateSwitcher::isTemplateAvailable($existingTemplate), 'Existing template');
+        $this->assertFalse(TemplateSwitcher::isTemplateAvailable($nonExistentTemplate), 'Non-existent template');
+    }
+}