Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ public class LayoutModel {

private String textSubtitleFontStyle;

private String iconColor;

private String textTitleBackgroundColor;

private String textTitleBackgroundImage;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ public ResponseEntity<LayoutModel> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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";
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -264,6 +266,7 @@ public void updateSiteLayout(SiteKey siteKey,
siteKey,
username));
}
LayoutStyleValidator.validate(site.getPortalLayout());
portalConfig.setPortalLayout(site.getPortalLayout());
try {
if (publish) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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, "&lt;top&gt;@&lt;middle&gt;", 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));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading