1 /*
2 * #%L
3 * wcm.io
4 * %%
5 * Copyright (C) 2014 wcm.io
6 * %%
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * #L%
19 */
20 package io.wcm.handler.url.impl;
21
22 import java.net.URI;
23 import java.net.URISyntaxException;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.commons.lang3.Strings;
29 import org.apache.sling.api.SlingHttpServletRequest;
30 import org.apache.sling.api.resource.ResourceResolver;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import io.wcm.sling.commons.util.Escape;
35
36 /**
37 * Utility methods for externalizing URLs.
38 */
39 final class Externalizer {
40
41 private Externalizer() {
42 // static util methods only
43 }
44
45 /**
46 * Externalizes an URL by applying Sling Mapping. Hostname and scheme are not added because they are added by the
47 * link handler depending on site URL configuration and secure/non-secure mode. URLs that are already externalized
48 * remain untouched.
49 * @param url Unexternalized URL (without scheme or hostname)
50 * @param resolver Resource resolver
51 * @param request Request
52 * @return Exernalized URL without scheme or hostname, but with short URLs (if configured in Sling Mapping is
53 * configured), and the path is URL-encoded if it contains special chars.
54 */
55 public static @Nullable String externalizeUrl(@NotNull String url, @NotNull ResourceResolver resolver, @Nullable SlingHttpServletRequest request) {
56 return externalizeUrlWithSlingMapping(url, resolver, request, false);
57 }
58
59 /**
60 * Externalizes a URL by applying Sling Mapping. Hostname and scheme will be added. URLs that are already externalized
61 * remain untouched.
62 * @param url non-externalized URL (without scheme or hostname)
63 * @param resolver Resource resolver
64 * @param request Request
65 * @return Externalized URL with scheme or hostname, short URLs (if configured in Sling Mapping),
66 * and the path is URL-encoded if it contains special chars.
67 */
68 public static @Nullable String externalizeUrlWithHost(@NotNull String url, @NotNull ResourceResolver resolver, @Nullable SlingHttpServletRequest request) {
69 return externalizeUrlWithSlingMapping(url, resolver, request, true);
70 }
71
72 @SuppressWarnings("java:S112") // allow runtime exception
73 private static @Nullable String externalizeUrlWithSlingMapping(@NotNull String url, @NotNull ResourceResolver resolver,
74 @Nullable SlingHttpServletRequest request, boolean keepHost) {
75
76 // apply externalization only path part
77 String path = url;
78
79 // split off query string or fragment that may be appended to the URL
80 String urlRemainder = null;
81 int urlRemainderPos = StringUtils.indexOfAny(path, '?', '#');
82 if (urlRemainderPos >= 0) {
83 urlRemainder = path.substring(urlRemainderPos);
84 path = path.substring(0, urlRemainderPos);
85 }
86
87 // apply reverse mapping based on current sling mapping configuration for current request
88 // e.g. to support a host-based prefix stripping mapping configuration configured at /etc/map
89
90 // please note: the sling map method does a lot of things:
91 // 1. applies reverse mapping depending on the sling mapping configuration
92 // (this can even add a hostname if defined in sling mapping configuration)
93 // 2. applies namespace mangling (e.g. replace jcr: with _jcr_)
94 // 3. adds webapp context path if required
95 // 4. url-encodes the whole url
96 if (request != null) {
97 path = resolver.map(request, path);
98 }
99 else {
100 path = resolver.map(path);
101 }
102
103 if (!keepHost) {
104 // remove scheme and hostname (probably added by sling mapping), but leave path in escaped form
105 try {
106 path = new URI(path).getRawPath();
107 // replace %2F back to / for better readability
108 path = Strings.CS.replace(path, "%2F", "/");
109 }
110 catch (URISyntaxException ex) {
111 throw new RuntimeException("Sling map method returned invalid URI: " + path, ex);
112 }
113 }
114
115 // build full URL again
116 if (path == null) {
117 return null;
118 }
119 else {
120 return path + (urlRemainder != null ? urlRemainder : "");
121 }
122 }
123
124 /**
125 * Externalizes an URL without applying Sling Mapping. Instead the servlet context path is added and sling namespace
126 * mangling is applied manually.
127 * Hostname and scheme are not added because they are added by the link handler depending on site URL configuration
128 * and secure/non-secure mode. URLs that are already externalized remain untouched.
129 * @param url Unexternalized URL (without scheme or hostname)
130 * @param request Request
131 * @return Exernalized URL without scheme or hostname, the path is URL-encoded if it contains special chars.
132 */
133 public static @NotNull String externalizeUrlWithoutMapping(@NotNull String url, @Nullable SlingHttpServletRequest request) {
134
135 // apply externalization only path part
136 String path = url;
137
138 // split off query string or fragment that may be appended to the URL
139 String urlRemainder = null;
140 int urlRemainderPos = StringUtils.indexOfAny(path, '?', '#');
141 if (urlRemainderPos >= 0) {
142 urlRemainder = path.substring(urlRemainderPos);
143 path = path.substring(0, urlRemainderPos);
144 }
145
146 // apply namespace mangling (e.g. replace jcr: with _jcr_)
147 path = mangleNamespaces(path);
148
149 // add webapp context path
150 if (request != null) {
151 path = StringUtils.defaultString(request.getContextPath()) + path; //NOPMD
152 }
153
154 // url-encode path
155 path = Escape.urlEncode(path);
156 path = Strings.CS.replace(path, "+", "%20");
157 // replace %2F back to / for better readability
158 path = Strings.CS.replace(path, "%2F", "/");
159
160 // build full URL again
161 return path + (urlRemainder != null ? urlRemainder : "");
162 }
163
164 /*
165 * Detect as externalized:
166 * - everything staring with protocol and a colon is handled as externalized (http:, tel:, mailto:, javascript: etc.)
167 * - everything starting with // or # is handles as exteranlized
168 * - all other strings handles as not externalized
169 */
170 private static final Pattern EXTERNALIZED_PATTERN = Pattern.compile("^([^/]+:|//|#).+?");
171
172 /**
173 * Checks if the given URL is already externalized.
174 * For this check some heuristics are applied.
175 * @param url URL
176 * @return true if path is already externalized.
177 */
178 public static boolean isExternalized(@NotNull String url) {
179 return EXTERNALIZED_PATTERN.matcher(url).matches();
180 }
181
182 /**
183 * Checks if the given URL can be externalize, that means seems to be an content path that needs externalization.
184 * @param url URL
185 * @return true if url seems to be a path than needs externaliziation
186 */
187 public static boolean isExternalizable(@NotNull String url) {
188 return Strings.CS.startsWith(url, "/");
189 }
190
191 private static final String MANGLED_NAMESPACE_PREFIX = "/_";
192 private static final String MANGLED_NAMESPACE_SUFFIX = "_";
193 private static final char NAMESPACE_SEPARATOR = ':';
194 private static final Pattern NAMESPACE_PATTERN = Pattern.compile("/([^:/]+):");
195
196 /**
197 * Mangle the namespaces in the given path for usage in sling-based URLs.
198 *
199 * <p>
200 * Example: /path/jcr:content to /path/_jcr_content
201 * </p>
202 *
203 * @param path Path to mangle
204 * @return Mangled path
205 */
206 public static @NotNull String mangleNamespaces(@NotNull String path) {
207 if (!StringUtils.contains(path, NAMESPACE_SEPARATOR)) {
208 return path;
209 }
210 Matcher matcher = NAMESPACE_PATTERN.matcher(path);
211 StringBuffer sb = new StringBuffer();
212 while (matcher.find()) {
213 String replacement = MANGLED_NAMESPACE_PREFIX + matcher.group(1) + MANGLED_NAMESPACE_SUFFIX;
214 matcher.appendReplacement(sb, replacement);
215 }
216 matcher.appendTail(sb);
217 return sb.toString();
218 }
219
220 }