CVE-2023-26480: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Platform is a generic wiki platform. Starting in version 12.10, a user without script rights can introduce a stored cross-site scripting by using the Live Data macro. This has been patched in XWiki 14.9, 14.4.7, and 13.10.10. There are no known workarounds.

CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-03-02T18:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: JavaScript
Files changed: xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/pom.xml, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacro.java, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroConfiguration.java, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroRights.java, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources.properties, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_de.properties, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_fr.properties, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/META-INF/components.txt, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/test/java/org/xwiki/livedata/internal/macro/LiveDataMacroTest.java, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package-lock.json, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package.json, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/BaseDisplayer.spec.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerDocTitle.spec.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerHtml.spec.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerLink.spec.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/displayerTestsHelper.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/BaseDisplayer.vue, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerActions.vue, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerHtml.vue, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerLink.vue, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerXObjectProperty.vue, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/displayerMixin.js, xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/webjar/Logic.js
Commit date: 2022-10-20 11:18:19 +0200
Commit message: XWIKI-20143: Improved Live Data escaping

Diff: diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/pom.xml b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/pom.xml
index 6894e5180248d..af2d6beba6577 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/pom.xml
@@ -56,10 +56,15 @@
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>xwiki-platform-skin-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-bridge</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <!-- Test dependencies -->
     <dependency>
       <groupId>org.xwiki.rendering</groupId>
       <artifactId>xwiki-rendering-test</artifactId>
       <version>${rendering.version}</version>
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacro.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacro.java
index cb8657f31c069..f8b81b71b0296 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacro.java
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacro.java
@@ -17,39 +17,22 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.livedata.internal.macro;
 
-import java.net.URL;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.commons.lang3.StringUtils;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.livedata.LiveDataConfiguration;
 import org.xwiki.livedata.LiveDataConfigurationResolver;
-import org.xwiki.livedata.LiveDataLayoutDescriptor;
-import org.xwiki.livedata.LiveDataMeta;
-import org.xwiki.livedata.LiveDataPaginationConfiguration;
-import org.xwiki.livedata.LiveDataQuery;
-import org.xwiki.livedata.LiveDataQuery.Constraint;
-import org.xwiki.livedata.LiveDataQuery.Filter;
-import org.xwiki.livedata.LiveDataQuery.SortEntry;
-import org.xwiki.livedata.LiveDataQuery.Source;
-import org.xwiki.livedata.internal.JSONMerge;
 import org.xwiki.livedata.macro.LiveDataMacroParameters;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.GroupBlock;
 import org.xwiki.rendering.macro.AbstractMacro;
 import org.xwiki.rendering.macro.MacroExecutionException;
@@ -57,10 +40,13 @@
 import org.xwiki.rendering.transformation.MacroTransformationContext;
 import org.xwiki.skinx.SkinExtension;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+
 /**
  * Display dynamic lists of data.
  * 
  * @version $Id$
  * @since 12.10
@@ -68,31 +54,21 @@
 @Component
 @Named("liveData")
 @Singleton
 public class LiveDataMacro extends AbstractMacro<LiveDataMacroParameters>
 {
-    private static final Pattern PATTERN_COMMA = Pattern.compile("\\s*,\\s*");
-
-    private static final String UTF8 = "UTF-8";
-
     /**
      * Used to add default Live Data configuration values.
      */
     @Inject
     private LiveDataConfigurationResolver<LiveDataConfiguration> defaultLiveDataConfigResolver;
 
-    /**
-     * Used to read the Live Data configuration from the macro content.
-     */
     @Inject
-    private LiveDataConfigurationResolver<String> stringLiveDataConfigResolver;
+    private LiveDataMacroConfiguration liveDataMacroConfiguration;
 
-    /**
-     * Used to merge the Live Data configuration built from the macro parameters with the live data configuration read
-     * from the macro content.
-     */
-    private JSONMerge jsonMerge = new JSONMerge();
+    @Inject
+    private LiveDataMacroRights liveDataMacroRights;
 
     /**
      * The component used to load the JavaScript code of the Live Data widget.
      */
     @Inject
@@ -113,181 +89,40 @@ public LiveDataMacro()
     @Override
     public List<Block> execute(LiveDataMacroParameters parameters, String content, MacroTransformationContext context)
         throws MacroExecutionException
     {
         // Load the JavaScript code of the Live Data widget.
-        Map<String, Object> skinExtensionParameters = Collections.singletonMap("forceSkinAction", Boolean.TRUE);
+        Map<String, Object> skinExtensionParameters = singletonMap("forceSkinAction", Boolean.TRUE);
         this.jsfx.use("uicomponents/widgets/liveData.js", skinExtensionParameters);
 
         GroupBlock output = new GroupBlock();
         output.setParameter("class", "liveData loading");
         if (parameters.getId() != null) {
             output.setParameter("id", parameters.getId());
         }
         try {
             // Compute the live data configuration based on the macro parameters.
-            LiveDataConfiguration liveDataConfig = getLiveDataConfiguration(content, parameters);
+            LiveDataConfiguration liveDataConfig = this.liveDataMacroConfiguration.getLiveDataConfiguration(content,
+                parameters);
             // Add the default values.
             liveDataConfig = this.defaultLiveDataConfigResolver.resolve(liveDataConfig);
             // Serialize as JSON.
             ObjectMapper objectMapper = new ObjectMapper();
             output.setParameter("data-config", objectMapper.writeValueAsString(liveDataConfig));
+            // The content is trusted if the author has script right, or if no advanced configuration is used (i.e., 
+            // no macro content), and we are running in a trusted context.
+            boolean trustedContent =
+                StringUtils.isBlank(content) || (this.liveDataMacroRights.authorHasScriptRight()
+                    && !context.getTransformationContext().isRestricted());
+            output.setParameter("data-config-content-trusted", Boolean.toString(trustedContent));
         } catch (Exception e) {
             throw new MacroExecutionException("Failed to generate live data configuration from macro parameters.", e);
         }
-        return Collections.singletonList(output);
+        return singletonList(output);
     }
 
     @Override
     public boolean supportsInlineMode()
     {
         return false;
     }
-
-    private LiveDataConfiguration getLiveDataConfiguration(String content, LiveDataMacroParameters parameters)
-        throws Exception
-    {
-        String json = StringUtils.defaultIfBlank(content, "{}");
-        LiveDataConfiguration advancedConfig = this.stringLiveDataConfigResolver.resolve(json);
-        LiveDataConfiguration basicConfig = getLiveDataConfiguration(parameters);
-        // Make sure both configurations have the same id so that they are properly merged.
-        advancedConfig.setId(basicConfig.getId());
-        return this.jsonMerge.merge(advancedConfig, basicConfig);
-    }
-
-    private LiveDataConfiguration getLiveDataConfiguration(LiveDataMacroParameters parameters) throws Exception
-    {
-        LiveDataConfiguration liveDataConfig = new LiveDataConfiguration();
-        liveDataConfig.setId(parameters.getId());
-        liveDataConfig.setQuery(getQuery(parameters));
-        liveDataConfig.setMeta(getMeta(parameters));
-        return liveDataConfig;
-    }
-
-    private LiveDataQuery getQuery(LiveDataMacroParameters parameters) throws Exception
-    {
-        LiveDataQuery query = new LiveDataQuery();
-        query.setProperties(getProperties(parameters.getProperties()));
-        query.setSource(new Source(parameters.getSource()));
-        query.getSource().getParameters().putAll(getSourceParameters(parameters.getSourceParameters()));
-        query.setSort(getSortEntries(parameters.getSort()));
-        query.setFilters(getFilters(parameters.getFilters()));
-        query.setLimit(parameters.getLimit());
-        query.setOffset(parameters.getOffset());
-        return query;
-    }
-
-    private List<String> getProperties(String properties)
-    {
-        if (properties == null) {
-            return null;
-        } else {
-            return Stream.of(PATTERN_COMMA.split(properties)).collect(Collectors.toList());
-        }
-    }
-
-    private Map<String, Object> getSourceParameters(String sourceParametersString) throws Exception
-    {
-        if (StringUtils.isEmpty(sourceParametersString)) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, List<String>> urlParams = getURLParameters('?' + sourceParametersString);
-        Map<String, Object> sourceParams = new HashMap<>();
-        for (Map.Entry<String, List<String>> entry : urlParams.entrySet()) {
-            if (entry.getValue().size() > 1) {
-                sourceParams.put(entry.getKey(), entry.getValue());
-            } else {
-                sourceParams.put(entry.getKey(), entry.getValue().get(0));
-            }
-        }
-        return sourceParams;
-    }
-
-    private List<SortEntry> getSortEntries(String sort)
-    {
-        if (sort == null) {
-            return null;
-        } else {
-            return Stream.of(PATTERN_COMMA.split(sort)).filter(StringUtils::isNotEmpty).map(this::getSortEntry)
-                .collect(Collectors.toList());
-        }
-    }
-
-    private SortEntry getSortEntry(String sortEntryString)
-    {
-        SortEntry sortEntry = new SortEntry();
-        sortEntry.setDescending(sortEntryString.endsWith(":desc"));
-        if (sortEntry.isDescending() || sortEntryString.endsWith(":asc")) {
-            sortEntry.setProperty(StringUtils.substringBeforeLast(sortEntryString, ":"));
-        } else {
-            sortEntry.setProperty(sortEntryString);
-        }
-        return sortEntry;
-    }
-
-    private List<Filter> getFilters(String filtersString) throws Exception
-    {
-        List<Filter> filters = getURLParameters('?' + StringUtils.defaultString(filtersString)).entrySet().stream()
-            .map(this::getFilter).collect(Collectors.toList());
-        return filters.isEmpty() ? null : filters;
-    }
-
-    private Filter getFilter(Map.Entry<String, List<String>> entry)
-    {
-        Filter filter = new Filter();
-        filter.setProperty(entry.getKey());
-        filter.getConstraints().addAll(entry.getValue().stream().map(Constraint::new).collect(Collectors.toList()));
-        return filter;
-    }
-
-    private LiveDataMeta getMeta(LiveDataMacroParameters parameters)
-    {
-        LiveDataMeta meta = new LiveDataMeta();
-        meta.setLayouts(getLayouts(parameters));
-        meta.setPagination(getPagination(parameters));
-        return meta;
-    }
-
-    private List<LiveDataLayoutDescriptor> getLayouts(LiveDataMacroParameters parameters)
-    {
-        if (parameters.getLayouts() == null) {
-            return null;
-        } else {
-            return Stream.of(PATTERN_COMMA.split(parameters.getLayouts())).map(LiveDataLayoutDescriptor::new)
-                .collect(Collectors.toList());
-        }
-    }
-
-    private LiveDataPaginationConfiguration getPagination(LiveDataMacroParameters parameters)
-    {
-        LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration();
-        pagination.setShowPageSizeDropdown(parameters.getShowPageSizeDropdown());
-        if (parameters.getPageSizes() != null) {
-            pagination.setPageSizes(Stream.of(PATTERN_COMMA.split(parameters.getPageSizes())).map(Integer::parseInt)
-                .collect(Collectors.toList()));
-        }
-        return pagination;
-    }
-
-    private Map<String, List<String>> getURLParameters(String url) throws Exception
-    {
-        URL baseURL = new URL("http://www.xwiki.org");
-        String queryString = new URL(baseURL, url).getQuery();
-        Map<String, List<String>> parameters = new HashMap<>();
-        for (String entry : queryString.split("&")) {
-            String[] parts = entry.split("=", 2);
-            String key = URLDecoder.decode(parts[0], UTF8);
-            if (key.isEmpty()) {
-                continue;
-            }
-            String value = parts.length == 2 ? URLDecoder.decode(parts[1], UTF8) : "";
-            List<String> values = parameters.get(key);
-            if (values == null) {
-                values = new ArrayList<>();
-                parameters.put(key, values);
-            }
-            values.add(value);
-        }
-        return parameters;
-    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroConfiguration.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroConfiguration.java
new file mode 100644
index 0000000000000..8e427becb95fa
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroConfiguration.java
@@ -0,0 +1,233 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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 software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.livedata.internal.macro;
+
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.StringUtils;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.livedata.LiveDataConfiguration;
+import org.xwiki.livedata.LiveDataConfigurationResolver;
+import org.xwiki.livedata.LiveDataLayoutDescriptor;
+import org.xwiki.livedata.LiveDataMeta;
+import org.xwiki.livedata.LiveDataPaginationConfiguration;
+import org.xwiki.livedata.LiveDataQuery;
+import org.xwiki.livedata.internal.JSONMerge;
+import org.xwiki.livedata.macro.LiveDataMacroParameters;
+
+/**
+ * Provides services to manipulate configurations for the {@link LiveDataMacroConfiguration}.
+ *
+ * @version $Id$
+ * @since 14.9
+ * @since 14.4.7
+ * @since 13.10.10
+ */
+@Component(roles = LiveDataMacroConfiguration.class)
+@Singleton
+public class LiveDataMacroConfiguration
+{
+    private static final Pattern PATTERN_COMMA = Pattern.compile("\\s*,\\s*");
+
+    private static final String UTF8 = "UTF-8";
+
+    /**
+     * Used to read the Live Data configuration from the macro content.
+     */
+    @Inject
+    private LiveDataConfigurationResolver<String> stringLiveDataConfigResolver;
+
+    /**
+     * Used to merge the Live Data configuration built from the macro parameters with the live data configuration read
+     * from the macro content.
+     */
+    private JSONMerge jsonMerge = new JSONMerge();
+
+    /**
+     * Resolve a complete Live Data configuration from a json advanced configuration (the content) and a set of macro
+     * parameters.
+     *
+     * @param content the string representation of the json live data advanced configuration
+     * @param parameters the Live Data macro parameters
+     * @return the complete Live Data configuration
+     * @throws Exception in case of error when resolving the configuration
+     */
+    public LiveDataConfiguration getLiveDataConfiguration(String content, LiveDataMacroParameters parameters)
+        throws Exception
+    {
+        String json = StringUtils.defaultIfBlank(content, "{}");
+        LiveDataConfiguration advancedConfig = this.stringLiveDataConfigResolver.resolve(json);
+        LiveDataConfiguration basicConfig = getLiveDataConfiguration(parameters);
+        // Make sure both configurations have the same id so that they are properly merged.
+        advancedConfig.setId(basicConfig.getId());
+        return this.jsonMerge.merge(advancedConfig, basicConfig);
+    }
+
+    private LiveDataConfiguration getLiveDataConfiguration(LiveDataMacroParameters parameters) throws Exception
+    {
+        LiveDataConfiguration liveDataConfig = new LiveDataConfiguration();
+        liveDataConfig.setId(parameters.getId());
+        liveDataConfig.setQuery(getQuery(parameters));
+        liveDataConfig.setMeta(getMeta(parameters));
+        return liveDataConfig;
+    }
+
+    private LiveDataQuery getQuery(LiveDataMacroParameters parameters) throws Exception
+    {
+        LiveDataQuery query = new LiveDataQuery();
+        query.setProperties(getProperties(parameters.getProperties()));
+        query.setSource(new LiveDataQuery.Source(parameters.getSource()));
+        query.getSource().getParameters().putAll(getSourceParameters(parameters.getSourceParameters()));
+        query.setSort(getSortEntries(parameters.getSort()));
+        query.setFilters(getFilters(parameters.getFilters()));
+        query.setLimit(parameters.getLimit());
+        query.setOffset(parameters.getOffset());
+        return query;
+    }
+
+    private List<String> getProperties(String properties)
+    {
+        if (properties == null) {
+            return null;
+        } else {
+            return Stream.of(PATTERN_COMMA.split(properties)).collect(Collectors.toList());
+        }
+    }
+
+    private Map<String, Object> getSourceParameters(String sourceParametersString) throws Exception
+    {
+        if (StringUtils.isEmpty(sourceParametersString)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, List<String>> urlParams = getURLParameters('?' + sourceParametersString);
+        Map<String, Object> sourceParams = new HashMap<>();
+        for (Map.Entry<String, List<String>> entry : urlParams.entrySet()) {
+            if (entry.getValue().size() > 1) {
+                sourceParams.put(entry.getKey(), entry.getValue());
+            } else {
+                sourceParams.put(entry.getKey(), entry.getValue().get(0));
+            }
+        }
+        return sourceParams;
+    }
+
+    private List<LiveDataQuery.SortEntry> getSortEntries(String sort)
+    {
+        if (sort == null) {
+            return null;
+        } else {
+            return Stream.of(PATTERN_COMMA.split(sort)).filter(StringUtils::isNotEmpty).map(this::getSortEntry)
+                .collect(Collectors.toList());
+        }
+    }
+
+    private LiveDataQuery.SortEntry getSortEntry(String sortEntryString)
+    {
+        LiveDataQuery.SortEntry sortEntry = new LiveDataQuery.SortEntry();
+        sortEntry.setDescending(sortEntryString.endsWith(":desc"));
+        if (sortEntry.isDescending() || sortEntryString.endsWith(":asc")) {
+            sortEntry.setProperty(StringUtils.substringBeforeLast(sortEntryString, ":"));
+        } else {
+            sortEntry.setProperty(sortEntryString);
+        }
+        return sortEntry;
+    }
+
+    private List<LiveDataQuery.Filter> getFilters(String filtersString) throws Exception
+    {
+        List<LiveDataQuery.Filter> filters =
+            getURLParameters('?' + StringUtils.defaultString(filtersString)).entrySet().stream()
+                .map(this::getFilter).collect(Collectors.toList());
+        return filters.isEmpty() ? null : filters;
+    }
+
+    private LiveDataQuery.Filter getFilter(Map.Entry<String, List<String>> entry)
+    {
+        LiveDataQuery.Filter filter = new LiveDataQuery.Filter();
+        filter.setProperty(entry.getKey());
+        filter.getConstraints()
+            .addAll(entry.getValue().stream().map(LiveDataQuery.Constraint::new).collect(Collectors.toList()));
+        return filter;
+    }
+
+    private LiveDataMeta getMeta(LiveDataMacroParameters parameters)
+    {
+        LiveDataMeta meta = new LiveDataMeta();
+        meta.setLayouts(getLayouts(parameters));
+        meta.setPagination(getPagination(parameters));
+        return meta;
+    }
+
+    private List<LiveDataLayoutDescriptor> getLayouts(LiveDataMacroParameters parameters)
+    {
+        if (parameters.getLayouts() == null) {
+            return null;
+        } else {
+            return Stream.of(PATTERN_COMMA.split(parameters.getLayouts())).map(LiveDataLayoutDescriptor::new)
+                .collect(Collectors.toList());
+        }
+    }
+
+    private LiveDataPaginationConfiguration getPagination(LiveDataMacroParameters parameters)
+    {
+        LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration();
+        pagination.setShowPageSizeDropdown(parameters.getShowPageSizeDropdown());
+        if (parameters.getPageSizes() != null) {
+            pagination.setPageSizes(Stream.of(PATTERN_COMMA.split(parameters.getPageSizes())).map(Integer::parseInt)
+                .collect(Collectors.toList()));
+        }
+        return pagination;
+    }
+
+    private Map<String, List<String>> getURLParameters(String url) throws Exception
+    {
+        URL baseURL = new URL("http://www.xwiki.org");
+        String queryString = new URL(baseURL, url).getQuery();
+        Map<String, List<String>> parameters = new HashMap<>();
+        for (String entry : queryString.split("&")) {
+            String[] parts = entry.split("=", 2);
+            String key = URLDecoder.decode(parts[0], UTF8);
+            if (key.isEmpty()) {
+                continue;
+            }
+            String value = parts.length == 2 ? URLDecoder.decode(parts[1], UTF8) : "";
+            List<String> values = parameters.get(key);
+            if (values == null) {
+                values = new ArrayList<>();
+                parameters.put(key, values);
+            }
+            values.add(value);
+        }
+        return parameters;
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroRights.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroRights.java
new file mode 100644
index 0000000000000..05590fe41c1fe
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/java/org/xwiki/livedata/internal/macro/LiveDataMacroRights.java
@@ -0,0 +1,60 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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 software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.livedata.internal.macro;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorizationManager;
+
+import static org.xwiki.security.authorization.Right.SCRIPT;
+
+/**
+ * Provides the services related to rights for {@link LiveDataMacro}.
+ *
+ * @version $Id$
+ * @since 14.9
+ * @since 14.4.7
+ * @since 13.10.10
+ */
+@Component(roles = LiveDataMacroRights.class)
+@Singleton
+public class LiveDataMacroRights
+{
+    @Inject
+    private DocumentAccessBridge documentAccessBridge;
+
+    @Inject
+    private AuthorizationManager authorizationManager;
+
+    /**
+     * @return {@code true} if the last author of the document where the Live Data macro is executed has script rights
+     *     on said document, {@code false} otherwise
+     */
+    public boolean authorHasScriptRight()
+    {
+        DocumentReference currentDocumentReference = this.documentAccessBridge.getCurrentDocumentReference();
+        DocumentReference currentAuthorReference = this.documentAccessBridge.getCurrentAuthorReference();
+        return this.authorizationManager.hasAccess(SCRIPT, currentAuthorReference, currentDocumentReference);
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources.properties
index 9b99a964a90c9..f27e7566c9e19 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources.properties
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources.properties
@@ -104,11 +104,11 @@ livedata.panel.sort.noneSortable=None of the displayed properties is sortable.
 livedata.panel.sort.direction.ascending=Ascending
 livedata.panel.sort.direction.descending=Descending
 livedata.panel.sort.add=Add sort
 livedata.panel.sort.delete=Delete sort
 
-livedata.displayer.emptyValue=N/A<sup>*</sup>
+livedata.displayer.emptyValue=N/A
 
 livedata.displayer.link.noValue=(no value)
 
 livedata.displayer.boolean.true=True
 livedata.displayer.boolean.false=False
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_de.properties b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_de.properties
index 761db7522b6a4..b012e755eec62 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_de.properties
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_de.properties
@@ -85,11 +85,11 @@ livedata.panel.sort.title=Sortieren
 livedata.panel.sort.noneSortable=Keine der angezeigten Eigenschaften ist sortierbar.
 livedata.panel.sort.direction.ascending=Aufsteigend
 livedata.panel.sort.direction.descending=Absteigend
 livedata.panel.sort.add=Sortierung hinzuf\u00FCgen
 livedata.panel.sort.delete=Sortierung l\u00F6schen
-livedata.displayer.emptyValue=n.v.<sup>*</sup>
+livedata.displayer.emptyValue=n.v.
 livedata.displayer.link.noValue=(kein Wert)
 livedata.displayer.boolean.true=Wahr
 livedata.displayer.boolean.false=Falsch
 livedata.displayer.xObjectProperty.missingDocumentName.errorMessage=Eine XObject-Eigenschaft mit einem fehlenden Dokumentnamen kann nicht gespeichert werden.
 livedata.displayer.xObjectProperty.failedToRetrieveField.errorMessage=Das Feld {0} konnte nicht abgerufen werden.
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_fr.properties b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_fr.properties
index 2215c3dfb1fed..d33a9282f12b4 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_fr.properties
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/ApplicationResources_fr.properties
@@ -85,11 +85,11 @@ livedata.panel.sort.title=Trier
 livedata.panel.sort.noneSortable=Aucun des \u00E9l\u00E9ments affich\u00E9s ne peut \u00EAtre tri\u00E9.
 livedata.panel.sort.direction.ascending=Croissant
 livedata.panel.sort.direction.descending=D\u00E9croissant
 livedata.panel.sort.add=Ajouter un tri
 livedata.panel.sort.delete=Supprimer le tri
-livedata.displayer.emptyValue=SO<sup>*</sup>
+livedata.displayer.emptyValue=SO
 livedata.displayer.link.noValue=(aucune valeur)
 livedata.displayer.boolean.true=Vrai
 livedata.displayer.boolean.false=Faux
 ### Missing: livedata.displayer.xObjectProperty.missingDocumentName.errorMessage=Can't save an XObject property with a missing document name.
 livedata.displayer.xObjectProperty.failedToRetrieveField.errorMessage=Impossible de r\u00E9cup\u00E9rer le champ {0}.
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/META-INF/components.txt
index aadb93280dbd8..9954e28b52893 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/main/resources/META-INF/components.txt
@@ -1 +1,3 @@
 org.xwiki.livedata.internal.macro.LiveDataMacro
+org.xwiki.livedata.internal.macro.LiveDataMacroConfiguration
+org.xwiki.livedata.internal.macro.LiveDataMacroRights
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/test/java/org/xwiki/livedata/internal/macro/LiveDataMacroTest.java b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/test/java/org/xwiki/livedata/internal/macro/LiveDataMacroTest.java
index 454969f0b0b72..291d8c55bc26f 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/test/java/org/xwiki/livedata/internal/macro/LiveDataMacroTest.java
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-macro/src/test/java/org/xwiki/livedata/internal/macro/LiveDataMacroTest.java
@@ -25,10 +25,11 @@
 import org.apache.commons.text.StringEscapeUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
+import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.context.internal.DefaultExecution;
 import org.xwiki.livedata.LiveDataConfiguration;
 import org.xwiki.livedata.LiveDataConfigurationResolver;
 import org.xwiki.livedata.LiveDataQuery;
@@ -41,10 +42,11 @@
 import org.xwiki.rendering.internal.renderer.xhtml.image.DefaultXHTMLImageRenderer;
 import org.xwiki.rendering.internal.renderer.xhtml.image.DefaultXHTMLImageTypeRenderer;
 import org.xwiki.rendering.internal.renderer.xhtml.link.DefaultXHTMLLinkRenderer;
 import org.xwiki.rendering.internal.renderer.xhtml.link.DefaultXHTMLLinkTypeRenderer;
 import org.xwiki.rendering.renderer.PrintRendererFactory;
+import org.xwiki.security.authorization.AuthorizationManager;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.test.mockito.MockitoComponentManager;
@@ -78,11 +80,13 @@
     HTMLElementSanitizerConfiguration.class,
     RestrictedConfigurationSourceProvider.class,
     HTMLDefinitions.class,
     MathMLDefinitions.class,
     SVGDefinitions.class,
-    DefaultExecution.class
+    DefaultExecution.class,
+    LiveDataMacroConfiguration.class,
+    LiveDataMacroRights.class
 })
 class LiveDataMacroTest
 {
     @InjectMockComponents
     private LiveDataMacro liveDataMacro;
@@ -91,10 +95,16 @@
     private LiveDataConfigurationResolver<LiveDataConfiguration> defaultConfigResolver;
 
     @MockComponent
     private LiveDataConfigurationResolver<String> stringConfigResolver;
 
+    @MockComponent
+    private DocumentAccessBridge documentAccessBridge;
+
+    @MockComponent
+    private AuthorizationManager authorizationManager;
+
     private PrintRendererFactory rendererFactory;
 
     @BeforeEach
     void before(MockitoComponentManager componentManager) throws Exception
     {
@@ -115,11 +125,12 @@ public LiveDataConfiguration answer(InvocationOnMock invocation) throws Throwabl
 
     @Test
     void executeWithoutParams() throws Exception
     {
         String expectedConfig = json("{'query':{'source':{}},'meta':{'pagination':{}}}");
-        String expected = "<div class=\"liveData loading\" data-config=\"" + escapeXML(expectedConfig) + "\"></div>";
+        String expected = "<div class=\"liveData loading\" data-config=\"" + escapeXML(expectedConfig) + "\" "
+            + "data-config-content-trusted=\"true\"></div>";
 
         List<Block> blocks = this.liveDataMacro.execute(new LiveDataMacroParameters(), null, null);
         assertBlocks(expected, blocks, this.rendererFactory);
     }
 
@@ -153,12 +164,12 @@ void execute() throws Exception
         expectedConfig.append("      'showPageSizeDropdown':true".trim());
         expectedConfig.append("    }".trim());
         expectedConfig.append("  }".trim());
         expectedConfig.append("}");
 
-        String expected = "<div class=\"liveData loading\" id=\"test\" data-config=\""
-            + escapeXML(json(expectedConfig.toString())) + "\"></div>";
+        String expected = String.format("<div class=\"liveData loading\" id=\"test\" data-config=\"%s\" "
+            + "data-config-content-trusted=\"true\"></div>", escapeXML(json(expectedConfig.toString())));
 
         LiveDataMacroParameters parameters = new LiveDataMacroParameters();
         parameters.setId("test");
         parameters.setSource("users");
         parameters.setSourceParameters("wiki=dev&group=apps");
@@ -212,12 +223,12 @@ void executeWithContent() throws Exception
         expectedConfig.append("  'meta':{".trim());
         expectedConfig.append("    'pagination':{}".trim());
         expectedConfig.append("  }".trim());
         expectedConfig.append("}");
 
-        String expected = "<div class=\"liveData loading\" id=\"test\" data-config=\""
-            + escapeXML(json(expectedConfig.toString())) + "\"></div>";
+        String expected = String.format("<div class=\"liveData loading\" id=\"test\" data-config=\"%s\" "
+            + "data-config-content-trusted=\"false\"></div>", escapeXML(json(expectedConfig.toString())));
 
         List<Block> blocks = this.liveDataMacro.execute(parameters, "{...}", null);
         assertBlocks(expected, blocks, this.rendererFactory);
     }
 
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package-lock.json b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package-lock.json
index 43a7527d79602..7647ec377bd06 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package-lock.json
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package-lock.json
@@ -6,10 +6,11 @@
   "packages": {
     "": {
       "name": "xwiki-livedata-vue",
       "version": "1.0.0",
       "dependencies": {
+        "dompurify": "2.4.0",
         "lz-string": "1.4.4",
         "u-node": "0.0.2",
         "vue": "2.7.10",
         "vue-i18n": "8.27.2",
         "vue-tippy": "4.16.0",
@@ -6473,10 +6474,15 @@
       },
       "funding": {
         "url": "https://github.com/fb55/domhandler?sponsor=1"
       }
     },
+    "node_modules/dompurify": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
+      "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
+    },
     "node_modules/domutils": {
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
       "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
       "dev": true,
@@ -21825,10 +21831,15 @@
       "dev": true,
       "requires": {
         "domelementtype": "^2.2.0"
       }
     },
+    "dompurify": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
+      "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
+    },
     "domutils": {
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
       "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
       "dev": true,
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package.json b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package.json
index 20ebce589df4e..9c65db637ce26 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package.json
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/config/package.json
@@ -17,11 +17,12 @@
     "vue-i18n": "8.27.2",
     "lz-string": "1.4.4",
     "u-node": "0.0.2",
     "vuedraggable": "2.24.3",
     "vue-tippy": "4.16.0",
-    "vue2-touch-events": "3.2.2"
+    "vue2-touch-events": "3.2.2",
+    "dompurify": "2.4.0"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "5.0.8",
     "@vue/cli-plugin-eslint": "5.0.8",
     "@vue/cli-service": "5.0.8",
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/BaseDisplayer.spec.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/BaseDisplayer.spec.js
index e8de0b933a029..22169328d555c 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/BaseDisplayer.spec.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/BaseDisplayer.spec.js
@@ -79,11 +79,11 @@ describe('BaseDisplayer.vue', () => {
           return false;
         }
       }
     });
 
-    expect(wrapper.find('div.view > div').html()).toBe('<div><span></span> <span>livedata.displayer.emptyValue</span></div>');
+    expect(wrapper.find('div.view > div').text()).toBe('livedata.displayer.emptyValue*');
   })
 
   it('Renders a viewable entry with an empty content', () => {
     const wrapper = initWrapper(BaseDisplayer, {
       props: {
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerDocTitle.spec.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerDocTitle.spec.js
index 0e7bedd94c9d2..14595b516273c 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerDocTitle.spec.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerDocTitle.spec.js
@@ -35,11 +35,12 @@ describe('DisplayerDocTitle.vue', () => {
       logic: {
         getDisplayerDescriptor() {
           return {
             propertyHref: 'colorHref'
           };
-        }
+        },
+        isContentTrusted: () => true
       }
     });
     expect(wrapper.find('a').html()).toBe('<a href="entryLink" class="">Test <sup>1</sup></a>');
   })
 
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerHtml.spec.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerHtml.spec.js
index 08885161d436b..53128502fffa4 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerHtml.spec.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerHtml.spec.js
@@ -26,10 +26,28 @@ describe('DisplayerHtml.vue', () => {
     const wrapper = initWrapper(DisplayerHtml, {
       props: {
         entry: {
           color: '<strong>some content</strong>'
         }
+      },
+      logic: {
+        isContentTrusted: () => true
+      }
+    })
+    expect(wrapper.find('.html-wrapper').html())
+      .toBe('<div class="html-wrapper"><strong>some content</strong></div>')
+  })
+
+  it('Renders an entry in view mode with untrusted content', () => {
+    const wrapper = initWrapper(DisplayerHtml, {
+      props: {
+        entry: {
+          color: '<strong>some content<script>console.log("hello world")</script></strong>'
+        }
+      },
+      logic: {
+        isContentTrusted: () => false
       }
     })
     expect(wrapper.find('.html-wrapper').html())
       .toBe('<div class="html-wrapper"><strong>some content</strong></div>')
   })
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerLink.spec.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerLink.spec.js
index 86c03e800a4e5..d757f4d77560b 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerLink.spec.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/DisplayerLink.spec.js
@@ -41,10 +41,45 @@ describe('DisplayerLink.vue', () => {
     });
     expect(wrapper.text()).toMatch('yellow')
     expect(wrapper.find('a').element.href).toBe('http://localhost/entryLink');
   })
 
+  it('Renders an entry in view mode with untrusted content', () => {
+    const logic = {
+      getDisplayerDescriptor() {
+        return {
+          propertyHref: 'colorHref'
+        };
+      },
+      isContentTrusted: () => false
+    };
+    const wrapperHttpLink = initWrapper(DisplayerLink, {
+      props: {
+        entry: {
+          color: 'yellow<script>console.log("hello")</script>',
+          colorHref: 'http://test.com'
+        }
+      },
+      logic
+    });
+    expect(wrapperHttpLink.text()).toMatch('yellow')
+    expect(wrapperHttpLink.find('a').element.href).toBe('http://test.com/');
+
+    const wrapperJavascriptLink = initWrapper(DisplayerLink, {
+      props: {
+        entry: {
+          color: 'yellow<script>console.log("hello")</script>',
+          colorHref: 'javascript:console.log("world")'
+        }
+      },
+      logic
+    });
+
+    expect(wrapperJavascriptLink.text()).toMatch('yellow')
+    expect(wrapperJavascriptLink.find('a').element.href).toBe('http://localhost/#');
+  })
+
   it('Renders an entry in view mode with an empty content', () => {
     const wrapper = initWrapper(DisplayerLink, {
       props: {
         entry: {
           colorHref: 'entryLink'
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/displayerTestsHelper.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/displayerTestsHelper.js
index 655b99c3cb92a..363060490487a 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/displayerTestsHelper.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/tests/unit/displayers/displayerTestsHelper.js
@@ -156,11 +156,12 @@ export function initWrapper(displayer, {props, logic, editBus, mocks})
         },
         footnotes: {
           put() {},
           reset() {},
           list() { return []; }
-        }
+        },
+        isContentTrusted: () => true
       }, logic),
     },
     mocks: Object.assign({
       $t: (key) => key
     }, mocks)
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/BaseDisplayer.vue b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/BaseDisplayer.vue
index 2c975acd78d10..b64053d2c0fd5 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/BaseDisplayer.vue
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/BaseDisplayer.vue
@@ -69,14 +69,13 @@
               However, this is useful if a custom displayer only implement
               its Editor widget, as a default Viewer widget would still be provided
             -->
             <span>{{ value }}</span>
           </slot>
-          <span
-            v-if="!isViewable"
-            v-html="$t('livedata.displayer.emptyValue')"
-          ></span>
+          <span v-if="!isViewable">
+            {{ $t('livedata.displayer.emptyValue') }}<sup>*</sup>
+          </span>
         </div>
 
         <!-- The slot containing the displayer Editor widget -->
         <div @keypress.enter="applyEdit"
              @keydown.esc.capture="cancelEdit"
@@ -114,11 +113,11 @@
           v-if="isEditable"
           :displayer="{ setEdit: () => { setEdit() } }"
           :close-popover="closePopover"
         />
         <ActionFollowLink
-          :displayer="{ href }"
+          :displayer="{ href: sanitizeUrl(href) }"
           v-if="href"
           :close-popover="closePopover"
         />
       </div>
     </template>
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerActions.vue b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerActions.vue
index 2cf7e57a4768b..06911a2da5029 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerActions.vue
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerActions.vue
@@ -44,11 +44,11 @@
         <a
           v-for="action in actions"
           :key="action.id"
           :class="'action action_' + action.id"
           :title="action.description"
-          :href="entry[action.urlProperty] || '#'"
+          :href="sanitizeUrl(entry[action.urlProperty]) || '#'"
         >
           <XWikiIcon :iconDescriptor="action.icon" /><span class="action-name">{{ action.name }}</span>
         </a>
       </div>
     </template>
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerHtml.vue b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerHtml.vue
index 52e37ca21da09..4755c8a846dd4 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerHtml.vue
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerHtml.vue
@@ -31,11 +31,12 @@
     :entry="entry"
     is-view
     view-only>
     <!-- Provide the Html Viewer widget to the `viewer` slot -->
     <template #viewer>
-      <div class="html-wrapper" v-html="value"></div>
+      <div class="html-wrapper"
+           v-html="safeValue"/>
     </template>
   </BaseDisplayer>
 </template>
 
 <script>
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerLink.vue b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerLink.vue
index 50daa05cfa504..20568ed657b34 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerLink.vue
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerLink.vue
@@ -43,15 +43,15 @@
       <!--
         If there is no value but still a link, the user should still be able to click the link,
         so we create an explicit "no value" message in that case
       -->
       <a v-if="linkContent && hasViewRight"
-        :href="href"
+        :href="sanitizeUrl(href)"
         :class="{'explicit-empty-value': !html && !htmlValue}"
-        v-html="linkContent"
+        v-html="sanitizeHtml(linkContent)"
       ></a>
-      <span v-else v-html="linkContent"></span>
+      <span v-else v-html="sanitizeHtml(linkContent)"></span>
     </template>
 
 
     <!-- Keep the default Editor widget -->
     <template #editor></template>
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerXObjectProperty.vue b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerXObjectProperty.vue
index 98160538eb789..71e62a1c57857 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerXObjectProperty.vue
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/DisplayerXObjectProperty.vue
@@ -33,16 +33,19 @@
     :is-loading="isLoading"
     @saveEdit="applyEdit">
 
     <!-- Provide the Html Viewer widget to the `viewer` slot -->
     <template #viewer>
-      <div :class="['html-wrapper', isLoading ? 'disabled' : '']" v-html="value" ref="xObjectPropertyView"></div>
+      <div :class="['html-wrapper', isLoading ? 'disabled' : '']"
+           v-html="sanitizeHtml(value)"
+           ref="xObjectPropertyView"/>
     </template>
 
     <!-- Provide the Html Editor widget to the `editor` slot -->
     <template #editor>
-      <div v-html="editField" ref="xObjectPropertyEdit"></div>
+      <div v-html="sanitizeHtml(editField)"
+           ref="xObjectPropertyEdit"/>
     </template>
   </BaseDisplayer>
 </template>
 
 
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/displayerMixin.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/displayerMixin.js
index 04c1cea1d6084..0498045d28249 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/displayerMixin.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/vue/displayers/displayerMixin.js
@@ -16,10 +16,11 @@
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 
+import * as DOMPurify from 'dompurify';
 
 /**
  * The displayerMixin is a vue mixin containing all the needed
  * props, computed values, methods, etc. for any custom displayer:
  * `propertyId`, `entry`, `value`, `config`, `applyEdit()`, ...
@@ -57,10 +58,13 @@ export default {
   computed: {
     // The value to be displayed
     value () {
       return this.entry[this.propertyId];
     },
+    safeValue() {
+      return this.sanitizeHtml(this.value)
+    },
     // The property descriptor of `this.propertyId`
     propertyDescriptor () {
       return this.logic.getPropertyDescriptor(this.propertyId);
     },
     // The configuration (aka displayerDescriptor) of the displayer
@@ -104,10 +108,26 @@ export default {
      * @param value of a value is provided, it is used for saving the property, otherwise `this.editedValue` is used
      */
     genericSave(value) {
       const savedValue =  value || this.editedValue;
       this.logic.getEditBus().save(this.entry, this.propertyId, {[this.propertyId]: savedValue})
+    },
+    sanitizeHtml(value) {
+      if (!this.logic.isContentTrusted()) {
+        // TODO: Take into account xml.htmlElementSanitizer properties when sanitizing (see XWIKI-20249).
+        return DOMPurify.sanitize(value);
+      } else {
+        return value;
+      }
+    },
+    sanitizeUrl(url, subtitute) {
+      // TODO: Take into account xml.htmlElementSanitizer properties when sanitizing (see XWIKI-20249).
+      if (this.logic.isContentTrusted() || DOMPurify.isValidAttribute('a', 'href', url)) {
+        return url;
+      } else {
+        return (subtitute || '#');
+      }
     }
   },
   
   data() {
     return {
diff --git a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/webjar/Logic.js b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/webjar/Logic.js
index 99a70cdebe8fa..7f4648c62f36f 100644
--- a/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/webjar/Logic.js
+++ b/xwiki-platform-core/xwiki-platform-livedata/xwiki-platform-livedata-webjar/src/main/webjar/Logic.js
@@ -103,10 +103,11 @@ define('xwiki-livedata', [
    * @param {HTMLElement} element The HTML Element corresponding to the Livedata
    */
   const Logic = function (element) {
     this.element = element;
     this.data = JSON.parse(element.getAttribute("data-config") || "{}");
+    this.contentTrusted = element.getAttribute("data-config-content-trusted") === "true"; 
     this.data.entries = Object.freeze(this.data.entries);
 
     // Reactive properties must be initialized before Vue is instantiated.
     this.firstEntriesLoading = true;
     this.currentLayoutId = "";
@@ -1517,10 +1518,22 @@ define('xwiki-livedata', [
       if (index === -1) {
         this.panels.push(panel);
       } else {
         this.panels.splice(index, 0, panel);
       }
+    },
+
+    //
+    // Content status
+    //
+
+    /**
+     * @returns {boolean} when false, the content is not trusted will be sanitized whenever Vue integrated escaping
+     * is not enough. When true, the content is never sanitized
+     */
+    isContentTrusted() {
+      return this.contentTrusted;
     }
   };
 
 
 

Vulnerable code: import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.xwiki.livedata.LiveDataLayoutDescriptor;
import org.xwiki.livedata.LiveDataMeta;
import org.xwiki.livedata.LiveDataPaginationConfiguration;
import org.xwiki.livedata.LiveDataQuery;
import org.xwiki.livedata.LiveDataQuery.Constraint;
import org.xwiki.livedata.LiveDataQuery.Filter;
import org.xwiki.livedata.LiveDataQuery.SortEntry;
import org.xwiki.livedata.LiveDataQuery.Source;
import org.xwiki.livedata.internal.JSONMerge;
    private static final Pattern PATTERN_COMMA = Pattern.compile("\\s*,\\s*");

    private static final String UTF8 = "UTF-8";

    /**
     * Used to read the Live Data configuration from the macro content.
     */
    private LiveDataConfigurationResolver<String> stringLiveDataConfigResolver;
    /**
     * Used to merge the Live Data configuration built from the macro parameters with the live data configuration read
     * from the macro content.
     */
    private JSONMerge jsonMerge = new JSONMerge();
        Map<String, Object> skinExtensionParameters = Collections.singletonMap("forceSkinAction", Boolean.TRUE);
            LiveDataConfiguration liveDataConfig = getLiveDataConfiguration(content, parameters);
        return Collections.singletonList(output);

    private LiveDataConfiguration getLiveDataConfiguration(String content, LiveDataMacroParameters parameters)
        throws Exception
    {
        String json = StringUtils.defaultIfBlank(content, "{}");
        LiveDataConfiguration advancedConfig = this.stringLiveDataConfigResolver.resolve(json);
        LiveDataConfiguration basicConfig = getLiveDataConfiguration(parameters);
        // Make sure both configurations have the same id so that they are properly merged.
        advancedConfig.setId(basicConfig.getId());
        return this.jsonMerge.merge(advancedConfig, basicConfig);
    }

    private LiveDataConfiguration getLiveDataConfiguration(LiveDataMacroParameters parameters) throws Exception
    {
        LiveDataConfiguration liveDataConfig = new LiveDataConfiguration();
        liveDataConfig.setId(parameters.getId());
        liveDataConfig.setQuery(getQuery(parameters));
        liveDataConfig.setMeta(getMeta(parameters));
        return liveDataConfig;
    }

    private LiveDataQuery getQuery(LiveDataMacroParameters parameters) throws Exception
    {
        LiveDataQuery query = new LiveDataQuery();
        query.setProperties(getProperties(parameters.getProperties()));
        query.setSource(new Source(parameters.getSource()));
        query.getSource().getParameters().putAll(getSourceParameters(parameters.getSourceParameters()));
        query.setSort(getSortEntries(parameters.getSort()));
        query.setFilters(getFilters(parameters.getFilters()));
        query.setLimit(parameters.getLimit());
        query.setOffset(parameters.getOffset());
        return query;
    }

    private List<String> getProperties(String properties)
    {
        if (properties == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(properties)).collect(Collectors.toList());
        }
    }

    private Map<String, Object> getSourceParameters(String sourceParametersString) throws Exception
    {
        if (StringUtils.isEmpty(sourceParametersString)) {
            return Collections.emptyMap();
        }

        Map<String, List<String>> urlParams = getURLParameters('?' + sourceParametersString);
        Map<String, Object> sourceParams = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : urlParams.entrySet()) {
            if (entry.getValue().size() > 1) {
                sourceParams.put(entry.getKey(), entry.getValue());
            } else {
                sourceParams.put(entry.getKey(), entry.getValue().get(0));
            }
        }
        return sourceParams;
    }

    private List<SortEntry> getSortEntries(String sort)
    {
        if (sort == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(sort)).filter(StringUtils::isNotEmpty).map(this::getSortEntry)
                .collect(Collectors.toList());
        }
    }

    private SortEntry getSortEntry(String sortEntryString)
    {
        SortEntry sortEntry = new SortEntry();
        sortEntry.setDescending(sortEntryString.endsWith(":desc"));
        if (sortEntry.isDescending() || sortEntryString.endsWith(":asc")) {
            sortEntry.setProperty(StringUtils.substringBeforeLast(sortEntryString, ":"));
        } else {
            sortEntry.setProperty(sortEntryString);
        }
        return sortEntry;
    }

    private List<Filter> getFilters(String filtersString) throws Exception
    {
        List<Filter> filters = getURLParameters('?' + StringUtils.defaultString(filtersString)).entrySet().stream()
            .map(this::getFilter).collect(Collectors.toList());
        return filters.isEmpty() ? null : filters;
    }

    private Filter getFilter(Map.Entry<String, List<String>> entry)
    {
        Filter filter = new Filter();
        filter.setProperty(entry.getKey());
        filter.getConstraints().addAll(entry.getValue().stream().map(Constraint::new).collect(Collectors.toList()));
        return filter;
    }

    private LiveDataMeta getMeta(LiveDataMacroParameters parameters)
    {
        LiveDataMeta meta = new LiveDataMeta();
        meta.setLayouts(getLayouts(parameters));
        meta.setPagination(getPagination(parameters));
        return meta;
    }

    private List<LiveDataLayoutDescriptor> getLayouts(LiveDataMacroParameters parameters)
    {
        if (parameters.getLayouts() == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(parameters.getLayouts())).map(LiveDataLayoutDescriptor::new)
                .collect(Collectors.toList());
        }
    }

    private LiveDataPaginationConfiguration getPagination(LiveDataMacroParameters parameters)
    {
        LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration();
        pagination.setShowPageSizeDropdown(parameters.getShowPageSizeDropdown());
        if (parameters.getPageSizes() != null) {
            pagination.setPageSizes(Stream.of(PATTERN_COMMA.split(parameters.getPageSizes())).map(Integer::parseInt)
                .collect(Collectors.toList()));
        }
        return pagination;
    }

    private Map<String, List<String>> getURLParameters(String url) throws Exception
    {
        URL baseURL = new URL("http://www.xwiki.org");
        String queryString = new URL(baseURL, url).getQuery();
        Map<String, List<String>> parameters = new HashMap<>();
        for (String entry : queryString.split("&")) {
            String[] parts = entry.split("=", 2);
            String key = URLDecoder.decode(parts[0], UTF8);
            if (key.isEmpty()) {
                continue;
            }
            String value = parts.length == 2 ? URLDecoder.decode(parts[1], UTF8) : "";
            List<String> values = parameters.get(key);
            if (values == null) {
                values = new ArrayList<>();
                parameters.put(key, values);
            }
            values.add(value);
        }
        return parameters;
    }
livedata.displayer.emptyValue=N/A<sup>*</sup>
livedata.displayer.emptyValue=n.v.<sup>*</sup>
livedata.displayer.emptyValue=SO<sup>*</sup>
    DefaultExecution.class
        String expected = "<div class=\"liveData loading\" data-config=\"" + escapeXML(expectedConfig) + "\"></div>";
        String expected = "<div class=\"liveData loading\" id=\"test\" data-config=\""
            + escapeXML(json(expectedConfig.toString())) + "\"></div>";
        String expected = "<div class=\"liveData loading\" id=\"test\" data-config=\""
            + escapeXML(json(expectedConfig.toString())) + "\"></div>";
    "vue2-touch-events": "3.2.2"
    expect(wrapper.find('div.view > div').html()).toBe('<div><span></span> <span>livedata.displayer.emptyValue</span></div>');
        }
        }
          <span
            v-if="!isViewable"
            v-html="$t('livedata.displayer.emptyValue')"
          ></span>
          :displayer="{ href }"
          :href="entry[action.urlProperty] || '#'"
      <div class="html-wrapper" v-html="value"></div>
        :href="href"
        v-html="linkContent"
      <span v-else v-html="linkContent"></span>
      <div :class="['html-wrapper', isLoading ? 'disabled' : '']" v-html="value" ref="xObjectPropertyView"></div>
      <div v-html="editField" ref="xObjectPropertyEdit"></div>
Fixed code:     <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-bridge</artifactId>
      <version>${project.version}</version>
    </dependency>
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;

    private LiveDataMacroConfiguration liveDataMacroConfiguration;
    @Inject
    private LiveDataMacroRights liveDataMacroRights;
        Map<String, Object> skinExtensionParameters = singletonMap("forceSkinAction", Boolean.TRUE);
            LiveDataConfiguration liveDataConfig = this.liveDataMacroConfiguration.getLiveDataConfiguration(content,
                parameters);
            // The content is trusted if the author has script right, or if no advanced configuration is used (i.e., 
            // no macro content), and we are running in a trusted context.
            boolean trustedContent =
                StringUtils.isBlank(content) || (this.liveDataMacroRights.authorHasScriptRight()
                    && !context.getTransformationContext().isRestricted());
            output.setParameter("data-config-content-trusted", Boolean.toString(trustedContent));
        return singletonList(output);
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.livedata.internal.macro;

import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.xwiki.component.annotation.Component;
import org.xwiki.livedata.LiveDataConfiguration;
import org.xwiki.livedata.LiveDataConfigurationResolver;
import org.xwiki.livedata.LiveDataLayoutDescriptor;
import org.xwiki.livedata.LiveDataMeta;
import org.xwiki.livedata.LiveDataPaginationConfiguration;
import org.xwiki.livedata.LiveDataQuery;
import org.xwiki.livedata.internal.JSONMerge;
import org.xwiki.livedata.macro.LiveDataMacroParameters;

/**
 * Provides services to manipulate configurations for the {@link LiveDataMacroConfiguration}.
 *
 * @version $Id$
 * @since 14.9
 * @since 14.4.7
 * @since 13.10.10
 */
@Component(roles = LiveDataMacroConfiguration.class)
@Singleton
public class LiveDataMacroConfiguration
{
    private static final Pattern PATTERN_COMMA = Pattern.compile("\\s*,\\s*");

    private static final String UTF8 = "UTF-8";

    /**
     * Used to read the Live Data configuration from the macro content.
     */
    @Inject
    private LiveDataConfigurationResolver<String> stringLiveDataConfigResolver;

    /**
     * Used to merge the Live Data configuration built from the macro parameters with the live data configuration read
     * from the macro content.
     */
    private JSONMerge jsonMerge = new JSONMerge();

    /**
     * Resolve a complete Live Data configuration from a json advanced configuration (the content) and a set of macro
     * parameters.
     *
     * @param content the string representation of the json live data advanced configuration
     * @param parameters the Live Data macro parameters
     * @return the complete Live Data configuration
     * @throws Exception in case of error when resolving the configuration
     */
    public LiveDataConfiguration getLiveDataConfiguration(String content, LiveDataMacroParameters parameters)
        throws Exception
    {
        String json = StringUtils.defaultIfBlank(content, "{}");
        LiveDataConfiguration advancedConfig = this.stringLiveDataConfigResolver.resolve(json);
        LiveDataConfiguration basicConfig = getLiveDataConfiguration(parameters);
        // Make sure both configurations have the same id so that they are properly merged.
        advancedConfig.setId(basicConfig.getId());
        return this.jsonMerge.merge(advancedConfig, basicConfig);
    }

    private LiveDataConfiguration getLiveDataConfiguration(LiveDataMacroParameters parameters) throws Exception
    {
        LiveDataConfiguration liveDataConfig = new LiveDataConfiguration();
        liveDataConfig.setId(parameters.getId());
        liveDataConfig.setQuery(getQuery(parameters));
        liveDataConfig.setMeta(getMeta(parameters));
        return liveDataConfig;
    }

    private LiveDataQuery getQuery(LiveDataMacroParameters parameters) throws Exception
    {
        LiveDataQuery query = new LiveDataQuery();
        query.setProperties(getProperties(parameters.getProperties()));
        query.setSource(new LiveDataQuery.Source(parameters.getSource()));
        query.getSource().getParameters().putAll(getSourceParameters(parameters.getSourceParameters()));
        query.setSort(getSortEntries(parameters.getSort()));
        query.setFilters(getFilters(parameters.getFilters()));
        query.setLimit(parameters.getLimit());
        query.setOffset(parameters.getOffset());
        return query;
    }

    private List<String> getProperties(String properties)
    {
        if (properties == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(properties)).collect(Collectors.toList());
        }
    }

    private Map<String, Object> getSourceParameters(String sourceParametersString) throws Exception
    {
        if (StringUtils.isEmpty(sourceParametersString)) {
            return Collections.emptyMap();
        }

        Map<String, List<String>> urlParams = getURLParameters('?' + sourceParametersString);
        Map<String, Object> sourceParams = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : urlParams.entrySet()) {
            if (entry.getValue().size() > 1) {
                sourceParams.put(entry.getKey(), entry.getValue());
            } else {
                sourceParams.put(entry.getKey(), entry.getValue().get(0));
            }
        }
        return sourceParams;
    }

    private List<LiveDataQuery.SortEntry> getSortEntries(String sort)
    {
        if (sort == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(sort)).filter(StringUtils::isNotEmpty).map(this::getSortEntry)
                .collect(Collectors.toList());
        }
    }

    private LiveDataQuery.SortEntry getSortEntry(String sortEntryString)
    {
        LiveDataQuery.SortEntry sortEntry = new LiveDataQuery.SortEntry();
        sortEntry.setDescending(sortEntryString.endsWith(":desc"));
        if (sortEntry.isDescending() || sortEntryString.endsWith(":asc")) {
            sortEntry.setProperty(StringUtils.substringBeforeLast(sortEntryString, ":"));
        } else {
            sortEntry.setProperty(sortEntryString);
        }
        return sortEntry;
    }

    private List<LiveDataQuery.Filter> getFilters(String filtersString) throws Exception
    {
        List<LiveDataQuery.Filter> filters =
            getURLParameters('?' + StringUtils.defaultString(filtersString)).entrySet().stream()
                .map(this::getFilter).collect(Collectors.toList());
        return filters.isEmpty() ? null : filters;
    }

    private LiveDataQuery.Filter getFilter(Map.Entry<String, List<String>> entry)
    {
        LiveDataQuery.Filter filter = new LiveDataQuery.Filter();
        filter.setProperty(entry.getKey());
        filter.getConstraints()
            .addAll(entry.getValue().stream().map(LiveDataQuery.Constraint::new).collect(Collectors.toList()));
        return filter;
    }

    private LiveDataMeta getMeta(LiveDataMacroParameters parameters)
    {
        LiveDataMeta meta = new LiveDataMeta();
        meta.setLayouts(getLayouts(parameters));
        meta.setPagination(getPagination(parameters));
        return meta;
    }

    private List<LiveDataLayoutDescriptor> getLayouts(LiveDataMacroParameters parameters)
    {
        if (parameters.getLayouts() == null) {
            return null;
        } else {
            return Stream.of(PATTERN_COMMA.split(parameters.getLayouts())).map(LiveDataLayoutDescriptor::new)
                .collect(Collectors.toList());
        }
    }

    private LiveDataPaginationConfiguration getPagination(LiveDataMacroParameters parameters)
    {
        LiveDataPaginationConfiguration pagination = new LiveDataPaginationConfiguration();
        pagination.setShowPageSizeDropdown(parameters.getShowPageSizeDropdown());
        if (parameters.getPageSizes() != null) {
            pagination.setPageSizes(Stream.of(PATTERN_COMMA.split(parameters.getPageSizes())).map(Integer::parseInt)
                .collect(Collectors.toList()));
        }
        return pagination;
    }

    private Map<String, List<String>> getURLParameters(String url) throws Exception
    {
        URL baseURL = new URL("http://www.xwiki.org");
        String queryString = new URL(baseURL, url).getQuery();
        Map<String, List<String>> parameters = new HashMap<>();
        for (String entry : queryString.split("&")) {
            String[] parts = entry.split("=", 2);
            String key = URLDecoder.decode(parts[0], UTF8);
            if (key.isEmpty()) {
                continue;
            }
            String value = parts.length == 2 ? URLDecoder.decode(parts[1], UTF8) : "";
            List<String> values = parameters.get(key);
            if (values == null) {
                values = new ArrayList<>();
                parameters.put(key, values);
            }
            values.add(value);
        }
        return parameters;
    }
}
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.livedata.internal.macro;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorizationManager;

import static org.xwiki.security.authorization.Right.SCRIPT;

/**
 * Provides the services related to rights for {@link LiveDataMacro}.
 *
 * @version $Id$
 * @since 14.9
 * @since 14.4.7
 * @since 13.10.10
 */
@Component(roles = LiveDataMacroRights.class)
@Singleton
public class LiveDataMacroRights
{
    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private AuthorizationManager authorizationManager;

    /**
     * @return {@code true} if the last author of the document where the Live Data macro is executed has script rights
     *     on said document, {@code false} otherwise
     */
    public boolean authorHasScriptRight()
    {
        DocumentReference currentDocumentReference = this.documentAccessBridge.getCurrentDocumentReference();
        DocumentReference currentAuthorReference = this.documentAccessBridge.getCurrentAuthorReference();
        return this.authorizationManager.hasAccess(SCRIPT, currentAuthorReference, currentDocumentReference);
    }
}
livedata.displayer.emptyValue=N/A
livedata.displayer.emptyValue=n.v.
livedata.displayer.emptyValue=SO
org.xwiki.livedata.internal.macro.LiveDataMacroConfiguration
org.xwiki.livedata.internal.macro.LiveDataMacroRights
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.security.authorization.AuthorizationManager;
    DefaultExecution.class,
    LiveDataMacroConfiguration.class,
    LiveDataMacroRights.class
    @MockComponent
    private DocumentAccessBridge documentAccessBridge;

    @MockComponent
    private AuthorizationManager authorizationManager;

        String expected = "<div class=\"liveData loading\" data-config=\"" + escapeXML(expectedConfig) + "\" "
            + "data-config-content-trusted=\"true\"></div>";
        String expected = String.format("<div class=\"liveData loading\" id=\"test\" data-config=\"%s\" "
            + "data-config-content-trusted=\"true\"></div>", escapeXML(json(expectedConfig.toString())));
        String expected = String.format("<div class=\"liveData loading\" id=\"test\" data-config=\"%s\" "
            + "data-config-content-trusted=\"false\"></div>", escapeXML(json(expectedConfig.toString())));
        "dompurify": "2.4.0",
    "node_modules/dompurify": {
      "version": "2.4.0",
      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
      "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
    },
    "dompurify": {
      "version": "2.4.0",
      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
      "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
    },
    "vue2-touch-events": "3.2.2",
    "dompurify": "2.4.0"
    expect(wrapper.find('div.view > div').text()).toBe('livedata.displayer.emptyValue*');
        },
        isContentTrusted: () => true
      },
      logic: {
        isContentTrusted: () => true
      }
    })
    expect(wrapper.find('.html-wrapper').html())
      .toBe('<div class="html-wrapper"><strong>some content</strong></div>')
  })

  it('Renders an entry in view mode with untrusted content', () => {
    const wrapper = initWrapper(DisplayerHtml, {
      props: {
        entry: {
          color: '<strong>some content<script>console.log("hello world")</script></strong>'
        }
      },
      logic: {
        isContentTrusted: () => false
  it('Renders an entry in view mode with untrusted content', () => {
    const logic = {
      getDisplayerDescriptor() {
        return {
          propertyHref: 'colorHref'
        };
      },
      isContentTrusted: () => false
    };
    const wrapperHttpLink = initWrapper(DisplayerLink, {
      props: {
        entry: {
          color: 'yellow<script>console.log("hello")</script>',
          colorHref: 'http://test.com'
        }
      },
      logic
    });
    expect(wrapperHttpLink.text()).toMatch('yellow')
    expect(wrapperHttpLink.find('a').element.href).toBe('http://test.com/');

    const wrapperJavascriptLink = initWrapper(DisplayerLink, {
      props: {
        entry: {
          color: 'yellow<script>console.log("hello")</script>',
          colorHref: 'javascript:console.log("world")'
        }
      },
      logic
    });

    expect(wrapperJavascriptLink.text()).toMatch('yellow')
    expect(wrapperJavascriptLink.find('a').element.href).toBe('http://localhost/#');
  })

        },
        isContentTrusted: () => true
          <span v-if="!isViewable">
            {{ $t('livedata.displayer.emptyValue') }}<sup>*</sup>
          </span>
          :displayer="{ href: sanitizeUrl(href) }"
          :href="sanitizeUrl(entry[action.urlProperty]) || '#'"
      <div class="html-wrapper"
           v-html="safeValue"/>
        :href="sanitizeUrl(href)"
        v-html="sanitizeHtml(linkContent)"
      <span v-else v-html="sanitizeHtml(linkContent)"></span>
      <div :class="['html-wrapper', isLoading ? 'disabled' : '']"
           v-html="sanitizeHtml(value)"
           ref="xObjectPropertyView"/>
      <div v-html="sanitizeHtml(editField)"
           ref="xObjectPropertyEdit"/>
import * as DOMPurify from 'dompurify';
    safeValue() {
      return this.sanitizeHtml(this.value)
    },
    },
    sanitizeHtml(value) {
      if (!this.logic.isContentTrusted()) {
        // TODO: Take into account xml.htmlElementSanitizer properties when sanitizing (see XWIKI-20249).
        return DOMPurify.sanitize(value);
      } else {
        return value;
      }
    },
    sanitizeUrl(url, subtitute) {
      // TODO: Take into account xml.htmlElementSanitizer properties when sanitizing (see XWIKI-20249).
      if (this.logic.isContentTrusted() || DOMPurify.isValidAttribute('a', 'href', url)) {
        return url;
      } else {
        return (subtitute || '#');
      }
    this.contentTrusted = element.getAttribute("data-config-content-trusted") === "true"; 
    },

    //
    // Content status
    //

    /**
     * @returns {boolean} when false, the content is not trusted will be sanitized whenever Vue integrated escaping
     * is not enough. When true, the content is never sanitized
     */
    isContentTrusted() {
      return this.contentTrusted;