001package ca.uhn.fhir.util;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.RuntimeResourceDefinition;
005import ca.uhn.fhir.model.primitive.IdDt;
006import ca.uhn.fhir.parser.DataFormatException;
007import ca.uhn.fhir.rest.api.Constants;
008import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
009import com.google.common.escape.Escaper;
010import com.google.common.net.PercentEscaper;
011
012import java.io.UnsupportedEncodingException;
013import java.net.MalformedURLException;
014import java.net.URL;
015import java.net.URLDecoder;
016import java.util.*;
017import java.util.Map.Entry;
018
019import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
020import static org.apache.commons.lang3.StringUtils.defaultString;
021import static org.apache.commons.lang3.StringUtils.isBlank;
022
023/*
024 * #%L
025 * HAPI FHIR - Core Library
026 * %%
027 * Copyright (C) 2014 - 2019 University Health Network
028 * %%
029 * Licensed under the Apache License, Version 2.0 (the "License");
030 * you may not use this file except in compliance with the License.
031 * You may obtain a copy of the License at
032 * 
033 *      http://www.apache.org/licenses/LICENSE-2.0
034 * 
035 * Unless required by applicable law or agreed to in writing, software
036 * distributed under the License is distributed on an "AS IS" BASIS,
037 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
038 * See the License for the specific language governing permissions and
039 * limitations under the License.
040 * #L%
041 */
042
043public class UrlUtil {
044        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
045
046        private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
047        private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
048
049
050        /**
051         * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
052         */
053        public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
054                if (theEndpoint == null) {
055                        return null;
056                }
057                if (isAbsolute(theEndpoint)) {
058                        return theEndpoint;
059                }
060                if (theBase == null) {
061                        return theEndpoint;
062                }
063
064                try {
065                        return new URL(new URL(theBase), theEndpoint).toString();
066                } catch (MalformedURLException e) {
067                        ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e);
068                        return theEndpoint;
069                }
070        }
071
072        public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) {
073                if (theParentExtensionUrl == null) {
074                        return theExtensionUrl;
075                }
076                if (theExtensionUrl == null) {
077                        return null;
078                }
079
080                int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/');
081                int childLastSlashIdx = theExtensionUrl.lastIndexOf('/');
082
083                if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) {
084                        return theExtensionUrl;
085                }
086
087                if (parentLastSlashIdx != childLastSlashIdx) {
088                        return theExtensionUrl;
089                }
090
091                if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) {
092                        return theExtensionUrl;
093                }
094
095                if (theExtensionUrl.length() > parentLastSlashIdx) {
096                        return theExtensionUrl.substring(parentLastSlashIdx + 1);
097                }
098
099                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}