blob: 5d704cb09dcb2780323ac277f023f37b985eac60 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
Nate Fischer3442c742017-09-08 17:02:00 -070019import android.annotation.Nullable;
Mathew Inwood42afea22018-08-16 19:18:28 +010020import android.annotation.UnsupportedAppUsage;
Nate Fischer0a6140d2017-09-05 12:37:49 -070021import android.net.ParseException;
22import android.net.Uri;
23import android.net.WebAddress;
24import android.util.Log;
25
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import java.io.UnsupportedEncodingException;
Elliott Hughescb64d432013-08-02 10:00:44 -070027import java.util.Locale;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import java.util.regex.Matcher;
29import java.util.regex.Pattern;
30
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031public final class URLUtil {
32
33 private static final String LOGTAG = "webkit";
Ignacio Solla451e3382014-11-10 10:35:54 +000034 private static final boolean TRACE = false;
Grace Klobabd5c8232009-12-07 10:11:28 -080035
36 // to refer to bar.png under your package's asset/foo/ directory, use
37 // "file:///android_asset/foo/bar.png".
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038 static final String ASSET_BASE = "file:///android_asset/";
Grace Klobabd5c8232009-12-07 10:11:28 -080039 // to refer to bar.png under your package's res/drawable/ directory, use
40 // "file:///android_res/drawable/bar.png". Use "drawable" to refer to
41 // "drawable-hdpi" directory as well.
42 static final String RESOURCE_BASE = "file:///android_res/";
Nate Fischerd674b092018-02-27 20:59:29 -080043 static final String FILE_BASE = "file:";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044 static final String PROXY_BASE = "file:///cookieless_proxy/";
Jonathan Dixon0fa72ef2012-03-27 22:18:19 +010045 static final String CONTENT_BASE = "content:";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046
47 /**
48 * Cleans up (if possible) user-entered web addresses
49 */
50 public static String guessUrl(String inUrl) {
51
52 String retVal = inUrl;
53 WebAddress webAddress;
54
Ignacio Solla451e3382014-11-10 10:35:54 +000055 if (TRACE) Log.v(LOGTAG, "guessURL before queueRequest: " + inUrl);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056
57 if (inUrl.length() == 0) return inUrl;
58 if (inUrl.startsWith("about:")) return inUrl;
59 // Do not try to interpret data scheme URLs
60 if (inUrl.startsWith("data:")) return inUrl;
61 // Do not try to interpret file scheme URLs
62 if (inUrl.startsWith("file:")) return inUrl;
63 // Do not try to interpret javascript scheme URLs
64 if (inUrl.startsWith("javascript:")) return inUrl;
65
66 // bug 762454: strip period off end of url
67 if (inUrl.endsWith(".") == true) {
68 inUrl = inUrl.substring(0, inUrl.length() - 1);
69 }
70
71 try {
72 webAddress = new WebAddress(inUrl);
73 } catch (ParseException ex) {
74
Ignacio Solla451e3382014-11-10 10:35:54 +000075 if (TRACE) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076 Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl);
77 }
78 return retVal;
79 }
80
81 // Check host
Bjorn Bringerteb8be972010-10-12 16:24:55 +010082 if (webAddress.getHost().indexOf('.') == -1) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080083 // no dot: user probably entered a bare domain. try .com
Bjorn Bringerteb8be972010-10-12 16:24:55 +010084 webAddress.setHost("www." + webAddress.getHost() + ".com");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085 }
86 return webAddress.toString();
87 }
88
89 public static String composeSearchUrl(String inQuery, String template,
90 String queryPlaceHolder) {
91 int placeHolderIndex = template.indexOf(queryPlaceHolder);
92 if (placeHolderIndex < 0) {
93 return null;
94 }
95
96 String query;
97 StringBuilder buffer = new StringBuilder();
98 buffer.append(template.substring(0, placeHolderIndex));
99
100 try {
101 query = java.net.URLEncoder.encode(inQuery, "utf-8");
102 buffer.append(query);
103 } catch (UnsupportedEncodingException ex) {
104 return null;
105 }
106
107 buffer.append(template.substring(
108 placeHolderIndex + queryPlaceHolder.length()));
109
110 return buffer.toString();
111 }
112
113 public static byte[] decode(byte[] url) throws IllegalArgumentException {
114 if (url.length == 0) {
115 return new byte[0];
116 }
117
118 // Create a new byte array with the same length to ensure capacity
119 byte[] tempData = new byte[url.length];
120
121 int tempCount = 0;
122 for (int i = 0; i < url.length; i++) {
123 byte b = url[i];
124 if (b == '%') {
125 if (url.length - i > 2) {
126 b = (byte) (parseHex(url[i + 1]) * 16
127 + parseHex(url[i + 2]));
128 i += 2;
129 } else {
130 throw new IllegalArgumentException("Invalid format");
131 }
132 }
133 tempData[tempCount++] = b;
134 }
135 byte[] retData = new byte[tempCount];
136 System.arraycopy(tempData, 0, retData, 0, tempCount);
137 return retData;
138 }
139
Grace Kloba758bf412009-08-11 11:47:24 -0700140 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700141 * @return {@code true} if the url is correctly URL encoded
Grace Kloba758bf412009-08-11 11:47:24 -0700142 */
Mathew Inwood42afea22018-08-16 19:18:28 +0100143 @UnsupportedAppUsage
Grace Kloba758bf412009-08-11 11:47:24 -0700144 static boolean verifyURLEncoding(String url) {
145 int count = url.length();
146 if (count == 0) {
147 return false;
148 }
149
150 int index = url.indexOf('%');
151 while (index >= 0 && index < count) {
152 if (index < count - 2) {
153 try {
154 parseHex((byte) url.charAt(++index));
155 parseHex((byte) url.charAt(++index));
156 } catch (IllegalArgumentException e) {
157 return false;
158 }
159 } else {
160 return false;
161 }
162 index = url.indexOf('%', index + 1);
163 }
164 return true;
165 }
166
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800167 private static int parseHex(byte b) {
168 if (b >= '0' && b <= '9') return (b - '0');
169 if (b >= 'A' && b <= 'F') return (b - 'A' + 10);
170 if (b >= 'a' && b <= 'f') return (b - 'a' + 10);
171
172 throw new IllegalArgumentException("Invalid hex char '" + b + "'");
173 }
174
175 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700176 * @return {@code true} if the url is an asset file.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800177 */
178 public static boolean isAssetUrl(String url) {
179 return (null != url) && url.startsWith(ASSET_BASE);
180 }
Grace Klobabd5c8232009-12-07 10:11:28 -0800181
182 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700183 * @return {@code true} if the url is a resource file.
Grace Klobabd5c8232009-12-07 10:11:28 -0800184 * @hide
185 */
Mathew Inwood42afea22018-08-16 19:18:28 +0100186 @UnsupportedAppUsage
Grace Klobabd5c8232009-12-07 10:11:28 -0800187 public static boolean isResourceUrl(String url) {
188 return (null != url) && url.startsWith(RESOURCE_BASE);
189 }
190
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700192 * @return {@code true} if the url is a proxy url to allow cookieless network
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193 * requests from a file url.
194 * @deprecated Cookieless proxy is no longer supported.
195 */
Dianne Hackborn4a51c202009-08-21 15:14:02 -0700196 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800197 public static boolean isCookielessProxyUrl(String url) {
198 return (null != url) && url.startsWith(PROXY_BASE);
199 }
200
201 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700202 * @return {@code true} if the url is a local file.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 */
204 public static boolean isFileUrl(String url) {
205 return (null != url) && (url.startsWith(FILE_BASE) &&
206 !url.startsWith(ASSET_BASE) &&
207 !url.startsWith(PROXY_BASE));
208 }
209
210 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700211 * @return {@code true} if the url is an about: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800212 */
213 public static boolean isAboutUrl(String url) {
214 return (null != url) && url.startsWith("about:");
215 }
216
217 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700218 * @return {@code true} if the url is a data: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800219 */
220 public static boolean isDataUrl(String url) {
221 return (null != url) && url.startsWith("data:");
222 }
223
224 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700225 * @return {@code true} if the url is a javascript: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 */
227 public static boolean isJavaScriptUrl(String url) {
228 return (null != url) && url.startsWith("javascript:");
229 }
230
231 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700232 * @return {@code true} if the url is an http: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800233 */
234 public static boolean isHttpUrl(String url) {
235 return (null != url) &&
236 (url.length() > 6) &&
237 url.substring(0, 7).equalsIgnoreCase("http://");
238 }
239
240 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700241 * @return {@code true} if the url is an https: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800242 */
243 public static boolean isHttpsUrl(String url) {
244 return (null != url) &&
245 (url.length() > 7) &&
246 url.substring(0, 8).equalsIgnoreCase("https://");
247 }
248
249 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700250 * @return {@code true} if the url is a network url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800251 */
252 public static boolean isNetworkUrl(String url) {
253 if (url == null || url.length() == 0) {
254 return false;
255 }
256 return isHttpUrl(url) || isHttpsUrl(url);
257 }
258
259 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700260 * @return {@code true} if the url is a content: url.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800261 */
262 public static boolean isContentUrl(String url) {
Jonathan Dixon0fa72ef2012-03-27 22:18:19 +0100263 return (null != url) && url.startsWith(CONTENT_BASE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800264 }
265
266 /**
Nate Fischer2be201e2017-10-26 10:43:00 -0700267 * @return {@code true} if the url is valid.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268 */
269 public static boolean isValidUrl(String url) {
270 if (url == null || url.length() == 0) {
271 return false;
272 }
273
274 return (isAssetUrl(url) ||
Grace Klobabd5c8232009-12-07 10:11:28 -0800275 isResourceUrl(url) ||
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 isFileUrl(url) ||
277 isAboutUrl(url) ||
278 isHttpUrl(url) ||
279 isHttpsUrl(url) ||
280 isJavaScriptUrl(url) ||
281 isContentUrl(url));
282 }
283
284 /**
285 * Strips the url of the anchor.
286 */
287 public static String stripAnchor(String url) {
288 int anchorIndex = url.indexOf('#');
289 if (anchorIndex != -1) {
290 return url.substring(0, anchorIndex);
291 }
292 return url;
293 }
Ignacio Solla451e3382014-11-10 10:35:54 +0000294
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800295 /**
296 * Guesses canonical filename that a download would have, using
297 * the URL and contentDisposition. File extension, if not defined,
298 * is added based on the mimetype
299 * @param url Url to the content
Nate Fischer0a6140d2017-09-05 12:37:49 -0700300 * @param contentDisposition Content-Disposition HTTP header or {@code null}
301 * @param mimeType Mime-type of the content or {@code null}
Ignacio Solla451e3382014-11-10 10:35:54 +0000302 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800303 * @return suggested filename
304 */
305 public static final String guessFileName(
306 String url,
Nate Fischer3442c742017-09-08 17:02:00 -0700307 @Nullable String contentDisposition,
308 @Nullable String mimeType) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800309 String filename = null;
310 String extension = null;
311
312 // If we couldn't do anything with the hint, move toward the content disposition
313 if (filename == null && contentDisposition != null) {
314 filename = parseContentDisposition(contentDisposition);
315 if (filename != null) {
316 int index = filename.lastIndexOf('/') + 1;
317 if (index > 0) {
318 filename = filename.substring(index);
319 }
320 }
321 }
322
323 // If all the other http-related approaches failed, use the plain uri
324 if (filename == null) {
325 String decodedUrl = Uri.decode(url);
326 if (decodedUrl != null) {
327 int queryIndex = decodedUrl.indexOf('?');
328 // If there is a query string strip it, same as desktop browsers
329 if (queryIndex > 0) {
330 decodedUrl = decodedUrl.substring(0, queryIndex);
331 }
332 if (!decodedUrl.endsWith("/")) {
333 int index = decodedUrl.lastIndexOf('/') + 1;
334 if (index > 0) {
335 filename = decodedUrl.substring(index);
336 }
337 }
338 }
339 }
340
341 // Finally, if couldn't get filename from URI, get a generic filename
342 if (filename == null) {
343 filename = "downloadfile";
344 }
345
346 // Split filename between base and extension
347 // Add an extension if filename does not have one
348 int dotIndex = filename.indexOf('.');
349 if (dotIndex < 0) {
350 if (mimeType != null) {
351 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
352 if (extension != null) {
353 extension = "." + extension;
354 }
355 }
356 if (extension == null) {
Elliott Hughescb64d432013-08-02 10:00:44 -0700357 if (mimeType != null && mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800358 if (mimeType.equalsIgnoreCase("text/html")) {
359 extension = ".html";
360 } else {
361 extension = ".txt";
362 }
363 } else {
364 extension = ".bin";
365 }
366 }
367 } else {
368 if (mimeType != null) {
369 // Compare the last segment of the extension against the mime type.
370 // If there's a mismatch, discard the entire extension.
371 int lastDotIndex = filename.lastIndexOf('.');
372 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
373 filename.substring(lastDotIndex + 1));
374 if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
375 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
376 if (extension != null) {
377 extension = "." + extension;
378 }
379 }
380 }
381 if (extension == null) {
382 extension = filename.substring(dotIndex);
383 }
384 filename = filename.substring(0, dotIndex);
385 }
386
387 return filename + extension;
388 }
389
390 /** Regex used to parse content-disposition headers */
391 private static final Pattern CONTENT_DISPOSITION_PATTERN =
Ben Murdoch208360b2009-10-13 10:46:15 +0100392 Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$",
393 Pattern.CASE_INSENSITIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800394
Nate Fischer5cca7bd2017-09-26 18:51:49 -0700395 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800396 * Parse the Content-Disposition HTTP Header. The format of the header
397 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
398 * This header provides a filename for content that is going to be
399 * downloaded to the file system. We only support the attachment type.
Ben Murdoch208360b2009-10-13 10:46:15 +0100400 * Note that RFC 2616 specifies the filename value must be double-quoted.
401 * Unfortunately some servers do not quote the value so to maintain
402 * consistent behaviour with other browsers, we allow unquoted values too.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800403 */
Mathew Inwood42afea22018-08-16 19:18:28 +0100404 @UnsupportedAppUsage
Grace Klobaf8ddc982009-07-14 15:44:58 -0700405 static String parseContentDisposition(String contentDisposition) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800406 try {
407 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
408 if (m.find()) {
Ben Murdoch208360b2009-10-13 10:46:15 +0100409 return m.group(2);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800410 }
411 } catch (IllegalStateException ex) {
412 // This function is defined as returning null when it can't parse the header
413 }
414 return null;
415 }
416}