This is an automated email from the ASF dual-hosted git repository.

fabricio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 3166e64891f Add support for new variables to the GUI whitelabel 
runtime system (#12760)
3166e64891f is described below

commit 3166e64891fc75d4d32b66d874cff3f613b09b52
Author: Henrique Sato <[email protected]>
AuthorDate: Fri Apr 17 10:59:50 2026 -0300

    Add support for new variables to the GUI whitelabel runtime system (#12760)
    
    * Add support for new variables to the GUI whitelabel runtime system
    
    * Address review
---
 .../cloudstack/gui/theme/GuiThemeServiceImpl.java  | 110 +--------------------
 .../validator/JsonConfigAttributeValidator.java    |  27 +++++
 .../json/config/validator/JsonConfigValidator.java |  76 ++++++++++++++
 .../config/validator/attributes/AttributeBase.java |  72 ++++++++++++++
 .../validator/attributes/ErrorAttribute.java       |  40 ++++++++
 .../validator/attributes/PluginsAttribute.java     |  68 +++++++++++++
 .../validator/attributes/ThemeAttribute.java       |  43 ++++++++
 .../validator/attributes/UserCardAttribute.java    |  88 +++++++++++++++++
 .../core/spring-server-core-misc-context.xml       |   5 +
 ui/src/components/view/Setting.vue                 |   2 +-
 ui/src/utils/guiTheme.js                           |  27 ++++-
 11 files changed, 447 insertions(+), 111 deletions(-)

diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java 
b/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java
index 6fb6a1235f7..9a92b9bef01 100644
--- 
a/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeServiceImpl.java
@@ -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", 
"defaultLanguage");
-
-    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);
     }
 
     /**
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigAttributeValidator.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigAttributeValidator.java
new file mode 100644
index 00000000000..a3d6a7b4ef9
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigAttributeValidator.java
@@ -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);
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigValidator.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigValidator.java
new file mode 100644
index 00000000000..69a707d4d4d
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/JsonConfigValidator.java
@@ -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", "defaultLanguage");
+    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);
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/AttributeBase.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/AttributeBase.java
new file mode 100644
index 00000000000..b4bafb1f5da
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/AttributeBase.java
@@ -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);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ErrorAttribute.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ErrorAttribute.java
new file mode 100644
index 00000000000..40e519f8133
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ErrorAttribute.java
@@ -0,0 +1,40 @@
+// 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;
+
+/**
+ * Specific validator for the "error" object within the GUI theme JSON 
configuration.
+ *
+ * <p>
+ * This component is defined as a bean in the Spring XML configuration and is 
automatically injected into
+ * the {@link 
org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator} 
attribute list.
+ * </p>
+ */
+public class ErrorAttribute extends AttributeBase {
+
+    @Override
+    protected String getAttributeName() {
+        return "error";
+    }
+
+    @Override
+    protected List<String> getAllowedProperties() {
+        return List.of("403", "404", "500");
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/PluginsAttribute.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/PluginsAttribute.java
new file mode 100644
index 00000000000..1e0fa64669f
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/PluginsAttribute.java
@@ -0,0 +1,68 @@
+// 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;
+
+/**
+ * Specific validator for the "plugins" object within the GUI theme JSON 
configuration.
+ *
+ * <p>
+ * This component is defined as a bean in the Spring XML configuration and is 
automatically injected into
+ * the {@link 
org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator} 
attribute list.
+ * </p>
+ */
+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);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ThemeAttribute.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ThemeAttribute.java
new file mode 100644
index 00000000000..92c74626b89
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/ThemeAttribute.java
@@ -0,0 +1,43 @@
+// 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;
+
+/**
+ * Specific validator for the "theme" object within the GUI theme JSON 
configuration.
+ *
+ * <p>
+ * This component is defined as a bean in the Spring XML configuration and is 
automatically injected into
+ * the {@link 
org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator} 
attribute list.
+ * </p>
+ */
+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");
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/UserCardAttribute.java
 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/UserCardAttribute.java
new file mode 100644
index 00000000000..c6dc5dc6910
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cloudstack/gui/theme/json/config/validator/attributes/UserCardAttribute.java
@@ -0,0 +1,88 @@
+// 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;
+
+/**
+ * Specific validator for the "userCard" object within the GUI theme JSON 
configuration.
+ *
+ * <p>
+ * This component is defined as a bean in the Spring XML configuration and is 
automatically injected into
+ * the {@link 
org.apache.cloudstack.gui.theme.json.config.validator.JsonConfigValidator} 
attribute list.
+ * </p>
+ */
+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);
+    }
+}
diff --git 
a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
 
b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
index f4fd57d59fc..755c50f7a3a 100644
--- 
a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
+++ 
b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml
@@ -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>
diff --git a/ui/src/components/view/Setting.vue 
b/ui/src/components/view/Setting.vue
index 4987a0bd612..5f1db1445ee 100644
--- a/ui/src/components/view/Setting.vue
+++ b/ui/src/components/view/Setting.vue
@@ -251,7 +251,7 @@ export default {
       this.parentToggleSetting(false)
     },
     downloadSetting () {
-      this.downloadObjectAsJson(this.uiSettings)
+      this.downloadObjectAsJson(this.$config.theme)
     },
     resetSetting () {
       this.uiSettings = {}
diff --git a/ui/src/utils/guiTheme.js b/ui/src/utils/guiTheme.js
index 6499539f052..438ce9333a4 100644
--- a/ui/src/utils/guiTheme.js
+++ b/ui/src/utils/guiTheme.js
@@ -70,13 +70,15 @@ 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
   vueProps.$config.defaultLanguage = vueProps.$localStorage.get('LOCALE') ?? 
jsonConfig?.defaultLanguage ?? vueProps.$config.defaultLanguage
 
-  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 => {
@@ -84,6 +86,11 @@ 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
 
@@ -95,6 +102,16 @@ async function applyDynamicCustomization (response) {
   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
 

Reply via email to