diff --git a/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java b/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java index 975c61950..06b8f3edc 100644 --- a/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java +++ b/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java @@ -176,6 +176,8 @@ public class LayoutModel { private String textSubtitleFontStyle; + private String iconColor; + private String textTitleBackgroundColor; private String textTitleBackgroundImage; @@ -360,6 +362,7 @@ private void init(ModelObject model, PortletInstanceService portletInstanceServi this.textSubtitleFontSize = cssStyle.getTextSubtitleFontSize(); this.textSubtitleFontWeight = cssStyle.getTextSubtitleFontWeight(); this.textSubtitleFontStyle = cssStyle.getTextSubtitleFontStyle(); + this.iconColor = cssStyle.getIconColor(); this.textTitleBackgroundColor = cssStyle.getTextTitleBackgroundColor(); this.textTitleBackgroundImage = cssStyle.getTextTitleBackgroundImage(); this.textTitleBackgroundEffect = cssStyle.getTextTitleBackgroundEffect(); diff --git a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java index e1c2c0eca..c61744c5d 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java @@ -396,6 +396,8 @@ public ResponseEntity updateSiteLayout( publish.orElse(false).booleanValue(), request.getRemoteUser()); return getSiteLayout(webRequest, request, siteType, siteName, expand); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java index 9382eace3..819101f61 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java @@ -19,7 +19,6 @@ package io.meeds.layout.service; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; @@ -32,8 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.google.javascript.jscomp.jarjar.com.google.re2j.Pattern; - import org.exoplatform.commons.addons.AddOnService; import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.portal.config.UserPortalConfigService; @@ -59,6 +56,7 @@ import io.meeds.layout.model.PortletInstancePreference; import io.meeds.layout.util.EntityMapper; import io.meeds.layout.util.JsonUtils; +import io.meeds.layout.util.LayoutStyleValidator; import lombok.SneakyThrows; @@ -75,8 +73,6 @@ public class PageLayoutService { private static final Log LOG = ExoLogger.getLogger(PageLayoutService.class); - private static final Pattern GENERIC_STYLE_MATCHER_VALIDATOR = Pattern.compile("[#0-9a-zA-Z\\(\\),\\./\"'\\-%_ ]+"); - private static final String PAGE_NOT_EXISTS_MESSAGE = "Page with key %s doesn't exist"; private static final String PAGE_NOT_ACCESSIBLE_MESSAGE = "Page with ref %s isn't accessible for user %s"; @@ -248,7 +244,7 @@ public PageContext createPage(PageCreateModel pageModel, String username) throws portalConfig.getEditPermission() : pageModel.getEditPermission(); page.setEditPermission(editPermission); - validateCSSInputs(page); + LayoutStyleValidator.validate(page); layoutService.save(new PageContext(page.getPageKey(), Utils.toPageState(page)), page); listenerService.broadcast(PAGE_UPDATED_EVENT, username, page.getPageKey().format()); return layoutService.getPageContext(page.getPageKey()); @@ -359,7 +355,7 @@ public PageContext updatePageLayout(String pageRef, LOG.debug("Error while accessing page applications storage information", e); throw new IllegalStateException("layout.pageOutdatedError"); } - validateCSSInputs(page); + LayoutStyleValidator.validate(page); existingPage.setChildren(page.getChildren()); layoutService.save(existingPage); listenerService.broadcast(PAGE_UPDATED_EVENT, username, pageKey.format()); @@ -544,51 +540,6 @@ private boolean replaceAddonContainerChildren(Container container) { return replaced; } - private void validateCSSInputs(ModelObject modelObject) { // NOSONAR - ModelStyle cssStyle = modelObject.getCssStyle(); - Arrays.asList(modelObject.getHeight(), - modelObject.getWidth(), - cssStyle == null ? null : cssStyle.getBorderColor(), - cssStyle == null ? null : cssStyle.getBorderSize(), - cssStyle == null ? null : cssStyle.getBoxShadow(), - cssStyle == null ? null : cssStyle.getBackgroundColor(), - cssStyle == null ? null : cssStyle.getBackgroundImage(), - cssStyle == null ? null : cssStyle.getBackgroundEffect(), - cssStyle == null ? null : cssStyle.getBackgroundPosition(), - cssStyle == null ? null : cssStyle.getBackgroundSize(), - cssStyle == null ? null : cssStyle.getBackgroundRepeat(), - cssStyle == null ? null : cssStyle.getTextTitleColor(), - cssStyle == null ? null : cssStyle.getTextTitleFontSize(), - cssStyle == null ? null : cssStyle.getTextTitleFontWeight(), - cssStyle == null ? null : cssStyle.getTextTitleFontStyle(), - cssStyle == null ? null : cssStyle.getTextColor(), - cssStyle == null ? null : cssStyle.getTextFontSize(), - cssStyle == null ? null : cssStyle.getTextFontWeight(), - cssStyle == null ? null : cssStyle.getTextFontStyle(), - cssStyle == null ? null : cssStyle.getTextHeaderColor(), - cssStyle == null ? null : cssStyle.getTextHeaderFontSize(), - cssStyle == null ? null : cssStyle.getTextHeaderFontWeight(), - cssStyle == null ? null : cssStyle.getTextHeaderFontStyle(), - cssStyle == null ? null : cssStyle.getTextSubtitleColor(), - cssStyle == null ? null : cssStyle.getTextSubtitleFontSize(), - cssStyle == null ? null : cssStyle.getTextSubtitleFontWeight(), - cssStyle == null ? null : cssStyle.getTextSubtitleFontStyle()) - .forEach(this::validateCSSStyleValue); - if (modelObject instanceof Container container && !CollectionUtils.isEmpty(container.getChildren())) { - container.getChildren().forEach(this::validateCSSInputs); - } - } - - private void validateCSSStyleValue(String value) { - if (StringUtils.isNotBlank(value) - && (!GENERIC_STYLE_MATCHER_VALIDATOR.matches(value) - || value.contains("javascript") - || value.contains("eval"))) { - throw new IllegalArgumentException(String.format("Invalid css value input %s", - value)); - } - } - private ModelObject filterByPermission(ModelObject modelObject, String username) { if (!aclService.hasAccessPermission(modelObject, username)) { return null; diff --git a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java index c83adffce..f8f067cf5 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java @@ -48,6 +48,8 @@ import org.exoplatform.services.resources.LocaleConfigService; import org.exoplatform.services.resources.LocaleContextInfo; +import io.meeds.layout.util.LayoutStyleValidator; + import lombok.SneakyThrows; @Service @@ -264,6 +266,7 @@ public void updateSiteLayout(SiteKey siteKey, siteKey, username)); } + LayoutStyleValidator.validate(site.getPortalLayout()); portalConfig.setPortalLayout(site.getPortalLayout()); try { if (publish) { diff --git a/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java index a2eea0aca..e9c102907 100644 --- a/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java +++ b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java @@ -168,6 +168,7 @@ private static ModelStyle mapToStyle(LayoutModel layoutModel) { || StringUtils.isNotBlank(layoutModel.getTextColor()) || StringUtils.isNotBlank(layoutModel.getTextHeaderColor()) || StringUtils.isNotBlank(layoutModel.getTextSubtitleColor()) + || StringUtils.isNotBlank(layoutModel.getIconColor()) || StringUtils.isNotBlank(layoutModel.getTextTitleBackgroundColor()) || StringUtils.isNotBlank(layoutModel.getTextTitleBackgroundImage()) || StringUtils.isNotBlank(layoutModel.getTextHeaderBackgroundColor()) @@ -212,6 +213,7 @@ private static ModelStyle mapToStyle(LayoutModel layoutModel) { cssStyle.setTextSubtitleFontSize(layoutModel.getTextSubtitleFontSize()); cssStyle.setTextSubtitleFontWeight(layoutModel.getTextSubtitleFontWeight()); cssStyle.setTextSubtitleFontStyle(layoutModel.getTextSubtitleFontStyle()); + cssStyle.setIconColor(layoutModel.getIconColor()); cssStyle.setTextTitleBackgroundColor(layoutModel.getTextTitleBackgroundColor()); cssStyle.setTextTitleBackgroundImage(layoutModel.getTextTitleBackgroundImage()); cssStyle.setTextTitleBackgroundEffect(layoutModel.getTextTitleBackgroundEffect()); diff --git a/layout-service/src/main/java/io/meeds/layout/util/LayoutStyleValidator.java b/layout-service/src/main/java/io/meeds/layout/util/LayoutStyleValidator.java new file mode 100644 index 000000000..90f9dfd81 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/util/LayoutStyleValidator.java @@ -0,0 +1,134 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.layout.util; + +import java.util.Arrays; +import java.util.regex.Pattern; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.ModelStyle; + +/** + * Validates the style values a page or a site layout carries before it is + * saved, on every editor save path (page layout and site layout alike). A + * refused value fails the save with an {@link IllegalArgumentException}. + */ +public final class LayoutStyleValidator { + + private static final Pattern GENERIC_STYLE_VALIDATOR = Pattern.compile("[#0-9a-zA-Z\\(\\),\\./\"'\\-%_ ]+"); + + /** + * The page, application and site icon colour is a hex colour only: no + * keyword, since "not set" at this level is the absent field, and no + * function, since the value is written by page editors and site editors. + */ + private static final Pattern ICON_COLOR_VALIDATOR = Pattern.compile("#[0-9a-fA-F]{3,8}"); + + /** + * A sticky site section (Topbar, Sidebar) stores its two on-scroll colours as + * one background value, "<top>@<middle>", which the renderer splits + * back; each half meets the generic check on its own. + */ + private static final String SCROLL_COLOR_SEPARATOR = "@"; + + private LayoutStyleValidator() { + // Static utility + } + + /** + * Validates the style values of the given layout object and of every + * descendant container. + * + * @param modelObject the page, site layout or container to validate, null + * accepted (nothing to validate) + * @throws IllegalArgumentException when a style value is refused + */ + public static void validate(ModelObject modelObject) { // NOSONAR + if (modelObject == null) { + return; + } + ModelStyle cssStyle = modelObject.getCssStyle(); + Arrays.asList(modelObject.getHeight(), + modelObject.getWidth(), + cssStyle == null ? null : cssStyle.getBorderColor(), + cssStyle == null ? null : cssStyle.getBorderSize(), + cssStyle == null ? null : cssStyle.getBoxShadow(), + cssStyle == null ? null : cssStyle.getBackgroundImage(), + cssStyle == null ? null : cssStyle.getBackgroundEffect(), + cssStyle == null ? null : cssStyle.getBackgroundPosition(), + cssStyle == null ? null : cssStyle.getBackgroundSize(), + cssStyle == null ? null : cssStyle.getBackgroundRepeat(), + cssStyle == null ? null : cssStyle.getTextTitleColor(), + cssStyle == null ? null : cssStyle.getTextTitleFontSize(), + cssStyle == null ? null : cssStyle.getTextTitleFontWeight(), + cssStyle == null ? null : cssStyle.getTextTitleFontStyle(), + cssStyle == null ? null : cssStyle.getTextColor(), + cssStyle == null ? null : cssStyle.getTextFontSize(), + cssStyle == null ? null : cssStyle.getTextFontWeight(), + cssStyle == null ? null : cssStyle.getTextFontStyle(), + cssStyle == null ? null : cssStyle.getTextHeaderColor(), + cssStyle == null ? null : cssStyle.getTextHeaderFontSize(), + cssStyle == null ? null : cssStyle.getTextHeaderFontWeight(), + cssStyle == null ? null : cssStyle.getTextHeaderFontStyle(), + cssStyle == null ? null : cssStyle.getTextSubtitleColor(), + cssStyle == null ? null : cssStyle.getTextSubtitleFontSize(), + cssStyle == null ? null : cssStyle.getTextSubtitleFontWeight(), + cssStyle == null ? null : cssStyle.getTextSubtitleFontStyle()) + .forEach(LayoutStyleValidator::validateStyleValue); + if (cssStyle != null) { + validateBackgroundColor(cssStyle.getBackgroundColor()); + validateIconColor(cssStyle.getIconColor()); + } + if (modelObject instanceof Container container && !CollectionUtils.isEmpty(container.getChildren())) { + container.getChildren().forEach(LayoutStyleValidator::validate); + } + } + + private static void validateStyleValue(String value) { + if (StringUtils.isNotBlank(value) + && (!GENERIC_STYLE_VALIDATOR.matcher(value).matches() + || value.contains("javascript") + || value.contains("eval"))) { + throw new IllegalArgumentException(String.format("Invalid css value input %s", value)); + } + } + + private static void validateBackgroundColor(String value) { + if (StringUtils.isNotBlank(value)) { + String[] colors = StringUtils.split(value, SCROLL_COLOR_SEPARATOR); + if (colors.length > 2) { + throw new IllegalArgumentException(String.format("Invalid css value input %s", value)); + } + for (String color : colors) { + validateStyleValue(color); + } + } + } + + private static void validateIconColor(String value) { + if (StringUtils.isNotBlank(value) && !ICON_COLOR_VALIDATOR.matcher(value).matches()) { + throw new IllegalArgumentException(String.format("Invalid icon color input %s", value)); + } + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java b/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java index 7e0d1bcd8..6f4b6d1b8 100644 --- a/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java +++ b/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java @@ -19,14 +19,18 @@ package io.meeds.layout.model; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.ModelObject; import org.exoplatform.portal.config.model.ModelStyle; import org.exoplatform.portal.config.model.TransientApplicationState; +import io.meeds.layout.util.EntityMapper; + /** * Legacy application margins are read once, server-side, from * the Vuetify spacing tokens still stored in the cssClass (value = N x 4 + 20 @@ -77,6 +81,35 @@ public void shouldReadPositiveAndBreakpointTokens() { assertNull(model.getCssClass()); } + @Test + public void shouldRoundTripIconColorThroughModelAndMapper() { + // The page/app icon colour rides ModelStyle -> LayoutModel -> ModelStyle like the text colour + Application application = application(null, null, null, null, null); + application.getCssStyle().setTextColor("#20282C"); + application.getCssStyle().setIconColor("#AABBCC"); + + LayoutModel model = new LayoutModel(application); + assertEquals("#20282C", model.getTextColor()); + assertEquals("#AABBCC", model.getIconColor()); + + ModelObject mapped = EntityMapper.toModelObject(model); + assertNotNull(mapped.getCssStyle()); + assertEquals("#20282C", mapped.getCssStyle().getTextColor()); + assertEquals("#AABBCC", mapped.getCssStyle().getIconColor()); + } + + @Test + public void shouldMapStyleWhenIconColorIsTheOnlyStyledValue() { + // an application whose only custom value is the icon colour still gets a css-style, else the colour is dropped on save + LayoutModel model = new LayoutModel(application(null, null, null, null, null)); + model.setIconColor("#AABBCC"); + assertNull(EntityMapper.toModelObject(new LayoutModel(application(null, null, null, null, null))).getCssStyle()); + + ModelObject mapped = EntityMapper.toModelObject(model); + assertNotNull(mapped.getCssStyle()); + assertEquals("#AABBCC", mapped.getCssStyle().getIconColor()); + } + private Application application(Integer top, Integer bottom, Integer right, Integer left, String cssClass) { Application application = new Application("storageId"); application.setState(new TransientApplicationState("layout/Test")); diff --git a/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java index bb3a3f336..7b18f9510 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java @@ -392,6 +392,21 @@ void updateSiteLayoutWhenNotFound() { response.andExpect(status().isNotFound()); } + @Test + @SneakyThrows + void updateSiteLayoutWhenInvalidStyle() { + // a refused style value (the icon colour hex check, the generic css check) is a 400, never a 500 + doThrow(new IllegalArgumentException("Invalid icon color input red")).when(siteLayoutService) + .updateSiteLayout(eq(SITE_KEY), + any(), + anyBoolean(), + eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(put(LAYOUT_REST_PATH_WITH_PARAMS).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isBadRequest()); + } + @Test @SneakyThrows void updateSiteLayoutWhenIllegalAccess() { diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java index ad8688e69..e6337768a 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java @@ -489,6 +489,22 @@ public void updatePageLayout() throws IllegalAccessException, ObjectNotFoundExce when(applicationCssStyle.getBorderColor()).thenReturn(borderColor); assertDoesNotThrow(() -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); + + // The icon colour passes a hex-only check, stricter than the generic character class + when(cssStyle.getIconColor()).thenReturn("rgb(1, 2, 3)"); + assertThrows(IllegalArgumentException.class, + () -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); + when(cssStyle.getIconColor()).thenReturn("initial"); + assertThrows(IllegalArgumentException.class, + () -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); + when(cssStyle.getIconColor()).thenReturn("#AABBCCDD"); + assertDoesNotThrow(() -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); + + when(applicationCssStyle.getIconColor()).thenReturn("#GGHHII"); + assertThrows(IllegalArgumentException.class, + () -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); + when(applicationCssStyle.getIconColor()).thenReturn("#abc"); + assertDoesNotThrow(() -> pageLayoutService.updatePageLayout(PAGE_KEY.format(), page, true, TEST_USER)); } @Test diff --git a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java index 40a82552c..746890b41 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -47,7 +48,10 @@ import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.portal.config.UserPortalConfig; import org.exoplatform.portal.config.UserPortalConfigService; +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelStyle; import org.exoplatform.portal.config.model.PortalConfig; +import org.exoplatform.portal.config.serialize.model.SiteLayout; import org.exoplatform.portal.mop.SiteKey; import org.exoplatform.portal.mop.importer.ImportMode; import org.exoplatform.portal.mop.service.LayoutService; @@ -250,6 +254,63 @@ public void restoreSite() { true); } + @Test + @SneakyThrows + public void updateSiteLayoutValidatesStyles() { + // The site layout save path validates its style values like the page one, icon colour hex-only + PortalConfig site = mock(PortalConfig.class); + SiteLayout siteLayout = new SiteLayout(); + ModelStyle style = new ModelStyle(); + siteLayout.setCssStyle(style); + when(site.getPortalLayout()).thenReturn(siteLayout); + + when(layoutService.getPortalConfig(SITE_KEY)).thenReturn(portalConfig); + when(aclService.canEditSite(SITE_KEY, TEST_USER)).thenReturn(true); + + style.setTextColor("eval('alert(`XSS in site text CSS style`)')"); + assertThrows(IllegalArgumentException.class, + () -> siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER)); + style.setTextColor("#20282C"); + + style.setIconColor("rgb(1, 2, 3)"); + assertThrows(IllegalArgumentException.class, + () -> siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER)); + verify(layoutService, never()).save(any(PortalConfig.class)); + + style.setIconColor("#AABBCC"); + siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER); + verify(portalConfig).setPortalLayout(siteLayout); + verify(layoutService).save(portalConfig); + } + + @Test + @SneakyThrows + public void updateSiteLayoutAcceptsStickySectionScrollColors() { + // a sticky Topbar or Sidebar stores its two on-scroll colours as one "@" background value + PortalConfig site = mock(PortalConfig.class); + SiteLayout siteLayout = new SiteLayout(); + Container stickySection = new Container(); + ModelStyle style = new ModelStyle(); + stickySection.setCssStyle(style); + siteLayout.setChildren(new ArrayList<>(List.of(stickySection))); + when(site.getPortalLayout()).thenReturn(siteLayout); + + when(layoutService.getPortalConfig(SITE_KEY)).thenReturn(portalConfig); + when(aclService.canEditSite(SITE_KEY, TEST_USER)).thenReturn(true); + + style.setBackgroundColor("#FFF@eval(1)"); + assertThrows(IllegalArgumentException.class, + () -> siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER)); + style.setBackgroundColor("#FFFFFF@#20282C@#000000"); + assertThrows(IllegalArgumentException.class, + () -> siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER)); + verify(layoutService, never()).save(any(PortalConfig.class)); + + style.setBackgroundColor("#FFFFFF@#20282C"); + siteLayoutService.updateSiteLayout(SITE_KEY, site, false, TEST_USER); + verify(layoutService).save(portalConfig); + } + @Test @SneakyThrows public void deleteSite() { diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue index 9d31d2868..c18cca322 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue @@ -77,7 +77,9 @@ v-if="initialized" ref="textInput" v-model="container" + :icon-placeholder="inheritedIconColor" class="mt-4" + custom-icon @refresh="refresh++" />
@@ -177,6 +179,7 @@