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
)) {
65 static LazyLogModule
sXULBroadCastManager("XULBroadcastManager");
67 class XULBroadcastManager::nsDelayedBroadcastUpdate
{
69 nsDelayedBroadcastUpdate(Element
* aBroadcaster
, Element
* aListener
,
70 const nsAString
& aAttr
)
71 : mBroadcaster(aBroadcaster
),
75 mNeedsAttrChange(false) {}
77 nsDelayedBroadcastUpdate(Element
* aBroadcaster
, Element
* aListener
,
78 nsAtom
* aAttrName
, const nsAString
& aAttr
,
79 bool aSetAttr
, bool aNeedsAttrChange
)
80 : mBroadcaster(aBroadcaster
),
85 mNeedsAttrChange(aNeedsAttrChange
) {}
87 nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate
& aOther
) = delete;
88 nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate
&& aOther
) = default;
90 nsCOMPtr
<Element
> mBroadcaster
;
91 nsCOMPtr
<Element
> mListener
;
92 // Note if mAttrName isn't used, this is the name of the attr, otherwise
93 // this is the value of the attribute.
95 RefPtr
<nsAtom
> mAttrName
;
97 bool mNeedsAttrChange
;
101 static bool Equals(const nsDelayedBroadcastUpdate
& a
,
102 const nsDelayedBroadcastUpdate
& b
) {
103 return a
.mBroadcaster
== b
.mBroadcaster
&& a
.mListener
== b
.mListener
&&
104 a
.mAttrName
== b
.mAttrName
;
110 bool XULBroadcastManager::MayNeedListener(const Element
& aElement
) {
111 if (aElement
.NodeInfo()->Equals(nsGkAtoms::observes
, kNameSpaceID_XUL
)) {
114 if (aElement
.HasAttr(nsGkAtoms::observes
)) {
117 if (aElement
.HasAttr(nsGkAtoms::command
) &&
118 !(aElement
.NodeInfo()->Equals(nsGkAtoms::menuitem
, kNameSpaceID_XUL
) ||
119 aElement
.NodeInfo()->Equals(nsGkAtoms::key
, kNameSpaceID_XUL
))) {
125 XULBroadcastManager::XULBroadcastManager(Document
* aDocument
)
126 : mDocument(aDocument
),
127 mBroadcasterMap(nullptr),
128 mHandlingDelayedAttrChange(false),
129 mHandlingDelayedBroadcasters(false) {}
131 XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap
; }
133 void XULBroadcastManager::DropDocumentReference(void) { mDocument
= nullptr; }
135 void XULBroadcastManager::SynchronizeBroadcastListener(Element
* aBroadcaster
,
137 const nsAString
& aAttr
) {
138 if (!nsContentUtils::IsSafeToRunScript()) {
139 mDelayedBroadcasters
.EmplaceBack(aBroadcaster
, aListener
, aAttr
);
143 bool notify
= mHandlingDelayedBroadcasters
;
145 if (aAttr
.EqualsLiteral("*")) {
146 uint32_t count
= aBroadcaster
->GetAttrCount();
147 nsTArray
<nsAttrNameInfo
> attributes(count
);
148 for (uint32_t i
= 0; i
< count
; ++i
) {
149 const nsAttrName
* attrName
= aBroadcaster
->GetAttrNameAt(i
);
150 int32_t nameSpaceID
= attrName
->NamespaceID();
151 nsAtom
* name
= attrName
->LocalName();
153 // _Don't_ push the |id|, |ref|, or |persist| attribute's value!
154 if (!CanBroadcast(nameSpaceID
, name
)) continue;
156 attributes
.AppendElement(
157 nsAttrNameInfo(nameSpaceID
, name
, attrName
->GetPrefix()));
160 count
= attributes
.Length();
161 while (count
-- > 0) {
162 int32_t nameSpaceID
= attributes
[count
].mNamespaceID
;
163 nsAtom
* name
= attributes
[count
].mName
;
165 if (aBroadcaster
->GetAttr(nameSpaceID
, name
, value
)) {
166 aListener
->SetAttr(nameSpaceID
, name
, attributes
[count
].mPrefix
, value
,
171 // XXX we don't fire the |onbroadcast| handler during
172 // initial hookup: doing so would potentially run the
173 // |onbroadcast| handler before the |onload| handler,
174 // which could define JS properties that mask XBL
176 ExecuteOnBroadcastHandlerFor(aBroadcaster
, aListener
, name
);
180 // Find out if the attribute is even present at all.
181 RefPtr
<nsAtom
> name
= NS_Atomize(aAttr
);
184 if (aBroadcaster
->GetAttr(kNameSpaceID_None
, name
, value
)) {
185 aListener
->SetAttr(kNameSpaceID_None
, name
, value
, notify
);
187 aListener
->UnsetAttr(kNameSpaceID_None
, name
, notify
);
191 // XXX we don't fire the |onbroadcast| handler during initial
192 // hookup: doing so would potentially run the |onbroadcast|
193 // handler before the |onload| handler, which could define JS
194 // properties that mask XBL properties, etc.
195 ExecuteOnBroadcastHandlerFor(aBroadcaster
, aListener
, name
);
200 void XULBroadcastManager::AddListenerFor(Element
& aBroadcaster
,
202 const nsAString
& aAttr
,
205 aRv
.Throw(NS_ERROR_FAILURE
);
209 nsresult rv
= nsContentUtils::CheckSameOrigin(mDocument
, &aBroadcaster
);
216 rv
= nsContentUtils::CheckSameOrigin(mDocument
, &aListener
);
223 static const PLDHashTableOps gOps
= {
224 PLDHashTable::HashVoidPtrKeyStub
, PLDHashTable::MatchEntryStub
,
225 PLDHashTable::MoveEntryStub
, ClearBroadcasterMapEntry
, nullptr};
227 if (!mBroadcasterMap
) {
228 mBroadcasterMap
= new PLDHashTable(&gOps
, sizeof(BroadcasterMapEntry
));
232 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(&aBroadcaster
));
234 entry
= static_cast<BroadcasterMapEntry
*>(
235 mBroadcasterMap
->Add(&aBroadcaster
, fallible
));
238 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
242 entry
->mBroadcaster
= &aBroadcaster
;
244 // N.B. placement new to construct the nsTArray object in-place
245 new (&entry
->mListeners
) nsTArray
<BroadcastListener
*>();
248 // Only add the listener if it's not there already!
249 RefPtr
<nsAtom
> attr
= NS_Atomize(aAttr
);
251 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
252 BroadcastListener
* bl
= entry
->mListeners
[i
];
253 nsCOMPtr
<Element
> blListener
= do_QueryReferent(bl
->mListener
);
255 if (blListener
== &aListener
&& bl
->mAttribute
== attr
) return;
258 BroadcastListener
* bl
= new BroadcastListener
;
259 bl
->mListener
= do_GetWeakReference(&aListener
);
260 bl
->mAttribute
= attr
;
262 entry
->mListeners
.AppendElement(bl
);
264 SynchronizeBroadcastListener(&aBroadcaster
, &aListener
, aAttr
);
267 void XULBroadcastManager::RemoveListenerFor(Element
& aBroadcaster
,
269 const nsAString
& aAttr
) {
270 // If we haven't added any broadcast listeners, then there sure
271 // aren't any to remove.
272 if (!mBroadcasterMap
) return;
275 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(&aBroadcaster
));
277 RefPtr
<nsAtom
> attr
= NS_Atomize(aAttr
);
278 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
279 BroadcastListener
* bl
= entry
->mListeners
[i
];
280 nsCOMPtr
<Element
> blListener
= do_QueryReferent(bl
->mListener
);
282 if (blListener
== &aListener
&& bl
->mAttribute
== attr
) {
283 entry
->mListeners
.RemoveElementAt(i
);
286 if (entry
->mListeners
.IsEmpty()) mBroadcasterMap
->RemoveEntry(entry
);
294 nsresult
XULBroadcastManager::ExecuteOnBroadcastHandlerFor(
295 Element
* aBroadcaster
, Element
* aListener
, nsAtom
* aAttr
) {
299 // Now we execute the onchange handler in the context of the
300 // observer. We need to find the observer in order to
301 // execute the handler.
303 for (nsIContent
* child
= aListener
->GetFirstChild(); child
;
304 child
= child
->GetNextSibling()) {
305 // Look for an <observes> element beneath the listener. This
306 // ought to have an |element| attribute that refers to
307 // aBroadcaster, and an |attribute| element that tells us what
308 // attriubtes we're listening for.
309 if (!child
->IsXULElement(nsGkAtoms::observes
)) continue;
311 // Is this the element that was listening to us?
312 nsAutoString listeningToID
;
313 child
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::element
,
316 nsAutoString broadcasterID
;
317 aBroadcaster
->GetAttr(kNameSpaceID_None
, nsGkAtoms::id
, broadcasterID
);
319 if (listeningToID
!= broadcasterID
) continue;
321 // We are observing the broadcaster, but is this the right
323 nsAutoString listeningToAttribute
;
324 child
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::attribute
,
325 listeningToAttribute
);
327 if (!aAttr
->Equals(listeningToAttribute
) &&
328 !listeningToAttribute
.EqualsLiteral("*")) {
332 // This is the right <observes> element. Execute the
333 // |onbroadcast| event handler
334 WidgetEvent
event(true, eXULBroadcast
);
336 RefPtr
<nsPresContext
> presContext
= mDocument
->GetPresContext();
338 // Handle the DOM event
339 nsEventStatus status
= nsEventStatus_eIgnore
;
340 EventDispatcher::Dispatch(child
, presContext
, &event
, nullptr, &status
);
347 void XULBroadcastManager::AttributeChanged(Element
* aElement
,
348 int32_t aNameSpaceID
,
349 nsAtom
* aAttribute
) {
353 NS_ASSERTION(aElement
->OwnerDoc() == mDocument
, "unexpected doc");
355 // Synchronize broadcast listeners
356 if (mBroadcasterMap
&& CanBroadcast(aNameSpaceID
, aAttribute
)) {
358 static_cast<BroadcasterMapEntry
*>(mBroadcasterMap
->Search(aElement
));
361 // We've got listeners: push the value.
363 bool attrSet
= aElement
->GetAttr(kNameSpaceID_None
, aAttribute
, value
);
365 for (size_t i
= entry
->mListeners
.Length() - 1; i
!= (size_t)-1; --i
) {
366 BroadcastListener
* bl
= entry
->mListeners
[i
];
367 if ((bl
->mAttribute
== aAttribute
) ||
368 (bl
->mAttribute
== nsGkAtoms::_asterisk
)) {
369 nsCOMPtr
<Element
> listenerEl
= do_QueryReferent(bl
->mListener
);
371 nsAutoString currentValue
;
372 bool hasAttr
= listenerEl
->GetAttr(kNameSpaceID_None
, aAttribute
,
374 // We need to update listener only if we're
375 // (1) removing an existing attribute,
376 // (2) adding a new attribute or
377 // (3) changing the value of an attribute.
378 bool needsAttrChange
=
379 attrSet
!= hasAttr
|| !value
.Equals(currentValue
);
380 nsDelayedBroadcastUpdate
delayedUpdate(aElement
, listenerEl
,
381 aAttribute
, value
, attrSet
,
384 size_t index
= mDelayedAttrChangeBroadcasts
.IndexOf(
385 delayedUpdate
, 0, nsDelayedBroadcastUpdate::Comparator());
386 if (index
!= mDelayedAttrChangeBroadcasts
.NoIndex
) {
387 if (mHandlingDelayedAttrChange
) {
388 NS_WARNING("Broadcasting loop!");
391 mDelayedAttrChangeBroadcasts
.RemoveElementAt(index
);
394 mDelayedAttrChangeBroadcasts
.AppendElement(
395 std::move(delayedUpdate
));
403 void XULBroadcastManager::MaybeBroadcast() {
404 // Only broadcast when not in an update and when safe to run scripts.
405 if (mDocument
&& mDocument
->UpdateNestingLevel() == 0 &&
406 (mDelayedAttrChangeBroadcasts
.Length() ||
407 mDelayedBroadcasters
.Length())) {
408 if (!nsContentUtils::IsSafeToRunScript()) {
410 nsContentUtils::AddScriptRunner(
411 NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this,
412 &XULBroadcastManager::MaybeBroadcast
));
416 if (!mHandlingDelayedAttrChange
) {
417 mHandlingDelayedAttrChange
= true;
418 for (uint32_t i
= 0; i
< mDelayedAttrChangeBroadcasts
.Length(); ++i
) {
419 nsAtom
* attrName
= mDelayedAttrChangeBroadcasts
[i
].mAttrName
;
420 if (mDelayedAttrChangeBroadcasts
[i
].mNeedsAttrChange
) {
421 nsCOMPtr
<Element
> listener
=
422 mDelayedAttrChangeBroadcasts
[i
].mListener
;
423 const nsString
& value
= mDelayedAttrChangeBroadcasts
[i
].mAttr
;
424 if (mDelayedAttrChangeBroadcasts
[i
].mSetAttr
) {
425 listener
->SetAttr(kNameSpaceID_None
, attrName
, value
, true);
427 listener
->UnsetAttr(kNameSpaceID_None
, attrName
, true);
430 ExecuteOnBroadcastHandlerFor(
431 mDelayedAttrChangeBroadcasts
[i
].mBroadcaster
,
432 mDelayedAttrChangeBroadcasts
[i
].mListener
, attrName
);
434 mDelayedAttrChangeBroadcasts
.Clear();
435 mHandlingDelayedAttrChange
= false;
438 uint32_t length
= mDelayedBroadcasters
.Length();
440 bool oldValue
= mHandlingDelayedBroadcasters
;
441 mHandlingDelayedBroadcasters
= true;
442 nsTArray
<nsDelayedBroadcastUpdate
> delayedBroadcasters
=
443 std::move(mDelayedBroadcasters
);
444 for (uint32_t i
= 0; i
< length
; ++i
) {
445 SynchronizeBroadcastListener(delayedBroadcasters
[i
].mBroadcaster
,
446 delayedBroadcasters
[i
].mListener
,
447 delayedBroadcasters
[i
].mAttr
);
449 mHandlingDelayedBroadcasters
= oldValue
;
454 nsresult
XULBroadcastManager::FindBroadcaster(Element
* aElement
,
456 nsString
& aBroadcasterID
,
457 nsString
& aAttribute
,
458 Element
** aBroadcaster
) {
459 NodeInfo
* ni
= aElement
->NodeInfo();
460 *aListener
= nullptr;
461 *aBroadcaster
= nullptr;
463 if (ni
->Equals(nsGkAtoms::observes
, kNameSpaceID_XUL
)) {
464 // It's an <observes> element, which means that the actual
465 // listener is the _parent_ node. This element should have an
466 // 'element' attribute that specifies the ID of the
467 // broadcaster element, and an 'attribute' element, which
468 // specifies the name of the attribute to observe.
469 nsIContent
* parent
= aElement
->GetParent();
471 // <observes> is the root element
472 return NS_FINDBROADCASTER_NOT_FOUND
;
475 *aListener
= Element::FromNode(parent
);
476 NS_IF_ADDREF(*aListener
);
478 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::element
, aBroadcasterID
);
479 if (aBroadcasterID
.IsEmpty()) {
480 return NS_FINDBROADCASTER_NOT_FOUND
;
482 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::attribute
, aAttribute
);
484 // It's a generic element, which means that we'll use the
485 // value of the 'observes' attribute to determine the ID of
486 // the broadcaster element, and we'll watch _all_ of its
488 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::observes
, aBroadcasterID
);
490 // Bail if there's no aBroadcasterID
491 if (aBroadcasterID
.IsEmpty()) {
492 // Try the command attribute next.
493 aElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::command
, aBroadcasterID
);
494 if (!aBroadcasterID
.IsEmpty()) {
495 // We've got something in the command attribute. We
496 // only treat this as a normal broadcaster if we are
497 // not a menuitem or a key.
499 if (ni
->Equals(nsGkAtoms::menuitem
, kNameSpaceID_XUL
) ||
500 ni
->Equals(nsGkAtoms::key
, kNameSpaceID_XUL
)) {
501 return NS_FINDBROADCASTER_NOT_FOUND
;
504 return NS_FINDBROADCASTER_NOT_FOUND
;
508 *aListener
= aElement
;
509 NS_ADDREF(*aListener
);
511 aAttribute
.Assign('*');
514 // Make sure we got a valid listener.
515 NS_ENSURE_TRUE(*aListener
, NS_ERROR_UNEXPECTED
);
517 // Try to find the broadcaster element in the document.
518 Document
* doc
= aElement
->GetComposedDoc();
520 *aBroadcaster
= doc
->GetElementById(aBroadcasterID
);
523 // The broadcaster element is missing.
524 if (!*aBroadcaster
) {
525 return NS_FINDBROADCASTER_NOT_FOUND
;
528 NS_ADDREF(*aBroadcaster
);
530 return NS_FINDBROADCASTER_FOUND
;
533 nsresult
XULBroadcastManager::UpdateListenerHookup(Element
* aElement
,
534 HookupAction aAction
) {
535 // Resolve a broadcaster hookup. Look at the element that we're
536 // trying to resolve: it could be an '<observes>' element, or just
537 // a vanilla element with an 'observes' attribute on it.
540 nsCOMPtr
<Element
> listener
;
541 nsAutoString broadcasterID
;
542 nsAutoString attribute
;
543 nsCOMPtr
<Element
> broadcaster
;
545 rv
= FindBroadcaster(aElement
, getter_AddRefs(listener
), broadcasterID
,
546 attribute
, getter_AddRefs(broadcaster
));
548 case NS_FINDBROADCASTER_NOT_FOUND
:
550 case NS_FINDBROADCASTER_FOUND
:
556 NS_ENSURE_ARG(broadcaster
&& listener
);
557 if (aAction
== eHookupAdd
) {
559 AddListenerFor(*broadcaster
, *listener
, attribute
, domRv
);
560 if (domRv
.Failed()) {
561 return domRv
.StealNSResult();
564 RemoveListenerFor(*broadcaster
, *listener
, attribute
);
567 // Tell the world we succeeded
568 if (MOZ_LOG_TEST(sXULBroadCastManager
, LogLevel::Debug
)) {
569 nsCOMPtr
<nsIContent
> content
= listener
;
570 NS_ASSERTION(content
!= nullptr, "not an nsIContent");
575 nsAutoCString attributeC
, broadcasteridC
;
576 LossyCopyUTF16toASCII(attribute
, attributeC
);
577 LossyCopyUTF16toASCII(broadcasterID
, broadcasteridC
);
578 MOZ_LOG(sXULBroadCastManager
, LogLevel::Debug
,
579 ("xul: broadcaster hookup <%s attribute='%s'> to %s",
580 nsAtomCString(content
->NodeInfo()->NameAtom()).get(),
581 attributeC
.get(), broadcasteridC
.get()));
587 nsresult
XULBroadcastManager::AddListener(Element
* aElement
) {
588 return UpdateListenerHookup(aElement
, eHookupAdd
);
591 nsresult
XULBroadcastManager::RemoveListener(Element
* aElement
) {
592 return UpdateListenerHookup(aElement
, eHookupRemove
);
596 } // namespace mozilla