Bug 402272: Replace <link rel="offline-resource"> with manifests, and navigator.offlineResources/pendingOfflineLoads with window.applicationCache. p=dcamp/Honza Bombas, r=biesi, r+sr=jst, blocking1.9=jst
authordcamp@mozilla.com
Wed, 16 Jan 2008 13:54:33 -0800
changeset 10340 d74379797d5ee51522875bcdb2e15321b9e4b73b
parent 10339 664c45c1ae2c893b6f61fc1a3736258e84494398
child 10341 81098817d5c908b7c27c919ab5f53337b43edac8
push idunknown
push userunknown
push dateunknown
reviewersbiesi, r
bugs402272
milestone1.9b3pre
Bug 402272: Replace <link rel="offline-resource"> with manifests, and navigator.offlineResources/pendingOfflineLoads with window.applicationCache. p=dcamp/Honza Bombas, r=biesi, r+sr=jst, blocking1.9=jst
content/base/public/nsContentUtils.h
content/base/src/nsContentSink.cpp
content/base/src/nsContentSink.h
content/base/src/nsContentUtils.cpp
content/base/src/nsGkAtomList.h
content/html/document/src/nsHTMLContentSink.cpp
dom/public/base/nsPIDOMWindow.h
dom/public/idl/base/nsIDOMClientInformation.idl
dom/public/idl/base/nsIDOMWindow2.idl
dom/public/idl/offline/nsIDOMOfflineResourceList.idl
dom/src/base/nsGlobalWindow.cpp
dom/src/base/nsGlobalWindow.h
dom/src/offline/Makefile.in
dom/src/offline/nsDOMOfflineLoadStatusList.h
dom/src/offline/nsDOMOfflineResourceList.cpp
dom/src/offline/nsDOMOfflineResourceList.h
dom/tests/mochitest/ajax/offline/Makefile.in
dom/tests/mochitest/ajax/offline/badManifestMagic.cacheManifest
dom/tests/mochitest/ajax/offline/badManifestMagic.cacheManifest^headers^
dom/tests/mochitest/ajax/offline/missingFile.cacheManifest
dom/tests/mochitest/ajax/offline/missingFile.cacheManifest^headers^
dom/tests/mochitest/ajax/offline/offlineChild.html
dom/tests/mochitest/ajax/offline/offlineTests.js
dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest
dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest^headers^
dom/tests/mochitest/ajax/offline/simpleManifest.notmanifest
dom/tests/mochitest/ajax/offline/test_badManifestMagic.html
dom/tests/mochitest/ajax/offline/test_badManifestMime.html
dom/tests/mochitest/ajax/offline/test_isLocallyAvailable.html
dom/tests/mochitest/ajax/offline/test_missingFile.html
dom/tests/mochitest/ajax/offline/test_offlineIFrame.html
dom/tests/mochitest/ajax/offline/test_offlineResources.html
dom/tests/mochitest/ajax/offline/test_pendingOfflineLoads.html
dom/tests/mochitest/ajax/offline/test_simpleManifest.html
uriloader/prefetch/nsIOfflineCacheUpdate.idl
uriloader/prefetch/nsOfflineCacheUpdate.cpp
uriloader/prefetch/nsOfflineCacheUpdate.h
--- a/content/base/public/nsContentUtils.h
+++ b/content/base/public/nsContentUtils.h
@@ -1147,16 +1147,31 @@ public:
    * nsNativeKeyEvent. It's also used in DOMEventToNativeKeyEvent.
    * See bug 406407 for details.
    */
   static nsEvent* GetNativeEvent(nsIDOMEvent* aDOMEvent);
   static PRBool DOMEventToNativeKeyEvent(nsIDOMEvent* aDOMEvent,
                                          nsNativeKeyEvent* aNativeEvent,
                                          PRBool aGetCharCode);
 
+  /**
+   * Get the application manifest URI for this context.  The manifest URI
+   * is specified in the manifest= attribute of the root element of the
+   * toplevel window.
+   *
+   * @param aWindow The context to check.
+   * @param aURI The manifest URI.
+   */
+  static void GetOfflineAppManifest(nsIDOMWindow *aWindow, nsIURI **aURI);
+
+  /**
+   * Check whether an application should be allowed to use offline APIs.
+   */
+  static PRBool OfflineAppAllowed(nsIURI *aURI);
+
 private:
 
   static PRBool InitializeEventTable();
 
   static nsresult doReparentContentWrapper(nsIContent *aChild,
                                            JSContext *cx,
                                            JSObject *aOldGlobal,
                                            JSObject *aNewGlobal,
@@ -1218,17 +1233,16 @@ private:
 
 #ifdef IBMBIDI
   static nsIBidiKeyboard* sBidiKeyboard;
 #endif
 
   static PRBool sInitialized;
 };
 
-
 #define NS_HOLD_JS_OBJECTS(obj, clazz)                                         \
   nsContentUtils::HoldJSObjects(NS_CYCLE_COLLECTION_UPCAST(obj, clazz),        \
                                 &NS_CYCLE_COLLECTION_NAME(clazz))
 
 #define NS_DROP_JS_OBJECTS(obj, clazz)                                         \
   nsContentUtils::DropJSObjects(NS_CYCLE_COLLECTION_UPCAST(obj, clazz))
 
 
--- a/content/base/src/nsContentSink.cpp
+++ b/content/base/src/nsContentSink.cpp
@@ -65,20 +65,18 @@
 #include "nsIViewManager.h"
 #include "nsIContentViewer.h"
 #include "nsIAtom.h"
 #include "nsGkAtoms.h"
 #include "nsIDOMWindowInternal.h"
 #include "nsIPrincipal.h"
 #include "nsIScriptGlobalObject.h"
 #include "nsNetCID.h"
-#include "nsICache.h"
-#include "nsICacheService.h"
-#include "nsICacheSession.h"
 #include "nsIOfflineCacheUpdate.h"
+#include "nsIScriptSecurityManager.h"
 #include "nsIDOMLoadStatus.h"
 #include "nsICookieService.h"
 #include "nsIPrompt.h"
 #include "nsServiceManagerUtils.h"
 #include "nsContentUtils.h"
 #include "nsParserUtils.h"
 #include "nsCRT.h"
 #include "nsEscape.h"
@@ -89,16 +87,17 @@
 #include "nsIAppShell.h"
 #include "nsWidgetsCID.h"
 #include "nsIDOMNSDocument.h"
 #include "nsIRequest.h"
 #include "nsNodeUtils.h"
 #include "nsIDOMNode.h"
 #include "nsThreadUtils.h"
 #include "nsPresShellIterator.h"
+#include "nsPIDOMWindow.h"
 
 PRLogModuleInfo* gContentSinkLogModuleInfo;
 
 class nsScriptLoaderObserverProxy : public nsIScriptLoaderObserver
 {
 public:
   nsScriptLoaderObserverProxy(nsIScriptLoaderObserver* aInner)
     : mInner(do_GetWeakReference(aInner))
@@ -679,21 +678,16 @@ nsContentSink::ProcessLink(nsIContent* a
   nsStyleLinkElement::ParseLinkTypes(aRel, linkTypes);
 
   PRBool hasPrefetch = (linkTypes.IndexOf(NS_LITERAL_STRING("prefetch")) != -1);
   // prefetch href if relation is "next" or "prefetch"
   if (hasPrefetch || linkTypes.IndexOf(NS_LITERAL_STRING("next")) != -1) {
     PrefetchHref(aHref, aElement, hasPrefetch);
   }
 
-  // fetch href into the offline cache if relation is "offline-resource"
-  if (linkTypes.IndexOf(NS_LITERAL_STRING("offline-resource")) != -1) {
-    AddOfflineResource(aHref, aElement);
-  }
-
   // is it a stylesheet link?
   if (linkTypes.IndexOf(NS_LITERAL_STRING("stylesheet")) == -1) {
     return NS_OK;
   }
 
   PRBool isAlternate = linkTypes.IndexOf(NS_LITERAL_STRING("alternate")) != -1;
   return ProcessStyleLink(aElement, aHref, isAlternate, aTitle, aType,
                           aMedia);
@@ -815,74 +809,72 @@ nsContentSink::PrefetchHref(const nsAStr
               mDocumentBaseURI);
     if (uri) {
       nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(aSource);
       prefetchService->PrefetchURI(uri, mDocumentURI, domNode, aExplicit);
     }
   }
 }
 
-nsresult
-nsContentSink::AddOfflineResource(const nsAString &aHref, nsIContent *aSource)
+void
+nsContentSink::ProcessOfflineManifest(nsIContent *aElement)
 {
-  PRBool match;
-  nsresult rv;
+  // Check for a manifest= attribute.
+  nsAutoString manifestSpec;
+  aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::manifest, manifestSpec);
 
-  nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(mDocumentURI);
-  if (!innerURI)
-    return NS_ERROR_FAILURE;
-
-  if (!mHaveOfflineResources) {
-    mHaveOfflineResources = PR_TRUE;
-
-    // only let http and https urls add offline resources
-    nsresult rv = innerURI->SchemeIs("http", &match);
-    NS_ENSURE_SUCCESS(rv, rv);
+  if (manifestSpec.IsEmpty() ||
+      manifestSpec.FindChar('#') != kNotFound) {
+    return;
+  }
 
-    if (!match) {
-      rv = innerURI->SchemeIs("https", &match);
-      NS_ENSURE_SUCCESS(rv, rv);
-      if (!match)
-        return NS_OK;
-    }
-
-    // create updater
-    mOfflineCacheUpdate =
-      do_CreateInstance(NS_OFFLINECACHEUPDATE_CONTRACTID, &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
+  // We only care about manifests in toplevel windows.
+  nsCOMPtr<nsPIDOMWindow> pwindow =
+    do_QueryInterface(mDocument->GetScriptGlobalObject());
+  if (!pwindow) {
+    return;
+  }
 
-    nsCAutoString ownerDomain;
-    rv = innerURI->GetHostPort(ownerDomain);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    nsCAutoString ownerSpec;
-    rv = mDocumentURI->GetSpec(ownerSpec);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = mOfflineCacheUpdate->Init(PR_FALSE, ownerDomain,
-                                   ownerSpec, mDocumentURI);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // Kick off this update when the document is done loading
-    nsCOMPtr<nsIDOMDocument> doc = do_QueryInterface(mDocument);
-    mOfflineCacheUpdate->ScheduleOnDocumentStop(doc);
+  nsCOMPtr<nsIDOMWindow> window =
+    do_QueryInterface(pwindow->GetOuterWindow());
+  if (!window) {
+    return;
   }
 
-  if (!mOfflineCacheUpdate) return NS_OK;
+  nsCOMPtr<nsIDOMWindow> parent;
+  window->GetParent(getter_AddRefs(parent));
+  if (parent.get() != window.get()) {
+    return;
+  }
+
+  // Only update if the document has permission to use offline APIs.
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return;
+  }
 
-  const nsACString &charset = mDocument->GetDocumentCharacterSet();
-  nsCOMPtr<nsIURI> uri;
-  rv = NS_NewURI(getter_AddRefs(uri), aHref,
-                 charset.IsEmpty() ? nsnull : PromiseFlatCString(charset).get(),
-                 mDocumentBaseURI);
-  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<nsIURI> manifestURI;
+  nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(manifestURI),
+                                            manifestSpec, mDocument,
+                                            mDocumentURI);
+  if (!manifestURI) {
+    return;
+  }
 
-  nsCOMPtr<nsIDOMNode> domNode = do_QueryInterface(aSource);
+  // Documents must list a manifest from the same origin
+  nsresult rv = nsContentUtils::GetSecurityManager()->
+                   CheckSameOriginURI(manifestURI, mDocumentURI, PR_TRUE);
+  if (NS_FAILED(rv)) {
+    return;
+  }
 
-  return mOfflineCacheUpdate->AddURI(uri, domNode);
+  // Start the update
+  nsCOMPtr<nsIDOMDocument> domdoc = do_QueryInterface(mDocument);
+  nsCOMPtr<nsIOfflineCacheUpdateService> updateService =
+    do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID);
+  updateService->ScheduleOnDocumentStop(manifestURI, mDocumentURI, domdoc);
 }
 
 void
 nsContentSink::ScrollToRef()
 {
   if (mRef.IsEmpty()) {
     return;
   }
--- a/content/base/src/nsContentSink.h
+++ b/content/base/src/nsContentSink.h
@@ -70,17 +70,16 @@ class nsIDocShell;
 class nsICSSLoader;
 class nsIParser;
 class nsIAtom;
 class nsIChannel;
 class nsIContent;
 class nsIViewManager;
 class nsNodeInfoManager;
 class nsScriptLoader;
-class nsIOfflineCacheUpdate;
 
 #ifdef NS_DEBUG
 
 extern PRLogModuleInfo* gContentSinkLogModuleInfo;
 
 #define SINK_TRACE_CALLS              0x1
 #define SINK_TRACE_REFLOW             0x2
 #define SINK_ALWAYS_REFLOW            0x4
@@ -164,17 +163,17 @@ protected:
                                     const nsSubstring& aHref,
                                     PRBool aAlternate,
                                     const nsSubstring& aTitle,
                                     const nsSubstring& aType,
                                     const nsSubstring& aMedia);
 
   void PrefetchHref(const nsAString &aHref, nsIContent *aSource,
                     PRBool aExplicit);
-  nsresult AddOfflineResource(const nsAString &aHref, nsIContent *aSource);
+  void ProcessOfflineManifest(nsIContent *aElement);
 
   void ScrollToRef();
   nsresult RefreshIfEnabled(nsIViewManager* vm);
 
   // Start layout.  If aIgnorePendingSheets is true, this will happen even if
   // we still have stylesheet loads pending.  Otherwise, we'll wait until the
   // stylesheets are all done loading.
   void StartLayout(PRBool aIgnorePendingSheets);
@@ -255,33 +254,28 @@ protected:
   // frequency parser interrupt mode without falling through to the
   // logic which decides whether to switch to the high frequency
   // parser interrupt mode.
   PRUint8 mDeflectedCount;
 
   // Do we notify based on time?
   PRPackedBool mNotifyOnTimer;
 
-  // For saving <link rel="offline-resource"> links
-  nsCOMPtr<nsIOfflineCacheUpdate> mOfflineCacheUpdate;
-
   // Have we already called BeginUpdate for this set of content changes?
   PRUint8 mBeganUpdate : 1;
   PRUint8 mLayoutStarted : 1;
   PRUint8 mScrolledToRefAlready : 1;
   PRUint8 mCanInterruptParser : 1;
   PRUint8 mDynamicLowerValue : 1;
   PRUint8 mParsing : 1;
   PRUint8 mDroppedTimer : 1;
   PRUint8 mInTitle : 1;
   PRUint8 mChangeScrollPosWhenScrollingToRef : 1;
   // If true, we deferred starting layout until sheets load
   PRUint8 mDeferredLayoutStart : 1;
-  // true if an <link rel="offline-resource"> nodes have been encountered.
-  PRUint8 mHaveOfflineResources : 1;
   // If true, we deferred notifications until sheets load
   PRUint8 mDeferredFlushTags : 1;
   
   // -- Can interrupt parsing members --
   PRUint32 mDelayTimerStart;
 
   // Interrupt parsing during token procesing after # of microseconds
   PRInt32 mMaxTokenProcessingTime;
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -140,16 +140,17 @@ static NS_DEFINE_CID(kXTFServiceCID, NS_
 #include "nsContentErrors.h"
 #include "nsUnicharUtilCIID.h"
 #include "nsICaseConversion.h"
 #include "nsCompressedCharMap.h"
 #include "nsINativeKeyBindings.h"
 #include "nsIDOMNSUIEvent.h"
 #include "nsIDOMNSEvent.h"
 #include "nsIPrivateDOMEvent.h"
+#include "nsIPermissionManager.h"
 
 #ifdef IBMBIDI
 #include "nsIBidiKeyboard.h"
 #endif
 #include "nsCycleCollectionParticipant.h"
 
 // for ReportToConsole
 #include "nsIStringBundle.h"
@@ -670,16 +671,92 @@ DEFINE_CCMAP(gPuncCharsCCMap, const);
 
 // static
 PRBool
 nsContentUtils::IsPunctuationMark(PRUnichar aChar)
 {
   return CCMAP_HAS_CHAR(gPuncCharsCCMap, aChar);
 }
 
+/* static */
+void
+nsContentUtils::GetOfflineAppManifest(nsIDOMWindow *aWindow, nsIURI **aURI)
+{
+  nsCOMPtr<nsIDOMWindow> top;
+  aWindow->GetTop(getter_AddRefs(top));
+  if (!top) {
+    return;
+  }
+
+  nsCOMPtr<nsIDOMDocument> topDOMDocument;
+  top->GetDocument(getter_AddRefs(topDOMDocument));
+  nsCOMPtr<nsIDocument> topDoc = do_QueryInterface(topDOMDocument);
+  if (!topDoc) {
+    return;
+  }
+
+  nsCOMPtr<nsIContent> docElement = topDoc->GetRootContent();
+  if (!docElement) {
+    return;
+  }
+
+  nsAutoString manifestSpec;
+  docElement->GetAttr(kNameSpaceID_None, nsGkAtoms::manifest, manifestSpec);
+
+  // Manifest URIs can't have fragment identifiers.
+  if (manifestSpec.IsEmpty() ||
+      manifestSpec.FindChar('#') != kNotFound) {
+    return;
+  }
+
+  nsContentUtils::NewURIWithDocumentCharset(aURI, manifestSpec,
+                                            topDoc, topDoc->GetBaseURI());
+}
+
+/* static */
+PRBool
+nsContentUtils::OfflineAppAllowed(nsIURI *aURI)
+{
+  nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aURI);
+  if (!innerURI)
+    return PR_FALSE;
+
+  // only http and https applications can use offline APIs.
+  PRBool match;
+  nsresult rv = innerURI->SchemeIs("http", &match);
+  NS_ENSURE_SUCCESS(rv, PR_FALSE);
+
+  if (!match) {
+    rv = innerURI->SchemeIs("https", &match);
+    NS_ENSURE_SUCCESS(rv, PR_FALSE);
+    if (!match) {
+      return PR_FALSE;
+    }
+  }
+
+  nsCOMPtr<nsIPermissionManager> permissionManager =
+    do_GetService(NS_PERMISSIONMANAGER_CONTRACTID);
+  if (!permissionManager) {
+    return PR_FALSE;
+  }
+
+  PRUint32 perm;
+  permissionManager->TestExactPermission(innerURI, "offline-app", &perm);
+
+  if (perm == nsIPermissionManager::UNKNOWN_ACTION) {
+    return GetBoolPref("offline-apps.allow_by_default");
+  }
+
+  if (perm == nsIPermissionManager::DENY_ACTION) {
+    return PR_FALSE;
+  }
+
+  return PR_TRUE;
+}
+
 // static
 void
 nsContentUtils::Shutdown()
 {
   sInitialized = PR_FALSE;
 
   NS_HTMLParanoidFragmentSinkShutdown();
   NS_XHTMLParanoidFragmentSinkShutdown();
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -471,16 +471,17 @@ GK_ATOM(listrows, "listrows")
 GK_ATOM(load, "load")
 GK_ATOM(localName, "local-name")
 GK_ATOM(longdesc, "longdesc")
 GK_ATOM(lowerFirst, "lower-first")
 GK_ATOM(lowest, "lowest")
 GK_ATOM(lowsrc, "lowsrc")
 GK_ATOM(ltr, "ltr")
 GK_ATOM(map, "map")
+GK_ATOM(manifest, "manifest")
 GK_ATOM(marginheight, "marginheight")
 GK_ATOM(marginwidth, "marginwidth")
 GK_ATOM(marquee, "marquee")
 GK_ATOM(match, "match")
 GK_ATOM(max, "max")
 GK_ATOM(maxheight, "maxheight")
 GK_ATOM(maxlength, "maxlength")
 GK_ATOM(maxpos, "maxpos")
--- a/content/html/document/src/nsHTMLContentSink.cpp
+++ b/content/html/document/src/nsHTMLContentSink.cpp
@@ -2341,16 +2341,17 @@ HTMLContentSink::OpenContainer(const nsI
     case eHTMLTag_html:
       if (mRoot) {
         // If we've already hit this code once, need to check for
         // already-present attributes on the root.
         AddAttributes(aNode, mRoot, PR_TRUE, mNotifiedRootInsertion);
         if (!mNotifiedRootInsertion) {
           NotifyRootInsertion();
         }
+        ProcessOfflineManifest(mRoot);
       }
       break;
     case eHTMLTag_form:
       rv = OpenForm(aNode);
       break;
     default:
       rv = mCurrentContext->OpenContainer(aNode);
       break;
@@ -2987,23 +2988,16 @@ HTMLContentSink::ProcessLINKTag(const ns
         PRBool hasPrefetch = (linkTypes.IndexOf(NS_LITERAL_STRING("prefetch")) != -1);
         if (hasPrefetch || linkTypes.IndexOf(NS_LITERAL_STRING("next")) != -1) {
           nsAutoString hrefVal;
           element->GetAttr(kNameSpaceID_None, nsGkAtoms::href, hrefVal);
           if (!hrefVal.IsEmpty()) {
             PrefetchHref(hrefVal, element, hasPrefetch);
           }
         }
-        if (linkTypes.IndexOf(NS_LITERAL_STRING("offline-resource")) != -1) {
-          nsAutoString hrefVal;
-          element->GetAttr(kNameSpaceID_None, nsGkAtoms::href, hrefVal);
-          if (!hrefVal.IsEmpty()) {
-            AddOfflineResource(hrefVal, element);
-          }
-        }
       }
     }
   }
 
   return result;
 }
 
 #ifdef DEBUG
--- a/dom/public/base/nsPIDOMWindow.h
+++ b/dom/public/base/nsPIDOMWindow.h
@@ -67,18 +67,18 @@ enum PopupControlState {
 class nsIDocShell;
 class nsIFocusController;
 class nsIDocument;
 class nsIScriptTimeoutHandler;
 class nsPresContext;
 struct nsTimeout;
 
 #define NS_PIDOMWINDOW_IID \
-{ 0x51316cbc, 0x595e, 0x40b1, \
-  { 0xab, 0x07, 0x21, 0xbf, 0xe0, 0x92, 0x62, 0xa9 } }
+{ 0x909852b5, 0xb9e6, 0x4d94, \
+  { 0x8d, 0xe3, 0x05, 0x16, 0x34, 0x80, 0x0b, 0x73 } }
 
 class nsPIDOMWindow : public nsIDOMWindowInternal
 {
 public:
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_PIDOMWINDOW_IID)
 
   virtual nsPIDOMWindow* GetPrivateRoot() = 0;
 
@@ -267,16 +267,18 @@ public:
 
   // Resume suspended timeouts in this window and in child windows.
   virtual nsresult ResumeTimeouts() = 0;
   
   // Fire any DOM notification events related to things that happened while
   // the window was frozen.
   virtual nsresult FireDelayedDOMEvents() = 0;
 
+  virtual PRBool IsFrozen() const = 0;
+
   // Add a timeout to this window.
   virtual nsresult SetTimeoutOrInterval(nsIScriptTimeoutHandler *aHandler,
                                         PRInt32 interval,
                                         PRBool aIsInterval, PRInt32 *aReturn) = 0;
 
   // Clear a timeout from this window.
   virtual nsresult ClearTimeoutOrInterval(PRInt32 aTimerID) = 0;
 
--- a/dom/public/idl/base/nsIDOMClientInformation.idl
+++ b/dom/public/idl/base/nsIDOMClientInformation.idl
@@ -35,27 +35,24 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "domstubs.idl"
 
 interface nsIDOMOfflineResourceList;
 interface nsIDOMLoadStatusList;
 
-[scriptable, uuid(02bb1271-05dd-4bde-a9ca-68571bf8c702)]
+[scriptable, uuid(4de9dbd5-0eff-47aa-8520-c062c6933d6a)]
 interface nsIDOMClientInformation : nsISupports
 {
   /**
    * Web Applications 1.0 Browser State: registerContentHandler
    * Allows web services to register themselves as handlers for certain content
    * types.
    * http://whatwg.org/specs/web-apps/current-work/
    */
   void registerContentHandler(in DOMString mimeType, in DOMString uri, in DOMString title);
   void registerProtocolHandler(in DOMString protocol, in DOMString uri, in DOMString title);
 
   boolean isLocallyAvailable(in DOMString uri, in boolean whenOffline);
-
-  readonly attribute nsIDOMOfflineResourceList offlineResources;
-  readonly attribute nsIDOMLoadStatusList pendingOfflineLoads;
 };
 
 
--- a/dom/public/idl/base/nsIDOMWindow2.idl
+++ b/dom/public/idl/base/nsIDOMWindow2.idl
@@ -32,18 +32,25 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsIDOMWindow.idl"
 
-[scriptable, uuid(65455132-b96a-40ec-adea-52fa22b1028c)]
+interface nsIDOMOfflineResourceList;
+
+[scriptable, uuid(73c5fa35-3add-4c87-a303-a850ccf4d65a)]
 interface nsIDOMWindow2 : nsIDOMWindow
 {
   /**
    * Get the window root for this window. This is useful for hooking
    * up event listeners to this window and every other window nested
    * in the window root.
    */
   [noscript] readonly attribute nsIDOMEventTarget windowRoot;
+
+  /**
+   * Get the application cache object for this window.
+   */
+  readonly attribute nsIDOMOfflineResourceList applicationCache;
 };
--- a/dom/public/idl/offline/nsIDOMOfflineResourceList.idl
+++ b/dom/public/idl/offline/nsIDOMOfflineResourceList.idl
@@ -36,18 +36,81 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "domstubs.idl"
 
 [scriptable, uuid(8449bce2-0d8c-4c74-ab79-b41b8d81f1c4)]
 interface nsIDOMOfflineResourceList : nsISupports
 {
+  /**
+   * Enumerate the list of dynamically-managed entries.
+   */
   readonly attribute unsigned long length;
   DOMString item(in unsigned long index);
 
+  /**
+   * Add an item to the list of dynamically-managed entries.  The resource
+   * will be fetched into the application cache.
+   *
+   * @param uri
+   *        The resource to add.
+   */
   void add(in DOMString uri);
+
+  /**
+   * Remove an item from the list of dynamically-managed entries.  If this
+   * was the last reference to a URI in the application cache, the cache
+   * entry will be removed.
+   *
+   * @param uri
+   *        The resource to remove.
+   */
   void remove(in DOMString uri);
-  boolean has(in DOMString uri);
-  void clear();
+
+  /**
+   * State of the application cache this object is associated with.
+   */
+
+  /* This object is not associated with an application cache. */
+  const unsigned short UNCACHED = 0;
+
+  /* The application cache is not being updated. */
+  const unsigned short IDLE = 1;
+
+  /* The manifest is being fetched and checked for updates */
+  const unsigned short CHECKING = 2;
+
+  /* Resources are being downloaded to be added to the cache */
+  const unsigned short DOWNLOADING = 3;
+
+  /**
+   * There is a new version of the application cache available
+   *
+   * Versioned application caches are not currently implemented, so this
+   * value will not yet be returned
+   */
+  const unsigned short UPDATEREADY = 4;
 
-  void refresh();
+  readonly attribute unsigned short status;
+
+  /**
+   * Begin the application update process on the associated application cache.
+   */
+  void update();
+
+  /**
+   * Swap in the newest version of the application cache.
+   *
+   * Versioned application caches are not currently implemented, so this
+   * method will throw an exception.
+   */
+  void swapCache();
+
+  /* Events */
+  attribute nsIDOMEventListener onchecking;
+  attribute nsIDOMEventListener onerror;
+  attribute nsIDOMEventListener onnoupdate;
+  attribute nsIDOMEventListener ondownloading;
+  attribute nsIDOMEventListener onprogress;
+  attribute nsIDOMEventListener onupdateready;
+  attribute nsIDOMEventListener oncached;
 };
--- a/dom/src/base/nsGlobalWindow.cpp
+++ b/dom/src/base/nsGlobalWindow.cpp
@@ -46,17 +46,16 @@
 
 // Local Includes
 #include "nsGlobalWindow.h"
 #include "nsScreen.h"
 #include "nsHistory.h"
 #include "nsBarProps.h"
 #include "nsDOMStorage.h"
 #include "nsDOMOfflineResourceList.h"
-#include "nsDOMOfflineLoadStatusList.h"
 #include "nsDOMError.h"
 
 // Helper Classes
 #include "nsXPIDLString.h"
 #include "nsJSUtils.h"
 #include "prmem.h"
 #include "jsapi.h"              // for JSAutoRequest
 #include "jsdbgapi.h"           // for JS_ClearWatchPointsForObject
@@ -759,16 +758,17 @@ nsGlobalWindow::CleanUp()
   mMenubar = nsnull;
   mToolbar = nsnull;
   mLocationbar = nsnull;
   mPersonalbar = nsnull;
   mStatusbar = nsnull;
   mScrollbars = nsnull;
   mLocation = nsnull;
   mFrames = nsnull;
+  mApplicationCache = nsnull;
 
   ClearControllers();
 
   mOpener = nsnull;             // Forces Release
   if (mContext) {
 #ifdef DEBUG
     nsCycleCollector_DEBUG_shouldBeFreed(mContext);
 #endif
@@ -844,16 +844,21 @@ nsGlobalWindow::FreeInnerObjects(PRBool 
   if (mDocument)
     nsCycleCollector_DEBUG_shouldBeFreed(nsCOMPtr<nsISupports>(do_QueryInterface(mDocument)));
 #endif
 
   // Remove our reference to the document and the document principal.
   mDocument = nsnull;
   mDoc = nsnull;
 
+  if (mApplicationCache) {
+    static_cast<nsDOMOfflineResourceList*>(mApplicationCache.get())->Disconnect();
+    mApplicationCache = nsnull;
+  }
+
   if (aClearScope) {
     PRUint32 lang_id;
     NS_STID_FOR_ID(lang_id) {
       // Note that scx comes from the outer window.  If this is an inner
       // window, it may not be the current inner for its outer.
       nsIScriptContext *scx = GetScriptContextInternal(lang_id);
       if (scx)
         scx->ClearScope(mScriptGlobals[NS_STID_INDEX(lang_id)], PR_TRUE);
@@ -935,16 +940,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
 
   for (PRUint32 i = 0; i < NS_STID_ARRAY_UBOUND; ++i) {      
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mInnerWindowHolders[i])
   }
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOpenerScriptPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mListenerManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mSessionStorage)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mApplicationCache)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mDocumentPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mDoc)
 
   // Traverse any associated preserved wrappers.
   {
     if (tmp->mDoc) {
       cb.NoteXPCOMChild(tmp->mDoc->GetReference(tmp));
     }
@@ -978,16 +984,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
 
   for (PRUint32 i = 0; i < NS_STID_ARRAY_UBOUND; ++i) {      
     NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mInnerWindowHolders[i])
   }
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOpenerScriptPrincipal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mListenerManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mSessionStorage)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mApplicationCache)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mDocumentPrincipal)
 
   // Unlink any associated preserved wrapper.
   if (tmp->mDoc) {
     tmp->mDoc->RemoveReference(tmp);
     NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mDoc)
   }
 
@@ -2685,16 +2692,56 @@ nsGlobalWindow::GetFrames(nsIDOMWindowCo
   }
 
   *aFrames = static_cast<nsIDOMWindowCollection *>(mFrames);
   NS_IF_ADDREF(*aFrames);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsGlobalWindow::GetApplicationCache(nsIDOMOfflineResourceList **aApplicationCache)
+{
+  FORWARD_TO_INNER(GetApplicationCache, (aApplicationCache), NS_ERROR_UNEXPECTED);
+
+  NS_ENSURE_ARG_POINTER(aApplicationCache);
+
+  if (!mApplicationCache) {
+    nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(GetDocShell()));
+    if (!webNav) {
+      return NS_ERROR_FAILURE;
+    }
+
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = webNav->GetCurrentURI(getter_AddRefs(uri));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIURI> manifestURI;
+    nsContentUtils::GetOfflineAppManifest(GetOuterWindowInternal(),
+                                          getter_AddRefs(manifestURI));
+
+    PRBool isToplevel;
+    nsCOMPtr<nsIDOMWindow> parentWindow;
+    GetParent(getter_AddRefs(parentWindow));
+    isToplevel = (parentWindow.get() == static_cast<nsIDOMWindow*>(GetOuterWindowInternal()));
+
+    nsDOMOfflineResourceList* applicationCache =
+      new nsDOMOfflineResourceList(isToplevel, manifestURI, uri, this);
+
+    if (!applicationCache) 
+        return NS_ERROR_OUT_OF_MEMORY;
+
+    mApplicationCache = applicationCache;
+  }
+
+  NS_IF_ADDREF(*aApplicationCache = mApplicationCache);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsGlobalWindow::GetCrypto(nsIDOMCrypto** aCrypto)
 {
   FORWARD_TO_OUTER(GetCrypto, (aCrypto), NS_ERROR_NOT_INITIALIZED);
 
   if (!mCrypto) {
     mCrypto = do_CreateInstance(kCryptoContractID);
   }
 
@@ -6648,16 +6695,20 @@ nsGlobalWindow::FireDelayedDOMEvents()
   if (mPendingStorageEvents) {
     // Fire pending storage events.
     mPendingStorageEvents->EnumerateRead(FirePendingStorageEvents, this);
 
     delete mPendingStorageEvents;
     mPendingStorageEvents = nsnull;
   }
 
+  if (mApplicationCache) {
+    static_cast<nsDOMOfflineResourceList*>(mApplicationCache.get())->FirePendingEvents();
+  }
+
   if (mFireOfflineStatusChangeEventOnThaw) {
     mFireOfflineStatusChangeEventOnThaw = PR_FALSE;
     FireOfflineStatusEvent();
   }
 
   nsCOMPtr<nsIDocShellTreeNode> node =
     do_QueryInterface(GetDocShell());
   if (node) {
@@ -8991,18 +9042,16 @@ nsNavigator::Preference()
 void
 nsNavigator::LoadingNewDocument()
 {
   // Release these so that they will be recreated for the
   // new document (if requested).  The plugins or mime types
   // arrays may have changed.  See bug 150087.
   mMimeTypes = nsnull;
   mPlugins = nsnull;
-  mOfflineResources = nsnull;
-  mPendingOfflineLoads = nsnull;
 }
 
 nsresult
 nsNavigator::RefreshMIMEArray()
 {
   nsresult rv = NS_OK;
   if (mMimeTypes)
     rv = mMimeTypes->Refresh();
@@ -9113,56 +9162,8 @@ nsNavigator::IsLocallyAvailable(const ns
     NS_ENSURE_SUCCESS(rv, rv);
   } else {
     *aIsAvailable = PR_FALSE;
   }
 
   return NS_OK;
 }
 
-NS_IMETHODIMP
-nsNavigator::GetOfflineResources(nsIDOMOfflineResourceList **aList)
-{
-  NS_ENSURE_ARG_POINTER(aList);
-
-  if (!mOfflineResources) {
-    nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(GetDocShell()));
-    if (!webNav) {
-      return NS_ERROR_FAILURE;
-    }
-
-    nsCOMPtr<nsIURI> uri;
-    nsresult rv = webNav->GetCurrentURI(getter_AddRefs(uri));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    mOfflineResources = new nsDOMOfflineResourceList(uri);
-    if (!mOfflineResources) return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  NS_IF_ADDREF(*aList = mOfflineResources);
-
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsNavigator::GetPendingOfflineLoads(nsIDOMLoadStatusList **aList)
-{
-  NS_ENSURE_ARG_POINTER(aList);
-
-  if (!mPendingOfflineLoads) {
-    nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(GetDocShell()));
-    if (!webNav) {
-      return NS_ERROR_FAILURE;
-    }
-
-    nsCOMPtr<nsIURI> uri;
-    nsresult rv = webNav->GetCurrentURI(getter_AddRefs(uri));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    mPendingOfflineLoads = new nsDOMOfflineLoadStatusList(uri);
-    if (!mPendingOfflineLoads) return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  NS_IF_ADDREF(*aList = mPendingOfflineLoads);
-
-  return NS_OK;
-}
-
--- a/dom/src/base/nsGlobalWindow.h
+++ b/dom/src/base/nsGlobalWindow.h
@@ -296,16 +296,20 @@ public:
   virtual NS_HIDDEN_(PopupControlState) PushPopupControlState(PopupControlState state, PRBool aForce) const;
   virtual NS_HIDDEN_(void) PopPopupControlState(PopupControlState state) const;
   virtual NS_HIDDEN_(PopupControlState) GetPopupControlState() const;
 
   virtual NS_HIDDEN_(nsresult) SaveWindowState(nsISupports **aState);
   virtual NS_HIDDEN_(nsresult) RestoreWindowState(nsISupports *aState);
   virtual NS_HIDDEN_(nsresult) ResumeTimeouts();
   virtual NS_HIDDEN_(nsresult) FireDelayedDOMEvents();
+  virtual NS_HIDDEN_(PRBool) IsFrozen() const
+  {
+    return mIsFrozen;
+  }
 
   virtual NS_HIDDEN_(PRBool) WouldReuseInnerWindow(nsIDocument *aNewDocument);
 
   virtual NS_HIDDEN_(nsPIDOMEventTarget*) GetTargetForDOMEvent()
   {
     return static_cast<nsPIDOMEventTarget*>(GetOuterWindowInternal());
   }
   virtual NS_HIDDEN_(nsPIDOMEventTarget*) GetTargetForEventTargetChain()
@@ -387,21 +391,16 @@ public:
     return static_cast<nsGlobalWindow *>(mInnerWindow);
   }
 
   nsGlobalWindow *EnsureInnerWindowInternal()
   {
     return static_cast<nsGlobalWindow *>(EnsureInnerWindow());
   }
 
-  PRBool IsFrozen() const
-  {
-    return mIsFrozen;
-  }
-
   PRBool IsCreatingInnerWindow() const
   {
     return  mCreatingInnerWindow;
   }
 
   PRBool IsChromeWindow() const
   {
     return mIsChrome;
@@ -713,16 +712,18 @@ protected:
   JSObject* mJSObject;
 
   nsDataHashtable<nsStringHashKey, PRBool> *mPendingStorageEvents;
 
 #ifdef DEBUG
   PRBool mSetOpenerWindowCalled;
 #endif
 
+  nsCOMPtr<nsIDOMOfflineResourceList> mApplicationCache;
+
   friend class nsDOMScriptableHelper;
   friend class nsDOMWindowUtils;
   static nsIFactory *sComputedDOMStyleFactory;
 };
 
 /*
  * nsGlobalChromeWindow inherits from nsGlobalWindow. It is the global
  * object created for a Chrome Window only.
@@ -799,18 +800,16 @@ public:
   }
 
   void LoadingNewDocument();
   nsresult RefreshMIMEArray();
 
 protected:
   nsRefPtr<nsMimeTypeArray> mMimeTypes;
   nsRefPtr<nsPluginArray> mPlugins;
-  nsRefPtr<nsDOMOfflineResourceList> mOfflineResources;
-  nsRefPtr<nsDOMOfflineLoadStatusList> mPendingOfflineLoads;
   nsIDocShell* mDocShell; // weak reference
 
   static jsval       sPrefInternal_id;
 };
 
 class nsIURI;
 
 //*****************************************************************************
--- a/dom/src/offline/Makefile.in
+++ b/dom/src/offline/Makefile.in
@@ -60,17 +60,16 @@ REQUIRES       = xpcom         \
 		 pref          \
 		 prefetch      \
 		 widget        \
 		 xpconnect     \
 		 $(NULL)
 
 CPPSRCS =                              \
 	nsDOMOfflineResourceList.cpp   \
-	nsDOMOfflineLoadStatusList.cpp \
 	$(NULL)
 
 # we don't want the shared lib, but we want to force the creation of a static lib.
 FORCE_STATIC_LIB = 1
 
 LOCAL_INCLUDES = \
 		-I$(srcdir)/../base \
 		-I$(topsrcdir)/content/events/src
deleted file mode 100644
--- a/dom/src/offline/nsDOMOfflineLoadStatusList.h
+++ /dev/null
@@ -1,123 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (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.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code mozilla.org code.
- *
- * The Initial Developer of the Original Code is
- * Mozilla Corporation
- * Portions created by the Initial Developer are Copyright (C) 2007
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  Dave Camp <dcamp@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-#ifndef nsDOMOfflineLoadStatusList_h___
-#define nsDOMOfflineLoadStatusList_h___
-
-#include "nscore.h"
-#include "nsIDOMLoadStatus.h"
-#include "nsIDOMLoadStatusEvent.h"
-#include "nsIDOMLoadStatusList.h"
-#include "nsIOfflineCacheUpdate.h"
-#include "nsCOMPtr.h"
-#include "nsCOMArray.h"
-#include "nsIURI.h"
-#include "nsDOMEvent.h"
-#include "nsIDOMEventTarget.h"
-#include "nsIDOMEventListener.h"
-#include "nsIObserver.h"
-#include "nsWeakReference.h"
-#include "nsIScriptContext.h"
-
-class nsDOMOfflineLoadStatus;
-
-class nsDOMOfflineLoadStatusList : public nsIDOMLoadStatusList,
-                                   public nsIDOMEventTarget,
-                                   public nsIObserver,
-                                   public nsSupportsWeakReference
-{
-public:
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSIDOMLOADSTATUSLIST
-  NS_DECL_NSIDOMEVENTTARGET
-  NS_DECL_NSIOBSERVER
-
-  nsDOMOfflineLoadStatusList(nsIURI *aURI);
-  virtual ~nsDOMOfflineLoadStatusList();
-
-  nsresult Init();
-
-private :
-  nsresult          UpdateAdded         (nsIOfflineCacheUpdate *aUpdate);
-  nsresult          UpdateCompleted     (nsIOfflineCacheUpdate *aUpdate);
-  nsIDOMLoadStatus *FindWrapper         (nsIDOMLoadStatus *aStatus,
-                                         PRUint32 *aIndex);
-  void              NotifyEventListeners(const nsCOMArray<nsIDOMEventListener>& aListeners,
-                                         nsIDOMEvent* aEvent);
-
-  nsresult          SendLoadEvent       (const nsAString& aEventName,
-                                         const nsCOMArray<nsIDOMEventListener>& aListeners,
-                                         nsIDOMLoadStatus *aStatus);
-
-  PRBool mInitialized;
-
-  nsCOMPtr<nsIURI> mURI;
-  nsCOMArray<nsIDOMLoadStatus> mItems;
-  nsCString mHostPort;
-
-  nsCOMPtr<nsIScriptContext> mScriptContext;
-
-  nsCOMArray<nsIDOMEventListener> mLoadRequestedEventListeners;
-  nsCOMArray<nsIDOMEventListener> mLoadCompletedEventListeners;
-  nsCOMArray<nsIDOMEventListener> mUpdateCompletedEventListeners;
-};
-
-class nsDOMLoadStatusEvent : public nsDOMEvent,
-                             public nsIDOMLoadStatusEvent
-{
-public:
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSIDOMLOADSTATUSEVENT
-  NS_FORWARD_NSIDOMEVENT(nsDOMEvent::)
-
-  nsDOMLoadStatusEvent(const nsAString& aEventName, nsIDOMLoadStatus *aStatus)
-    : nsDOMEvent(nsnull, nsnull), mEventName(aEventName), mStatus(aStatus)
-  {
-  }
-
-  virtual ~nsDOMLoadStatusEvent() { }
-
-  nsresult Init();
-
-private:
-  nsAutoString mEventName;
-  nsCOMPtr<nsIDOMLoadStatus> mStatus;
-};
-
-#endif
-
--- a/dom/src/offline/nsDOMOfflineResourceList.cpp
+++ b/dom/src/offline/nsDOMOfflineResourceList.cpp
@@ -45,107 +45,265 @@
 #include "nsNetCID.h"
 #include "nsICacheSession.h"
 #include "nsICacheService.h"
 #include "nsIOfflineCacheSession.h"
 #include "nsIOfflineCacheUpdate.h"
 #include "nsIDOMLoadStatus.h"
 #include "nsAutoPtr.h"
 #include "nsContentUtils.h"
+#include "nsIJSContextStack.h"
+#include "nsEventDispatcher.h"
+#include "nsIPrivateDOMEvent.h"
+#include "nsIObserverService.h"
+#include "nsIScriptGlobalObject.h"
+
+// Event names
+
+#define CHECKING_STR    "checking"
+#define ERROR_STR       "error"
+#define NOUPDATE_STR    "noupdate"
+#define DOWNLOADING_STR "downloading"
+#define PROGRESS_STR    "progress"
+#define CACHED_STR      "cached"
+#define UPDATEREADY_STR "updateready"
 
 // To prevent abuse of the resource list for data storage, the number
 // of offline urls and their length are limited.
 
 static const char kMaxEntriesPref[] =  "offline.max_site_resources";
 #define DEFAULT_MAX_ENTRIES 100
 #define MAX_URI_LENGTH 2048
 
 static nsCAutoString gCachedHostPort;
 static char **gCachedKeys = nsnull;
 static PRUint32 gCachedKeysCount = 0;
 
 //
 // nsDOMOfflineResourceList
 //
 
-NS_INTERFACE_MAP_BEGIN(nsDOMOfflineResourceList)
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsDOMOfflineResourceList)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsDOMOfflineResourceList)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mCacheSession)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mCacheUpdate)
+
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mCheckingListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mErrorListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mNoUpdateListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mDownloadingListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mProgressListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mCachedListeners)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mUpdateReadyListeners)
+
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnCheckingListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnErrorListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnNoUpdateListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnDownloadingListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnProgressListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnCachedListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mOnUpdateReadyListener)
+
+  for (PRUint32 i = 0; i < tmp->mPendingEvents.Length(); i++) {
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mPendingEvents[i].event);
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mPendingEvents[i].listener);
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMARRAY(mPendingEvents[i].listeners);
+  }
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsDOMOfflineResourceList)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mCacheSession)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mCacheUpdate)
+
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mCheckingListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mErrorListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mNoUpdateListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mDownloadingListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mProgressListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mCachedListeners)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mUpdateReadyListeners)
+
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnCheckingListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnErrorListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnNoUpdateListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnDownloadingListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnProgressListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnCachedListener)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnUpdateReadyListener)
+
+  for (PRUint32 i = 0; i < tmp->mPendingEvents.Length(); i++) {
+    NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mPendingEvents[i].event);
+    NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mPendingEvents[i].listener);
+    NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mPendingEvents[i].listeners);
+  }
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMOfflineResourceList)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMOfflineResourceList)
   NS_INTERFACE_MAP_ENTRY(nsIDOMOfflineResourceList)
+  NS_INTERFACE_MAP_ENTRY(nsIOfflineCacheUpdateObserver)
+  NS_INTERFACE_MAP_ENTRY(nsIDOMEventTarget)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
+  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
   NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(OfflineResourceList)
 NS_INTERFACE_MAP_END
 
-NS_IMPL_ADDREF(nsDOMOfflineResourceList)
-NS_IMPL_RELEASE(nsDOMOfflineResourceList)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDOMOfflineResourceList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDOMOfflineResourceList)
 
-nsDOMOfflineResourceList::nsDOMOfflineResourceList(nsIURI *aURI)
+nsDOMOfflineResourceList::nsDOMOfflineResourceList(PRBool aToplevel,
+                                                   nsIURI *aManifestURI,
+                                                   nsIURI *aDocumentURI,
+                                                   nsIDOMWindow *aWindow)
   : mInitialized(PR_FALSE)
-  , mURI(aURI)
+  , mToplevel(aToplevel)
+  , mManifestURI(aManifestURI)
+  , mDocumentURI(aDocumentURI)
 {
+  mWindow = do_GetWeakReference(aWindow);
 }
 
 nsDOMOfflineResourceList::~nsDOMOfflineResourceList()
 {
 }
 
 nsresult
 nsDOMOfflineResourceList::Init()
 {
   if (mInitialized) {
     return NS_OK;
   }
 
-  nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(mURI);
+  if (!mManifestURI) {
+    return NS_ERROR_DOM_INVALID_STATE_ERR;
+  }
+
+  nsresult rv = nsContentUtils::GetSecurityManager()->
+                   CheckSameOriginURI(mManifestURI, mDocumentURI, PR_TRUE);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Dynamically-managed resources are stored as a separate ownership list
+  // from the manifest.
+  rv = mManifestURI->GetSpec(mDynamicOwnerSpec);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mDynamicOwnerSpec.Append("#dynamic");
+
+  nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(mDocumentURI);
   if (!innerURI)
     return NS_ERROR_FAILURE;
 
-  nsresult rv = innerURI->GetHostPort(mHostPort);
+  rv = innerURI->GetHostPort(mHostPort);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsICacheService> serv = do_GetService(NS_CACHESERVICE_CONTRACTID,
                                                  &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsICacheSession> session;
   rv = serv->CreateSession("HTTP-offline",
                            nsICache::STORE_OFFLINE,
                            nsICache::STREAM_BASED,
                            getter_AddRefs(session));
   NS_ENSURE_SUCCESS(rv, rv);
 
   mCacheSession = do_QueryInterface(session, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // Check for in-progress cache updates
+  nsCOMPtr<nsIOfflineCacheUpdateService> cacheUpdateService =
+    do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRUint32 numUpdates;
+  rv = cacheUpdateService->GetNumUpdates(&numUpdates);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (PRUint32 i = 0; i < numUpdates; i++) {
+    nsCOMPtr<nsIOfflineCacheUpdate> cacheUpdate;
+    rv = cacheUpdateService->GetUpdate(i, getter_AddRefs(cacheUpdate));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    UpdateAdded(cacheUpdate);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // watch for new offline cache updates
+  nsCOMPtr<nsIObserverService> observerServ =
+    do_GetService("@mozilla.org/observer-service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = observerServ->AddObserver(this, "offline-cache-update-added", PR_TRUE);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = observerServ->AddObserver(this, "offline-cache-update-completed", PR_TRUE);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   mInitialized = PR_TRUE;
 
   return NS_OK;
 }
 
+void
+nsDOMOfflineResourceList::Disconnect()
+{
+  mCheckingListeners.Clear();
+  mErrorListeners.Clear();
+  mNoUpdateListeners.Clear();
+  mDownloadingListeners.Clear();
+  mProgressListeners.Clear();
+  mCachedListeners.Clear();
+  mUpdateReadyListeners.Clear();
+
+  mOnCheckingListener = nsnull;
+  mOnErrorListener = nsnull;
+  mOnNoUpdateListener = nsnull;
+  mOnDownloadingListener = nsnull;
+  mOnProgressListener = nsnull;
+  mOnCachedListener = nsnull;
+  mOnUpdateReadyListener = nsnull;
+
+  mPendingEvents.Clear();
+}
+
 //
 // nsDOMOfflineResourceList::nsIDOMOfflineResourceList
 //
 
 NS_IMETHODIMP
 nsDOMOfflineResourceList::GetLength(PRUint32 *aLength)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   rv = CacheKeys();
   NS_ENSURE_SUCCESS(rv, rv);
 
   *aLength = gCachedKeysCount;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMOfflineResourceList::Item(PRUint32 aIndex, nsAString& aURI)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   SetDOMStringToNull(aURI);
 
   rv = CacheKeys();
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (aIndex >= gCachedKeysCount)
     return NS_ERROR_NOT_AVAILABLE;
 
@@ -155,123 +313,665 @@ nsDOMOfflineResourceList::Item(PRUint32 
 }
 
 NS_IMETHODIMP
 nsDOMOfflineResourceList::Add(const nsAString& aURI)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   if (aURI.Length() > MAX_URI_LENGTH) return NS_ERROR_DOM_BAD_URI;
 
   // this will fail if the URI is not absolute
   nsCOMPtr<nsIURI> requestedURI;
   rv = NS_NewURI(getter_AddRefs(requestedURI), aURI);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  nsCAutoString scheme;
+  rv = requestedURI->GetScheme(scheme);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRBool match;
+  rv = mManifestURI->SchemeIs(scheme.get(), &match);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!match) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   PRUint32 length;
   rv = GetLength(&length);
   NS_ENSURE_SUCCESS(rv, rv);
   PRUint32 maxEntries = nsContentUtils::GetIntPref(kMaxEntriesPref,
                                                    DEFAULT_MAX_ENTRIES);
 
   if (length > maxEntries) return NS_ERROR_NOT_AVAILABLE;
 
   ClearCachedKeys();
 
   nsCOMPtr<nsIOfflineCacheUpdate> update =
     do_CreateInstance(NS_OFFLINECACHEUPDATE_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = update->Init(PR_TRUE, mHostPort, NS_LITERAL_CSTRING(""), mURI);
+  rv = update->Init(PR_TRUE, mManifestURI, mDocumentURI);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = update->AddURI(requestedURI, nsnull);
+  rv = update->AddDynamicURI(requestedURI);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = update->Schedule();
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMOfflineResourceList::Remove(const nsAString& aURI)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
   nsCAutoString key;
   rv = GetCacheKey(aURI, key);
   NS_ENSURE_SUCCESS(rv, rv);
 
   ClearCachedKeys();
 
-  return mCacheSession->RemoveOwnedKey(mHostPort,
-                                       NS_LITERAL_CSTRING(""),
-                                       key);
+  rv = mCacheSession->RemoveOwnedKey(mHostPort, mDynamicOwnerSpec, key);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mCacheSession->EvictUnownedEntries();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetStatus(PRUint16 *aStatus)
+{
+  nsresult rv = Init();
+
+  // It is OK to check the status without a manifest attribute (you'll
+  // just get "uncached").
+  if (rv == NS_ERROR_DOM_INVALID_STATE_ERR && !mManifestURI) {
+    *aStatus = nsIDOMOfflineResourceList::UNCACHED;
+    return NS_OK;
+  }
+
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (mCacheUpdate) {
+    return mCacheUpdate->GetStatus(aStatus);
+  }
+
+  // XXX: the spec allows either UNCACHED or IDLE, depending on whether
+  // the application is associated with a specific offline cache.  Until
+  // we have versioned application caches, the best approximation is
+  // probably IDLE if the offline-app permission is set, and UNCACHED
+  // otherwise.
+
+  if (nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    *aStatus = nsIDOMOfflineResourceList::IDLE;
+  } else {
+    *aStatus = nsIDOMOfflineResourceList::UNCACHED;
+  }
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP
-nsDOMOfflineResourceList::Has(const nsAString& aURI, PRBool *aExists)
+nsDOMOfflineResourceList::Update()
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
+  nsCOMPtr<nsIOfflineCacheUpdateService> updateService =
+    do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIOfflineCacheUpdate> update;
+  rv = updateService->ScheduleUpdate(mManifestURI, mDocumentURI,
+                                     getter_AddRefs(update));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SwapCache()
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!nsContentUtils::OfflineAppAllowed(mDocumentURI)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+//
+// nsDOMOfflineResourceList::nsIDOMEventTarget
+//
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOnchecking(nsIDOMEventListener **aOnchecking)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ENSURE_ARG_POINTER(aOnchecking);
+  NS_IF_ADDREF(*aOnchecking = mOnCheckingListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOnchecking(nsIDOMEventListener *aOnchecking)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnCheckingListener = aOnchecking;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOnerror(nsIDOMEventListener **aOnerror)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCAutoString key;
-  rv = GetCacheKey(aURI, key);
+  NS_ENSURE_ARG_POINTER(aOnerror);
+  NS_IF_ADDREF(*aOnerror = mOnErrorListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOnerror(nsIDOMEventListener *aOnerror)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnErrorListener = aOnerror;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOnnoupdate(nsIDOMEventListener **aOnnoupdate)
+{
+  nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  return mCacheSession->KeyIsOwned(mHostPort, NS_LITERAL_CSTRING(""),
-                                   key, aExists);
+  NS_ENSURE_ARG_POINTER(aOnnoupdate);
+  NS_IF_ADDREF(*aOnnoupdate = mOnNoUpdateListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOnnoupdate(nsIDOMEventListener *aOnnoupdate)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnNoUpdateListener = aOnnoupdate;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOndownloading(nsIDOMEventListener **aOndownloading)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ENSURE_ARG_POINTER(aOndownloading);
+  NS_IF_ADDREF(*aOndownloading = mOnDownloadingListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOndownloading(nsIDOMEventListener *aOndownloading)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnDownloadingListener = aOndownloading;
+  return NS_OK;
 }
 
 NS_IMETHODIMP
-nsDOMOfflineResourceList::Clear()
+nsDOMOfflineResourceList::GetOnprogress(nsIDOMEventListener **aOnprogress)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ENSURE_ARG_POINTER(aOnprogress);
+  NS_IF_ADDREF(*aOnprogress = mOnProgressListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOnprogress(nsIDOMEventListener *aOnprogress)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnProgressListener = aOnprogress;
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOnupdateready(nsIDOMEventListener **aOnupdateready)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ENSURE_ARG_POINTER(aOnupdateready);
+  NS_IF_ADDREF(*aOnupdateready = mOnUpdateReadyListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOncached(nsIDOMEventListener *aOncached)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnCachedListener = aOncached;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::GetOncached(nsIDOMEventListener **aOncached)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_ENSURE_ARG_POINTER(aOncached);
+  NS_IF_ADDREF(*aOncached = mOnCachedListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::SetOnupdateready(nsIDOMEventListener *aOnupdateready)
+{
+  nsresult rv = Init();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mOnUpdateReadyListener = aOnupdateready;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::AddEventListener(const nsAString& aType,
+                                           nsIDOMEventListener *aListener,
+                                           PRBool aUseCapture)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  ClearCachedKeys();
+  NS_ENSURE_ARG(aListener);
+
+  nsCOMArray<nsIDOMEventListener> *array;
+
+#define IMPL_ADD_LISTENER(_type, _member)    \
+  if (aType.EqualsLiteral(_type)) {           \
+    array = &(_member);                      \
+  } else
 
-  return mCacheSession->SetOwnedKeys(mHostPort,
-                                     NS_LITERAL_CSTRING(""),
-                                     0, nsnull);
+  IMPL_ADD_LISTENER(CHECKING_STR, mCheckingListeners)
+  IMPL_ADD_LISTENER(ERROR_STR, mErrorListeners)
+  IMPL_ADD_LISTENER(NOUPDATE_STR, mNoUpdateListeners)
+  IMPL_ADD_LISTENER(DOWNLOADING_STR, mDownloadingListeners)
+  IMPL_ADD_LISTENER(PROGRESS_STR, mProgressListeners)
+  IMPL_ADD_LISTENER(CACHED_STR, mCachedListeners)
+  IMPL_ADD_LISTENER(UPDATEREADY_STR, mUpdateReadyListeners)
+  {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  array->AppendObject(aListener);
+#undef IMPL_ADD_LISTENER
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP
-nsDOMOfflineResourceList::Refresh()
+nsDOMOfflineResourceList::RemoveEventListener(const nsAString &aType,
+                                              nsIDOMEventListener *aListener,
+                                              PRBool aUseCapture)
 {
   nsresult rv = Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<nsIOfflineCacheUpdate> update =
-    do_CreateInstance(NS_OFFLINECACHEUPDATE_CONTRACTID, &rv);
+  NS_ENSURE_ARG(aListener);
+
+  nsCOMArray<nsIDOMEventListener> *array;
+
+#define IMPL_REMOVE_LISTENER(_type, _member)  \
+  if (aType.EqualsLiteral(_type)) {            \
+    array = &(_member);                       \
+  } else
+
+  IMPL_REMOVE_LISTENER(CHECKING_STR, mCheckingListeners)
+  IMPL_REMOVE_LISTENER(ERROR_STR, mErrorListeners)
+  IMPL_REMOVE_LISTENER(NOUPDATE_STR, mNoUpdateListeners)
+  IMPL_REMOVE_LISTENER(DOWNLOADING_STR, mDownloadingListeners)
+  IMPL_REMOVE_LISTENER(PROGRESS_STR, mProgressListeners)
+  IMPL_REMOVE_LISTENER(CACHED_STR, mCachedListeners)
+  IMPL_REMOVE_LISTENER(UPDATEREADY_STR, mUpdateReadyListeners)
+  {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  // Allow a caller to remove O(N^2) behavior by removing end-to-start.
+  for (PRUint32 i = array->Count() - 1; i != PRUint32(-1); --i) {
+    if (array->ObjectAt(i) == aListener) {
+      array->RemoveObjectAt(i);
+      break;
+    }
+  }
+
+#undef IMPL_REMOVE_LISTENER
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::DispatchEvent(nsIDOMEvent *evt, PRBool *_retval)
+{
+  // Ignored
+
+  return NS_OK;
+}
+
+void
+nsDOMOfflineResourceList::NotifyEventListeners(nsIDOMEventListener *aListener,
+                                               const nsCOMArray<nsIDOMEventListener>& aListeners,
+                                               nsIDOMEvent* aEvent)
+{
+  // XXXbz wouldn't it be easier to just have an actual nsEventListenerManager
+  // to work with or something?  I feel like we're duplicating code here...
+  //
+  // (and this was duplicated from XMLHttpRequest)
+  if (!aEvent)
+    return;
+
+  nsCOMPtr<nsIJSContextStack> stack;
+  JSContext *cx = nsnull;
+
+  nsCOMPtr<nsIScriptGlobalObject> scriptGlobal = do_QueryReferent(mWindow);
+  if (!scriptGlobal)
+    return;
+
+  nsCOMPtr<nsIScriptContext> context = scriptGlobal->GetContext();
+  if (context) {
+    stack = do_GetService("@mozilla.org/js/xpc/ContextStack;1");
+
+    if (stack) {
+      cx = (JSContext *)context->GetNativeContext();
+
+      if (cx) {
+        stack->Push(cx);
+      }
+    }
+  }
+
+  if (aListener) {
+    aListener->HandleEvent(aEvent);
+  }
+
+  PRInt32 count = aListeners.Count();
+  for (PRInt32 index = 0; index < count; ++index) {
+    nsIDOMEventListener* listener = aListeners[index];
+
+    if (listener) {
+      listener->HandleEvent(aEvent);
+    }
+  }
+
+  if (cx) {
+    stack->Pop(&cx);
+  }
+}
+
+void
+nsDOMOfflineResourceList::FirePendingEvents()
+{
+  for (PRUint32 i = 0; i < mPendingEvents.Length(); i++) {
+    const PendingEvent &pending = mPendingEvents[i];
+    NotifyEventListeners(pending.listener, pending.listeners, pending.event);
+  }
+  mPendingEvents.Clear();
+}
+
+nsresult
+nsDOMOfflineResourceList::SendEvent(const nsAString &aEventName,
+                                    nsIDOMEventListener *aListener,
+                                    const nsCOMArray<nsIDOMEventListener> &aListeners)
+{
+  // Only toplevel windows get application cache events.
+  if (!mToplevel) {
+    return NS_OK;
+  }
+
+  if (!aListener && aListeners.Count() == 0) {
+    return NS_OK;
+  }
+
+  // Don't send events to closed windows
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
+  if (!window) {
+    return NS_OK;
+  }
+
+  if (!window->GetDocShell()) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIDOMEvent> event;
+  nsresult rv = nsEventDispatcher::CreateEvent(nsnull, nsnull,
+                                               NS_LITERAL_STRING("Events"),
+                                               getter_AddRefs(event));
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = update->Init(PR_FALSE, mHostPort, NS_LITERAL_CSTRING(""), mURI);
-  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<nsIPrivateDOMEvent> privevent = do_QueryInterface(event);
+  if (!privevent) {
+    return NS_ERROR_FAILURE;
+  }
+
+  event->InitEvent(aEventName, PR_FALSE, PR_TRUE);
+
+  privevent->SetTarget(this);
+  privevent->SetCurrentTarget(this);
+  privevent->SetOriginalTarget(this);
+
+  // We assume anyone that managed to call SendEvent is trusted
+  privevent->SetTrusted(PR_TRUE);
+
+  // If the window is frozen or we're still catching up on events that were
+  // queued while frozen, save the event for later.
+  if (window->IsFrozen() || mPendingEvents.Length() > 0) {
+    PendingEvent *pending = mPendingEvents.AppendElement();
+    pending->event = event;
+    pending->listener = aListener;
+    pending->listeners.SetCapacity(aListeners.Count());
+    pending->listeners.AppendObjects(aListeners);
+
+    return NS_OK;
+  }
+
+  NotifyEventListeners(aListener, aListeners, event);
+
+  return NS_OK;
+}
+
+
+//
+// nsDOMOfflineResourceList::nsIObserver
+//
+NS_IMETHODIMP
+nsDOMOfflineResourceList::Observe(nsISupports *aSubject,
+                                    const char *aTopic,
+                                    const PRUnichar *aData)
+{
+  if (!strcmp(aTopic, "offline-cache-update-added")) {
+    nsCOMPtr<nsIOfflineCacheUpdate> update = do_QueryInterface(aSubject);
+    if (update) {
+      UpdateAdded(update);
+    }
+  } else if (!strcmp(aTopic, "offline-cache-update-completed")) {
+    nsCOMPtr<nsIOfflineCacheUpdate> update = do_QueryInterface(aSubject);
+    if (update) {
+      UpdateCompleted(update);
+    }
+  }
 
-  rv = update->Schedule();
-  NS_ENSURE_SUCCESS(rv, rv);
+  return NS_OK;
+}
+
+//
+// nsDOMOfflineResourceList::nsIOfflineCacheUpdateObserver
+//
+NS_IMETHODIMP
+nsDOMOfflineResourceList::Error(nsIOfflineCacheUpdate *aUpdate)
+{
+  SendEvent(NS_LITERAL_STRING(ERROR_STR), mOnErrorListener, mErrorListeners);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::Checking(nsIOfflineCacheUpdate *aUpdate)
+{
+  SendEvent(NS_LITERAL_STRING(CHECKING_STR),
+            mOnCheckingListener, mCheckingListeners);
+  return NS_OK;
+}
 
-  return rv;
+NS_IMETHODIMP
+nsDOMOfflineResourceList::NoUpdate(nsIOfflineCacheUpdate *aUpdate)
+{
+  SendEvent(NS_LITERAL_STRING(NOUPDATE_STR),
+            mOnNoUpdateListener, mNoUpdateListeners);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::Downloading(nsIOfflineCacheUpdate *aUpdate)
+{
+  SendEvent(NS_LITERAL_STRING(DOWNLOADING_STR),
+            mOnDownloadingListener, mDownloadingListeners);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::ItemStarted(nsIOfflineCacheUpdate *aUpdate,
+                                      nsIDOMLoadStatus *aItem)
+{
+  SendEvent(NS_LITERAL_STRING(PROGRESS_STR),
+            mOnProgressListener, mProgressListeners);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMOfflineResourceList::ItemCompleted(nsIOfflineCacheUpdate *aUpdate,
+                                        nsIDOMLoadStatus *aItem)
+{
+  return NS_OK;
 }
 
 nsresult
 nsDOMOfflineResourceList::GetCacheKey(const nsAString &aURI, nsCString &aKey)
 {
   nsCOMPtr<nsIURI> requestedURI;
   nsresult rv = NS_NewURI(getter_AddRefs(requestedURI), aURI);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return GetCacheKey(requestedURI, aKey);
 }
 
 nsresult
+nsDOMOfflineResourceList::UpdateAdded(nsIOfflineCacheUpdate *aUpdate)
+{
+  // Ignore partial updates.
+  PRBool partial;
+  nsresult rv = aUpdate->GetPartial(&partial);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (partial) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIURI> updateURI;
+  rv = aUpdate->GetManifestURI(getter_AddRefs(updateURI));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRBool equals;
+  rv = updateURI->Equals(mManifestURI, &equals);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!equals) {
+    // This update doesn't belong to us
+    return NS_OK;
+  }
+
+  NS_ENSURE_TRUE(!mCacheUpdate, NS_ERROR_FAILURE);
+
+  // We don't need to emit signals here.  Updates are either added
+  // when they are scheduled (in which case they are always IDLE) or
+  // they are added when the applicationCache object is initialized, so there
+  // are no listeners to accept signals anyway.
+
+  mCacheUpdate = aUpdate;
+  mCacheUpdate->AddObserver(this, PR_TRUE);
+
+  return NS_OK;
+}
+
+nsresult
+nsDOMOfflineResourceList::UpdateCompleted(nsIOfflineCacheUpdate *aUpdate)
+{
+  if (aUpdate != mCacheUpdate) {
+    // This isn't the update we're watching.
+    return NS_OK;
+  }
+
+  PRBool succeeded;
+  nsresult rv = mCacheUpdate->GetSucceeded(&succeeded);
+
+  mCacheUpdate->RemoveObserver(this);
+  mCacheUpdate = nsnull;
+
+  if (NS_SUCCEEDED(rv) && succeeded) {
+    // XXX: the spec requires a "cached" event to be sent if this is a
+    // first-time cache attempt, and "updateready" if this page was loaded
+    // from an existing application cache.  Since we don't have versioned
+    // application caches yet, basically each update acts like a first-time
+    // update, so we'll always fire "cached" for now.
+    SendEvent(NS_LITERAL_STRING(CACHED_STR),
+              mOnCachedListener, mCachedListeners);
+  }
+
+  return NS_OK;
+}
+
+nsresult
 nsDOMOfflineResourceList::GetCacheKey(nsIURI *aURI, nsCString &aKey)
 {
   nsresult rv = aURI->GetSpec(aKey);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // url fragments aren't used in cache keys
   nsCAutoString::const_iterator specStart, specEnd;
   aKey.BeginReading(specStart);
@@ -287,17 +987,17 @@ nsDOMOfflineResourceList::GetCacheKey(ns
 nsresult
 nsDOMOfflineResourceList::CacheKeys()
 {
   if (gCachedKeys && mHostPort == gCachedHostPort)
     return NS_OK;
 
   ClearCachedKeys();
 
-  nsresult rv = mCacheSession->GetOwnedKeys(mHostPort, NS_LITERAL_CSTRING(""),
+  nsresult rv = mCacheSession->GetOwnedKeys(mHostPort, mDynamicOwnerSpec,
                                             &gCachedKeysCount, &gCachedKeys);
 
   if (NS_SUCCEEDED(rv))
     gCachedHostPort = mHostPort;
 
   return rv;
 }
 
--- a/dom/src/offline/nsDOMOfflineResourceList.h
+++ b/dom/src/offline/nsDOMOfflineResourceList.h
@@ -37,38 +37,105 @@
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef nsDOMOfflineResourceList_h___
 #define nsDOMOfflineResourceList_h___
 
 #include "nscore.h"
 #include "nsIDOMOfflineResourceList.h"
 #include "nsIOfflineCacheSession.h"
+#include "nsIOfflineCacheUpdate.h"
 #include "nsTArray.h"
 #include "nsString.h"
 #include "nsIURI.h"
 #include "nsCOMPtr.h"
+#include "nsWeakReference.h"
+#include "nsCOMArray.h"
+#include "nsIDOMEventListener.h"
+#include "nsIDOMEventTarget.h"
+#include "nsDOMEvent.h"
+#include "nsIObserver.h"
+#include "nsIScriptContext.h"
+#include "nsCycleCollectionParticipant.h"
 
-class nsDOMOfflineResourceList : public nsIDOMOfflineResourceList
+class nsIDOMWindow;
+
+class nsDOMOfflineResourceList : public nsIDOMOfflineResourceList,
+                                 public nsIObserver,
+                                 public nsIOfflineCacheUpdateObserver,
+                                 public nsIDOMEventTarget,
+                                 public nsSupportsWeakReference
 {
 public:
-  NS_DECL_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIDOMOFFLINERESOURCELIST
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER
+  NS_DECL_NSIDOMEVENTTARGET
+
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsDOMOfflineResourceList,
+                                           nsIDOMOfflineResourceList)
 
-  nsDOMOfflineResourceList(nsIURI* aURI);
+  nsDOMOfflineResourceList(PRBool aToplevel,
+                           nsIURI* aManifestURI,
+                           nsIURI* aDocumentURI,
+                           nsIDOMWindow* aWindow);
   virtual ~nsDOMOfflineResourceList();
 
+  void FirePendingEvents();
+  void Disconnect();
+
 private:
   nsresult Init();
 
+  void NotifyEventListeners(nsIDOMEventListener *aListener,
+                            const nsCOMArray<nsIDOMEventListener>& aListeners,
+                            nsIDOMEvent* aEvent);
+
+  nsresult SendEvent(const nsAString &aEventName,
+                     nsIDOMEventListener *aListener,
+                     const nsCOMArray<nsIDOMEventListener> &aListeners);
+
+  nsresult UpdateAdded(nsIOfflineCacheUpdate *aUpdate);
+  nsresult UpdateCompleted(nsIOfflineCacheUpdate *aUpdate);
+
   nsresult GetCacheKey(const nsAString &aURI, nsCString &aKey);
   nsresult GetCacheKey(nsIURI *aURI, nsCString &aKey);
 
   nsresult CacheKeys();
   void ClearCachedKeys();
 
   PRBool mInitialized;
-  nsCOMPtr<nsIURI> mURI;
+  PRBool mToplevel;
+  nsCOMPtr<nsIURI> mManifestURI;
+  nsCOMPtr<nsIURI> mDocumentURI;
+  nsCOMPtr<nsIWeakReference> mWindow;
   nsCOMPtr<nsIOfflineCacheSession> mCacheSession;
+  nsCOMPtr<nsIOfflineCacheUpdate> mCacheUpdate;
   nsCAutoString mHostPort;
+  nsCAutoString mDynamicOwnerSpec;
+
+  nsCOMArray<nsIDOMEventListener> mCheckingListeners;
+  nsCOMArray<nsIDOMEventListener> mErrorListeners;
+  nsCOMArray<nsIDOMEventListener> mNoUpdateListeners;
+  nsCOMArray<nsIDOMEventListener> mDownloadingListeners;
+  nsCOMArray<nsIDOMEventListener> mProgressListeners;
+  nsCOMArray<nsIDOMEventListener> mCachedListeners;
+  nsCOMArray<nsIDOMEventListener> mUpdateReadyListeners;
+
+  nsCOMPtr<nsIDOMEventListener> mOnCheckingListener;
+  nsCOMPtr<nsIDOMEventListener> mOnErrorListener;
+  nsCOMPtr<nsIDOMEventListener> mOnNoUpdateListener;
+  nsCOMPtr<nsIDOMEventListener> mOnDownloadingListener;
+  nsCOMPtr<nsIDOMEventListener> mOnProgressListener;
+  nsCOMPtr<nsIDOMEventListener> mOnCachedListener;
+  nsCOMPtr<nsIDOMEventListener> mOnUpdateReadyListener;
+
+  struct PendingEvent {
+    nsCOMPtr<nsIDOMEvent> event;
+    nsCOMPtr<nsIDOMEventListener> listener;
+    nsCOMArray<nsIDOMEventListener> listeners;
+  };
+
+  nsTArray<PendingEvent> mPendingEvents;
 };
 
 #endif
--- a/dom/tests/mochitest/ajax/offline/Makefile.in
+++ b/dom/tests/mochitest/ajax/offline/Makefile.in
@@ -41,18 +41,26 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= dom/tests/mochitest/ajax/offline
 
 include $(DEPTH)/config/autoconf.mk
 
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES	= \
-        test_pendingOfflineLoads.html \
+	offlineTests.js \
+	test_badManifestMagic.html \
+	test_badManifestMime.html \
+	test_missingFile.html \
+	test_simpleManifest.html \
+	test_offlineIFrame.html \
+	badManifestMagic.cacheManifest \
+	badManifestMagic.cacheManifest^headers^ \
+	missingFile.cacheManifest \
+	missingFile.cacheManifest^headers^ \
+	simpleManifest.cacheManifest \
+	simpleManifest.cacheManifest^headers^ \
+	simpleManifest.notmanifest \
+	offlineChild.html \
         $(NULL)
 
-# XXX: disabled pending fixes
-#        test_isLocallyAvailable.html \
-#        test_offlineResources.html
-
-
 libs::	$(_TEST_FILES)
-	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
\ No newline at end of file
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/badManifestMagic.cacheManifest
@@ -0,0 +1,4 @@
+# This doesn't start with the magic cache manifest line.
+http://localhost:8888/tests/SimpleTest/SimpleTest.js
+http://localhost:8888/MochiKit/packed.js
+http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/badManifestMagic.cacheManifest^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/missingFile.cacheManifest
@@ -0,0 +1,7 @@
+CACHE MANIFEST
+http://localhost:8888/tests/SimpleTest/SimpleTest.js
+http://localhost:8888/MochiKit/packed.js
+http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js
+
+# The following item doesn't exist, and will cause an update error.
+http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/doesntExist.html
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/missingFile.cacheManifest^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/offlineChild.html
@@ -0,0 +1,34 @@
+<html>
+<head>
+<title></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">
+
+// This is called by the parent when the offline update is complete
+// (since we don't get those events)
+function doneLoading()
+{
+  window.top.childFinished();
+}
+
+if (OfflineTest.setupChild()) {
+  // Child frames shouldn't be receiving cache events.
+  applicationCache.onerror = OfflineTest.failEvent;
+  applicationCache.onnoupdate = OfflineTest.failEvent;
+  applicationCache.ondownloading = OfflineTest.failEvent;
+  applicationCache.onprogress = OfflineTest.failEvent;
+  applicationCache.onupdateready = OfflineTest.failEvent;
+  applicationCache.oncached = OfflineTest.failEvent;
+}
+</script>
+
+</head>
+
+<body>
+
+</body> </html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/offlineTests.js
@@ -0,0 +1,211 @@
+// Utility functions for offline tests.
+netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+var OfflineTest = {
+
+_slaveWindow: null,
+
+// The window where test results should be sent.
+_masterWindow: null,
+
+setupChild: function()
+{
+  if (window.parent.OfflineTest.hasSlave()) {
+    return false;
+  }
+
+  this._slaveWindow = null;
+  this._masterWindow = window.top;
+
+  return true;
+},
+
+// Setup the tests.  This will reload the current page in a new window
+// if necessary.
+setup: function()
+{
+  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+  if (!window.opener || !window.opener.OfflineTest ||
+      !window.opener.OfflineTest._isMaster) {
+    // Offline applications must be toplevel windows and have the
+    // offline-app permission.  Because we were loaded without the
+    // offline-app permission and (probably) in an iframe, we need to
+    // enable the pref and spawn a new window to perform the actual
+    // tests.  It will use this window to report successes and
+    // failures.
+    var pm = Cc["@mozilla.org/permissionmanager;1"]
+      .getService(Ci.nsIPermissionManager);
+    var uri = Cc["@mozilla.org/network/io-service;1"]
+      .getService(Ci.nsIIOService)
+      .newURI(window.location.href, null, null);
+    pm.add(uri, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
+
+    // Tests must run as toplevel windows.  Open a slave window to run
+    // the test.
+    this._isMaster = true;
+    this._slaveWindow = window.open(window.location, "offlinetest");
+
+    this._slaveWindow._OfflineSlaveWindow = true;
+
+    return false;
+  }
+
+  this._masterWindow = window.opener;
+
+  return true;
+},
+
+teardown: function()
+{
+  // Remove the offline-app permission we gave ourselves.
+
+  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+  var pm = Cc["@mozilla.org/permissionmanager;1"]
+           .getService(Ci.nsIPermissionManager);
+  var uri = Cc["@mozilla.org/network/io-service;1"]
+            .getService(Ci.nsIIOService)
+            .newURI(window.location.href, null, null);
+  pm.remove(uri.host, "offline-app");
+
+  this.clear();
+},
+
+finish: function()
+{
+  SimpleTest.finish();
+
+  if (this._masterWindow) {
+    this._masterWindow.OfflineTest.finish();
+    window.close();
+  }
+},
+
+hasSlave: function()
+{
+  return (this._slaveWindow != null);
+},
+
+//
+// Mochitest wrappers - These forward tests to the proper mochitest window.
+//
+ok: function(condition, name, diag)
+{
+  return this._masterWindow.SimpleTest.ok(condition, name, diag);
+},
+
+is: function(a, b, name)
+{
+  return this._masterWindow.SimpleTest.is(a, b, name);
+},
+
+clear: function()
+{
+  // Clear the ownership list
+  var cacheService = Cc["@mozilla.org/network/cache-service;1"]
+                     .getService(Ci.nsICacheService);
+  var cacheSession = cacheService.createSession("HTTP-offline",
+                                                Ci.nsICache.STORE_OFFLINE,
+                                                true)
+                     .QueryInterface(Ci.nsIOfflineCacheSession);
+
+  // Clear manifest-owned urls
+  cacheSession.setOwnedKeys(window.location.host,
+                            this.getManifestUrl() + "#manifest", 0, []);
+
+  // Clear dynamically-owned urls
+  cacheSession.setOwnedKeys(window.location.host,
+                            this.getManifestUrl() + "#dynamic", 0, []);
+
+  cacheSession.evictUnownedEntries();
+},
+
+failEvent: function(e)
+{
+  OfflineTest.ok(false, "Unexpected event: " + e.type);
+},
+
+// The offline API as specified has no way to watch the load of a resource
+// added with applicationCache.add().
+waitForAdd: function(url, onFinished) {
+  // Check every half second for ten seconds.
+  var numChecks = 20;
+  var waitFunc = function() {
+    var cacheService = Cc["@mozilla.org/network/cache-service;1"]
+    .getService(Ci.nsICacheService);
+    var cacheSession = cacheService.createSession("HTTP-offline",
+                                                  Ci.nsICache.STORE_OFFLINE,
+                                                  true);
+    var entry;
+    try {
+      var entry = cacheSession.openCacheEntry(url, Ci.nsICache.ACCESS_READ, true);
+    } catch (e) {
+    }
+
+    if (entry) {
+      entry.close();
+      onFinished();
+      return;
+    }
+
+    if (--numChecks == 0) {
+      onFinished();
+      return;
+    }
+
+    setTimeout(OfflineTest.priv(waitFunc), 500);
+  }
+
+  setTimeout(this.priv(waitFunc), 500);
+},
+
+getManifestUrl: function()
+{
+  return window.top.document.documentElement.getAttribute("manifest");
+},
+
+priv: function(func)
+{
+  var self = this;
+  return function() {
+    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+    func();
+  }
+},
+
+checkCache: function(url, expectEntry)
+{
+  var cacheService = Cc["@mozilla.org/network/cache-service;1"]
+  .getService(Ci.nsICacheService);
+  var cacheSession = cacheService.createSession("HTTP-offline",
+                                                Ci.nsICache.STORE_OFFLINE,
+                                                true);
+  try {
+    var entry = cacheSession.openCacheEntry(url, Ci.nsICache.ACCESS_READ, true);
+    if (expectEntry) {
+      this.ok(true, url + " should exist in the offline cache");
+    } else {
+      this.ok(false, url + " should not exist in the offline cache");
+    }
+    entry.close();
+  } catch (e) {
+    // this constant isn't in Components.results
+    const kNetBase = 2152398848; // 0x804B0000
+    var NS_ERROR_CACHE_KEY_NOT_FOUND = kNetBase + 61
+    if (e.result == NS_ERROR_CACHE_KEY_NOT_FOUND) {
+      if (expectEntry) {
+        this.ok(false, url + " should exist in the offline cache");
+      } else {
+        this.ok(true, url + " should not exist in the offline cache");
+      }
+    } else {
+      throw e;
+    }
+  }
+}
+
+};
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest
@@ -0,0 +1,12 @@
+CACHE MANIFEST
+http://localhost:8888/tests/SimpleTest/SimpleTest.js
+http://localhost:8888/MochiKit/packed.js
+http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js
+
+# The following item doesn't have the same scheme as the manifest, and
+# will be ignored.
+https://localhost:8888/MochiKit/packed.js
+
+# The following item is not a valid URI and will be ignored
+bad:/uri/invalid
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/cache-manifest
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/simpleManifest.notmanifest
@@ -0,0 +1,13 @@
+CACHE MANIFEST
+
+http://localhost:8888/tests/SimpleTest/SimpleTest.js
+http://localhost:8888/MochiKit/packed.js
+http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js
+
+# The following item doesn't have the same scheme as the manifest, and
+# will be ignored.
+https://localhost:8888/MochiKit/packed.js
+
+# The following item is not a valid URI and will be ignored
+bad:/uri/invalid
+
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_badManifestMagic.html
@@ -0,0 +1,50 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/badManifestMagic.cacheManifest">
+<head>
+<title>bad manifest magic</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">
+
+var gGotChecking = false;
+
+function handleError() {
+  OfflineTest.ok(gGotChecking, "Expected checking event");
+  OfflineTest.ok(true, "Expected error event");
+
+  // These items are listed in the manifest, but the error should have
+  // prevented them from being committed to the cache.
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", false);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", false);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", false);
+
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
+
+if (OfflineTest.setup()) {
+  // Don't expect a bunch of events.
+  applicationCache.ondownloading = OfflineTest.failEvent;
+  applicationCache.onupdateready = OfflineTest.failEvent;
+  applicationCache.oncached = OfflineTest.failEvent;
+  applicationCache.onnoupdate = OfflineTest.failEvent;
+  applicationCache.onprogress = OfflineTest.failEvent;
+
+  // ... but expect 'checking' and 'error'
+  applicationCache.onchecking = function() { gGotChecking = true; };
+  applicationCache.onerror = OfflineTest.priv(handleError);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_badManifestMime.html
@@ -0,0 +1,49 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/simpleManifest.notmanifest">
+<head>
+<title>bad manifest content type</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">
+
+var gGotChecking = false;
+
+function handleError() {
+  OfflineTest.ok(gGotChecking, "Expected checking event");
+  OfflineTest.ok(true, "Expected error event");
+
+  // These items are listed in the manifest, but the error should have
+  // prevented them from being committed to the cache.
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", false);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", false);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", false);
+
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
+if (OfflineTest.setup()) {
+  // Don't expect a bunch of events.
+  applicationCache.ondownloading = OfflineTest.failEvent;
+  applicationCache.onupdateready = OfflineTest.failEvent;
+  applicationCache.oncached = OfflineTest.failEvent;
+  applicationCache.onnoupdate = OfflineTest.failEvent;
+  applicationCache.onprogress = OfflineTest.failEvent;
+
+  // ... but expect 'checking' and 'error'
+  applicationCache.onchecking = function() { gGotChecking = true; };
+  applicationCache.onerror = OfflineTest.priv(handleError);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/offline/test_isLocallyAvailable.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-<title>navigator.isLocallyAvailable Test</title>
-
-<link rel="offline-resource" href="http://localhost:8888/tests/SimpleTest/SimpleTest.js">
-
-<script type="text/javascript" src="/MochiKit/packed.js"></script>
-<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-
-<script type="text/javascript">
-
-function run_test()
-{
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
-  // check invalid urls
-  try {
-    navigator.isLocallyAvailable("http://different.origin.com", false);
-    ok(false, "can't check from a different origin");
-  } catch(e) {
-    ok(true, "can't check from a different origin");
-  }
-
-  try {
-    navigator.isLocallyAvailable("ftp://localhost/blah/blah", false);
-    ok(false, "urls must be http or https");
-  } catch(e) {
-    ok(true, "urls must be http or https");
-  }
-
-  // check an URL that should definitely not be there
-  ok(navigator.isLocallyAvailable("http://localhost:8888/blah/blah/blah",
-                                  false) == false,
-     "unknown item shouldn't be available online");
-  ok(navigator.isLocallyAvailable("http://localhost:8888/blah/blah/blah",
-                                  true) == false,
-     "unknown item shouldn't be available offline");
-
-  // check a URL that should be available on and offline
-  var url = "http://localhost:8888/tests/SimpleTest/SimpleTest.js";
-  ok(navigator.isLocallyAvailable(url, false) == true,
-     url + " should be available online");
-  ok(navigator.isLocallyAvailable(url, true) == true,
-     url + " should be available offline");
-
-  // Pull it out of the disk cache
-  var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-    .getService(Components.interfaces.nsICacheService);
-  var session = cacheService.createSession
-    ("HTTP",
-     Components.interfaces.nsICache.STORE_ON_DISK,
-     Components.interfaces.nsICache.STREAM_BASED);
-  var entry = session.openCacheEntry(url, Components.interfaces.nsICache.ACCESS_WRITE, false);
-  entry.doom();
-  entry.close();
-
-  // Now it should be available offline but not online
-  ok(navigator.isLocallyAvailable(url, false) == false,
-     url + " should not be available online");
-
-  ok(navigator.isLocallyAvailable(url, true) == true,
-     url + " should be available offline");
-
-  // Clear the offline cache/ownership on the way out
-  var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-    .getService(Components.interfaces.nsICacheService);
-  var cacheSession = cacheService.createSession("HTTP-offline",
-                                                Components.interfaces.nsICache.STORE_OFFLINE,
-                                                true)
-    .QueryInterface(Components.interfaces.nsIOfflineCacheSession);
-  cacheSession.setOwnedKeys(window.location.host,
-                            window.location.protocol + "//" + window.location.host + window.location.pathname,
-                            0, []);
-  cacheSession.setOwnedKeys(window.location.host, "", 0, []);
-  cacheSession.evictUnownedEntries();
-
-  cacheService.evictEntries(Components.interfaces.nsICache.STORE_OFFLINE);
-  SimpleTest.finish();
-}
-
-// Give the offline resources some time to load.
-setTimeout("run_test()", 1000);
-
-SimpleTest.waitForExplicitFinish();
-
-</script>
-
-</head>
-<body>
-
-
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_missingFile.html
@@ -0,0 +1,50 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/missingFile.cacheManifest">
+<head>
+<title>missing manifest file 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">
+
+var gGotChecking = false;
+var gGotDownloading = false;
+
+function handleError() {
+  OfflineTest.ok(gGotChecking, "Expected checking event");
+  OfflineTest.ok(gGotDownloading, "Expected downloading event");
+  OfflineTest.ok(true, "Expected error event");
+
+  // These items are listed in the manifest, but the error should have
+  // prevented them from being committed to the cache.
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", false);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", false);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", false);
+
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
+if (OfflineTest.setup()) {
+  // Don't expect any "success" events.
+  applicationCache.onupdateready = function() { OfflineTest.failEvent("updateready"); }
+  applicationCache.oncached = function() { OfflineTest.failEvent("cached"); }
+  applicationCache.onnoupdate = function() { OfflineTest.failEvent("noupdate"); }
+
+  applicationCache.ondownloading = function() { gGotDownloading = true; };
+  applicationCache.onchecking = function() { gGotChecking = true; };
+  applicationCache.onerror = OfflineTest.priv(handleError);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_offlineIFrame.html
@@ -0,0 +1,47 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest">
+<head>
+<title>offline iframe 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 childFinished()
+{
+  OfflineTest.teardown();
+  OfflineTest.finish();
+}
+
+function manifestUpdated()
+{
+  // The manifest itself should be in the cache
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest", true);
+
+  // The entries from the manifest should be in the cache
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", true);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", true);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", true);
+
+  window.frames["offlineChild"].doneLoading();
+}
+
+if (OfflineTest.setup()) {
+  applicationCache.onerror = OfflineTest.failEvent;
+
+  applicationCache.oncached = OfflineTest.priv(manifestUpdated);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body>
+
+<iframe name="offlineChild" src="offlineChild.html"></iframe>
+
+</body>
+</html>
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/offline/test_offlineResources.html
+++ /dev/null
@@ -1,150 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-<title>navigator.offlineResources 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">
-
-netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
-var nsICache = Components.interfaces.nsICache;
-var base = window.location.protocol + "//" + window.location.host;
-var packedURL = base + "/MochiKit/packed.js";
-var simpleTestURL = base + "/tests/SimpleTest/SimpleTest.js";
-var thisURL = base + window.location.pathname;
-
-function check_cache(url, expectEntry)
-{
-  var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-    .getService(Components.interfaces.nsICacheService);
-  var cacheSession = cacheService.createSession("HTTP-offline",
-                                                nsICache.STORE_OFFLINE,
-                                                true);
-  try {
-    var entry = cacheSession.openCacheEntry(url, nsICache.ACCESS_READ, true);
-    ok(expectEntry == true, url + " should exist in the offline cache");
-    entry.close();
-  } catch (e) {
-    // this constant isn't in Components.results
-    const kNetBase = 2152398848; // 0x804B0000
-    var NS_ERROR_CACHE_KEY_NOT_FOUND = kNetBase + 61
-    if (e.result == NS_ERROR_CACHE_KEY_NOT_FOUND) {
-      ok(expectEntry == false, url + " should not exist in the offline cache");
-    } else {
-      throw e;
-    }
-  }
-}
-
-function check_list(array)
-{
-  var checkObj = {};
-  for (var i = 0; i < array.length; i++) {
-    checkObj[array[i]] = true;
-  }
-  for (var i = 0; i < navigator.offlineResources.length; i++) {
-    ok (navigator.offlineResources[i] in checkObj,
-        navigator.offlineResources[i] + " should not be in offlineResources");
-    delete checkObj[navigator.offlineResources[i]];
-  }
-
-  for (var key in checkObj) {
-    ok(false, key + " was not in navigator.offlineResources");
-  }
-}
-
-function run_eviction()
-{
-  var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-    .getService(Components.interfaces.nsICacheService);
-  var cacheSession = cacheService.createSession("HTTP-offline",
-                                                nsICache.STORE_OFFLINE,
-                                                true)
-                     .QueryInterface(Components.interfaces.nsIOfflineCacheSession);
-  cacheSession.evictUnownedEntries();
-}
-
-function clear_cache()
-{
-  var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-    .getService(Components.interfaces.nsICacheService);
-
-  cacheService.evictEntries(nsICache.STORE_OFFLINE);
-}
-
-function load_done() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  check_list([packedURL, simpleTestURL, thisURL]);
-
-  check_cache(packedURL, true);
-  check_cache(simpleTestURL, true);
-  check_cache(thisURL, true);
-
-  navigator.offlineResources.remove(packedURL);
-  run_eviction();
-
-  check_list([simpleTestURL, thisURL]);
-
-  // Make sure the removed entry was evicted but the others weren't
-  check_cache(packedURL, false);
-  check_cache(simpleTestURL, true);
-  check_cache(thisURL, true);
-
-  navigator.offlineResources.clear();
-  run_eviction();
-
-  check_list([]);
-
-  check_cache(packedURL, false);
-  check_cache(simpleTestURL, false);
-  check_cache(thisURL, false);
-
-  // Clear out the offline cache on the way out
-  clear_cache();
-  SimpleTest.finish();
-}
-
-function run_test()
-{
-  // Clear the offline cache to make sure we're the one adding to it
-  clear_cache();
-
-  // Check invalid urls
-  try {
-    navigator.offlineResources.add("/blah/blah");
-    ok(false, "relative URLs cannot be added");
-  } catch(e) {
-    ok(true, "relative URLs should throw an error");
-  }
-
-  try {
-    navigator.offlineResources.add("ftp://localhost/blah/blah");
-    ok(false, "urls must be http or https");
-  } catch(e) {
-    ok(true, "urls must be http or https");
-  }
-
-  // Add the correct resources
-  navigator.offlineResources.add(packedURL);
-  navigator.offlineResources.add(simpleTestURL);
-  navigator.offlineResources.add(thisURL);
-
-  // Give the offline resources some time to load;  Once pendingOfflineLoads
-  // is implemented, we can use that instead.
-  setTimeout("load_done()", 1000);
-
-  SimpleTest.waitForExplicitFinish();
-}
-
-run_test();
-
-</script>
-
-</head>
-<body>
-
-
-
-</body>
-</html>
deleted file mode 100644
--- a/dom/tests/mochitest/ajax/offline/test_pendingOfflineLoads.html
+++ /dev/null
@@ -1,127 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-<title>navigator.pendingOfflineLoads Test</title>
-
-<link id="load1" rel="offline-resource" href="http://localhost:8888/unknown1">
-<link id="load2" rel="offline-resource" href="http://localhost:8888/MochiKit/packed.js">
-
-<script type="text/javascript" src="/MochiKit/packed.js"></script>
-<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-
-<script type="text/javascript">
-
-var base = window.location.protocol + "//" + window.location.host;
-var thisURL = base + window.location.pathname;
-
-var expectedCompletions = {};
-
-expectedCompletions["http://localhost:8888/unknown1"] = {
-  "source": document.getElementById("load1"),
-  "status": 404
-};
-expectedCompletions["http://localhost:8888/unknown2"] = {
-  "source": null,
-  "status": 404
-};
-expectedCompletions["http://localhost:8888/MochiKit/packed.js"] = {
-  "source": document.getElementById("load2"),
-  "status": 200
-};
-expectedCompletions[thisURL] = {
-  "source": null,
-  "status": 200
-};
-
-var expectedAdditions = { };
-for (var uri in expectedCompletions) {
-  expectedAdditions[uri] = expectedCompletions[uri];
-}
-
-function check_load_added(load)
-{
-  var source = load.source;
-  var uri = load.uri;
-
-  ok(expectedAdditions[uri], uri + " should be in the expected addition list");
-  ok(expectedAdditions[uri].source == source,
-     uri + " should come from the expected source");
-  ok(load.status == 0, "status should be 0");
-  delete expectedAdditions[uri];
-}
-
-
-function load_requested(e)
-{
-  check_load_added(e.status);
-}
-
-function load_completed(e)
-{
-  var load = e.status;
-  var uri = load.uri;
-
-  ok(expectedCompletions[uri],
-     uri + " should be in the expected completion list");
-  ok(expectedCompletions[uri].source == load.source,
-     uri + " should come from the expected source");
-  ok(expectedCompletions[uri].status == load.status,
-     uri + " should have a load status of " + expectedCompletions[uri].status);
-
-  delete expectedCompletions[uri];
-
-  if (navigator.pendingOfflineLoads.length == 0) {
-    for (var name in expectedAdditions) {
-      ok(false, name + " did not receive a request event");
-    }
-
-    for (var name in expectedCompletions) {
-      ok(false, name + " did not receive a completed event");
-    }
-
-    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
-    // Clear the ownership list
-    var cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
-      .getService(Components.interfaces.nsICacheService);
-    var cacheSession = cacheService.createSession("HTTP-offline",
-                                                  Components.interfaces.nsICache.STORE_OFFLINE,
-                                                  true)
-      .QueryInterface(Components.interfaces.nsIOfflineCacheSession);
-    cacheSession.setOwnedKeys(window.location.host, thisURL, 0, []);
-    cacheSession.setOwnedKeys(window.location.host, "", 0, []);
-    cacheSession.evictUnownedEntries();
-
-    SimpleTest.finish();
-  }
-}
-
-function run_test()
-{
-  navigator.pendingOfflineLoads.addEventListener("loadrequested",
-                                                 load_requested,
-                                                 false);
-  navigator.pendingOfflineLoads.addEventListener("loadcompleted",
-                                                 load_completed,
-                                                 false);
-
-  for (var i = 0; i < navigator.pendingOfflineLoads.length; i++) {
-    var load = navigator.pendingOfflineLoads[i];
-    check_load_added(navigator.pendingOfflineLoads[i]);
-  }
-
-  // Add the correct resources
-  navigator.offlineResources.add("http://localhost:8888/unknown2");
-  navigator.offlineResources.add(thisURL);
-}
-
-SimpleTest.waitForExplicitFinish();
-
-</script>
-
-</head>
-<body onload="run_test()">
-
-
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/ajax/offline/test_simpleManifest.html
@@ -0,0 +1,116 @@
+<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest">
+<head>
+<title>simple 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">
+
+var gGotChecking = false;
+var gGotDownloading = false;
+
+function addFinished()
+{
+  // Check that the entry was added successfully
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/EventUtils.js",
+                         true);
+
+  OfflineTest.ok(applicationCache.length == 1, "applicationCache should have one dynamic entry");
+  OfflineTest.ok(applicationCache.item(0) == "http://localhost:8888/tests/SimpleTest/EventUtils.js",
+    "applicationCache's dynamic entry should be the one we expect");
+
+  // Now test that removes work
+  applicationCache.remove("http://localhost:8888/tests/SimpleTest/EventUtils.js");
+
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/EventUtils.js",
+                         false);
+  OfflineTest.ok(applicationCache.length == 0,
+                 "applicationCache should have no dynamic entries");
+
+  // We're done
+
+  OfflineTest.teardown();
+
+  OfflineTest.finish();
+}
+
+function secondUpdate()
+{
+  OfflineTest.ok(gGotChecking, "Should get a checking event");
+  OfflineTest.ok(gGotDownloading, "Should get a downloading event");
+
+  // The document that requested the manifest should be in the cache
+  OfflineTest.checkCache(window.location.href, true);
+
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", true);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", true);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", true);
+
+  // Now add a file using the applicationCache API
+  applicationCache.add("http://localhost:8888/tests/SimpleTest/EventUtils.js");
+
+  // Wait for the add() to be downloaded
+  OfflineTest.waitForAdd("http://localhost:8888/tests/SimpleTest/EventUtils.js",
+                         OfflineTest.priv(addFinished));
+}
+
+function manifestUpdated()
+{
+  OfflineTest.ok(gGotChecking, "Should get a checking event");
+  OfflineTest.ok(gGotDownloading, "Should get a downloading event");
+
+  // The manifest itself should be in the cache
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/simpleManifest.cacheManifest", true);
+
+  // The document that requested the manifest should be in the cache
+  OfflineTest.checkCache(window.location.href, true);
+
+  // The entries from the manifest should be in the cache
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", true);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", true);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", true);
+
+  // The bad entries from the manifest should not be in the cache
+  OfflineTest.checkCache("https://localhost:8888/MochiKit/packed.js", false);
+  OfflineTest.checkCache("bad:/uri/invalid", false);
+
+  // Remove items from the cache.
+  OfflineTest.clear();
+
+  // Make sure OfflineTest.clear() properly removed the items
+
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/SimpleTest.js", false);
+  OfflineTest.checkCache("http://localhost:8888/MochiKit/packed.js", false);
+  OfflineTest.checkCache("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/offlineTests.js", false);
+  OfflineTest.checkCache("http://localhost:8888/tests/SimpleTest/EventUtils.js",
+                         false);
+
+  // Now make sure applicationCache.update() does what we expect.
+  applicationCache.oncached = OfflineTest.priv(secondUpdate);
+
+  gGotChecking = false;
+  gGotDownloading = false;
+  applicationCache.update();
+}
+
+
+if (OfflineTest.setup()) {
+  applicationCache.onerror = OfflineTest.failEvent;
+
+  applicationCache.onchecking = function() { gGotChecking = true; };
+  applicationCache.ondownloading = function() { gGotDownloading = true; };
+  applicationCache.oncached = OfflineTest.priv(manifestUpdated);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
--- a/uriloader/prefetch/nsIOfflineCacheUpdate.idl
+++ b/uriloader/prefetch/nsIOfflineCacheUpdate.idl
@@ -37,97 +37,150 @@
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
 interface nsIURI;
 interface nsIDOMNode;
 interface nsIDOMDocument;
 interface nsIDOMLoadStatus;
+interface nsIOfflineCacheUpdate;
 
-[scriptable, uuid(e0785ebb-b3a1-426a-a70e-be2b923e973e)]
+[scriptable, uuid(0aa38757-999c-44d6-bdb4-7dd32634fa83)]
 interface nsIOfflineCacheUpdateObserver : nsISupports {
   /**
+   * There was an error updating the cache.
+   *
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   */
+  void error(in nsIOfflineCacheUpdate aUpdate);
+
+  /**
+   * The manifest is being checked for updates
+   *
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   */
+  void checking(in nsIOfflineCacheUpdate aUpdate);
+
+  /**
+   * No update was necessary.
+   *
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   */
+  void noUpdate(in nsIOfflineCacheUpdate aUpdate);
+
+  /**
+   * Starting to download resources
+   *
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   */
+  void downloading(in nsIOfflineCacheUpdate aUpdate);
+
+  /**
+   * An item has started downloading.
+   *
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   * @param aItem
+   *        load status for the item that is being downloaded.
+   */
+  void itemStarted(in nsIOfflineCacheUpdate aUpdate,
+                   in nsIDOMLoadStatus aItem);
+
+  /**
    * An item has finished loading.
    *
-   * @param aItem load status for the item that completed.
+   * @param aUpdate
+   *        The nsIOfflineCacheUpdate being processed.
+   * @param aItem
+   *         load status for the item that completed.
    */
-  void itemCompleted(in nsIDOMLoadStatus aItem);
+  void itemCompleted(in nsIOfflineCacheUpdate aUpdate,
+                     in nsIDOMLoadStatus aItem);
 };
 
 /**
- * An nsIOfflineCacheUpdate is used to update a domain's offline resources.
+ * An nsIOfflineCacheUpdate is used to update an application's offline
+ * resources.
+ *
  * It can be used to perform partial or complete updates.
  *
  * Each update object maintains a list of nsIDOMLoadStatus items for the
  * resources it is updating.  The list of these items will be available
  * after the object is scheduled.
  *
  * One update object will be updating at a time.  The active object will
  * load its items one by one, sending itemCompleted() to any registered
  * observers.
  */
 [scriptable, uuid(7dc06ede-1098-4384-b95e-65525ab254c9)]
 interface nsIOfflineCacheUpdate : nsISupports {
   /**
+   * Fetch the status of the running update.  This will return a value
+   * defined in nsIDOMOfflineResourceList.
+   */
+  readonly attribute unsigned short status;
+
+  /**
+   * TRUE if the update is being used to add specific resources.
+   * FALSE if the complete cache update process is happening.
+   */
+  readonly attribute boolean partial;
+
+  /**
    * The domain being updated, and the domain that will own any URIs added
    * with this update.
    */
   readonly attribute ACString updateDomain;
 
   /**
-   * The URI that will own any URIs added by this update
+   * The manifest for the offline application being updated.
    */
-  readonly attribute ACString ownerURI;
+  readonly attribute nsIURI manifestURI;
+
+  /**
+   * TRUE if the cache update completed successfully.
+   */
+  readonly attribute boolean succeeded;
 
   /**
    * Initialize the update.
    *
    * @param aPartialUpdate
    *        TRUE if the update should just update the URIs given to it,
    *        FALSE if all URLs for the owner domain should be added.
-   * @param aUpdateDomain
-   *        The domain which is being updated, and which will own any
-   *        URIs added.
-   * @param aOwnerURI
-   *        The owner URI for any URIs added.
-   * @param aReferrerURI
+   * @param aManifestURI
+   *        The manifest URI to be checked, or for partial updates the
+   *        manifest that should own resources that are added.
+   * @param aDocumentURI
    *        The page that is requesting the update.
    */
   void init(in boolean aPartialUpdate,
-            in ACString aUpdateDomain,
-            in ACString aOwnerURI,
-            in nsIURI aReferrerURI);
+            in nsIURI aManifestURI,
+            in nsIURI aDocumentURI);
 
   /**
    * Add a URI to the offline cache as part of the update.
    *
    * @param aURI
    *        The URI to add.
-   * @param aSource
-   *        The DOM node (<link> tag) associated with this node (for
-   *        implementing nsIDOMLoadStatus).
    */
-  void addURI(in nsIURI aURI, in nsIDOMNode aSource);
+  void addDynamicURI(in nsIURI aURI);
 
   /**
    * Add the update to the offline update queue.  An offline-cache-update-added
    * event will be sent to the observer service.
    */
   void schedule();
 
   /**
-   * Request that the update be scheduled when a document finishes loading.
-   *
-   * @param aDocument
-   *        When this document finishes loading, the update will be scheduled.
-   */
-  void scheduleOnDocumentStop(in nsIDOMDocument aDocument);
-
-  /**
    * Access to the list of items in the update.
    */
   readonly attribute unsigned long count;
   nsIDOMLoadStatus item(in unsigned long index);
 
   /**
    * Observe loads that are added to the update.
    *
@@ -144,16 +197,32 @@ interface nsIOfflineCacheUpdate : nsISup
    * Remove an observer from the update.
    *
    * @param aObserver
    *        the observer to remove.
    */
   void removeObserver(in nsIOfflineCacheUpdateObserver aObserver);
 };
 
-[scriptable, uuid(f99ca10f-5cde-4966-b845-433f2921a201)]
+[scriptable, uuid(3abee04b-5bbb-4405-b659-35f780e38da0)]
 interface nsIOfflineCacheUpdateService : nsISupports {
     /**
      * Access to the list of cache updates that have been scheduled.
      */
     readonly attribute unsigned long numUpdates;
     nsIOfflineCacheUpdate getUpdate(in unsigned long index);
+
+    /**
+     * Schedule a cache update for a given offline manifest.  If an
+     * existing update is scheduled or running, that update will be returned.
+     * Otherwise a new update will be scheduled.
+     */
+    nsIOfflineCacheUpdate scheduleUpdate(in nsIURI aManifestURI,
+                                         in nsIURI aDocumentURI);
+
+    /**
+     * Schedule a cache update for a manifest when the document finishes
+     * loading.
+     */
+    void scheduleOnDocumentStop(in nsIURI aManifestURI,
+                                in nsIURI aDocumentURI,
+                                in nsIDOMDocument aDocument);
 };
--- a/uriloader/prefetch/nsOfflineCacheUpdate.cpp
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp
@@ -40,16 +40,17 @@
 
 #include "nsCPrefetchService.h"
 #include "nsCURILoader.h"
 #include "nsICache.h"
 #include "nsICacheService.h"
 #include "nsICacheSession.h"
 #include "nsICachingChannel.h"
 #include "nsIDOMWindow.h"
+#include "nsIDOMOfflineResourceList.h"
 #include "nsIObserverService.h"
 #include "nsIOfflineCacheSession.h"
 #include "nsIWebProgress.h"
 #include "nsNetCID.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "nsStreamUtils.h"
 #include "nsThreadUtils.h"
@@ -96,27 +97,25 @@ NS_IMPL_ISUPPORTS6(nsOfflineCacheUpdateI
 
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdateItem <public>
 //-----------------------------------------------------------------------------
 
 nsOfflineCacheUpdateItem::nsOfflineCacheUpdateItem(nsOfflineCacheUpdate *aUpdate,
                                                    nsIURI *aURI,
                                                    nsIURI *aReferrerURI,
-                                                   nsIDOMNode *aSource,
                                                    const nsACString &aClientID)
     : mURI(aURI)
     , mReferrerURI(aReferrerURI)
     , mClientID(aClientID)
     , mUpdate(aUpdate)
     , mChannel(nsnull)
     , mState(nsIDOMLoadStatus::UNINITIALIZED)
     , mBytesRead(0)
 {
-    mSource = do_GetWeakReference(aSource);
 }
 
 nsOfflineCacheUpdateItem::~nsOfflineCacheUpdateItem()
 {
 }
 
 nsresult
 nsOfflineCacheUpdateItem::OpenChannel()
@@ -272,25 +271,23 @@ nsOfflineCacheUpdateItem::OnChannelRedir
         rv = newCachingChannel->SetCacheForOfflineUse(PR_TRUE);
         NS_ENSURE_SUCCESS(rv, rv);
         if (!mClientID.IsEmpty()) {
             rv = newCachingChannel->SetOfflineCacheClientID(mClientID);
             NS_ENSURE_SUCCESS(rv, rv);
         }
     }
 
+    nsCAutoString oldScheme;
+    mURI->GetScheme(oldScheme);
 
     PRBool match;
-    rv = newURI->SchemeIs("http", &match);
-    if (NS_FAILED(rv) || !match) {
-        if (NS_FAILED(newURI->SchemeIs("https", &match)) ||
-            !match) {
-            LOG(("rejected: URL is not of type http\n"));
-            return NS_ERROR_ABORT;
-        }
+    if (NS_FAILED(newURI->SchemeIs(oldScheme.get(), &match)) || !match) {
+        LOG(("rejected: redirected to a different scheme\n"));
+        return NS_ERROR_ABORT;
     }
 
     // HTTP request headers are not automatically forwarded to the new channel.
     nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
     NS_ENSURE_STATE(httpChannel);
 
     httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
                                   NS_LITERAL_CSTRING("offline-resource"),
@@ -303,22 +300,18 @@ nsOfflineCacheUpdateItem::OnChannelRedir
 
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdateItem::nsIDOMLoadStatus
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsOfflineCacheUpdateItem::GetSource(nsIDOMNode **aSource)
 {
-    if (mSource) {
-        return CallQueryReferent(mSource.get(), aSource);
-    } else {
-        *aSource = nsnull;
-        return NS_OK;
-    }
+    *aSource = nsnull;
+    return NS_OK;
 }
 
 NS_IMETHODIMP
 nsOfflineCacheUpdateItem::GetUri(nsAString &aURI)
 {
     nsCAutoString spec;
     nsresult rv = mURI->GetSpec(spec);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -381,16 +374,268 @@ nsOfflineCacheUpdateItem::GetStatus(PRUi
     }
 
     NS_ENSURE_SUCCESS(rv, rv);
     *aStatus = PRUint16(httpStatus);
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
+// nsOfflineManifestItem
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// nsOfflineManifestItem <public>
+//-----------------------------------------------------------------------------
+
+nsOfflineManifestItem::nsOfflineManifestItem(nsOfflineCacheUpdate *aUpdate,
+                                             nsIURI *aURI,
+                                             nsIURI *aReferrerURI,
+                                             const nsACString &aClientID)
+    : nsOfflineCacheUpdateItem(aUpdate, aURI, aReferrerURI, aClientID)
+    , mParserState(PARSE_INIT)
+    , mNeedsUpdate(PR_TRUE)
+{
+}
+
+nsOfflineManifestItem::~nsOfflineManifestItem()
+{
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineManifestItem <private>
+//-----------------------------------------------------------------------------
+
+/* static */
+NS_METHOD
+nsOfflineManifestItem::ReadManifest(nsIInputStream *aInputStream,
+                                    void *aClosure,
+                                    const char *aFromSegment,
+                                    PRUint32 aOffset,
+                                    PRUint32 aCount,
+                                    PRUint32 *aBytesConsumed)
+{
+    nsOfflineManifestItem *manifest =
+        static_cast<nsOfflineManifestItem*>(aClosure);
+
+    *aBytesConsumed = aCount;
+
+    if (manifest->mParserState == PARSE_ERROR) {
+        // parse already failed, ignore this
+        return NS_OK;
+    }
+
+    manifest->mReadBuf.Append(aFromSegment, aCount);
+
+    nsCString::const_iterator begin, iter, end;
+    manifest->mReadBuf.BeginReading(begin);
+    manifest->mReadBuf.EndReading(end);
+
+    for (iter = begin; iter != end; iter++) {
+        if (*iter == '\r' || *iter == '\n') {
+            nsresult rv = manifest->HandleManifestLine(begin, iter);
+            
+            if (NS_FAILED(rv)) {
+                LOG(("HandleManifestLine failed with 0x%08x", rv));
+                return NS_ERROR_ABORT;
+            }
+
+            begin = iter;
+            begin++;
+        }
+    }
+
+    // any leftovers are saved for next time
+    manifest->mReadBuf = Substring(begin, end);
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineManifestItem::HandleManifestLine(const nsCString::const_iterator &aBegin,
+                                          const nsCString::const_iterator &aEnd)
+{
+    nsCString::const_iterator begin = aBegin;
+    nsCString::const_iterator end = aEnd;
+
+    // all lines ignore trailing spaces and tabs
+    nsCString::const_iterator last = end;
+    --last;
+    while (end != begin && (*last == ' ' || *last == '\t')) {
+        --end;
+        --last;
+    }
+
+    if (mParserState == PARSE_INIT) {
+        // Allow a UTF-8 BOM
+        if (begin != end && static_cast<unsigned char>(*begin) == 0xef) {
+            if (++begin == end || static_cast<unsigned char>(*begin) != 0xbb ||
+                ++begin == end || static_cast<unsigned char>(*begin) != 0xbf) {
+                mParserState = PARSE_ERROR;
+                return NS_OK;
+            }
+            ++begin;
+        }
+
+        const nsCSubstring &magic = Substring(begin, end);
+
+        if (!magic.EqualsLiteral("CACHE MANIFEST")) {
+            mParserState = PARSE_ERROR;
+            return NS_OK;
+        }
+
+        mParserState = PARSE_CACHE_ENTRIES;
+        return NS_OK;
+    }
+
+    // lines other than the first ignore leading spaces and tabs
+    while (begin != end && (*begin == ' ' || *begin == '\t'))
+        begin++;
+
+    // ignore blank lines and comments
+    if (begin == end || *begin == '#')
+        return NS_OK;
+
+    const nsCSubstring &line = Substring(begin, end);
+
+    if (line.EqualsLiteral("CACHE:")) {
+        mParserState = PARSE_CACHE_ENTRIES;
+        return NS_OK;
+    }
+
+    if (line.EqualsLiteral("FALLBACK:")) {
+        mParserState = PARSE_FALLBACK_ENTRIES;
+        return NS_OK;
+    }
+
+    if (line.EqualsLiteral("NETWORK:")) {
+        mParserState = PARSE_NETWORK_ENTRIES;
+        return NS_OK;
+    }
+
+    nsresult rv;
+
+    switch(mParserState) {
+    case PARSE_INIT:
+    case PARSE_ERROR: {
+        // this should have been dealt with earlier
+        return NS_ERROR_FAILURE;
+    }
+    case PARSE_CACHE_ENTRIES: {
+        nsCOMPtr<nsIURI> uri;
+        rv = NS_NewURI(getter_AddRefs(uri), line, nsnull, mURI);
+        if (NS_FAILED(rv))
+            break;
+
+        nsCAutoString scheme;
+        uri->GetScheme(scheme);
+
+        // Manifest URIs must have the same scheme as the manifest.
+        PRBool match;
+        if (NS_FAILED(mURI->SchemeIs(scheme.get(), &match)) || !match)
+            break;
+
+        mExplicitURIs.AppendObject(uri);
+        break;
+    }
+    case PARSE_FALLBACK_ENTRIES:
+    case PARSE_NETWORK_ENTRIES: {
+        // we don't currently implement fallbacks or whitelists,
+        // ignore these for now.
+        break;
+    }
+    }
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnStartRequest(nsIRequest *aRequest,
+                                      nsISupports *aContext)
+{
+    nsresult rv;
+
+    nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    PRBool succeeded;
+    rv = channel->GetRequestSucceeded(&succeeded);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (!succeeded) {
+        LOG(("HTTP request failed"));
+        mParserState = PARSE_ERROR;
+        return NS_ERROR_ABORT;
+    }
+
+    nsCAutoString contentType;
+    rv = channel->GetContentType(contentType);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (!contentType.EqualsLiteral("text/cache-manifest")) {
+        LOG(("Rejected cache manifest with Content-Type %s (expecting text/cache-manifest)",
+             contentType.get()));
+        mParserState = PARSE_ERROR;
+        return NS_ERROR_ABORT;
+    }
+
+    return nsOfflineCacheUpdateItem::OnStartRequest(aRequest, aContext);
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnDataAvailable(nsIRequest *aRequest,
+                                       nsISupports *aContext,
+                                       nsIInputStream *aStream,
+                                       PRUint32 aOffset,
+                                       PRUint32 aCount)
+{
+    PRUint32 bytesRead = 0;
+    aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
+    mBytesRead += bytesRead;
+
+    if (mParserState == PARSE_ERROR) {
+        LOG(("OnDataAvailable is canceling the request due a parse error\n"));
+        return NS_ERROR_ABORT;
+    }
+
+    LOG(("loaded %u bytes into offline cache [offset=%u]\n",
+         bytesRead, aOffset));
+
+    // All the parent method does is read and discard, don't bother
+    // chaining up.
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnStopRequest(nsIRequest *aRequest,
+                                     nsISupports *aContext,
+                                     nsresult aStatus)
+{
+    // handle any leftover manifest data
+    nsCString::const_iterator begin, end;
+    mReadBuf.BeginReading(begin);
+    mReadBuf.EndReading(end);
+    nsresult rv = HandleManifestLine(begin, end);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (mBytesRead == 0) {
+        // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was
+        // specified.)
+        mNeedsUpdate = PR_FALSE;
+
+        // XXX: The spec calls for a byte-by-byte comparison of the manifest,
+        // with an exact match skipping the update.  Need to implement this
+        // or we'll be fetching way too often.
+    }
+
+    return nsOfflineCacheUpdateItem::OnStopRequest(aRequest, aContext, aStatus);
+}
+
+//-----------------------------------------------------------------------------
 // nsOfflineCacheUpdate::nsISupports
 //-----------------------------------------------------------------------------
 
 NS_IMPL_ISUPPORTS1(nsOfflineCacheUpdate,
                    nsIOfflineCacheUpdate)
 
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdate <public>
@@ -407,34 +652,64 @@ nsOfflineCacheUpdate::nsOfflineCacheUpda
 
 nsOfflineCacheUpdate::~nsOfflineCacheUpdate()
 {
     LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this));
 }
 
 nsresult
 nsOfflineCacheUpdate::Init(PRBool aPartialUpdate,
-                           const nsACString &aUpdateDomain,
-                           const nsACString &aOwnerURI,
-                           nsIURI *aReferrerURI)
+                           nsIURI *aManifestURI,
+                           nsIURI *aDocumentURI)
 {
     nsresult rv;
 
     // Make sure the service has been initialized
     nsOfflineCacheUpdateService* service =
         nsOfflineCacheUpdateService::EnsureService();
     if (!service)
         return NS_ERROR_FAILURE;
 
     LOG(("nsOfflineCacheUpdate::Init [%p]", this));
 
     mPartialUpdate = aPartialUpdate;
-    mUpdateDomain = aUpdateDomain;
-    mOwnerURI = aOwnerURI;
-    mReferrerURI = aReferrerURI;
+
+    // Only http and https applications are supported.
+    PRBool match;
+    rv = aManifestURI->SchemeIs("http", &match);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (!match) {
+        rv = aManifestURI->SchemeIs("https", &match);
+        NS_ENSURE_SUCCESS(rv, rv);
+        if (!match)
+            return NS_ERROR_ABORT;
+    }
+
+    mManifestURI = aManifestURI;
+
+    rv = mManifestURI->GetHostPort(mUpdateDomain);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCAutoString manifestSpec;
+
+    rv = mManifestURI->GetSpec(manifestSpec);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    PRInt32 ref = manifestSpec.FindChar('#');
+    if (ref != kNotFound)
+        manifestSpec.Truncate(ref);
+
+    mManifestOwnerSpec = manifestSpec;
+    mManifestOwnerSpec.AppendLiteral("#manifest");
+
+    mDynamicOwnerSpec = manifestSpec;
+    mDynamicOwnerSpec.AppendLiteral("#dynamic");
+
+    mDocumentURI = aDocumentURI;
 
     nsCOMPtr<nsICacheService> cacheService =
         do_GetService(NS_CACHESERVICE_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsICacheSession> session;
     rv = cacheService->CreateSession("HTTP-offline",
                                      nsICache::STORE_OFFLINE,
@@ -456,70 +731,161 @@ nsOfflineCacheUpdate::Init(PRBool aParti
         rv = cacheService->CreateSession(mClientID.get(),
                                          nsICache::STORE_OFFLINE,
                                          nsICache::STREAM_BASED,
                                          getter_AddRefs(session));
         NS_ENSURE_SUCCESS(rv, rv);
 
         mCacheSession = do_QueryInterface(session, &rv);
         NS_ENSURE_SUCCESS(rv, rv);
+
+        // The manifest implicitly owns itself.
+        rv = mCacheSession->AddOwnedKey(mUpdateDomain, mManifestOwnerSpec,
+                                        manifestSpec);
+        NS_ENSURE_SUCCESS(rv, rv);
     }
 
     mState = STATE_INITIALIZED;
 
     return NS_OK;
 }
 
+nsresult
+nsOfflineCacheUpdate::HandleManifest(PRBool *aDoUpdate)
+{
+    // Be pessimistic
+    *aDoUpdate = PR_FALSE;
+
+    PRUint16 status;
+    nsresult rv = mManifestItem->GetStatus(&status);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (status == 0 || status >= 400 || !mManifestItem->ParseSucceeded()) {
+        return NS_ERROR_FAILURE;
+    }
+
+    if (!mManifestItem->NeedsUpdate()) {
+        return NS_OK;
+    }
+
+    // Add items requested by the manifest.
+    const nsCOMArray<nsIURI> &manifestURIs = mManifestItem->GetExplicitURIs();
+    for (PRInt32 i = 0; i < manifestURIs.Count(); i++) {
+        rv = AddURI(manifestURIs[i], mManifestOwnerSpec);
+        NS_ENSURE_SUCCESS(rv, rv);
+    }
+
+    // The document that requested the manifest is implicitly included
+    // as part of that manifest update.
+    rv = AddURI(mDocumentURI, mManifestOwnerSpec);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Add items requested by the script API
+    rv = AddOwnedItems(mDynamicOwnerSpec);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    *aDoUpdate = PR_TRUE;
+
+    return NS_OK;
+}
+
 void
 nsOfflineCacheUpdate::LoadCompleted()
 {
     nsresult rv;
 
     LOG(("nsOfflineCacheUpdate::LoadCompleted [%p]", this));
 
+    if (mState == STATE_CANCELLED) {
+        Finish();
+        return;
+    }
+
+    if (mState == STATE_CHECKING) {
+        // Manifest load finished.
+
+        NS_ASSERTION(mManifestItem,
+                     "Must have a manifest item in STATE_CHECKING.");
+
+        PRBool doUpdate;
+        if (NS_FAILED(HandleManifest(&doUpdate))) {
+            mSucceeded = PR_FALSE;
+            NotifyError();
+            Finish();
+            return;
+        }
+
+        if (!doUpdate) {
+            mSucceeded = PR_FALSE;
+            NotifyNoUpdate();
+            Finish();
+            return;
+        }
+
+        mState = STATE_DOWNLOADING;
+        NotifyDownloading();
+
+        // Start fetching resources.
+        ProcessNextURI();
+
+        return;
+    }
+
+    // Normal load finished.
+
     nsRefPtr<nsOfflineCacheUpdateItem> item = mItems[mCurrentItem];
     mCurrentItem++;
 
     PRUint16 status;
     rv = item->GetStatus(&status);
 
-    // Check for failures.  Only connection or server errors (5XX) will cause
-    // the update to fail.
-    if (NS_FAILED(rv) || status == 0 || status >= 500) {
-        // Only fail updates from this domain.  Outside-of-domain updates
-        // are not guaranteeed to be updated.
-        nsCAutoString domain;
-        item->mURI->GetHostPort(domain);
-        if (domain == mUpdateDomain) {
-            mSucceeded = PR_FALSE;
-        }
+    // Check for failures.  4XX and 5XX errors will cause the update to fail.
+    if (NS_FAILED(rv) || status == 0 || status >= 400) {
+        mSucceeded = PR_FALSE;
+        NotifyError();
+        Finish();
+        return;
     }
 
     rv = NotifyCompleted(item);
     if (NS_FAILED(rv)) return;
 
     ProcessNextURI();
 }
 
 nsresult
 nsOfflineCacheUpdate::Begin()
 {
     LOG(("nsOfflineCacheUpdate::Begin [%p]", this));
 
-    if (!mPartialUpdate) {
-        // All offline items for a domain should be updated as a group;  add
-        // the other offline items requested for this domain
-        nsresult rv = AddDomainItems();
-        NS_ENSURE_SUCCESS(rv, rv);
+    mCurrentItem = 0;
+
+    if (mPartialUpdate) {
+        mState = STATE_DOWNLOADING;
+        NotifyDownloading();
+        ProcessNextURI();
+        return NS_OK;
     }
 
-    mState = STATE_RUNNING;
+    // Start checking the manifest.
+    nsCOMPtr<nsIURI> uri;
 
-    mCurrentItem = 0;
-    ProcessNextURI();
+    mManifestItem = new nsOfflineManifestItem(this, mManifestURI,
+                                              mDocumentURI, mClientID);
+    if (!mManifestItem) {
+        return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    mState = STATE_CHECKING;
+    NotifyChecking();
+
+    nsresult rv = mManifestItem->OpenChannel();
+    if (NS_FAILED(rv)) {
+        LoadCompleted();
+    }
 
     return NS_OK;
 }
 
 nsresult
 nsOfflineCacheUpdate::Cancel()
 {
     LOG(("nsOfflineCacheUpdate::Cancel [%p]", this));
@@ -550,132 +916,204 @@ nsOfflineCacheUpdate::AddOwnedItems(cons
     NS_ENSURE_SUCCESS(rv, rv);
 
     AutoFreeArray autoFree(count, keys);
 
     for (PRUint32 i = 0; i < count; i++) {
         nsCOMPtr<nsIURI> uri;
         if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), keys[i]))) {
             nsRefPtr<nsOfflineCacheUpdateItem> item =
-                new nsOfflineCacheUpdateItem(this, uri, mReferrerURI,
-                                             nsnull, mClientID);
+                new nsOfflineCacheUpdateItem(this, uri, mDocumentURI,
+                                             mClientID);
             if (!item) return NS_ERROR_OUT_OF_MEMORY;
 
             mItems.AppendElement(item);
         }
     }
 
     return NS_OK;
 }
 
-// Add all URIs needed by this domain to the update
-nsresult
-nsOfflineCacheUpdate::AddDomainItems()
-{
-    PRUint32 count;
-    char **uris;
-    nsresult rv = mMainCacheSession->GetOwnerURIs(mUpdateDomain, &count, &uris);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    AutoFreeArray autoFree(count, uris);
-
-    for (PRUint32 i = 0; i < count; i++) {
-        const char *ownerURI = uris[i];
-        // if this update includes changes to this owner URI, ignore the
-        // set in the database.
-        if (!mAddedItems || !mOwnerURI.Equals(ownerURI)) {
-            rv = AddOwnedItems(nsDependentCString(ownerURI));
-            NS_ENSURE_SUCCESS(rv, rv);
-        }
-    }
-
-    return NS_OK;
-}
-
 nsresult
 nsOfflineCacheUpdate::ProcessNextURI()
 {
     LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, current=%d, numItems=%d]",
          this, mCurrentItem, mItems.Length()));
 
-    if (mState == STATE_CANCELLED ||
-        mCurrentItem >= static_cast<PRInt32>(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 defined(PR_LOGGING)
     if (LOG_ENABLED()) {
         nsCAutoString spec;
         mItems[mCurrentItem]->mURI->GetSpec(spec);
         LOG(("%p: Opening channel for %s", this, spec.get()));
     }
 #endif
 
+    NotifyStarted(mItems[mCurrentItem]);
+
     nsresult rv = mItems[mCurrentItem]->OpenChannel();
     if (NS_FAILED(rv)) {
         LoadCompleted();
         return rv;
     }
 
     return NS_OK;
 }
 
 nsresult
-nsOfflineCacheUpdate::NotifyCompleted(nsOfflineCacheUpdateItem *aItem)
+nsOfflineCacheUpdate::GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers)
 {
-    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
-
     for (PRInt32 i = 0; i < mWeakObservers.Count(); i++) {
         nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
             do_QueryReferent(mWeakObservers[i]);
         if (observer)
-            observers.AppendObject(observer);
+            aObservers.AppendObject(observer);
         else
             mWeakObservers.RemoveObjectAt(i--);
     }
 
     for (PRInt32 i = 0; i < mObservers.Count(); i++) {
-        observers.AppendObject(mObservers[i]);
+        aObservers.AppendObject(mObservers[i]);
+    }
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyError()
+{
+    LOG(("nsOfflineCacheUpdate::NotifyError [%p]", this));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    for (PRInt32 i = 0; i < observers.Count(); i++) {
+        observers[i]->Error(this);
+    }
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyChecking()
+{
+    LOG(("nsOfflineCacheUpdate::NotifyChecking [%p]", this));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    for (PRInt32 i = 0; i < observers.Count(); i++) {
+        observers[i]->Checking(this);
     }
 
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyNoUpdate()
+{
+    LOG(("nsOfflineCacheUpdate::NotifyNoUpdate [%p]", this));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
     for (PRInt32 i = 0; i < observers.Count(); i++) {
-        observers[i]->ItemCompleted(aItem);
+        observers[i]->NoUpdate(this);
+    }
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyDownloading()
+{
+    LOG(("nsOfflineCacheUpdate::NotifyDownloading [%p]", this));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    for (PRInt32 i = 0; i < observers.Count(); i++) {
+        observers[i]->Downloading(this);
+    }
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyStarted(nsOfflineCacheUpdateItem *aItem)
+{
+    LOG(("nsOfflineCacheUpdate::NotifyStarted [%p, %p]", this, aItem));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    for (PRInt32 i = 0; i < observers.Count(); i++) {
+        observers[i]->ItemStarted(this, aItem);
+    }
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::NotifyCompleted(nsOfflineCacheUpdateItem *aItem)
+{
+    LOG(("nsOfflineCacheUpdate::NotifyCompleted [%p, %p]", this, aItem));
+
+    nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+    nsresult rv = GatherObservers(observers);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    for (PRInt32 i = 0; i < observers.Count(); i++) {
+        observers[i]->ItemCompleted(this, aItem);
     }
 
     return NS_OK;
 }
 
 nsresult
 nsOfflineCacheUpdate::Finish()
 {
     LOG(("nsOfflineCacheUpdate::Finish [%p]", this));
 
     mState = STATE_FINISHED;
 
     nsOfflineCacheUpdateService* service =
         nsOfflineCacheUpdateService::EnsureService();
 
+    if (!service)
+        return NS_ERROR_FAILURE;
+
     if (!mPartialUpdate) {
         if (mSucceeded) {
             nsresult rv = mMainCacheSession->MergeTemporaryClientID(mClientID);
-            if (NS_FAILED(rv))
+            if (NS_FAILED(rv)) {
+                NotifyError();
                 mSucceeded = PR_FALSE;
+            }
         }
 
         if (!mSucceeded) {
             // Update was not merged, mark all the loads as failures
             for (PRUint32 i = 0; i < mItems.Length(); i++) {
                 mItems[i]->Cancel();
             }
         }
     }
 
-    if (!service)
-        return NS_ERROR_FAILURE;
-
     return service->UpdateFinished(this);
 }
 
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdate::nsIOfflineCacheUpdate
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
@@ -683,75 +1121,116 @@ nsOfflineCacheUpdate::GetUpdateDomain(ns
 {
     NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
 
     aUpdateDomain = mUpdateDomain;
     return NS_OK;
 }
 
 NS_IMETHODIMP
-nsOfflineCacheUpdate::GetOwnerURI(nsACString &aOwnerURI)
+nsOfflineCacheUpdate::GetStatus(PRUint16 *aStatus)
 {
-    NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+    switch (mState) {
+    case STATE_CHECKING :
+        return nsIDOMOfflineResourceList::CHECKING;
+    case STATE_DOWNLOADING :
+        return nsIDOMOfflineResourceList::DOWNLOADING;
+    default :
+        return nsIDOMOfflineResourceList::IDLE;
+    }
 
-    aOwnerURI = mOwnerURI;
+    return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetPartial(PRBool *aPartial)
+{
+    *aPartial = mPartialUpdate;
     return NS_OK;
 }
 
 NS_IMETHODIMP
-nsOfflineCacheUpdate::AddURI(nsIURI *aURI, nsIDOMNode *aSource)
+nsOfflineCacheUpdate::GetManifestURI(nsIURI **aManifestURI)
 {
     NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
 
-    if (mState >= STATE_RUNNING)
+    NS_IF_ADDREF(*aManifestURI = mManifestURI);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetSucceeded(PRBool *aSucceeded)
+{
+    NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE);
+
+    *aSucceeded = mSucceeded;
+
+    return NS_OK;
+}
+
+nsresult
+nsOfflineCacheUpdate::AddURI(nsIURI *aURI, const nsACString &aOwnerSpec)
+{
+    NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+    if (mState >= STATE_DOWNLOADING)
         return NS_ERROR_NOT_AVAILABLE;
 
-    // only http and https urls can be put in the offline cache
-    PRBool match;
-    nsresult rv = aURI->SchemeIs("http", &match);
-    NS_ENSURE_SUCCESS(rv, rv);
+    // Manifest URIs must have the same scheme as the manifest.
+    nsCAutoString scheme;
+    aURI->GetScheme(scheme);
 
-    if (!match) {
-        rv = aURI->SchemeIs("https", &match);
-        NS_ENSURE_SUCCESS(rv, rv);
-        if (!match)
-            return NS_ERROR_ABORT;
-    }
+    PRBool match;
+    if (NS_FAILED(mManifestURI->SchemeIs(scheme.get(), &match)) || !match)
+        return NS_ERROR_FAILURE;
 
     // Save the cache key as an owned URI
     nsCAutoString spec;
-    rv = aURI->GetSpec(spec);
+    nsresult rv = aURI->GetSpec(spec);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // url fragments aren't used in cache keys
     nsCAutoString::const_iterator specStart, specEnd;
     spec.BeginReading(specStart);
     spec.EndReading(specEnd);
     if (FindCharInReadable('#', specStart, specEnd)) {
         spec.BeginReading(specEnd);
-        rv = mCacheSession->AddOwnedKey(mUpdateDomain, mOwnerURI,
+        rv = mCacheSession->AddOwnedKey(mUpdateDomain, aOwnerSpec,
                                         Substring(specEnd, specStart));
         NS_ENSURE_SUCCESS(rv, rv);
     } else {
-        rv = mCacheSession->AddOwnedKey(mUpdateDomain, mOwnerURI, spec);
+        rv = mCacheSession->AddOwnedKey(mUpdateDomain, aOwnerSpec, spec);
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
+    // Don't fetch the same URI twice.
+    for (PRUint32 i = 0; i < mItems.Length(); i++) {
+        PRBool equals;
+        if (NS_SUCCEEDED(mItems[i]->mURI->Equals(aURI, &equals)) && equals) {
+            return NS_OK;
+        }
+    }
+
     nsRefPtr<nsOfflineCacheUpdateItem> item =
-        new nsOfflineCacheUpdateItem(this, aURI, mReferrerURI,
-                                     aSource, mClientID);
+        new nsOfflineCacheUpdateItem(this, aURI, mDocumentURI, mClientID);
     if (!item) return NS_ERROR_OUT_OF_MEMORY;
 
     mItems.AppendElement(item);
     mAddedItems = PR_TRUE;
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
+nsOfflineCacheUpdate::AddDynamicURI(nsIURI *aURI)
+{
+    return AddURI(aURI, mDynamicOwnerSpec);
+}
+
+NS_IMETHODIMP
 nsOfflineCacheUpdate::GetCount(PRUint32 *aNumItems)
 {
     LOG(("nsOfflineCacheUpdate::GetNumItems [%p, num=%d]",
          this, mItems.Length()));
 
     NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
 
     *aNumItems = mItems.Length();
@@ -828,31 +1307,16 @@ nsOfflineCacheUpdate::Schedule()
 
     if (!service) {
         return NS_ERROR_FAILURE;
     }
 
     return service->Schedule(this);
 }
 
-NS_IMETHODIMP
-nsOfflineCacheUpdate::ScheduleOnDocumentStop(nsIDOMDocument *aDocument)
-{
-    LOG(("nsOfflineCacheUpdate::ScheduleOnDocumentStop [%p]", this));
-
-    nsOfflineCacheUpdateService* service =
-        nsOfflineCacheUpdateService::EnsureService();
-
-    if (!service) {
-        return NS_ERROR_FAILURE;
-    }
-
-    return service->ScheduleOnDocumentStop(this, aDocument);
-}
-
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdateService::nsISupports
 //-----------------------------------------------------------------------------
 
 NS_IMPL_ISUPPORTS4(nsOfflineCacheUpdateService,
                    nsIOfflineCacheUpdateService,
                    nsIWebProgressListener,
                    nsIObserver,
@@ -905,16 +1369,17 @@ nsOfflineCacheUpdateService::Init()
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     gOfflineCacheUpdateService = this;
 
     return NS_OK;
 }
 
+/* static */
 nsOfflineCacheUpdateService *
 nsOfflineCacheUpdateService::GetInstance()
 {
     if (!gOfflineCacheUpdateService) {
         gOfflineCacheUpdateService = new nsOfflineCacheUpdateService();
         if (!gOfflineCacheUpdateService)
             return nsnull;
         NS_ADDREF(gOfflineCacheUpdateService);
@@ -926,16 +1391,17 @@ nsOfflineCacheUpdateService::GetInstance
         return gOfflineCacheUpdateService;
     }
 
     NS_ADDREF(gOfflineCacheUpdateService);
 
     return gOfflineCacheUpdateService;
 }
 
+/* static */
 nsOfflineCacheUpdateService *
 nsOfflineCacheUpdateService::EnsureService()
 {
     if (!gOfflineCacheUpdateService) {
         // Make the service manager hold a long-lived reference to the service
         nsCOMPtr<nsIOfflineCacheUpdateService> service =
             do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID);
     }
@@ -960,24 +1426,28 @@ nsOfflineCacheUpdateService::Schedule(ns
 
     mUpdates.AppendElement(aUpdate);
 
     ProcessNextUpdate();
 
     return NS_OK;
 }
 
-nsresult
-nsOfflineCacheUpdateService::ScheduleOnDocumentStop(nsOfflineCacheUpdate *aUpdate,
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::ScheduleOnDocumentStop(nsIURI *aManifestURI,
+                                                    nsIURI *aDocumentURI,
                                                     nsIDOMDocument *aDocument)
 {
-    LOG(("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, update=%p, doc=%p]",
-         this, aUpdate, aDocument));
+    LOG(("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, manifestURI=%p, documentURI=%p doc=%p]",
+         this, aManifestURI, aDocumentURI, aDocument));
 
-    if (!mDocUpdates.Put(aDocument, aUpdate))
+    PendingUpdate *update = new PendingUpdate();
+    update->mManifestURI = aManifestURI;
+    update->mDocumentURI = aDocumentURI;
+    if (!mDocUpdates.Put(aDocument, update))
         return NS_ERROR_FAILURE;
 
     return NS_OK;
 }
 
 nsresult
 nsOfflineCacheUpdateService::UpdateFinished(nsOfflineCacheUpdate *aUpdate)
 {
@@ -1053,16 +1523,65 @@ nsOfflineCacheUpdateService::GetUpdate(P
         NS_ADDREF(*aUpdate = mUpdates[aIndex]);
     } else {
         *aUpdate = nsnull;
     }
 
     return NS_OK;
 }
 
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::ScheduleUpdate(nsIURI *aManifestURI,
+                                            nsIURI *aDocumentURI,
+                                            nsIOfflineCacheUpdate **aUpdate)
+{
+    // Check for existing updates
+    nsresult rv;
+    for (PRUint32 i = 0; i < mUpdates.Length(); i++) {
+        nsRefPtr<nsOfflineCacheUpdate> update = mUpdates[i];
+
+        PRBool partial;
+        rv = update->GetPartial(&partial);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        if (partial) {
+            // Partial updates aren't considered
+            continue;
+        }
+
+        nsCOMPtr<nsIURI> manifestURI;
+        update->GetManifestURI(getter_AddRefs(manifestURI));
+        if (manifestURI) {
+            PRBool equals;
+            rv = manifestURI->Equals(aManifestURI, &equals);
+            if (equals) {
+                NS_ADDREF(*aUpdate = update);
+                return NS_OK;
+            }
+        }
+    }
+
+    // There is no existing update, start one.
+
+    nsRefPtr<nsOfflineCacheUpdate> update = new nsOfflineCacheUpdate();
+    if (!update)
+        return NS_ERROR_OUT_OF_MEMORY;
+
+    rv = update->Init(PR_FALSE, aManifestURI, aDocumentURI);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = update->Schedule();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    NS_ADDREF(*aUpdate = update);
+
+    return NS_OK;
+}
+
 //-----------------------------------------------------------------------------
 // nsOfflineCacheUpdateService::nsIObserver
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsOfflineCacheUpdateService::Observe(nsISupports     *aSubject,
                                      const char      *aTopic,
                                      const PRUnichar *aData)
@@ -1109,19 +1628,26 @@ nsOfflineCacheUpdateService::OnStateChan
 
         nsCOMPtr<nsIDOMDocument> doc;
         window->GetDocument(getter_AddRefs(doc));
         if (!doc) return NS_OK;
 
         LOG(("nsOfflineCacheUpdateService::OnStateChange [%p, doc=%p]",
              this, doc.get()));
 
-        nsRefPtr<nsOfflineCacheUpdate> update;
-        if (mDocUpdates.Get(doc, getter_AddRefs(update))) {
-            Schedule(update);
+
+        PendingUpdate *pendingUpdate;
+        if (mDocUpdates.Get(doc, &pendingUpdate)) {
+            // Only schedule the update if the document loaded successfull
+            if (NS_SUCCEEDED(aStatus)) {
+                nsCOMPtr<nsIOfflineCacheUpdate> update;
+                ScheduleUpdate(pendingUpdate->mManifestURI,
+                               pendingUpdate->mDocumentURI,
+                               getter_AddRefs(update));
+            }
             mDocUpdates.Remove(doc);
         }
 
         return NS_OK;
     }
 
     return NS_OK;
 }
--- a/uriloader/prefetch/nsOfflineCacheUpdate.h
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.h
@@ -54,17 +54,17 @@
 #include "nsIObserverService.h"
 #include "nsIOfflineCacheSession.h"
 #include "nsIPrefetchService.h"
 #include "nsIRequestObserver.h"
 #include "nsIRunnable.h"
 #include "nsIStreamListener.h"
 #include "nsIURI.h"
 #include "nsIWebProgressListener.h"
-#include "nsRefPtrHashtable.h"
+#include "nsClassHashtable.h"
 #include "nsString.h"
 #include "nsTArray.h"
 #include "nsWeakReference.h"
 
 class nsOfflineCacheUpdate;
 
 class nsOfflineCacheUpdateItem : public nsIDOMLoadStatus
                                , public nsIStreamListener
@@ -79,79 +79,137 @@ public:
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSIRUNNABLE
     NS_DECL_NSIINTERFACEREQUESTOR
     NS_DECL_NSICHANNELEVENTSINK
 
     nsOfflineCacheUpdateItem(nsOfflineCacheUpdate *aUpdate,
                              nsIURI *aURI,
                              nsIURI *aReferrerURI,
-                             nsIDOMNode *aSource,
                              const nsACString &aClientID);
-    ~nsOfflineCacheUpdateItem();
+    virtual ~nsOfflineCacheUpdateItem();
 
     nsCOMPtr<nsIURI>           mURI;
     nsCOMPtr<nsIURI>           mReferrerURI;
-    nsCOMPtr<nsIWeakReference> mSource;
     nsCString                  mClientID;
 
     nsresult OpenChannel();
     nsresult Cancel();
 
 private:
     nsOfflineCacheUpdate*          mUpdate;
     nsCOMPtr<nsIChannel>           mChannel;
     PRUint16                       mState;
+
+protected:
     PRInt32                        mBytesRead;
 };
 
+
+class nsOfflineManifestItem : public nsOfflineCacheUpdateItem
+{
+public:
+    NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSIREQUESTOBSERVER
+
+    nsOfflineManifestItem(nsOfflineCacheUpdate *aUpdate,
+                          nsIURI *aURI,
+                          nsIURI *aReferrerURI,
+                          const nsACString &aClientID);
+    virtual ~nsOfflineManifestItem();
+
+    nsCOMArray<nsIURI> &GetExplicitURIs() { return mExplicitURIs; }
+
+    PRBool ParseSucceeded()
+        { return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR); }
+    PRBool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; }
+
+private:
+    static NS_METHOD ReadManifest(nsIInputStream *aInputStream,
+                                  void *aClosure,
+                                  const char *aFromSegment,
+                                  PRUint32 aOffset,
+                                  PRUint32 aCount,
+                                  PRUint32 *aBytesConsumed);
+
+    nsresult HandleManifestLine(const nsCString::const_iterator &aBegin,
+                                const nsCString::const_iterator &aEnd);
+
+    enum {
+        PARSE_INIT,
+        PARSE_CACHE_ENTRIES,
+        PARSE_FALLBACK_ENTRIES,
+        PARSE_NETWORK_ENTRIES,
+        PARSE_ERROR
+    } mParserState;
+
+    nsCString mReadBuf;
+    nsCOMArray<nsIURI> mExplicitURIs;
+    PRBool mNeedsUpdate;
+};
+
 class nsOfflineCacheUpdate : public nsIOfflineCacheUpdate
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIOFFLINECACHEUPDATE
 
     nsOfflineCacheUpdate();
     ~nsOfflineCacheUpdate();
 
     nsresult Init();
 
     nsresult Begin();
     nsresult Cancel();
 
     void LoadCompleted();
 private:
+    nsresult HandleManifest(PRBool *aDoUpdate);
+    nsresult AddURI(nsIURI *aURI, const nsACString &aOwnerSpec);
+
     nsresult ProcessNextURI();
+
     nsresult AddOwnedItems(const nsACString &aOwnerURI);
-    nsresult AddDomainItems();
-    nsresult NotifyAdded(nsOfflineCacheUpdateItem *aItem);
+
+    nsresult GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers);
+    nsresult NotifyError();
+    nsresult NotifyChecking();
+    nsresult NotifyNoUpdate();
+    nsresult NotifyDownloading();
+    nsresult NotifyStarted(nsOfflineCacheUpdateItem *aItem);
     nsresult NotifyCompleted(nsOfflineCacheUpdateItem *aItem);
     nsresult Finish();
 
     enum {
         STATE_UNINITIALIZED,
         STATE_INITIALIZED,
-        STATE_RUNNING,
+        STATE_CHECKING,
+        STATE_DOWNLOADING,
         STATE_CANCELLED,
         STATE_FINISHED
     } mState;
 
     PRBool mAddedItems;
     PRBool mPartialUpdate;
     PRBool mSucceeded;
     nsCString mUpdateDomain;
-    nsCString mOwnerURI;
-    nsCOMPtr<nsIURI> mReferrerURI;
+    nsCOMPtr<nsIURI> mManifestURI;
+    nsCString mManifestOwnerSpec;
+    nsCString mDynamicOwnerSpec;
+
+    nsCOMPtr<nsIURI> mDocumentURI;
 
     nsCString mClientID;
     nsCOMPtr<nsIOfflineCacheSession> mCacheSession;
     nsCOMPtr<nsIOfflineCacheSession> mMainCacheSession;
 
     nsCOMPtr<nsIObserverService> mObserverService;
 
+    nsRefPtr<nsOfflineManifestItem> mManifestItem;
+
     /* Items being updated */
     PRInt32 mCurrentItem;
     nsTArray<nsRefPtr<nsOfflineCacheUpdateItem> > mItems;
 
     /* Clients watching this update for changes */
     nsCOMArray<nsIWeakReference> mWeakObservers;
     nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers;
 };
@@ -168,32 +226,35 @@ public:
     NS_DECL_NSIOBSERVER
 
     nsOfflineCacheUpdateService();
     ~nsOfflineCacheUpdateService();
 
     nsresult Init();
 
     nsresult Schedule(nsOfflineCacheUpdate *aUpdate);
-    nsresult ScheduleOnDocumentStop(nsOfflineCacheUpdate *aUpdate,
-                                    nsIDOMDocument *aDocument);
     nsresult UpdateFinished(nsOfflineCacheUpdate *aUpdate);
 
     /**
      * Returns the singleton nsOfflineCacheUpdateService without an addref, or
      * nsnull if the service couldn't be created.
      */
     static nsOfflineCacheUpdateService *EnsureService();
 
     /** Addrefs and returns the singleton nsOfflineCacheUpdateService. */
     static nsOfflineCacheUpdateService *GetInstance();
     
 private:
     nsresult ProcessNextUpdate();
 
     nsTArray<nsRefPtr<nsOfflineCacheUpdate> > mUpdates;
-    nsRefPtrHashtable<nsVoidPtrHashKey, nsOfflineCacheUpdate> mDocUpdates;
+
+    struct PendingUpdate {
+        nsCOMPtr<nsIURI> mManifestURI;
+        nsCOMPtr<nsIURI> mDocumentURI;
+    };
+    nsClassHashtable<nsVoidPtrHashKey, PendingUpdate> mDocUpdates;
 
     PRBool mDisabled;
     PRBool mUpdateRunning;
 };
 
 #endif