1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=4 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "XULBroadcastManager.h"
9 #include "nsContentUtils.h"
10 #include "mozilla/EventDispatcher.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/dom/DocumentInlines.h"
13 #include "nsXULElement.h"
15 struct BroadcastListener
{
17 RefPtr
<nsAtom
> mAttribute
;
20 struct BroadcasterMapEntry
: public PLDHashEntryHdr
{
21 mozilla::dom::Element
* mBroadcaster
; // [WEAK]
22 nsTArray
<BroadcastListener
*>
23 mListeners
; // [OWNING] of BroadcastListener objects
26 struct nsAttrNameInfo
{
27 nsAttrNameInfo(int32_t aNamespaceID
, nsAtom
* aName
, nsAtom
* aPrefix
)
28 : mNamespaceID(aNamespaceID
), mName(aName
), mPrefix(aPrefix
) {}
29 nsAttrNameInfo(const nsAttrNameInfo
& aOther
) = delete;
30 nsAttrNameInfo(nsAttrNameInfo
&& aOther
) = default;
34 RefPtr
<nsAtom
> mPrefix
;
37 static void ClearBroadcasterMapEntry(PLDHashTable
* aTable
,
38 PLDHashEntryHdr
* aEntry
) {
39 BroadcasterMapEntry
* entry
= static_cast<BroadcasterMapEntry
*>(aEntry
);
40 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
41 delete entry
->mListeners
[i
];
43 entry
->mListeners
.Clear();
45 // N.B. that we need to manually run the dtor because we
46 // constructed the nsTArray object in-place.
47 entry
->mListeners
.~nsTArray
<BroadcastListener
*>();
50 static bool CanBroadcast(int32_t aNameSpaceID
, nsAtom
* aAttribute
) {
51 // Don't push changes to the |id|, |persist|, |command| or
52 // |observes| attribute.
53 if (aNameSpaceID
== kNameSpaceID_None
) {
54 if ((aAttribute
== nsGkAtoms::id
) || (aAttribute
== nsGkAtoms::persist
) ||
55 (aAttribute
== nsGkAtoms::command
) ||
56 (aAttribute
== nsGkAtoms::observes
)) {
63 namespace mozilla::dom
{
64 static LazyLogModule
sXULBroadCastManager("XULBroadcastManager");
66 class XULBroadcastManager::nsDelayedBroadcastUpdate
{
68 nsDelayedBroadcastUpdate(Element
* aBroadcaster
, Element
* aListener
,
69 const nsAString
& aAttr
)
70 : mBroadcaster(aBroadcaster
),
74 mNeedsAttrChange(false) {}
76 nsDelayedBroadcastUpdate(Element
* aBroadcaster
, Element
* aListener
,
77 nsAtom
* aAttrName
, const nsAString
& aAttr
,
78 bool aSetAttr
, bool aNeedsAttrChange
)
79 : mBroadcaster(aBroadcaster
),
84 mNeedsAttrChange(aNeedsAttrChange
) {}
86 nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate
& aOther
) = delete;
87 nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate
&& aOther
) = default;
89 RefPtr
<Element
> mBroadcaster
;
90 RefPtr
<Element
> mListener
;
91 // Note if mAttrName isn't used, this is the name of the attr, otherwise
92 // this is the value of the attribute.
94 RefPtr
<nsAtom
> mAttrName
;
96 bool mNeedsAttrChange
;
100 static bool Equals(const nsDelayedBroadcastUpdate
& a
,
101 const nsDelayedBroadcastUpdate
& b
) {
102 return a
.mBroadcaster
== b
.mBroadcaster
&& a
.mListener
== b
.mListener
&&
103 a
.mAttrName
== b
.mAttrName
;
109 bool XULBroadcastManager::MayNeedListener(const Element
& aElement
) {
110 if (aElement
.NodeInfo()->Equals(nsGkAtoms::observes
, kNameSpaceID_XUL
)) {
113 if (aElement
.HasAttr(nsGkAtoms::observes
)) {
116 if (aElement
.HasAttr(nsGkAtoms::command
) &&
117 !(aElement
.NodeInfo()->Equals(nsGkAtoms::menuitem
, kNameSpaceID_XUL
) ||
118 aElement
.NodeInfo()->Equals(nsGkAtoms::key
, kNameSpaceID_XUL
))) {
124 XULBroadcastManager::XULBroadcastManager(Document
* aDocument
)
125 : mDocument(aDocument
),
126 mBroadcasterMap(nullptr),
127 mHandlingDelayedAttrChange(false),
128 mHandlingDelayedBroadcasters(false) {}
130 XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap
; }
132 void XULBroadcastManager::DropDocumentReference(void) { mDocument
= nullptr; }
134 void XULBroadcastManager::SynchronizeBroadcastListener(Element
* aBroadcaster
,
136 const nsAString
& aAttr
) {
137 if (!nsContentUtils::IsSafeToRunScript()) {
138 mDelayedBroadcasters
.EmplaceBack(aBroadcaster
, aListener
, aAttr
);
142 bool notify
= mHandlingDelayedBroadcasters
;
144 if (aAttr
.EqualsLiteral("*")) {
145 uint32_t count
= aBroadcaster
->GetAttrCount();
146 nsTArray
<nsAttrNameInfo
> attributes(count
);
147 for (uint32_t i
= 0; i
< count
; ++i
) {
148 const nsAttrName
* attrName
= aBroadcaster
->GetAttrNameAt(i
);
149 int32_t nameSpaceID
= attrName
->NamespaceID();
150 nsAtom
* name
= attrName
->LocalName();
152 // _Don't_ push the |id|, |ref|, or |persist| attribute's value!
153 if (!CanBroadcast(nameSpaceID
, name
)) continue;
155 attributes
.AppendElement(
156 nsAttrNameInfo(nameSpaceID
, name
, attrName
->GetPrefix()));
159 count
= attributes
.Length();
160 while (count
-- > 0) {
161 int32_t nameSpaceID
= attributes
[count
].mNamespaceID
;
162 nsAtom
* name
= attributes
[count
].mName
;
164 if (aBroadcaster
->GetAttr(nameSpaceID
, name
, value
)) {
165 aListener
->SetAttr(nameSpaceID
, name
, attributes
[count
].mPrefix
, value
,
170 // XXX we don't fire the |onbroadcast| handler during
171 // initial hookup: doing so would potentially run the
172 // |onbroadcast| handler before the |onload| handler,
173 // which could define JS properties that mask XBL
175 ExecuteOnBroadcastHandlerFor(aBroadcaster
, aListener
, name
);
179 // Find out if the attribute is even present at all.
180 RefPtr
<nsAtom
> name
= NS_Atomize(aAttr
);
183 if (aBroadcaster
->GetAttr(kNameSpaceID_None
, name
, value
)) {
184 aListener
->SetAttr(kNameSpaceID_None
, name
, value
, notify
);
186 aListener
->UnsetAttr(kNameSpaceID_None
, name
, notify
);
190 // XXX we don't fire the |onbroadcast| handler during initial
191 // hookup: doing so would potentially run the |onbroadcast|
192 // handler before the |onload| handler, which could define JS
193 // properties that mask XBL properties, etc.
194 ExecuteOnBroadcastHandlerFor(aBroadcaster
, aListener
, name
);
199 void XULBroadcastManager::AddListenerFor(Element
& aBroadcaster
,
201 const nsAString
& aAttr
,
204 aRv
.Throw(NS_ERROR_FAILURE
);
208 nsresult rv
= nsContentUtils::CheckSameOrigin(mDocument
, &aBroadcaster
);
215 rv
= nsContentUtils::CheckSameOrigin(mDocument
, &aListener
);
222 static const PLDHashTableOps gOps
= {
223 PLDHashTable::HashVoidPtrKeyStub
, PLDHashTable::MatchEntryStub
,
224 PLDHashTable::MoveEntryStub
, ClearBroadcasterMapEntry
, nullptr};
226 if (!mBroadcasterMap
) {
227 mBroadcasterMap
= new PLDHashTable(&gOps
, sizeof(BroadcasterMapEntry
));
231 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(&aBroadcaster
));
233 entry
= static_cast<BroadcasterMapEntry
*>(
234 mBroadcasterMap
->Add(&aBroadcaster
, fallible
));
237 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
241 entry
->mBroadcaster
= &aBroadcaster
;
243 // N.B. placement new to construct the nsTArray object in-place
244 new (&entry
->mListeners
) nsTArray
<BroadcastListener
*>();
247 // Only add the listener if it's not there already!
248 RefPtr
<nsAtom
> attr
= NS_Atomize(aAttr
);
250 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
251 BroadcastListener
* bl
= entry
->mListeners
[i
];
252 nsCOMPtr
<Element
> blListener
= do_QueryReferent(bl
->mListener
);
254 if (blListener
== &aListener
&& bl
->mAttribute
== attr
) return;
257 BroadcastListener
* bl
= new BroadcastListener
;
258 bl
->mListener
= do_GetWeakReference(&aListener
);
259 bl
->mAttribute
= attr
;
261 entry
->mListeners
.AppendElement(bl
);
263 SynchronizeBroadcastListener(&aBroadcaster
, &aListener
, aAttr
);
266 void XULBroadcastManager::RemoveListenerFor(Element
& aBroadcaster
,
268 const nsAString
& aAttr
) {
269 // If we haven't added any broadcast listeners, then there sure
270 // aren't any to remove.
271 if (!mBroadcasterMap
) return;
274 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(&aBroadcaster
));
276 RefPtr
<nsAtom
> attr
= NS_Atomize(aAttr
);
277 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
278 BroadcastListener
* bl
= entry
->mListeners
[i
];
279 nsCOMPtr
<Element
> blListener
= do_QueryReferent(bl
->mListener
);
281 if (blListener
== &aListener
&& bl
->mAttribute
== attr
) {
282 entry
->mListeners
.RemoveElementAt(i
);
285 if (entry
->mListeners
.IsEmpty()) mBroadcasterMap
->RemoveEntry(entry
);
293 nsresult
XULBroadcastManager::ExecuteOnBroadcastHandlerFor(
294 Element
* aBroadcaster
, Element
* aListener
, nsAtom
* aAttr
) {
298 // Now we execute the onchange handler in the context of the
299 // observer. We need to find the observer in order to
300 // execute the handler.
302 for (nsCOMPtr
<nsIContent
> child
= aListener
->GetFirstChild(); child
;
303 child
= child
->GetNextSibling()) {
304 // Look for an <observes> element beneath the listener. This
305 // ought to have an |element| attribute that refers to
306 // aBroadcaster, and an |attribute| element that tells us what
307 // attriubtes we're listening for.
308 if (!child
->IsXULElement(nsGkAtoms::observes
)) continue;
310 // Is this the element that was listening to us?
311 nsAutoString listeningToID
;
312 child
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::element
,
315 nsAutoString broadcasterID
;
316 aBroadcaster
->GetAttr(kNameSpaceID_None
, nsGkAtoms::id
, broadcasterID
);
318 if (listeningToID
!= broadcasterID
) continue;
320 // We are observing the broadcaster, but is this the right
322 nsAutoString listeningToAttribute
;
323 child
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::attribute
,
324 listeningToAttribute
);
326 if (!aAttr
->Equals(listeningToAttribute
) &&
327 !listeningToAttribute
.EqualsLiteral("*")) {
331 // This is the right <observes> element. Execute the
332 // |onbroadcast| event handler
333 WidgetEvent
event(true, eXULBroadcast
);
335 if (RefPtr
<nsPresContext
> presContext
= mDocument
->GetPresContext()) {
336 // Handle the DOM event
337 nsEventStatus status
= nsEventStatus_eIgnore
;
338 EventDispatcher::Dispatch(child
, presContext
, &event
, nullptr, &status
);
345 void XULBroadcastManager::AttributeChanged(Element
* aElement
,
346 int32_t aNameSpaceID
,
347 nsAtom
* aAttribute
) {
351 NS_ASSERTION(aElement
->OwnerDoc() == mDocument
, "unexpected doc");
353 // Synchronize broadcast listeners
354 if (mBroadcasterMap
&& CanBroadcast(aNameSpaceID
, aAttribute
)) {
356 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(aElement
));
359 // We've got listeners: push the value.
361 bool attrSet
= aElement
->GetAttr(kNameSpaceID_None
, aAttribute
, value
);
363 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
364 BroadcastListener
* bl
= entry
->mListeners
[i
];
365 if ((bl
->mAttribute
== aAttribute
) ||
366 (bl
->mAttribute
== nsGkAtoms::_asterisk
)) {
367 nsCOMPtr
<Element
> listenerEl
= do_QueryReferent(bl
->mListener
);
369 nsAutoString currentValue
;
370 bool hasAttr
= listenerEl
->GetAttr(kNameSpaceID_None
, aAttribute
,
372 // We need to update listener only if we're
373 // (1) removing an existing attribute,
374 // (2) adding a new attribute or
375 // (3) changing the value of an attribute.
376 bool needsAttrChange
=
377 attrSet
!= hasAttr
|| !value
.Equals(currentValue
);
378 nsDelayedBroadcastUpdate
delayedUpdate(aElement
, listenerEl
,
379 aAttribute
, value
, attrSet
,
382 size_t index
= mDelayedAttrChangeBroadcasts
.IndexOf(
383 delayedUpdate
, 0, nsDelayedBroadcastUpdate::Comparator());
384 if (index
!= mDelayedAttrChangeBroadcasts
.NoIndex
) {
385 if (mHandlingDelayedAttrChange
) {
386 NS_WARNING("Broadcasting loop!");
389 mDelayedAttrChangeBroadcasts
.RemoveElementAt(index
);
392 mDelayedAttrChangeBroadcasts
.AppendElement(
393 std::move(delayedUpdate
));
401 void XULBroadcastManager::MaybeBroadcast() {
402 // Only broadcast when not in an update and when safe to run scripts.
403 if (mDocument
&& mDocument
->UpdateNestingLevel() == 0 &&
404 (mDelayedAttrChangeBroadcasts
.Length() ||
405 mDelayedBroadcasters
.Length())) {
406 if (!nsContentUtils::IsSafeToRunScript()) {
408 nsContentUtils::AddScriptRunner(
409 NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this,
410 &XULBroadcastManager::MaybeBroadcast
));
414 if (!mHandlingDelayedAttrChange
) {
415 mHandlingDelayedAttrChange
= true;
416 for (uint32_t i
= 0; i
< mDelayedAttrChangeBroadcasts
.Length(); ++i
) {
417 RefPtr
<nsAtom
> attrName
= mDelayedAttrChangeBroadcasts
[i
].mAttrName
;
418 RefPtr
<Element
> listener
= mDelayedAttrChangeBroadcasts
[i
].mListener
;
419 if (mDelayedAttrChangeBroadcasts
[i
].mNeedsAttrChange
) {
420 const nsString
& value
= mDelayedAttrChangeBroadcasts
[i
].mAttr
;
421 if (mDelayedAttrChangeBroadcasts
[i
].mSetAttr
) {
422 listener
->SetAttr(kNameSpaceID_None
, attrName
, value
, true);
424 listener
->UnsetAttr(kNameSpaceID_None
, attrName
, true);
427 RefPtr
<Element
> broadcaster
=
428 mDelayedAttrChangeBroadcasts
[i
].mBroadcaster
;
429 ExecuteOnBroadcastHandlerFor(broadcaster
, listener
, attrName
);
431 mDelayedAttrChangeBroadcasts
.Clear();
432 mHandlingDelayedAttrChange
= false;
435 uint32_t length
= mDelayedBroadcasters
.Length();
437 bool oldValue
= mHandlingDelayedBroadcasters
;
438 mHandlingDelayedBroadcasters
= true;
439 nsTArray
<nsDelayedBroadcastUpdate
> delayedBroadcasters
=
440 std::move(mDelayedBroadcasters
);
441 for (uint32_t i
= 0; i
< length
; ++i
) {
442 SynchronizeBroadcastListener(delayedBroadcasters
[i
].mBroadcaster
,
443 delayedBroadcasters
[i
].mListener
,
444 delayedBroadcasters
[i
].mAttr
);
446 mHandlingDelayedBroadcasters
= oldValue
;
451 nsresult
XULBroadcastManager::FindBroadcaster(Element
* aElement
,
453 nsString
& aBroadcasterID
,
454 nsString
& aAttribute
,
455 Element
** aBroadcaster
) {
456 NodeInfo
* ni
= aElement
->NodeInfo();
457 *aListener
= nullptr;
458 *aBroadcaster
= nullptr;
460 if (ni
->Equals(nsGkAtoms::observes
, kNameSpaceID_XUL
)) {
461 // It's an <observes> element, which means that the actual
462 // listener is the _parent_ node. This element should have an
463 // 'element' attribute that specifies the ID of the
464 // broadcaster element, and an 'attribute' element, which
465 // specifies the name of the attribute to observe.
466 nsIContent
* parent
= aElement
->GetParent();
468 // <observes> is the root element
469 return NS_FINDBROADCASTER_NOT_FOUND
;
472 *aListener
= Element::FromNode(parent
);
473 NS_IF_ADDREF(*aListener
);
475 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::element
, aBroadcasterID
);
476 if (aBroadcasterID
.IsEmpty()) {
477 return NS_FINDBROADCASTER_NOT_FOUND
;
479 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::attribute
, aAttribute
);
481 // It's a generic element, which means that we'll use the
482 // value of the 'observes' attribute to determine the ID of
483 // the broadcaster element, and we'll watch _all_ of its
485 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::observes
, aBroadcasterID
);
487 // Bail if there's no aBroadcasterID
488 if (aBroadcasterID
.IsEmpty()) {
489 // Try the command attribute next.
490 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::command
, aBroadcasterID
);
491 if (!aBroadcasterID
.IsEmpty()) {
492 // We've got something in the command attribute. We
493 // only treat this as a normal broadcaster if we are
494 // not a menuitem or a key.
496 if (ni
->Equals(nsGkAtoms::menuitem
, kNameSpaceID_XUL
) ||
497 ni
->Equals(nsGkAtoms::key
, kNameSpaceID_XUL
)) {
498 return NS_FINDBROADCASTER_NOT_FOUND
;
501 return NS_FINDBROADCASTER_NOT_FOUND
;
505 *aListener
= aElement
;
506 NS_ADDREF(*aListener
);
508 aAttribute
.Assign('*');
511 // Make sure we got a valid listener.
512 NS_ENSURE_TRUE(*aListener
, NS_ERROR_UNEXPECTED
);
514 // Try to find the broadcaster element in the document.
515 Document
* doc
= aElement
->GetComposedDoc();
517 *aBroadcaster
= doc
->GetElementById(aBroadcasterID
);
520 // The broadcaster element is missing.
521 if (!*aBroadcaster
) {
522 return NS_FINDBROADCASTER_NOT_FOUND
;
525 NS_ADDREF(*aBroadcaster
);
527 return NS_FINDBROADCASTER_FOUND
;
530 nsresult
XULBroadcastManager::UpdateListenerHookup(Element
* aElement
,
531 HookupAction aAction
) {
532 // Resolve a broadcaster hookup. Look at the element that we're
533 // trying to resolve: it could be an '<observes>' element, or just
534 // a vanilla element with an 'observes' attribute on it.
537 nsCOMPtr
<Element
> listener
;
538 nsAutoString broadcasterID
;
539 nsAutoString attribute
;
540 nsCOMPtr
<Element
> broadcaster
;
542 rv
= FindBroadcaster(aElement
, getter_AddRefs(listener
), broadcasterID
,
543 attribute
, getter_AddRefs(broadcaster
));
545 case NS_FINDBROADCASTER_NOT_FOUND
:
547 case NS_FINDBROADCASTER_FOUND
:
553 NS_ENSURE_ARG(broadcaster
&& listener
);
554 if (aAction
== eHookupAdd
) {
556 AddListenerFor(*broadcaster
, *listener
, attribute
, domRv
);
557 if (domRv
.Failed()) {
558 return domRv
.StealNSResult();
561 RemoveListenerFor(*broadcaster
, *listener
, attribute
);
564 // Tell the world we succeeded
565 if (MOZ_LOG_TEST(sXULBroadCastManager
, LogLevel::Debug
)) {
566 nsCOMPtr
<nsIContent
> content
= listener
;
567 NS_ASSERTION(content
!= nullptr, "not an nsIContent");
572 nsAutoCString attributeC
, broadcasteridC
;
573 LossyCopyUTF16toASCII(attribute
, attributeC
);
574 LossyCopyUTF16toASCII(broadcasterID
, broadcasteridC
);
575 MOZ_LOG(sXULBroadCastManager
, LogLevel::Debug
,
576 ("xul: broadcaster hookup <%s attribute='%s'> to %s",
577 nsAtomCString(content
->NodeInfo()->NameAtom()).get(),
578 attributeC
.get(), broadcasteridC
.get()));
584 nsresult
XULBroadcastManager::AddListener(Element
* aElement
) {
585 return UpdateListenerHookup(aElement
, eHookupAdd
);
588 nsresult
XULBroadcastManager::RemoveListener(Element
* aElement
) {
589 return UpdateListenerHookup(aElement
, eHookupRemove
);
592 } // namespace mozilla::dom