This commit is contained in:
Henrique Sato 2026-03-09 14:15:56 +01:00 committed by GitHub
commit ded69256f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 415 additions and 111 deletions

View File

@ -27,11 +27,6 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.command.user.gui.theme.CreateGuiThemeCmd;
import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd;
@ -43,6 +38,7 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.gui.theme.dao.GuiThemeDao;
import org.apache.cloudstack.gui.theme.dao.GuiThemeDetailsDao;
import org.apache.cloudstack.gui.theme.dao.GuiThemeJoinDao;
import org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -52,24 +48,12 @@ import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class GuiThemeServiceImpl implements GuiThemeService {
protected Logger logger = LogManager.getLogger(getClass());
private static final List<String> ALLOWED_PRIMITIVE_PROPERTIES = List.of("appTitle", "footer", "loginFooter", "logo", "minilogo", "banner");
private static final List<String> ALLOWED_ERROR_PROPERTIES = List.of("403", "404", "500");
private static final List<String> ALLOWED_PLUGIN_PROPERTIES = List.of("name", "path", "icon", "isExternalLink");
private static final String ERROR = "error";
private static final String PLUGINS = "plugins";
@Inject
GuiThemeDao guiThemeDao;
@ -91,6 +75,9 @@ public class GuiThemeServiceImpl implements GuiThemeService {
@Inject
DomainDao domainDao;
@Inject
JsonConfigValidator jsonConfigValidator;
@Override
public ListResponse<GuiThemeResponse> listGuiThemes(ListGuiThemesCmd cmd) {
ListResponse<GuiThemeResponse> response = new ListResponse<>();
@ -244,94 +231,7 @@ public class GuiThemeServiceImpl implements GuiThemeService {
validateObjectUuids(accountIds, Account.class);
validateObjectUuids(domainIds, Domain.class);
validateJsonConfiguration(jsonConfig);
}
protected void validateJsonConfiguration(String jsonConfig) {
if (jsonConfig == null) {
return;
}
JsonObject jsonObject = new JsonObject();
try {
JsonElement jsonElement = new JsonParser().parse(jsonConfig);
Set<Map.Entry<String, JsonElement>> entries = jsonElement.getAsJsonObject().entrySet();
entries.stream().forEach(entry -> validateJsonAttributes(entry, jsonObject));
} catch (JsonSyntaxException exception) {
logger.error("The following exception was thrown while parsing the JSON object: [{}].", exception.getMessage());
throw new CloudRuntimeException("Specified JSON configuration is not a valid JSON object.");
}
}
/**
* Validates the informed JSON attributes considering the allowed properties by the API, any invalid option is ignored.
* All valid options are added to a {@link JsonObject} that will be considered as the final JSON configuration used by the GUI theme.
*/
private void validateJsonAttributes(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
JsonElement entryValue = entry.getValue();
String entryKey = entry.getKey();
if (entryValue.isJsonPrimitive() && ALLOWED_PRIMITIVE_PROPERTIES.contains(entryKey)) {
logger.trace("The JSON attribute [{}] is a valid option.", entryKey);
jsonObject.add(entryKey, entryValue);
} else if (entryValue.isJsonObject() && ERROR.equals(entryKey)) {
validateErrorAttribute(entry, jsonObject);
} else if (entryValue.isJsonArray() && PLUGINS.equals(entryKey)) {
validatePluginsAttribute(entry, jsonObject);
} else {
warnOfInvalidJsonAttribute(entryKey);
}
}
/**
* Creates a {@link JsonObject} with only the valid options for the Plugins' properties specified in the {@link #ALLOWED_PLUGIN_PROPERTIES}.
*/
protected void validatePluginsAttribute(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonArray().get(0).getAsJsonObject().entrySet();
JsonObject objectToBeAdded = createJsonObject(entries, ALLOWED_PLUGIN_PROPERTIES);
JsonArray jsonArray = new JsonArray();
if (objectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonArray.add(objectToBeAdded);
jsonObject.add(entry.getKey(), jsonArray);
}
/**
* Creates a {@link JsonObject} with only the valid options for the Error's properties specified in the {@link #ALLOWED_ERROR_PROPERTIES}.
*/
protected void validateErrorAttribute(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonObject().entrySet();
JsonObject objectToBeAdded = createJsonObject(entries, ALLOWED_ERROR_PROPERTIES);
if (objectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonObject.add(entry.getKey(), objectToBeAdded);
}
protected JsonObject createJsonObject(Set<Map.Entry<String, JsonElement>> entries, List<String> allowedProperties) {
JsonObject objectToBeAdded = new JsonObject();
for (Map.Entry<String, JsonElement> recursiveEntry : entries) {
String entryKey = recursiveEntry.getKey();
if (!allowedProperties.contains(entryKey)) {
warnOfInvalidJsonAttribute(entryKey);
continue;
}
objectToBeAdded.add(entryKey, recursiveEntry.getValue());
}
return objectToBeAdded;
}
protected void warnOfInvalidJsonAttribute(String entryKey) {
logger.warn("The JSON attribute [{}] is not a valid option, therefore, it will be ignored.", entryKey);
jsonConfigValidator.validateJsonConfiguration(jsonConfig);
}
/**

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Map;
public interface JsonConfigAttributeValidator {
void validate(Map.Entry<String, JsonElement> entry, JsonObject jsonObject);
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JsonConfigValidator {
protected Logger logger = LogManager.getLogger(getClass());
private static final List<String> ALLOWED_PRIMITIVE_PROPERTIES = List.of("appTitle", "footer", "loginFooter", "logo", "minilogo", "banner", "docBase", "apidocs");
private static final List<String> ALLOWED_DYNAMIC_PROPERTIES = List.of("error", "theme", "plugins", "keyboardOptions", "userCard", "docHelpMappings");
@Inject
private List<JsonConfigAttributeValidator> attributes;
public void validateJsonConfiguration(String jsonConfig) {
if (StringUtils.isBlank(jsonConfig)) {
return;
}
JsonObject jsonObject = new JsonObject();
try {
JsonElement jsonElement = JsonParser.parseString(jsonConfig);
Set<Map.Entry<String, JsonElement>> entries = jsonElement.getAsJsonObject().entrySet();
entries.forEach(entry -> validateJsonAttributes(entry, jsonObject));
} catch (JsonSyntaxException exception) {
logger.error("The following exception was thrown while parsing the JSON object: [{}].", exception.getMessage());
throw new CloudRuntimeException("Specified JSON configuration is not a valid JSON object.");
}
}
/**
* Validates the informed JSON attributes considering the allowed properties by the API, any invalid option is ignored.
* All valid options are added to a {@link JsonObject} that will be considered as the final JSON configuration used by the GUI theme.
*/
private void validateJsonAttributes(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
JsonElement entryValue = entry.getValue();
String entryKey = entry.getKey();
if (entryValue.isJsonPrimitive() && ALLOWED_PRIMITIVE_PROPERTIES.contains(entryKey)) {
logger.trace("The JSON attribute [{}] is a valid option.", entryKey);
jsonObject.add(entryKey, entryValue);
} else if (ALLOWED_DYNAMIC_PROPERTIES.contains(entryKey)) {
attributes.forEach(attribute -> attribute.validate(entry, jsonObject));
} else {
logger.warn("The JSON attribute [{}] is not a valid option, therefore, it will be ignored.", entryKey);
}
}
}

View File

@ -0,0 +1,72 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator.attributes;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigAttributeValidator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class AttributeBase implements JsonConfigAttributeValidator {
protected Logger logger = LogManager.getLogger(getClass());
protected abstract String getAttributeName();
protected abstract List<String> getAllowedProperties();
@Override
public void validate(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
if (!getAttributeName().equals(entry.getKey())) {
return;
}
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonObject().entrySet();
JsonObject objectToBeAdded = createJsonObject(entries, getAllowedProperties());
if (!objectToBeAdded.entrySet().isEmpty()) {
jsonObject.add(entry.getKey(), objectToBeAdded);
}
}
/**
* Creates a {@link JsonObject} with only the valid options for the attribute properties specified in the allowedProperties parameter.
*/
public JsonObject createJsonObject(Set<Map.Entry<String, JsonElement>> entries, List<String> allowedProperties) {
JsonObject objectToBeAdded = new JsonObject();
for (Map.Entry<String, JsonElement> recursiveEntry : entries) {
String entryKey = recursiveEntry.getKey();
if (!allowedProperties.contains(entryKey)) {
warnOfInvalidJsonAttribute(entryKey);
continue;
}
objectToBeAdded.add(entryKey, recursiveEntry.getValue());
}
logger.trace("JSON object with valid options: {}.", objectToBeAdded);
return objectToBeAdded;
}
protected void warnOfInvalidJsonAttribute(String entryKey) {
logger.warn("The JSON attribute [{}] is not a valid option, therefore, it will be ignored.", entryKey);
}
}

View File

@ -0,0 +1,32 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator.attributes;
import java.util.List;
public class ErrorAttribute extends AttributeBase {
@Override
protected String getAttributeName() {
return "error";
}
@Override
protected List<String> getAllowedProperties() {
return List.of("403", "404", "500");
}
}

View File

@ -0,0 +1,60 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator.attributes;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PluginsAttribute extends AttributeBase {
@Override
protected String getAttributeName() {
return "plugins";
}
@Override
protected List<String> getAllowedProperties() {
return List.of("name", "path", "icon", "isExternalLink");
}
@Override
public void validate(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
if (!getAttributeName().equals(entry.getKey())) {
return;
}
JsonArray jsonArrayResult = new JsonArray();
JsonArray sourceJsonArray = entry.getValue().getAsJsonArray();
for (JsonElement jsonElement : sourceJsonArray) {
Set<Map.Entry<String, JsonElement>> pluginEntries = jsonElement.getAsJsonObject().entrySet();
JsonObject pluginObjectToBeAdded = createJsonObject(pluginEntries, getAllowedProperties());
if (pluginObjectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonArrayResult.add(pluginObjectToBeAdded);
}
jsonObject.add(entry.getKey(), jsonArrayResult);
}
}

View File

@ -0,0 +1,35 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator.attributes;
import java.util.List;
public class ThemeAttribute extends AttributeBase {
@Override
protected String getAttributeName() {
return "theme";
}
@Override
protected List<String> getAllowedProperties() {
return List.of("@layout-mode", "@logo-background-color", "@mini-logo-background-color", "@navigation-background-color",
"@project-nav-background-color", "@project-nav-text-color", "@navigation-text-color", "@primary-color", "@link-color", "@link-hover-color", "@loading-color", "@processing-color",
"@success-color", "@warning-color", "@error-color", "@font-size-base", "@heading-color", "@text-color", "@text-color-secondary", "@disabled-color", "@border-color-base", "@border-radius-base",
"@box-shadow-base", "@logo-width", "@logo-height", "@mini-logo-width", "@mini-logo-height", "@banner-width", "@banner-height", "@error-width", "@error-height");
}
}

View File

@ -0,0 +1,80 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.gui.theme.json.config.validator.attributes;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class UserCardAttribute extends AttributeBase {
private static final List<String> ALLOWED_USER_CARD_LINKS_PROPERTIES = List.of("title", "text", "link", "icon");
private static final String LINKS = "links";
@Override
protected String getAttributeName() {
return "userCard";
}
@Override
protected List<String> getAllowedProperties() {
return List.of("title", "icon", "links");
}
@Override
public void validate(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
if (!getAttributeName().equals(entry.getKey())) {
return;
}
Set<Map.Entry<String, JsonElement>> entries = entry.getValue().getAsJsonObject().entrySet();
JsonObject objectToBeAdded = new JsonObject();
for (Map.Entry<String, JsonElement> recursiveEntry : entries) {
String entryKey = recursiveEntry.getKey();
if (!getAllowedProperties().contains(entryKey)) {
warnOfInvalidJsonAttribute(entryKey);
continue;
}
if (LINKS.equals(entryKey)) {
createLinkJsonObject(recursiveEntry, jsonObject);
}
objectToBeAdded.add(entryKey, recursiveEntry.getValue());
}
}
private void createLinkJsonObject(Map.Entry<String, JsonElement> entry, JsonObject jsonObject) {
JsonArray jsonArrayResult = new JsonArray();
JsonArray sourceJsonArray = entry.getValue().getAsJsonArray();
for (JsonElement jsonElement : sourceJsonArray) {
Set<Map.Entry<String, JsonElement>> linkEntries = jsonElement.getAsJsonObject().entrySet();
JsonObject linkObjectToBeAdded = createJsonObject(linkEntries, ALLOWED_USER_CARD_LINKS_PROPERTIES);
if (linkObjectToBeAdded.entrySet().isEmpty()) {
return;
}
jsonArrayResult.add(linkObjectToBeAdded);
}
jsonObject.add(entry.getKey(), jsonArrayResult);
}
}

View File

@ -83,4 +83,9 @@
<bean id="domainHelper" class="com.cloud.utils.DomainHelper" />
<bean id="jsonConfigValidator" class="org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator" />
<bean id="errorAttribute" class="org.apache.cloudstack.gui.theme.json.config.validator.attributes.ErrorAttribute" />
<bean id="pluginsAttribute" class="org.apache.cloudstack.gui.theme.json.config.validator.attributes.PluginsAttribute" />
<bean id="themeAttribute" class="org.apache.cloudstack.gui.theme.json.config.validator.attributes.ThemeAttribute" />
<bean id="userCardAttribute" class="org.apache.cloudstack.gui.theme.json.config.validator.attributes.UserCardAttribute" />
</beans>

View File

@ -251,7 +251,7 @@ export default {
this.parentToggleSetting(false)
},
downloadSetting () {
this.downloadObjectAsJson(this.uiSettings)
this.downloadObjectAsJson(this.$config.theme)
},
resetSetting () {
this.uiSettings = {}

View File

@ -69,12 +69,14 @@ async function applyDynamicCustomization (response) {
vueProps.$config.logo = jsonConfig?.logo ?? vueProps.$config.logo
vueProps.$config.minilogo = jsonConfig?.minilogo ?? vueProps.$config.minilogo
vueProps.$config.banner = jsonConfig?.banner ?? vueProps.$config.banner
vueProps.$config.docBase = jsonConfig?.docBase ?? vueProps.$config.docBase
vueProps.$config.apidocs = jsonConfig?.apidocs ?? vueProps.$config.apidocs
vueProps.$config.docHelpMappings = jsonConfig?.docHelpMappings ?? vueProps.$config.docHelpMappings
vueProps.$config.keyboardOptions = jsonConfig?.keyboardOptions ?? vueProps.$config.keyboardOptions
if (jsonConfig?.error) {
vueProps.$config.error[403] = jsonConfig?.error[403] ?? vueProps.$config.error[403]
vueProps.$config.error[404] = jsonConfig?.error[404] ?? vueProps.$config.error[404]
vueProps.$config.error[500] = jsonConfig?.error[500] ?? vueProps.$config.error[500]
}
applyJsonConfigToObject(jsonConfig?.error, vueProps.$config.error)
applyJsonConfigToObject(jsonConfig?.userCard, vueProps.$config.userCard)
applyJsonConfigToObject(jsonConfig?.theme, vueProps.$config.theme)
if (jsonConfig?.plugins) {
jsonConfig.plugins.forEach(plugin => {
@ -82,12 +84,27 @@ async function applyDynamicCustomization (response) {
})
}
if (vueProps.$store) {
vueProps.$store.dispatch('SetDarkMode', (vueProps.$config.theme['@layout-mode'] === 'dark'))
}
window.less.modifyVars(vueProps.$config.theme)
vueProps.$config.favicon = jsonConfig?.favicon ?? vueProps.$config.favicon
vueProps.$config.css = response?.css ?? null
await applyStaticCustomization(vueProps.$config.favicon, vueProps.$config.css)
}
function applyJsonConfigToObject (sourceConfig, targetObject) {
if (!sourceConfig) {
return
}
for (const [variableName, value] of Object.entries(targetObject)) {
targetObject[variableName] = sourceConfig?.[variableName] ?? value
}
}
async function applyStaticCustomization (favicon, css) {
document.getElementById('favicon').href = favicon