Bug 462549: Verify that an appcache manifest hasn't changed at the end of an update. r+sr=biesi
authorDave Camp <dcamp@mozilla.com>
Wed, 05 Nov 2008 16:01:07 -0800
changeset 21365 037702607ad852836fdb1b249e2bda8b47dff22f
parent 21364 d824478adb04cfb953febe34c6db2b29b0c416f9
child 21366 bfaeea708b98cd7c9b727c3379c1b7a74c64a222
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs462549
milestone1.9.1b2pre
Bug 462549: Verify that an appcache manifest hasn't changed at the end of an update. r+sr=biesi
dom/tests/mochitest/ajax/offline/Makefile.in
dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest
dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest^headers^
dom/tests/mochitest/ajax/offline/changingManifest.sjs
dom/tests/mochitest/ajax/offline/test_changingManifest.html
dom/tests/mochitest/ajax/offline/test_refetchManifest.html
uriloader/prefetch/nsOfflineCacheUpdate.cpp
uriloader/prefetch/nsOfflineCacheUpdate.h
--- a/dom/tests/mochitest/ajax/offline/Makefile.in
+++ b/dom/tests/mochitest/ajax/offline/Makefile.in
@@ -51,16 +51,17 @@ include $(topsrcdir)/config/rules.mk
 	offlineTests.js \
 	test_badManifestMagic.html \
 	test_badManifestMime.html \
 	test_missingFile.html \
 	test_noManifest.html \
 	test_simpleManifest.html \
 	test_identicalManifest.html \
 	test_changingManifest.html \
+	test_refetchManifest.html \
 	test_offlineIFrame.html \
 	test_bug445544.html \
 	445544_part1.html \
 	445544_part2.html \
 	445544.cacheManifest \
 	445544.cacheManifest^headers^ \
 	test_obsolete.html \
 	obsolete.html \
@@ -83,14 +84,15 @@ include $(topsrcdir)/config/rules.mk
 	missingFile.cacheManifest^headers^ \
 	simpleManifest.cacheManifest \
 	simpleManifest.cacheManifest^headers^ \
 	updating.cacheManifest \
 	updating.cacheManifest^headers^ \
 	simpleManifest.notmanifest \
 	changing1Sec.sjs \
 	changing1Hour.sjs \
-	changingManifest.sjs \
+	changingManifest.cacheManifest \
+	changingManifest.cacheManifest^headers^ \
 	offlineChild.html \
         $(NULL)
 
 libs::	$(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest
@@ -0,0 +1,5 @@
+CACHE MANIFEST
+# v1 - this will be replaced by test scripts.
+changing1Hour.sjs
+changing1Sec.sjs
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+Cache-Control: no-cache
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/offline/changingManifest.sjs
+++ /dev/null
@@ -1,12 +0,0 @@
-function handleRequest(request, response)
-{
-  response.setStatusLine(request.httpVersion, 200, "Ok");
-  response.setHeader("Content-Type", "text/cache-manifest");
-  response.setHeader("Cache-Control", "no-cache");
-
-  response.write("CACHE MANIFEST\n");
-  response.write("#" + Date.now() + "\n");
-  response.write("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Hour.sjs\n");
-  response.write("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Sec.sjs\n");
-}
-
--- a/dom/tests/mochitest/ajax/offline/test_changingManifest.html
+++ b/dom/tests/mochitest/ajax/offline/test_changingManifest.html
@@ -1,9 +1,9 @@
-<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.sjs">
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest">
 <head>
 <title>changing manifest test</title>
 
 <script type="text/javascript" src="/MochiKit/packed.js"></script>
 <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
 <script type="text/javascript" src="/tests/dom/tests/mochitest/ajax/offline/offlineTests.js"></script>
 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 
@@ -12,51 +12,73 @@
 var gGotChecking = false;
 var gGotDownloading = false;
 
 var g1SecUrl =  "http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Sec.sjs";
 var g1HourUrl = "http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Hour.sjs";
 
 var gCacheContents = null;
 
+function finish()
+{
+  // Get rid of the changed manifest.
+  var req = new XMLHttpRequest();
+  req.open("DELETE", "changingManifest.cacheManifest", false);
+  req.send("");
+
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
 function manifestUpdatedAgain()
 {
   OfflineTest.ok(gGotChecking, "Should get a checking event on the second update");
   OfflineTest.ok(gGotDownloading, "Should get a downloading event on the second update");
 
   // Get the initial contents of the first two files.
   fetcher = new OfflineCacheContents([g1SecUrl, g1HourUrl]);
   fetcher.fetch(function(contents) {
     // Make sure the contents of the 1-second-expiration file have changed,
     // but that the 1-hour-expiration has not.
     OfflineTest.isnot(gCacheContents[g1SecUrl], contents[g1SecUrl], "1-second expiration should have changed");
     OfflineTest.is(gCacheContents[g1HourUrl], contents[g1HourUrl], "1-hour expiration should not have changed");
 
-    OfflineTest.teardown();
-    OfflineTest.finish();
+    finish();
   });
 }
 
 function failAndFinish(e) {
   OfflineTest.ok(false, "Unexpected event: " + e.type);
-  OfflineTest.teardown();
-  OfflineTest.finish();
+  finish();
 }
 
 function manifestUpdated()
 {
   OfflineTest.ok(gGotChecking, "Should get a checking event");
   OfflineTest.ok(gGotDownloading, "Should get a downloading event");
 
+  // Replace this manifest with a new one.
+
+  // XXX: After this put, we will no longer have Cache-Control:
+  // no-cache on the manifest, so future updates will just use the
+  // cached manifest.
+  var req = new XMLHttpRequest();
+  req.open("PUT", "changingManifest.cacheManifest", false);
+  req.setRequestHeader("Content-Type", "text/cache-manifest");
+  req.send("CACHE MANIFEST\n" +
+           "# v2\n" +
+           "changing1Hour.sjs\n" +
+           "changing1Sec.sjs\n");
+
   // Get the initial contents of the first two files.
   fetcher = new OfflineCacheContents([g1SecUrl, g1HourUrl]);
   fetcher.fetch(function(contents) {
     gCacheContents = contents;
 
-     // Now make sure applicationCache.update() does what we expect.
+    // Now make sure applicationCache.update() does what we expect.
     applicationCache.onupdateready = OfflineTest.priv(manifestUpdatedAgain);
     applicationCache.onnoupdate = failAndFinish;
     applicationCache.oncached = failAndFinish;
 
     gGotChecking = false;
     gGotDownloading = false;
 
     // The changing versions give out a new version each second,
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_refetchManifest.html
@@ -0,0 +1,103 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest">
+<head>
+<title>refetch manifest test</title>
+
+<script type="text/javascript" src="/MochiKit/packed.js"></script>
+<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/dom/tests/mochitest/ajax/offline/offlineTests.js"></script>
+
+<script type="text/javascript">
+
+function finish()
+{
+  // Get rid of the changed manifest.
+  var req = new XMLHttpRequest();
+  req.open("DELETE", "changingManifest.cacheManifest", false);
+  req.send("");
+
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
+function failAndFinish(e)
+{
+  OfflineTest.ok(false, "Unexpected event: " + e.type);
+  finish();
+}
+
+function manifestUpdated()
+{
+  // Replace this manifest with a new one.
+
+  // XXX: After this put, we will no longer have Cache-Control:
+  // no-cache on the manifest, so future updates will just use the
+  // cached manifest.
+
+  // Get the initial contents of the first two files.
+  fetcher = new OfflineCacheContents([g1SecUrl, g1HourUrl]);
+  fetcher.fetch(function(contents) {
+    gCacheContents = contents;
+
+    // Now make sure applicationCache.update() does what we expect.
+    applicationCache.onupdateready = OfflineTest.priv(manifestUpdatedAgain);
+    applicationCache.onnoupdate = failAndFinish;
+    applicationCache.oncached = failAndFinish;
+
+    gGotChecking = false;
+    gGotDownloading = false;
+
+    // The changing versions give out a new version each second,
+    // make sure it has time to grab a new version, and for the
+    // 1-second cache timeout to pass.
+    window.setTimeout("applicationCache.update()", 5000);
+  });
+}
+
+function replaceManifest()
+{
+  // If we replace the manifest after a downloading event, the update
+  // should fail when it revalidates the manifest at the end of the update.
+
+  var req = new XMLHttpRequest();
+  req.open("PUT", "changingManifest.cacheManifest", false);
+  req.setRequestHeader("Content-Type", "text/cache-manifest");
+  req.send("CACHE MANIFEST\n" +
+           "# v2\n" +
+           "changing1Hour.sjs\n" +
+           "changing1Sec.sjs\n");
+
+}
+
+function cached()
+{
+  OfflineTest.ok(true, "Got the expected cached event.");
+  finish();
+}
+
+function gotError()
+{
+  OfflineTest.ok(true, "Got the expected error event.");
+
+  // Now this update will be rescheduled, and it should succeed.
+  applicationCache.onerror = failAndFinish;
+  applicationCache.oncached = cached;
+}
+
+if (OfflineTest.setup()) {
+  applicationCache.onerror = gotError;
+  applicationCache.onnoupdate = failAndFinish;
+
+  applicationCache.ondownloading = replaceManifest;
+  applicationCache.oncached = failAndFinish;
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
--- a/uriloader/prefetch/nsOfflineCacheUpdate.cpp
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp
@@ -63,16 +63,18 @@
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "nsStreamUtils.h"
 #include "nsThreadUtils.h"
 #include "prlog.h"
 
 static nsOfflineCacheUpdateService *gOfflineCacheUpdateService = nsnull;
 
+static const PRUint32 kRescheduleLimit = 3;
+
 #if defined(PR_LOGGING)
 //
 // To enable logging (see prlog.h for full details):
 //
 //    set NSPR_LOG_MODULES=nsOfflineCacheUpdate:5
 //    set NSPR_LOG_FILE=offlineupdate.log
 //
 // this enables PR_LOG_ALWAYS level information and places all output in
@@ -101,16 +103,198 @@ DropReferenceFromURL(nsIURI * aURI)
         nsresult rv = url->SetRef(EmptyCString());
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
+// nsManifestCheck
+//-----------------------------------------------------------------------------
+
+class nsManifestCheck : public nsIStreamListener
+                      , public nsIChannelEventSink
+                      , public nsIInterfaceRequestor
+{
+public:
+    nsManifestCheck(nsOfflineCacheUpdate *aUpdate,
+                    nsIURI *aURI,
+                    nsIURI *aReferrerURI)
+        : mUpdate(aUpdate)
+        , mURI(aURI)
+        , mReferrerURI(aReferrerURI)
+        {}
+
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIREQUESTOBSERVER
+    NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSICHANNELEVENTSINK
+    NS_DECL_NSIINTERFACEREQUESTOR
+
+    nsresult Begin();
+
+private:
+
+    static NS_METHOD ReadManifest(nsIInputStream *aInputStream,
+                                  void *aClosure,
+                                  const char *aFromSegment,
+                                  PRUint32 aOffset,
+                                  PRUint32 aCount,
+                                  PRUint32 *aBytesConsumed);
+
+    nsRefPtr<nsOfflineCacheUpdate> mUpdate;
+    nsCOMPtr<nsIURI> mURI;
+    nsCOMPtr<nsIURI> mReferrerURI;
+    nsCOMPtr<nsICryptoHash> mManifestHash;
+    nsCOMPtr<nsIChannel> mChannel;
+};
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsISupports
+//-----------------------------------------------------------------------------
+NS_IMPL_ISUPPORTS4(nsManifestCheck,
+                   nsIRequestObserver,
+                   nsIStreamListener,
+                   nsIChannelEventSink,
+                   nsIInterfaceRequestor)
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck <public>
+//-----------------------------------------------------------------------------
+
+nsresult
+nsManifestCheck::Begin()
+{
+    nsresult rv;
+    mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = mManifestHash->Init(nsICryptoHash::MD5);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = NS_NewChannel(getter_AddRefs(mChannel),
+                       mURI,
+                       nsnull, nsnull, nsnull,
+                       nsIRequest::LOAD_BYPASS_CACHE);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // configure HTTP specific stuff
+    nsCOMPtr<nsIHttpChannel> httpChannel =
+        do_QueryInterface(mChannel);
+    if (httpChannel) {
+        httpChannel->SetReferrer(mReferrerURI);
+        httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
+                                      NS_LITERAL_CSTRING("offline-resource"),
+                                      PR_FALSE);
+    }
+
+    rv = mChannel->AsyncOpen(this, nsnull);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck <public>
+//-----------------------------------------------------------------------------
+
+/* static */
+NS_METHOD
+nsManifestCheck::ReadManifest(nsIInputStream *aInputStream,
+                              void *aClosure,
+                              const char *aFromSegment,
+                              PRUint32 aOffset,
+                              PRUint32 aCount,
+                              PRUint32 *aBytesConsumed)
+{
+    nsManifestCheck *manifestCheck =
+        static_cast<nsManifestCheck*>(aClosure);
+
+    nsresult rv;
+    *aBytesConsumed = aCount;
+
+    rv = manifestCheck->mManifestHash->Update(
+        reinterpret_cast<const PRUint8 *>(aFromSegment), aCount);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::OnStartRequest(nsIRequest *aRequest,
+                                nsISupports *aContext)
+{
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsManifestCheck::OnDataAvailable(nsIRequest *aRequest,
+                                 nsISupports *aContext,
+                                 nsIInputStream *aStream,
+                                 PRUint32 aOffset,
+                                 PRUint32 aCount)
+{
+    PRUint32 bytesRead;
+    aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsManifestCheck::OnStopRequest(nsIRequest *aRequest,
+                               nsISupports *aContext,
+                               nsresult aStatus)
+{
+    nsCAutoString manifestHash;
+    if (NS_SUCCEEDED(aStatus)) {
+        mManifestHash->Finish(PR_TRUE, manifestHash);
+    }
+
+    mUpdate->ManifestCheckCompleted(aStatus, manifestHash);
+
+    return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::GetInterface(const nsIID &aIID, void **aResult)
+{
+    if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+        NS_ADDREF_THIS();
+        *aResult = static_cast<nsIChannelEventSink *>(this);
+        return NS_OK;
+    }
+
+    return NS_ERROR_NO_INTERFACE;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIChannelEventSink
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::OnChannelRedirect(nsIChannel *aOldChannel,
+                                   nsIChannel *aNewChannel,
+                                   PRUint32 aFlags)
+{
+    // Redirects should cause the load (and therefore the update) to fail.
+    if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)
+        return NS_OK;
+    aOldChannel->Cancel(NS_ERROR_ABORT);
+    return NS_ERROR_ABORT;
+}
+
+//-----------------------------------------------------------------------------
 // nsOfflineCacheUpdateItem::nsISupports
 //-----------------------------------------------------------------------------
 
 NS_IMPL_ISUPPORTS6(nsOfflineCacheUpdateItem,
                    nsIDOMLoadStatus,
                    nsIRequestObserver,
                    nsIStreamListener,
                    nsIRunnable,
@@ -749,47 +933,47 @@ nsOfflineManifestItem::CheckNewManifestC
     nsresult rv;
 
     if (!mManifestHash) {
         // Nothing to compare against...
         return NS_OK;
     }
 
     nsCString newManifestHashValue;
-    rv = mManifestHash->Finish(PR_TRUE, newManifestHashValue);
+    rv = mManifestHash->Finish(PR_TRUE, mManifestHashValue);
     mManifestHash = nsnull;
 
     if (NS_FAILED(rv)) {
         LOG(("Could not finish manifest hash, rv=%08x", rv));
         // This is not critical error
         return NS_OK;
     }
 
     if (!ParseSucceeded()) {
         // Parsing failed, the hash is not valid
         return NS_OK;
     }
 
-    if (mOldManifestHashValue == newManifestHashValue) {
+    if (mOldManifestHashValue == mManifestHashValue) {
         LOG(("Update not needed, downloaded manifest content is byte-for-byte identical"));
         mNeedsUpdate = PR_FALSE;
     }
 
     // Store the manifest content hash value to the new
     // offline cache token
     nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsISupports> cacheToken;
     cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken));
     if (cacheToken) {
         nsCOMPtr<nsICacheEntryDescriptor> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
         NS_ENSURE_SUCCESS(rv, rv);
     
-        rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", PromiseFlatCString(newManifestHashValue).get());
+        rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", mManifestHashValue.get());
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     return NS_OK;
 }
 
 void
 nsOfflineManifestItem::ReadStrictFileOriginPolicyPref()
@@ -899,16 +1083,17 @@ NS_IMPL_ISUPPORTS1(nsOfflineCacheUpdate,
 
 nsOfflineCacheUpdate::nsOfflineCacheUpdate()
     : mState(STATE_UNINITIALIZED)
     , mAddedItems(PR_FALSE)
     , mPartialUpdate(PR_FALSE)
     , mSucceeded(PR_TRUE)
     , mObsolete(PR_FALSE)
     , mCurrentItem(-1)
+    , mRescheduleCount(0)
 {
 }
 
 nsOfflineCacheUpdate::~nsOfflineCacheUpdate()
 {
     LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this));
 }
 
@@ -1194,16 +1379,50 @@ nsOfflineCacheUpdate::LoadCompleted()
     }
 
     rv = NotifyCompleted(item);
     if (NS_FAILED(rv)) return;
 
     ProcessNextURI();
 }
 
+void
+nsOfflineCacheUpdate::ManifestCheckCompleted(nsresult aStatus,
+                                             const nsCString &aManifestHash)
+{
+    if (NS_SUCCEEDED(aStatus)) {
+        nsCAutoString firstManifestHash;
+        mManifestItem->GetManifestHash(firstManifestHash);
+        if (aManifestHash != firstManifestHash) {
+            aStatus = NS_ERROR_FAILURE;
+        }
+    }
+
+    if (NS_FAILED(aStatus)) {
+        mSucceeded = PR_FALSE;
+        NotifyError();
+    }
+
+    Finish();
+
+    if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) {
+        // Reschedule this update.
+        nsRefPtr<nsOfflineCacheUpdate> newUpdate =
+            new nsOfflineCacheUpdate();
+        newUpdate->Init(mManifestURI, mDocumentURI);
+
+        for (PRInt32 i = 0; i < mDocuments.Count(); i++) {
+            newUpdate->AddDocument(mDocuments[i]);
+        }
+
+        newUpdate->mRescheduleCount = mRescheduleCount + 1;
+        newUpdate->Schedule();
+    }
+}
+
 nsresult
 nsOfflineCacheUpdate::Begin()
 {
     LOG(("nsOfflineCacheUpdate::Begin [%p]", this));
 
     mCurrentItem = 0;
 
     if (mPartialUpdate) {
@@ -1305,17 +1524,33 @@ nsOfflineCacheUpdate::ProcessNextURI()
 {
     LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, current=%d, numItems=%d]",
          this, mCurrentItem, mItems.Length()));
 
     NS_ASSERTION(mState == STATE_DOWNLOADING,
                  "ProcessNextURI should only be called from the DOWNLOADING state");
 
     if (mCurrentItem >= static_cast<PRInt32>(mItems.Length())) {
-        return Finish();
+        if (mPartialUpdate) {
+            return Finish();
+        } else {
+            // Verify that the manifest wasn't changed during the
+            // update, to prevent capturing a cache while the server
+            // is being updated.  The check will call
+            // ManifestCheckCompleted() when it's done.
+            nsRefPtr<nsManifestCheck> manifestCheck =
+                new nsManifestCheck(this, mManifestURI, mDocumentURI);
+            if (NS_FAILED(manifestCheck->Begin())) {
+                mSucceeded = PR_FALSE;
+                NotifyError();
+                return Finish();
+            }
+
+            return NS_OK;
+        }
     }
 
 #if defined(PR_LOGGING)
     if (LOG_ENABLED()) {
         nsCAutoString spec;
         mItems[mCurrentItem]->mURI->GetSpec(spec);
         LOG(("%p: Opening channel for %s", this, spec.get()));
     }
--- a/uriloader/prefetch/nsOfflineCacheUpdate.h
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.h
@@ -133,16 +133,19 @@ public:
         { return mOpportunisticNamespaces; }
     nsIArray *GetNamespaces()
         { return mNamespaces.get(); }
 
     PRBool ParseSucceeded()
         { return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR); }
     PRBool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; }
 
+    void GetManifestHash(nsCString &aManifestHash)
+        { aManifestHash = mManifestHashValue; }
+
 private:
     static NS_METHOD ReadManifest(nsIInputStream *aInputStream,
                                   void *aClosure,
                                   const char *aFromSegment,
                                   PRUint32 aOffset,
                                   PRUint32 aCount,
                                   PRUint32 *aBytesConsumed);
 
@@ -191,16 +194,17 @@ private:
     nsCOMPtr<nsIMutableArray> mNamespaces;
 
     PRBool mNeedsUpdate;
     PRBool mStrictFileOriginPolicy;
 
     // manifest hash data
     nsCOMPtr<nsICryptoHash> mManifestHash;
     PRBool mManifestHashInitialized;
+    nsCString mManifestHashValue;
     nsCString mOldManifestHashValue;
 };
 
 class nsOfflineCacheUpdate : public nsIOfflineCacheUpdate
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIOFFLINECACHEUPDATE
@@ -211,16 +215,18 @@ public:
     static nsresult GetCacheKey(nsIURI *aURI, nsACString &aKey);
 
     nsresult Init();
 
     nsresult Begin();
     nsresult Cancel();
 
     void LoadCompleted();
+    void ManifestCheckCompleted(nsresult aStatus,
+                                const nsCString &aManifestHash);
 
     void AddDocument(nsIDOMDocument *aDocument) {
         mDocuments.AppendObject(aDocument);
     };
 
 private:
     nsresult HandleManifest(PRBool *aDoUpdate);
     nsresult AddURI(nsIURI *aURI, PRUint32 aItemType);
@@ -276,16 +282,20 @@ private:
     nsTArray<nsRefPtr<nsOfflineCacheUpdateItem> > mItems;
 
     /* Clients watching this update for changes */
     nsCOMArray<nsIWeakReference> mWeakObservers;
     nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers;
 
     /* Documents that requested this update */
     nsCOMArray<nsIDOMDocument> mDocuments;
+
+    /* Reschedule count.  When an update is rescheduled due to
+     * mismatched manifests, the reschedule count will be increased. */
+    PRUint32 mRescheduleCount;
 };
 
 class nsOfflineCacheUpdateService : public nsIOfflineCacheUpdateService
                                   , public nsIWebProgressListener
                                   , public nsIObserver
                                   , public nsSupportsWeakReference
 {
 public: