From d4a7eee241d1494757f409c4769c2293f573b235 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:13:44 +0000 Subject: [PATCH 1/5] Initial plan From ab0e924b3af3ff469574af50f5cbd7f0d91b3e56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:19:46 +0000 Subject: [PATCH 2/5] Add browser-like headers to TempFileAPI URL requests to fix remote image downloads Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com> --- .../dotcms/rest/api/v1/temp/TempFileAPI.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java index 15ff0f3cdad0..55e3cd93651a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.liferay.portal.model.User; import com.liferay.portal.util.PortalUtil; @@ -43,6 +44,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -72,6 +74,26 @@ public class TempFileAPI { private static final String WHO_CAN_USE_TEMP_FILE = "whoCanUse.tmp"; private static final String TEMP_RESOURCE_BY_URL_ADMIN_ONLY="TEMP_RESOURCE_BY_URL_ADMIN_ONLY"; private static final Lazy allowAccessToPrivateSubnets = Lazy.of(()->Config.getBooleanProperty("ALLOW_ACCESS_TO_PRIVATE_SUBNETS", false)); + + /** + * Browser-like HTTP request headers used when downloading remote files via URL. + * Many image servers (CDNs, Unsplash, Pexels, Cloudflare-protected sites) reject + * requests that lack standard browser headers, returning 403 or 406 responses. + */ + static final Map BROWSER_HEADERS = ImmutableMap.builder() + .put("User-Agent", Config.getStringProperty("TEMP_FILE_URL_USER_AGENT", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")) + .put("Accept", Config.getStringProperty("TEMP_FILE_URL_ACCEPT", + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")) + .put("Accept-Language", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_LANGUAGE", + "en-US,en;q=0.9")) + .put("Accept-Encoding", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_ENCODING", + "gzip, deflate, br")) + .put("Connection", "keep-alive") + .put("Sec-Fetch-Dest", "image") + .put("Sec-Fetch-Mode", "no-cors") + .put("Sec-Fetch-Site", "cross-site") + .build(); /** @@ -242,6 +264,7 @@ public DotTempFile createTempFileFromUrl(final String incomingFileName, Files.newOutputStream(tempFile.toPath()))){ final CircuitBreakerUrl urlGetter = CircuitBreakerUrl.builder().setMethod(Method.GET).setUrl(finalUrl) + .setHeaders(BROWSER_HEADERS) .setTimeout(timeoutSeconds * 1000L).build(); urlGetter.doOut(out); } @@ -270,7 +293,8 @@ public boolean validUrl(final String url) { String done; try { final CircuitBreakerUrl urlGetter = - CircuitBreakerUrl.builder().setMethod(Method.GET).setUrl(url).build(); + CircuitBreakerUrl.builder().setMethod(Method.GET).setUrl(url) + .setHeaders(BROWSER_HEADERS).build(); done = urlGetter.doString(); } catch (IOException | BadRequestException e) {//If response is not 200, CircuitBreakerUrl throws BadRequestException return false; From f35c3221c8983adf87fa27a249650e64673012d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:52:16 +0000 Subject: [PATCH 3/5] Add tests for BROWSER_HEADERS constant in TempFileAPITest Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com> --- .../rest/api/v1/temp/TempFileAPITest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java index a68cb204898d..f60106c1a5e2 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java @@ -3,6 +3,7 @@ import static com.dotcms.datagen.TestDataUtils.getFileAssetContent; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -18,6 +19,8 @@ import com.liferay.portal.util.WebKeys; import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; @@ -108,5 +111,60 @@ public static HttpServletRequest mockHttpServletRequest() { return request; } + /** + * Method to test: {@link TempFileAPI#BROWSER_HEADERS} + * Test scenario: Verify that all required browser-like header keys are present + * Expected: The map contains all 8 required header keys + */ + @Test + public void testBrowserHeaders_containsAllRequiredKeys() { + final List requiredHeaders = Arrays.asList( + "User-Agent", + "Accept", + "Accept-Language", + "Accept-Encoding", + "Connection", + "Sec-Fetch-Dest", + "Sec-Fetch-Mode", + "Sec-Fetch-Site" + ); + + assertNotNull("BROWSER_HEADERS must not be null", TempFileAPI.BROWSER_HEADERS); + for (final String header : requiredHeaders) { + assertTrue( + "BROWSER_HEADERS must contain header: " + header, + TempFileAPI.BROWSER_HEADERS.containsKey(header)); + assertNotNull( + "Value for header '" + header + "' must not be null", + TempFileAPI.BROWSER_HEADERS.get(header)); + assertFalse( + "Value for header '" + header + "' must not be empty", + TempFileAPI.BROWSER_HEADERS.get(header).isEmpty()); + } + } + + /** + * Method to test: {@link TempFileAPI#BROWSER_HEADERS} + * Test scenario: Verify the default values of the browser-like headers + * Expected: Static/non-configurable headers have their expected default values; + * configurable headers have valid non-empty values + */ + @Test + public void testBrowserHeaders_defaultValues() { + assertEquals("keep-alive", TempFileAPI.BROWSER_HEADERS.get("Connection")); + assertEquals("image", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Dest")); + assertEquals("no-cors", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Mode")); + assertEquals("cross-site", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Site")); + + // Configurable headers must be present and non-empty (may be overridden via Config) + assertTrue("User-Agent must not be empty", + !TempFileAPI.BROWSER_HEADERS.get("User-Agent").isEmpty()); + assertTrue("Accept must not be empty", + !TempFileAPI.BROWSER_HEADERS.get("Accept").isEmpty()); + assertTrue("Accept-Language must not be empty", + !TempFileAPI.BROWSER_HEADERS.get("Accept-Language").isEmpty()); + assertTrue("Accept-Encoding must not be empty", + !TempFileAPI.BROWSER_HEADERS.get("Accept-Encoding").isEmpty()); + } } From 880ef976d1b2f451bf43e4e1f4bfc3195eb1ba4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:17:56 +0000 Subject: [PATCH 4/5] Address code review: getBrowserHeaders() method, drop Accept-Encoding br, add wire-level header test Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com> --- .../dotcms/rest/api/v1/temp/TempFileAPI.java | 38 ++++--- .../rest/api/v1/temp/TempFileAPITest.java | 101 +++++++++++++++--- 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java index 55e3cd93651a..9f0d6b3e2ec9 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java @@ -76,24 +76,28 @@ public class TempFileAPI { private static final Lazy allowAccessToPrivateSubnets = Lazy.of(()->Config.getBooleanProperty("ALLOW_ACCESS_TO_PRIVATE_SUBNETS", false)); /** - * Browser-like HTTP request headers used when downloading remote files via URL. + * Builds a map of browser-like HTTP request headers for remote URL downloads. * Many image servers (CDNs, Unsplash, Pexels, Cloudflare-protected sites) reject * requests that lack standard browser headers, returning 403 or 406 responses. + * The four content-negotiation headers are re-read from {@link Config} on every call + * so that runtime changes (system table updates / hot-reload) take effect without a restart. */ - static final Map BROWSER_HEADERS = ImmutableMap.builder() - .put("User-Agent", Config.getStringProperty("TEMP_FILE_URL_USER_AGENT", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")) - .put("Accept", Config.getStringProperty("TEMP_FILE_URL_ACCEPT", - "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")) - .put("Accept-Language", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_LANGUAGE", - "en-US,en;q=0.9")) - .put("Accept-Encoding", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_ENCODING", - "gzip, deflate, br")) - .put("Connection", "keep-alive") - .put("Sec-Fetch-Dest", "image") - .put("Sec-Fetch-Mode", "no-cors") - .put("Sec-Fetch-Site", "cross-site") - .build(); + static Map getBrowserHeaders() { + return ImmutableMap.builder() + .put("User-Agent", Config.getStringProperty("TEMP_FILE_URL_USER_AGENT", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")) + .put("Accept", Config.getStringProperty("TEMP_FILE_URL_ACCEPT", + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")) + .put("Accept-Language", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_LANGUAGE", + "en-US,en;q=0.9")) + .put("Accept-Encoding", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_ENCODING", + "gzip, deflate")) + .put("Connection", "keep-alive") + .put("Sec-Fetch-Dest", "image") + .put("Sec-Fetch-Mode", "no-cors") + .put("Sec-Fetch-Site", "cross-site") + .build(); + } /** @@ -264,7 +268,7 @@ public DotTempFile createTempFileFromUrl(final String incomingFileName, Files.newOutputStream(tempFile.toPath()))){ final CircuitBreakerUrl urlGetter = CircuitBreakerUrl.builder().setMethod(Method.GET).setUrl(finalUrl) - .setHeaders(BROWSER_HEADERS) + .setHeaders(getBrowserHeaders()) .setTimeout(timeoutSeconds * 1000L).build(); urlGetter.doOut(out); } @@ -294,7 +298,7 @@ public boolean validUrl(final String url) { try { final CircuitBreakerUrl urlGetter = CircuitBreakerUrl.builder().setMethod(Method.GET).setUrl(url) - .setHeaders(BROWSER_HEADERS).build(); + .setHeaders(getBrowserHeaders()).build(); done = urlGetter.doString(); } catch (IOException | BadRequestException e) {//If response is not 200, CircuitBreakerUrl throws BadRequestException return false; diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java index f60106c1a5e2..d559bb6f1208 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java @@ -10,8 +10,11 @@ import com.dotcms.datagen.TestDataUtils; import com.dotcms.datagen.TestDataUtils.TestFile; +import com.dotcms.http.server.mock.MockHttpServer; +import com.dotcms.http.server.mock.MockHttpServerContext; import com.dotcms.mock.request.MockSession; import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.util.network.IPUtils; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.FileUtil; @@ -19,9 +22,12 @@ import com.liferay.portal.util.WebKeys; import java.io.File; import java.io.IOException; +import java.net.HttpURLConnection; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import javax.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; import org.junit.BeforeClass; @@ -112,7 +118,7 @@ public static HttpServletRequest mockHttpServletRequest() { } /** - * Method to test: {@link TempFileAPI#BROWSER_HEADERS} + * Method to test: {@link TempFileAPI#getBrowserHeaders()} * Test scenario: Verify that all required browser-like header keys are present * Expected: The map contains all 8 required header keys */ @@ -129,42 +135,103 @@ public void testBrowserHeaders_containsAllRequiredKeys() { "Sec-Fetch-Site" ); - assertNotNull("BROWSER_HEADERS must not be null", TempFileAPI.BROWSER_HEADERS); + final Map headers = TempFileAPI.getBrowserHeaders(); + assertNotNull("getBrowserHeaders() must not return null", headers); for (final String header : requiredHeaders) { assertTrue( - "BROWSER_HEADERS must contain header: " + header, - TempFileAPI.BROWSER_HEADERS.containsKey(header)); + "getBrowserHeaders() must contain header: " + header, + headers.containsKey(header)); assertNotNull( "Value for header '" + header + "' must not be null", - TempFileAPI.BROWSER_HEADERS.get(header)); + headers.get(header)); assertFalse( "Value for header '" + header + "' must not be empty", - TempFileAPI.BROWSER_HEADERS.get(header).isEmpty()); + headers.get(header).isEmpty()); } } /** - * Method to test: {@link TempFileAPI#BROWSER_HEADERS} + * Method to test: {@link TempFileAPI#getBrowserHeaders()} * Test scenario: Verify the default values of the browser-like headers * Expected: Static/non-configurable headers have their expected default values; - * configurable headers have valid non-empty values + * configurable headers have valid non-empty values; + * Accept-Encoding does not advertise Brotli (br) since Apache HttpClient lacks a Brotli decoder */ @Test public void testBrowserHeaders_defaultValues() { - assertEquals("keep-alive", TempFileAPI.BROWSER_HEADERS.get("Connection")); - assertEquals("image", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Dest")); - assertEquals("no-cors", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Mode")); - assertEquals("cross-site", TempFileAPI.BROWSER_HEADERS.get("Sec-Fetch-Site")); + final Map headers = TempFileAPI.getBrowserHeaders(); + assertEquals("keep-alive", headers.get("Connection")); + assertEquals("image", headers.get("Sec-Fetch-Dest")); + assertEquals("no-cors", headers.get("Sec-Fetch-Mode")); + assertEquals("cross-site", headers.get("Sec-Fetch-Site")); // Configurable headers must be present and non-empty (may be overridden via Config) assertTrue("User-Agent must not be empty", - !TempFileAPI.BROWSER_HEADERS.get("User-Agent").isEmpty()); + !headers.get("User-Agent").isEmpty()); assertTrue("Accept must not be empty", - !TempFileAPI.BROWSER_HEADERS.get("Accept").isEmpty()); + !headers.get("Accept").isEmpty()); assertTrue("Accept-Language must not be empty", - !TempFileAPI.BROWSER_HEADERS.get("Accept-Language").isEmpty()); - assertTrue("Accept-Encoding must not be empty", - !TempFileAPI.BROWSER_HEADERS.get("Accept-Encoding").isEmpty()); + !headers.get("Accept-Language").isEmpty()); + + // Accept-Encoding must not advertise brotli; Apache HttpClient has no brotli decoder + final String acceptEncoding = headers.get("Accept-Encoding"); + assertTrue("Accept-Encoding must not be empty", !acceptEncoding.isEmpty()); + assertFalse("Accept-Encoding must not advertise brotli (br)", acceptEncoding.contains("br")); + } + + /** + * Method to test: {@link TempFileAPI#validUrl(String)} + * Test scenario: Verify that browser-like headers are actually sent in the outbound HTTP request + * Expected: All headers returned by getBrowserHeaders() arrive at the mock server + */ + @Test + public void testValidUrl_sendsBrowserHeaders() { + final String mockIp = "127.0.0.1"; + final int mockPort = 50881; + final String path = "/image.png"; + + // Capture the incoming request headers via an AtomicReference + final AtomicReference capturedHeaders = + new AtomicReference<>(); + + final MockHttpServerContext context = new MockHttpServerContext.Builder() + .uri(path) + .responseStatus(HttpURLConnection.HTTP_OK) + .responseBody("OK") + .requestCondition( + "Capture request headers", + requestCtx -> { + capturedHeaders.set(requestCtx.getHeaders()); + return true; + }) + .mustBeCalled() + .build(); + + final MockHttpServer mockHttpServer = new MockHttpServer(mockIp, mockPort); + mockHttpServer.addContext(context); + mockHttpServer.start(); + IPUtils.disabledIpPrivateSubnet(true); + + try { + final boolean valid = APILocator.getTempFileAPI() + .validUrl("http://" + mockIp + ":" + mockPort + path); + + assertTrue("validUrl should return true for a 200 response", valid); + mockHttpServer.validate(); + + final com.sun.net.httpserver.Headers received = capturedHeaders.get(); + assertNotNull("Request headers must have been captured", received); + + // Every header in getBrowserHeaders() must have been sent to the server + for (final String expectedHeader : TempFileAPI.getBrowserHeaders().keySet()) { + assertTrue( + "Outbound request must include header: " + expectedHeader, + received.containsKey(expectedHeader)); + } + } finally { + mockHttpServer.stop(); + IPUtils.disabledIpPrivateSubnet(false); + } } } From 9cca63bbd9f1966fab5c44d194d0f0f33672a4d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:19:29 +0000 Subject: [PATCH 5/5] Remove Sec-Fetch headers, generalize Accept to */*, make all headers configurable and disableable Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com> --- .../dotcms/rest/api/v1/temp/TempFileAPI.java | 57 ++++++++++++------- .../rest/api/v1/temp/TempFileAPITest.java | 36 +++++++----- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java index 9f0d6b3e2ec9..fb7366070670 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java @@ -43,6 +43,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -76,27 +77,45 @@ public class TempFileAPI { private static final Lazy allowAccessToPrivateSubnets = Lazy.of(()->Config.getBooleanProperty("ALLOW_ACCESS_TO_PRIVATE_SUBNETS", false)); /** - * Builds a map of browser-like HTTP request headers for remote URL downloads. - * Many image servers (CDNs, Unsplash, Pexels, Cloudflare-protected sites) reject - * requests that lack standard browser headers, returning 403 or 406 responses. - * The four content-negotiation headers are re-read from {@link Config} on every call - * so that runtime changes (system table updates / hot-reload) take effect without a restart. + * Builds a map of browser-compatible HTTP request headers for remote URL downloads. + * Many servers (CDNs, Cloudflare-protected sites, etc.) reject requests that lack + * standard browser headers, returning 403 or 406 responses. + * + *

All headers are read from {@link Config} on every call so that runtime changes + * (system table updates / hot-reload) take effect without a restart. + * Setting a config key to a blank value disables that header entirely, giving operators + * full control over which headers are sent.

+ * + *

Config keys and their defaults: + *

    + *
  • {@code TEMP_FILE_URL_USER_AGENT} – Chrome-compatible User-Agent string
  • + *
  • {@code TEMP_FILE_URL_ACCEPT} – {@code *}{@code /*} (accepts any content type)
  • + *
  • {@code TEMP_FILE_URL_ACCEPT_LANGUAGE} – {@code en-US,en;q=0.9}
  • + *
  • {@code TEMP_FILE_URL_ACCEPT_ENCODING} – {@code gzip, deflate}
  • + *
  • {@code TEMP_FILE_URL_CONNECTION} – {@code keep-alive}
  • + *
+ *

*/ static Map getBrowserHeaders() { - return ImmutableMap.builder() - .put("User-Agent", Config.getStringProperty("TEMP_FILE_URL_USER_AGENT", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")) - .put("Accept", Config.getStringProperty("TEMP_FILE_URL_ACCEPT", - "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")) - .put("Accept-Language", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_LANGUAGE", - "en-US,en;q=0.9")) - .put("Accept-Encoding", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_ENCODING", - "gzip, deflate")) - .put("Connection", "keep-alive") - .put("Sec-Fetch-Dest", "image") - .put("Sec-Fetch-Mode", "no-cors") - .put("Sec-Fetch-Site", "cross-site") - .build(); + final Map headers = new LinkedHashMap<>(); + final String[][] defs = { + {"User-Agent", Config.getStringProperty("TEMP_FILE_URL_USER_AGENT", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")}, + {"Accept", Config.getStringProperty("TEMP_FILE_URL_ACCEPT", + "*/*")}, + {"Accept-Language", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_LANGUAGE", + "en-US,en;q=0.9")}, + {"Accept-Encoding", Config.getStringProperty("TEMP_FILE_URL_ACCEPT_ENCODING", + "gzip, deflate")}, + {"Connection", Config.getStringProperty("TEMP_FILE_URL_CONNECTION", + "keep-alive")}, + }; + for (final String[] entry : defs) { + if (UtilMethods.isSet(entry[1])) { + headers.put(entry[0], entry[1]); + } + } + return ImmutableMap.copyOf(headers); } diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java index d559bb6f1208..8e2828f33190 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/temp/TempFileAPITest.java @@ -119,8 +119,8 @@ public static HttpServletRequest mockHttpServletRequest() { /** * Method to test: {@link TempFileAPI#getBrowserHeaders()} - * Test scenario: Verify that all required browser-like header keys are present - * Expected: The map contains all 8 required header keys + * Test scenario: Verify that all required browser-compatible header keys are present + * Expected: The map contains the 5 required header keys (no Sec-Fetch-* headers) */ @Test public void testBrowserHeaders_containsAllRequiredKeys() { @@ -129,10 +129,7 @@ public void testBrowserHeaders_containsAllRequiredKeys() { "Accept", "Accept-Language", "Accept-Encoding", - "Connection", - "Sec-Fetch-Dest", - "Sec-Fetch-Mode", - "Sec-Fetch-Site" + "Connection" ); final Map headers = TempFileAPI.getBrowserHeaders(); @@ -152,24 +149,26 @@ public void testBrowserHeaders_containsAllRequiredKeys() { /** * Method to test: {@link TempFileAPI#getBrowserHeaders()} - * Test scenario: Verify the default values of the browser-like headers - * Expected: Static/non-configurable headers have their expected default values; - * configurable headers have valid non-empty values; - * Accept-Encoding does not advertise Brotli (br) since Apache HttpClient lacks a Brotli decoder + * Test scenario: Verify the default values of the browser-compatible headers + * Expected: + * - Accept defaults to {@code *}{@code /*} (generic, not image-specific) + * - Connection defaults to {@code keep-alive} and is configurable + * - Accept-Encoding does not advertise Brotli (br) — Apache HttpClient has no brotli decoder + * - No Sec-Fetch-* headers are present (they are browser-generated metadata, not for server use) */ @Test public void testBrowserHeaders_defaultValues() { final Map headers = TempFileAPI.getBrowserHeaders(); + + // Connection defaults to keep-alive (configurable via TEMP_FILE_URL_CONNECTION) assertEquals("keep-alive", headers.get("Connection")); - assertEquals("image", headers.get("Sec-Fetch-Dest")); - assertEquals("no-cors", headers.get("Sec-Fetch-Mode")); - assertEquals("cross-site", headers.get("Sec-Fetch-Site")); + + // Accept must default to */* — the endpoint downloads any file type, not just images + assertEquals("*/*", headers.get("Accept")); // Configurable headers must be present and non-empty (may be overridden via Config) assertTrue("User-Agent must not be empty", !headers.get("User-Agent").isEmpty()); - assertTrue("Accept must not be empty", - !headers.get("Accept").isEmpty()); assertTrue("Accept-Language must not be empty", !headers.get("Accept-Language").isEmpty()); @@ -177,6 +176,13 @@ public void testBrowserHeaders_defaultValues() { final String acceptEncoding = headers.get("Accept-Encoding"); assertTrue("Accept-Encoding must not be empty", !acceptEncoding.isEmpty()); assertFalse("Accept-Encoding must not advertise brotli (br)", acceptEncoding.contains("br")); + + // Sec-Fetch-* headers must NOT be present — they are browser-generated Fetch Metadata + // headers whose purpose is to let servers verify a request came from a real browser context. + // Sending them from a server-side HTTP client is misleading and may cause rejections. + assertFalse("Sec-Fetch-Dest must not be present", headers.containsKey("Sec-Fetch-Dest")); + assertFalse("Sec-Fetch-Mode must not be present", headers.containsKey("Sec-Fetch-Mode")); + assertFalse("Sec-Fetch-Site must not be present", headers.containsKey("Sec-Fetch-Site")); } /**