diff --git a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/DefaultPathProcessor.java b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/DefaultPathProcessor.java index caff1cc655..c0e86a3a33 100644 --- a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/DefaultPathProcessor.java +++ b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/DefaultPathProcessor.java @@ -15,6 +15,8 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.wcm.core.components.internal.link; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; @@ -146,11 +148,13 @@ public boolean accepts(@NotNull String path, @NotNull SlingHttpServletRequest re public @NotNull String map(@NotNull String path, @NotNull SlingHttpServletRequest request) { ResourceResolver resourceResolver = request.getResourceResolver(); String mappedPath; + Map placeholders = new HashMap<>(); + String maskedPath = LinkUtil.mask(path, placeholders); try { if (vanityConfig == VanityConfig.MAPPING || vanityConfig == VanityConfig.ALWAYS) { - mappedPath = StringUtils.defaultString(resourceResolver.map(request, getPathOrVanityUrl(path, resourceResolver))); + mappedPath = LinkUtil.unmask(StringUtils.defaultString(resourceResolver.map(request, getPathOrVanityUrl(maskedPath, resourceResolver))), placeholders); } else { - mappedPath = StringUtils.defaultString(resourceResolver.map(request, path)); + mappedPath = LinkUtil.unmask(StringUtils.defaultString(resourceResolver.map(request, maskedPath)), placeholders); } } catch (Exception e) { mappedPath = path; diff --git a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtil.java b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtil.java index 95442a7b44..8855b48fff 100644 --- a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtil.java +++ b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtil.java @@ -21,13 +21,15 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.lang3.StringUtils; @@ -41,7 +43,21 @@ public class LinkUtil { private final static Logger LOG = LoggerFactory.getLogger(LinkUtil.class); - private final static List PATTERNS = Collections.singletonList(Pattern.compile("(<%[=@].*?%>)")); + //RFC 3986 section 2.2 Reserved Characters + private static final String[] RESERVED_CHARACTERS_ENCODED = { + "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27", "%28","%29", + "%2A", "%2B", "%2C", "%2F", "%3A", "%3B", "%3D", "%3F", "%40", "%5B", "%5D", + "%2a", "%2b", "%2c", "%2f", "%3a", "%3b", "%3d", "%3f", "%5b", "%5d" + }; + private final static List PATTERNS = new ArrayList<>(); + + static { + PATTERNS.add(Pattern.compile("(<%[=@].*?%>)")); + PATTERNS.addAll(Arrays.stream(RESERVED_CHARACTERS_ENCODED) + .map(encoded -> Pattern.compile("(" + encoded + ")") ) + .collect(Collectors.toList())); + } + /** * Decodes and encoded or escaped URL taking care to not break Adobe Campaign expressions @@ -78,8 +94,10 @@ public static String escape(final String path, final String queryString, final S final String maskedQueryString = mask(queryString, placeholders); String escaped; URI parsed; + final String maskedPath = LinkUtil.mask(path, placeholders); + try { - parsed = new URI(path, false); + parsed = new URI(maskedPath, false); } catch (URIException e) { parsed = null; LOG.error(e.getMessage(), e); @@ -104,7 +122,7 @@ public static String escape(final String path, final String queryString, final S .toString(); } } - + } catch (Exception e) { LOG.error(e.getMessage(), e); StringBuilder sb = new StringBuilder(path); @@ -132,7 +150,7 @@ public static String escape(final String path, final String queryString, final S * @return the masked {@link String} * @see LinkUtil#unmask(String, Map) */ - private static String mask(final String original, final Map placeholders) { + static String mask(final String original, final Map placeholders) { if (original == null) { return null; } @@ -159,7 +177,7 @@ private static String mask(final String original, final Map plac * @param placeholders the {@link Map} of placeholders to replace * @return the unmasked {@link String} */ - private static String unmask(final String masked, final Map placeholders) { + static String unmask(final String masked, final Map placeholders) { if (masked == null) { return null; } @@ -208,5 +226,5 @@ private static String replaceEncodedCharacters(final String str) { .replace("%28", "(") .replace("%29", ")") .replace("%2C", ","); - } + } } diff --git a/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtilTest.java b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtilTest.java new file mode 100644 index 0000000000..5104bbfe1a --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/link/LinkUtilTest.java @@ -0,0 +1,58 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2023 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.wcm.core.components.internal.link; + +import java.io.UnsupportedEncodingException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LinkUtilTest { + + @Test + void decode_whenReservedCharactersAreEncoded_thenReservedCharactersAreNotAffectedByTheDecode() throws UnsupportedEncodingException { + String encodedPath = "https://api01-platform.stream.co.jp/apiservice/plt3/Mzc3Mg%3d%3d%23Mjkx%23280%23168%230%233FE320DBC400%23OzEwOzEwOzEw%23"; + String decodedPath = LinkUtil.decode(encodedPath); + assertEquals(encodedPath, decodedPath); + } + + @Test + void decode_whenReservedCharactersAreNotEncoded_thenReservedCharactersAreNotAffectedBTheDecode() throws UnsupportedEncodingException { + String encodedPath = "http://google.com/?q=hello"; + String decodedPath = LinkUtil.decode(encodedPath); + assertEquals(encodedPath, decodedPath); + } + + @Test + void decode_whenUnreservedCharactersAreEncoded_thenUnreservedCharactersAreDecodedInTheDecodedString() throws UnsupportedEncodingException { + String encodedPath = "http://google.com/?q=hello%7Eworld"; + String decodedPath = LinkUtil.decode(encodedPath); + assertEquals("http://google.com/?q=hello~world", decodedPath); + } + + @Test + void decode_whenUnreservedCharactersAreNotEncoded_thenUnreservedCharactersAreNotAffectedByTheDecode() throws UnsupportedEncodingException { + String encodedPath = "http://google.com/?q=hello-world"; + String decodedPath = LinkUtil.decode(encodedPath); + assertEquals(encodedPath, decodedPath); + } + + @Test + void decode_whenCampaignPatternIsPresentInTheString_thenCampaignPatternIsNotAffectedByTheDecode() throws UnsupportedEncodingException { + String encodedPath = "/content/path/to/page.html?recipient=<%= recipient.id %>"; + String decodedPath = LinkUtil.decode(encodedPath); + assertEquals(encodedPath, decodedPath); + } +}