XULBroadcastManager.cpp (20507B)
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/. */ 6 7 #include "XULBroadcastManager.h" 8 9 #include "mozilla/EventDispatcher.h" 10 #include "mozilla/Logging.h" 11 #include "mozilla/dom/DocumentInlines.h" 12 #include "nsCOMPtr.h" 13 #include "nsContentUtils.h" 14 #include "nsXULElement.h" 15 16 struct BroadcastListener { 17 nsWeakPtr mListener; 18 RefPtr<nsAtom> mAttribute; 19 }; 20 21 struct BroadcasterMapEntry : public PLDHashEntryHdr { 22 mozilla::dom::Element* mBroadcaster; // [WEAK] 23 nsTArray<BroadcastListener*> 24 mListeners; // [OWNING] of BroadcastListener objects 25 }; 26 27 struct nsAttrNameInfo { 28 nsAttrNameInfo(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix) 29 : mNamespaceID(aNamespaceID), mName(aName), mPrefix(aPrefix) {} 30 nsAttrNameInfo(const nsAttrNameInfo& aOther) = delete; 31 nsAttrNameInfo(nsAttrNameInfo&& aOther) = default; 32 33 int32_t mNamespaceID; 34 RefPtr<nsAtom> mName; 35 RefPtr<nsAtom> mPrefix; 36 }; 37 38 static void ClearBroadcasterMapEntry(PLDHashTable* aTable, 39 PLDHashEntryHdr* aEntry) { 40 BroadcasterMapEntry* entry = static_cast<BroadcasterMapEntry*>(aEntry); 41 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { 42 delete entry->mListeners[i]; 43 } 44 entry->mListeners.Clear(); 45 46 // N.B. that we need to manually run the dtor because we 47 // constructed the nsTArray object in-place. 48 entry->mListeners.~nsTArray<BroadcastListener*>(); 49 } 50 51 static bool CanBroadcast(int32_t aNameSpaceID, nsAtom* aAttribute) { 52 // Don't push changes to the |id|, |persist|, |command| or 53 // |observes| attribute. 54 if (aNameSpaceID == kNameSpaceID_None) { 55 if ((aAttribute == nsGkAtoms::id) || (aAttribute == nsGkAtoms::persist) || 56 (aAttribute == nsGkAtoms::command) || 57 (aAttribute == nsGkAtoms::observes)) { 58 return false; 59 } 60 } 61 return true; 62 } 63 64 namespace mozilla::dom { 65 static LazyLogModule sXULBroadCastManager("XULBroadcastManager"); 66 67 class XULBroadcastManager::nsDelayedBroadcastUpdate { 68 public: 69 nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener, 70 const nsAString& aAttr) 71 : mBroadcaster(aBroadcaster), 72 mListener(aListener), 73 mAttr(aAttr), 74 mSetAttr(false), 75 mNeedsAttrChange(false) {} 76 77 nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener, 78 nsAtom* aAttrName, const nsAString& aAttr, 79 bool aSetAttr, bool aNeedsAttrChange) 80 : mBroadcaster(aBroadcaster), 81 mListener(aListener), 82 mAttr(aAttr), 83 mAttrName(aAttrName), 84 mSetAttr(aSetAttr), 85 mNeedsAttrChange(aNeedsAttrChange) {} 86 87 nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate& aOther) = delete; 88 nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate&& aOther) = default; 89 90 RefPtr<Element> mBroadcaster; 91 RefPtr<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. 94 nsString mAttr; 95 RefPtr<nsAtom> mAttrName; 96 bool mSetAttr; 97 bool mNeedsAttrChange; 98 99 class Comparator { 100 public: 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; 105 } 106 }; 107 }; 108 109 /* static */ 110 bool XULBroadcastManager::MayNeedListener(const Element& aElement) { 111 if (aElement.NodeInfo()->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) { 112 return true; 113 } 114 if (aElement.HasAttr(nsGkAtoms::observes)) { 115 return true; 116 } 117 if (aElement.HasAttr(nsGkAtoms::command) && 118 !(aElement.NodeInfo()->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) || 119 aElement.NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL))) { 120 return true; 121 } 122 return false; 123 } 124 125 XULBroadcastManager::XULBroadcastManager(Document* aDocument) 126 : mDocument(aDocument), 127 mBroadcasterMap(nullptr), 128 mHandlingDelayedAttrChange(false), 129 mHandlingDelayedBroadcasters(false) {} 130 131 XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap; } 132 133 void XULBroadcastManager::DropDocumentReference(void) { mDocument = nullptr; } 134 135 void XULBroadcastManager::SynchronizeBroadcastListener(Element* aBroadcaster, 136 Element* aListener, 137 const nsAString& aAttr) { 138 if (!nsContentUtils::IsSafeToRunScript()) { 139 mDelayedBroadcasters.EmplaceBack(aBroadcaster, aListener, aAttr); 140 MaybeBroadcast(); 141 return; 142 } 143 bool notify = mHandlingDelayedBroadcasters; 144 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(); 152 153 // _Don't_ push the |id|, |ref|, or |persist| attribute's value! 154 if (!CanBroadcast(nameSpaceID, name)) continue; 155 156 attributes.AppendElement( 157 nsAttrNameInfo(nameSpaceID, name, attrName->GetPrefix())); 158 } 159 160 count = attributes.Length(); 161 while (count-- > 0) { 162 int32_t nameSpaceID = attributes[count].mNamespaceID; 163 nsAtom* name = attributes[count].mName; 164 nsAutoString value; 165 if (aBroadcaster->GetAttr(nameSpaceID, name, value)) { 166 aListener->SetAttr(nameSpaceID, name, attributes[count].mPrefix, value, 167 notify); 168 } 169 170 #if 0 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 175 // properties, etc. 176 ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name); 177 #endif 178 } 179 } else { 180 // Find out if the attribute is even present at all. 181 RefPtr<nsAtom> name = NS_Atomize(aAttr); 182 183 nsAutoString value; 184 if (aBroadcaster->GetAttr(name, value)) { 185 aListener->SetAttr(kNameSpaceID_None, name, value, notify); 186 } else { 187 aListener->UnsetAttr(kNameSpaceID_None, name, notify); 188 } 189 190 #if 0 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); 196 #endif 197 } 198 } 199 200 void XULBroadcastManager::AddListenerFor(Element& aBroadcaster, 201 Element& aListener, 202 const nsAString& aAttr, 203 ErrorResult& aRv) { 204 if (!mDocument) { 205 aRv.Throw(NS_ERROR_FAILURE); 206 return; 207 } 208 209 nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, &aBroadcaster); 210 211 if (NS_FAILED(rv)) { 212 aRv.Throw(rv); 213 return; 214 } 215 216 rv = nsContentUtils::CheckSameOrigin(mDocument, &aListener); 217 218 if (NS_FAILED(rv)) { 219 aRv.Throw(rv); 220 return; 221 } 222 223 static const PLDHashTableOps gOps = { 224 PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub, 225 PLDHashTable::MoveEntryStub, ClearBroadcasterMapEntry, nullptr}; 226 227 if (!mBroadcasterMap) { 228 mBroadcasterMap = new PLDHashTable(&gOps, sizeof(BroadcasterMapEntry)); 229 } 230 231 auto entry = 232 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster)); 233 if (!entry) { 234 entry = static_cast<BroadcasterMapEntry*>( 235 mBroadcasterMap->Add(&aBroadcaster, fallible)); 236 237 if (!entry) { 238 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 239 return; 240 } 241 242 entry->mBroadcaster = &aBroadcaster; 243 244 // N.B. placement new to construct the nsTArray object in-place 245 new (&entry->mListeners) nsTArray<BroadcastListener*>(); 246 } 247 248 // Only add the listener if it's not there already! 249 RefPtr<nsAtom> attr = NS_Atomize(aAttr); 250 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); 254 255 if (blListener == &aListener && bl->mAttribute == attr) return; 256 } 257 258 BroadcastListener* bl = new BroadcastListener; 259 bl->mListener = do_GetWeakReference(&aListener); 260 bl->mAttribute = attr; 261 262 entry->mListeners.AppendElement(bl); 263 264 SynchronizeBroadcastListener(&aBroadcaster, &aListener, aAttr); 265 } 266 267 void XULBroadcastManager::RemoveListenerFor(Element& aBroadcaster, 268 Element& aListener, 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; 273 274 auto entry = 275 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster)); 276 if (entry) { 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); 281 282 if (blListener == &aListener && bl->mAttribute == attr) { 283 entry->mListeners.RemoveElementAt(i); 284 delete bl; 285 286 if (entry->mListeners.IsEmpty()) mBroadcasterMap->RemoveEntry(entry); 287 288 break; 289 } 290 } 291 } 292 } 293 294 nsresult XULBroadcastManager::ExecuteOnBroadcastHandlerFor( 295 Element* aBroadcaster, Element* aListener, nsAtom* aAttr) { 296 if (!mDocument) { 297 return NS_OK; 298 } 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. 302 303 for (nsCOMPtr<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; 310 311 // Is this the element that was listening to us? 312 nsAutoString listeningToID; 313 child->AsElement()->GetAttr(nsGkAtoms::element, listeningToID); 314 315 nsAutoString broadcasterID; 316 aBroadcaster->GetAttr(nsGkAtoms::id, broadcasterID); 317 318 if (listeningToID != broadcasterID) continue; 319 320 // We are observing the broadcaster, but is this the right 321 // attribute? 322 nsAutoString listeningToAttribute; 323 child->AsElement()->GetAttr(nsGkAtoms::attribute, listeningToAttribute); 324 325 if (!aAttr->Equals(listeningToAttribute) && 326 !listeningToAttribute.EqualsLiteral("*")) { 327 continue; 328 } 329 330 // This is the right <observes> element. Execute the 331 // |onbroadcast| event handler 332 WidgetEvent event(true, eXULBroadcast); 333 334 if (RefPtr<nsPresContext> presContext = mDocument->GetPresContext()) { 335 // Handle the DOM event 336 nsEventStatus status = nsEventStatus_eIgnore; 337 EventDispatcher::Dispatch(child, presContext, &event, nullptr, &status); 338 } 339 } 340 341 return NS_OK; 342 } 343 344 void XULBroadcastManager::AttributeChanged(Element* aElement, 345 int32_t aNameSpaceID, 346 nsAtom* aAttribute) { 347 if (!mDocument) { 348 return; 349 } 350 NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc"); 351 352 // Synchronize broadcast listeners 353 if (mBroadcasterMap && CanBroadcast(aNameSpaceID, aAttribute)) { 354 auto entry = 355 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(aElement)); 356 357 if (entry) { 358 // We've got listeners: push the value. 359 nsAutoString value; 360 bool attrSet = aElement->GetAttr(aAttribute, value); 361 362 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) { 363 BroadcastListener* bl = entry->mListeners[i]; 364 if ((bl->mAttribute == aAttribute) || 365 (bl->mAttribute == nsGkAtoms::_asterisk)) { 366 nsCOMPtr<Element> listenerEl = do_QueryReferent(bl->mListener); 367 if (listenerEl) { 368 nsAutoString currentValue; 369 bool hasAttr = listenerEl->GetAttr(aAttribute, currentValue); 370 // We need to update listener only if we're 371 // (1) removing an existing attribute, 372 // (2) adding a new attribute or 373 // (3) changing the value of an attribute. 374 bool needsAttrChange = 375 attrSet != hasAttr || !value.Equals(currentValue); 376 nsDelayedBroadcastUpdate delayedUpdate(aElement, listenerEl, 377 aAttribute, value, attrSet, 378 needsAttrChange); 379 380 size_t index = mDelayedAttrChangeBroadcasts.IndexOf( 381 delayedUpdate, 0, nsDelayedBroadcastUpdate::Comparator()); 382 if (index != mDelayedAttrChangeBroadcasts.NoIndex) { 383 if (mHandlingDelayedAttrChange) { 384 NS_WARNING("Broadcasting loop!"); 385 continue; 386 } 387 mDelayedAttrChangeBroadcasts.RemoveElementAt(index); 388 } 389 390 mDelayedAttrChangeBroadcasts.AppendElement( 391 std::move(delayedUpdate)); 392 } 393 } 394 } 395 } 396 } 397 } 398 399 void XULBroadcastManager::MaybeBroadcast() { 400 // Only broadcast when not in an update and when safe to run scripts. 401 if (mDocument && mDocument->UpdateNestingLevel() == 0 && 402 (mDelayedAttrChangeBroadcasts.Length() || 403 mDelayedBroadcasters.Length())) { 404 if (!nsContentUtils::IsSafeToRunScript()) { 405 if (mDocument) { 406 nsContentUtils::AddScriptRunner( 407 NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this, 408 &XULBroadcastManager::MaybeBroadcast)); 409 } 410 return; 411 } 412 if (!mHandlingDelayedAttrChange) { 413 mHandlingDelayedAttrChange = true; 414 for (uint32_t i = 0; i < mDelayedAttrChangeBroadcasts.Length(); ++i) { 415 RefPtr<nsAtom> attrName = mDelayedAttrChangeBroadcasts[i].mAttrName; 416 RefPtr<Element> listener = mDelayedAttrChangeBroadcasts[i].mListener; 417 if (mDelayedAttrChangeBroadcasts[i].mNeedsAttrChange) { 418 const nsString& value = mDelayedAttrChangeBroadcasts[i].mAttr; 419 if (mDelayedAttrChangeBroadcasts[i].mSetAttr) { 420 listener->SetAttr(kNameSpaceID_None, attrName, value, true); 421 } else { 422 listener->UnsetAttr(kNameSpaceID_None, attrName, true); 423 } 424 } 425 RefPtr<Element> broadcaster = 426 mDelayedAttrChangeBroadcasts[i].mBroadcaster; 427 ExecuteOnBroadcastHandlerFor(broadcaster, listener, attrName); 428 } 429 mDelayedAttrChangeBroadcasts.Clear(); 430 mHandlingDelayedAttrChange = false; 431 } 432 433 uint32_t length = mDelayedBroadcasters.Length(); 434 if (length) { 435 bool oldValue = mHandlingDelayedBroadcasters; 436 mHandlingDelayedBroadcasters = true; 437 nsTArray<nsDelayedBroadcastUpdate> delayedBroadcasters = 438 std::move(mDelayedBroadcasters); 439 for (uint32_t i = 0; i < length; ++i) { 440 SynchronizeBroadcastListener(delayedBroadcasters[i].mBroadcaster, 441 delayedBroadcasters[i].mListener, 442 delayedBroadcasters[i].mAttr); 443 } 444 mHandlingDelayedBroadcasters = oldValue; 445 } 446 } 447 } 448 449 nsresult XULBroadcastManager::FindBroadcaster(Element* aElement, 450 Element** aListener, 451 nsString& aBroadcasterID, 452 nsString& aAttribute, 453 Element** aBroadcaster) { 454 NodeInfo* ni = aElement->NodeInfo(); 455 *aListener = nullptr; 456 *aBroadcaster = nullptr; 457 458 if (ni->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) { 459 // It's an <observes> element, which means that the actual 460 // listener is the _parent_ node. This element should have an 461 // 'element' attribute that specifies the ID of the 462 // broadcaster element, and an 'attribute' element, which 463 // specifies the name of the attribute to observe. 464 nsIContent* parent = aElement->GetParent(); 465 if (!parent) { 466 // <observes> is the root element 467 return NS_FINDBROADCASTER_NOT_FOUND; 468 } 469 470 *aListener = Element::FromNode(parent); 471 NS_IF_ADDREF(*aListener); 472 473 aElement->GetAttr(nsGkAtoms::element, aBroadcasterID); 474 if (aBroadcasterID.IsEmpty()) { 475 return NS_FINDBROADCASTER_NOT_FOUND; 476 } 477 aElement->GetAttr(nsGkAtoms::attribute, aAttribute); 478 } else { 479 // It's a generic element, which means that we'll use the 480 // value of the 'observes' attribute to determine the ID of 481 // the broadcaster element, and we'll watch _all_ of its 482 // values. 483 aElement->GetAttr(nsGkAtoms::observes, aBroadcasterID); 484 485 // Bail if there's no aBroadcasterID 486 if (aBroadcasterID.IsEmpty()) { 487 // Try the command attribute next. 488 aElement->GetAttr(nsGkAtoms::command, aBroadcasterID); 489 if (!aBroadcasterID.IsEmpty()) { 490 // We've got something in the command attribute. We 491 // only treat this as a normal broadcaster if we are 492 // not a menuitem or a key. 493 494 if (ni->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) || 495 ni->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) { 496 return NS_FINDBROADCASTER_NOT_FOUND; 497 } 498 } else { 499 return NS_FINDBROADCASTER_NOT_FOUND; 500 } 501 } 502 503 *aListener = aElement; 504 NS_ADDREF(*aListener); 505 506 aAttribute.Assign('*'); 507 } 508 509 // Make sure we got a valid listener. 510 NS_ENSURE_TRUE(*aListener, NS_ERROR_UNEXPECTED); 511 512 // Try to find the broadcaster element in the document. 513 Document* doc = aElement->GetComposedDoc(); 514 if (doc) { 515 *aBroadcaster = doc->GetElementById(aBroadcasterID); 516 } 517 518 // The broadcaster element is missing. 519 if (!*aBroadcaster) { 520 return NS_FINDBROADCASTER_NOT_FOUND; 521 } 522 523 NS_ADDREF(*aBroadcaster); 524 525 return NS_FINDBROADCASTER_FOUND; 526 } 527 528 nsresult XULBroadcastManager::UpdateListenerHookup(Element* aElement, 529 HookupAction aAction) { 530 // Resolve a broadcaster hookup. Look at the element that we're 531 // trying to resolve: it could be an '<observes>' element, or just 532 // a vanilla element with an 'observes' attribute on it. 533 nsresult rv; 534 535 nsCOMPtr<Element> listener; 536 nsAutoString broadcasterID; 537 nsAutoString attribute; 538 nsCOMPtr<Element> broadcaster; 539 540 rv = FindBroadcaster(aElement, getter_AddRefs(listener), broadcasterID, 541 attribute, getter_AddRefs(broadcaster)); 542 switch (rv) { 543 case NS_FINDBROADCASTER_NOT_FOUND: 544 return NS_OK; 545 case NS_FINDBROADCASTER_FOUND: 546 break; 547 default: 548 return rv; 549 } 550 551 NS_ENSURE_ARG(broadcaster && listener); 552 if (aAction == eHookupAdd) { 553 ErrorResult domRv; 554 AddListenerFor(*broadcaster, *listener, attribute, domRv); 555 if (domRv.Failed()) { 556 return domRv.StealNSResult(); 557 } 558 } else { 559 RemoveListenerFor(*broadcaster, *listener, attribute); 560 } 561 562 // Tell the world we succeeded 563 if (MOZ_LOG_TEST(sXULBroadCastManager, LogLevel::Debug)) { 564 nsCOMPtr<nsIContent> content = listener; 565 NS_ASSERTION(content != nullptr, "not an nsIContent"); 566 if (!content) { 567 return rv; 568 } 569 570 nsAutoCString attributeC, broadcasteridC; 571 LossyCopyUTF16toASCII(attribute, attributeC); 572 LossyCopyUTF16toASCII(broadcasterID, broadcasteridC); 573 MOZ_LOG(sXULBroadCastManager, LogLevel::Debug, 574 ("xul: broadcaster hookup <%s attribute='%s'> to %s", 575 nsAtomCString(content->NodeInfo()->NameAtom()).get(), 576 attributeC.get(), broadcasteridC.get())); 577 } 578 579 return NS_OK; 580 } 581 582 nsresult XULBroadcastManager::AddListener(Element* aElement) { 583 return UpdateListenerHookup(aElement, eHookupAdd); 584 } 585 586 nsresult XULBroadcastManager::RemoveListener(Element* aElement) { 587 return UpdateListenerHookup(aElement, eHookupRemove); 588 } 589 590 } // namespace mozilla::dom