diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b84ef82c..129a251138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This is a log of major user-visible changes in each phpMyFAQ release. - added support for Flesch readability tests (Thorsten) - added storage abstraction layer with support for local filesystem, and Amazon S3 (Thorsten) - added support for SendGrid, AWS SES, and Mailgun (Thorsten) +- added theme manager with support for multiple themes and theme switching (Thorsten) - added experimental support for API key authentication via OAuth2 (Thorsten) - added experimental per-tenant quota enforcement, and API request rate limits (Thorsten) - improved audit and activity log with comprehensive security event tracking (Thorsten) diff --git a/docs/development.md b/docs/development.md index 29ac168743..81efda476e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -160,12 +160,55 @@ You can then use the `dump` function in your templates. For more detailed information, visit the [Twig documentation](https://twig.symfony.com/doc/). -### 9.2.2 Admin backend templates +### 9.2.3 Admin backend templates The admin backend templates are located in the **assets/templates/admin** directory. Usually, you don't need to modify these templates, but if you want to, you can do so. Please be aware that changes to the admin backend templates can break the functionality of phpMyFAQ. +### 9.2.4 Dynamic template set and theme upload + +phpMyFAQ v4.2 introduced dynamic template set loading and a storage-backed `ThemeManager`. + +- Runtime template set is read from configuration key `layout.templateSet`. +- `TwigWrapper` now validates the requested set and falls back to `default` if the + configured set is invalid or missing on disk. +- Tenant provisioning can skip physical template copies and only persist the + template reference in `faqconfig` (`layout.templateSet`). + +Theme uploads use `phpMyFAQ\Template\ThemeManager`: + +- Input format: ZIP archive. +- Required file: `index.twig` (theme root or `/index.twig`). +- Allowed file extensions: + `.twig`, `.css`, `.js`, `.json`, `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp`, `.gif`, + `.woff`, `.woff2`, `.ttf`, `.otf`. +- Upload target: `StorageInterface` at `//...` + (default root path: `themes`). +- Activation: `layout.templateSet` is updated via `ThemeManager::activateTheme()`. + +Example usage: + +```php +use phpMyFAQ\Template\ThemeManager; + +$themeManager = new ThemeManager($configuration, $storage, 'themes'); +$themeManager->uploadTheme('tenant-theme', '/tmp/tenant-theme.zip'); +$themeManager->activateTheme('tenant-theme'); +``` + +Admin UI: + +- Open `/admin/configuration` +- Use the `layout` tab +- Upload ZIP in the `layout` tab +- Select the template in `layout.templateSet` and save configuration + +Security checks: + +- Requires `CONFIGURATION_EDIT` permission +- CSRF token (`theme-manager`) is required for upload/activation + ## 9.3 Themes The default CSS theme is located in the **assets/templates/default** directory and is stored in the file **theme.css**. diff --git a/phpmyfaq/admin/assets/src/api/configuration.ts b/phpmyfaq/admin/assets/src/api/configuration.ts index 689e317674..1cca798924 100644 --- a/phpmyfaq/admin/assets/src/api/configuration.ts +++ b/phpmyfaq/admin/assets/src/api/configuration.ts @@ -146,3 +146,10 @@ export const saveConfiguration = async (data: FormData): Promise => { body: data, })) as Response; }; + +export const uploadThemeArchive = async (data: FormData): Promise => { + return (await fetchJson('api/configuration/themes/upload', { + method: 'POST', + body: data, + })) as Response; +}; diff --git a/phpmyfaq/admin/assets/src/configuration/configuration.ts b/phpmyfaq/admin/assets/src/configuration/configuration.ts index 74161a8e6d..27a61975c0 100644 --- a/phpmyfaq/admin/assets/src/configuration/configuration.ts +++ b/phpmyfaq/admin/assets/src/configuration/configuration.ts @@ -28,6 +28,7 @@ import { fetchTemplates, fetchTranslations, fetchTranslationProvider, + uploadThemeArchive, saveConfiguration, } from '../api'; import { handleWebPush } from './webpush'; @@ -51,6 +52,7 @@ export const handleConfiguration = async (): Promise => { break; case '#layout': await handleTemplates(); + await handleThemes(); break; case '#records': await handleFaqsSortingKeys(); @@ -270,6 +272,26 @@ export const handleMailProvider = async (): Promise => { } }; +export const handleThemes = async (): Promise => { + const uploadForm = document.getElementById('theme-upload-form') as HTMLFormElement | null; + + if (uploadForm) { + uploadForm.addEventListener('submit', async (event: Event): Promise => { + event.preventDefault(); + + const response = (await uploadThemeArchive(new FormData(uploadForm))) as unknown as Response; + if (typeof response.success === 'string') { + pushNotification(response.success); + await handleConfigurationTab('#layout'); + await handleTemplates(); + await handleThemes(); + } else { + pushErrorNotification(response.error || 'Theme upload failed.'); + } + }); + } +}; + export const handleConfigurationTab = async (target: string): Promise => { const languageElement = document.getElementById('pmf-language') as HTMLInputElement; if (!languageElement) { diff --git a/phpmyfaq/assets/templates/admin/configuration/tab-list.twig b/phpmyfaq/assets/templates/admin/configuration/tab-list.twig index d73100c1d2..af91109b21 100644 --- a/phpmyfaq/assets/templates/admin/configuration/tab-list.twig +++ b/phpmyfaq/assets/templates/admin/configuration/tab-list.twig @@ -20,6 +20,13 @@ {% endfor %} +{% if mode == 'layout' %} + {% include '@admin/configuration/themes.upload.twig' with { + csrfToken: themeCsrfToken, + activeTheme: activeTheme + } only %} +{% endif %} +