Home | History | Annotate | Download | only in webkit
      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 (settings.getAllowContentAccess() &&
    186                    URLUtil.isContentUrl(url)) {
    187             // Send the raw url to the ContentLoader because it will do a
    188             // permission check and the url has to match.
    189             if (loadListener.isSynchronous()) {
    190                 new ContentLoader(loadListener.url(), loadListener).load();
    191             } else {
    192                 // load content in a separate thread as it involves IO
    193                 WebViewWorker.getHandler().obtainMessage(
    194                         WebViewWorker.MSG_ADD_STREAMLOADER,
    195                         new ContentLoader(loadListener.url(), loadListener))
    196                         .sendToTarget();
    197             }
    198             return true;
    199         } else if (URLUtil.isDataUrl(url)) {
    200             // load data in the current thread to reduce the latency
    201             new DataLoader(url, loadListener).load();
    202             return true;
    203         } else if (URLUtil.isAboutUrl(url)) {
    204             loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length());
    205             loadListener.endData();
    206             return true;
    207         }
    208         return false;
    209     }
    210 
    211     boolean handleHTTPLoad() {
    212         if (mHeaders == null) {
    213             mHeaders = new HashMap<String, String>();
    214         }
    215         populateStaticHeaders();
    216         populateHeaders();
    217 
    218         // response was handled by Cache, don't issue HTTP request
    219         if (handleCache()) {
    220             // push the request data down to the LoadListener
    221             // as response from the cache could be a redirect
    222             // and we may need to initiate a network request if the cache
    223             // can't satisfy redirect URL
    224             mListener.setRequestData(mMethod, mHeaders, mPostData);
    225             return true;
    226         }
    227 
    228         if (DebugFlags.FRAME_LOADER) {
    229             Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: "
    230                     + mListener.url());
    231         }
    232 
    233         boolean ret = false;
    234         int error = EventHandler.ERROR_UNSUPPORTED_SCHEME;
    235 
    236         try {
    237             ret = mNetwork.requestURL(mMethod, mHeaders,
    238                     mPostData, mListener);
    239         } catch (android.net.ParseException ex) {
    240             error = EventHandler.ERROR_BAD_URL;
    241         } catch (java.lang.RuntimeException ex) {
    242             /* probably an empty header set by javascript.  We want
    243                the same result as bad URL  */
    244             error = EventHandler.ERROR_BAD_URL;
    245         }
    246         if (!ret) {
    247             mListener.error(error, mListener.getContext().getText(
    248                     EventHandler.errorStringResources[Math.abs(error)]).toString());
    249             return false;
    250         }
    251         return true;
    252     }
    253 
    254     /*
    255      * This function is used by handleCache to
    256      * setup a load from the byte stream in a CacheResult.
    257      */
    258     private void startCacheLoad(CacheResult result) {
    259         if (DebugFlags.FRAME_LOADER) {
    260             Log.v(LOGTAG, "FrameLoader: loading from cache: "
    261                   + mListener.url());
    262         }
    263         // Tell the Listener respond with the cache file
    264         CacheLoader cacheLoader =
    265                 new CacheLoader(mListener, result);
    266         mListener.setCacheLoader(cacheLoader);
    267         if (mListener.isSynchronous()) {
    268             cacheLoader.load();
    269         } else {
    270             // Load the cached file in a separate thread
    271             WebViewWorker.getHandler().obtainMessage(
    272                     WebViewWorker.MSG_ADD_STREAMLOADER, cacheLoader).sendToTarget();
    273         }
    274     }
    275 
    276     /*
    277      * This function is used by the handleHTTPLoad to setup the cache headers
    278      * correctly.
    279      * Returns true if the response was handled from the cache
    280      */
    281     private boolean handleCache() {
    282         switch (mCacheMode) {
    283             // This mode is normally used for a reload, it instructs the http
    284             // loader to not use the cached content.
    285             case WebSettings.LOAD_NO_CACHE:
    286                 break;
    287 
    288 
    289             // This mode is used when the content should only be loaded from
    290             // the cache. If it is not there, then fail the load. This is used
    291             // to load POST content in a history navigation.
    292             case WebSettings.LOAD_CACHE_ONLY: {
    293                 CacheResult result = CacheManager.getCacheFile(mListener.url(),
    294                         mListener.postIdentifier(), null);
    295                 if (result != null) {
    296                     startCacheLoad(result);
    297                 } else {
    298                     // This happens if WebCore was first told that the POST
    299                     // response was in the cache, then when we try to use it
    300                     // it has gone.
    301                     // Generate a file not found error
    302                     int err = EventHandler.FILE_NOT_FOUND_ERROR;
    303                     mListener.error(err, mListener.getContext().getText(
    304                             EventHandler.errorStringResources[Math.abs(err)])
    305                             .toString());
    306                 }
    307                 return true;
    308             }
    309 
    310             // This mode is for when the user is doing a history navigation
    311             // in the browser and should returned cached content regardless
    312             // of it's state. If it is not in the cache, then go to the
    313             // network.
    314             case WebSettings.LOAD_CACHE_ELSE_NETWORK: {
    315                 if (DebugFlags.FRAME_LOADER) {
    316                     Log.v(LOGTAG, "FrameLoader: checking cache: "
    317                             + mListener.url());
    318                 }
    319                 // Get the cache file name for the current URL, passing null for
    320                 // the validation headers causes no validation to occur
    321                 CacheResult result = CacheManager.getCacheFile(mListener.url(),
    322                         mListener.postIdentifier(), null);
    323                 if (result != null) {
    324                     startCacheLoad(result);
    325                     return true;
    326                 }
    327                 break;
    328             }
    329 
    330             // This is the default case, which is to check to see if the
    331             // content in the cache can be used. If it can be used, then
    332             // use it. If it needs revalidation then the relevant headers
    333             // are added to the request.
    334             default:
    335             case WebSettings.LOAD_NORMAL:
    336                 return mListener.checkCache(mHeaders);
    337         }// end of switch
    338 
    339         return false;
    340     }
    341 
    342     /**
    343      * Add the static headers that don't change with each request.
    344      */
    345     private void populateStaticHeaders() {
    346         // Accept header should already be there as they are built by WebCore,
    347         // but in the case they are missing, add some.
    348         String accept = mHeaders.get("Accept");
    349         if (accept == null || accept.length() == 0) {
    350             mHeaders.put("Accept", HEADER_STR);
    351         }
    352         mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7");
    353 
    354         String acceptLanguage = mSettings.getAcceptLanguage();
    355         if (acceptLanguage.length() > 0) {
    356             mHeaders.put("Accept-Language", acceptLanguage);
    357         }
    358 
    359         mHeaders.put("User-Agent", mSettings.getUserAgentString());
    360     }
    361 
    362     /**
    363      * Add the content related headers. These headers contain user private data
    364      * and is not used when we are proxying an untrusted request.
    365      */
    366     private void populateHeaders() {
    367 
    368         if (mReferrer != null) mHeaders.put("Referer", mReferrer);
    369         if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType);
    370 
    371         // if we have an active proxy and have proxy credentials, do pre-emptive
    372         // authentication to avoid an extra round-trip:
    373         if (mNetwork.isValidProxySet()) {
    374             String username;
    375             String password;
    376             /* The proxy credentials can be set in the Network thread */
    377             synchronized (mNetwork) {
    378                 username = mNetwork.getProxyUsername();
    379                 password = mNetwork.getProxyPassword();
    380             }
    381             if (username != null && password != null) {
    382                 // we collect credentials ONLY if the proxy scheme is BASIC!!!
    383                 String proxyHeader = RequestHandle.authorizationHeader(true);
    384                 mHeaders.put(proxyHeader,
    385                         "Basic " + RequestHandle.computeBasicAuthResponse(
    386                                 username, password));
    387             }
    388         }
    389 
    390         // Set cookie header
    391         String cookie = CookieManager.getInstance().getCookie(
    392                 mListener.getWebAddress());
    393         if (cookie != null && cookie.length() > 0) {
    394             mHeaders.put("Cookie", cookie);
    395         }
    396     }
    397 }
    398