Bug 1490464 - Fix XUL attribute persistence for browser.xhtml. r=smaug
authorBrendan Dahl <bdahl@mozilla.com>
Tue, 06 Nov 2018 21:45:20 +0000
changeset 501170 4350340785e41659616b77542a6632cdf7d72b4f
parent 501169 766330ff40408bc4df6879bbb5721f6bfb0655b5
child 501171 b00d8b617f0a85821e6350d68065340d600053c1
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1490464
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1490464 - Fix XUL attribute persistence for browser.xhtml. r=smaug Move XUL persistence handling into it's own class and make it a separate nsIDocumentObserver so it can also be used in non-XUL documents. To avoid adding persistence to all non-XUL documents, a document must add the "mozpersist" attribute to the root element if it wants enable the feature. Differential Revision: https://phabricator.services.mozilla.com/D6802
browser/base/content/browser.xul
dom/base/nsDocument.cpp
dom/base/nsIDocument.h
dom/xul/XULDocument.cpp
dom/xul/XULDocument.h
dom/xul/XULPersist.cpp
dom/xul/XULPersist.h
dom/xul/moz.build
xpcom/ds/StaticAtoms.py
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -68,16 +68,17 @@
         macanimationtype="document"
         screenX="4" screenY="4"
         fullscreenbutton="true"
         sizemode="normal"
         retargetdocumentfocus="urlbar"
         persist="screenX screenY width height sizemode"
 #ifdef BROWSER_XHTML
         hidden="true"
+        mozpersist=""
 #endif
         >
 
 # All JS files which are needed by browser.xul and other top level windows to
 # support MacOS specific features *must* go into the global-scripts.inc file so
 # that they can be shared with macWindow.inc.xul.
 #include global-scripts.inc
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -254,16 +254,17 @@
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/dom/SVGDocument.h"
 #include "mozilla/dom/SVGSVGElement.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/TabGroup.h"
 #ifdef MOZ_XUL
 #include "mozilla/dom/XULBroadcastManager.h"
+#include "mozilla/dom/XULPersist.h"
 #include "mozilla/dom/TreeBoxObject.h"
 #include "nsIXULWindow.h"
 #include "nsXULCommandDispatcher.h"
 #include "nsXULPopupManager.h"
 #include "nsIDocShellTreeOwner.h"
 #endif
 #include "nsIPresShellInlines.h"
 
@@ -1732,16 +1733,20 @@ nsDocument::~nsDocument()
   if (mStyleImageLoader) {
     mStyleImageLoader->DropDocumentReference();
   }
 
   if (mXULBroadcastManager) {
     mXULBroadcastManager->DropDocumentReference();
   }
 
+  if (mXULPersist) {
+    mXULPersist->DropDocumentReference();
+  }
+
   delete mHeaderData;
 
   ClearAllBoxObjects();
 
   mPendingTitleChangeEvent.Revoke();
 
   mPlugins.Clear();
 }
@@ -8967,16 +8972,23 @@ nsIDocument::SetReadyStateInternal(Ready
     }
   }
   // At the time of loading start, we don't have timing object, record time.
   if (READYSTATE_LOADING == rs) {
     mLoadingTimeStamp = mozilla::TimeStamp::Now();
   }
 
   if (READYSTATE_INTERACTIVE == rs) {
+    if (nsContentUtils::IsSystemPrincipal(NodePrincipal())) {
+      Element* root = GetRootElement();
+      if (root && root->HasAttr(kNameSpaceID_None, nsGkAtoms::mozpersist)) {
+        mXULPersist = new XULPersist(this);
+        mXULPersist->Init();
+      }
+    }
     TriggerInitialDocumentTranslation();
   }
 
   RecordNavigationTiming(rs);
 
   RefPtr<AsyncEventDispatcher> asyncDispatcher =
     new AsyncEventDispatcher(this,
                              NS_LITERAL_STRING("readystatechange"),
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -144,16 +144,17 @@ class Rule;
 } // namespace css
 
 namespace dom {
 class Animation;
 class AnonymousContent;
 class Attr;
 class BoxObject;
 class XULBroadcastManager;
+class XULPersist;
 class ClientInfo;
 class ClientState;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocGroup;
 class DocumentL10n;
 class DocumentFragment;
@@ -4751,16 +4752,17 @@ protected:
   uint32_t mThrowOnDynamicMarkupInsertionCounter;
 
   // Count of unload/beforeunload/pagehide operations in progress.
   uint32_t mIgnoreOpensDuringUnloadCounter;
 
   nsCOMPtr<nsIDOMXULCommandDispatcher> mCommandDispatcher; // [OWNER] of the focus tracker
 
   RefPtr<mozilla::dom::XULBroadcastManager> mXULBroadcastManager;
+  RefPtr<mozilla::dom::XULPersist> mXULPersist;
 
   // At the moment, trackers might be blocked by Tracking Protection or FastBlock.
   // In order to know the numbers of trackers detected and blocked, we add
   // these two values here and those are shared by TP and FB.
   uint32_t mNumTrackersFound;
   uint32_t mNumTrackersBlocked;
 
   mozilla::EnumSet<mozilla::Telemetry::LABELS_DOCUMENT_ANALYTICS_TRACKER_FASTBLOCKED>
--- a/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -77,16 +77,17 @@
 #include "nsURILoader.h"
 #include "mozilla/BasicEvents.h"
 #include "mozilla/dom/DocumentL10n.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/NodeInfoInlines.h"
 #include "mozilla/dom/ProcessingInstruction.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/XULDocumentBinding.h"
+#include "mozilla/dom/XULPersist.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/LoadInfo.h"
 #include "mozilla/Preferences.h"
 #include "nsTextNode.h"
 #include "nsJSUtils.h"
 #include "js/CompilationAndEvaluation.h"
 #include "js/SourceBufferHolder.h"
 #include "mozilla/dom/URL.h"
@@ -122,17 +123,16 @@ LazyLogModule XULDocument::gXULLog("XULD
 //
 
 namespace mozilla {
 namespace dom {
 
 XULDocument::XULDocument(void)
     : XMLDocument("application/vnd.mozilla.xul+xml"),
       mNextSrcLoadWaiter(nullptr),
-      mApplyingPersistedAttrs(false),
       mIsWritingFastLoad(false),
       mDocumentLoaded(false),
       mStillWalking(false),
       mPendingSheets(0),
       mCurrentScriptProto(nullptr),
       mOffThreadCompiling(false),
       mOffThreadCompileStringBuf(nullptr),
       mOffThreadCompileStringLength(0),
@@ -196,22 +196,20 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(XULDocume
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XULDocument, XMLDocument)
     NS_ASSERTION(!nsCCUncollectableMarker::InGeneration(cb, tmp->GetMarkedCCGeneration()),
                  "Shouldn't traverse XULDocument!");
     // XXX tmp->mContextStack?
 
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentPrototype)
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrototypes)
-    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocalStore)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XULDocument, XMLDocument)
-    NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocalStore)
     //XXX We should probably unlink all the objects we traverse.
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(XULDocument,
                                              XMLDocument,
                                              nsIStreamLoaderObserver,
                                              nsICSSLoaderObserver,
                                              nsIOffThreadScriptReceiver)
@@ -418,68 +416,16 @@ XULDocument::OnPrototypeLoadDone(bool aR
     if (NS_FAILED(rv)) return rv;
 
     if (aResumeWalk) {
         rv = ResumeWalk();
     }
     return rv;
 }
 
-static bool
-ShouldPersistAttribute(Element* aElement, nsAtom* aAttribute)
-{
-    if (aElement->IsXULElement(nsGkAtoms::window)) {
-        // This is not an element of the top document, its owner is
-        // not an nsXULWindow. Persist it.
-        if (aElement->OwnerDoc()->GetParentDocument()) {
-            return true;
-        }
-        // The following attributes of xul:window should be handled in
-        // nsXULWindow::SavePersistentAttributes instead of here.
-        if (aAttribute == nsGkAtoms::screenX ||
-            aAttribute == nsGkAtoms::screenY ||
-            aAttribute == nsGkAtoms::width ||
-            aAttribute == nsGkAtoms::height ||
-            aAttribute == nsGkAtoms::sizemode) {
-            return false;
-        }
-    }
-    return true;
-}
-
-void
-XULDocument::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
-                              nsAtom* aAttribute, int32_t aModType,
-                              const nsAttrValue* aOldValue)
-{
-    NS_ASSERTION(aElement->OwnerDoc() == this, "unexpected doc");
-
-    // Might not need this, but be safe for now.
-    nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
-
-    // See if there is anything we need to persist in the localstore.
-    //
-    // XXX Namespace handling broken :-(
-    nsAutoString persist;
-    aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::persist, persist);
-    // Persistence of attributes of xul:window is handled in nsXULWindow.
-    if (ShouldPersistAttribute(aElement, aAttribute) && !persist.IsEmpty() &&
-        // XXXldb This should check that it's a token, not just a substring.
-        persist.Find(nsDependentAtomString(aAttribute)) >= 0) {
-      nsContentUtils::AddScriptRunner(
-        NewRunnableMethod<Element*, int32_t, nsAtom*>(
-          "dom::XULDocument::Persist",
-          this,
-          &XULDocument::Persist,
-          aElement,
-          kNameSpaceID_None,
-          aAttribute));
-    }
-}
-
 void
 XULDocument::ContentAppended(nsIContent* aFirstNewContent)
 {
     NS_ASSERTION(aFirstNewContent->OwnerDoc() == this, "unexpected doc");
 
     // Might not need this, but be safe for now.
     nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
 
@@ -508,68 +454,16 @@ XULDocument::ContentRemoved(nsIContent* 
     // away, or something?
 }
 
 //----------------------------------------------------------------------
 //
 // nsIDocument interface
 //
 
-
-void
-XULDocument::Persist(Element* aElement, int32_t aNameSpaceID,
-                     nsAtom* aAttribute)
-{
-    // For non-chrome documents, persistance is simply broken
-    if (!nsContentUtils::IsSystemPrincipal(NodePrincipal()))
-        return;
-
-    if (!mLocalStore) {
-        mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-        if (NS_WARN_IF(!mLocalStore)) {
-            return;
-        }
-    }
-
-    nsAutoString id;
-
-    aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
-    nsAtomString attrstr(aAttribute);
-
-    nsAutoString valuestr;
-    aElement->GetAttr(kNameSpaceID_None, aAttribute, valuestr);
-
-    nsAutoCString utf8uri;
-    nsresult rv = mDocumentURI->GetSpec(utf8uri);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return;
-    }
-    NS_ConvertUTF8toUTF16 uri(utf8uri);
-
-    bool hasAttr;
-    rv = mLocalStore->HasValue(uri, id, attrstr, &hasAttr);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return;
-    }
-
-    if (hasAttr && valuestr.IsEmpty()) {
-        mLocalStore->RemoveValue(uri, id, attrstr);
-        return;
-    }
-
-    // Persisting attributes to top level windows is handled by nsXULWindow.
-    if (aElement->IsXULElement(nsGkAtoms::window)) {
-        if (nsCOMPtr<nsIXULWindow> win = GetXULWindowIfToplevelChrome()) {
-           return;
-        }
-    }
-
-    mLocalStore->SetValue(uri, id, attrstr, valuestr);
-}
-
 void
 XULDocument::AddElementToDocumentPost(Element* aElement)
 {
     if (aElement == GetRootElement()) {
         ResetDocumentDirection();
     }
 
     if (aElement->IsXULElement(nsGkAtoms::link)) {
@@ -716,155 +610,16 @@ XULDocument::PrepareToLoadPrototype(nsIU
                                kCharsetFromDocTypeDefault);
     parser->SetContentSink(sink); // grabs a reference to the parser
 
     parser.forget(aResult);
     return NS_OK;
 }
 
 
-nsresult
-XULDocument::ApplyPersistentAttributes()
-{
-    // For non-chrome documents, persistance is simply broken
-    if (!nsContentUtils::IsSystemPrincipal(NodePrincipal()))
-        return NS_ERROR_NOT_AVAILABLE;
-
-    // Add all of the 'persisted' attributes into the content
-    // model.
-    if (!mLocalStore) {
-        mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-        if (NS_WARN_IF(!mLocalStore)) {
-            return NS_ERROR_NOT_INITIALIZED;
-        }
-    }
-
-    mApplyingPersistedAttrs = true;
-    ApplyPersistentAttributesInternal();
-    mApplyingPersistedAttrs = false;
-
-    return NS_OK;
-}
-
-
-nsresult
-XULDocument::ApplyPersistentAttributesInternal()
-{
-    nsCOMArray<Element> elements;
-
-    nsAutoCString utf8uri;
-    nsresult rv = mDocumentURI->GetSpec(utf8uri);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
-    }
-    NS_ConvertUTF8toUTF16 uri(utf8uri);
-
-    // Get a list of element IDs for which persisted values are available
-    nsCOMPtr<nsIStringEnumerator> ids;
-    rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids));
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
-    }
-
-    while (1) {
-        bool hasmore = false;
-        ids->HasMore(&hasmore);
-        if (!hasmore) {
-            break;
-        }
-
-        nsAutoString id;
-        ids->GetNext(id);
-
-        nsIdentifierMapEntry* entry = mIdentifierMap.GetEntry(id);
-        if (!entry) {
-            continue;
-        }
-
-        // We want to hold strong refs to the elements while applying
-        // persistent attributes, just in case.
-        elements.Clear();
-        elements.SetCapacity(entry->GetIdElements().Length());
-        for (Element* element : entry->GetIdElements()) {
-            elements.AppendObject(element);
-        }
-        if (elements.IsEmpty()) {
-            continue;
-        }
-
-        rv = ApplyPersistentAttributesToElements(id, elements);
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-            return rv;
-        }
-    }
-
-    return NS_OK;
-}
-
-nsresult
-XULDocument::ApplyPersistentAttributesToElements(const nsAString &aID,
-                                                 nsCOMArray<Element>& aElements)
-{
-    nsAutoCString utf8uri;
-    nsresult rv = mDocumentURI->GetSpec(utf8uri);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
-    }
-    NS_ConvertUTF8toUTF16 uri(utf8uri);
-
-    // Get a list of attributes for which persisted values are available
-    nsCOMPtr<nsIStringEnumerator> attrs;
-    rv = mLocalStore->GetAttributeEnumerator(uri, aID, getter_AddRefs(attrs));
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
-    }
-
-    while (1) {
-        bool hasmore = PR_FALSE;
-        attrs->HasMore(&hasmore);
-        if (!hasmore) {
-            break;
-        }
-
-        nsAutoString attrstr;
-        attrs->GetNext(attrstr);
-
-        nsAutoString value;
-        rv = mLocalStore->GetValue(uri, aID, attrstr, value);
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-            return rv;
-        }
-
-        RefPtr<nsAtom> attr = NS_Atomize(attrstr);
-        if (NS_WARN_IF(!attr)) {
-            return NS_ERROR_OUT_OF_MEMORY;
-        }
-
-        uint32_t cnt = aElements.Count();
-        for (int32_t i = int32_t(cnt) - 1; i >= 0; --i) {
-            RefPtr<Element> element = aElements.SafeObjectAt(i);
-            if (!element) {
-                 continue;
-            }
-
-            // Applying persistent attributes to top level windows is handled
-            // by nsXULWindow.
-            if (element->IsXULElement(nsGkAtoms::window)) {
-                if (nsCOMPtr<nsIXULWindow> win = GetXULWindowIfToplevelChrome()) {
-                    continue;
-                }
-            }
-
-            Unused << element->SetAttr(kNameSpaceID_None, attr, value, true);
-        }
-    }
-
-    return NS_OK;
-}
-
 void
 XULDocument::TraceProtos(JSTracer* aTrc)
 {
     uint32_t i, count = mPrototypes.Length();
     for (i = 0; i < count; ++i) {
         mPrototypes[i]->TraceProtos(aTrc);
     }
 
@@ -1277,17 +1032,18 @@ XULDocument::ResumeWalk()
         // depleted. That means that the entire prototype has been
         // walked and content has been constructed.
         break;
     }
 
     // If we get here, there is nothing left for us to walk. The content
     // model is built and ready for layout.
 
-    ApplyPersistentAttributes();
+    mXULPersist = new XULPersist(this);
+    mXULPersist->Init();
 
     mStillWalking = false;
     if (mPendingSheets == 0) {
         rv = DoneWalking();
     }
     return rv;
 }
 
--- a/dom/xul/XULDocument.h
+++ b/dom/xul/XULDocument.h
@@ -14,17 +14,16 @@
 #include "mozilla/dom/XMLDocument.h"
 #include "mozilla/StyleSheet.h"
 #include "nsIContent.h"
 #include "nsCOMArray.h"
 #include "nsIURI.h"
 #include "nsIStreamListener.h"
 #include "nsIStreamLoader.h"
 #include "nsICSSLoaderObserver.h"
-#include "nsIXULStore.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/ScriptLoader.h"
 
 #include "js/TracingAPI.h"
 #include "js/TypeDecls.h"
 
 class nsPIWindowRoot;
@@ -78,17 +77,16 @@ public:
     virtual void SetContentType(const nsAString& aContentType) override;
 
     virtual void EndLoad() override;
 
     // nsIMutationObserver interface
     NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
     NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
     NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
-    NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
 
     /**
      * Notify the XUL document that a subtree has been added
      */
     void AddSubtreeToDocument(nsIContent* aContent);
     /**
      * This is invoked whenever the prototype for this document is loaded
      * and should be walked, regardless of whether the XUL cache is
@@ -141,49 +139,37 @@ protected:
                            nsIParser** aResult);
 
     nsresult
     PrepareToLoadPrototype(nsIURI* aURI,
                            const char* aCommand,
                            nsIPrincipal* aDocumentPrincipal,
                            nsIParser** aResult);
 
-    nsresult ApplyPersistentAttributes();
-    nsresult ApplyPersistentAttributesInternal();
-    nsresult ApplyPersistentAttributesToElements(const nsAString &aID,
-                                                 nsCOMArray<Element>& aElements);
-
     void AddElementToDocumentPost(Element* aElement);
 
     static void DirectionChanged(const char* aPrefName, XULDocument* aData);
 
     // pseudo constants
     static int32_t gRefCnt;
 
     static LazyLogModule gXULLog;
 
-    void
-    Persist(mozilla::dom::Element* aElement,
-            int32_t aNameSpaceID,
-            nsAtom* aAttribute);
-
     virtual JSObject* WrapNode(JSContext *aCx, JS::Handle<JSObject*> aGivenProto) override;
 
     // IMPORTANT: The ownership implicit in the following member
     // variables has been explicitly checked and set using nsCOMPtr
     // for owning pointers and raw COM interface pointers for weak
     // (ie, non owning) references. If you add any members to this
     // class, please make the ownership explicit (pinkerton, scc).
     // NOTE, THIS IS STILL IN PROGRESS, TALK TO PINK OR SCC BEFORE
     // CHANGING
 
     XULDocument*             mNextSrcLoadWaiter;  // [OWNER] but not COMPtr
 
-    nsCOMPtr<nsIXULStore>       mLocalStore;
-    bool                        mApplyingPersistedAttrs;
     bool                        mIsWritingFastLoad;
     bool                        mDocumentLoaded;
     /**
      * Since ResumeWalk is interruptible, it's possible that last
      * stylesheet finishes loading while the PD walk is still in
      * progress (waiting for an overlay to finish loading).
      * mStillWalking prevents DoneLoading (and StartLayout) from being
      * called in this situation.
new file mode 100644
--- /dev/null
+++ b/dom/xul/XULPersist.cpp
@@ -0,0 +1,285 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "XULPersist.h"
+
+#include "nsIXULStore.h"
+
+namespace mozilla {
+namespace dom {
+
+static bool
+ShouldPersistAttribute(Element* aElement, nsAtom* aAttribute)
+{
+  if (aElement->IsXULElement(nsGkAtoms::window)) {
+    // This is not an element of the top document, its owner is
+    // not an nsXULWindow. Persist it.
+    if (aElement->OwnerDoc()->GetParentDocument()) {
+      return true;
+    }
+    // The following attributes of xul:window should be handled in
+    // nsXULWindow::SavePersistentAttributes instead of here.
+    if (aAttribute == nsGkAtoms::screenX ||
+        aAttribute == nsGkAtoms::screenY ||
+        aAttribute == nsGkAtoms::width ||
+        aAttribute == nsGkAtoms::height ||
+        aAttribute == nsGkAtoms::sizemode) {
+      return false;
+    }
+  }
+  return true;
+}
+
+NS_IMPL_ISUPPORTS(XULPersist, nsIDocumentObserver)
+
+XULPersist::XULPersist(nsIDocument* aDocument)
+  : nsStubDocumentObserver(),
+    mDocument(aDocument)
+{
+}
+
+XULPersist::~XULPersist()
+{
+}
+
+void
+XULPersist::Init()
+{
+  ApplyPersistentAttributes();
+  mDocument->AddObserver(this);
+}
+
+void
+XULPersist::DropDocumentReference()
+{
+  mDocument->RemoveObserver(this);
+  mDocument = nullptr;
+}
+
+void
+XULPersist::AttributeChanged(dom::Element* aElement,
+                             int32_t aNameSpaceID,
+                             nsAtom* aAttribute,
+                             int32_t aModType,
+                             const nsAttrValue* aOldValue)
+{
+  NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc");
+
+  // Might not need this, but be safe for now.
+  nsCOMPtr<nsIDocumentObserver> kungFuDeathGrip(this);
+
+  // See if there is anything we need to persist in the localstore.
+  //
+  // XXX Namespace handling broken :-(
+  nsAutoString persist;
+  aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::persist, persist);
+  // Persistence of attributes of xul:window is handled in nsXULWindow.
+  if (ShouldPersistAttribute(aElement, aAttribute) && !persist.IsEmpty() &&
+      // XXXldb This should check that it's a token, not just a substring.
+      persist.Find(nsDependentAtomString(aAttribute)) >= 0) {
+    nsContentUtils::AddScriptRunner(
+      NewRunnableMethod<Element*, int32_t, nsAtom*>(
+        "dom::XULPersist::Persist",
+        this,
+        &XULPersist::Persist,
+        aElement,
+        kNameSpaceID_None,
+        aAttribute));
+  }
+}
+
+void
+XULPersist::Persist(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute)
+{
+  if (!mDocument) {
+    return;
+  }
+  // For non-chrome documents, persistance is simply broken
+  if (!nsContentUtils::IsSystemPrincipal(mDocument->NodePrincipal())) {
+    return;
+  }
+
+  if (!mLocalStore) {
+    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
+    if (NS_WARN_IF(!mLocalStore)) {
+      return;
+    }
+  }
+
+  nsAutoString id;
+
+  aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
+  nsAtomString attrstr(aAttribute);
+
+  nsAutoString valuestr;
+  aElement->GetAttr(kNameSpaceID_None, aAttribute, valuestr);
+
+  nsAutoCString utf8uri;
+  nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return;
+  }
+  NS_ConvertUTF8toUTF16 uri(utf8uri);
+
+  bool hasAttr;
+  rv = mLocalStore->HasValue(uri, id, attrstr, &hasAttr);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return;
+  }
+
+  if (hasAttr && valuestr.IsEmpty()) {
+      mLocalStore->RemoveValue(uri, id, attrstr);
+    return;
+  }
+
+  // Persisting attributes to top level windows is handled by nsXULWindow.
+  if (aElement->IsXULElement(nsGkAtoms::window)) {
+    if (nsCOMPtr<nsIXULWindow> win = mDocument->GetXULWindowIfToplevelChrome()) {
+      return;
+    }
+  }
+
+  mLocalStore->SetValue(uri, id, attrstr, valuestr);
+}
+
+nsresult
+XULPersist::ApplyPersistentAttributes()
+{
+  if (!mDocument) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  // For non-chrome documents, persistance is simply broken
+  if (!nsContentUtils::IsSystemPrincipal(mDocument->NodePrincipal())) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // Add all of the 'persisted' attributes into the content
+  // model.
+  if (!mLocalStore) {
+    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
+    if (NS_WARN_IF(!mLocalStore)) {
+      return NS_ERROR_NOT_INITIALIZED;
+    }
+  }
+
+  ApplyPersistentAttributesInternal();
+
+  return NS_OK;
+}
+
+nsresult
+XULPersist::ApplyPersistentAttributesInternal()
+{
+  nsCOMArray<Element> elements;
+
+  nsAutoCString utf8uri;
+  nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  NS_ConvertUTF8toUTF16 uri(utf8uri);
+
+  // Get a list of element IDs for which persisted values are available
+  nsCOMPtr<nsIStringEnumerator> ids;
+  rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  while (1) {
+    bool hasmore = false;
+    ids->HasMore(&hasmore);
+    if (!hasmore) {
+      break;
+    }
+
+    nsAutoString id;
+    ids->GetNext(id);
+
+    // We want to hold strong refs to the elements while applying
+    // persistent attributes, just in case.
+    const nsTArray<Element*>* allElements = mDocument->GetAllElementsForId(id);
+    if (!allElements) {
+      continue;
+    }
+    elements.Clear();
+    elements.SetCapacity(allElements->Length());
+    for (Element* element : *allElements) {
+      elements.AppendObject(element);
+    }
+
+    rv = ApplyPersistentAttributesToElements(id, elements);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+XULPersist::ApplyPersistentAttributesToElements(const nsAString &aID,
+                                                nsCOMArray<Element>& aElements)
+{
+  nsAutoCString utf8uri;
+  nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  NS_ConvertUTF8toUTF16 uri(utf8uri);
+
+  // Get a list of attributes for which persisted values are available
+  nsCOMPtr<nsIStringEnumerator> attrs;
+  rv = mLocalStore->GetAttributeEnumerator(uri, aID, getter_AddRefs(attrs));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  while (1) {
+    bool hasmore = PR_FALSE;
+    attrs->HasMore(&hasmore);
+    if (!hasmore) {
+      break;
+    }
+
+    nsAutoString attrstr;
+    attrs->GetNext(attrstr);
+
+    nsAutoString value;
+    rv = mLocalStore->GetValue(uri, aID, attrstr, value);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    RefPtr<nsAtom> attr = NS_Atomize(attrstr);
+    if (NS_WARN_IF(!attr)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    uint32_t cnt = aElements.Length();
+    for (int32_t i = int32_t(cnt) - 1; i >= 0; --i) {
+      Element* element = aElements.SafeElementAt(i);
+      if (!element) {
+        continue;
+      }
+
+      // Applying persistent attributes to top level windows is handled
+      // by nsXULWindow.
+      if (element->IsXULElement(nsGkAtoms::window)) {
+        if (nsCOMPtr<nsIXULWindow> win = mDocument->GetXULWindowIfToplevelChrome()) {
+          continue;
+        }
+      }
+
+      Unused << element->SetAttr(kNameSpaceID_None, attr, value, true);
+    }
+  }
+
+  return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/xul/XULPersist.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_XULPersist_h
+#define mozilla_dom_XULPersist_h
+
+class nsIXULStore;
+
+namespace mozilla {
+namespace dom {
+
+class XULPersist final : public nsStubDocumentObserver
+{
+
+public:
+  NS_DECL_ISUPPORTS
+
+  explicit XULPersist(nsIDocument* aDocument);
+  void Init();
+  void DropDocumentReference();
+
+  NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+
+protected:
+
+  void Persist(mozilla::dom::Element* aElement,
+               int32_t aNameSpaceID,
+               nsAtom* aAttribute);
+
+private:
+  ~XULPersist();
+  nsresult ApplyPersistentAttributes();
+  nsresult ApplyPersistentAttributesInternal();
+  nsresult ApplyPersistentAttributesToElements(const nsAString &aID,
+                                               nsCOMArray<Element>& aElements);
+
+  nsCOMPtr<nsIXULStore> mLocalStore;
+  // A weak pointer to our document. Nulled out by DropDocumentReference.
+  nsIDocument* MOZ_NON_OWNING_REF mDocument;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+
+#endif // mozilla_dom_XULPersist_h
--- a/dom/xul/moz.build
+++ b/dom/xul/moz.build
@@ -20,16 +20,17 @@ if CONFIG['MOZ_XUL']:
         'nsXULElement.h',
         'nsXULSortService.h',
     ]
 
     EXPORTS.mozilla.dom += [
         'XULBroadcastManager.h',
         'XULFrameElement.h',
         'XULMenuElement.h',
+        'XULPersist.h',
         'XULPopupElement.h',
         'XULScrollElement.h',
         'XULTextElement.h',
         'XULTooltipElement.h',
     ]
 
     UNIFIED_SOURCES += [
         'nsXULCommandDispatcher.cpp',
@@ -39,16 +40,17 @@ if CONFIG['MOZ_XUL']:
         'nsXULPopupListener.cpp',
         'nsXULPrototypeCache.cpp',
         'nsXULPrototypeDocument.cpp',
         'nsXULSortService.cpp',
         'XULBroadcastManager.cpp',
         'XULDocument.cpp',
         'XULFrameElement.cpp',
         'XULMenuElement.cpp',
+        'XULPersist.cpp',
         'XULPopupElement.cpp',
         'XULScrollElement.cpp',
         'XULTextElement.cpp',
         'XULTooltipElement.cpp',
     ]
 
 XPIDL_SOURCES += [
     'nsIController.idl',
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -34,16 +34,17 @@ STATIC_ATOMS = [
     Atom("_moz_abspos", "_moz_abspos"),
     Atom("_moz_activated", "_moz_activated"),
     Atom("_moz_anonclass", "_moz_anonclass"),
     Atom("_moz_resizing", "_moz_resizing"),
     Atom("mozallowfullscreen", "mozallowfullscreen"),
     Atom("moztype", "_moz-type"),
     Atom("mozdirty", "_moz_dirty"),
     Atom("mozdisallowselectionprint", "mozdisallowselectionprint"),
+    Atom("mozpersist", "mozpersist"),
     Atom("mozdonotsend", "moz-do-not-send"),
     Atom("mozeditorbogusnode", "_moz_editor_bogus_node"),
     Atom("mozgeneratedcontentbefore", "_moz_generated_content_before"),
     Atom("mozgeneratedcontentafter", "_moz_generated_content_after"),
     Atom("mozgeneratedcontentimage", "_moz_generated_content_image"),
     Atom("mozquote", "_moz_quote"),
     Atom("mozsignature", "moz-signature"),
     Atom("_moz_is_glyph", "-moz-is-glyph"),