1/* 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 19import java.io.UnsupportedEncodingException; 20import java.util.regex.Matcher; 21import java.util.regex.Pattern; 22 23import android.net.Uri; 24import android.net.ParseException; 25import android.net.WebAddress; 26import android.util.Log; 27 28public final class URLUtil { 29 30 private static final String LOGTAG = "webkit"; 31 32 // to refer to bar.png under your package's asset/foo/ directory, use 33 // "file:///android_asset/foo/bar.png". 34 static final String ASSET_BASE = "file:///android_asset/"; 35 // to refer to bar.png under your package's res/drawable/ directory, use 36 // "file:///android_res/drawable/bar.png". Use "drawable" to refer to 37 // "drawable-hdpi" directory as well. 38 static final String RESOURCE_BASE = "file:///android_res/"; 39 static final String FILE_BASE = "file://"; 40 static final String PROXY_BASE = "file:///cookieless_proxy/"; 41 42 /** 43 * Cleans up (if possible) user-entered web addresses 44 */ 45 public static String guessUrl(String inUrl) { 46 47 String retVal = inUrl; 48 WebAddress webAddress; 49 50 Log.v(LOGTAG, "guessURL before queueRequest: " + inUrl); 51 52 if (inUrl.length() == 0) return inUrl; 53 if (inUrl.startsWith("about:")) return inUrl; 54 // Do not try to interpret data scheme URLs 55 if (inUrl.startsWith("data:")) return inUrl; 56 // Do not try to interpret file scheme URLs 57 if (inUrl.startsWith("file:")) return inUrl; 58 // Do not try to interpret javascript scheme URLs 59 if (inUrl.startsWith("javascript:")) return inUrl; 60 61 // bug 762454: strip period off end of url 62 if (inUrl.endsWith(".") == true) { 63 inUrl = inUrl.substring(0, inUrl.length() - 1); 64 } 65 66 try { 67 webAddress = new WebAddress(inUrl); 68 } catch (ParseException ex) { 69 70 if (DebugFlags.URL_UTIL) { 71 Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); 72 } 73 return retVal; 74 } 75 76 // Check host 77 if (webAddress.mHost.indexOf('.') == -1) { 78 // no dot: user probably entered a bare domain. try .com 79 webAddress.mHost = "www." + webAddress.mHost + ".com"; 80 } 81 return webAddress.toString(); 82 } 83 84 public static String composeSearchUrl(String inQuery, String template, 85 String queryPlaceHolder) { 86 int placeHolderIndex = template.indexOf(queryPlaceHolder); 87 if (placeHolderIndex < 0) { 88 return null; 89 } 90 91 String query; 92 StringBuilder buffer = new StringBuilder(); 93 buffer.append(template.substring(0, placeHolderIndex)); 94 95 try { 96 query = java.net.URLEncoder.encode(inQuery, "utf-8"); 97 buffer.append(query); 98 } catch (UnsupportedEncodingException ex) { 99 return null; 100 } 101 102 buffer.append(template.substring( 103 placeHolderIndex + queryPlaceHolder.length())); 104 105 return buffer.toString(); 106 } 107 108 public static byte[] decode(byte[] url) throws IllegalArgumentException { 109 if (url.length == 0) { 110 return new byte[0]; 111 } 112 113 // Create a new byte array with the same length to ensure capacity 114 byte[] tempData = new byte[url.length]; 115 116 int tempCount = 0; 117 for (int i = 0; i < url.length; i++) { 118 byte b = url[i]; 119 if (b == '%') { 120 if (url.length - i > 2) { 121 b = (byte) (parseHex(url[i + 1]) * 16 122 + parseHex(url[i + 2])); 123 i += 2; 124 } else { 125 throw new IllegalArgumentException("Invalid format"); 126 } 127 } 128 tempData[tempCount++] = b; 129 } 130 byte[] retData = new byte[tempCount]; 131 System.arraycopy(tempData, 0, retData, 0, tempCount); 132 return retData; 133 } 134 135 /** 136 * @return True iff the url is correctly URL encoded 137 */ 138 static boolean verifyURLEncoding(String url) { 139 int count = url.length(); 140 if (count == 0) { 141 return false; 142 } 143 144 int index = url.indexOf('%'); 145 while (index >= 0 && index < count) { 146 if (index < count - 2) { 147 try { 148 parseHex((byte) url.charAt(++index)); 149 parseHex((byte) url.charAt(++index)); 150 } catch (IllegalArgumentException e) { 151 return false; 152 } 153 } else { 154 return false; 155 } 156 index = url.indexOf('%', index + 1); 157 } 158 return true; 159 } 160 161 private static int parseHex(byte b) { 162 if (b >= '0' && b <= '9') return (b - '0'); 163 if (b >= 'A' && b <= 'F') return (b - 'A' + 10); 164 if (b >= 'a' && b <= 'f') return (b - 'a' + 10); 165 166 throw new IllegalArgumentException("Invalid hex char '" + b + "'"); 167 } 168 169 /** 170 * @return True iff the url is an asset file. 171 */ 172 public static boolean isAssetUrl(String url) { 173 return (null != url) && url.startsWith(ASSET_BASE); 174 } 175 176 /** 177 * @return True iff the url is a resource file. 178 * @hide 179 */ 180 public static boolean isResourceUrl(String url) { 181 return (null != url) && url.startsWith(RESOURCE_BASE); 182 } 183 184 /** 185 * @return True iff the url is an proxy url to allow cookieless network 186 * requests from a file url. 187 * @deprecated Cookieless proxy is no longer supported. 188 */ 189 @Deprecated 190 public static boolean isCookielessProxyUrl(String url) { 191 return (null != url) && url.startsWith(PROXY_BASE); 192 } 193 194 /** 195 * @return True iff the url is a local file. 196 */ 197 public static boolean isFileUrl(String url) { 198 return (null != url) && (url.startsWith(FILE_BASE) && 199 !url.startsWith(ASSET_BASE) && 200 !url.startsWith(PROXY_BASE)); 201 } 202 203 /** 204 * @return True iff the url is an about: url. 205 */ 206 public static boolean isAboutUrl(String url) { 207 return (null != url) && url.startsWith("about:"); 208 } 209 210 /** 211 * @return True iff the url is a data: url. 212 */ 213 public static boolean isDataUrl(String url) { 214 return (null != url) && url.startsWith("data:"); 215 } 216 217 /** 218 * @return True iff the url is a javascript: url. 219 */ 220 public static boolean isJavaScriptUrl(String url) { 221 return (null != url) && url.startsWith("javascript:"); 222 } 223 224 /** 225 * @return True iff the url is an http: url. 226 */ 227 public static boolean isHttpUrl(String url) { 228 return (null != url) && 229 (url.length() > 6) && 230 url.substring(0, 7).equalsIgnoreCase("http://"); 231 } 232 233 /** 234 * @return True iff the url is an https: url. 235 */ 236 public static boolean isHttpsUrl(String url) { 237 return (null != url) && 238 (url.length() > 7) && 239 url.substring(0, 8).equalsIgnoreCase("https://"); 240 } 241 242 /** 243 * @return True iff the url is a network url. 244 */ 245 public static boolean isNetworkUrl(String url) { 246 if (url == null || url.length() == 0) { 247 return false; 248 } 249 return isHttpUrl(url) || isHttpsUrl(url); 250 } 251 252 /** 253 * @return True iff the url is a content: url. 254 */ 255 public static boolean isContentUrl(String url) { 256 return (null != url) && url.startsWith("content:"); 257 } 258 259 /** 260 * @return True iff the url is valid. 261 */ 262 public static boolean isValidUrl(String url) { 263 if (url == null || url.length() == 0) { 264 return false; 265 } 266 267 return (isAssetUrl(url) || 268 isResourceUrl(url) || 269 isFileUrl(url) || 270 isAboutUrl(url) || 271 isHttpUrl(url) || 272 isHttpsUrl(url) || 273 isJavaScriptUrl(url) || 274 isContentUrl(url)); 275 } 276 277 /** 278 * Strips the url of the anchor. 279 */ 280 public static String stripAnchor(String url) { 281 int anchorIndex = url.indexOf('#'); 282 if (anchorIndex != -1) { 283 return url.substring(0, anchorIndex); 284 } 285 return url; 286 } 287 288 /** 289 * Guesses canonical filename that a download would have, using 290 * the URL and contentDisposition. File extension, if not defined, 291 * is added based on the mimetype 292 * @param url Url to the content 293 * @param contentDisposition Content-Disposition HTTP header or null 294 * @param mimeType Mime-type of the content or null 295 * 296 * @return suggested filename 297 */ 298 public static final String guessFileName( 299 String url, 300 String contentDisposition, 301 String mimeType) { 302 String filename = null; 303 String extension = null; 304 305 // If we couldn't do anything with the hint, move toward the content disposition 306 if (filename == null && contentDisposition != null) { 307 filename = parseContentDisposition(contentDisposition); 308 if (filename != null) { 309 int index = filename.lastIndexOf('/') + 1; 310 if (index > 0) { 311 filename = filename.substring(index); 312 } 313 } 314 } 315 316 // If all the other http-related approaches failed, use the plain uri 317 if (filename == null) { 318 String decodedUrl = Uri.decode(url); 319 if (decodedUrl != null) { 320 int queryIndex = decodedUrl.indexOf('?'); 321 // If there is a query string strip it, same as desktop browsers 322 if (queryIndex > 0) { 323 decodedUrl = decodedUrl.substring(0, queryIndex); 324 } 325 if (!decodedUrl.endsWith("/")) { 326 int index = decodedUrl.lastIndexOf('/') + 1; 327 if (index > 0) { 328 filename = decodedUrl.substring(index); 329 } 330 } 331 } 332 } 333 334 // Finally, if couldn't get filename from URI, get a generic filename 335 if (filename == null) { 336 filename = "downloadfile"; 337 } 338 339 // Split filename between base and extension 340 // Add an extension if filename does not have one 341 int dotIndex = filename.indexOf('.'); 342 if (dotIndex < 0) { 343 if (mimeType != null) { 344 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 345 if (extension != null) { 346 extension = "." + extension; 347 } 348 } 349 if (extension == null) { 350 if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 351 if (mimeType.equalsIgnoreCase("text/html")) { 352 extension = ".html"; 353 } else { 354 extension = ".txt"; 355 } 356 } else { 357 extension = ".bin"; 358 } 359 } 360 } else { 361 if (mimeType != null) { 362 // Compare the last segment of the extension against the mime type. 363 // If there's a mismatch, discard the entire extension. 364 int lastDotIndex = filename.lastIndexOf('.'); 365 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 366 filename.substring(lastDotIndex + 1)); 367 if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) { 368 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 369 if (extension != null) { 370 extension = "." + extension; 371 } 372 } 373 } 374 if (extension == null) { 375 extension = filename.substring(dotIndex); 376 } 377 filename = filename.substring(0, dotIndex); 378 } 379 380 return filename + extension; 381 } 382 383 /** Regex used to parse content-disposition headers */ 384 private static final Pattern CONTENT_DISPOSITION_PATTERN = 385 Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", 386 Pattern.CASE_INSENSITIVE); 387 388 /* 389 * Parse the Content-Disposition HTTP Header. The format of the header 390 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 391 * This header provides a filename for content that is going to be 392 * downloaded to the file system. We only support the attachment type. 393 * Note that RFC 2616 specifies the filename value must be double-quoted. 394 * Unfortunately some servers do not quote the value so to maintain 395 * consistent behaviour with other browsers, we allow unquoted values too. 396 */ 397 static String parseContentDisposition(String contentDisposition) { 398 try { 399 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 400 if (m.find()) { 401 return m.group(2); 402 } 403 } catch (IllegalStateException ex) { 404 // This function is defined as returning null when it can't parse the header 405 } 406 return null; 407 } 408} 409