View Javadoc
1   package ca.uhn.fhir.util;
2   
3   import ca.uhn.fhir.context.FhirContext;
4   import ca.uhn.fhir.context.RuntimeResourceDefinition;
5   import ca.uhn.fhir.model.primitive.IdDt;
6   import ca.uhn.fhir.parser.DataFormatException;
7   import ca.uhn.fhir.rest.api.Constants;
8   import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
9   import com.google.common.escape.Escaper;
10  import com.google.common.net.PercentEscaper;
11  
12  import java.io.UnsupportedEncodingException;
13  import java.net.MalformedURLException;
14  import java.net.URL;
15  import java.net.URLDecoder;
16  import java.util.*;
17  import java.util.Map.Entry;
18  
19  import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
20  import static org.apache.commons.lang3.StringUtils.defaultString;
21  import static org.apache.commons.lang3.StringUtils.isBlank;
22  
23  /*
24   * #%L
25   * HAPI FHIR - Core Library
26   * %%
27   * Copyright (C) 2014 - 2019 University Health Network
28   * %%
29   * Licensed under the Apache License, Version 2.0 (the "License");
30   * you may not use this file except in compliance with the License.
31   * You may obtain a copy of the License at
32   * 
33   *      http://www.apache.org/licenses/LICENSE-2.0
34   * 
35   * Unless required by applicable law or agreed to in writing, software
36   * distributed under the License is distributed on an "AS IS" BASIS,
37   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38   * See the License for the specific language governing permissions and
39   * limitations under the License.
40   * #L%
41   */
42  
43  public class UrlUtil {
44  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
45  
46  	private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
47  	private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
48  
49  
50  	/**
51  	 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
52  	 */
53  	public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
54  		if (theEndpoint == null) {
55  			return null;
56  		}
57  		if (isAbsolute(theEndpoint)) {
58  			return theEndpoint;
59  		}
60  		if (theBase == null) {
61  			return theEndpoint;
62  		}
63  
64  		try {
65  			return new URL(new URL(theBase), theEndpoint).toString();
66  		} catch (MalformedURLException e) {
67  			ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e);
68  			return theEndpoint;
69  		}
70  	}
71  
72  	public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) {
73  		if (theParentExtensionUrl == null) {
74  			return theExtensionUrl;
75  		}
76  		if (theExtensionUrl == null) {
77  			return null;
78  		}
79  
80  		int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/');
81  		int childLastSlashIdx = theExtensionUrl.lastIndexOf('/');
82  
83  		if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) {
84  			return theExtensionUrl;
85  		}
86  
87  		if (parentLastSlashIdx != childLastSlashIdx) {
88  			return theExtensionUrl;
89  		}
90  
91  		if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) {
92  			return theExtensionUrl;
93  		}
94  
95  		if (theExtensionUrl.length() > parentLastSlashIdx) {
96  			return theExtensionUrl.substring(parentLastSlashIdx + 1);
97  		}
98  
99  		return theExtensionUrl;
100 	}
101 
102 	/**
103 	 * URL encode a value according to RFC 3986
104 	 * <p>
105 	 * This method is intended to be applied to an individual parameter
106 	 * name or value. For example, if you are creating the URL
107 	 * <code>http://example.com/fhir/Patient?key=føø</code>
108 	 * it would be appropriate to pass the string "føø" to this method,
109 	 * but not appropriate to pass the entire URL since characters
110 	 * such as "/" and "?" would also be escaped.
111 	 * </P>
112 	 */
113 	public static String escapeUrlParam(String theUnescaped) {
114 		if (theUnescaped == null) {
115 			return null;
116 		}
117 		return PARAMETER_ESCAPER.escape(theUnescaped);
118 	}
119 
120 
121 	public static boolean isAbsolute(String theValue) {
122 		String value = theValue.toLowerCase();
123 		return value.startsWith("http://") || value.startsWith("https://");
124 	}
125 
126 	public static boolean isNeedsSanitization(String theString) {
127 		if (theString != null) {
128 			for (int i = 0; i < theString.length(); i++) {
129 				char nextChar = theString.charAt(i);
130 				if (nextChar == '<' || nextChar == '"') {
131 					return true;
132 				}
133 			}
134 		}
135 		return false;
136 	}
137 
138 	public static boolean isValid(String theUrl) {
139 		if (theUrl == null || theUrl.length() < 8) {
140 			return false;
141 		}
142 
143 		String url = theUrl.toLowerCase();
144 		if (url.charAt(0) != 'h') {
145 			return false;
146 		}
147 		if (url.charAt(1) != 't') {
148 			return false;
149 		}
150 		if (url.charAt(2) != 't') {
151 			return false;
152 		}
153 		if (url.charAt(3) != 'p') {
154 			return false;
155 		}
156 		int slashOffset;
157 		if (url.charAt(4) == ':') {
158 			slashOffset = 5;
159 		} else if (url.charAt(4) == 's') {
160 			if (url.charAt(5) != ':') {
161 				return false;
162 			}
163 			slashOffset = 6;
164 		} else {
165 			return false;
166 		}
167 
168 		if (url.charAt(slashOffset) != '/') {
169 			return false;
170 		}
171 		if (url.charAt(slashOffset + 1) != '/') {
172 			return false;
173 		}
174 
175 		return true;
176 	}
177 
178 	public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException {
179 		int paramIndex = theUrl.indexOf('?');
180 		String resourceName = theUrl.substring(0, paramIndex);
181 		if (resourceName.contains("/")) {
182 			resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
183 		}
184 		return theCtx.getResourceDefinition(resourceName);
185 	}
186 
187 	public static Map<String, String[]> parseQueryString(String theQueryString) {
188 		HashMap<String, List<String>> map = new HashMap<>();
189 		parseQueryString(theQueryString, map);
190 		return toQueryStringMap(map);
191 	}
192 
193 	private static void parseQueryString(String theQueryString, HashMap<String, List<String>> map) {
194 		String query = defaultString(theQueryString);
195 		if (query.startsWith("?")) {
196 			query = query.substring(1);
197 		}
198 
199 
200 		StringTokenizer tok = new StringTokenizer(query, "&");
201 		while (tok.hasMoreTokens()) {
202 			String nextToken = tok.nextToken();
203 			if (isBlank(nextToken)) {
204 				continue;
205 			}
206 
207 			int equalsIndex = nextToken.indexOf('=');
208 			String nextValue;
209 			String nextKey;
210 			if (equalsIndex == -1) {
211 				nextKey = nextToken;
212 				nextValue = "";
213 			} else {
214 				nextKey = nextToken.substring(0, equalsIndex);
215 				nextValue = nextToken.substring(equalsIndex + 1);
216 			}
217 
218 			nextKey = unescape(nextKey);
219 			nextValue = unescape(nextValue);
220 
221 			List<String> list = map.computeIfAbsent(nextKey, k -> new ArrayList<>());
222 			list.add(nextValue);
223 		}
224 	}
225 
226 	public static Map<String, String[]> parseQueryStrings(String... theQueryString) {
227 		HashMap<String, List<String>> map = new HashMap<>();
228 		for (String next : theQueryString) {
229 			parseQueryString(next, map);
230 		}
231 		return toQueryStringMap(map);
232 	}
233 
234 	/**
235 	 * Parse a URL in one of the following forms:
236 	 * <ul>
237 	 * <li>[Resource Type]?[Search Params]
238 	 * <li>[Resource Type]/[Resource ID]
239 	 * <li>[Resource Type]/[Resource ID]/_history/[Version ID]
240 	 * </ul>
241 	 */
242 	public static UrlParts parseUrl(String theUrl) {
243 		String url = theUrl;
244 		UrlParts retVal = new UrlParts();
245 		if (url.startsWith("http")) {
246 			if (url.startsWith("/")) {
247 				url = url.substring(1);
248 			}
249 
250 			int qmIdx = url.indexOf('?');
251 			if (qmIdx != -1) {
252 				retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null));
253 				url = url.substring(0, qmIdx);
254 			}
255 
256 			IdDt id = new IdDt(url);
257 			retVal.setResourceType(id.getResourceType());
258 			retVal.setResourceId(id.getIdPart());
259 			retVal.setVersionId(id.getVersionIdPart());
260 			return retVal;
261 		}
262 		if (url.matches("/[a-zA-Z]+\\?.*")) {
263 			url = url.substring(1);
264 		}
265 		int nextStart = 0;
266 		boolean nextIsHistory = false;
267 
268 		for (int idx = 0; idx < url.length(); idx++) {
269 			char nextChar = url.charAt(idx);
270 			boolean atEnd = (idx + 1) == url.length();
271 			if (nextChar == '?' || nextChar == '/' || atEnd) {
272 				int endIdx = (atEnd && nextChar != '?') ? idx + 1 : idx;
273 				String nextSubstring = url.substring(nextStart, endIdx);
274 				if (retVal.getResourceType() == null) {
275 					retVal.setResourceType(nextSubstring);
276 				} else if (retVal.getResourceId() == null) {
277 					retVal.setResourceId(nextSubstring);
278 				} else if (nextIsHistory) {
279 					retVal.setVersionId(nextSubstring);
280 				} else {
281 					if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) {
282 						nextIsHistory = true;
283 					} else {
284 						throw new InvalidRequestException("Invalid FHIR resource URL: " + url);
285 					}
286 				}
287 				if (nextChar == '?') {
288 					if (url.length() > idx + 1) {
289 						retVal.setParams(url.substring(idx + 1, url.length()));
290 					}
291 					break;
292 				}
293 				nextStart = idx + 1;
294 			}
295 		}
296 
297 		return retVal;
298 
299 	}
300 
301 	/**
302 	 * This method specifically HTML-encodes the &quot; and
303 	 * &lt; characters in order to prevent injection attacks
304 	 */
305 	public static String sanitizeUrlPart(String theString) {
306 		if (theString == null) {
307 			return null;
308 		}
309 
310 		boolean needsSanitization = isNeedsSanitization(theString);
311 
312 		if (needsSanitization) {
313 			// Ok, we're sanitizing
314 			StringBuilder buffer = new StringBuilder(theString.length() + 10);
315 			for (int j = 0; j < theString.length(); j++) {
316 
317 				char nextChar = theString.charAt(j);
318 				switch (nextChar) {
319 					case '"':
320 						buffer.append("&quot;");
321 						break;
322 					case '<':
323 						buffer.append("&lt;");
324 						break;
325 					default:
326 						buffer.append(nextChar);
327 						break;
328 				}
329 
330 			} // for build escaped string
331 
332 			return buffer.toString();
333 		}
334 
335 		return theString;
336 	}
337 
338 	private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
339 		HashMap<String, String[]> retVal = new HashMap<>();
340 		for (Entry<String, List<String>> nextEntry : map.entrySet()) {
341 			retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[0]));
342 		}
343 		return retVal;
344 	}
345 
346 	public static String unescape(String theString) {
347 		if (theString == null) {
348 			return null;
349 		}
350 		for (int i = 0; i < theString.length(); i++) {
351 			char nextChar = theString.charAt(i);
352 			if (nextChar == '%' || nextChar == '+') {
353 				try {
354 					return URLDecoder.decode(theString, "UTF-8");
355 				} catch (UnsupportedEncodingException e) {
356 					throw new Error("UTF-8 not supported, this shouldn't happen", e);
357 				}
358 			}
359 		}
360 		return theString;
361 	}
362 
363 	public static class UrlParts {
364 		private String myParams;
365 		private String myResourceId;
366 		private String myResourceType;
367 		private String myVersionId;
368 
369 		public String getParams() {
370 			return myParams;
371 		}
372 
373 		public void setParams(String theParams) {
374 			myParams = theParams;
375 		}
376 
377 		public String getResourceId() {
378 			return myResourceId;
379 		}
380 
381 		public void setResourceId(String theResourceId) {
382 			myResourceId = theResourceId;
383 		}
384 
385 		public String getResourceType() {
386 			return myResourceType;
387 		}
388 
389 		public void setResourceType(String theResourceType) {
390 			myResourceType = theResourceType;
391 		}
392 
393 		public String getVersionId() {
394 			return myVersionId;
395 		}
396 
397 		public void setVersionId(String theVersionId) {
398 			myVersionId = theVersionId;
399 		}
400 	}
401 
402 }