CVE-2023-48240 (Java, published 2023-11-20T18:15Z)
Description: value: XWiki Platform is a generic wiki platform. The rendered diff in XWiki embeds images to be able to compare the contents and not display a difference for an actually unchanged image. For this, XWiki requests all embedded images on the server side. These requests are also sent for images from other domains and include all cookies that were sent in the original request to ensure that images with restricted view right can be compared. Starting in version 11.10.1 and prior to versions 14.10.15, 15.5.1, and 15.6, this allows an attacker to steal login and session cookies that allow impersonating the current user who views the diff. The attack can be triggered with an image that references the rendered diff, thus making it easy to trigger. Apart from stealing login cookies, this also allows server-side request forgery (the result of any successful request is returned in the image's source) and viewing protected content as once a resource is cached, it is returned for all users. As only successful requests are cached, the cache will be filled by the first user who is allowed to access the resource. This has been patched in XWiki 14.10.15, 15.5.1 and 15.6. The rendered diff now only downloads images from trusted domains. Further, cookies are only sent when the image's domain is the same the requested domain. The cache has been changed to be specific for each user. As a workaround, the image embedding feature can be disabled by deleting `xwiki-platform-diff-xml-<version>.jar` in `WEB-INF/lib/`.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/pom.xml
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/java/org/xwiki/diff/xml/XMLDiffDataURIConverterConfiguration.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/java/org/xwiki/diff/xml/internal/DefaultDataURIConverter.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/java/org/xwiki/diff/xml/internal/DefaultXMLDiffDataURIConverterConfiguration.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/java/org/xwiki/diff/xml/internal/HttpClientBuilderFactory.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/java/org/xwiki/diff/xml/internal/ImageDownloader.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/main/resources/META-INF/components.txt
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/test/java/org/xwiki/diff/xml/internal/DefaultDataURIConverterTest.java
xwiki-platform-core/xwiki-platform-diff/xwiki-platform-diff-xml/src/test/java/org/xwiki/diff/xml/internal/ImageDownloaderTest.java
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/pom.xml
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/AllIT.java
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/CompareIT.java
xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/AuthenticationConfiguration.java
xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultAuthenticationConfiguration.java
xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultAuthenticationConfigurationTest.java
xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm
Commit message: XWIKI-20818: Improve data URI converter

* Cache failures
* Properly dispose the caches
* Only send requests to trusted domains
* Only embed actual images
* Limit responses to 1MB
* Introduce configuration options for timeout, maximum size and if the
  feature is enabled at all
* Add a UI test that checks that attachment embedding is working in
  general
* Move to httpclient5
* Expose the cookie domains configuration in AuthenticationConfiguration


Vulnerable code:
    <xwiki.jacoco.instructionRatio>0.00</xwiki.jacoco.instructionRatio>
import java.net.URI;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import com.xpn.xwiki.web.XWikiRequest;
 * Default implementation of {@link DataURIConverter}.
 * 
public class DefaultDataURIConverter implements DataURIConverter, Initializable
    private static final String HEADER_COOKIE = "Cookie";

        cacheConfig.put(LRUEvictionConfiguration.CONFIGURATIONID, lru);
    public String convert(String url) throws DiffException
        if (url.startsWith("data:")) {
            // Already data URI.
            return url;
        String cachedDataURI = this.cache.get(url);
        if (cachedDataURI == null) {
            try {
                cachedDataURI = convert(getAbsoluteURI(url));
                this.cache.set(url, cachedDataURI);
            } catch (IOException | URISyntaxException e) {
                throw new DiffException("Failed to convert [" + url + "] to data URI.", e);

        return cachedDataURI;
    private URL getAbsoluteURI(String relativeURL) throws MalformedURLException
        XWikiContext xcontext = this.xcontextProvider.get();
        URL baseURL = xcontext.getURLFactory().getServerURL(xcontext);
        return new URL(baseURL, relativeURL);
    private String convert(URL url) throws IOException, URISyntaxException
        HttpEntity entity = fetch(url.toURI());
        // Remove the content type parameters, such as the charset, so they don't influence the diff.
        String contentType = StringUtils.substringBefore(entity.getContentType().getValue(), ";");
        byte[] content = IOUtils.toByteArray(entity.getContent());
        return String.format("data:%s;base64,%s", contentType, Base64.getEncoder().encodeToString(content));
    private HttpEntity fetch(URI uri) throws IOException
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.useSystemProperties();
        httpClientBuilder.setUserAgent("XWikiHTMLDiff");
        CloseableHttpClient httpClient = httpClientBuilder.build();
        HttpGet getMethod = new HttpGet(uri);
        XWikiRequest request = this.xcontextProvider.get().getRequest();
        if (request != null) {
            // Copy the cookies from the current request.
            getMethod.setHeader(HEADER_COOKIE, request.getHeader(HEADER_COOKIE));
        CloseableHttpResponse response = httpClient.execute(getMethod);
        StatusLine statusLine = response.getStatusLine();
        if (statusLine.getStatusCode() == HttpStatus.SC_OK) {
            return response.getEntity();
        } else {
            throw new IOException(statusLine.getStatusCode() + " " + statusLine.getReasonPhrase());

Fixed code:
    <xwiki.jacoco.instructionRatio>0.77</xwiki.jacoco.instructionRatio>
    <dependency>
      <groupId>org.apache.httpcomponents.client5</groupId>
      <artifactId>httpclient5</artifactId>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-security-authentication-api</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>org.xwiki.commons</groupId>
      <artifactId>xwiki-commons-tool-test-component</artifactId>
      <version>${commons.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-oldcore</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
      <type>test-jar</type>
    </dependency>
/*
 * 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.diff.xml;

import org.xwiki.component.annotation.Role;
import org.xwiki.stability.Unstable;

/**
 * Configuration for the data URI converter in the XML diff module.
 *
 * @since 14.10.15
 * @since 15.5.1
 * @since 15.6
 * @version $Id$
 */
@Unstable
@Role
public interface XMLDiffDataURIConverterConfiguration
{
    /**
     * @return the timeout to use when fetching data from the web to embed as data URI
     */
    int getHTTPTimeout();

    /**
     * @return the maximum size of the data to embed as data URI
     */
    long getMaximumContentSize();

    /**
     * @return true if the data URI converter is enabled
     */
    boolean isEnabled();
}
import org.xwiki.cache.eviction.EntryEvictionConfiguration;
import org.xwiki.component.phase.Disposable;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;
import org.xwiki.url.URLSecurityManager;
import org.xwiki.user.CurrentUserReference;
import org.xwiki.user.UserReferenceSerializer;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiException;
 * Default Implementation of {@link DataURIConverter} that uses an HTTP client to embed images.
 *
public class DefaultDataURIConverter implements Initializable, Disposable, DataURIConverter
    @Inject
    private URLSecurityManager urlSecurityManager;

    @Inject
    private UserReferenceSerializer<String> userReferenceSerializer;

    @Inject
    private ImageDownloader imageDownloader;

    @Inject
    private XMLDiffDataURIConverterConfiguration configuration;

    private Cache<DiffException> failureCache;

        if (!this.configuration.isEnabled()) {
            return;
        }

        cacheConfig.put(EntryEvictionConfiguration.CONFIGURATIONID, lru);

        CacheConfiguration failureCacheConfiguration = new CacheConfiguration();
        failureCacheConfiguration.setConfigurationId("diff.html.dataURIFailureCache");
        LRUEvictionConfiguration failureLRU = new LRUEvictionConfiguration();
        failureLRU.setMaxEntries(1000);
        // Cache failures for an hour. This is to avoid hammering the server with requests for images that don't
        // exist or are inaccessible or too large.
        failureLRU.setLifespan(3600);
        failureCacheConfiguration.put(EntryEvictionConfiguration.CONFIGURATIONID, failureLRU);

            this.failureCache = this.cacheManager.createNewCache(failureCacheConfiguration);
            // Dispose the cache if it has been created.
            if (this.cache != null) {
                this.cache.dispose();
            }
    public void dispose()
        if (this.cache != null) {
            this.cache.dispose();
        }
        if (this.failureCache != null) {
            this.failureCache.dispose();
    }
    /**
     * Convert the given URL to an absolute URL using the request URL from the given context.
     *
     * @param url the URL to convert
     * @param xcontext the XWiki context
     * @return the absolute URL
     * @throws DiffException if the URL cannot be converted due to being malformed
     */
    protected URL getAbsoluteURL(String url, XWikiContext xcontext) throws DiffException
    {
        URL absoluteURL;
        try {
            if (xcontext.getRequest() != null) {
                URL requestURL = XWiki.getRequestURL(xcontext.getRequest());
                absoluteURL = new URL(requestURL, url);
            } else {
                absoluteURL = new URL(url);
        } catch (MalformedURLException | XWikiException e) {
            throw new DiffException(String.format("Failed to resolve [%s] to an absolute URL.", url), e);
        return absoluteURL;
    /**
     * Get a data URI for the given content and content type.
     *
     * @param contentType the content type
     * @param content the content
     * @return the data URI
     */
    protected static String getDataURI(String contentType, byte[] content)
        return String.format("data:%s;base64,%s", contentType, Base64.getEncoder().encodeToString(content));
    /**
     * Compute a cache key based on the current user and the URL.
     *
     * @param url the url
     * @return the cache key
     */
    private String getCacheKey(URL url)
        String userPart = this.userReferenceSerializer.serialize(CurrentUserReference.INSTANCE);
        // Prepend the length of the user part to avoid any kind of confusion between user and URL.
        return String.format("%d:%s:%s", userPart.length(), userPart, url.toString());
    @Override
    public String convert(String url) throws DiffException
        if (url.startsWith("data:") || !this.configuration.isEnabled()) {
            // Already data URI.
            return url;
        }

        // Convert URL to absolute URL to avoid issues with relative URLs that might reference different images
        // in different subwikis.
        URL absoluteURL = getAbsoluteURL(url, this.xcontextProvider.get());
        String cacheKey = getCacheKey(absoluteURL);
        try {
            String dataURI = this.cache.get(cacheKey);

            if (dataURI == null) {
                DiffException failure = this.failureCache.get(cacheKey);

                if (failure != null) {
                    throw failure;
                }

                dataURI = convert(absoluteURL);
                this.cache.set(cacheKey, dataURI);
            }

            return dataURI;
        } catch (IOException | URISyntaxException e) {
            DiffException diffException = new DiffException("Failed to convert [" + url + "] to data URI.", e);
            this.failureCache.set(cacheKey, diffException);
            throw diffException;
    }
    private String convert(URL url) throws IOException, URISyntaxException
    {
        if (!this.urlSecurityManager.isDomainTrusted(url)) {
            throw new IOException(String.format("The URL [%s] is not trusted.", url));

        ImageDownloader.DownloadResult downloadResult = this.imageDownloader.download(url.toURI());

        return getDataURI(downloadResult.getContentType(), downloadResult.getData());
/*
 * 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.diff.xml.internal;

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

import org.xwiki.component.annotation.Component;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;

/**
 * Configuration for the XML diff.
 *
 * @version $Id$
 * @since 14.10.15
 * @since 15.5.1
 * @since 15.6
 */
@Component
@Singleton
public class DefaultXMLDiffDataURIConverterConfiguration implements XMLDiffDataURIConverterConfiguration
{
    private static final String PREFIX = "diff.xml.dataURI";

    @Inject
    @Named("xwikiproperties")
    private ConfigurationSource configurationSource;

    @Override
    public int getHTTPTimeout()
    {
        return this.configurationSource.getProperty(getFullKeyName("httpTimeout"), Integer.class, 10);
    }

    @Override
    public long getMaximumContentSize()
    {
        return this.configurationSource.getProperty(getFullKeyName("maximumContentSize"), Long.class, 1024L * 1024L);
    }

    @Override
    public boolean isEnabled()
    {
        return this.configurationSource.getProperty(getFullKeyName("enabled"), Boolean.class, true);
    }

    private String getFullKeyName(String shortKeyName)
    {
        return String.format("%s.%s", PREFIX, shortKeyName);
    }

}
/*
 * 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.diff.xml.internal;

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

import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.xwiki.component.annotation.Component;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;

/**
 * Simple factory for HttpClientBuilder to help testing and set basic properties including user agent and timeouts.
 *
 * @since 14.10.15
 * @since 15.5.1
 * @since 15.6
 * @version $Id$
 */
@Component(roles = HttpClientBuilderFactory.class)
@Singleton
public class HttpClientBuilderFactory
{
    @Inject
    private XMLDiffDataURIConverterConfiguration configuration;

    /**
     * @return a new HTTPClientBuilder
     */
    public HttpClientBuilder create()
    {
        HttpClientBuilder result = HttpClientBuilder.create();
        result.useSystemProperties();
        result.setUserAgent("XWikiHTMLDiff");

        // Set the connection timeout.
        Timeout timeout = Timeout.ofSeconds(this.configuration.getHTTPTimeout());
        ConnectionConfig connectionConfig = ConnectionConfig.custom()
            .setConnectTimeout(timeout)
            .setSocketTimeout(timeout)
            .build();

        BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager();
        cm.setConnectionConfig(connectionConfig);
        result.setConnectionManager(cm);

        // Set the response timeout.
        result.setDefaultRequestConfig(RequestConfig.custom().setResponseTimeout(timeout).build());

        return result;
    }
}
/*
 * 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.diff.xml.internal;

import java.io.IOException;
import java.net.URI;
import java.util.Optional;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.xwiki.component.annotation.Component;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;
import org.xwiki.security.authentication.AuthenticationConfiguration;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.web.XWikiRequest;

/**
 * Component for downloading images from a URL with the given cookies.
 *
 * @since 14.10.15
 * @since 15.5.1
 * @since 15.6
 * @version $Id$
 */
@Component(roles = ImageDownloader.class)
@Singleton
public class ImageDownloader
{
    private static final String COOKIE_DOMAIN_PREFIX = ".";

    private static final String HEADER_COOKIE = "Cookie";

    @Inject
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @Inject
    private Provider<XWikiContext> xcontextProvider;

    @Inject
    private XMLDiffDataURIConverterConfiguration configuration;

    @Inject
    private AuthenticationConfiguration authenticationConfiguration;

    /**
     * The result of a download request.
     */
    public static class DownloadResult
    {
        private final byte[] data;

        private final String contentType;

        /**
         * @param data the downloaded data
         * @param contentType the MIME type of the downloaded data
         */
        public DownloadResult(byte[] data, String contentType)
        {
            this.data = data;
            this.contentType = contentType;
        }

        /**
         * @return the downloaded data
         */
        public byte[] getData()
        {
            return this.data;
        }

        /**
         * @return the MIME type of the downloaded data
         */
        public String getContentType()
        {
            return this.contentType;
        }
    }

    /**
     * Download the image from the given URL with the cookies from the current request.
     *
     * @param uri the URL of the image
     * @return the image as a byte array
     * @throws IOException if there was an error downloading the image
     */
    public DownloadResult download(URI uri) throws IOException
    {
        HttpClientBuilder httpClientBuilder = this.httpClientBuilderFactory.create();

        HttpGet getMethod = initializeGetMethod(uri);

        try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
            return httpClient.execute(getMethod, response -> handleResponse(uri, response));
        }
    }

    private DownloadResult handleResponse(URI uri, ClassicHttpResponse response) throws IOException
    {
        if (response.getCode() == HttpStatus.SC_OK) {
            HttpEntity entity = response.getEntity();
            // Remove the content type parameters, such as the charset, so they don't influence the diff.
            String contentType = entity.getContentType();
            contentType = StringUtils.substringBefore(contentType, ";");

            if (!StringUtils.startsWith(contentType, "image/")) {
                throw new IOException(String.format("The content of [%s] is not an image.", uri));
            }

            long maximumSize = this.configuration.getMaximumContentSize();
            if (maximumSize > 0 && entity.getContentLength() > maximumSize) {
                throw new IOException(String.format("The content length of [%s] is too big.", uri));
            }

            byte[] content;
            if (maximumSize > 0) {
                // The content length is not always available (then it is negative), so we need to use a bounded
                // input stream to make sure we don't read more than the maximum size.
                try (BoundedInputStream boundedInputStream = new BoundedInputStream(entity.getContent(),
                    maximumSize))
                {
                    content = IOUtils.toByteArray(boundedInputStream);
                }

                if (content.length == maximumSize) {
                    throw new IOException(String.format("The content of [%s] is too big.", uri));
                }
            } else {
                content = IOUtils.toByteArray(entity.getContent());
            }

            return new DownloadResult(content, contentType);
        } else {
            throw new IOException(response.getCode() + " " + response.getReasonPhrase());
        }
    }

    private HttpGet initializeGetMethod(URI uri)
    {
        HttpGet getMethod = new HttpGet(uri);

        XWikiRequest request = this.xcontextProvider.get().getRequest();
        if (request != null && matchesCookieDomain(uri.getHost(), request)) {
            // Copy the cookie header from the current request.
            getMethod.setHeader(HEADER_COOKIE, request.getHeader(HEADER_COOKIE));
        }

        return getMethod;
    }

    /**
     * @return if the host matches the cookie domain of the current request
     */
    private boolean matchesCookieDomain(String host, HttpServletRequest request)
    {
        String serverName = request.getServerName();
        // Add a leading dot to avoid matching domains that are longer versions of the cookie domain and to ensure
        // that the cookie domain itself is matched as the cookie domain also contains the leading dot. Always add
        // the dot as two dots will still match.
        String prefixedServerName = COOKIE_DOMAIN_PREFIX + serverName;

        Optional<String> cookieDomain =
            this.authenticationConfiguration.getCookieDomains().stream()
                .filter(prefixedServerName::endsWith)
                .findFirst();

        // If there is a cookie domain, check if the host also matches it.
        return cookieDomain.map((COOKIE_DOMAIN_PREFIX + host)::endsWith)
            // If no cookie domain is configured, check for an exact match with the server name as no domain is sent in
            // this case and thus the cookie isn't valid for subdomains.
            .orElseGet(() -> host.equals(serverName));
    }

}
org.xwiki.diff.xml.internal.HttpClientBuilderFactory
org.xwiki.diff.xml.internal.ImageDownloader
org.xwiki.diff.xml.internal.DefaultXMLDiffDataURIConverterConfiguration
/*
 * 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.diff.xml.internal;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.inject.Provider;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.diff.DiffException;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;
import org.xwiki.test.annotation.BeforeComponent;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.url.URLSecurityManager;
import org.xwiki.user.CurrentUserReference;
import org.xwiki.user.UserReferenceSerializer;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.web.XWikiServletRequestStub;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for {@link DefaultDataURIConverter}.
 *
 * @version $Id$
 */
@ComponentTest
class DefaultDataURIConverterTest
{
    private static final String CURRENT_USER = "XWiki.CurrentUser";

    private static final String CACHE_PREFIX = CURRENT_USER.length() + ":" + CURRENT_USER + ":";

    private static final String URL_PREFIX = "http://localhost:8080";

    @MockComponent
    private ImageDownloader imageDownloader;

    @MockComponent
    private CacheManager cacheManager;

    @MockComponent
    private Provider<XWikiContext> xwikiContextProvider;

    @MockComponent
    private URLSecurityManager urlSecurityManager;

    private Cache<String> cache;

    private Cache<DiffException> failureCache;

    @MockComponent
    private UserReferenceSerializer<String> userReferenceSerializer;

    @MockComponent
    private XMLDiffDataURIConverterConfiguration configuration;

    @InjectMockComponents
    private DefaultDataURIConverter converter;

    @BeforeComponent
    public void configureCacheManager() throws CacheException
    {
        this.cache = mock();
        this.failureCache = mock();

        when(this.configuration.isEnabled()).thenReturn(true);

        when(this.cacheManager.createNewCache(any())).then(invocationOnMock -> {
            CacheConfiguration cacheConfiguration = invocationOnMock.getArgument(0);
            if ("diff.html.dataURI".equals(cacheConfiguration.getConfigurationId())) {
                return this.cache;
            } else if ("diff.html.dataURIFailureCache".equals(cacheConfiguration.getConfigurationId())) {
                return this.failureCache;
            }

            return null;
        });
    }

    @BeforeEach
    public void setUp()
    {
        when(this.userReferenceSerializer.serialize(CurrentUserReference.INSTANCE)).thenReturn(CURRENT_USER);

        XWikiContext xwikiContext = mock();
        when(this.xwikiContextProvider.get()).thenReturn(xwikiContext);
        XWikiServletRequestStub request = new XWikiServletRequestStub.Builder()
            .setHeaders(Map.of("forwarded", List.of("host=localhost:8080;proto=http")))
            .build();
        request.setRequestURI("/xwiki");
        when(xwikiContext.getRequest()).thenReturn(request);
    }

    @Test
    void returnsURLWhenDisabled() throws DiffException
    {
        when(this.configuration.isEnabled()).thenReturn(false);
        String url = "http://www.example.com";
        assertEquals(url, this.converter.convert(url));
    }

    @Test
    void dataURIIsKept() throws DiffException
    {
        String dataURI = "data:image/png;base64,abc";
        assertEquals(dataURI, this.converter.convert(dataURI));
    }

    @Test
    void throwsExceptionWhenURLIsMalFormed() throws IOException
    {
        String url = "http://w w w.example.com";
        when(this.urlSecurityManager.isDomainTrusted(new URL(url))).thenReturn(true);
        DiffException exception = assertThrows(DiffException.class, () -> this.converter.convert(url));
        assertEquals(getFailureMessage(url), exception.getMessage());
        assertEquals("Illegal character in authority at index 7: http://w w w.example.com",
            exception.getCause().getMessage());

        verify(this.imageDownloader, never()).download(any());
        verify(this.cache, never()).set(any(), any());
        verify(this.failureCache).set(CACHE_PREFIX + url,
            exception);
    }

    @Test
    void usesCacheWhenAvailable() throws DiffException
    {
        String dataURI = "data:image/png;base64,def";
        String url = "/image.png";
        when(this.cache.get(CACHE_PREFIX + URL_PREFIX + url)).thenReturn(dataURI);

        assertEquals(dataURI, this.converter.convert(url));
    }

    @Test
    void throwsCachedFailure()
    {
        String url = "/image.png";
        DiffException exception = new DiffException("Failed to convert url to absolute URL.");
        when(this.failureCache.get(CACHE_PREFIX + URL_PREFIX + url)).thenReturn(exception);

        DiffException thrown = assertThrows(DiffException.class, () -> this.converter.convert(url));
        assertEquals(exception, thrown);
    }

    @Test
    void throwsWhenURLIsNotTrusted() throws MalformedURLException
    {
        String url = "http://example.com/image.png";
        when(this.urlSecurityManager.isDomainTrusted(new URL(url))).thenReturn(false);

        DiffException thrown = assertThrows(DiffException.class, () -> this.converter.convert(url));
        assertEquals(getFailureMessage(url), thrown.getMessage());
        assertEquals(String.format("The URL [%s] is not trusted.", url), thrown.getCause().getMessage());

        // Make sure that the failure is cached.
        verify(this.failureCache).set(CACHE_PREFIX + url, thrown);
    }

    @Test
    void throwsExceptionWhenImageDownloaderFails() throws URISyntaxException, IOException
    {
        String url = "/image.png";
        URI uri = new URI(URL_PREFIX + url);
        when(this.urlSecurityManager.isDomainTrusted(uri.toURL())).thenReturn(true);
        IOException exception = new IOException("Failed to download image.");
        when(this.imageDownloader.download(uri)).thenThrow(exception);

        DiffException thrown = assertThrows(DiffException.class, () -> this.converter.convert(url));
        assertEquals(getFailureMessage(url), thrown.getMessage());
        assertEquals(exception, thrown.getCause());

        // Make sure the failure is cached.
        verify(this.failureCache).set(CACHE_PREFIX + URL_PREFIX + url, thrown);
        // Make sure nothing is stored in the data cache.
        verify(this.cache, never()).set(any(), any());
    }

    @Test
    void returnsDataURIForReturnedDataAndMimeType() throws IOException, DiffException
    {
        String url = "/image.png";
        URI uri = URI.create(URL_PREFIX + url);
        when(this.urlSecurityManager.isDomainTrusted(uri.toURL())).thenReturn(true);
        String dataURI = "data:image/jpeg;base64,ZGVm";
        when(this.imageDownloader.download(uri))
            .thenReturn(new ImageDownloader.DownloadResult(new byte[] { 'd', 'e', 'f' }, "image/jpeg"));

        assertEquals(dataURI, this.converter.convert(url));

        // Make sure the data is cached.
        verify(this.cache).set(CACHE_PREFIX + URL_PREFIX + url, dataURI);
        // Make sure nothing is stored in the failure cache.
        verify(this.failureCache, never()).set(any(), any());
    }

    private static String getFailureMessage(String url)
    {
        return String.format("Failed to convert [%s] to data URI.", url);
    }
}
/*
 * 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.diff.xml.internal;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;

import javax.inject.Provider;

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.xwiki.diff.xml.XMLDiffDataURIConverterConfiguration;
import org.xwiki.security.authentication.AuthenticationConfiguration;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.web.XWikiRequest;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for {@link ImageDownloader}.
 *
 * @version $Id$
 */
@ComponentTest
class ImageDownloaderTest
{
    private static final ProtocolVersion HTTP_VERSION = new ProtocolVersion("HTTP", 1, 1);

    private static final URI IMAGE_URI = URI.create("https://www.example.com/image.png");

    private static final String IMAGE_CONTENT_TYPE = "image/png";

    @MockComponent
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @MockComponent
    private Provider<XWikiContext> xwikiContextProvider;

    @MockComponent
    private XMLDiffDataURIConverterConfiguration configuration;

    @MockComponent
    private AuthenticationConfiguration authenticationConfiguration;

    @InjectMockComponents
    private ImageDownloader imageDownloader;

    @Mock
    private HttpClientBuilder httpClientBuilder;

    @Mock
    private CloseableHttpClient httpClient;

    @Mock
    private CloseableHttpResponse httpResponse;

    @Mock
    private XWikiContext xwikiContext;

    @Mock
    private HttpEntity httpEntity;

    @BeforeEach
    public void setupMocks() throws IOException
    {
        when(this.httpClientBuilderFactory.create()).thenReturn(this.httpClientBuilder);
        when(this.httpClientBuilder.build()).thenReturn(this.httpClient);
        when(this.httpClient.execute(any(ClassicHttpRequest.class), any(HttpClientResponseHandler.class)))
            .then(invocation ->
            {
                HttpClientResponseHandler<?> responseHandler = invocation.getArgument(1);
                return responseHandler.handleResponse(this.httpResponse);
            });
        when(this.xwikiContextProvider.get()).thenReturn(this.xwikiContext);
        when(this.httpResponse.getEntity()).thenReturn(this.httpEntity);
        when(this.httpResponse.getCode()).thenReturn(HttpStatus.SC_OK);
        when(this.httpResponse.getReasonPhrase()).thenReturn("OK");
        when(this.httpEntity.getContentType()).thenReturn(IMAGE_CONTENT_TYPE);
    }

    @Test
    void throwsOnNon200Status()
    {
        when(this.httpResponse.getCode()).thenReturn(HttpStatus.SC_NOT_FOUND);
        when(this.httpResponse.getReasonPhrase()).thenReturn("Not Found");
        IOException ioException = assertThrows(IOException.class, () -> this.imageDownloader.download(IMAGE_URI));
        assertEquals("404 Not Found", ioException.getMessage());
    }

    @Test
    void throwsWhenNonImageContentType()
    {
        when(this.httpEntity.getContentType()).thenReturn("text/html");
        IOException ioException = assertThrows(IOException.class, () -> this.imageDownloader.download(IMAGE_URI));
        assertEquals(String.format("The content of [%s] is not an image.", IMAGE_URI), ioException.getMessage());
    }

    @Test
    void throwsWhenContentTypeHeaderMissing()
    {
        when(this.httpEntity.getContentType()).thenReturn(null);
        IOException ioException = assertThrows(IOException.class, () -> this.imageDownloader.download(IMAGE_URI));
        assertEquals(String.format("The content of [%s] is not an image.", IMAGE_URI), ioException.getMessage());
    }

    @Test
    void throwsWhenContentLengthTooBig()
    {
        when(this.httpEntity.getContentLength()).thenReturn(1000000000L);
        when(this.configuration.getMaximumContentSize()).thenReturn(100L);
        IOException ioException = assertThrows(IOException.class, () -> this.imageDownloader.download(IMAGE_URI));
        assertEquals(String.format("The content length of [%s] is too big.", IMAGE_URI), ioException.getMessage());
    }

    @Test
    void throwsWhenContentLengthUnknownAndTooBig() throws IOException
    {
        when(this.httpEntity.getContentLength()).thenReturn(-1L);
        InputStream inputStream = new InputStream()
        {
            @Override
            public int read()
            {
                return 1;
            }
        };
        when(this.configuration.getMaximumContentSize()).thenReturn(100L);

        when(this.httpEntity.getContent()).thenReturn(inputStream);
        IOException ioException = assertThrows(IOException.class, () -> this.imageDownloader.download(IMAGE_URI));
        assertEquals(String.format("The content of [%s] is too big.", IMAGE_URI), ioException.getMessage());
    }

    @ParameterizedTest
    @ValueSource(longs = { -1, 1, 100, 200 })
    void returnsContent(long contentLength) throws IOException
    {
        // Set different content lengths to test the different code paths.
        when(this.httpEntity.getContentLength()).thenReturn(contentLength);
        if (contentLength < 200) {
            when(this.configuration.getMaximumContentSize()).thenReturn(200L);
        } else {
            // Test unlimited size.
            when(this.configuration.getMaximumContentSize()).thenReturn(0L);
        }
        byte[] content = new byte[] { 1, 2, 3 };
        when(this.httpEntity.getContent()).thenReturn(new ByteArrayInputStream(content));
        ImageDownloader.DownloadResult result = this.imageDownloader.download(IMAGE_URI);
        assertArrayEquals(content, result.getData());
        assertEquals(IMAGE_CONTENT_TYPE, result.getContentType());
    }

    @ParameterizedTest
    @CsvSource({
        "www.example.com, true, ",
        "www.xwiki.org, false, ",
        "test.example.com, false, ",
        "matches.example.com, true, .example.com"
    })
    void passesCookiesFromRequest(String requestDomain, boolean shouldSendCookie, String cookieDomain)
        throws IOException
    {
        // Set a mock request in the context.
        XWikiRequest request = mock();
        when(request.getServerName()).thenReturn(requestDomain);
        String cookieHeader = "cookie1=value1; cookie2=value2";
        when(request.getHeader("Cookie")).thenReturn(cookieHeader);
        when(this.xwikiContext.getRequest()).thenReturn(request);

        if (StringUtils.isNotBlank(cookieDomain)) {
            when(this.authenticationConfiguration.getCookieDomains()).thenReturn(List.of(cookieDomain));
        }

        // Trigger the download.
        byte[] content = new byte[] { 1, 2, 3 };
        when(this.httpEntity.getContent()).thenReturn(new ByteArrayInputStream(content));
        this.imageDownloader.download(IMAGE_URI);

        ArgumentCaptor<ClassicHttpRequest> requestCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
        verify(this.httpClient).execute(requestCaptor.capture(), any(HttpClientResponseHandler.class));

        // Verify that the cookies are passed to the HTTP client if it should do so.
        ClassicHttpRequest httpRequest = requestCaptor.getValue();
        Header[] headers = httpRequest.getHeaders("Cookie");
        if (shouldSendCookie) {
            assertEquals(1, headers.length);
            assertEquals(cookieHeader, headers[0].getValue());
        } else {
            assertEquals(0, headers.length);
        }
    }
}
    <!-- Needed for CompareIT -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-diff-xml</artifactId>
      <version>${project.version}</version>
    </dependency>

    @Nested
    @DisplayName("Compare Tests")
    class NestedCompareIT extends CompareIT
    {
    }
/*
 * 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.flamingo.test.docker;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.test.docker.junit5.TestReference;
import org.xwiki.test.docker.junit5.UITest;
import org.xwiki.test.ui.TestUtils;
import org.xwiki.test.ui.po.ComparePage;
import org.xwiki.test.ui.po.ViewPage;
import org.xwiki.test.ui.po.diff.RenderedChanges;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Tests related to the compare versions feature.
 *
 * @version $Id$
 */
@UITest(properties = {
    // Trust picsum.photos to allow the rendered diff to download images from it
    "xwikiPropertiesAdditionalProperties=url.trustedDomains=picsum.photos"
})
class CompareIT
{
    private static final String ATTACHMENT_NAME_1 = "image.gif";

    private static final String ATTACHMENT_NAME_2 = "image2.gif";

    private static final String ATTACHMENT_NAME_3 = "image.png";

    private static final String IMAGE_SYNTAX = "[[image:%s]]";

    private String getLocalAttachmentURL(TestUtils setup, TestReference testReference, String attachmentName)
        throws URISyntaxException
    {
        AttachmentReference attachmentReference = new AttachmentReference(attachmentName, testReference);
        URI attachmentURL = new URI(setup.getURL(attachmentReference, "download", null));
        // Replace host and port with localhost and 8080 to make the URL usable from the container.
        return (new URI("http", null, "localhost", 8080, attachmentURL.getPath(),
            attachmentURL.getQuery(), attachmentURL.getFragment())).toString();
    }

    @Test
    @Order(1)
    void compareRenderedImageChanges(TestUtils setup, TestReference testReference) throws Exception
    {
        setup.loginAsSuperAdmin();
        setup.attachFile(testReference, ATTACHMENT_NAME_1,
            getClass().getResourceAsStream("/AttachmentIT/image.gif"), false);
        // Upload the image a second time under a different name to check that the content and not the URL is used
        // for comparison when changing the URL to the second image.
        setup.attachFile(testReference, ATTACHMENT_NAME_2,
            getClass().getResourceAsStream("/AttachmentIT/image.gif"), false);
        String url1 = getLocalAttachmentURL(setup, testReference, ATTACHMENT_NAME_1);
        ViewPage viewPage = setup.createPage(testReference, String.format(IMAGE_SYNTAX, url1));
        String firstRevision = viewPage.getMetaDataValue("version");
        // Create a second revision with the new image.
        String url2 = getLocalAttachmentURL(setup, testReference, ATTACHMENT_NAME_2);
        viewPage = setup.createPage(testReference, String.format(IMAGE_SYNTAX, url2));
        String secondRevision = viewPage.getMetaDataValue("version");

        // Open the history pane.
        ComparePage compare = viewPage.openHistoryDocExtraPane().compare(firstRevision, secondRevision);
        RenderedChanges renderedChanges = compare.getChangesPane().getRenderedChanges();
        assertTrue(renderedChanges.hasNoChanges());

        // Upload a new image with different content to verify that the changes are detected.
        setup.attachFile(testReference, ATTACHMENT_NAME_3,
            getClass().getResourceAsStream("/AttachmentIT/SmallSizeAttachment.png"), false);

        // Create a third revision with the new image.
        String url3 = getLocalAttachmentURL(setup, testReference, ATTACHMENT_NAME_3);
        viewPage = setup.createPage(testReference, String.format(IMAGE_SYNTAX, url3));
        String thirdRevision = viewPage.getMetaDataValue("version");

        // Open the history pane.
        compare = viewPage.openHistoryDocExtraPane().compare(secondRevision, thirdRevision);
        renderedChanges = compare.getChangesPane().getRenderedChanges();
        assertFalse(renderedChanges.hasNoChanges());
        List<WebElement> changes = renderedChanges.getChangedBlocks();
        assertEquals(2, changes.size());

        // Check that the first change is the deletion and the second change the insertion of the new image.
        WebElement firstChange = changes.get(0);
        WebElement secondChange = changes.get(1);
        assertEquals("deleted", firstChange.getAttribute("data-xwiki-html-diff-block"));
        assertEquals("inserted", secondChange.getAttribute("data-xwiki-html-diff-block"));
        WebElement deletedImage = firstChange.findElement(By.tagName("img"));
        WebElement insertedImage = secondChange.findElement(By.tagName("img"));

        // Check that the src attribute of the deleted image ends with the image2 (don't check the start as it
        // depends on the container setup and the nested/non-nested test execution).
        assertEquals(url2, deletedImage.getAttribute("src"));

        // Compute the expected base64-encoded content of the inserted image. The HTML diff embeds both images but
        // replaces the deleted image by the original URL again after the diff computation.
        String expectedInsertedImageContent = Base64.getEncoder().encodeToString(
            IOUtils.toByteArray(getClass().getResourceAsStream("/AttachmentIT/SmallSizeAttachment.png")));
        assertEquals("data:image/png;base64," + expectedInsertedImageContent, insertedImage.getAttribute("src"));
    }
}
import java.util.List;

import org.xwiki.stability.Unstable;

    /**
     * @return the list of cookie domains to use for the authentication cookies. Domains are prefix with a dot.
     * @since 14.10.15
     * @since 15.5.1
     * @since 15.6
     */
    @Unstable
    default List<String> getCookieDomains()
    {
        return List.of();
    }
import java.util.List;

import static java.util.stream.Collectors.toList;

    private static final String COOKIE_PREFIX = ".";

    @Inject
    @Named("xwikicfg")
    private ConfigurationSource xwikiCfgConfiguration;


    @Override
    public List<String> getCookieDomains()
    {
        List<?> rawValues = this.xwikiCfgConfiguration.getProperty("xwiki.authentication.cookiedomains", List.class,
            List.of());
        return rawValues.stream()
            .map(Object::toString)
            .map(cookie -> StringUtils.startsWith(cookie, COOKIE_PREFIX) ? cookie : COOKIE_PREFIX + cookie)
            .collect(toList());
    }
/*
 * 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.security.authentication.internal;

import java.util.List;

import javax.inject.Named;

import org.junit.jupiter.api.Test;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

/**
 * Unit test of {@link DefaultAuthenticationConfiguration}.
 */
@ComponentTest
class DefaultAuthenticationConfigurationTest
{
    @MockComponent
    @Named("xwikicfg")
    private ConfigurationSource xwikiCfgConfiguration;

    @InjectMockComponents
    private DefaultAuthenticationConfiguration configuration;

    @Test
    void getCookieDomains()
    {
        // Test with empty configuration.
        String configurationKey = "xwiki.authentication.cookiedomains";
        when(this.xwikiCfgConfiguration.getProperty(configurationKey, List.class, List.of()))
            .thenReturn(List.of());

        assertEquals(List.of(), this.configuration.getCookieDomains());

        // Test with domains without prefix.
        when(this.xwikiCfgConfiguration.getProperty(configurationKey, List.class, List.of()))
            .thenReturn(List.of("xwiki.org", "xwiki.com"));

        String xwikiComWithPrefix = ".xwiki.com";
        assertEquals(List.of(".xwiki.org", xwikiComWithPrefix), this.configuration.getCookieDomains());

        // Test with domains where some have a prefix already.
        when(this.xwikiCfgConfiguration.getProperty(configurationKey, List.class, List.of()))
            .thenReturn(List.of("example.com", xwikiComWithPrefix));

        assertEquals(List.of(".example.com", xwikiComWithPrefix), this.configuration.getCookieDomains());
    }
}
#-------------------------------------------------------------------------------------
# XML Diff
#-------------------------------------------------------------------------------------

#-# [Since 14.10.15, 15.5.1, 15.6]
#-# If the compared documents contain images, they can be embedded as data URI to compare the images themselves
#-# instead of their URLs. For this, images are downloaded via HTTP and embedded as data URI. Images are only
#-# downloaded from trusted domains when enabled above. Still, this can be a security and stability risk as
#-# downloading images can easily increase the server load. If this option is set to "false", no images are
#-# downloaded by the diff and images are compared by URL instead.
#-# Default is "true".
# diff.xml.dataURI.enabled = true

#-# [Since 14.10.15, 15.5.1, 15.6]
#-# Configure the maximum size in bytes of an image to be embedded into the XML diff output as data URI.
#-# Default is 1MB.
# diff.xml.dataURI.maximumContentSize = 1048576

#-# [Since 14.10.15, 15.5.1, 15.6]
#-# Configure the timeout in seconds for downloading an image via HTTP to embed it into the XML diff output as data URI.
#-# Default is 10 seconds.
# diff.xml.dataURI.httpTimeout = 10