diff --git a/CHANGELOG.md b/CHANGELOG.md index 58dee305d..01460435a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Changelog -## [v2.0.0-alpha.5](https://github.com/marcantondahmen/automad/commit/3443ff5f06d1a52661f7f5318ee183248e95a4d3) +## [v2.0.0-alpha.6](https://github.com/marcantondahmen/automad/commit/2f32588222c4829ca3fad8009d9093949f48360d) -Sun, 30 Jun 2024 19:35:04 +0200 +Sun, 15 Sep 2024 19:28:10 +0200 + +### New Features + +- **ui**: improve button loading animation ([7fbb813ef](https://github.com/marcantondahmen/automad/commit/7fbb813ef89b19da0b16662764da3fa914a9aa7c)) +- add customization fields for CSS and JS code and files ([660691c24](https://github.com/marcantondahmen/automad/commit/660691c2459c33750436a57e50ad7d8237b18e47)) +- add support for remote webp images ([0f1885dd9](https://github.com/marcantondahmen/automad/commit/0f1885dd9d1236549d45b90a936e98ed24f39fab)) +- move mail config to a separate file ([b48b25329](https://github.com/marcantondahmen/automad/commit/b48b253292e950dcbd74d8bd81f898e9933c31c2)) + +### Bugfixes + +- **ui**: fix visibility of navbar items on medium size screens ([2f3258822](https://github.com/marcantondahmen/automad/commit/2f32588222c4829ca3fad8009d9093949f48360d)) +- fix processing of nested in-page editing buttons ([4fa5eb0d3](https://github.com/marcantondahmen/automad/commit/4fa5eb0d3cad40ee39338c975eb427b0c68c6674)) + +## [v2.0.0-alpha.5](https://github.com/marcantondahmen/automad/commit/09e8864bdc5a62ba735aa0a7f08d358e92aaa735) + +Sun, 30 Jun 2024 19:41:20 +0200 ### New Features @@ -329,16 +345,3 @@ Mon, 9 Aug 2021 23:21:36 +0200 ### Bugfixes - **ui**: fix updating links to images that belong to the page they are used on ([723a6be37](https://github.com/marcantondahmen/automad/commit/723a6be37fb283fdcd42a5a365e6089509a25139)) - -## [v1.8.2](https://github.com/marcantondahmen/automad/commit/e070a89209c2e90eadb9be4b77580beef6aa75d1) - -Sun, 8 Aug 2021 22:25:34 +0200 - -### New Features - -- **samples**: add pagelist example page ([2a033f4da](https://github.com/marcantondahmen/automad/commit/2a033f4dada04e3d2fd7d24f2d2d6b1bfd04986e)) -- **samples**: add tags and filters to example pages ([d18f2482b](https://github.com/marcantondahmen/automad/commit/d18f2482b2f258d7b42a6609a439769773ffb7f8)) - -### Bugfixes - -- **themes**: fix thumbnail visibility ([45ed2eee5](https://github.com/marcantondahmen/automad/commit/45ed2eee5f1cf0b81148678820b6a796bf4791e2)) diff --git a/README.md b/README.md index 2c5a7cd5e..c32aa1fc7 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,12 @@ In case you quickly want to try out Automad without setting up a server first, j ## Installation -Note that this repository only contains source code. Please follow the instructions below in order to install a fully bundled -version of Automad using [Docker](https://docker.com) or [Composer](https://getcomposer.org). -It is also possible to manually [download](https://github.com/automadcms/automad-dist/archive/refs/heads/master.zip) -and [install](#manual-installation) Automad. +Note that this repository only contains source code. Please follow the instructions below in order to install a fully bundled version of Automad using [Docker](https://github.com/automadcms/automad-docker) or [Composer](https://packagist.org/packages/automad/automad). +It is also possible to manually [download](https://github.com/automadcms/automad-dist/archive/refs/heads/master.zip) and [install](#manual-installation) Automad. ### Composer -The fastest way to get Automad up and running is to use Composer. +The fastest way to get Automad up and running is to use [Composer](https://packagist.org/packages/automad/automad). ```bash composer create-project automad/automad . v2.x-dev @@ -44,7 +42,7 @@ Follow this [guide](https://automad.org/version-2#getting-started) to finish the ### Docker -It is also possible to run Automad in a [Docker](https://hub.docker.com/r/automad/automad) container including **Nginx** and **PHP 8.3**. +It is also possible to run Automad in a [Docker](https://github.com/automadcms/automad-docker) container including **Nginx** and **PHP 8.3**. ```bash docker run -dp 80:80 -v ./app:/app --name mysite automad/automad:v2 @@ -93,6 +91,7 @@ In case you are interested in contributing, the following types of contribution - [Publishing packages](https://automad.org/developer-guide/publishing-packages) like themes or extensions to the Automad package [browser](https://packages.automad.org) - Giving feedback and helping to grow a [community](https://automad.org/discuss) - Reporting bugs or requesting features at [GitHub](https://github.com/marcantondahmen/automad/issues) +- Reporting [security vulnerabilities](https://github.com/marcantondahmen/automad/security) However, I do not exclude at this point using parts of Automad's source in future projects under different licenses. In order to avoid having to ask anybody for permission when doing so, I will not accept any contributions to **this** repository. Please understand that pull requests will therefore be ignored. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e9afc7fb0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,58 @@ +# Reporting Vulnerabilities + +Security should be taken seriously whenever private data and/or the digital distribution of any data is at play. Automad is **no exception** here. + +Whenever you encounter a security vulnerability, please feel free to [report it privately](https://github.com/marcantondahmen/automad/security/advisories/new) and provide the following information: + +- A brief description of the vulnerability +- A use case for an exploit and a valid attack vector +- All required steps in order to enable an attacker to exploit the vulnerability + +> [!IMPORTANT] +> Please note that pull-requests for this repository will be ignored as stated in the README. + +## Quality of Reports + +Unfortunately false positive vulnerability reports pose a substantial threat to cybersecurity since maintainers of open-source projects keep on drowning in reports. This implies that real threats will not get the attention that will be required to handle them properly and with care. + +However, _all reported vulnerabilities are reviewed_ and handled with priority as soon as possible. + +Please note that after an initial triage only reports of exploitable vulnerabilities with realistic attack vectors are followed up. Please make sure that you are famliliar with Automad's architecture and fully understand its implications for security as described below. + +## Architecture + +Automad is a _flat-file_ content management system that doesn't have a database. Content is stored on disk in `.json` files. Pages are only rendered and saved as static `.html` files when content has changed. From a security perspective, this architecture has significant advantages over database driven websites. + +### Users and Roles + +Automad only knows two types of users — _visitors_ and _admins_. Only admins can create, delete or modify content and change settings. Visitors can only view content. + +Only admins have actual user accounts on an Automad installation. They all share the same privileges. Usually there is only a single admin but it is possible to add additional ones via invitations. Visitors have no user account. + +### Sessions + +On every visit of an Automad site, a session is created on the server for both types of users — visitors as well as admins. On the client, a cookie is created that only contains the session id in order to identify a session. The session id and also the cookie itself don't contain any personal data or any data that can be used in order to identify an actual person. + +When a visitor visits the site, also the user's session on the server doesn't contain or store any personal data. In fact it stays empty except a user chooses to persist preferences such as language or color scheme settings and as long as the installation and templates support such features. + +Regarding Automad's core functionality, the session is only used to verify whether a user is signed in as an admin and therefore authorized to edit content — this is not only true for the dashboard but for the entire site in order to enable admins to edit content in the in-page editing mode. + +After successfully being authenticated, the _username_ and a _csrf token_ will be stored in a user's session. During password reset requests a reset token may be stored temporarily as well. Automad itself will not store any other data than the aforementioned. + +### Implications for Security + +In order to fully understand possible attack vectors and the severity of reported vulnerabilities, one has to take the architectural concept, the way sessions work in Automad and the limitation of the visitor role into account. Generally, vulnerabilities can be broken down into two categories — _XSS (cross-site-scripting)_ and _CSRF (cross-site-request-forgery)_. + +#### XSS + +In general, XSS attacks imply that an **unauthorized** user can store malicious code in some kind of data store due to the lack of sanitization of user input. This code is then typically executed in the browser by other users and can therefore be used for stealing user related data such as cookies. Typically forum software or commenting systems are exposed to such attack vectors since anybody can register and post content. In such scenarios a proper sanitization of user input is mandatory. + +In Automad this kind of attacks are technically not possible due to the nature of the underlying architecture. The input of unprivileged users such as visitors is never stored or used in any way to permanently alter the system as it would be the case in a commenting system or forum. + +As previously described, only admins can create, update or delete content. Please note that this also includes the ability to install templates and modify them. An admin is allowed to add executable JavaScript code to a site. It cannot be stressed enough that this ability itself doesn't pose a threat and also is fundamentally different to the nature of an XSS attack. Admins are by design privileged users that on one hand must understand their responsibility and on the other hand need the necessary freedom to actually keep a site running. This concept is not new and applies to almost every system that is connected to the internet. + +Therefore the only type of user that can act as a malicious party are admins. Since visitors have no session data on the server or inside of the cookie, even a hacked admin account can't steal relevant data. This alone renders XSS attacks useless. + +#### CSRF + +In contrast to XSS attacks, CSRF attacks potentially pose a real threat. Automad has standard measures in place in order to prevent CSRF attacks. diff --git a/automad/lang/english.json b/automad/lang/english.json index 8b2c6d21d..04d631a69 100644 --- a/automad/lang/english.json +++ b/automad/lang/english.json @@ -10,8 +10,8 @@ "addWatermarkTitle": "Choose the watermark type", "addedSuccess": "Successfully added", "adjustTab": "Adjust", - "alignLeft": "Align left", "alignCenter": "Align center", + "alignLeft": "Align left", "alignRight": "Align right", "alreadyExists": "already exists!", "annotateTab": "Draw", @@ -20,33 +20,33 @@ "back": "Back", "backgroundColor": "Background Color", "backgroundImage": "Background Image", + "blockquote": "Blockquote", + "blur": "Blur", + "blurTool": "Blur", "bold": "Bold", "borderColor": "Border Color", "borderRadius": "Corner Radius", - "borderWidth": "Border Width", "borderStyle": "Border Style", - "blur": "Blur", - "blurTool": "Blur", - "blockquote": "Blockquote", + "borderWidth": "Border Width", "brightnessTool": "Brightness", "browseFiles": "Browse Files", - "buttonsBlockTitle": "Buttons", - "buttonsBlockSettings": "Settings", "buttonsBlockAlignment": "Edit alignment", "buttonsBlockGap": "Gap between buttons", "buttonsBlockPlaceholder": "Enter text ...", - "caption": "Caption", + "buttonsBlockSettings": "Settings", + "buttonsBlockTitle": "Buttons", "cacheClearedSuccess": "The cache got cleared successfully!", "cacheDisabled": "Caching is disabled", "cacheEnabled": "Caching is enabled", "cachePurgedError": "The cache directory could not be purged!", "cachePurgedSuccess": "The cache directory got purged successfully!", "cancel": "Cancel", + "caption": "Caption", "changesLoseConfirmation": "All changes will be lost", "changesLoseConfirmationHint": "Are you sure you want to continue?", "cinemascope": "Cinemascope", - "classicTv": "Classic TV", "className": "CSS Class", + "classicTv": "Classic TV", "clickToDelete": "Click to delete", "close": "Close", "code": "Source Code", @@ -67,6 +67,10 @@ "currentPassword": "Current Password", "currentPasswordError": "The current password is wrong!", "custom": "Custom", + "customCSS": "Custom CSS", + "customCSSFile": "Custom CSS File", + "customJS": "Custom JavaScript", + "customJSFile": "Custom JavaScript File", "dashboardTitle": "Dashboard", "date": "Date", "debugDisabled": "Debugging is disabled", @@ -74,6 +78,7 @@ "delete": "Delete", "deletePage": "Delete Page", "deleteSelected": "Delete Selected", + "delimiter": "Delimiter", "deteledSuccess": "Successfully removed", "disable": "Disable", "discardImageChanges": "Are you sure you want to close the editor without saving changes to the image?", @@ -83,40 +88,39 @@ "duplicate": "Duplicate", "duplicatePage": "Duplicate Page", "edit": "Edit", - "editorPlaceholder": "Click here to add content", "editFileInfo": "Edit File Info", "editImage": "Edit Image", "editStyle": "Edit Style", + "editorPlaceholder": "Click here to add content", "ellipse": "Ellipse", "ellipseTool": "Ellipse", "email": "Email address", - "emailFrom": "Email sender address", "emailAutomatic": "This email has been sent automatically. Please do not reply to this email.", + "emailFrom": "Email sender address", "emailHello": "Hello", "emailInviteButton": "Create Password Now", "emailInviteSubject": "You have been added as a new user!", "emailInviteText": "a new user account on {} has been created for you. You can use the button below to create your password.", + "emailRequiredError": "Please enter a valid email address", "emailResetPasswordSubject": "Your authentication token", "emailResetPasswordTextBottom": "In case you did not initiate this request yourself, you can safely ignore this message.", "emailResetPasswordTextTop": "the requested authentication token for your account on {} can be found below. You can use that token in order to create a new password for you now.", - "emailRequiredError": "Please enter a valid email address", "enable": "Enable", "extension": "Extension", - "delimiter": "Delimiter", "feedDisabled": "The RSS feed is disabled", "feedEnabled": "The RSS feed is enabled", "fetchingDataError": "Error fetching data from API!", - "fieldsColors": "Colors", "fieldsContent": "Content", + "fieldsCustomize": "Customize", "fieldsSettings": "Settings", "file": "File", - "files": "Files", "fileName": "Filename", - "filelistBlockTitle": "Filelist", + "filelistBlockDefaultFile": "Default", "filelistBlockFile": "Template file", "filelistBlockPattern": "File pattern", - "filelistBlockDefaultFile": "Default", "filelistBlockSortOrder": "Sort order", + "filelistBlockTitle": "Filelist", + "files": "Files", "filterContent": "Filter Content", "filtersTab": "Filters", "finetuneTab": "Finetune", @@ -125,45 +129,45 @@ "fontFamily": "Font family", "fontSize": "Font Size", "forgotPassword": "Forgot Password", - "galleryBlockTitle": "Image Gallery", "galleryBlockLayout": "Layout", - "galleryBlockLayoutColumns": "Column Layout", - "galleryBlockLayoutRows": "Row Layout", + "galleryBlockLayoutCleanBottom": "Clean bottom edge", "galleryBlockLayoutColumnWidth": "Column Width", - "galleryBlockLayoutRowHeight": "Row Height", + "galleryBlockLayoutColumns": "Column Layout", "galleryBlockLayoutGap": "Gap", - "galleryBlockLayoutCleanBottom": "Clean bottom edge", + "galleryBlockLayoutRowHeight": "Row Height", + "galleryBlockLayoutRows": "Row Layout", + "galleryBlockTitle": "Image Gallery", "heading": "Heading", "headings": "Headings", "hidePage": "Hide Page in Navigation", "horizontal": "Horizontal", "hsvTool": "HSV", "hue": "Hue", - "i18nEnabled": "Language routing enabled", "i18nDisabled": "Language routing disabled", + "i18nEnabled": "Language routing enabled", "imageDimensionsHoverTitle": "Saved image size (width x height)", - "imageTool": "Image", - "imageSlideshowBlockTitle": "Image Slideshow", - "imageSlideshowBlockSettings": "Settings", - "imageSlideshowBlockEffect": "Effect", - "imageSlideshowBlockImageWidth": "Image Width", - "imageSlideshowBlockImageHeight": "Image Height", - "imageSlideshowBlockSpaceBetween": "Space between images in pixels", - "imageSlideshowBlockSlidesPerView": "Images per view", - "imageSlideshowBlockLoop": "Continous looping", "imageSlideshowBlockAutoplay": "Autoplay mode", "imageSlideshowBlockBreakpoints": "Number of images based on block size", - "imageSlideshowBlockBreakpointsHelp": "Add breakpoints as pairs of a minimum block size and a number of images per view, separated by a colon. Multiple breakpoints can be separated by whitespace.", "imageSlideshowBlockBreakpointsError": "Please enter valid breakpoints that include block width and image width separate by a colon.", + "imageSlideshowBlockBreakpointsHelp": "Add breakpoints as pairs of a minimum block size and a number of images per view, separated by a colon. Multiple breakpoints can be separated by whitespace.", + "imageSlideshowBlockEffect": "Effect", + "imageSlideshowBlockImageHeight": "Image Height", + "imageSlideshowBlockImageWidth": "Image Width", + "imageSlideshowBlockLoop": "Continous looping", + "imageSlideshowBlockSettings": "Settings", + "imageSlideshowBlockSlidesPerView": "Images per view", + "imageSlideshowBlockSpaceBetween": "Space between images in pixels", + "imageSlideshowBlockTitle": "Image Slideshow", + "imageTool": "Image", "import": "Import", "importFailedError": "The file import has failed!", "importFromUrl": "Import", "importUrl": "Enter a file URL", "importing": "Importing ...", - "indent": "Indent", - "inlineCode": "Inline code", "inPageEdit": "In-Page Edit Mode", "inPagePlaceholder": "Add content here", + "indent": "Indent", + "inlineCode": "Inline code", "insertCodeBlock": "Insert code block", "insertHorizontalLine": "Horizontal line", "insertImage": "Insert image", @@ -191,29 +195,29 @@ "link": "Link", "list": "List", "loading": "Loading...", - "mailBlockTitle": "Mail Form", + "mailBlockAddressFieldLabel": "Address field label", + "mailBlockBodyFieldLabel": "Body field label", "mailBlockDefaultError": "An error occurred while sending mail.", - "mailBlockDefaultSuccess": "The mail has been sent successfully.", "mailBlockDefaultLabelAddress": "Email Address", - "mailBlockDefaultLabelSubject": "Subject", "mailBlockDefaultLabelBody": "Message", "mailBlockDefaultLabelSend": "Send Email", - "mailBlockTo": "Receiving mail address", - "mailBlockAddressFieldLabel": "Address field label", - "mailBlockSubjectFieldLabel": "Subject field label", - "mailBlockBodyFieldLabel": "Body field label", - "mailBlockSendButtonLabel": "Send button label", + "mailBlockDefaultLabelSubject": "Subject", + "mailBlockDefaultSuccess": "The mail has been sent successfully.", "mailBlockError": "Error message", + "mailBlockSendButtonLabel": "Send button label", + "mailBlockSubjectFieldLabel": "Subject field label", "mailBlockSuccess": "Success message", + "mailBlockTitle": "Mail Form", + "mailBlockTo": "Receiving mail address", "matchRowHeight": "Match Content to Row Height", "missingPageTitleError": "Title missing!", "missingTargetPageError": "Please select a page as destination!", "missingUrlError": "Please enter a valid URL!", "more": "More", "moreThemes": "Get more themes here", + "moveDown": "Move down", "movePage": "Move Page", "moveUp": "Move up", - "moveDown": "Move down", "name": "Name", "nameIsRequired": "Name is required.", "new": "New", @@ -246,13 +250,12 @@ "packagesUpdatingAll": "Updating packages ...", "packagistConnectionError": "Can't connect to the Packagist API", "padding": "Padding", - "paddingHorizontal": "Horizontal Padding", - "paddingVertical": "Vertical Padding", "paddingBottom": "Spacing Bottom", + "paddingHorizontal": "Horizontal Padding", "paddingLeft": "Spacing Left", "paddingRight": "Spacing Right", "paddingTop": "Spacing Top", - "pageImages": "Page Images", + "paddingVertical": "Vertical Padding", "pageHistory": "Restore a Previous Version", "pageHistoryNoRevision": "This page has no revisions yet.", "pageHistoryNotFound": "Revision not found!", @@ -260,24 +263,25 @@ "pageHistoryRestoreHomeConfirm": "Do you want to restore the page in place?", "pageHistoryRestoreHomeText": "Restore an older version of the homepage in place.", "pageHistoryRestoreText": "Restore an older version of the current page as a copy.", - "pagelistBlockTitle": "Pagelist", + "pageImages": "Page Images", + "pageNotFoundError": "Page Not Found!", + "pageSlug": "Directory Name", + "pageTags": "Tags (Separate multiple tags by comma or tab)", + "pageTemplate": "Template", + "pagelistBlockContext": "Parent page", "pagelistBlockDefaultFile": "Default", + "pagelistBlockExcludeCurrent": "Exclude this page", + "pagelistBlockExcludeHidden": "Exclude hidden pages", "pagelistBlockFile": "Template file", - "pagelistBlockContext": "Parent page", - "pagelistBlockSortField": "Sort by", - "pagelistBlockSortOrder": "Sort order", - "pagelistBlockType": "Type", "pagelistBlockFilter": "Filter by tag", - "pagelistBlockExcludeHidden": "Exclude hidden pages", - "pagelistBlockExcludeCurrent": "Exclude this page", "pagelistBlockFilterByTemplate": "Filter by template", "pagelistBlockFilterByUrl": "Filter by URL", - "pagelistBlockOffset": "Offset", "pagelistBlockLimit": "Limit", - "pageNotFoundError": "Page Not Found!", - "pageSlug": "Directory Name", - "pageTags": "Tags (Separate multiple tags by comma or tab)", - "pageTemplate": "Template", + "pagelistBlockOffset": "Offset", + "pagelistBlockSortField": "Sort by", + "pagelistBlockSortOrder": "Sort order", + "pagelistBlockTitle": "Pagelist", + "pagelistBlockType": "Type", "paragraph": "Paragraph", "password": "Password", "passwordChangedSuccess": "Your password has been changed successfully!", @@ -350,9 +354,9 @@ "signedOut": "Signed out", "signedOutSuccess": "You have been successfully signed out.", "size": "Size", - "snippetBlockTitle": "Snippet", "snippetBlockFile": "Snippet file", "snippetBlockSnippet": "Code snippet", + "snippetBlockTitle": "Snippet", "square": "Square", "strikeThrough": "Strike through", "stroke": "Stroke", @@ -375,21 +379,21 @@ "systemDebugInfo": "When debugging is enabled, all of Automad's processes will be logged to your browser's console as well as to .json files inside the temporary directory. Debugging is only needed for development or troubleshooting and should be disabled in all other cases.", "systemI18n": "Internationalization", "systemI18nCardInfo": "Enabled and configure multilingual content", - "systemI18nEnable": "Language Routing", "systemI18nDefault": "Default Locale", + "systemI18nEnable": "Language Routing", "systemI18nInfo": "Internationalization can be enabled in order to route visitors to pages that are served in the language that matches their locale. Adding languages can be achieved by grouping pages of a particular language under a top-level page that has a two letter language code as slug such as /en or /de. The first top-level page serves as fallback for all languages that are not available on a site.", "systemLanguage": "Language", "systemLanguageCardInfo": "Change the language for the Automad user interface", "systemLanguageInfo": "There are several languages available for the Automad user interface. The translations are created automatically. This feature is still in an experimental phase and therefore some translations might not be accurate.", "systemMail": "Mail", - "systemMailInfo": "You can configure how emails are sent for registration, account recovery or from contact forms on the website. By default, Automad uses PHP's sendmail function for this. However, it is recommended to send emails via an SMTP server, whose access data can be entered below.", "systemMailCardInfo": "Configure how emails are sent by your server", "systemMailConfigError": "Error saving mail configuration.", - "systemMailSmtpPasswordPlaceholder": "Leave this field empty in order to keep the existing password", + "systemMailInfo": "You can configure how emails are sent for registration, account recovery or from contact forms on the website. By default, Automad uses PHP's sendmail function for this. However, it is recommended to send emails via an SMTP server, whose access data can be entered below.", "systemMailReset": "Do you want to reset the current email configuration?", "systemMailSendTest": "Send Test Email", - "systemMailSendTestSuccess": "A test email was successfully sent to", "systemMailSendTestError": "Error sending test email.", + "systemMailSendTestSuccess": "A test email was successfully sent to", + "systemMailSmtpPasswordPlaceholder": "Leave this field empty in order to keep the existing password", "systemRssFeed": "RSS Feed", "systemRssFeedCardInfo": "Enable and configure the RSS feed for your site", "systemRssFeedEnable": "RSS Feed", @@ -431,15 +435,15 @@ "systemUsersSendInvitationSuccess": "The invitation has been sent successfully.", "systemUsersYou": "You", "table": "Table", - "tableWithHeadings": "With headings", - "tableWithoutHeadings": "Without headings", "tableAddColumnLeft": "Add column to left", "tableAddColumnRight": "Add column to right", - "tableDeleteColumn": "Delete column", - "tableOfContentsBlockTitle": "Table of Contents", "tableAddRowAbove": "Add row above", "tableAddRowBelow": "Add row below", + "tableDeleteColumn": "Delete column", "tableDeleteRow": "Delete row", + "tableOfContentsBlockTitle": "Table of Contents", + "tableWithHeadings": "With headings", + "tableWithoutHeadings": "Without headings", "textAlignment": "Text alignment", "textColor": "Text Color", "textSpacings": "Text spacings", @@ -453,12 +457,12 @@ "trashIsEmpty": "No pages found", "trashPermanentlyDelete": "Permanently delete", "trashPermanentlyDeleteConfirm": "Do you want to permanently delete this page?", - "trashTitle": "Trash", "trashRestore": "Restore page", + "trashTitle": "Trash", "tuneOrMove": "Click to tune or drag to move", - "underline": "Underline", "unFlipX": "Un-Flip X", "unFlipY": "Un-Flip Y", + "underline": "Underline", "undoTitle": "Undo last operation", "unorderedList": "Unordered list", "unsupportedFileTypeError": "Unsupported file type", diff --git a/automad/src/client/admin/components/Fields/Code.ts b/automad/src/client/admin/components/Fields/Code.ts new file mode 100644 index 000000000..d9e6a825e --- /dev/null +++ b/automad/src/client/admin/components/Fields/Code.ts @@ -0,0 +1,118 @@ +/* + * .... + * .: '':. + * :::: ':.. + * ::. ''.. + * .:'.. ..':.:::' . :. '':. + * :. '' '' '. ::::.. ..: + * ::::. ..':.. .'''::::: . + * :::::::.. '..:::: :. :::: : + * ::'':::::::. ':::.'':.:::: : + * :.. ''::::::....': '':: : + * :::::. '::::: : .. '' . + * .''::::::::... ':::.'' ..'' :.''''. + * :..:::''::::: :::::...:'' :..: + * ::::::. ':::: :::::::: ..:: . + * ::::::::.:::: :::::::: :'':.:: .'' + * ::: '::::::::.' ''::::: :.' '': : + * ::: :::::::::..' :::: ::...' . + * ::: .:::::::::: :::: :::: .:' + * '::' ''::::::: :::: : :: : + * ':::: :::: :'' .: + * :::: :::: ..'' + * :::: ..:::: .:'' + * '''' ''''' + * + * + * AUTOMAD + * + * Copyright (c) 2024 by Marc Anton Dahmen + * https://marcdahmen.de + * + * Licensed under the MIT license. + */ + +import { create, CSS, FieldTag, FormDataProviders } from '@/admin/core'; +import { CodeEditor } from '@/admin/core/code'; +import { UndoValue } from '@/admin/types'; +import { BaseFieldComponent } from './BaseField'; + +/** + * A code field with a label. + * + * @extends InputComponent + */ +export class CodeComponent extends BaseFieldComponent { + /** + * The editor value that serves a input value for the parent form. + */ + value: string = ''; + + /** + * The editor component. + */ + private editor: CodeEditor; + + /** + * Get the language based on the field name. + * + * @param name + */ + private getLanguageFromName = (name: string) => { + const sanitized = name.replace(/\W+/g, ''); + + if (sanitized.match(/js/i)) { + return 'javascript'; + } + + return 'css'; + }; + + /** + * Render the field. + */ + protected createInput(): void { + const { name, id, value } = this._data; + + this.setAttribute('name', name); + this.value = value as string; + + this.editor = new CodeEditor( + create('div', [CSS.codeflask], { id }, this), + value as string, + this.getLanguageFromName(name), + (code) => (this.value = code) + ); + } + + /** + * Return the field that is observed for changes. + * + * @return the input field + */ + getValueProvider(): HTMLElement { + return this; + } + + /** + * A function that can be used to mutate the field value. + * + * @param value + */ + async mutate(value: UndoValue): Promise { + this.editor.codeFlask.updateCode(value); + this.value = value; + } + + /** + * Query the current field value. + * + * @return the current value + */ + query() { + return this.value; + } +} + +FormDataProviders.add(FieldTag.code); +customElements.define(FieldTag.code, CodeComponent); diff --git a/automad/src/client/admin/components/Forms/Form.ts b/automad/src/client/admin/components/Forms/Form.ts index 2d8a7187c..e0266c1b9 100644 --- a/automad/src/client/admin/components/Forms/Form.ts +++ b/automad/src/client/admin/components/Forms/Form.ts @@ -253,8 +253,9 @@ export class FormComponent extends BaseComponent { const lockId = App.addNavigationLock(); this.submitButtons.forEach((button) => { - button.classList.add(CSS.buttonLoading); - button.prepend(create('am-spinner')); + if (button.classList.contains(CSS.button)) { + button.classList.add(CSS.buttonLoading); + } }); queryAll( @@ -285,7 +286,6 @@ export class FormComponent extends BaseComponent { this.submitButtons.forEach((button) => { button.classList.remove(CSS.buttonLoading); - query('am-spinner', button)?.remove(); }); } diff --git a/automad/src/client/admin/components/Forms/PageDataForm.ts b/automad/src/client/admin/components/Forms/PageDataForm.ts index e1a42f990..87fa91260 100644 --- a/automad/src/client/admin/components/Forms/PageDataForm.ts +++ b/automad/src/client/admin/components/Forms/PageDataForm.ts @@ -45,6 +45,7 @@ import { Attr, Binding, create, + createCustomizationFields, createField, createFieldSections, createLabelFromField, @@ -413,6 +414,8 @@ export class PageDataFormComponent extends FormComponent { readme, }); + createCustomizationFields(fields, this.sections); + Object.keys(this.sections).forEach((item: FieldSectionName) => { fieldGroup({ section: this.sections[item], diff --git a/automad/src/client/admin/components/Forms/SharedDataForm.ts b/automad/src/client/admin/components/Forms/SharedDataForm.ts index bb6323fa6..29d11466a 100644 --- a/automad/src/client/admin/components/Forms/SharedDataForm.ts +++ b/automad/src/client/admin/components/Forms/SharedDataForm.ts @@ -37,6 +37,7 @@ import { Attr, Binding, create, + createCustomizationFields, createField, createFieldSections, EventName, @@ -217,6 +218,8 @@ export class SharedDataFormComponent extends FormComponent { name: `data[${App.reservedFields.SYNTAX_THEME}]`, }); + createCustomizationFields(fields, this.sections); + Object.keys(this.sections).forEach((item: FieldSectionName) => { fieldGroup({ section: this.sections[item], diff --git a/automad/src/client/admin/components/Pages/Page.ts b/automad/src/client/admin/components/Pages/Page.ts index bfe87664e..12185433f 100644 --- a/automad/src/client/admin/components/Pages/Page.ts +++ b/automad/src/client/admin/components/Pages/Page.ts @@ -198,9 +198,9 @@ const renderMenu = (): string => { - ${App.text('fieldsColors')} + ${App.text('fieldsCustomize')} { - ${App.text('fieldsColors')} + ${App.text('fieldsCustomize')} { - ${App.text('fieldsColors')} + ${App.text('fieldsCustomize')} { - ${App.text('fieldsColors')} + ${App.text('fieldsCustomize')} {
${App.text('jumpbarButtonText')} @@ -105,18 +105,18 @@ export const dashboardLayout = ({ main }: Partials) => { > diff --git a/automad/src/client/admin/core/css.ts b/automad/src/client/admin/core/css.ts index d6473abc6..4beb29f2d 100644 --- a/automad/src/client/admin/core/css.ts +++ b/automad/src/client/admin/core/css.ts @@ -81,6 +81,8 @@ export const enum CSS { checkbox = 'am-f-checkbox', + codeflask = 'am-f-codeflask', + contents = 'am-e-contents', customIconCheckbox = 'am-f-custom-icon-checkbox', @@ -94,6 +96,8 @@ export const enum CSS { displayNone = 'am-u-display-none', displaySmall = 'am-u-display-small', displaySmallNone = 'am-u-display-small-none', + displayMedium = 'am-u-display-medium', + displayMediumNone = 'am-u-display-medium-none', dropdown = 'am-c-dropdown', dropdownOpen = 'am-c-dropdown--open', @@ -111,8 +115,6 @@ export const enum CSS { editorBlockButtonsEdit = 'am-c-ed-bl-buttons__edit', editorBlockButtonsButton = 'am-c-ed-bl-buttons__button', - editorBlockCode = 'am-c-ed-bl-code', - editorBlockDelimiter = 'am-c-ed-bl-delimiter', editorBlockHeader = 'am-c-ed-bl-header', diff --git a/automad/src/client/admin/core/factory.ts b/automad/src/client/admin/core/factory.ts index 578bcd0b2..69215253c 100644 --- a/automad/src/client/admin/core/factory.ts +++ b/automad/src/client/admin/core/factory.ts @@ -149,7 +149,7 @@ export const createFieldSections = ( const sections: FieldSectionCollection = { settings: createSection(Section.settings), text: createSection(Section.text), - colors: createSection(Section.colors), + customize: createSection(Section.customize), }; return sections; diff --git a/automad/src/client/admin/core/form.ts b/automad/src/client/admin/core/form.ts index 6e67fb34b..4fe6fc9ee 100644 --- a/automad/src/client/admin/core/form.ts +++ b/automad/src/client/admin/core/form.ts @@ -40,11 +40,13 @@ import { listen, query, queryAll, + Section, titleCase, } from '.'; import { FieldGroupData, FieldGroups, + FieldSectionCollection, InputElement, KeyValueMap, } from '@/admin/types'; @@ -53,27 +55,28 @@ import { * The tag names enum for fields. */ export const enum FieldTag { + code = 'am-code', + color = 'am-color', + date = 'am-date', editor = 'am-editor', email = 'am-email', feedFieldSelect = 'am-feed-field-select', + imageSelect = 'am-image-select', input = 'am-input', mainTheme = 'am-main-theme', + markdown = 'am-markdown', + number = 'am-number', + numberUnit = 'am-number-unit', pageTags = 'am-page-tags', pageTemplate = 'am-page-template', password = 'am-password', - number = 'am-number', - numberUnit = 'am-number-unit', - toggle = 'am-toggle', - toggleSelect = 'am-toggle-select', - toggleLarge = 'am-toggle-large', - color = 'am-color', - date = 'am-date', - markdown = 'am-markdown', - imageSelect = 'am-image-select', - url = 'am-url', syntaxSelect = 'am-syntax-theme-select', textarea = 'am-textarea', title = 'am-title', + toggle = 'am-toggle', + toggleLarge = 'am-toggle-large', + toggleSelect = 'am-toggle-select', + url = 'am-url', } /** @@ -116,6 +119,81 @@ export class FormDataProviders { } } +/** + * Create all custom CSS and JS fields. + * + * @param fields + * @param sections + */ +export const createCustomizationFields = ( + fields: KeyValueMap, + sections: FieldSectionCollection +) => { + const buildFieldProps = ( + field: string, + label: string | null = null, + placeholder: string | null = null + ) => { + const key = App.reservedFields[field]; + + return { + key, + label, + placeholder, + value: fields[key], + name: `data[${key}]`, + }; + }; + + createField( + FieldTag.input, + sections.customize, + buildFieldProps( + 'CUSTOM_CSS_FILE', + App.text('customCSSFile'), + '/shared/custom.css' + ) + ); + + createField( + FieldTag.input, + sections.customize, + buildFieldProps( + 'CUSTOM_JS_HEADER_FILE', + `${App.text('customJSFile')} (Header)`, + '/shared/header.js' + ) + ); + + createField( + FieldTag.input, + sections.customize, + buildFieldProps( + 'CUSTOM_JS_FOOTER_FILE', + `${App.text('customJSFile')} (Footer)`, + '/shared/footer.js' + ) + ); + + createField( + FieldTag.code, + sections.customize, + buildFieldProps('CUSTOM_CSS', App.text('customCSS')) + ); + + createField( + FieldTag.code, + sections.customize, + buildFieldProps('CUSTOM_JS_HEADER', `${App.text('customJS')} (Header)`) + ); + + createField( + FieldTag.code, + sections.customize, + buildFieldProps('CUSTOM_JS_FOOTER', `${App.text('customJS')} (Footer)`) + ); +}; + /** * Create an ID from a field key. * @@ -263,7 +341,7 @@ export const prepareFieldGroups = (fields: KeyValueMap): FieldGroups => { const groups: FieldGroups = { settings: {}, text: {}, - colors: {}, + customize: {}, }; Object.keys(fields).forEach((name) => { @@ -275,7 +353,7 @@ export const prepareFieldGroups = (fields: KeyValueMap): FieldGroups => { groups.text[name] = fields[name]; break; case 'color': - groups.colors[name] = fields[name]; + groups.customize[name] = fields[name]; break; default: groups.settings[name] = fields[name]; diff --git a/automad/src/client/admin/editor/blocks/Code.ts b/automad/src/client/admin/editor/blocks/Code.ts index 72ed0c484..2d2319875 100644 --- a/automad/src/client/admin/editor/blocks/Code.ts +++ b/automad/src/client/admin/editor/blocks/Code.ts @@ -169,7 +169,7 @@ export class CodeBlock extends BaseBlock { const code = create( 'div', - [CSS.editorBlockCode], + [CSS.codeflask], {}, container ) as HTMLDivElement; diff --git a/automad/src/client/admin/editor/blocks/Raw.ts b/automad/src/client/admin/editor/blocks/Raw.ts index 0b2af96af..769556d97 100644 --- a/automad/src/client/admin/editor/blocks/Raw.ts +++ b/automad/src/client/admin/editor/blocks/Raw.ts @@ -109,7 +109,7 @@ export class RawBlock extends BaseBlock { const container = create( 'div', - [CSS.editorBlockCode], + [CSS.codeflask], {}, this.wrapper ) as HTMLDivElement; diff --git a/automad/src/client/admin/index.ts b/automad/src/client/admin/index.ts index c3bb2d40f..82dbe2558 100644 --- a/automad/src/client/admin/index.ts +++ b/automad/src/client/admin/index.ts @@ -44,6 +44,7 @@ import './styles/index.less'; import './components/Breadcrumbs/BreadcrumbsPage'; import './components/Breadcrumbs/BreadcrumbsRoute'; +import './components/Fields/Code'; import './components/Fields/Color'; import './components/Fields/Date'; import './components/Fields/Editor'; diff --git a/automad/src/client/admin/styles/components/editor/blocks/index.less b/automad/src/client/admin/styles/components/editor/blocks/index.less index 92a908bc0..3e7a07ddc 100644 --- a/automad/src/client/admin/styles/components/editor/blocks/index.less +++ b/automad/src/client/admin/styles/components/editor/blocks/index.less @@ -33,7 +33,6 @@ */ @import 'buttons.less'; -@import 'code.less'; @import 'delimiter.less'; @import 'embed.less'; @import 'header.less'; diff --git a/automad/src/client/admin/styles/components/icon-text.less b/automad/src/client/admin/styles/components/icon-text.less index 321c847cb..e6b269339 100644 --- a/automad/src/client/admin/styles/components/icon-text.less +++ b/automad/src/client/admin/styles/components/icon-text.less @@ -36,9 +36,11 @@ display: inline-flex; align-items: center; gap: @am-flex-gap; - max-width: 100%; + max-width: min(100%, 85vw); & > span { + display: inline-block; + max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/automad/src/client/admin/styles/components/nav.less b/automad/src/client/admin/styles/components/nav.less index adf997062..7ca75185f 100644 --- a/automad/src/client/admin/styles/components/nav.less +++ b/automad/src/client/admin/styles/components/nav.less @@ -60,7 +60,11 @@ border-radius: @am-nav-item-radius; } - :has([am-publication-state*='draft']) > div > &__item > &__link:after { + .am-c-nav + :has([am-publication-state*='draft']) + > div + > &__item + > &__link:after { content: '●'; font-size: 0.8rem; color: hsl(var(--am-clr-text-muted)); @@ -72,6 +76,12 @@ color: hsl(var(--am-clr-text)) !important; } + &__item--active[am-publication-state*='draft'] > &__link:after { + content: '●'; + font-size: 0.8rem; + color: hsl(var(--am-clr-primary-text)) !important; + } + &__link { box-sizing: border-box; display: flex; diff --git a/automad/src/client/admin/styles/elements/button.less b/automad/src/client/admin/styles/elements/button.less index f45959dc0..459b84792 100644 --- a/automad/src/client/admin/styles/elements/button.less +++ b/automad/src/client/admin/styles/elements/button.less @@ -45,6 +45,8 @@ justify-content: center; align-items: center; gap: @am-flex-gap; + position: relative; + overflow: hidden; height: @am-form-height; padding: 0 1.25em; font-size: 1em; @@ -108,9 +110,53 @@ background-color: transparent; } - &--loading { + // Loading animation + &:before { + content: ''; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + z-index: 5; + width: 1rem; + height: 1rem; + border: 2px solid; + border-top-color: transparent; + border-radius: 1rem; + animation: 0.8s linear 0s infinite loading-button-spinner; + transition: opacity 0.2s; + opacity: 0; + } + + &:after { + content: ''; + position: absolute; + inset: 0; + background-color: inherit; + transition: opacity 0.2s; + opacity: 0; + } + + &&--loading { pointer-events: none; cursor: progress; - opacity: 0.8; + + &:before { + opacity: 1; + } + + &:after { + opacity: 0.65; + } + } +} + +@keyframes loading-button-spinner { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); } } diff --git a/automad/src/client/admin/styles/components/editor/blocks/code.less b/automad/src/client/admin/styles/form/codeflask.less similarity index 95% rename from automad/src/client/admin/styles/components/editor/blocks/code.less rename to automad/src/client/admin/styles/form/codeflask.less index 0fe480812..20fd613cc 100644 --- a/automad/src/client/admin/styles/components/editor/blocks/code.less +++ b/automad/src/client/admin/styles/form/codeflask.less @@ -32,7 +32,7 @@ * Licensed under the MIT license. */ -.am-c-ed-bl-code { +.am-f-codeflask { position: relative; overflow: hidden; height: auto; @@ -79,5 +79,9 @@ font-size: 1rem; line-height: @am-base-line-height; } + + & ::selection { + background-color: hsla(var(--am-clr-text), 0.1); + } } } diff --git a/automad/src/client/admin/styles/form/index.less b/automad/src/client/admin/styles/form/index.less index 4be20e068..f6d15d4bf 100644 --- a/automad/src/client/admin/styles/form/index.less +++ b/automad/src/client/admin/styles/form/index.less @@ -33,6 +33,7 @@ */ @import 'checkbox.less'; +@import 'codeflask.less'; @import 'custom-icon-checkbox.less'; @import 'group.less'; @import 'input.less'; diff --git a/automad/src/client/admin/styles/layout/dashboard.less b/automad/src/client/admin/styles/layout/dashboard.less index a5b114149..fec191cf2 100644 --- a/automad/src/client/admin/styles/layout/dashboard.less +++ b/automad/src/client/admin/styles/layout/dashboard.less @@ -189,7 +189,7 @@ justify-self: center; width: 100%; - max-width: @am-main-width; + max-width: min(100vw, @am-main-width); padding: 0 var(--am-body-padding-x); &--row { diff --git a/automad/src/client/admin/styles/utils/display.less b/automad/src/client/admin/styles/utils/display.less index 401424674..73e8a84d3 100644 --- a/automad/src/client/admin/styles/utils/display.less +++ b/automad/src/client/admin/styles/utils/display.less @@ -38,12 +38,24 @@ } &-small { - @media (min-width: @am-breakpoint-medium) { + @media (min-width: @am-breakpoint-small) { display: none !important; } } &-small-none { + @media (max-width: @am-breakpoint-small-max) { + display: none !important; + } + } + + &-medium { + @media (min-width: @am-breakpoint-medium) { + display: none !important; + } + } + + &-medium-none { @media (max-width: @am-breakpoint-medium-max) { display: none !important; } diff --git a/automad/src/client/admin/types/field.ts b/automad/src/client/admin/types/field.ts index 397fa0cc4..1adeb25ca 100644 --- a/automad/src/client/admin/types/field.ts +++ b/automad/src/client/admin/types/field.ts @@ -35,7 +35,7 @@ import { KeyValueMap } from '.'; import { SwitcherSectionComponent } from '@/admin/components/Switcher/SwitcherSection'; -export type FieldSectionName = 'settings' | 'text' | 'colors'; +export type FieldSectionName = 'settings' | 'text' | 'customize'; export type FieldSectionCollection = { [name in FieldSectionName]: SwitcherSectionComponent; diff --git a/automad/src/client/common/sections.ts b/automad/src/client/common/sections.ts index aa0b6d28b..d78120c7a 100644 --- a/automad/src/client/common/sections.ts +++ b/automad/src/client/common/sections.ts @@ -45,6 +45,6 @@ export const enum Section { config = 'config', settings = 'settings', text = 'text', - colors = 'colors', + customize = 'customize', files = 'files', } diff --git a/automad/src/server/Admin/InPage.php b/automad/src/server/Admin/InPage.php index e73ca20a4..3db83de2a 100644 --- a/automad/src/server/Admin/InPage.php +++ b/automad/src/server/Admin/InPage.php @@ -65,13 +65,18 @@ class InPage { * * @see injectTemporaryEditButton() */ - const TEMP_REGEX = '/\{\{@open:([\w=]+)@\}\}(.*?)\{\{@close@\}\}/s'; + const TEMP_REGEX = '/\{\{@open:([\w=]+)@\}\}(.*?)\{\{@close:\1@\}\}/s'; /** * The Automad instance. */ private Automad $Automad; + /** + * The incremental button id. + */ + private static int $buttonId = 0; + /** * The constructor. * @@ -107,11 +112,12 @@ public function createUI(string $str): string { * @param Context $Context * @return string The processed $value */ - public function injectTemporaryEditButton(string $value, string $field, Context $Context) { + public function injectTemporaryEditButton(string $value, string $field, Context $Context): string { // Only inject button if $key is no runtime var and a user is logged in. if (preg_match('/^(\+|\w)/', $field) && Session::getUsername()) { $data = base64_encode( json_encode(array( + 'id' => self::$buttonId++, 'context' => $Context->get()->origUrl, 'field' => $field, 'page' => AM_REQUEST, @@ -119,7 +125,7 @@ public function injectTemporaryEditButton(string $value, string $field, Context ), JSON_UNESCAPED_SLASHES) ); - return "{{@open:$data@}}$value{{@close@}}"; + return "{{@open:$data@}}$value{{@close:$data@}}"; } return $value; @@ -131,7 +137,7 @@ public function injectTemporaryEditButton(string $value, string $field, Context * @param string $str * @return string The processed markup */ - private function injectAssets(string $str) { + private function injectAssets(string $str): string { $fn = function (mixed $expression): string { return $expression; }; @@ -147,7 +153,7 @@ private function injectAssets(string $str) { * @param string $str * @return string The processed $str */ - private function injectDock(string $str) { + private function injectDock(string $str): string { $state = $this->Automad->getPage(AM_REQUEST)?->get(Fields::PUBLICATION_STATE) ?? ''; $urlDashboard = AM_BASE_INDEX . AM_PAGE_DASHBOARD; $urlApi = AM_BASE_INDEX . RequestHandler::$apiBase; @@ -192,7 +198,7 @@ private function injectDock(string $str) { * @param string $str * @return string The processed markup */ - private function processEditButtons(string $str) { + private function processEditButtons(string $str): string { // Remove invalid buttons. // Within HTML tags. // Like
@@ -208,7 +214,7 @@ private function processEditButtons(string $str) { $str = preg_replace_callback(InPage::TEMP_REGEX, function ($matches) { $base64Data = $matches[1]; - $value = $matches[2]; + $value = $this->processEditButtons($matches[2]); $data = json_decode(base64_decode($base64Data)); $label = Text::get('edit'); $placeholder = !$value ? 'placeholder="' . Text::get('inPagePlaceholder') . '"' : ''; diff --git a/automad/src/server/App.php b/automad/src/server/App.php index fde7e1c37..edf6baf2d 100644 --- a/automad/src/server/App.php +++ b/automad/src/server/App.php @@ -56,7 +56,7 @@ * @license MIT license - https://automad.org/license */ class App { - const VERSION = '2.0.0-alpha.5'; + const VERSION = '2.0.0-alpha.6'; /** * Required PHP version. diff --git a/automad/src/server/Core/Config.php b/automad/src/server/Core/Config.php index bf180f65b..bd1abe8bb 100644 --- a/automad/src/server/Core/Config.php +++ b/automad/src/server/Core/Config.php @@ -158,11 +158,22 @@ public static function defaults(): void { } /** - * Define constants based on the configuration array. + * Merge default constants with overrides that are saved in config files + * such as "config.php" and "config.mail.php". */ public static function overrides(): void { - foreach (self::read() as $name => $value) { - self::set($name, $value); + $files = FileSystem::glob(AM_BASE_DIR . '/config/config.*'); + + foreach ($files as $file) { + // Strip filename until it is an empty string (main config) + // or the name of the config such as "mail" (config.mail.php). + $configName = Str::stripStart($file, AM_BASE_DIR . '/config/config'); + $configName = Str::stripEnd($configName, 'php'); + $configName = trim($configName, '.'); + + foreach (self::read($configName) as $name => $value) { + self::set($name, $value); + } } } @@ -171,14 +182,16 @@ public static function overrides(): void { * and decode the returned string. Note that now the configuration is stored in * PHP files instead of JSON files to make it less easy to access from outside. * + * @param string $name * @return array The configuration array */ - public static function read(): array { + public static function read(string $name = ''): array { $json = false; $config = array(); + $file = self::getConfigPath($name); - if (is_readable(Config::FILE)) { - $json = require Config::FILE; + if (is_readable($file)) { + $json = require $file; } if ($json) { @@ -204,20 +217,34 @@ public static function set(string $name, mixed $value): void { * Write the configuration file. * * @param array $config + * @param string $name * @return bool True on success */ - public static function write(array $config): bool { + public static function write(array $config, string $name = ''): bool { $json = json_encode($config, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); $content = "setLanguage($output); $output = $this->resizeImages($output); $output = Blocks::injectAssets($output); + $output = $this->addCustomizations($output); $output = $MailAddressProcessor->obfuscate($output); $output = $SyntaxHighlightingProcessor->addAssets($output); $output = $this->addCacheBustingTimestamps($output); @@ -131,11 +133,54 @@ function ($matches) { ); } + /** + * Add custom JS and CSS customizations. + * + * @param string $str + * @return string The rendered output + */ + private function addCustomizations(string $str): string { + $Page = $this->Automad->Context->get(); + + $css = $Page->get(Fields::CUSTOM_CSS); + $cssFile = $Page->get(Fields::CUSTOM_CSS_FILE); + $jsHeader = $Page->get(Fields::CUSTOM_JS_HEADER); + $jsHeaderFile = $Page->get(Fields::CUSTOM_JS_HEADER_FILE); + $jsFooter = $Page->get(Fields::CUSTOM_JS_FOOTER); + $jsFooterFile = $Page->get(Fields::CUSTOM_JS_FOOTER_FILE); + + if ($cssFile) { + $str = Head::append($str, ''); + } + + if ($jsHeaderFile) { + $str = Head::append($str, ''); + } + + if ($jsFooterFile) { + $str = Body::append($str, ''); + } + + if ($css) { + $str = Head::append($str, ""); + } + + if ($jsHeader) { + $str = Head::append($str, ""); + } + + if ($jsFooter) { + $str = Body::append($str, ""); + } + + return $str; + } + /** * Add meta tags to the head of $str. * * @param string $str - * @return string The meta tag + * @return string The rendered output */ private function addMetaTags(string $str): string { $base = AM_SERVER . AM_BASE_INDEX; diff --git a/automad/src/server/Models/Image.php b/automad/src/server/Models/Image.php index 5e8dfba89..63ae81151 100644 --- a/automad/src/server/Models/Image.php +++ b/automad/src/server/Models/Image.php @@ -64,8 +64,7 @@ class Image { public static function save(string $path, string $name, string $extension, string $base64, Messenger $Messenger): void { if (!in_array($extension, FileUtils::allowedFileTypes())) { $Messenger->setError( - Text::get('unsupportedFileTypeError') . ' "' . - FileSystem::getExtension($extension) . '"' + Text::get('unsupportedFileTypeError') . ' "' . $extension . '"' ); return; diff --git a/automad/src/server/Models/MailConfig.php b/automad/src/server/Models/MailConfig.php index fa5d13097..bf35f9b13 100644 --- a/automad/src/server/Models/MailConfig.php +++ b/automad/src/server/Models/MailConfig.php @@ -52,6 +52,7 @@ * @license MIT license - https://automad.org/license */ class MailConfig { + const CONFIG_NAME = 'mail'; const DEFAULT_PORT = 587; const DEFAULT_TRANSPORT = 'sendmail'; @@ -112,7 +113,7 @@ public static function getDefaultFrom(): string { * @return bool */ public static function reset(): bool { - $config = Config::read(); + $config = Config::read(MailConfig::CONFIG_NAME); $config['AM_MAIL_TRANSPORT'] = self::DEFAULT_TRANSPORT; $config['AM_MAIL_FROM'] = ''; @@ -121,7 +122,7 @@ public static function reset(): bool { $config['AM_MAIL_SMTP_PASSWORD'] = ''; $config['AM_MAIL_SMTP_PORT'] = self::DEFAULT_PORT; - return Config::write($config); + return Config::write($config, MailConfig::CONFIG_NAME); } /** @@ -130,7 +131,7 @@ public static function reset(): bool { * @return bool */ public function save(): bool { - $config = Config::read(); + $config = Config::read(MailConfig::CONFIG_NAME); $config['AM_MAIL_TRANSPORT'] = $this->transport; $config['AM_MAIL_FROM'] = $this->from; @@ -139,6 +140,6 @@ public function save(): bool { $config['AM_MAIL_SMTP_PASSWORD'] = $this->smtpPassword; $config['AM_MAIL_SMTP_PORT'] = $this->smtpPort; - return Config::write($config); + return Config::write($config, MailConfig::CONFIG_NAME); } } diff --git a/automad/src/server/System/Fields.php b/automad/src/server/System/Fields.php index 63a8731a9..3cfd0d1f3 100644 --- a/automad/src/server/System/Fields.php +++ b/automad/src/server/System/Fields.php @@ -55,6 +55,12 @@ class Fields { const CAPTION = ':caption'; const CURRENT_PAGE = ':current'; const CURRENT_PATH = ':currentPath'; + const CUSTOM_CSS = 'customCSS'; + const CUSTOM_CSS_FILE = 'customCSSFile'; + const CUSTOM_JS_FOOTER = 'customJSFooter'; + const CUSTOM_JS_FOOTER_FILE = 'customJSFooterFile'; + const CUSTOM_JS_HEADER = 'customJSHeader'; + const CUSTOM_JS_HEADER_FILE = 'customJSHeaderFile'; const DATE = 'date'; const FILE = ':file'; const FILE_RESIZED = ':fileResized'; @@ -96,10 +102,19 @@ class Fields { * Array with reserved variable fields. */ public static array $reserved = array( + 'CUSTOM_CSS' => Fields::CUSTOM_CSS, + 'CUSTOM_CSS_FILE' => Fields::CUSTOM_CSS_FILE, + 'CUSTOM_JS_FOOTER' => Fields::CUSTOM_JS_FOOTER, + 'CUSTOM_JS_FOOTER_FILE' => Fields::CUSTOM_JS_FOOTER_FILE, + 'CUSTOM_JS_HEADER' => Fields::CUSTOM_JS_HEADER, + 'CUSTOM_JS_HEADER_FILE' => Fields::CUSTOM_JS_HEADER_FILE, 'DATE' => Fields::DATE, 'HIDDEN' => Fields::HIDDEN, 'PRIVATE' => Fields::PRIVATE, 'PUBLICATION_STATE' => Fields::PUBLICATION_STATE, + 'SITENAME' => Fields::SITENAME, + 'SLUG' => Fields::SLUG, + 'SYNTAX_THEME' => Fields::SYNTAX_THEME, 'TAGS' => Fields::TAGS, 'TEMPLATE' => Fields::TEMPLATE, 'THEME' => Fields::THEME, @@ -107,10 +122,7 @@ class Fields { 'TIME_LAST_MODIFIED' => Fields::TIME_LAST_MODIFIED, 'TIME_LAST_PUBLISHED' => Fields::TIME_LAST_PUBLISHED, 'TITLE' => Fields::TITLE, - 'SITENAME' => Fields::SITENAME, - 'SLUG' => Fields::SLUG, - 'SYNTAX_THEME' => Fields::SYNTAX_THEME, - 'URL' => Fields::URL + 'URL' => Fields::URL, ); /** diff --git a/config/config.php b/config/config.php index 128fdd33f..26dd4c956 100644 --- a/config/config.php +++ b/config/config.php @@ -13,13 +13,6 @@ "AM_MAIL_OBFUSCATION_ENABLED": true, - "AM_MAIL_TRANSPORT": "sendmail", - "AM_MAIL_FROM": "", - "AM_MAIL_SMTP_SERVER": "", - "AM_MAIL_SMTP_PORT": 587, - "AM_MAIL_SMTP_USERNAME": "", - "AM_MAIL_SMTP_PASSWORD": "", - "AM_PASSWORD_MIN_LENGTH": 8, "AM_PASSWORD_REQUIRED_CHARS": "@#%^~+=*$&! A-Z a-z 0-9" } diff --git a/lib/composer.lock b/lib/composer.lock index 8694ec68e..54dc37f0a 100644 --- a/lib/composer.lock +++ b/lib/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/automadcms/automad-language-packs.git", - "reference": "c5b18f9af31b8eba84c55fb084eff81e8e9e1c2e" + "reference": "d975a1b8a8b10a74fe1feb7891ef1640af641cbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/automadcms/automad-language-packs/zipball/c5b18f9af31b8eba84c55fb084eff81e8e9e1c2e", - "reference": "c5b18f9af31b8eba84c55fb084eff81e8e9e1c2e", + "url": "https://api.github.com/repos/automadcms/automad-language-packs/zipball/d975a1b8a8b10a74fe1feb7891ef1640af641cbf", + "reference": "d975a1b8a8b10a74fe1feb7891ef1640af641cbf", "shasum": "" }, "default-branch": true, @@ -55,7 +55,7 @@ "type": "ko_fi" } ], - "time": "2024-04-12T21:34:55+00:00" + "time": "2024-09-01T19:42:01+00:00" }, { "name": "doctrine/lexer", diff --git a/package-lock.json b/package-lock.json index ca6c9bcbe..0e6644ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "automad", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index f11ff5979..9388af2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "automad", - "version": "2.0.0-alpha.5", + "version": "2.0.0-alpha.6", "description": "Automad", "author": "Marc Anton Dahmen", "license": "MIT",