summaryrefslogtreecommitdiffstats
path: root/dom/xul/XULBroadcastManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/xul/XULBroadcastManager.cpp')
-rw-r--r--dom/xul/XULBroadcastManager.cpp589
1 files changed, 589 insertions, 0 deletions
diff --git a/dom/xul/XULBroadcastManager.cpp b/dom/xul/XULBroadcastManager.cpp
new file mode 100644
index 0000000000..c9afefd0c8
--- /dev/null
+++ b/dom/xul/XULBroadcastManager.cpp
@@ -0,0 +1,589 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 sw=2 et 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/. */
+
+#include "XULBroadcastManager.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsXULElement.h"
+
+struct BroadcastListener {
+ nsWeakPtr mListener;
+ RefPtr<nsAtom> mAttribute;
+};
+
+struct BroadcasterMapEntry : public PLDHashEntryHdr {
+ mozilla::dom::Element* mBroadcaster; // [WEAK]
+ nsTArray<BroadcastListener*>
+ mListeners; // [OWNING] of BroadcastListener objects
+};
+
+struct nsAttrNameInfo {
+ nsAttrNameInfo(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix)
+ : mNamespaceID(aNamespaceID), mName(aName), mPrefix(aPrefix) {}
+ nsAttrNameInfo(const nsAttrNameInfo& aOther) = delete;
+ nsAttrNameInfo(nsAttrNameInfo&& aOther) = default;
+
+ int32_t mNamespaceID;
+ RefPtr<nsAtom> mName;
+ RefPtr<nsAtom> mPrefix;
+};
+
+static void ClearBroadcasterMapEntry(PLDHashTable* aTable,
+ PLDHashEntryHdr* aEntry) {
+ BroadcasterMapEntry* entry = static_cast<BroadcasterMapEntry*>(aEntry);
+ for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
+ delete entry->mListeners[i];
+ }
+ entry->mListeners.Clear();
+
+ // N.B. that we need to manually run the dtor because we
+ // constructed the nsTArray object in-place.
+ entry->mListeners.~nsTArray<BroadcastListener*>();
+}
+
+static bool CanBroadcast(int32_t aNameSpaceID, nsAtom* aAttribute) {
+ // Don't push changes to the |id|, |persist|, |command| or
+ // |observes| attribute.
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if ((aAttribute == nsGkAtoms::id) || (aAttribute == nsGkAtoms::persist) ||
+ (aAttribute == nsGkAtoms::command) ||
+ (aAttribute == nsGkAtoms::observes)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+namespace mozilla::dom {
+static LazyLogModule sXULBroadCastManager("XULBroadcastManager");
+
+class XULBroadcastManager::nsDelayedBroadcastUpdate {
+ public:
+ nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
+ const nsAString& aAttr)
+ : mBroadcaster(aBroadcaster),
+ mListener(aListener),
+ mAttr(aAttr),
+ mSetAttr(false),
+ mNeedsAttrChange(false) {}
+
+ nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
+ nsAtom* aAttrName, const nsAString& aAttr,
+ bool aSetAttr, bool aNeedsAttrChange)
+ : mBroadcaster(aBroadcaster),
+ mListener(aListener),
+ mAttr(aAttr),
+ mAttrName(aAttrName),
+ mSetAttr(aSetAttr),
+ mNeedsAttrChange(aNeedsAttrChange) {}
+
+ nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate& aOther) = delete;
+ nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate&& aOther) = default;
+
+ RefPtr<Element> mBroadcaster;
+ RefPtr<Element> mListener;
+ // Note if mAttrName isn't used, this is the name of the attr, otherwise
+ // this is the value of the attribute.
+ nsString mAttr;
+ RefPtr<nsAtom> mAttrName;
+ bool mSetAttr;
+ bool mNeedsAttrChange;
+
+ class Comparator {
+ public:
+ static bool Equals(const nsDelayedBroadcastUpdate& a,
+ const nsDelayedBroadcastUpdate& b) {
+ return a.mBroadcaster == b.mBroadcaster && a.mListener == b.mListener &&
+ a.mAttrName == b.mAttrName;
+ }
+ };
+};
+
+/* static */
+bool XULBroadcastManager::MayNeedListener(const Element& aElement) {
+ if (aElement.NodeInfo()->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
+ return true;
+ }
+ if (aElement.HasAttr(nsGkAtoms::observes)) {
+ return true;
+ }
+ if (aElement.HasAttr(nsGkAtoms::command) &&
+ !(aElement.NodeInfo()->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
+ aElement.NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL))) {
+ return true;
+ }
+ return false;
+}
+
+XULBroadcastManager::XULBroadcastManager(Document* aDocument)
+ : mDocument(aDocument),
+ mBroadcasterMap(nullptr),
+ mHandlingDelayedAttrChange(false),
+ mHandlingDelayedBroadcasters(false) {}
+
+XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap; }
+
+void XULBroadcastManager::DropDocumentReference(void) { mDocument = nullptr; }
+
+void XULBroadcastManager::SynchronizeBroadcastListener(Element* aBroadcaster,
+ Element* aListener,
+ const nsAString& aAttr) {
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ mDelayedBroadcasters.EmplaceBack(aBroadcaster, aListener, aAttr);
+ MaybeBroadcast();
+ return;
+ }
+ bool notify = mHandlingDelayedBroadcasters;
+
+ if (aAttr.EqualsLiteral("*")) {
+ uint32_t count = aBroadcaster->GetAttrCount();
+ nsTArray<nsAttrNameInfo> attributes(count);
+ for (uint32_t i = 0; i < count; ++i) {
+ const nsAttrName* attrName = aBroadcaster->GetAttrNameAt(i);
+ int32_t nameSpaceID = attrName->NamespaceID();
+ nsAtom* name = attrName->LocalName();
+
+ // _Don't_ push the |id|, |ref|, or |persist| attribute's value!
+ if (!CanBroadcast(nameSpaceID, name)) continue;
+
+ attributes.AppendElement(
+ nsAttrNameInfo(nameSpaceID, name, attrName->GetPrefix()));
+ }
+
+ count = attributes.Length();
+ while (count-- > 0) {
+ int32_t nameSpaceID = attributes[count].mNamespaceID;
+ nsAtom* name = attributes[count].mName;
+ nsAutoString value;
+ if (aBroadcaster->GetAttr(nameSpaceID, name, value)) {
+ aListener->SetAttr(nameSpaceID, name, attributes[count].mPrefix, value,
+ notify);
+ }
+
+#if 0
+ // XXX we don't fire the |onbroadcast| handler during
+ // initial hookup: doing so would potentially run the
+ // |onbroadcast| handler before the |onload| handler,
+ // which could define JS properties that mask XBL
+ // properties, etc.
+ ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
+#endif
+ }
+ } else {
+ // Find out if the attribute is even present at all.
+ RefPtr<nsAtom> name = NS_Atomize(aAttr);
+
+ nsAutoString value;
+ if (aBroadcaster->GetAttr(name, value)) {
+ aListener->SetAttr(kNameSpaceID_None, name, value, notify);
+ } else {
+ aListener->UnsetAttr(kNameSpaceID_None, name, notify);
+ }
+
+#if 0
+ // XXX we don't fire the |onbroadcast| handler during initial
+ // hookup: doing so would potentially run the |onbroadcast|
+ // handler before the |onload| handler, which could define JS
+ // properties that mask XBL properties, etc.
+ ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
+#endif
+ }
+}
+
+void XULBroadcastManager::AddListenerFor(Element& aBroadcaster,
+ Element& aListener,
+ const nsAString& aAttr,
+ ErrorResult& aRv) {
+ if (!mDocument) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, &aBroadcaster);
+
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ rv = nsContentUtils::CheckSameOrigin(mDocument, &aListener);
+
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ static const PLDHashTableOps gOps = {
+ PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub,
+ PLDHashTable::MoveEntryStub, ClearBroadcasterMapEntry, nullptr};
+
+ if (!mBroadcasterMap) {
+ mBroadcasterMap = new PLDHashTable(&gOps, sizeof(BroadcasterMapEntry));
+ }
+
+ auto entry =
+ static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
+ if (!entry) {
+ entry = static_cast<BroadcasterMapEntry*>(
+ mBroadcasterMap->Add(&aBroadcaster, fallible));
+
+ if (!entry) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ entry->mBroadcaster = &aBroadcaster;
+
+ // N.B. placement new to construct the nsTArray object in-place
+ new (&entry->mListeners) nsTArray<BroadcastListener*>();
+ }
+
+ // Only add the listener if it's not there already!
+ RefPtr<nsAtom> attr = NS_Atomize(aAttr);
+
+ for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
+ BroadcastListener* bl = entry->mListeners[i];
+ nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);
+
+ if (blListener == &aListener && bl->mAttribute == attr) return;
+ }
+
+ BroadcastListener* bl = new BroadcastListener;
+ bl->mListener = do_GetWeakReference(&aListener);
+ bl->mAttribute = attr;
+
+ entry->mListeners.AppendElement(bl);
+
+ SynchronizeBroadcastListener(&aBroadcaster, &aListener, aAttr);
+}
+
+void XULBroadcastManager::RemoveListenerFor(Element& aBroadcaster,
+ Element& aListener,
+ const nsAString& aAttr) {
+ // If we haven't added any broadcast listeners, then there sure
+ // aren't any to remove.
+ if (!mBroadcasterMap) return;
+
+ auto entry =
+ static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
+ if (entry) {
+ RefPtr<nsAtom> attr = NS_Atomize(aAttr);
+ for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
+ BroadcastListener* bl = entry->mListeners[i];
+ nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);
+
+ if (blListener == &aListener && bl->mAttribute == attr) {
+ entry->mListeners.RemoveElementAt(i);
+ delete bl;
+
+ if (entry->mListeners.IsEmpty()) mBroadcasterMap->RemoveEntry(entry);
+
+ break;
+ }
+ }
+ }
+}
+
+nsresult XULBroadcastManager::ExecuteOnBroadcastHandlerFor(
+ Element* aBroadcaster, Element* aListener, nsAtom* aAttr) {
+ if (!mDocument) {
+ return NS_OK;
+ }
+ // Now we execute the onchange handler in the context of the
+ // observer. We need to find the observer in order to
+ // execute the handler.
+
+ for (nsCOMPtr<nsIContent> child = aListener->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ // Look for an <observes> element beneath the listener. This
+ // ought to have an |element| attribute that refers to
+ // aBroadcaster, and an |attribute| element that tells us what
+ // attriubtes we're listening for.
+ if (!child->IsXULElement(nsGkAtoms::observes)) continue;
+
+ // Is this the element that was listening to us?
+ nsAutoString listeningToID;
+ child->AsElement()->GetAttr(nsGkAtoms::element, listeningToID);
+
+ nsAutoString broadcasterID;
+ aBroadcaster->GetAttr(nsGkAtoms::id, broadcasterID);
+
+ if (listeningToID != broadcasterID) continue;
+
+ // We are observing the broadcaster, but is this the right
+ // attribute?
+ nsAutoString listeningToAttribute;
+ child->AsElement()->GetAttr(nsGkAtoms::attribute, listeningToAttribute);
+
+ if (!aAttr->Equals(listeningToAttribute) &&
+ !listeningToAttribute.EqualsLiteral("*")) {
+ continue;
+ }
+
+ // This is the right <observes> element. Execute the
+ // |onbroadcast| event handler
+ WidgetEvent event(true, eXULBroadcast);
+
+ if (RefPtr<nsPresContext> presContext = mDocument->GetPresContext()) {
+ // Handle the DOM event
+ nsEventStatus status = nsEventStatus_eIgnore;
+ EventDispatcher::Dispatch(child, presContext, &event, nullptr, &status);
+ }
+ }
+
+ return NS_OK;
+}
+
+void XULBroadcastManager::AttributeChanged(Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute) {
+ if (!mDocument) {
+ return;
+ }
+ NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc");
+
+ // Synchronize broadcast listeners
+ if (mBroadcasterMap && CanBroadcast(aNameSpaceID, aAttribute)) {
+ auto entry =
+ static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(aElement));
+
+ if (entry) {
+ // We've got listeners: push the value.
+ nsAutoString value;
+ bool attrSet = aElement->GetAttr(aAttribute, value);
+
+ for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
+ BroadcastListener* bl = entry->mListeners[i];
+ if ((bl->mAttribute == aAttribute) ||
+ (bl->mAttribute == nsGkAtoms::_asterisk)) {
+ nsCOMPtr<Element> listenerEl = do_QueryReferent(bl->mListener);
+ if (listenerEl) {
+ nsAutoString currentValue;
+ bool hasAttr = listenerEl->GetAttr(aAttribute, currentValue);
+ // We need to update listener only if we're
+ // (1) removing an existing attribute,
+ // (2) adding a new attribute or
+ // (3) changing the value of an attribute.
+ bool needsAttrChange =
+ attrSet != hasAttr || !value.Equals(currentValue);
+ nsDelayedBroadcastUpdate delayedUpdate(aElement, listenerEl,
+ aAttribute, value, attrSet,
+ needsAttrChange);
+
+ size_t index = mDelayedAttrChangeBroadcasts.IndexOf(
+ delayedUpdate, 0, nsDelayedBroadcastUpdate::Comparator());
+ if (index != mDelayedAttrChangeBroadcasts.NoIndex) {
+ if (mHandlingDelayedAttrChange) {
+ NS_WARNING("Broadcasting loop!");
+ continue;
+ }
+ mDelayedAttrChangeBroadcasts.RemoveElementAt(index);
+ }
+
+ mDelayedAttrChangeBroadcasts.AppendElement(
+ std::move(delayedUpdate));
+ }
+ }
+ }
+ }
+ }
+}
+
+void XULBroadcastManager::MaybeBroadcast() {
+ // Only broadcast when not in an update and when safe to run scripts.
+ if (mDocument && mDocument->UpdateNestingLevel() == 0 &&
+ (mDelayedAttrChangeBroadcasts.Length() ||
+ mDelayedBroadcasters.Length())) {
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ if (mDocument) {
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this,
+ &XULBroadcastManager::MaybeBroadcast));
+ }
+ return;
+ }
+ if (!mHandlingDelayedAttrChange) {
+ mHandlingDelayedAttrChange = true;
+ for (uint32_t i = 0; i < mDelayedAttrChangeBroadcasts.Length(); ++i) {
+ RefPtr<nsAtom> attrName = mDelayedAttrChangeBroadcasts[i].mAttrName;
+ RefPtr<Element> listener = mDelayedAttrChangeBroadcasts[i].mListener;
+ if (mDelayedAttrChangeBroadcasts[i].mNeedsAttrChange) {
+ const nsString& value = mDelayedAttrChangeBroadcasts[i].mAttr;
+ if (mDelayedAttrChangeBroadcasts[i].mSetAttr) {
+ listener->SetAttr(kNameSpaceID_None, attrName, value, true);
+ } else {
+ listener->UnsetAttr(kNameSpaceID_None, attrName, true);
+ }
+ }
+ RefPtr<Element> broadcaster =
+ mDelayedAttrChangeBroadcasts[i].mBroadcaster;
+ ExecuteOnBroadcastHandlerFor(broadcaster, listener, attrName);
+ }
+ mDelayedAttrChangeBroadcasts.Clear();
+ mHandlingDelayedAttrChange = false;
+ }
+
+ uint32_t length = mDelayedBroadcasters.Length();
+ if (length) {
+ bool oldValue = mHandlingDelayedBroadcasters;
+ mHandlingDelayedBroadcasters = true;
+ nsTArray<nsDelayedBroadcastUpdate> delayedBroadcasters =
+ std::move(mDelayedBroadcasters);
+ for (uint32_t i = 0; i < length; ++i) {
+ SynchronizeBroadcastListener(delayedBroadcasters[i].mBroadcaster,
+ delayedBroadcasters[i].mListener,
+ delayedBroadcasters[i].mAttr);
+ }
+ mHandlingDelayedBroadcasters = oldValue;
+ }
+ }
+}
+
+nsresult XULBroadcastManager::FindBroadcaster(Element* aElement,
+ Element** aListener,
+ nsString& aBroadcasterID,
+ nsString& aAttribute,
+ Element** aBroadcaster) {
+ NodeInfo* ni = aElement->NodeInfo();
+ *aListener = nullptr;
+ *aBroadcaster = nullptr;
+
+ if (ni->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
+ // It's an <observes> element, which means that the actual
+ // listener is the _parent_ node. This element should have an
+ // 'element' attribute that specifies the ID of the
+ // broadcaster element, and an 'attribute' element, which
+ // specifies the name of the attribute to observe.
+ nsIContent* parent = aElement->GetParent();
+ if (!parent) {
+ // <observes> is the root element
+ return NS_FINDBROADCASTER_NOT_FOUND;
+ }
+
+ *aListener = Element::FromNode(parent);
+ NS_IF_ADDREF(*aListener);
+
+ aElement->GetAttr(nsGkAtoms::element, aBroadcasterID);
+ if (aBroadcasterID.IsEmpty()) {
+ return NS_FINDBROADCASTER_NOT_FOUND;
+ }
+ aElement->GetAttr(nsGkAtoms::attribute, aAttribute);
+ } else {
+ // It's a generic element, which means that we'll use the
+ // value of the 'observes' attribute to determine the ID of
+ // the broadcaster element, and we'll watch _all_ of its
+ // values.
+ aElement->GetAttr(nsGkAtoms::observes, aBroadcasterID);
+
+ // Bail if there's no aBroadcasterID
+ if (aBroadcasterID.IsEmpty()) {
+ // Try the command attribute next.
+ aElement->GetAttr(nsGkAtoms::command, aBroadcasterID);
+ if (!aBroadcasterID.IsEmpty()) {
+ // We've got something in the command attribute. We
+ // only treat this as a normal broadcaster if we are
+ // not a menuitem or a key.
+
+ if (ni->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
+ ni->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) {
+ return NS_FINDBROADCASTER_NOT_FOUND;
+ }
+ } else {
+ return NS_FINDBROADCASTER_NOT_FOUND;
+ }
+ }
+
+ *aListener = aElement;
+ NS_ADDREF(*aListener);
+
+ aAttribute.Assign('*');
+ }
+
+ // Make sure we got a valid listener.
+ NS_ENSURE_TRUE(*aListener, NS_ERROR_UNEXPECTED);
+
+ // Try to find the broadcaster element in the document.
+ Document* doc = aElement->GetComposedDoc();
+ if (doc) {
+ *aBroadcaster = doc->GetElementById(aBroadcasterID);
+ }
+
+ // The broadcaster element is missing.
+ if (!*aBroadcaster) {
+ return NS_FINDBROADCASTER_NOT_FOUND;
+ }
+
+ NS_ADDREF(*aBroadcaster);
+
+ return NS_FINDBROADCASTER_FOUND;
+}
+
+nsresult XULBroadcastManager::UpdateListenerHookup(Element* aElement,
+ HookupAction aAction) {
+ // Resolve a broadcaster hookup. Look at the element that we're
+ // trying to resolve: it could be an '<observes>' element, or just
+ // a vanilla element with an 'observes' attribute on it.
+ nsresult rv;
+
+ nsCOMPtr<Element> listener;
+ nsAutoString broadcasterID;
+ nsAutoString attribute;
+ nsCOMPtr<Element> broadcaster;
+
+ rv = FindBroadcaster(aElement, getter_AddRefs(listener), broadcasterID,
+ attribute, getter_AddRefs(broadcaster));
+ switch (rv) {
+ case NS_FINDBROADCASTER_NOT_FOUND:
+ return NS_OK;
+ case NS_FINDBROADCASTER_FOUND:
+ break;
+ default:
+ return rv;
+ }
+
+ NS_ENSURE_ARG(broadcaster && listener);
+ if (aAction == eHookupAdd) {
+ ErrorResult domRv;
+ AddListenerFor(*broadcaster, *listener, attribute, domRv);
+ if (domRv.Failed()) {
+ return domRv.StealNSResult();
+ }
+ } else {
+ RemoveListenerFor(*broadcaster, *listener, attribute);
+ }
+
+ // Tell the world we succeeded
+ if (MOZ_LOG_TEST(sXULBroadCastManager, LogLevel::Debug)) {
+ nsCOMPtr<nsIContent> content = listener;
+ NS_ASSERTION(content != nullptr, "not an nsIContent");
+ if (!content) {
+ return rv;
+ }
+
+ nsAutoCString attributeC, broadcasteridC;
+ LossyCopyUTF16toASCII(attribute, attributeC);
+ LossyCopyUTF16toASCII(broadcasterID, broadcasteridC);
+ MOZ_LOG(sXULBroadCastManager, LogLevel::Debug,
+ ("xul: broadcaster hookup <%s attribute='%s'> to %s",
+ nsAtomCString(content->NodeInfo()->NameAtom()).get(),
+ attributeC.get(), broadcasteridC.get()));
+ }
+
+ return NS_OK;
+}
+
+nsresult XULBroadcastManager::AddListener(Element* aElement) {
+ return UpdateListenerHookup(aElement, eHookupAdd);
+}
+
+nsresult XULBroadcastManager::RemoveListener(Element* aElement) {
+ return UpdateListenerHookup(aElement, eHookupRemove);
+}
+
+} // namespace mozilla::dom