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 17 package android.webkit; 18 19 import android.net.http.EventHandler; 20 import android.net.http.RequestHandle; 21 import android.util.Log; 22 import android.webkit.CacheManager.CacheResult; 23 24 import java.util.HashMap; 25 import java.util.Map; 26 27 class FrameLoader { 28 29 private final LoadListener mListener; 30 private final String mMethod; 31 private final WebSettings mSettings; 32 private Map<String, String> mHeaders; 33 private byte[] mPostData; 34 private Network mNetwork; 35 private int mCacheMode; 36 private String mReferrer; 37 private String mContentType; 38 39 private static final int URI_PROTOCOL = 0x100; 40 41 private static final String CONTENT_TYPE = "content-type"; 42 43 // Contents of an about:blank page 44 private static final String mAboutBlank = 45 "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EB\">" + 46 "<html><head><title>about:blank</title></head><body></body></html>"; 47 48 static final String HEADER_STR = "text/xml, text/html, " + 49 "application/xhtml+xml, image/png, text/plain, */*;q=0.8"; 50 51 private static final String LOGTAG = "webkit"; 52 53 FrameLoader(LoadListener listener, WebSettings settings, 54 String method) { 55 mListener = listener; 56 mHeaders = null; 57 mMethod = method; 58 mCacheMode = WebSettings.LOAD_NORMAL; 59 mSettings = settings; 60 } 61 62 public void setReferrer(String ref) { 63 // only set referrer for http or https 64 if (URLUtil.isNetworkUrl(ref)) mReferrer = ref; 65 } 66 67 public void setPostData(byte[] postData) { 68 mPostData = postData; 69 } 70 71 public void setContentTypeForPost(String postContentType) { 72 mContentType = postContentType; 73 } 74 75 public void setCacheMode(int cacheMode) { 76 mCacheMode = cacheMode; 77 } 78 79 public void setHeaders(HashMap headers) { 80 mHeaders = headers; 81 } 82 83 public LoadListener getLoadListener() { 84 return mListener; 85 } 86 87 /** 88 * Issues the load request. 89 * 90 * Return value does not indicate if the load was successful or not. It 91 * simply indicates that the load request is reasonable. 92 * 93 * @return true if the load is reasonable. 94 */ 95 public boolean executeLoad() { 96 String url = mListener.url(); 97 98 if (URLUtil.isNetworkUrl(url)){ 99 if (mSettings.getBlockNetworkLoads()) { 100 mListener.error(EventHandler.ERROR_BAD_URL, 101 mListener.getContext().getString( 102 com.android.internal.R.string.httpErrorBadUrl)); 103 return false; 104 } 105 // Make sure the host part of the url is correctly 106 // encoded before sending the request 107 if (!URLUtil.verifyURLEncoding(mListener.host())) { 108 mListener.error(EventHandler.ERROR_BAD_URL, 109 mListener.getContext().getString( 110 com.android.internal.R.string.httpErrorBadUrl)); 111 return false; 112 } 113 mNetwork = Network.getInstance(mListener.getContext()); 114 if (mListener.isSynchronous()) { 115 return handleHTTPLoad(); 116 } 117 WebViewWorker.getHandler().obtainMessage( 118 WebViewWorker.MSG_ADD_HTTPLOADER, this).sendToTarget(); 119 return true; 120 } else if (handleLocalFile(url, mListener, mSettings)) { 121 return true; 122 } 123 if (DebugFlags.FRAME_LOADER) { 124 Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" 125 + mListener.url()); 126 } 127 mListener.error(EventHandler.ERROR_UNSUPPORTED_SCHEME, 128 mListener.getContext().getText( 129 com.android.internal.R.string.httpErrorUnsupportedScheme).toString()); 130 return false; 131 132 } 133 134 /* package */ 135 static boolean handleLocalFile(String url, LoadListener loadListener, 136 WebSettings settings) { 137 // Attempt to decode the percent-encoded url before passing to the 138 // local loaders. 139 try { 140 url = new String(URLUtil.decode(url.getBytes())); 141 } catch (IllegalArgumentException e) { 142 loadListener.error(EventHandler.ERROR_BAD_URL, 143 loadListener.getContext().getString( 144 com.android.internal.R.string.httpErrorBadUrl)); 145 // Return true here so we do not trigger an unsupported scheme 146 // error. 147 return true; 148 } 149 if (URLUtil.isAssetUrl(url)) { 150 if (loadListener.isSynchronous()) { 151 new FileLoader(url, loadListener, FileLoader.TYPE_ASSET, 152 true).load(); 153 } else { 154 // load asset in a separate thread as it involves IO 155 WebViewWorker.getHandler().obtainMessage( 156 WebViewWorker.MSG_ADD_STREAMLOADER, 157 new FileLoader(url, loadListener, FileLoader.TYPE_ASSET, 158 true)).sendToTarget(); 159 } 160 return true; 161 } else if (URLUtil.isResourceUrl(url)) { 162 if (loadListener.isSynchronous()) { 163 new FileLoader(url, loadListener, FileLoader.TYPE_RES, 164 true).load(); 165 } else { 166 // load resource in a separate thread as it involves IO 167 WebViewWorker.getHandler().obtainMessage( 168 WebViewWorker.MSG_ADD_STREAMLOADER, 169 new FileLoader(url, loadListener, FileLoader.TYPE_RES, 170 true)).sendToTarget(); 171 } 172 return true; 173 } else if (URLUtil.isFileUrl(url)) { 174 if (loadListener.isSynchronous()) { 175 new FileLoader(url, loadListener, FileLoader.TYPE_FILE, 176 settings.getAllowFileAccess()).load(); 177 } else { 178 // load file in a separate thread as it involves IO 179 WebViewWorker.getHandler().obtainMessage( 180 WebViewWorker.MSG_ADD_STREAMLOADER, 181 new FileLoader(url, loadListener, FileLoader.TYPE_FILE, 182 settings.getAllowFileAccess())).sendToTarget(); 183 } 184 return true; 185 } else if (URLUtil.isContentUrl(url)) { 186 // Send the raw url to the ContentLoader because it will do a 187 // permission check and the url has to match. 188 if (loadListener.isSynchronous()) { 189 new ContentLoader(loadListener.url(), loadListener).load(); 190 } else { 191 // load content in a separate thread as it involves IO 192 WebViewWorker.getHandler().obtainMessage( 193 WebViewWorker.MSG_ADD_STREAMLOADER, 194 new ContentLoader(loadListener.url(), loadListener)) 195 .sendToTarget(); 196 } 197 return true; 198 } else if (URLUtil.isDataUrl(url)) { 199 // load data in the current thread to reduce the latency 200 new DataLoader(url, loadListener).load(); 201 return true; 202 } else if (URLUtil.isAboutUrl(url)) { 203 loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length()); 204 loadListener.endData(); 205 return true; 206 } 207 return false; 208 } 209 210 boolean handleHTTPLoad() { 211 if (mHeaders == null) { 212 mHeaders = new HashMap<String, String>(); 213 } 214 populateStaticHeaders(); 215 populateHeaders(); 216 217 // response was handled by Cache, don't issue HTTP request 218 if (handleCache()) { 219 // push the request data down to the LoadListener 220 // as response from the cache could be a redirect 221 // and we may need to initiate a network request if the cache 222 // can't satisfy redirect URL 223 mListener.setRequestData(mMethod, mHeaders, mPostData); 224 return true; 225 } 226 227 if (DebugFlags.FRAME_LOADER) { 228 Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " 229 + mListener.url()); 230 } 231 232 boolean ret = false; 233 int error = EventHandler.ERROR_UNSUPPORTED_SCHEME; 234 235 try { 236 ret = mNetwork.requestURL(mMethod, mHeaders, 237 mPostData, mListener); 238 } catch (android.net.ParseException ex) { 239 error = EventHandler.ERROR_BAD_URL; 240 } catch (java.lang.RuntimeException ex) { 241 /* probably an empty header set by javascript. We want 242 the same result as bad URL */ 243 error = EventHandler.ERROR_BAD_URL; 244 } 245 if (!ret) { 246 mListener.error(error, mListener.getContext().getText( 247 EventHandler.errorStringResources[Math.abs(error)]).toString()); 248 return false; 249 } 250 return true; 251 } 252 253 /* 254 * This function is used by handleCache to 255 * setup a load from the byte stream in a CacheResult. 256 */ 257 private void startCacheLoad(CacheResult result) { 258 if (DebugFlags.FRAME_LOADER) { 259 Log.v(LOGTAG, "FrameLoader: loading from cache: " 260 + mListener.url()); 261 } 262 // Tell the Listener respond with the cache file 263 CacheLoader cacheLoader = 264 new CacheLoader(mListener, result); 265 mListener.setCacheLoader(cacheLoader); 266 if (mListener.isSynchronous()) { 267 cacheLoader.load(); 268 } else { 269 // Load the cached file in a separate thread 270 WebViewWorker.getHandler().obtainMessage( 271 WebViewWorker.MSG_ADD_STREAMLOADER, cacheLoader).sendToTarget(); 272 } 273 } 274 275 /* 276 * This function is used by the handleHTTPLoad to setup the cache headers 277 * correctly. 278 * Returns true if the response was handled from the cache 279 */ 280 private boolean handleCache() { 281 switch (mCacheMode) { 282 // This mode is normally used for a reload, it instructs the http 283 // loader to not use the cached content. 284 case WebSettings.LOAD_NO_CACHE: 285 break; 286 287 288 // This mode is used when the content should only be loaded from 289 // the cache. If it is not there, then fail the load. This is used 290 // to load POST content in a history navigation. 291 case WebSettings.LOAD_CACHE_ONLY: { 292 CacheResult result = CacheManager.getCacheFile(mListener.url(), 293 mListener.postIdentifier(), null); 294 if (result != null) { 295 startCacheLoad(result); 296 } else { 297 // This happens if WebCore was first told that the POST 298 // response was in the cache, then when we try to use it 299 // it has gone. 300 // Generate a file not found error 301 int err = EventHandler.FILE_NOT_FOUND_ERROR; 302 mListener.error(err, mListener.getContext().getText( 303 EventHandler.errorStringResources[Math.abs(err)]) 304 .toString()); 305 } 306 return true; 307 } 308 309 // This mode is for when the user is doing a history navigation 310 // in the browser and should returned cached content regardless 311 // of it's state. If it is not in the cache, then go to the 312 // network. 313 case WebSettings.LOAD_CACHE_ELSE_NETWORK: { 314 if (DebugFlags.FRAME_LOADER) { 315 Log.v(LOGTAG, "FrameLoader: checking cache: " 316 + mListener.url()); 317 } 318 // Get the cache file name for the current URL, passing null for 319 // the validation headers causes no validation to occur 320 CacheResult result = CacheManager.getCacheFile(mListener.url(), 321 mListener.postIdentifier(), null); 322 if (result != null) { 323 startCacheLoad(result); 324 return true; 325 } 326 break; 327 } 328 329 // This is the default case, which is to check to see if the 330 // content in the cache can be used. If it can be used, then 331 // use it. If it needs revalidation then the relevant headers 332 // are added to the request. 333 default: 334 case WebSettings.LOAD_NORMAL: 335 return mListener.checkCache(mHeaders); 336 }// end of switch 337 338 return false; 339 } 340 341 /** 342 * Add the static headers that don't change with each request. 343 */ 344 private void populateStaticHeaders() { 345 // Accept header should already be there as they are built by WebCore, 346 // but in the case they are missing, add some. 347 String accept = mHeaders.get("Accept"); 348 if (accept == null || accept.length() == 0) { 349 mHeaders.put("Accept", HEADER_STR); 350 } 351 mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7"); 352 353 String acceptLanguage = mSettings.getAcceptLanguage(); 354 if (acceptLanguage.length() > 0) { 355 mHeaders.put("Accept-Language", acceptLanguage); 356 } 357 358 mHeaders.put("User-Agent", mSettings.getUserAgentString()); 359 } 360 361 /** 362 * Add the content related headers. These headers contain user private data 363 * and is not used when we are proxying an untrusted request. 364 */ 365 private void populateHeaders() { 366 367 if (mReferrer != null) mHeaders.put("Referer", mReferrer); 368 if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType); 369 370 // if we have an active proxy and have proxy credentials, do pre-emptive 371 // authentication to avoid an extra round-trip: 372 if (mNetwork.isValidProxySet()) { 373 String username; 374 String password; 375 /* The proxy credentials can be set in the Network thread */ 376 synchronized (mNetwork) { 377 username = mNetwork.getProxyUsername(); 378 password = mNetwork.getProxyPassword(); 379 } 380 if (username != null && password != null) { 381 // we collect credentials ONLY if the proxy scheme is BASIC!!! 382 String proxyHeader = RequestHandle.authorizationHeader(true); 383 mHeaders.put(proxyHeader, 384 "Basic " + RequestHandle.computeBasicAuthResponse( 385 username, password)); 386 } 387 } 388 389 // Set cookie header 390 String cookie = CookieManager.getInstance().getCookie( 391 mListener.getWebAddress()); 392 if (cookie != null && cookie.length() > 0) { 393 mHeaders.put("Cookie", cookie); 394 } 395 } 396 } 397