CVE-2023-26480 (JavaScript, published 2023-03-02T18:15Z)
Description: 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.

Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/xwiki/xwiki-platform
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 message: XWIKI-20143: Improved Live Data escaping


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;