/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit; import android.annotation.Nullable; import android.net.ParseException; import android.net.Uri; import android.net.WebAddress; import android.util.Log; import java.io.UnsupportedEncodingException; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class URLUtil { private static final String LOGTAG = "webkit"; private static final boolean TRACE = false; // to refer to bar.png under your package's asset/foo/ directory, use // "file:///android_asset/foo/bar.png". static final String ASSET_BASE = "file:///android_asset/"; // to refer to bar.png under your package's res/drawable/ directory, use // "file:///android_res/drawable/bar.png". Use "drawable" to refer to // "drawable-hdpi" directory as well. static final String RESOURCE_BASE = "file:///android_res/"; static final String FILE_BASE = "file:"; static final String PROXY_BASE = "file:///cookieless_proxy/"; static final String CONTENT_BASE = "content:"; /** * Cleans up (if possible) user-entered web addresses */ public static String guessUrl(String inUrl) { String retVal = inUrl; WebAddress webAddress; if (TRACE) Log.v(LOGTAG, "guessURL before queueRequest: " + inUrl); if (inUrl.length() == 0) return inUrl; if (inUrl.startsWith("about:")) return inUrl; // Do not try to interpret data scheme URLs if (inUrl.startsWith("data:")) return inUrl; // Do not try to interpret file scheme URLs if (inUrl.startsWith("file:")) return inUrl; // Do not try to interpret javascript scheme URLs if (inUrl.startsWith("javascript:")) return inUrl; // bug 762454: strip period off end of url if (inUrl.endsWith(".") == true) { inUrl = inUrl.substring(0, inUrl.length() - 1); } try { webAddress = new WebAddress(inUrl); } catch (ParseException ex) { if (TRACE) { Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); } return retVal; } // Check host if (webAddress.getHost().indexOf('.') == -1) { // no dot: user probably entered a bare domain. try .com webAddress.setHost("www." + webAddress.getHost() + ".com"); } return webAddress.toString(); } public static String composeSearchUrl(String inQuery, String template, String queryPlaceHolder) { int placeHolderIndex = template.indexOf(queryPlaceHolder); if (placeHolderIndex < 0) { return null; } String query; StringBuilder buffer = new StringBuilder(); buffer.append(template.substring(0, placeHolderIndex)); try { query = java.net.URLEncoder.encode(inQuery, "utf-8"); buffer.append(query); } catch (UnsupportedEncodingException ex) { return null; } buffer.append(template.substring( placeHolderIndex + queryPlaceHolder.length())); return buffer.toString(); } public static byte[] decode(byte[] url) throws IllegalArgumentException { if (url.length == 0) { return new byte[0]; } // Create a new byte array with the same length to ensure capacity byte[] tempData = new byte[url.length]; int tempCount = 0; for (int i = 0; i < url.length; i++) { byte b = url[i]; if (b == '%') { if (url.length - i > 2) { b = (byte) (parseHex(url[i + 1]) * 16 + parseHex(url[i + 2])); i += 2; } else { throw new IllegalArgumentException("Invalid format"); } } tempData[tempCount++] = b; } byte[] retData = new byte[tempCount]; System.arraycopy(tempData, 0, retData, 0, tempCount); return retData; } /** * @return {@code true} if the url is correctly URL encoded */ static boolean verifyURLEncoding(String url) { int count = url.length(); if (count == 0) { return false; } int index = url.indexOf('%'); while (index >= 0 && index < count) { if (index < count - 2) { try { parseHex((byte) url.charAt(++index)); parseHex((byte) url.charAt(++index)); } catch (IllegalArgumentException e) { return false; } } else { return false; } index = url.indexOf('%', index + 1); } return true; } private static int parseHex(byte b) { if (b >= '0' && b <= '9') return (b - '0'); if (b >= 'A' && b <= 'F') return (b - 'A' + 10); if (b >= 'a' && b <= 'f') return (b - 'a' + 10); throw new IllegalArgumentException("Invalid hex char '" + b + "'"); } /** * @return {@code true} if the url is an asset file. */ public static boolean isAssetUrl(String url) { return (null != url) && url.startsWith(ASSET_BASE); } /** * @return {@code true} if the url is a resource file. * @hide */ public static boolean isResourceUrl(String url) { return (null != url) && url.startsWith(RESOURCE_BASE); } /** * @return {@code true} if the url is a proxy url to allow cookieless network * requests from a file url. * @deprecated Cookieless proxy is no longer supported. */ @Deprecated public static boolean isCookielessProxyUrl(String url) { return (null != url) && url.startsWith(PROXY_BASE); } /** * @return {@code true} if the url is a local file. */ public static boolean isFileUrl(String url) { return (null != url) && (url.startsWith(FILE_BASE) && !url.startsWith(ASSET_BASE) && !url.startsWith(PROXY_BASE)); } /** * @return {@code true} if the url is an about: url. */ public static boolean isAboutUrl(String url) { return (null != url) && url.startsWith("about:"); } /** * @return {@code true} if the url is a data: url. */ public static boolean isDataUrl(String url) { return (null != url) && url.startsWith("data:"); } /** * @return {@code true} if the url is a javascript: url. */ public static boolean isJavaScriptUrl(String url) { return (null != url) && url.startsWith("javascript:"); } /** * @return {@code true} if the url is an http: url. */ public static boolean isHttpUrl(String url) { return (null != url) && (url.length() > 6) && url.substring(0, 7).equalsIgnoreCase("http://"); } /** * @return {@code true} if the url is an https: url. */ public static boolean isHttpsUrl(String url) { return (null != url) && (url.length() > 7) && url.substring(0, 8).equalsIgnoreCase("https://"); } /** * @return {@code true} if the url is a network url. */ public static boolean isNetworkUrl(String url) { if (url == null || url.length() == 0) { return false; } return isHttpUrl(url) || isHttpsUrl(url); } /** * @return {@code true} if the url is a content: url. */ public static boolean isContentUrl(String url) { return (null != url) && url.startsWith(CONTENT_BASE); } /** * @return {@code true} if the url is valid. */ public static boolean isValidUrl(String url) { if (url == null || url.length() == 0) { return false; } return (isAssetUrl(url) || isResourceUrl(url) || isFileUrl(url) || isAboutUrl(url) || isHttpUrl(url) || isHttpsUrl(url) || isJavaScriptUrl(url) || isContentUrl(url)); } /** * Strips the url of the anchor. */ public static String stripAnchor(String url) { int anchorIndex = url.indexOf('#'); if (anchorIndex != -1) { return url.substring(0, anchorIndex); } return url; } /** * Guesses canonical filename that a download would have, using * the URL and contentDisposition. File extension, if not defined, * is added based on the mimetype * @param url Url to the content * @param contentDisposition Content-Disposition HTTP header or {@code null} * @param mimeType Mime-type of the content or {@code null} * * @return suggested filename */ public static final String guessFileName( String url, @Nullable String contentDisposition, @Nullable String mimeType) { String filename = null; String extension = null; // If we couldn't do anything with the hint, move toward the content disposition if (filename == null && contentDisposition != null) { filename = parseContentDisposition(contentDisposition); if (filename != null) { int index = filename.lastIndexOf('/') + 1; if (index > 0) { filename = filename.substring(index); } } } // If all the other http-related approaches failed, use the plain uri if (filename == null) { String decodedUrl = Uri.decode(url); if (decodedUrl != null) { int queryIndex = decodedUrl.indexOf('?'); // If there is a query string strip it, same as desktop browsers if (queryIndex > 0) { decodedUrl = decodedUrl.substring(0, queryIndex); } if (!decodedUrl.endsWith("/")) { int index = decodedUrl.lastIndexOf('/') + 1; if (index > 0) { filename = decodedUrl.substring(index); } } } } // Finally, if couldn't get filename from URI, get a generic filename if (filename == null) { filename = "downloadfile"; } // Split filename between base and extension // Add an extension if filename does not have one int dotIndex = filename.indexOf('.'); if (dotIndex < 0) { if (mimeType != null) { extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); if (extension != null) { extension = "." + extension; } } if (extension == null) { if (mimeType != null && mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) { if (mimeType.equalsIgnoreCase("text/html")) { extension = ".html"; } else { extension = ".txt"; } } else { extension = ".bin"; } } } else { if (mimeType != null) { // Compare the last segment of the extension against the mime type. // If there's a mismatch, discard the entire extension. int lastDotIndex = filename.lastIndexOf('.'); String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( filename.substring(lastDotIndex + 1)); if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) { extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); if (extension != null) { extension = "." + extension; } } } if (extension == null) { extension = filename.substring(dotIndex); } filename = filename.substring(0, dotIndex); } return filename + extension; } /** Regex used to parse content-disposition headers */ private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", Pattern.CASE_INSENSITIVE); /** * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be * downloaded to the file system. We only support the attachment type. * Note that RFC 2616 specifies the filename value must be double-quoted. * Unfortunately some servers do not quote the value so to maintain * consistent behaviour with other browsers, we allow unquoted values too. */ static String parseContentDisposition(String contentDisposition) { try { Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); if (m.find()) { return m.group(2); } } catch (IllegalStateException ex) { // This function is defined as returning null when it can't parse the header } return null; } }