001package ca.uhn.fhir.util;
002
003import ca.uhn.fhir.model.primitive.IdDt;
004import ca.uhn.fhir.rest.api.Constants;
005import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
006import com.google.common.escape.Escaper;
007import com.google.common.net.PercentEscaper;
008
009import java.io.UnsupportedEncodingException;
010import java.net.MalformedURLException;
011import java.net.URL;
012import java.net.URLDecoder;
013import java.util.*;
014import java.util.Map.Entry;
015
016import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
017import static org.apache.commons.lang3.StringUtils.defaultString;
018import static org.apache.commons.lang3.StringUtils.isBlank;
019
020/*
021 * #%L
022 * HAPI FHIR - Core Library
023 * %%
024 * Copyright (C) 2014 - 2018 University Health Network
025 * %%
026 * Licensed under the Apache License, Version 2.0 (the "License");
027 * you may not use this file except in compliance with the License.
028 * You may obtain a copy of the License at
029 * 
030 *      http://www.apache.org/licenses/LICENSE-2.0
031 * 
032 * Unless required by applicable law or agreed to in writing, software
033 * distributed under the License is distributed on an "AS IS" BASIS,
034 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
035 * See the License for the specific language governing permissions and
036 * limitations under the License.
037 * #L%
038 */
039
040public class UrlUtil {
041        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
042
043        private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
044        private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
045
046
047        /**
048         * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
049         */
050        public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
051                if (theEndpoint == null) {
052                        return null;
053                }
054                if (isAbsolute(theEndpoint)) {
055                        return theEndpoint;
056                }
057                if (theBase == null) {
058                        return theEndpoint;
059                }
060
061                try {
062                        return new URL(new URL(theBase), theEndpoint).toString();
063                } catch (MalformedURLException e) {
064                        ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e);
065                        return theEndpoint;
066                }
067        }
068
069        public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) {
070                if (theParentExtensionUrl == null) {
071                        return theExtensionUrl;
072                }
073                if (theExtensionUrl == null) {
074                        return null;
075                }
076
077                int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/');
078                int childLastSlashIdx = theExtensionUrl.lastIndexOf('/');
079
080                if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) {
081                        return theExtensionUrl;
082                }
083
084                if (parentLastSlashIdx != childLastSlashIdx) {
085                        return theExtensionUrl;
086                }
087
088                if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) {
089                        return theExtensionUrl;
090                }
091
092                if (theExtensionUrl.length() > parentLastSlashIdx) {
093                        return theExtensionUrl.substring(parentLastSlashIdx + 1);
094                }
095
096                return theExtensionUrl;
097        }
098
099        /**
100         * URL encode a value according to RFC 3986
101         * <p>
102         * This method is intended to be applied to an individual parameter
103         * name or value. For example, if you are creating the URL
104         * <code>http://example.com/fhir/Patient?key=føø</code>
105         * it would be appropriate to pass the string "føø" to this method,
106         * but not appropriate to pass the entire URL since characters
107         * such as "/" and "?" would also be escaped.
108         * </P>
109         */
110        public static String escapeUrlParam(String theUnescaped) {
111                if (theUnescaped == null) {
112                        return null;
113                }
114                return PARAMETER_ESCAPER.escape(theUnescaped);
115        }
116
117
118        public static boolean isAbsolute(String theValue) {
119                String value = theValue.toLowerCase();
120                return value.startsWith("http://") || value.startsWith("https://");
121        }
122
123        public static boolean isNeedsSanitization(String theString) {
124                if (theString != null) {
125                        for (int i = 0; i < theString.length(); i++) {
126                                char nextChar = theString.charAt(i);
127                                if (nextChar == '<' || nextChar == '"') {
128                                        return true;
129                                }
130                        }
131                }
132                return false;
133        }
134
135        public static boolean isValid(String theUrl) {
136                if (theUrl == null || theUrl.length() < 8) {
137                        return false;
138                }
139
140                String url = theUrl.toLowerCase();
141                if (url.charAt(0) != 'h') {
142                        return false;
143                }
144                if (url.charAt(1) != 't') {
145                        return false;
146                }
147                if (url.charAt(2) != 't') {
148                        return false;
149                }
150                if (url.charAt(3) != 'p') {
151                        return false;
152                }
153                int slashOffset;
154                if (url.charAt(4) == ':') {
155                        slashOffset = 5;
156                } else if (url.charAt(4) == 's') {
157                        if (url.charAt(5) != ':') {
158                                return false;
159                        }
160                        slashOffset = 6;
161                } else {
162                        return false;
163                }
164
165                if (url.charAt(slashOffset) != '/') {
166                        return false;
167                }
168                if (url.charAt(slashOffset + 1) != '/') {
169                        return false;
170                }
171
172                return true;
173        }
174
175        public static void main(String[] args) {
176                System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005"));
177        }
178
179        public static Map<String, String[]> parseQueryString(String theQueryString) {
180                HashMap<String, List<String>> map = new HashMap<>();
181                parseQueryString(theQueryString, map);
182                return toQueryStringMap(map);
183        }
184
185        private static void parseQueryString(String theQueryString, HashMap<String, List<String>> map) {
186                String query = defaultString(theQueryString);
187                if (query.startsWith("?")) {
188                        query = query.substring(1);
189                }
190
191
192                StringTokenizer tok = new StringTokenizer(query, "&");
193                while (tok.hasMoreTokens()) {
194                        String nextToken = tok.nextToken();
195                        if (isBlank(nextToken)) {
196                                continue;
197                        }
198
199                        int equalsIndex = nextToken.indexOf('=');
200                        String nextValue;
201                        String nextKey;
202                        if (equalsIndex == -1) {
203                                nextKey = nextToken;
204                                nextValue = "";
205                        } else {
206                                nextKey = nextToken.substring(0, equalsIndex);
207                                nextValue = nextToken.substring(equalsIndex + 1);
208                        }
209
210                        nextKey = unescape(nextKey);
211                        nextValue = unescape(nextValue);
212
213                        List<String> list = map.computeIfAbsent(nextKey, k -> new ArrayList<>());
214                        list.add(nextValue);
215                }
216        }
217
218        public static Map<String, String[]> parseQueryStrings(String... theQueryString) {
219                HashMap<String, List<String>> map = new HashMap<>();
220                for (String next : theQueryString) {
221                        parseQueryString(next, map);
222                }
223                return toQueryStringMap(map);
224        }
225
226        /**
227         * Parse a URL in one of the following forms:
228         * <ul>
229         * <li>[Resource Type]?[Search Params]
230         * <li>[Resource Type]/[Resource ID]
231         * <li>[Resource Type]/[Resource ID]/_history/[Version ID]
232         * </ul>
233         */
234        public static UrlParts parseUrl(String theUrl) {
235                String url = theUrl;
236                UrlParts retVal = new UrlParts();
237                if (url.startsWith("http")) {
238                        if (url.startsWith("/")) {
239                                url = url.substring(1);
240                        }
241
242                        int qmIdx = url.indexOf('?');
243                        if (qmIdx != -1) {
244                                retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null));
245                                url = url.substring(0, qmIdx);
246                        }
247
248                        IdDt id = new IdDt(url);
249                        retVal.setResourceType(id.getResourceType());
250                        retVal.setResourceId(id.getIdPart());
251                        retVal.setVersionId(id.getVersionIdPart());
252                        return retVal;
253                }
254                if (url.matches("/[a-zA-Z]+\\?.*")) {
255                        url = url.substring(1);
256                }
257                int nextStart = 0;
258                boolean nextIsHistory = false;
259
260                for (int idx = 0; idx < url.length(); idx++) {
261                        char nextChar = url.charAt(idx);
262                        boolean atEnd = (idx + 1) == url.length();
263                        if (nextChar == '?' || nextChar == '/' || atEnd) {
264                                int endIdx = (atEnd && nextChar != '?') ? idx + 1 : idx;
265                                String nextSubstring = url.substring(nextStart, endIdx);
266                                if (retVal.getResourceType() == null) {
267                                        retVal.setResourceType(nextSubstring);
268                                } else if (retVal.getResourceId() == null) {
269                                        retVal.setResourceId(nextSubstring);
270                                } else if (nextIsHistory) {
271                                        retVal.setVersionId(nextSubstring);
272                                } else {
273                                        if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) {
274                                                nextIsHistory = true;
275                                        } else {
276                                                throw new InvalidRequestException("Invalid FHIR resource URL: " + url);
277                                        }
278                                }
279                                if (nextChar == '?') {
280                                        if (url.length() > idx + 1) {
281                                                retVal.setParams(url.substring(idx + 1, url.length()));
282                                        }
283                                        break;
284                                }
285                                nextStart = idx + 1;
286                        }
287                }
288
289                return retVal;
290
291        }
292
293        /**
294         * This method specifically HTML-encodes the &quot; and
295         * &lt; characters in order to prevent injection attacks
296         */
297        public static String sanitizeUrlPart(String theString) {
298                if (theString == null) {
299                        return null;
300                }
301
302                boolean needsSanitization = isNeedsSanitization(theString);
303
304                if (needsSanitization) {
305                        // Ok, we're sanitizing
306                        StringBuilder buffer = new StringBuilder(theString.length() + 10);
307                        for (int j = 0; j < theString.length(); j++) {
308
309                                char nextChar = theString.charAt(j);
310                                switch (nextChar) {
311                                        case '"':
312                                                buffer.append("&quot;");
313                                                break;
314                                        case '<':
315                                                buffer.append("&lt;");
316                                                break;
317                                        default:
318                                                buffer.append(nextChar);
319                                                break;
320                                }
321
322                        } // for build escaped string
323
324                        return buffer.toString();
325                }
326
327                return theString;
328        }
329
330        private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
331                HashMap<String, String[]> retVal = new HashMap<>();
332                for (Entry<String, List<String>> nextEntry : map.entrySet()) {
333                        retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[0]));
334                }
335                return retVal;
336        }
337
338        public static String unescape(String theString) {
339                if (theString == null) {
340                        return null;
341                }
342                for (int i = 0; i < theString.length(); i++) {
343                        char nextChar = theString.charAt(i);
344                        if (nextChar == '%' || nextChar == '+') {
345                                try {
346                                        return URLDecoder.decode(theString, "UTF-8");
347                                } catch (UnsupportedEncodingException e) {
348                                        throw new Error("UTF-8 not supported, this shouldn't happen", e);
349                                }
350                        }
351                }
352                return theString;
353        }
354
355        public static class UrlParts {
356                private String myParams;
357                private String myResourceId;
358                private String myResourceType;
359                private String myVersionId;
360
361                public String getParams() {
362                        return myParams;
363                }
364
365                public void setParams(String theParams) {
366                        myParams = theParams;
367                }
368
369                public String getResourceId() {
370                        return myResourceId;
371                }
372
373                public void setResourceId(String theResourceId) {
374                        myResourceId = theResourceId;
375                }
376
377                public String getResourceType() {
378                        return myResourceType;
379                }
380
381                public void setResourceType(String theResourceType) {
382                        myResourceType = theResourceType;
383                }
384
385                public String getVersionId() {
386                        return myVersionId;
387                }
388
389                public void setVersionId(String theVersionId) {
390                        myVersionId = theVersionId;
391                }
392        }
393
394}