Skip to content
Snippets Groups Projects
Commit bff0203e authored by Michael Hamann's avatar Michael Hamann
Browse files

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
parent 18c079fc
No related branches found
No related tags found
No related merge requests found
Showing
with 1305 additions and 57 deletions
......@@ -31,9 +31,13 @@
<name>XWiki Platform - Diff - XML</name>
<description>XWiki Platform - Diff - XML</description>
<properties>
<xwiki.jacoco.instructionRatio>0.00</xwiki.jacoco.instructionRatio>
<xwiki.jacoco.instructionRatio>0.77</xwiki.jacoco.instructionRatio>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.xwiki.commons</groupId>
<artifactId>xwiki-commons-diff-xml</artifactId>
......@@ -44,9 +48,27 @@
<artifactId>xwiki-platform-oldcore</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-security-authentication-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</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>
</dependencies>
</project>
\ No newline at end of file
/*
* 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();
}
......@@ -21,7 +21,6 @@
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Base64;
......@@ -30,121 +29,196 @@
import javax.inject.Provider;
import javax.inject.Singleton;
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 org.xwiki.cache.Cache;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.cache.eviction.EntryEvictionConfiguration;
import org.xwiki.cache.eviction.LRUEvictionConfiguration;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Disposable;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.diff.DiffException;
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.XWikiContext;
import com.xpn.xwiki.web.XWikiRequest;
import com.xpn.xwiki.XWikiException;
/**
* Default implementation of {@link DataURIConverter}.
*
* Default Implementation of {@link DataURIConverter} that uses an HTTP client to embed images.
*
* @version $Id$
* @since 11.10.1
* @since 12.0RC1
*/
@Component
@Singleton
public class DefaultDataURIConverter implements DataURIConverter, Initializable
public class DefaultDataURIConverter implements Initializable, Disposable, DataURIConverter
{
private static final String HEADER_COOKIE = "Cookie";
@Inject
private Provider<XWikiContext> xcontextProvider;
@Inject
private CacheManager cacheManager;
@Inject
private URLSecurityManager urlSecurityManager;
@Inject
private UserReferenceSerializer<String> userReferenceSerializer;
@Inject
private ImageDownloader imageDownloader;
@Inject
private XMLDiffDataURIConverterConfiguration configuration;
private Cache<String> cache;
private Cache<DiffException> failureCache;
@Override
public void initialize() throws InitializationException
{
if (!this.configuration.isEnabled()) {
return;
}
CacheConfiguration cacheConfig = new CacheConfiguration();
cacheConfig.setConfigurationId("diff.html.dataURI");
LRUEvictionConfiguration lru = new LRUEvictionConfiguration();
lru.setMaxEntries(100);
cacheConfig.put(LRUEvictionConfiguration.CONFIGURATIONID, lru);
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);
try {
this.cache = this.cacheManager.createNewCache(cacheConfig);
this.failureCache = this.cacheManager.createNewCache(failureCacheConfiguration);
} catch (Exception e) {
// Dispose the cache if it has been created.
if (this.cache != null) {
this.cache.dispose();
}
throw new InitializationException("Failed to create the Data URI cache.", e);
}
}
@Override
public String convert(String url) throws DiffException
public void dispose()
{
if (url.startsWith("data:")) {
// Already data URI.
return url;
if (this.cache != null) {
this.cache.dispose();
}
if (this.failureCache != null) {
this.failureCache.dispose();
}
}
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);
/**
* 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 cachedDataURI;
return absoluteURL;
}
private URL getAbsoluteURI(String relativeURL) throws MalformedURLException
/**
* 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)
{
XWikiContext xcontext = this.xcontextProvider.get();
URL baseURL = xcontext.getURLFactory().getServerURL(xcontext);
return new URL(baseURL, relativeURL);
return String.format("data:%s;base64,%s", contentType, Base64.getEncoder().encodeToString(content));
}
private String convert(URL url) throws IOException, URISyntaxException
/**
* 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)
{
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));
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());
}
private HttpEntity fetch(URI uri) throws IOException
@Override
public String convert(String url) throws DiffException
{
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.useSystemProperties();
httpClientBuilder.setUserAgent("XWikiHTMLDiff");
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());
CloseableHttpClient httpClient = httpClientBuilder.build();
HttpGet getMethod = new HttpGet(uri);
String cacheKey = getCacheKey(absoluteURL);
XWikiRequest request = this.xcontextProvider.get().getRequest();
if (request != null) {
// Copy the cookies from the current request.
getMethod.setHeader(HEADER_COOKIE, request.getHeader(HEADER_COOKIE));
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;
}
}
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());
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.DefaultDataURIConverter
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);
}
}
}
......@@ -134,6 +134,12 @@
<version>${project.version}</version>
<type>xar</type>
</dependency>
<!-- Needed for CompareIT -->
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-diff-xml</artifactId>
<version>${project.version}</version>
</dependency>
<!-- ================================
Test only dependencies
================================ -->
......
......@@ -200,4 +200,10 @@ class NestedFormTokenInjectionIT extends FormTokenInjectionIT
class NestedPagePickerIT extends PagePickerIT
{
}
@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"));
}
}
......@@ -19,7 +19,10 @@
*/
package org.xwiki.security.authentication;
import java.util.List;
import org.xwiki.component.annotation.Role;
import org.xwiki.stability.Unstable;
/**
* Configuration of the authentication properties.
......@@ -54,4 +57,16 @@ default boolean isAuthenticationSecurityEnabled()
{
return true;
}
/**
* @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();
}
}
......@@ -19,6 +19,8 @@
*/
package org.xwiki.security.authentication.internal;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
......@@ -28,6 +30,8 @@
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.security.authentication.AuthenticationConfiguration;
import static java.util.stream.Collectors.toList;
/**
* Default implementation for {@link AuthenticationConfiguration}.
*
......@@ -38,6 +42,8 @@
@Singleton
public class DefaultAuthenticationConfiguration implements AuthenticationConfiguration
{
private static final String COOKIE_PREFIX = ".";
/**
* Defines from where to read the Resource configuration data.
*/
......@@ -45,6 +51,10 @@ public class DefaultAuthenticationConfiguration implements AuthenticationConfigu
@Named("authentication")
private ConfigurationSource configuration;
@Inject
@Named("xwikicfg")
private ConfigurationSource xwikiCfgConfiguration;
@Override
public int getMaxAuthorizedAttempts()
{
......@@ -73,4 +83,15 @@ public boolean isAuthenticationSecurityEnabled()
{
return configuration.getProperty("isAuthenticationSecurityEnabled", true);
}
@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());
}
}
......@@ -1556,4 +1556,27 @@ edit.defaultEditor.org.xwiki.rendering.block.XDOM#wysiwyg=$xwikiPropertiesDefaul
# whatsnew.sources = xwikisas = xwikiblog
# whatsnew.sources.xwikisas.rssURL = https://xwiki.com/news
#-------------------------------------------------------------------------------------
# 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
$!xwikiPropertiesAdditionalProperties
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment