From 119153841c48f081297ec2a11ccad12f0ccf06c0 Mon Sep 17 00:00:00 2001 From: Thomas Mortagne Date: Thu, 13 Nov 2025 18:19:25 +0100 Subject: [PATCH] XCOMMONS-3286: Stop using http client 3 in XWiki Standard (#1506) (cherry picked from commit 0888ce05c7223627ee73161b70fc082b82720484) --- pom.xml | 35 ++- xwiki-commons-core/pom.xml | 1 + xwiki-commons-core/xwiki-commons-http/pom.xml | 54 ++++ .../main/java/org/xwiki/http/URIUtils.java | 72 +++++ .../xwiki/http/internal/XWikiCredentials.java | 61 ++++ .../xwiki/http/internal/XWikiHTTPClient.java | 265 ++++++++++++++++++ .../XWikiHTTPClientResponseHandler.java | 50 ++++ 7 files changed, 525 insertions(+), 13 deletions(-) create mode 100644 xwiki-commons-core/xwiki-commons-http/pom.xml create mode 100644 xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/URIUtils.java create mode 100644 xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiCredentials.java create mode 100644 xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClient.java create mode 100644 xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClientResponseHandler.java diff --git a/pom.xml b/pom.xml index 0efbb69855..1944646e44 100644 --- a/pom.xml +++ b/pom.xml @@ -182,6 +182,9 @@ ${xwiki.enforcer.skip} ${xwiki.enforcer.skip} ${xwiki.enforcer.skip} + ${xwiki.enforcer.skip} + + false false @@ -465,19 +468,6 @@ httpasyncclient 4.1.5 - - - commons-httpclient - commons-httpclient - 3.1 - - - - commons-logging - commons-logging - - - org.apache.commons commons-dbcp2 @@ -1815,6 +1805,25 @@ + + + enforce-apache-http5 + + enforce + + + ${xwiki.enforcer.enforce-apache-http5.skip} + + + ${xwiki.enforcer.enforce-apache-http5.searchTransitive} + Best practice is to use Apache HTTPClient 5.x + + commons-httpclient:commons-httpclient + + + + + enforce-jakarta.activation-api diff --git a/xwiki-commons-core/pom.xml b/xwiki-commons-core/pom.xml index 0d1475326f..478b56e757 100644 --- a/xwiki-commons-core/pom.xml +++ b/xwiki-commons-core/pom.xml @@ -50,6 +50,7 @@ xwiki-commons-extension xwiki-commons-filter xwiki-commons-groovy + xwiki-commons-http xwiki-commons-job xwiki-commons-logging xwiki-commons-management diff --git a/xwiki-commons-core/xwiki-commons-http/pom.xml b/xwiki-commons-core/xwiki-commons-http/pom.xml new file mode 100644 index 0000000000..17c73d8ae7 --- /dev/null +++ b/xwiki-commons-core/xwiki-commons-http/pom.xml @@ -0,0 +1,54 @@ + + + + + + 4.0.0 + + org.xwiki.commons + xwiki-commons-core + 18.0.0-SNAPSHOT + + xwiki-commons-http + XWiki Commons - HTTP + jar + Offers HTTP-related helpers + + 0.00 + + HTTP helpers + + + + org.apache.httpcomponents.client5 + httpclient5 + + + commons-codec + commons-codec + + + org.xwiki.commons + xwiki-commons-stability + ${project.version} + + + diff --git a/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/URIUtils.java b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/URIUtils.java new file mode 100644 index 0000000000..6708376a4d --- /dev/null +++ b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/URIUtils.java @@ -0,0 +1,72 @@ +/* + * 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.http; + +import java.io.IOException; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.net.URLCodec; +import org.apache.hc.core5.net.PercentCodec; +import org.xwiki.stability.Unstable; + +/** + * Provide various helpers around URIs. + * + * @version $Id$ + * @since 17.10.0RC1 + * @since 17.4.8 + * @since 16.10.15 + */ +@Unstable +public final class URIUtils +{ + private static final URLCodec FORMENCODED_CODEC = new URLCodec(); + + // For retro compatibility reasons it's safer to use a decoder which convert + into white spaces + private static final PercentCodec URI_CODEC = new PercentCodec(); + + private URIUtils() + { + + } + + /** + * @param decoded the string to escape according to URI path specification + * @return the UTF-8 escaped path element + */ + public static String encodePathSegment(String decoded) + { + return URI_CODEC.encode(decoded); + } + + /** + * @param encoded the encoded string to parse + * @return the decoded version of the string + * @throws IOException when failing to parse the path + */ + public static String decode(String encoded) throws IOException + { + try { + return FORMENCODED_CODEC.decode(encoded); + } catch (DecoderException e) { + throw new IOException("Failed to decode string [" + encoded + "]", e); + } + } +} diff --git a/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiCredentials.java b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiCredentials.java new file mode 100644 index 0000000000..7f304755d7 --- /dev/null +++ b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiCredentials.java @@ -0,0 +1,61 @@ +/* + * 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.http.internal; + +/** + * Represent user credentials used in the context of a test. + * + * @version $Id$ + * @since 17.10.0RC1 + * @since 17.4.8 + * @since 16.10.15 + */ +public class XWikiCredentials +{ + private final String login; + + private final String password; + + /** + * @param login the login + * @param password the password + */ + public XWikiCredentials(String login, String password) + { + this.login = login; + this.password = password; + } + + /** + * @return the login + */ + public String getUserName() + { + return this.login; + } + + /** + * @return the password + */ + public String getPassword() + { + return this.password; + } +} diff --git a/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClient.java b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClient.java new file mode 100644 index 0000000000..f2c3e7f4af --- /dev/null +++ b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClient.java @@ -0,0 +1,265 @@ +/* + * 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.http.internal; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.ContextBuilder; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +/** + * Encapsulate an Apache HTTP Client 5 instance and add some helpers. + * + * @version $Id$ + * @since 17.10.0RC1 + * @since 17.4.8 + * @since 16.10.15 + */ +public class XWikiHTTPClient implements Closeable +{ + private static final String DEFAULT_USER_AGENT = "XWiki"; + + private final CloseableHttpClient client; + + private XWikiCredentials defaultCredentials; + + /** + * Create the client. + */ + public XWikiHTTPClient() + { + this(builder()); + } + + /** + * Create the client. + * + * @param userAgent the user agent to use + * @param timeout the socket and connection timeout + */ + public XWikiHTTPClient(String userAgent, int timeout) + { + this(builder(userAgent, timeout)); + } + + /** + * @param builder the builder to use + */ + public XWikiHTTPClient(HttpClientBuilder builder) + { + this.client = builder.build(); + } + + /** + * Create the client. + * + * @param client the client + */ + public XWikiHTTPClient(CloseableHttpClient client) + { + this.client = client; + } + + /** + * @return a pre configured {@link HttpClientBuilder} + */ + public static HttpClientBuilder builder() + { + return builder(DEFAULT_USER_AGENT); + } + + /** + * @param userAgent a custom user agent + * @return a pre configured {@link HttpClientBuilder} + */ + public static HttpClientBuilder builder(String userAgent) + { + return HttpClients.custom().useSystemProperties().setUserAgent(userAgent); + } + + /** + * @param userAgent a custom user agent + * @param timeout a custom timeout + * @return a pre configured {@link HttpClientBuilder} + */ + public static HttpClientBuilder builder(String userAgent, int timeout) + { + ConnectionConfig connConfig = ConnectionConfig.custom().setConnectTimeout(timeout, TimeUnit.MILLISECONDS) + .setSocketTimeout(timeout, TimeUnit.MILLISECONDS).build(); + + BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); + cm.setConnectionConfig(connConfig); + + return builder(userAgent).setConnectionManager(cm); + } + + /** + * @param defaultCredentials the default credentials to use for requests + */ + public void setDefaultCredentials(XWikiCredentials defaultCredentials) + { + this.defaultCredentials = defaultCredentials; + } + + /** + * @return the default credentials to use for requests + */ + public XWikiCredentials getDefaultCredentials() + { + return this.defaultCredentials; + } + + /** + * @return the client + */ + public CloseableHttpClient getClient() + { + return this.client; + } + + @Override + public void close() throws IOException + { + this.client.close(); + } + + /** + * Executes a request using the default context and processes the response using the given response handler. The + * content entity associated with the response is fully consumed and the underlying connection is released back to + * the connection manager automatically in all cases relieving individual {@link HttpClientResponseHandler}s from + * having to manage resource deallocation internally. + * + * @param the type of the result determined by the response handler + * @param request the request to execute + * @param responseHandler the response handler + * @return the response object as generated by the response handler. + * @throws IOException in case of a problem or the connection was aborted + * @throws ClientProtocolException in case of an http protocol error + */ + public T execute(ClassicHttpRequest request, XWikiHTTPClientResponseHandler responseHandler) + throws IOException + { + return execute(request, null, responseHandler); + } + + /** + * Executes a request using the default context and processes the response using the given response handler. The + * content entity associated with the response is fully consumed and the underlying connection is released back to + * the connection manager automatically in all cases relieving individual {@link HttpClientResponseHandler}s from + * having to manage resource deallocation internally. + * + * @param the type of the result determined by the response handler + * @param request the request to execute + * @param credentials the credentials to use for the request + * @param responseHandler the response handler + * @return the response object as generated by the response handler. + * @throws IOException in case of a problem or the connection was aborted + * @throws ClientProtocolException in case of an http protocol error + */ + public T execute(ClassicHttpRequest request, XWikiCredentials credentials, + XWikiHTTPClientResponseHandler responseHandler) throws IOException + { + HttpClientContext context = getHttpClientContext(request, credentials); + + return this.client.execute(request, context, response -> responseHandler.handleResponse(response, context)); + } + + /** + * @param request the request to get the context for + * @param credentials the credentials to use for the request + * @return the context to use for the request, or null if no authentication is needed (which cause the standard + * HttpClient to use the default context) + * @throws IOException when failing to get the URI of the request + */ + public HttpClientContext getHttpClientContext(HttpRequest request, XWikiCredentials credentials) throws IOException + { + UsernamePasswordCredentials finalCredentials; + if (credentials == null) { + if (this.defaultCredentials != null) { + finalCredentials = new UsernamePasswordCredentials(this.defaultCredentials.getUserName(), + this.defaultCredentials.getPassword().toCharArray()); + } else { + // The only point of the context is to hold the credentials so if we don't have any we return null. + return null; + } + } else { + finalCredentials = + new UsernamePasswordCredentials(credentials.getUserName(), credentials.getPassword().toCharArray()); + } + + URI uri; + try { + uri = request.getUri(); + } catch (URISyntaxException e) { + // Should fail before arriving here + throw new IOException("Cannot get the URI of the request", e); + } + + return ContextBuilder.create() + .preemptiveBasicAuth(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), finalCredentials).build(); + } + + /** + * @param the type of the result determined by the response handler + * @param uri the URI to get + * @param responseHandler the response handler + * @return the response object as generated by the response handler. + * @throws IOException in case of a problem or the connection was aborted + */ + public T executeGet(String uri, XWikiHTTPClientResponseHandler responseHandler) throws IOException + { + return execute(new HttpGet(uri), responseHandler); + } + + /** + * @param the type of the result determined by the response handler + * @param uri the URI to get + * @param entity the entity to put + * @param responseHandler the response handler + * @return the response object as generated by the response handler. + * @throws IOException in case of a problem or the connection was aborted + */ + public T executePut(String uri, HttpEntity entity, XWikiHTTPClientResponseHandler responseHandler) + throws IOException + { + ClassicHttpRequest request = new HttpPut(uri); + request.setEntity(entity); + + return execute(request, responseHandler); + } +} diff --git a/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClientResponseHandler.java b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClientResponseHandler.java new file mode 100644 index 0000000000..23628982c7 --- /dev/null +++ b/xwiki-commons-core/xwiki-commons-http/src/main/java/org/xwiki/http/internal/XWikiHTTPClientResponseHandler.java @@ -0,0 +1,50 @@ +/* + * 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.http.internal; + +import java.io.IOException; + +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; + +/** + * Handler that encapsulates the process of generating a response object from a {@link ClassicHttpResponse}. + * + * @param the type of the response. + * @version $Id$ + * @since 17.10.0RC1 + * @since 17.4.8 + * @since 16.10.15 + */ +@FunctionalInterface +public interface XWikiHTTPClientResponseHandler +{ + /** + * Processes an {@link ClassicHttpResponse} and returns some value corresponding to that response. + * + * @param response The response to process + * @param context The HTTP context + * @return A value determined by the response + * @throws IOException in case of a problem or the connection was aborted + * @throws HttpException in case of an HTTP protocol violation. + */ + T handleResponse(ClassicHttpResponse response, HttpClientContext context) throws HttpException, IOException; +}