diff options
Diffstat (limited to 'dom/xul/XULBroadcastManager.cpp')
-rw-r--r-- | dom/xul/XULBroadcastManager.cpp | 589 |
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 |