diff options
Diffstat (limited to 'dom/xul')
96 files changed, 13144 insertions, 0 deletions
diff --git a/dom/xul/ChromeObserver.cpp b/dom/xul/ChromeObserver.cpp new file mode 100644 index 0000000000..4a21fdbbbb --- /dev/null +++ b/dom/xul/ChromeObserver.cpp @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "ChromeObserver.h" + +#include "nsIBaseWindow.h" +#include "nsIWidget.h" +#include "nsIFrame.h" + +#include "nsContentUtils.h" +#include "nsView.h" +#include "nsPresContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(ChromeObserver, nsIMutationObserver) + +ChromeObserver::ChromeObserver(Document* aDocument) + : nsStubMutationObserver(), mDocument(aDocument) {} + +ChromeObserver::~ChromeObserver() = default; + +void ChromeObserver::Init() { + mDocument->AddMutationObserver(this); + Element* rootElement = mDocument->GetRootElement(); + if (!rootElement) { + return; + } + nsAutoScriptBlocker scriptBlocker; + uint32_t attributeCount = rootElement->GetAttrCount(); + for (uint32_t i = 0; i < attributeCount; i++) { + BorrowedAttrInfo info = rootElement->GetAttrInfoAt(i); + const nsAttrName* name = info.mName; + if (name->LocalName() == nsGkAtoms::chromemargin) { + // Some linux windows managers have an issue when the chrome margin is + // applied while the browser is loading (bug 1598848). For now, skip + // applying this attribute when initializing. + continue; + } + AttributeChanged(rootElement, name->NamespaceID(), name->LocalName(), + MutationEvent_Binding::ADDITION, nullptr); + } +} + +nsIWidget* ChromeObserver::GetWindowWidget() { + // only top level chrome documents can set the titlebar color + if (mDocument && mDocument->IsRootDisplayDocument()) { + nsCOMPtr<nsISupports> container = mDocument->GetContainer(); + nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(container); + if (baseWindow) { + nsCOMPtr<nsIWidget> mainWidget; + baseWindow->GetMainWidget(getter_AddRefs(mainWidget)); + return mainWidget; + } + } + return nullptr; +} + +class SetDrawInTitleBarEvent : public Runnable { + public: + SetDrawInTitleBarEvent(nsIWidget* aWidget, bool aState) + : mozilla::Runnable("SetDrawInTitleBarEvent"), + mWidget(aWidget), + mState(aState) {} + + NS_IMETHOD Run() override { + NS_ASSERTION(mWidget, + "You shouldn't call this runnable with a null widget!"); + + mWidget->SetDrawsInTitlebar(mState); + return NS_OK; + } + + private: + nsCOMPtr<nsIWidget> mWidget; + bool mState; +}; + +void ChromeObserver::SetDrawsInTitlebar(bool aState) { + nsIWidget* mainWidget = GetWindowWidget(); + if (mainWidget) { + nsContentUtils::AddScriptRunner( + new SetDrawInTitleBarEvent(mainWidget, aState)); + } +} + +void ChromeObserver::SetDrawsTitle(bool aState) { + nsIWidget* mainWidget = GetWindowWidget(); + if (mainWidget) { + // We can do this synchronously because SetDrawsTitle doesn't have any + // synchronous effects apart from a harmless invalidation. + mainWidget->SetDrawsTitle(aState); + } +} + +class MarginSetter : public Runnable { + public: + explicit MarginSetter(nsIWidget* aWidget) + : mozilla::Runnable("MarginSetter"), + mWidget(aWidget), + mMargin(-1, -1, -1, -1) {} + MarginSetter(nsIWidget* aWidget, const LayoutDeviceIntMargin& aMargin) + : mozilla::Runnable("MarginSetter"), mWidget(aWidget), mMargin(aMargin) {} + + NS_IMETHOD Run() override { + // SetNonClientMargins can dispatch native events, hence doing + // it off a script runner. + mWidget->SetNonClientMargins(mMargin); + return NS_OK; + } + + private: + nsCOMPtr<nsIWidget> mWidget; + LayoutDeviceIntMargin mMargin; +}; + +void ChromeObserver::SetChromeMargins(const nsAttrValue* aValue) { + if (!aValue) return; + + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) return; + + // top, right, bottom, left - see nsAttrValue + nsIntMargin margins; + bool gotMargins = false; + + if (aValue->Type() == nsAttrValue::eIntMarginValue) { + gotMargins = aValue->GetIntMarginValue(margins); + } else { + nsAutoString tmp; + aValue->ToString(tmp); + gotMargins = nsContentUtils::ParseIntMarginValue(tmp, margins); + } + if (gotMargins) { + nsContentUtils::AddScriptRunner(new MarginSetter( + mainWidget, LayoutDeviceIntMargin::FromUnknownMargin(margins))); + } +} + +void ChromeObserver::AttributeChanged(dom::Element* aElement, + int32_t aNamespaceID, nsAtom* aName, + int32_t aModType, + const nsAttrValue* aOldValue) { + // We only care about changes to the root element. + if (!mDocument || aElement != mDocument->GetRootElement()) { + return; + } + + const nsAttrValue* value = aElement->GetParsedAttr(aName, aNamespaceID); + if (value) { + // Hide chrome if needed + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::chromemargin) { + SetChromeMargins(value); + } + // title and drawintitlebar are settable on + // any root node (windows, dialogs, etc) + else if (aName == nsGkAtoms::title) { + mDocument->NotifyPossibleTitleChange(false); + } else if (aName == nsGkAtoms::drawintitlebar) { + SetDrawsInTitlebar(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::drawtitle) { + SetDrawsTitle(value->Equals(u"true"_ns, eCaseMatters)); + } else if (aName == nsGkAtoms::localedir) { + // if the localedir changed on the root element, reset the document + // direction + mDocument->ResetDocumentDirection(); + } else if (aName == nsGkAtoms::lwtheme) { + // if the lwtheme changed, make sure to reset the document lwtheme + // cache + mDocument->ResetDocumentLWTheme(); + } + } else { + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(false); + } else if (aName == nsGkAtoms::chromemargin) { + ResetChromeMargins(); + } else if (aName == nsGkAtoms::localedir) { + // if the localedir changed on the root element, reset the document + // direction + mDocument->ResetDocumentDirection(); + } else if (aName == nsGkAtoms::lwtheme) { + // if the lwtheme changed, make sure to restyle appropriately + mDocument->ResetDocumentLWTheme(); + } else if (aName == nsGkAtoms::drawintitlebar) { + SetDrawsInTitlebar(false); + } else if (aName == nsGkAtoms::drawtitle) { + SetDrawsTitle(false); + } + } +} + +void ChromeObserver::NodeWillBeDestroyed(nsINode* aNode) { + mDocument = nullptr; +} + +void ChromeObserver::ResetChromeMargins() { + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) return; + // See nsIWidget + nsContentUtils::AddScriptRunner(new MarginSetter(mainWidget)); +} + +nsresult ChromeObserver::HideWindowChrome(bool aShouldHide) { + // only top level chrome documents can hide the window chrome + if (!mDocument->IsRootDisplayDocument()) return NS_OK; + + nsPresContext* presContext = mDocument->GetPresContext(); + + if (presContext && presContext->IsChrome()) { + nsIFrame* frame = mDocument->GetDocumentElement()->GetPrimaryFrame(); + + if (frame) { + nsView* view = frame->GetClosestView(); + + if (view) { + nsIWidget* w = view->GetWidget(); + NS_ENSURE_STATE(w); + w->HideWindowChrome(aShouldHide); + } + } + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/ChromeObserver.h b/dom/xul/ChromeObserver.h new file mode 100644 index 0000000000..af897c4420 --- /dev/null +++ b/dom/xul/ChromeObserver.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef mozilla_dom_ChromeObserver_h +#define mozilla_dom_ChromeObserver_h + +#include "nsStubMutationObserver.h" + +class nsIWidget; + +namespace mozilla::dom { +class Document; + +class ChromeObserver final : public nsStubMutationObserver { + public: + NS_DECL_ISUPPORTS + + explicit ChromeObserver(Document* aDocument); + void Init(); + + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + + protected: + nsIWidget* GetWindowWidget(); + void SetDrawsInTitlebar(bool aState); + void SetDrawsTitle(bool aState); + void SetChromeMargins(const nsAttrValue* aValue); + nsresult HideWindowChrome(bool aShouldHide); + + private: + void ResetChromeMargins(); + ~ChromeObserver(); + // A weak pointer cleared when the element will be destroyed. + Document* MOZ_NON_OWNING_REF mDocument; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ChromeObserver_h diff --git a/dom/xul/XULBroadcastManager.cpp b/dom/xul/XULBroadcastManager.cpp new file mode 100644 index 0000000000..7347036f1c --- /dev/null +++ b/dom/xul/XULBroadcastManager.cpp @@ -0,0 +1,592 @@ +/* -*- 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(kNameSpaceID_None, 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(kNameSpaceID_None, nsGkAtoms::element, + listeningToID); + + nsAutoString broadcasterID; + aBroadcaster->GetAttr(kNameSpaceID_None, nsGkAtoms::id, broadcasterID); + + if (listeningToID != broadcasterID) continue; + + // We are observing the broadcaster, but is this the right + // attribute? + nsAutoString listeningToAttribute; + child->AsElement()->GetAttr(kNameSpaceID_None, 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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(kNameSpaceID_None, nsGkAtoms::element, aBroadcasterID); + if (aBroadcasterID.IsEmpty()) { + return NS_FINDBROADCASTER_NOT_FOUND; + } + aElement->GetAttr(kNameSpaceID_None, 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(kNameSpaceID_None, nsGkAtoms::observes, aBroadcasterID); + + // Bail if there's no aBroadcasterID + if (aBroadcasterID.IsEmpty()) { + // Try the command attribute next. + aElement->GetAttr(kNameSpaceID_None, 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 diff --git a/dom/xul/XULBroadcastManager.h b/dom/xul/XULBroadcastManager.h new file mode 100644 index 0000000000..77fe6168eb --- /dev/null +++ b/dom/xul/XULBroadcastManager.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef mozilla_dom_XULBroadcastManager_h +#define mozilla_dom_XULBroadcastManager_h + +#include "nsAtom.h" +#include "nsTArray.h" + +class PLDHashTable; +class nsXULElement; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class Document; +class Element; + +class XULBroadcastManager final { + public: + explicit XULBroadcastManager(Document* aDocument); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(XULBroadcastManager) + + /** + * Checks whether an element uses any of the special broadcaster attributes + * or is an observes element. This mimics the logic in FindBroadcaster, but + * is intended to be a lighter weight check and doesn't actually guarantee + * that the element will need a listener. + */ + static bool MayNeedListener(const Element& aElement); + + nsresult AddListener(Element* aElement); + nsresult RemoveListener(Element* aElement); + void AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void MaybeBroadcast(); + void DropDocumentReference(); // notification that doc is going away + protected: + enum HookupAction { eHookupAdd = 0, eHookupRemove }; + + nsresult UpdateListenerHookup(Element* aElement, HookupAction aAction); + + void RemoveListenerFor(Element& aBroadcaster, Element& aListener, + const nsAString& aAttr); + void AddListenerFor(Element& aBroadcaster, Element& aListener, + const nsAString& aAttr, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT nsresult ExecuteOnBroadcastHandlerFor( + Element* aBroadcaster, Element* aListener, nsAtom* aAttr); + // The out params of FindBroadcaster only have values that make sense when + // the method returns NS_FINDBROADCASTER_FOUND. In all other cases, the + // values of the out params should not be relied on (though *aListener and + // *aBroadcaster do need to be released if non-null, of course). + nsresult FindBroadcaster(Element* aElement, Element** aListener, + nsString& aBroadcasterID, nsString& aAttribute, + Element** aBroadcaster); + + void SynchronizeBroadcastListener(Element* aBroadcaster, Element* aListener, + const nsAString& aAttr); + + // This reference is nulled by the Document in it's destructor through + // DropDocumentReference(). + Document* MOZ_NON_OWNING_REF mDocument; + + /** + * A map from a broadcaster element to a list of listener elements. + */ + PLDHashTable* mBroadcasterMap; + + class nsDelayedBroadcastUpdate; + nsTArray<nsDelayedBroadcastUpdate> mDelayedBroadcasters; + nsTArray<nsDelayedBroadcastUpdate> mDelayedAttrChangeBroadcasts; + bool mHandlingDelayedAttrChange; + bool mHandlingDelayedBroadcasters; + + private: + ~XULBroadcastManager(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_XULBroadcastManager_h diff --git a/dom/xul/XULButtonElement.cpp b/dom/xul/XULButtonElement.cpp new file mode 100644 index 0000000000..546e1fbfe6 --- /dev/null +++ b/dom/xul/XULButtonElement.cpp @@ -0,0 +1,806 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "XULButtonElement.h" +#include "XULMenuParentElement.h" +#include "XULPopupElement.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/AncestorIterator.h" +#include "nsGkAtoms.h" +#include "nsITimer.h" +#include "nsLayoutUtils.h" +#include "nsCaseTreatment.h" +#include "nsChangeHint.h" +#include "nsMenuBarFrame.h" +#include "nsMenuPopupFrame.h" +#include "nsPlaceholderFrame.h" +#include "nsPresContext.h" +#include "nsXULPopupManager.h" +#include "nsIDOMXULButtonElement.h" +#include "nsISound.h" + +namespace mozilla::dom { + +XULButtonElement::XULButtonElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)), + mIsAlwaysMenu(IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menulist, + nsGkAtoms::menuitem)) {} + +XULButtonElement::~XULButtonElement() { + StopBlinking(); + KillMenuOpenTimer(); +} + +nsChangeHint XULButtonElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + if (aAttribute == nsGkAtoms::type && + IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton)) { + // type=menu switches to a menu frame. + return nsChangeHint_ReconstructFrame; + } + return nsXULElement::GetAttributeChangeHint(aAttribute, aModType); +} + +// This global flag is used to record the timestamp when a menu was opened or +// closed and is used to ignore the mousemove and mouseup events that would fire +// on the menu after the mousedown occurred. +static TimeStamp gMenuJustOpenedOrClosedTime = TimeStamp(); + +void XULButtonElement::PopupOpened() { + if (!IsMenu()) { + return; + } + gMenuJustOpenedOrClosedTime = TimeStamp::Now(); + SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true); +} + +void XULButtonElement::PopupClosed(bool aDeselectMenu) { + if (!IsMenu()) { + return; + } + nsContentUtils::AddScriptRunner( + new nsUnsetAttrRunnable(this, nsGkAtoms::open)); + + if (aDeselectMenu) { + if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) { + if (parent->GetActiveMenuChild() == this) { + parent->SetActiveMenuChild(nullptr); + } + } + } +} + +bool XULButtonElement::IsMenuActive() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->GetActiveMenuChild() == this; + } + return false; +} + +void XULButtonElement::HandleEnterKeyPress(WidgetEvent& aEvent) { + if (IsDisabled()) { +#ifdef XP_WIN + if (XULPopupElement* popup = GetContainingPopupElement()) { + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->HidePopup(popup, /* aHideChain = */ true, + /* aDeselectMenu = */ true, /* aAsynchronous = */ true, + /* aIsCancel = */ false); + } + } +#endif + return; + } + if (IsMenuPopupOpen()) { + return; + } + // The enter key press applies to us. + if (IsMenuItem()) { + ExecuteMenu(aEvent); + } else { + OpenMenuPopup(true); + } +} + +bool XULButtonElement::IsMenuPopupOpen() { + nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::None); + return popupFrame && popupFrame->IsOpen(); +} + +bool XULButtonElement::IsOnMenu() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return !menu->IsMenuBar(); + } + return false; +} + +bool XULButtonElement::IsOnMenuList() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->GetParent() && + menu->GetParent()->IsXULElement(nsGkAtoms::menulist); + } + return false; +} + +bool XULButtonElement::IsOnMenuBar() const { + if (XULMenuParentElement* menu = GetMenuParent()) { + return menu->IsMenuBar(); + } + return false; +} + +nsMenuPopupFrame* XULButtonElement::GetContainingPopupWithoutFlushing() const { + if (XULPopupElement* popup = GetContainingPopupElement()) { + return do_QueryFrame(popup->GetPrimaryFrame()); + } + return nullptr; +} + +XULPopupElement* XULButtonElement::GetContainingPopupElement() const { + return XULPopupElement::FromNodeOrNull(GetMenuParent()); +} + +bool XULButtonElement::IsOnContextMenu() const { + if (nsMenuPopupFrame* popup = GetContainingPopupWithoutFlushing()) { + return popup->IsContextMenu(); + } + return false; +} + +void XULButtonElement::ToggleMenuState() { + if (IsMenuPopupOpen()) { + CloseMenuPopup(false); + } else { + OpenMenuPopup(false); + } +} + +void XULButtonElement::KillMenuOpenTimer() { + if (mMenuOpenTimer) { + mMenuOpenTimer->Cancel(); + mMenuOpenTimer = nullptr; + } +} + +void XULButtonElement::OpenMenuPopup(bool aSelectFirstItem) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + + pm->KillMenuTimer(); + if (!pm->MayShowMenu(this)) { + return; + } + + if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) { + parent->SetActiveMenuChild(this); + } + + // Open the menu asynchronously. + OwnerDoc()->Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction( + "AsyncOpenMenu", [self = RefPtr{this}, aSelectFirstItem] { + if (self->GetMenuParent() && !self->IsMenuActive()) { + return; + } + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->ShowMenu(self, aSelectFirstItem); + } + })); +} + +void XULButtonElement::CloseMenuPopup(bool aDeselectMenu) { + gMenuJustOpenedOrClosedTime = TimeStamp::Now(); + // Close the menu asynchronously + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + if (auto* popup = GetMenuPopupContent()) { + pm->HidePopup(popup, false, aDeselectMenu, true, false); + } +} + +int32_t XULButtonElement::MenuOpenCloseDelay() const { + if (IsOnMenuBar()) { + return 0; + } + return LookAndFeel::GetInt(LookAndFeel::IntID::SubmenuDelay, 300); // ms +} + +void XULButtonElement::ExecuteMenu(Modifiers aModifiers, int16_t aButton, + bool aIsTrusted) { + MOZ_ASSERT(IsMenu()); + + StopBlinking(); + + auto menuType = GetMenuType(); + if (NS_WARN_IF(!menuType)) { + return; + } + + // Because the command event is firing asynchronously, a flag is needed to + // indicate whether user input is being handled. This ensures that a popup + // window won't get blocked. + const bool userinput = dom::UserActivation::IsHandlingUserInput(); + + // Flip "checked" state if we're a checkbox menu, or an un-checked radio menu. + bool needToFlipChecked = false; + if (*menuType == MenuType::Checkbox || + (*menuType == MenuType::Radio && !GetXULBoolAttr(nsGkAtoms::checked))) { + needToFlipChecked = !AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, + nsGkAtoms::_false, eCaseMatters); + } + + mDelayedMenuCommandEvent = new nsXULMenuCommandEvent( + this, aIsTrusted, aModifiers, userinput, needToFlipChecked, aButton); + StartBlinking(); +} + +void XULButtonElement::StopBlinking() { + if (mMenuBlinkTimer) { + if (auto* parent = GetMenuParent()) { + parent->LockMenuUntilClosed(false); + } + mMenuBlinkTimer->Cancel(); + mMenuBlinkTimer = nullptr; + } + mDelayedMenuCommandEvent = nullptr; +} + +void XULButtonElement::PassMenuCommandEventToPopupManager() { + if (mDelayedMenuCommandEvent) { + if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) { + RefPtr<nsXULMenuCommandEvent> event = std::move(mDelayedMenuCommandEvent); + nsCOMPtr<nsIContent> content = this; + pm->ExecuteMenu(content, event); + } + } + mDelayedMenuCommandEvent = nullptr; +} + +static constexpr int32_t kBlinkDelay = 67; // milliseconds + +void XULButtonElement::StartBlinking() { + if (!LookAndFeel::GetInt(LookAndFeel::IntID::ChosenMenuItemsShouldBlink)) { + PassMenuCommandEventToPopupManager(); + return; + } + + // Blink off. + UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); + if (auto* parent = GetMenuParent()) { + // Make this menu ignore events from now on. + parent->LockMenuUntilClosed(true); + } + + // Set up a timer to blink back on. + NS_NewTimerWithFuncCallback( + getter_AddRefs(mMenuBlinkTimer), + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast<XULButtonElement*>(aClosure); + if (auto* parent = self->GetMenuParent()) { + if (parent->GetActiveMenuChild() == self) { + // Restore the highlighting if we're still the active item. + self->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns, + true); + } + } + // Reuse our timer to actually execute. + self->mMenuBlinkTimer->InitWithNamedFuncCallback( + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast<XULButtonElement*>(aClosure); + if (auto* parent = self->GetMenuParent()) { + parent->LockMenuUntilClosed(false); + } + self->PassMenuCommandEventToPopupManager(); + self->StopBlinking(); + }, + aClosure, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::ContinueBlinking"); + }, + this, kBlinkDelay, nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::StartBlinking", + OwnerDoc()->EventTargetFor(TaskCategory::Other)); +} + +void XULButtonElement::UnbindFromTree(bool aNullParent) { + StopBlinking(); + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULButtonElement::ExecuteMenu(WidgetEvent& aEvent) { + MOZ_ASSERT(IsMenu()); + if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) { + sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE); + } + + Modifiers modifiers = 0; + if (WidgetInputEvent* inputEvent = aEvent.AsInputEvent()) { + modifiers = inputEvent->mModifiers; + } + + int16_t button = 0; + if (WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase()) { + button = mouseEvent->mButton; + } + + ExecuteMenu(modifiers, button, aEvent.IsTrusted()); +} + +void XULButtonElement::PostHandleEventForMenus( + EventChainPostVisitor& aVisitor) { + auto* event = aVisitor.mEvent; + + if (event->mOriginalTarget != this) { + return; + } + + if (auto* parent = GetMenuParent()) { + if (NS_WARN_IF(parent->IsLocked())) { + return; + } + } + + // If a menu just opened, ignore the mouseup event that might occur after a + // the mousedown event that opened it. However, if a different mousedown event + // occurs, just clear this flag. + if (!gMenuJustOpenedOrClosedTime.IsNull()) { + if (event->mMessage == eMouseDown) { + gMenuJustOpenedOrClosedTime = TimeStamp(); + } else if (event->mMessage == eMouseUp) { + return; + } + } + + if (event->mMessage == eKeyPress && !IsDisabled()) { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + uint32_t keyCode = keyEvent->mKeyCode; +#ifdef XP_MACOSX + // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed) + if (!IsMenuPopupOpen() && + ((keyEvent->mCharCode == ' ' && !keyEvent->IsMeta()) || + (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) { + // When pressing space, don't open the menu if performing an incremental + // search. + if (keyEvent->mCharCode != ' ' || + !nsMenuPopupFrame::IsWithinIncrementalTime(keyEvent->mTimeStamp)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + OpenMenuPopup(false); + } + } +#else + // On other platforms, toggle menulist on unmodified F4 or Alt arrow + if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) || + ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + ToggleMenuState(); + } +#endif + } else if (event->mMessage == eMouseDown && + event->AsMouseEvent()->mButton == MouseButton::ePrimary && +#ifdef XP_MACOSX + // On mac, ctrl-click will send a context menu event from the + // widget, so we don't want to bring up the menu. + !event->AsMouseEvent()->IsControl() && +#endif + !IsDisabled() && !IsMenuItem()) { + // The menu item was selected. Bring up the menu. + // We have children. + // Don't prevent the default action here, since that will also cancel + // potential drag starts. + if (!IsOnMenu()) { + ToggleMenuState(); + } else if (!IsMenuPopupOpen()) { + OpenMenuPopup(false); + } + } else if (event->mMessage == eMouseUp && IsMenuItem() && !IsDisabled() && + !event->mFlags.mMultipleActionsPrevented) { + // We accept left and middle clicks on all menu items to activate the item. + // On context menus we also accept right click to activate the item, because + // right-clicking on an item in a context menu cannot open another context + // menu. + bool isMacCtrlClick = false; +#ifdef XP_MACOSX + isMacCtrlClick = event->AsMouseEvent()->mButton == MouseButton::ePrimary && + event->AsMouseEvent()->IsControl(); +#endif + bool clickMightOpenContextMenu = + event->AsMouseEvent()->mButton == MouseButton::eSecondary || + isMacCtrlClick; + if (!clickMightOpenContextMenu || IsOnContextMenu()) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + ExecuteMenu(*event); + } + } else if (event->mMessage == eContextMenu && IsOnContextMenu() && + !IsMenuItem() && !IsDisabled()) { + // Make sure we cancel default processing of the context menu event so + // that it doesn't bubble and get seen again by the popuplistener and show + // another context menu. + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (event->mMessage == eMouseOut) { + KillMenuOpenTimer(); + if (RefPtr<XULMenuParentElement> parent = GetMenuParent()) { + if (parent->GetActiveMenuChild() == this) { + // Deactivate the menu on mouse out in some cases... + const bool shouldDeactivate = [&] { + if (IsMenuPopupOpen()) { + // If we're open we never deselect. PopupClosed will do as needed. + return false; + } + if (!parent->IsMenuBar()) { + // Don't de-select when not in the menubar. + // NOTE(emilio): Behavior from before bug 1811466 is equivalent to + // returning true here, consider flipping this. + return false; + } + // De-select when exiting a menubar item, if the menubar wasn't + // activated by keyboard. + nsMenuBarFrame* menubar = do_QueryFrame(parent->GetPrimaryFrame()); + const bool openedByKey = menubar && menubar->IsActiveByKeyboard(); + return !openedByKey; + }(); + + if (shouldDeactivate) { + parent->SetActiveMenuChild(nullptr); + } + } + } + } else if (event->mMessage == eMouseMove && (IsOnMenu() || IsOnMenuBar())) { + // Use a tolerance to address situations where a user might perform a + // "wiggly" click that is accompanied by near-simultaneous mousemove events. + const TimeDuration kTolerance = TimeDuration::FromMilliseconds(200); + if (!gMenuJustOpenedOrClosedTime.IsNull() && + gMenuJustOpenedOrClosedTime + kTolerance < TimeStamp::Now()) { + gMenuJustOpenedOrClosedTime = TimeStamp(); + return; + } + + if (IsDisabled() && IsOnMenuList()) { + return; + } + + RefPtr<XULMenuParentElement> parent = GetMenuParent(); + MOZ_ASSERT(parent, "How did IsOnMenu{,Bar} return true then?"); + + const bool isOnOpenMenubar = + parent->IsMenuBar() && parent->GetActiveMenuChild() && + parent->GetActiveMenuChild()->IsMenuPopupOpen(); + + parent->SetActiveMenuChild(this); + + // We need to check if we really became the current menu item or not. + if (!IsMenuActive()) { + // We didn't (presumably because a context menu was active) + return; + } + if (IsDisabled() || IsMenuItem() || IsMenuPopupOpen() || mMenuOpenTimer) { + // Disabled, or already opening or what not. + return; + } + + if (parent->IsMenuBar() && !isOnOpenMenubar) { + // We should only open on hover in the menubar iff the menubar is open + // already. + return; + } + + // A timer is used so that it doesn't open if the user moves the mouse + // quickly past the menu. The MenuOpenCloseDelay ensures that only menus + // have this behaviour. + NS_NewTimerWithFuncCallback( + getter_AddRefs(mMenuOpenTimer), + [](nsITimer*, void* aClosure) MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr self = static_cast<XULButtonElement*>(aClosure); + self->mMenuOpenTimer = nullptr; + if (self->IsMenuPopupOpen()) { + return; + } + // make sure we didn't open a context menu in the meantime + // (i.e. the user right-clicked while hovering over a submenu). + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + if (pm->HasContextMenu(nullptr) && !self->IsOnContextMenu()) { + return; + } + if (!self->IsMenuActive()) { + return; + } + self->OpenMenuPopup(false); + }, + this, MenuOpenCloseDelay(), nsITimer::TYPE_ONE_SHOT, + "XULButtonElement::OpenMenu", + OwnerDoc()->EventTargetFor(TaskCategory::Other)); + } +} + +nsresult XULButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { + return nsXULElement::PostHandleEvent(aVisitor); + } + + if (IsMenu()) { + PostHandleEventForMenus(aVisitor); + return nsXULElement::PostHandleEvent(aVisitor); + } + + auto* event = aVisitor.mEvent; + switch (event->mMessage) { + case eBlur: { + Blurred(); + break; + } + case eKeyDown: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (keyEvent->ShouldWorkAsSpaceKey() && aVisitor.mPresContext) { + EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); + // :hover:active state + esm->SetContentState(this, ElementState::HOVER); + esm->SetContentState(this, ElementState::ACTIVE); + mIsHandlingKeyEvent = true; + } + break; + } + +// On mac, Return fires the default button, not the focused one. +#ifndef XP_MACOSX + case eKeyPress: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (NS_VK_RETURN == keyEvent->mKeyCode) { + if (RefPtr<nsIDOMXULButtonElement> button = AsXULButton()) { + if (MouseClicked(*keyEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + } + break; + } +#endif + + case eKeyUp: { + WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent(); + if (!keyEvent) { + break; + } + if (keyEvent->ShouldWorkAsSpaceKey()) { + mIsHandlingKeyEvent = false; + ElementState buttonState = State(); + if (buttonState.HasAllStates(ElementState::ACTIVE | + ElementState::HOVER) && + aVisitor.mPresContext) { + // return to normal state + EventStateManager* esm = aVisitor.mPresContext->EventStateManager(); + esm->SetContentState(nullptr, ElementState::ACTIVE); + esm->SetContentState(nullptr, ElementState::HOVER); + if (MouseClicked(*keyEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + } + break; + } + + case eMouseClick: { + WidgetMouseEvent* mouseEvent = event->AsMouseEvent(); + if (mouseEvent->IsLeftClickEvent()) { + if (MouseClicked(*mouseEvent)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + break; + } + + default: + break; + } + + return nsXULElement::PostHandleEvent(aVisitor); +} + +void XULButtonElement::Blurred() { + ElementState buttonState = State(); + if (mIsHandlingKeyEvent && + buttonState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER)) { + // Return to normal state + if (nsPresContext* pc = OwnerDoc()->GetPresContext()) { + EventStateManager* esm = pc->EventStateManager(); + esm->SetContentState(nullptr, ElementState::ACTIVE); + esm->SetContentState(nullptr, ElementState::HOVER); + } + } + mIsHandlingKeyEvent = false; +} + +bool XULButtonElement::MouseClicked(WidgetGUIEvent& aEvent) { + // Don't execute if we're disabled. + if (IsDisabled() || !IsInComposedDoc()) { + return false; + } + + // Have the content handle the event, propagating it according to normal DOM + // rules. + RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell(); + if (!presShell) { + return false; + } + + // Execute the oncommand event handler. + WidgetInputEvent* inputEvent = aEvent.AsInputEvent(); + WidgetMouseEventBase* mouseEvent = aEvent.AsMouseEventBase(); + WidgetKeyboardEvent* keyEvent = aEvent.AsKeyboardEvent(); + // TODO: Set aSourceEvent? + nsContentUtils::DispatchXULCommand( + this, aEvent.IsTrusted(), /* aSourceEvent = */ nullptr, presShell, + inputEvent->IsControl(), inputEvent->IsAlt(), inputEvent->IsShift(), + inputEvent->IsMeta(), + mouseEvent ? mouseEvent->mInputSource + : (keyEvent ? MouseEvent_Binding::MOZ_SOURCE_KEYBOARD + : MouseEvent_Binding::MOZ_SOURCE_UNKNOWN), + mouseEvent ? mouseEvent->mButton : 0); + return true; +} + +bool XULButtonElement::IsMenu() const { + if (mIsAlwaysMenu) { + return true; + } + return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton) && + AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu, + eCaseMatters); +} + +void XULButtonElement::UncheckRadioSiblings() { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(GetMenuType() == Some(MenuType::Radio)); + nsAutoString groupName; + GetAttr(nsGkAtoms::name, groupName); + + nsIContent* parent = GetParent(); + if (!parent) { + return; + } + + auto ShouldUncheck = [&](const nsIContent& aSibling) { + const auto* button = XULButtonElement::FromNode(aSibling); + if (!button || button->GetMenuType() != Some(MenuType::Radio)) { + return false; + } + if (const auto* attr = button->GetParsedAttr(nsGkAtoms::name)) { + if (!attr->Equals(groupName, eCaseMatters)) { + return false; + } + } else if (!groupName.IsEmpty()) { + return false; + } + // we're in the same group, only uncheck if we're checked (for some reason, + // some tests rely on that specifically). + return button->GetXULBoolAttr(nsGkAtoms::checked); + }; + + for (nsIContent* child = parent->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child == this || !ShouldUncheck(*child)) { + continue; + } + child->AsElement()->UnsetAttr(nsGkAtoms::checked, IgnoreErrors()); + } +} + +nsresult XULButtonElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + MOZ_TRY(nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify)); + if (IsAlwaysMenu() && aNamespaceID == kNameSpaceID_None) { + // We need to uncheck radio siblings when we're a checked radio and switch + // groups, or become checked. + const bool shouldUncheckSiblings = [&] { + if (aName == nsGkAtoms::type || aName == nsGkAtoms::name) { + return *GetMenuType() == MenuType::Radio && + GetXULBoolAttr(nsGkAtoms::checked); + } + if (aName == nsGkAtoms::checked && aValue && + aValue->Equals(nsGkAtoms::_true, eCaseMatters)) { + return *GetMenuType() == MenuType::Radio; + } + return false; + }(); + if (shouldUncheckSiblings) { + UncheckRadioSiblings(); + } + } + return NS_OK; +} + +auto XULButtonElement::GetMenuType() const -> Maybe<MenuType> { + if (!IsAlwaysMenu()) { + return Nothing(); + } + + static Element::AttrValuesArray values[] = {nsGkAtoms::checkbox, + nsGkAtoms::radio, nullptr}; + switch (FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, values, + eCaseMatters)) { + case 0: + return Some(MenuType::Checkbox); + case 1: + return Some(MenuType::Radio); + default: + return Some(MenuType::Normal); + } +} + +nsMenuBarFrame* XULButtonElement::GetMenuBar(FlushType aFlushType) { + if (!IsMenu()) { + return nullptr; + } + nsIFrame* frame = GetPrimaryFrame(aFlushType); + for (; frame; frame = frame->GetParent()) { + if (nsMenuBarFrame* menubar = do_QueryFrame(frame)) { + return menubar; + } + } + return nullptr; +} + +XULMenuParentElement* XULButtonElement::GetMenuParent() const { + if (IsXULElement(nsGkAtoms::menulist)) { + return nullptr; + } + return FirstAncestorOfType<XULMenuParentElement>(); +} + +XULPopupElement* XULButtonElement::GetMenuPopupContent() const { + if (!IsMenu()) { + return nullptr; + } + for (auto* child = GetFirstChild(); child; child = child->GetNextSibling()) { + if (auto* popup = XULPopupElement::FromNode(child)) { + return popup; + } + } + return nullptr; +} + +nsMenuPopupFrame* XULButtonElement::GetMenuPopupWithoutFlushing() const { + return const_cast<XULButtonElement*>(this)->GetMenuPopup(FlushType::None); +} + +nsMenuPopupFrame* XULButtonElement::GetMenuPopup(FlushType aFlushType) { + RefPtr popup = GetMenuPopupContent(); + if (!popup) { + return nullptr; + } + return do_QueryFrame(popup->GetPrimaryFrame(aFlushType)); +} + +bool XULButtonElement::OpenedWithKey() { + nsMenuBarFrame* menubar = GetMenuBar(FlushType::Frames); + return menubar && menubar->IsActiveByKeyboard(); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULButtonElement.h b/dom/xul/XULButtonElement.h new file mode 100644 index 0000000000..78f50e78e0 --- /dev/null +++ b/dom/xul/XULButtonElement.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef dom_xul_XULButtonElement_h__ +#define dom_xul_XULButtonElement_h__ + +#include "mozilla/Attributes.h" +#include "nsINode.h" +#include "nsXULElement.h" + +class nsMenuBarFrame; +class nsMenuPopupFrame; +class nsXULMenuCommandEvent; + +namespace mozilla::dom { + +class KeyboardEvent; +class XULPopupElement; +class XULMenuParentElement; + +class XULButtonElement : public nsXULElement { + public: + explicit XULButtonElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + ~XULButtonElement() override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool MouseClicked(WidgetGUIEvent&); + MOZ_CAN_RUN_SCRIPT nsresult PostHandleEvent(EventChainPostVisitor&) override; + MOZ_CAN_RUN_SCRIPT void PostHandleEventForMenus(EventChainPostVisitor&); + MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(WidgetEvent&); + + void PopupOpened(); + MOZ_CAN_RUN_SCRIPT void PopupClosed(bool aDeselectMenu); + + XULPopupElement* GetContainingPopupElement() const; + nsMenuPopupFrame* GetContainingPopupWithoutFlushing() const; + MOZ_CAN_RUN_SCRIPT void ToggleMenuState(); + bool IsMenuPopupOpen(); + + bool IsMenuItem() const { return NodeInfo()->Equals(nsGkAtoms::menuitem); } + bool IsMenuList() const { return NodeInfo()->Equals(nsGkAtoms::menulist); } + bool IsMenuActive() const; + MOZ_CAN_RUN_SCRIPT void OpenMenuPopup(bool aSelectFirstItem); + void CloseMenuPopup(bool aDeselectMenu); + + bool IsOnMenu() const; + bool IsOnMenuList() const; + bool IsOnMenuBar() const; + bool IsOnContextMenu() const; + + XULMenuParentElement* GetMenuParent() const; + + void UnbindFromTree(bool aNullParent) override; + + MOZ_CAN_RUN_SCRIPT bool HandleKeyPress(KeyboardEvent& keyEvent); + MOZ_CAN_RUN_SCRIPT bool OpenedWithKey(); + // Called to execute our command handler. + MOZ_CAN_RUN_SCRIPT void ExecuteMenu(WidgetEvent&); + MOZ_CAN_RUN_SCRIPT void ExecuteMenu(Modifiers, int16_t aButton, + bool aIsTrusted); + + // Whether we are a menu/menulist/menuitem element. + bool IsAlwaysMenu() const { return mIsAlwaysMenu; } + // Whether we should behave like a menu. This is the above plus buttons with + // type=menu attribute. + bool IsMenu() const; + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + nsresult AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + NS_IMPL_FROMNODE_HELPER(XULButtonElement, + IsAnyOfXULElements(nsGkAtoms::checkbox, + nsGkAtoms::radio, nsGkAtoms::thumb, + nsGkAtoms::button, nsGkAtoms::menu, + nsGkAtoms::menulist, + nsGkAtoms::menuitem, + nsGkAtoms::toolbarbutton, + nsGkAtoms::toolbarpaletteitem, + nsGkAtoms::scrollbarbutton)) + + nsMenuPopupFrame* GetMenuPopup(FlushType aFlushType); + nsMenuPopupFrame* GetMenuPopupWithoutFlushing() const; + XULPopupElement* GetMenuPopupContent() const; + int32_t MenuOpenCloseDelay() const; + + bool IsDisabled() const { return GetXULBoolAttr(nsGkAtoms::disabled); } + + private: + void Blurred(); + nsMenuBarFrame* GetMenuBar(FlushType aFlushType); + enum class MenuType { + Checkbox, + Radio, + Normal, + }; + Maybe<MenuType> GetMenuType() const; + + void UncheckRadioSiblings(); + void StopBlinking(); + MOZ_CAN_RUN_SCRIPT void StartBlinking(); + void KillMenuOpenTimer(); + MOZ_CAN_RUN_SCRIPT void PassMenuCommandEventToPopupManager(); + + bool mIsHandlingKeyEvent = false; + + // Whether this is a XULMenuElement. + const bool mIsAlwaysMenu; + RefPtr<nsXULMenuCommandEvent> mDelayedMenuCommandEvent; + nsCOMPtr<nsITimer> mMenuOpenTimer; + nsCOMPtr<nsITimer> mMenuBlinkTimer; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/xul/XULFrameElement.cpp b/dom/xul/XULFrameElement.cpp new file mode 100644 index 0000000000..2825a610e7 --- /dev/null +++ b/dom/xul/XULFrameElement.cpp @@ -0,0 +1,201 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsCOMPtr.h" +#include "nsIBrowser.h" +#include "nsIContent.h" +#include "nsIOpenWindowInfo.h" +#include "nsFrameLoader.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/dom/XULFrameElement.h" +#include "mozilla/dom/XULFrameElementBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(XULFrameElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(XULFrameElement, nsXULElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpenWindowInfo) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XULFrameElement, nsXULElement) + if (tmp->mFrameLoader) { + tmp->mFrameLoader->Destroy(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpenWindowInfo) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(XULFrameElement, nsXULElement, + nsFrameLoaderOwner) + +JSObject* XULFrameElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULFrameElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsDocShell* XULFrameElement::GetDocShell() { + RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(); + return frameLoader ? frameLoader->GetDocShell(IgnoreErrors()) : nullptr; +} + +already_AddRefed<nsIWebNavigation> XULFrameElement::GetWebNavigation() { + nsCOMPtr<nsIDocShell> docShell = GetDocShell(); + nsCOMPtr<nsIWebNavigation> webnav = do_QueryInterface(docShell); + return webnav.forget(); +} + +Nullable<WindowProxyHolder> XULFrameElement::GetContentWindow() { + RefPtr<nsDocShell> docShell = GetDocShell(); + if (docShell) { + return docShell->GetWindowProxy(); + } + + return nullptr; +} + +Document* XULFrameElement::GetContentDocument() { + nsCOMPtr<nsIDocShell> docShell = GetDocShell(); + if (docShell) { + nsCOMPtr<nsPIDOMWindowOuter> win = docShell->GetWindow(); + if (win) { + return win->GetDoc(); + } + } + return nullptr; +} + +uint64_t XULFrameElement::BrowserId() { + if (mFrameLoader) { + if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) { + return bc->GetBrowserId(); + } + } + return 0; +} + +nsIOpenWindowInfo* XULFrameElement::GetOpenWindowInfo() const { + return mOpenWindowInfo; +} + +void XULFrameElement::SetOpenWindowInfo(nsIOpenWindowInfo* aInfo) { + mOpenWindowInfo = aInfo; +} + +void XULFrameElement::LoadSrc() { + if (!IsInUncomposedDoc() || !OwnerDoc()->GetRootElement()) { + return; + } + RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(); + if (!frameLoader) { + // We may have had a nsIOpenWindowInfo set on us by browser chrome, due to + // being used as the target for a `window.open` call. Fetch that information + // if it's available, and clear it out so we don't read it again. + nsCOMPtr<nsIOpenWindowInfo> openWindowInfo = mOpenWindowInfo.forget(); + + // false as the networkCreated parameter so that xul:iframe/browser/editor + // session history handling works like dynamic html:iframes. Usually xul + // elements are used in chrome, which doesn't have session history at all. + mFrameLoader = nsFrameLoader::Create(this, false, openWindowInfo); + if (NS_WARN_IF(!mFrameLoader)) { + return; + } + + (new AsyncEventDispatcher(this, u"XULFrameLoaderCreated"_ns, + CanBubble::eYes)) + ->RunDOMEventWhenSafe(); + } + + mFrameLoader->LoadFrame(false); +} + +void XULFrameElement::SwapFrameLoaders(HTMLIFrameElement& aOtherLoaderOwner, + ErrorResult& rv) { + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void XULFrameElement::SwapFrameLoaders(XULFrameElement& aOtherLoaderOwner, + ErrorResult& rv) { + if (&aOtherLoaderOwner == this) { + // nothing to do + return; + } + + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void XULFrameElement::SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner, + mozilla::ErrorResult& rv) { + if (RefPtr<Document> doc = GetComposedDoc()) { + // SwapWithOtherLoader relies on frames being up-to-date. + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr<nsFrameLoader> loader = GetFrameLoader(); + RefPtr<nsFrameLoader> otherLoader = aOtherLoaderOwner->GetFrameLoader(); + if (!loader || !otherLoader) { + rv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } + + rv = loader->SwapWithOtherLoader(otherLoader, this, aOtherLoaderOwner); +} + +nsresult XULFrameElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsXULElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInUncomposedDoc()) { + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "Missing a script blocker!"); + // We're in a document now. Kick off the frame load. + LoadSrc(); + } + + return NS_OK; +} + +void XULFrameElement::UnbindFromTree(bool aNullParent) { + if (RefPtr<nsFrameLoader> frameLoader = GetFrameLoader()) { + frameLoader->Destroy(); + } + mFrameLoader = nullptr; + + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULFrameElement::DestroyContent() { + RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(); + if (frameLoader) { + frameLoader->Destroy(); + } + mFrameLoader = nullptr; + + nsXULElement::DestroyContent(); +} + +nsresult XULFrameElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src && aValue) { + LoadSrc(); + } else if (aName == nsGkAtoms::disablefullscreen && mFrameLoader) { + if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) { + MOZ_ALWAYS_SUCCEEDS(bc->SetFullscreenAllowedByOwner(!aValue)); + } + } + } + + return nsXULElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULFrameElement.h b/dom/xul/XULFrameElement.h new file mode 100644 index 0000000000..72247c7f06 --- /dev/null +++ b/dom/xul/XULFrameElement.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULFrameElement_h__ +#define XULFrameElement_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "js/TypeDecls.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIOpenWindowInfo.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" +#include "nsFrameLoaderOwner.h" + +class nsIWebNavigation; +class nsFrameLoader; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BrowsingContext; + +class XULFrameElement final : public nsXULElement, public nsFrameLoaderOwner { + public: + explicit XULFrameElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULFrameElement, nsXULElement) + + // XULFrameElement.webidl + nsDocShell* GetDocShell(); + already_AddRefed<nsIWebNavigation> GetWebNavigation(); + Nullable<WindowProxyHolder> GetContentWindow(); + Document* GetContentDocument(); + uint64_t BrowserId(); + nsIOpenWindowInfo* GetOpenWindowInfo() const; + void SetOpenWindowInfo(nsIOpenWindowInfo* aInfo); + + void SwapFrameLoaders(mozilla::dom::HTMLIFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& rv); + void SwapFrameLoaders(XULFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& rv); + void SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner, + mozilla::ErrorResult& rv); + + // nsIContent + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent) override; + virtual void DestroyContent() override; + + virtual nsresult AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + + NS_IMPL_FROMNODE_HELPER(XULFrameElement, + IsAnyOfXULElements(nsGkAtoms::iframe, + nsGkAtoms::browser, + nsGkAtoms::editor)) + + protected: + virtual ~XULFrameElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void LoadSrc(); + + private: + nsCOMPtr<nsIOpenWindowInfo> mOpenWindowInfo; +}; + +} // namespace dom +} // namespace mozilla + +#endif // XULFrameElement_h diff --git a/dom/xul/XULMenuElement.cpp b/dom/xul/XULMenuElement.cpp new file mode 100644 index 0000000000..76c560fef6 --- /dev/null +++ b/dom/xul/XULMenuElement.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "mozilla/dom/XULMenuElement.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/XULMenuElementBinding.h" +#include "mozilla/dom/XULPopupElement.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "nsMenuBarListener.h" +#include "nsXULPopupManager.h" +#include "nsMenuPopupFrame.h" + +namespace mozilla::dom { + +JSObject* XULMenuElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULMenuElement_Binding::Wrap(aCx, this, aGivenProto); +} + +Element* XULMenuElement::GetActiveMenuChild() { + RefPtr popup = GetMenuPopupContent(); + return popup ? popup->GetActiveMenuChild() : nullptr; +} + +void XULMenuElement::SetActiveMenuChild(Element* aChild) { + RefPtr popup = GetMenuPopupContent(); + if (NS_WARN_IF(!popup)) { + return; + } + + if (!aChild) { + popup->SetActiveMenuChild(nullptr); + return; + } + auto* button = XULButtonElement::FromNode(aChild); + if (NS_WARN_IF(!button) || NS_WARN_IF(!button->IsMenu())) { + return; + } + // KnownLive because it's aChild. + popup->SetActiveMenuChild(MOZ_KnownLive(button)); +} + +bool XULButtonElement::HandleKeyPress(KeyboardEvent& keyEvent) { + RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return false; + } + + // if event has already been handled, bail + if (keyEvent.DefaultPrevented()) { + return false; + } + + if (nsMenuBarListener::IsAccessKeyPressed(keyEvent)) { + return false; + } + + nsMenuPopupFrame* popupFrame = GetMenuPopup(FlushType::Frames); + if (NS_WARN_IF(!popupFrame)) { + return false; + } + + uint32_t keyCode = keyEvent.KeyCode(); + switch (keyCode) { + case KeyboardEvent_Binding::DOM_VK_UP: + case KeyboardEvent_Binding::DOM_VK_DOWN: + case KeyboardEvent_Binding::DOM_VK_HOME: + case KeyboardEvent_Binding::DOM_VK_END: { + nsNavigationDirection theDirection; + theDirection = NS_DIRECTION_FROM_KEY_CODE(popupFrame, keyCode); + return pm->HandleKeyboardNavigationInPopup(popupFrame, theDirection); + } + default: + return pm->HandleShortcutNavigation(keyEvent, popupFrame); + } +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULMenuElement.h b/dom/xul/XULMenuElement.h new file mode 100644 index 0000000000..5def6839fc --- /dev/null +++ b/dom/xul/XULMenuElement.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef mozilla_dom_XULMenuElement_h +#define mozilla_dom_XULMenuElement_h + +#include "XULButtonElement.h" +#include "nsINode.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +class KeyboardEvent; + +class XULMenuElement final : public XULButtonElement { + public: + explicit XULMenuElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : XULButtonElement(std::move(aNodeInfo)) {} + + MOZ_CAN_RUN_SCRIPT void SetActiveMenuChild(Element*); + Element* GetActiveMenuChild(); + + NS_IMPL_FROMNODE_HELPER(XULMenuElement, + IsAnyOfXULElements(nsGkAtoms::menu, + nsGkAtoms::menulist)); + + private: + virtual ~XULMenuElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; +}; + +} // namespace mozilla::dom + +#endif // XULMenuElement_h diff --git a/dom/xul/XULMenuParentElement.cpp b/dom/xul/XULMenuParentElement.cpp new file mode 100644 index 0000000000..99d6f0a6c3 --- /dev/null +++ b/dom/xul/XULMenuParentElement.cpp @@ -0,0 +1,391 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "XULMenuParentElement.h" +#include "XULButtonElement.h" +#include "XULPopupElement.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/EventDispatcher.h" +#include "nsDebug.h" +#include "nsMenuBarFrame.h" +#include "nsMenuPopupFrame.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsUTF8Utils.h" +#include "nsXULElement.h" +#include "nsMenuBarListener.h" +#include "nsXULPopupManager.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULMenuParentElement, + nsXULElement) +NS_IMPL_CYCLE_COLLECTION_INHERITED(XULMenuParentElement, nsXULElement, + mActiveItem) + +XULMenuParentElement::XULMenuParentElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + +XULMenuParentElement::~XULMenuParentElement() = default; + +class MenuActivateEvent final : public Runnable { + public: + MenuActivateEvent(Element* aMenu, bool aIsActivate) + : Runnable("MenuActivateEvent"), mMenu(aMenu), mIsActivate(aIsActivate) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + nsAutoString domEventToFire; + if (mIsActivate) { + // Highlight the menu. + mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, u"true"_ns, + true); + // The menuactivated event is used by accessibility to track the user's + // movements through menus + domEventToFire.AssignLiteral("DOMMenuItemActive"); + } else { + // Unhighlight the menu. + mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); + domEventToFire.AssignLiteral("DOMMenuItemInactive"); + } + + RefPtr<nsPresContext> pc = mMenu->OwnerDoc()->GetPresContext(); + RefPtr<dom::Event> event = NS_NewDOMEvent(mMenu, pc, nullptr); + event->InitEvent(domEventToFire, true, true); + + event->SetTrusted(true); + + EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, pc, nullptr); + return NS_OK; + } + + private: + const RefPtr<Element> mMenu; + bool mIsActivate; +}; + +static void ActivateOrDeactivate(XULButtonElement& aButton, bool aActivate) { + if (!aButton.IsMenu()) { + return; + } + + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + if (aActivate) { + // Cancel the close timer if selecting a menu within the popup to be + // closed. + pm->CancelMenuTimer(aButton.GetContainingPopupWithoutFlushing()); + } else if (auto* popup = aButton.GetMenuPopupWithoutFlushing()) { + if (popup->IsOpen()) { + // Set up the close timer if deselecting an open sub-menu. + pm->HidePopupAfterDelay(popup, aButton.MenuOpenCloseDelay()); + } + } + } + + nsCOMPtr<nsIRunnable> event = new MenuActivateEvent(&aButton, aActivate); + aButton.OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); +} + +XULButtonElement* XULMenuParentElement::GetContainingMenu() const { + if (IsMenuBar()) { + return nullptr; + } + auto* button = XULButtonElement::FromNodeOrNull(GetParent()); + if (!button || !button->IsMenu()) { + return nullptr; + } + return button; +} + +void XULMenuParentElement::LockMenuUntilClosed(bool aLock) { + if (IsMenuBar()) { + return; + } + mLocked = aLock; + // Lock/Unlock the parent menu too. + if (XULButtonElement* menu = GetContainingMenu()) { + if (XULMenuParentElement* parent = menu->GetMenuParent()) { + parent->LockMenuUntilClosed(aLock); + } + } +} + +void XULMenuParentElement::SetActiveMenuChild(XULButtonElement* aChild, + ByKey aByKey) { + if (aChild == mActiveItem) { + return; + } + + if (mActiveItem) { + ActivateOrDeactivate(*mActiveItem, false); + } + mActiveItem = nullptr; + + if (nsMenuBarFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->SetActive(!!aChild); + } + + if (!aChild) { + return; + } + + // When a menu opens a submenu, the mouse will often be moved onto a sibling + // before moving onto an item within the submenu, causing the parent to become + // deselected. We need to ensure that the parent menu is reselected when an + // item in the submenu is selected. + if (RefPtr menu = GetContainingMenu()) { + if (RefPtr parent = menu->GetMenuParent()) { + parent->SetActiveMenuChild(menu, aByKey); + } + } + + mActiveItem = aChild; + ActivateOrDeactivate(*mActiveItem, true); + + if (IsInMenuList()) { + if (nsMenuPopupFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->EnsureActiveMenuListItemIsVisible(); +#ifdef XP_WIN + // On Windows, a menulist should update its value whenever navigation was + // done by the keyboard. + // + // NOTE(emilio): This is a rather odd per-platform behavior difference, + // but other browsers also do this. + if (aByKey == ByKey::Yes && f->IsOpen()) { + // Fire a command event as the new item, but we don't want to close the + // menu, blink it, or update any other state of the menuitem. The + // command event will cause the item to be selected. + RefPtr<mozilla::PresShell> presShell = OwnerDoc()->GetPresShell(); + nsContentUtils::DispatchXULCommand(aChild, /* aTrusted = */ true, + nullptr, presShell, false, false, + false, false); + } +#endif + } + } +} + +static bool IsValidMenuItem(const XULMenuParentElement& aMenuParent, + const nsIContent& aContent) { + const auto* button = XULButtonElement::FromNode(aContent); + if (!button || !button->IsMenu()) { + return false; + } + if (!button->GetPrimaryFrame()) { + // Hidden buttons are not focusable/activatable. + return false; + } + if (!button->IsDisabled()) { + return true; + } + // In the menubar or a menulist disabled items are always skipped. + const bool skipDisabled = + LookAndFeel::GetInt(LookAndFeel::IntID::SkipNavigatingDisabledMenuItem) || + aMenuParent.IsMenuBar() || aMenuParent.IsInMenuList(); + return !skipDisabled; +} + +enum class StartKind { Parent, Item }; + +template <bool aForward> +static XULButtonElement* DoGetNextMenuItem( + const XULMenuParentElement& aMenuParent, const nsIContent& aStart, + StartKind aStartKind) { + nsIContent* start = + aStartKind == StartKind::Item + ? (aForward ? aStart.GetNextSibling() : aStart.GetPreviousSibling()) + : (aForward ? aStart.GetFirstChild() : aStart.GetLastChild()); + for (nsIContent* node = start; node; + node = aForward ? node->GetNextSibling() : node->GetPreviousSibling()) { + if (IsValidMenuItem(aMenuParent, *node)) { + return static_cast<XULButtonElement*>(node); + } + if (node->IsXULElement(nsGkAtoms::menugroup)) { + if (XULButtonElement* child = DoGetNextMenuItem<aForward>( + aMenuParent, *node, StartKind::Parent)) { + return child; + } + } + } + if (aStartKind == StartKind::Item && aStart.GetParent() && + aStart.GetParent()->IsXULElement(nsGkAtoms::menugroup)) { + // We haven't found anything in aStart's sibling list, but if we're in a + // group we need to keep looking. + return DoGetNextMenuItem<aForward>(aMenuParent, *aStart.GetParent(), + StartKind::Item); + } + return nullptr; +} + +XULButtonElement* XULMenuParentElement::GetFirstMenuItem() const { + return DoGetNextMenuItem<true>(*this, *this, StartKind::Parent); +} + +XULButtonElement* XULMenuParentElement::GetLastMenuItem() const { + return DoGetNextMenuItem<false>(*this, *this, StartKind::Parent); +} + +XULButtonElement* XULMenuParentElement::GetNextMenuItemFrom( + const XULButtonElement& aStartingItem) const { + return DoGetNextMenuItem<true>(*this, aStartingItem, StartKind::Item); +} + +XULButtonElement* XULMenuParentElement::GetPrevMenuItemFrom( + const XULButtonElement& aStartingItem) const { + return DoGetNextMenuItem<false>(*this, aStartingItem, StartKind::Item); +} + +XULButtonElement* XULMenuParentElement::GetNextMenuItem(Wrap aWrap) const { + if (mActiveItem) { + if (auto* next = GetNextMenuItemFrom(*mActiveItem)) { + return next; + } + if (aWrap == Wrap::No) { + return nullptr; + } + } + return GetFirstMenuItem(); +} + +XULButtonElement* XULMenuParentElement::GetPrevMenuItem(Wrap aWrap) const { + if (mActiveItem) { + if (auto* prev = GetPrevMenuItemFrom(*mActiveItem)) { + return prev; + } + if (aWrap == Wrap::No) { + return nullptr; + } + } + return GetLastMenuItem(); +} + +void XULMenuParentElement::SelectFirstItem() { + if (RefPtr firstItem = GetFirstMenuItem()) { + SetActiveMenuChild(firstItem); + } +} + +XULButtonElement* XULMenuParentElement::FindMenuWithShortcut( + KeyboardEvent& aKeyEvent) const { + using AccessKeyArray = AutoTArray<uint32_t, 10>; + AccessKeyArray accessKeys; + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); + if (nativeKeyEvent) { + nativeKeyEvent->GetAccessKeyCandidates(accessKeys); + } + const uint32_t charCode = aKeyEvent.CharCode(); + if (accessKeys.IsEmpty() && charCode) { + accessKeys.AppendElement(charCode); + } + if (accessKeys.IsEmpty()) { + return nullptr; // no character was pressed so just return + } + XULButtonElement* foundMenu = nullptr; + size_t foundIndex = AccessKeyArray::NoIndex; + for (auto* item = GetFirstMenuItem(); item; + item = GetNextMenuItemFrom(*item)) { + nsAutoString shortcutKey; + item->GetAttr(nsGkAtoms::accesskey, shortcutKey); + if (shortcutKey.IsEmpty()) { + continue; + } + + ToLowerCase(shortcutKey); + const char16_t* start = shortcutKey.BeginReading(); + const char16_t* end = shortcutKey.EndReading(); + uint32_t ch = UTF16CharEnumerator::NextChar(&start, end); + size_t index = accessKeys.IndexOf(ch); + if (index == AccessKeyArray::NoIndex) { + continue; + } + if (foundIndex == AccessKeyArray::NoIndex || index < foundIndex) { + foundMenu = item; + foundIndex = index; + } + } + return foundMenu; +} + +XULButtonElement* XULMenuParentElement::FindMenuWithShortcut( + const nsAString& aString, bool& aDoAction) const { + aDoAction = false; + uint32_t accessKeyMatchCount = 0; + uint32_t matchCount = 0; + + XULButtonElement* foundAccessKeyMenu = nullptr; + XULButtonElement* foundMenuBeforeCurrent = nullptr; + XULButtonElement* foundMenuAfterCurrent = nullptr; + + bool foundActive = false; + for (auto* item = GetFirstMenuItem(); item; + item = GetNextMenuItemFrom(*item)) { + nsAutoString textKey; + // Get the shortcut attribute. + item->GetAttr(nsGkAtoms::accesskey, textKey); + const bool isAccessKey = !textKey.IsEmpty(); + if (textKey.IsEmpty()) { // No shortcut, try first letter + item->GetAttr(nsGkAtoms::label, textKey); + if (textKey.IsEmpty()) { // No label, try another attribute (value) + item->GetAttr(nsGkAtoms::value, textKey); + } + } + + const bool isActive = item == GetActiveMenuChild(); + foundActive |= isActive; + + if (!StringBeginsWith( + nsContentUtils::TrimWhitespace< + nsContentUtils::IsHTMLWhitespaceOrNBSP>(textKey, false), + aString, nsCaseInsensitiveStringComparator)) { + continue; + } + // There is one match + matchCount++; + if (isAccessKey) { + // There is one shortcut-key match + accessKeyMatchCount++; + // Record the matched item. If there is only one matched shortcut + // item, do it + foundAccessKeyMenu = item; + } + // Get the active status + if (isActive && aString.Length() > 1 && !foundMenuBeforeCurrent) { + // If there is more than one char typed and the current item matches, the + // current item has highest priority, otherwise the item next to current + // has highest priority. + return item; + } + if (!foundActive || isActive) { + // It's a first candidate item located before/on the current item + if (!foundMenuBeforeCurrent) { + foundMenuBeforeCurrent = item; + } + } else { + if (!foundMenuAfterCurrent) { + foundMenuAfterCurrent = item; + } + } + } + + aDoAction = !IsInMenuList() && (matchCount == 1 || accessKeyMatchCount == 1); + + if (accessKeyMatchCount == 1) { + // We have one matched accesskey item + return foundAccessKeyMenu; + } + // If we have matched an item after the current, use it. + if (foundMenuAfterCurrent) { + return foundMenuAfterCurrent; + } + // If we haven't, use the item before the current, if any. + return foundMenuBeforeCurrent; +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULMenuParentElement.h b/dom/xul/XULMenuParentElement.h new file mode 100644 index 0000000000..066fe541c8 --- /dev/null +++ b/dom/xul/XULMenuParentElement.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULMenuParentElement_h__ +#define XULMenuParentElement_h__ + +#include "mozilla/Attributes.h" +#include "nsISupports.h" +#include "nsXULElement.h" + +namespace mozilla::dom { + +class KeyboardEvent; +class XULButtonElement; + +nsXULElement* NS_NewXULMenuParentElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + +class XULMenuParentElement : public nsXULElement { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULMenuParentElement, nsXULElement) + + explicit XULMenuParentElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + bool IsMenuBar() const { return NodeInfo()->Equals(nsGkAtoms::menubar); } + bool IsMenu() const { return !IsMenuBar(); } + + bool IsLocked() const { return mLocked; } + + void LockMenuUntilClosed(bool aLock); + + bool IsInMenuList() const { + return GetParent() && GetParent()->IsXULElement(nsGkAtoms::menulist); + } + + XULButtonElement* FindMenuWithShortcut(KeyboardEvent&) const; + XULButtonElement* FindMenuWithShortcut(const nsAString& aString, + bool& aDoAction) const; + + NS_IMPL_FROMNODE_HELPER(XULMenuParentElement, + IsAnyOfXULElements(nsGkAtoms::menupopup, + nsGkAtoms::popup, nsGkAtoms::panel, + nsGkAtoms::tooltip, + nsGkAtoms::menubar)); + + XULButtonElement* GetActiveMenuChild() const { return mActiveItem.get(); } + enum class ByKey : bool { No, Yes }; + MOZ_CAN_RUN_SCRIPT void SetActiveMenuChild(XULButtonElement*, + ByKey = ByKey::No); + + XULButtonElement* GetFirstMenuItem() const; + XULButtonElement* GetLastMenuItem() const; + + XULButtonElement* GetNextMenuItemFrom(const XULButtonElement&) const; + XULButtonElement* GetPrevMenuItemFrom(const XULButtonElement&) const; + + enum class Wrap : bool { No, Yes }; + XULButtonElement* GetNextMenuItem(Wrap = Wrap::Yes) const; + XULButtonElement* GetPrevMenuItem(Wrap = Wrap::Yes) const; + + XULButtonElement* GetContainingMenu() const; + + MOZ_CAN_RUN_SCRIPT void SelectFirstItem(); + + protected: + RefPtr<XULButtonElement> mActiveItem; + bool mLocked = false; + + ~XULMenuParentElement() override; +}; + +} // namespace mozilla::dom + +#endif // XULMenuParentElement_h diff --git a/dom/xul/XULPersist.cpp b/dom/xul/XULPersist.cpp new file mode 100644 index 0000000000..3aa5586ff8 --- /dev/null +++ b/dom/xul/XULPersist.cpp @@ -0,0 +1,300 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */ + +#include "XULPersist.h" + +#ifdef MOZ_NEW_XULSTORE +# include "mozilla/XULStore.h" +#else +# include "nsIXULStore.h" +# include "nsIStringEnumerator.h" +# include "nsServiceManagerUtils.h" +#endif +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsContentUtils.h" +#include "nsIAppWindow.h" + +namespace mozilla::dom { + +static bool IsRootElement(Element* aElement) { + return aElement->OwnerDoc()->GetRootElement() == aElement; +} + +// FIXME: This is a hack to differentiate "attribute is missing" from "attribute +// is present but empty". Use a newline to avoid virtually all collisions. +// Ideally the XUL store would be able to store this more reasonably. +constexpr auto kMissingAttributeToken = u"-moz-missing\n"_ns; + +static bool ShouldPersistAttribute(Element* aElement, nsAtom* aAttribute) { + if (IsRootElement(aElement)) { + // This is not an element of the top document, its owner is + // not an AppWindow. Persist it. + if (aElement->OwnerDoc()->GetInProcessParentDocument()) { + return true; + } + // The following attributes of xul:window should be handled in + // AppWindow::SavePersistentAttributes instead of here. + if (aAttribute == nsGkAtoms::screenX || aAttribute == nsGkAtoms::screenY || + aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::sizemode) { + return false; + } + } + return true; +} + +NS_IMPL_ISUPPORTS(XULPersist, nsIDocumentObserver) + +XULPersist::XULPersist(Document* aDocument) + : nsStubDocumentObserver(), mDocument(aDocument) {} + +XULPersist::~XULPersist() = default; + +void XULPersist::Init() { + ApplyPersistentAttributes(); + mDocument->AddObserver(this); +} + +void XULPersist::DropDocumentReference() { + mDocument->RemoveObserver(this); + mDocument = nullptr; +} + +void XULPersist::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc"); + + if (aNameSpaceID != kNameSpaceID_None) { + return; + } + + // See if there is anything we need to persist in the localstore. + nsAutoString persist; + // Persistence of attributes of xul:window is handled in AppWindow. + if (aElement->GetAttr(nsGkAtoms::persist, persist) && + ShouldPersistAttribute(aElement, aAttribute) && + // XXXldb This should check that it's a token, not just a substring. + persist.Find(nsDependentAtomString(aAttribute)) >= 0) { + // Might not need this, but be safe for now. + nsCOMPtr<nsIDocumentObserver> kungFuDeathGrip(this); + nsContentUtils::AddScriptRunner(NewRunnableMethod<Element*, nsAtom*>( + "dom::XULPersist::Persist", this, &XULPersist::Persist, aElement, + aAttribute)); + } +} + +void XULPersist::Persist(Element* aElement, nsAtom* aAttribute) { + if (!mDocument) { + return; + } + // For non-chrome documents, persistance is simply broken + if (!mDocument->NodePrincipal()->IsSystemPrincipal()) { + return; + } + +#ifndef MOZ_NEW_XULSTORE + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return; + } + } +#endif + + nsAutoString id; + + aElement->GetAttr(nsGkAtoms::id, id); + nsAtomString attrstr(aAttribute); + + nsAutoCString utf8uri; + nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Persisting attributes to top level windows is handled by AppWindow. + if (IsRootElement(aElement)) { + if (nsCOMPtr<nsIAppWindow> win = + mDocument->GetAppWindowIfToplevelChrome()) { + return; + } + } + + NS_ConvertUTF8toUTF16 uri(utf8uri); + nsAutoString valuestr; + if (!aElement->GetAttr(aAttribute, valuestr)) { + valuestr = kMissingAttributeToken; + } + +#ifdef MOZ_NEW_XULSTORE + rv = XULStore::SetValue(uri, id, attrstr, valuestr); +#else + mLocalStore->SetValue(uri, id, attrstr, valuestr); +#endif + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value set"); +} + +nsresult XULPersist::ApplyPersistentAttributes() { + if (!mDocument) { + return NS_ERROR_NOT_AVAILABLE; + } + // For non-chrome documents, persistance is simply broken + if (!mDocument->NodePrincipal()->IsSystemPrincipal()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Add all of the 'persisted' attributes into the content + // model. +#ifndef MOZ_NEW_XULSTORE + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return NS_ERROR_NOT_INITIALIZED; + } + } +#endif + + nsCOMArray<Element> elements; + + nsAutoCString utf8uri; + nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); + + // Get a list of element IDs for which persisted values are available +#ifdef MOZ_NEW_XULSTORE + UniquePtr<XULStoreIterator> ids; + rv = XULStore::GetIDs(uri, ids); +#else + nsCOMPtr<nsIStringEnumerator> ids; + rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids)); +#endif + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef MOZ_NEW_XULSTORE + while (ids->HasMore()) { + nsAutoString id; + rv = ids->GetNext(&id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#else + while (1) { + bool hasmore = false; + ids->HasMore(&hasmore); + if (!hasmore) { + break; + } + + nsAutoString id; + ids->GetNext(id); +#endif + + // We want to hold strong refs to the elements while applying + // persistent attributes, just in case. + const nsTArray<Element*>* allElements = mDocument->GetAllElementsForId(id); + if (!allElements) { + continue; + } + elements.Clear(); + elements.SetCapacity(allElements->Length()); + for (Element* element : *allElements) { + elements.AppendObject(element); + } + + rv = ApplyPersistentAttributesToElements(id, uri, elements); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult XULPersist::ApplyPersistentAttributesToElements( + const nsAString& aID, const nsAString& aDocURI, + nsCOMArray<Element>& aElements) { + nsresult rv = NS_OK; + // Get a list of attributes for which persisted values are available +#ifdef MOZ_NEW_XULSTORE + UniquePtr<XULStoreIterator> attrs; + rv = XULStore::GetAttrs(aDocURI, aID, attrs); +#else + nsCOMPtr<nsIStringEnumerator> attrs; + rv = mLocalStore->GetAttributeEnumerator(aDocURI, aID, getter_AddRefs(attrs)); +#endif + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef MOZ_NEW_XULSTORE + while (attrs->HasMore()) { + nsAutoString attrstr; + rv = attrs->GetNext(&attrstr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString value; + rv = XULStore::GetValue(aDocURI, aID, attrstr, value); +#else + while (1) { + bool hasmore = PR_FALSE; + attrs->HasMore(&hasmore); + if (!hasmore) { + break; + } + + nsAutoString attrstr; + attrs->GetNext(attrstr); + + nsAutoString value; + rv = mLocalStore->GetValue(aDocURI, aID, attrstr, value); +#endif + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<nsAtom> attr = NS_Atomize(attrstr); + if (NS_WARN_IF(!attr)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t cnt = aElements.Length(); + for (int32_t i = int32_t(cnt) - 1; i >= 0; --i) { + Element* element = aElements.SafeElementAt(i); + if (!element) { + continue; + } + + // Applying persistent attributes to top level windows is handled + // by AppWindow. + if (IsRootElement(element)) { + if (nsCOMPtr<nsIAppWindow> win = + mDocument->GetAppWindowIfToplevelChrome()) { + continue; + } + } + + if (value == kMissingAttributeToken) { + Unused << element->UnsetAttr(kNameSpaceID_None, attr, true); + } else { + Unused << element->SetAttr(kNameSpaceID_None, attr, value, true); + } + } + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULPersist.h b/dom/xul/XULPersist.h new file mode 100644 index 0000000000..f389813cb7 --- /dev/null +++ b/dom/xul/XULPersist.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef mozilla_dom_XULPersist_h +#define mozilla_dom_XULPersist_h + +#include "nsStubDocumentObserver.h" + +#ifndef MOZ_NEW_XULSTORE +# include "nsCOMPtr.h" +class nsIXULStore; +#endif + +template <typename T> +class nsCOMArray; + +namespace mozilla::dom { + +class XULPersist final : public nsStubDocumentObserver { + public: + NS_DECL_ISUPPORTS + + explicit XULPersist(Document* aDocument); + void Init(); + void DropDocumentReference(); + + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + protected: + void Persist(mozilla::dom::Element* aElement, nsAtom* aAttribute); + + private: + ~XULPersist(); + nsresult ApplyPersistentAttributes(); + nsresult ApplyPersistentAttributesToElements(const nsAString& aID, + const nsAString& aDocURI, + nsCOMArray<Element>& aElements); + +#ifndef MOZ_NEW_XULSTORE + nsCOMPtr<nsIXULStore> mLocalStore; +#endif + + // A weak pointer to our document. Nulled out by DropDocumentReference. + Document* MOZ_NON_OWNING_REF mDocument; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_XULPersist_h diff --git a/dom/xul/XULPopupElement.cpp b/dom/xul/XULPopupElement.cpp new file mode 100644 index 0000000000..22d86ed3ff --- /dev/null +++ b/dom/xul/XULPopupElement.cpp @@ -0,0 +1,338 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "XULMenuParentElement.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsMenuBarListener.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsMenuPopupFrame.h" +#include "nsView.h" +#include "mozilla/AppUnits.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/XULPopupElement.h" +#include "mozilla/dom/XULButtonElement.h" +#include "mozilla/dom/XULMenuElement.h" +#include "mozilla/dom/XULPopupElementBinding.h" +#ifdef MOZ_WAYLAND +# include "mozilla/WidgetUtilsGtk.h" +#endif + +namespace mozilla::dom { + +nsXULElement* NS_NewXULPopupElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULPopupElement(nodeInfo.forget()); +} + +JSObject* XULPopupElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULPopupElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsMenuPopupFrame* XULPopupElement::GetFrame(FlushType aFlushType) { + nsIFrame* f = GetPrimaryFrame(aFlushType); + MOZ_ASSERT(!f || f->IsMenuPopupFrame()); + return static_cast<nsMenuPopupFrame*>(f); +} + +void XULPopupElement::OpenPopup(Element* aAnchorElement, + const StringOrOpenPopupOptions& aOptions, + int32_t aXPos, int32_t aYPos, + bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent) { + nsAutoString position; + if (aOptions.IsOpenPopupOptions()) { + const OpenPopupOptions& options = aOptions.GetAsOpenPopupOptions(); + position = options.mPosition; + aXPos = options.mX; + aYPos = options.mY; + aIsContextMenu = options.mIsContextMenu; + aAttributesOverride = options.mAttributesOverride; + aTriggerEvent = options.mTriggerEvent; + } else { + position = aOptions.GetAsString(); + } + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + // As a special case for popups that are menus when no anchor or position + // are specified, open the popup with ShowMenu instead of ShowPopup so that + // the popup is aligned with the menu. + if (!aAnchorElement && position.IsEmpty() && GetPrimaryFrame()) { + if (auto* menu = GetContainingMenu()) { + pm->ShowMenu(menu, false); + return; + } + } + + pm->ShowPopup(this, aAnchorElement, position, aXPos, aYPos, aIsContextMenu, + aAttributesOverride, false, aTriggerEvent); + } +} + +void XULPopupElement::OpenPopupAtScreen(int32_t aXPos, int32_t aYPos, + bool aIsContextMenu, + Event* aTriggerEvent) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->ShowPopupAtScreen(this, aXPos, aYPos, aIsContextMenu, aTriggerEvent); + } +} + +void XULPopupElement::OpenPopupAtScreenRect(const nsAString& aPosition, + int32_t aXPos, int32_t aYPos, + int32_t aWidth, int32_t aHeight, + bool aIsContextMenu, + bool aAttributesOverride, + Event* aTriggerEvent) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->ShowPopupAtScreenRect( + this, aPosition, nsIntRect(aXPos, aYPos, aWidth, aHeight), + aIsContextMenu, aAttributesOverride, aTriggerEvent); + } +} + +void XULPopupElement::HidePopup(bool aCancel) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + pm->HidePopup(this, false, true, false, aCancel); + } +} + +static Modifiers ConvertModifiers(const ActivateMenuItemOptions& aModifiers) { + Modifiers modifiers = 0; + if (aModifiers.mCtrlKey) { + modifiers |= MODIFIER_CONTROL; + } + if (aModifiers.mAltKey) { + modifiers |= MODIFIER_ALT; + } + if (aModifiers.mShiftKey) { + modifiers |= MODIFIER_SHIFT; + } + if (aModifiers.mMetaKey) { + modifiers |= MODIFIER_META; + } + return modifiers; +} + +void XULPopupElement::PopupOpened(bool aSelectFirstItem) { + if (aSelectFirstItem) { + SelectFirstItem(); + } + if (RefPtr button = GetContainingMenu()) { + if (RefPtr parent = button->GetMenuParent()) { + parent->SetActiveMenuChild(button); + } + } +} + +void XULPopupElement::PopupClosed(bool aDeselectMenu) { + LockMenuUntilClosed(false); + SetActiveMenuChild(nullptr); + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + this, u"DOMMenuInactive"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + dispatcher->PostDOMEvent(); + if (RefPtr button = GetContainingMenu()) { + button->PopupClosed(aDeselectMenu); + } +} + +void XULPopupElement::ActivateItem(Element& aItemElement, + const ActivateMenuItemOptions& aOptions, + ErrorResult& aRv) { + if (!Contains(&aItemElement)) { + return aRv.ThrowInvalidStateError("Menu item is not inside this menu."); + } + + Modifiers modifiers = ConvertModifiers(aOptions); + + // First, check if a native menu is open, and activate the item in it. + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + if (pm->ActivateNativeMenuItem(&aItemElement, modifiers, aOptions.mButton, + aRv)) { + return; + } + } + + auto* item = XULButtonElement::FromNode(aItemElement); + if (!item || !item->IsMenu()) { + return aRv.ThrowInvalidStateError("Not a menu item"); + } + + if (!item->GetPrimaryFrame(FlushType::Frames)) { + return aRv.ThrowInvalidStateError("Menu item is hidden"); + } + + auto* popup = item->GetContainingPopupElement(); + if (!popup) { + return aRv.ThrowInvalidStateError("No popup"); + } + + nsMenuPopupFrame* frame = popup->GetFrame(FlushType::None); + if (!frame || !frame->IsOpen()) { + return aRv.ThrowInvalidStateError("Popup is not open"); + } + + // This is a chrome-only API, so we're trusted. + const bool trusted = true; + // KnownLive because item is aItemElement. + MOZ_KnownLive(item)->ExecuteMenu(modifiers, aOptions.mButton, trusted); +} + +void XULPopupElement::MoveTo(int32_t aLeft, int32_t aTop) { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + if (menuPopupFrame) { + menuPopupFrame->MoveTo(CSSIntPoint(aLeft, aTop), true); + } +} + +void XULPopupElement::MoveToAnchor(Element* aAnchorElement, + const nsAString& aPosition, int32_t aXPos, + int32_t aYPos, bool aAttributesOverride) { + nsMenuPopupFrame* menuPopupFrame = GetFrame(FlushType::None); + if (menuPopupFrame && menuPopupFrame->IsVisibleOrShowing()) { + menuPopupFrame->MoveToAnchor(aAnchorElement, aPosition, aXPos, aYPos, + aAttributesOverride); + } +} + +void XULPopupElement::SizeTo(int32_t aWidth, int32_t aHeight) { + nsAutoString width, height; + width.AppendInt(aWidth); + height.AppendInt(aHeight); + + nsCOMPtr<nsIContent> kungFuDeathGrip = this; // keep a reference + + // We only want to pass aNotify=true to SetAttr once, but must make sure + // we pass it when a value is being changed. Thus, we check if the height + // is the same and if so, pass true when setting the width. + bool heightSame = + AttrValueIs(kNameSpaceID_None, nsGkAtoms::height, height, eCaseMatters); + + SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, heightSame); + SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true); + + // If the popup is open, force a reposition of the popup after resizing it + // with notifications set to true so that the popuppositioned event is fired. + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + if (menuPopupFrame && menuPopupFrame->PopupState() == ePopupShown) { + menuPopupFrame->SetPopupPosition(false); + } +} + +void XULPopupElement::GetState(nsString& aState) { + // set this here in case there's no frame for the popup + aState.AssignLiteral("closed"); + + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + switch (pm->GetPopupState(this)) { + case ePopupShown: + aState.AssignLiteral("open"); + break; + case ePopupShowing: + case ePopupPositioning: + case ePopupOpening: + case ePopupVisible: + aState.AssignLiteral("showing"); + break; + case ePopupHiding: + case ePopupInvisible: + aState.AssignLiteral("hiding"); + break; + case ePopupClosed: + break; + default: + MOZ_ASSERT_UNREACHABLE("Bad popup state"); + break; + } + } +} + +nsINode* XULPopupElement::GetTriggerNode() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + return nsMenuPopupFrame::GetTriggerContent(menuPopupFrame); +} + +// FIXME(emilio): should probably be renamed to GetAnchorElement? +Element* XULPopupElement::GetAnchorNode() const { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + if (!menuPopupFrame) { + return nullptr; + } + return Element::FromNodeOrNull(menuPopupFrame->GetAnchor()); +} + +already_AddRefed<DOMRect> XULPopupElement::GetOuterScreenRect() { + RefPtr<DOMRect> rect = new DOMRect(ToSupports(OwnerDoc())); + + // Return an empty rectangle if the popup is not open. + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(GetPrimaryFrame(FlushType::Frames)); + if (!menuPopupFrame || !menuPopupFrame->IsOpen()) { + return rect.forget(); + } + + Maybe<CSSRect> screenRect; + + if (menuPopupFrame->IsNativeMenu()) { + // For native menus we can't query the true size. Use the anchor rect + // instead, which at least has the position at which we were intending to + // open the menu. + screenRect = Some(CSSRect(menuPopupFrame->GetScreenAnchorRect())); + } else { + // For non-native menus, query the bounds from the widget. + if (nsView* view = menuPopupFrame->GetView()) { + if (nsIWidget* widget = view->GetWidget()) { + screenRect = Some(widget->GetScreenBounds() / + menuPopupFrame->PresContext()->CSSToDevPixelScale()); + } + } + } + + if (screenRect) { + rect->SetRect(screenRect->X(), screenRect->Y(), screenRect->Width(), + screenRect->Height()); + } + return rect.forget(); +} + +void XULPopupElement::SetConstraintRect(dom::DOMRectReadOnly& aRect) { + nsMenuPopupFrame* menuPopupFrame = + do_QueryFrame(GetPrimaryFrame(FlushType::Frames)); + if (menuPopupFrame) { + menuPopupFrame->SetOverrideConstraintRect(LayoutDeviceIntRect::Truncate( + aRect.Left(), aRect.Top(), aRect.Width(), aRect.Height())); + } +} + +bool XULPopupElement::IsWaylandDragSource() const { +#ifdef MOZ_WAYLAND + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetPrimaryFrame()); + return menuPopupFrame->IsDragSource(); +#else + return false; +#endif +} + +bool XULPopupElement::IsWaylandPopup() const { +#ifdef MOZ_WAYLAND + return widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol(); +#else + return false; +#endif +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULPopupElement.h b/dom/xul/XULPopupElement.h new file mode 100644 index 0000000000..fd9327616d --- /dev/null +++ b/dom/xul/XULPopupElement.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULPopupElement_h__ +#define XULPopupElement_h__ + +#include "XULMenuParentElement.h" +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsINode.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" + +class nsMenuPopupFrame; +struct JSContext; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class DOMRect; +class Element; +class Event; +class StringOrOpenPopupOptions; +struct ActivateMenuItemOptions; + +nsXULElement* NS_NewXULPopupElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + +class XULPopupElement : public XULMenuParentElement { + private: + nsMenuPopupFrame* GetFrame(FlushType); + + public: + explicit XULPopupElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : XULMenuParentElement(std::move(aNodeInfo)) {} + + void GetLabel(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::label, aValue); + } + void SetLabel(const nsAString& aValue, ErrorResult& rv) { + SetXULAttr(nsGkAtoms::label, aValue, rv); + } + + void GetPosition(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::position, aValue); + } + void SetPosition(const nsAString& aValue, ErrorResult& rv) { + SetXULAttr(nsGkAtoms::position, aValue, rv); + } + + void OpenPopup(Element* aAnchorElement, + const StringOrOpenPopupOptions& aOptions, int32_t aXPos, + int32_t aYPos, bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent); + + void OpenPopupAtScreen(int32_t aXPos, int32_t aYPos, bool aIsContextMenu, + Event* aTriggerEvent); + + void OpenPopupAtScreenRect(const nsAString& aPosition, int32_t aXPos, + int32_t aYPos, int32_t aWidth, int32_t aHeight, + bool aIsContextMenu, bool aAttributesOverride, + Event* aTriggerEvent); + + void HidePopup(bool aCancel); + + MOZ_CAN_RUN_SCRIPT void ActivateItem(Element& aItemElement, + const ActivateMenuItemOptions& aOptions, + ErrorResult& aRv); + + void GetState(nsString& aState); + + MOZ_CAN_RUN_SCRIPT void PopupOpened(bool aSelectFirstItem); + MOZ_CAN_RUN_SCRIPT void PopupClosed(bool aDeselectMenu); + + nsINode* GetTriggerNode() const; + + Element* GetAnchorNode() const; + + already_AddRefed<DOMRect> GetOuterScreenRect(); + + void MoveTo(int32_t aLeft, int32_t aTop); + + void MoveToAnchor(Element* aAnchorElement, const nsAString& aPosition, + int32_t aXPos, int32_t aYPos, bool aAttributesOverride); + + void SizeTo(int32_t aWidth, int32_t aHeight); + + void SetConstraintRect(DOMRectReadOnly& aRect); + + bool IsWaylandDragSource() const; + bool IsWaylandPopup() const; + + NS_IMPL_FROMNODE_HELPER(XULPopupElement, + IsAnyOfXULElements(nsGkAtoms::menupopup, + nsGkAtoms::popup, nsGkAtoms::panel, + nsGkAtoms::tooltip)); + + protected: + virtual ~XULPopupElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // XULPopupElement_h diff --git a/dom/xul/XULResizerElement.cpp b/dom/xul/XULResizerElement.cpp new file mode 100644 index 0000000000..9b2c6e534e --- /dev/null +++ b/dom/xul/XULResizerElement.cpp @@ -0,0 +1,354 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "mozilla/dom/XULResizerElement.h" +#include "mozilla/dom/XULResizerElementBinding.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/MouseEvents.h" +#include "nsContentUtils.h" +#include "nsICSSDeclaration.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsStyledElement.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULResizerElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULResizerElement(nodeInfo.forget()); +} + +static bool GetEventPoint(const WidgetGUIEvent* aEvent, + LayoutDeviceIntPoint& aPoint) { + NS_ENSURE_TRUE(aEvent, false); + + const WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + if (touchEvent) { + // return false if there is more than one touch on the page, or if + // we can't find a touch point + if (touchEvent->mTouches.Length() != 1) { + return false; + } + + const dom::Touch* touch = touchEvent->mTouches.SafeElementAt(0); + if (!touch) { + return false; + } + aPoint = touch->mRefPoint; + } else { + aPoint = aEvent->mRefPoint; + } + return true; +} + +JSObject* XULResizerElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULResizerElement_Binding::Wrap(aCx, this, aGivenProto); +} + +XULResizerElement::Direction XULResizerElement::GetDirection() { + static const mozilla::dom::Element::AttrValuesArray strings[] = { + // clang-format off + nsGkAtoms::topleft, nsGkAtoms::top, nsGkAtoms::topright, + nsGkAtoms::left, nsGkAtoms::right, + nsGkAtoms::bottomleft, nsGkAtoms::bottom, nsGkAtoms::bottomright, + nsGkAtoms::bottomstart, nsGkAtoms::bottomend, + nullptr + // clang-format on + }; + + static const Direction directions[] = { + // clang-format off + {-1, -1}, {0, -1}, {1, -1}, + {-1, 0}, {1, 0}, + {-1, 1}, {0, 1}, {1, 1}, + {-1, 1}, {1, 1} + // clang-format on + }; + + const auto* frame = GetPrimaryFrame(); + if (!frame) { + return directions[0]; // default: topleft + } + + int32_t index = + FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters); + if (index < 0) { + return directions[0]; // default: topleft + } + + if (index >= 8) { + // Directions 8 and higher are RTL-aware directions and should reverse the + // horizontal component if RTL. + auto wm = frame->GetWritingMode(); + if (wm.IsPhysicalRTL()) { + Direction direction = directions[index]; + direction.mHorizontal *= -1; + return direction; + } + } + + return directions[index]; +} + +nsresult XULResizerElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + PostHandleEventInternal(aVisitor); + } + return nsXULElement::PostHandleEvent(aVisitor); +} + +Maybe<nsSize> XULResizerElement::GetCurrentSize() const { + nsIContent* contentToResize = GetContentToResize(); + if (!contentToResize) { + return Nothing(); + } + nsIFrame* frame = contentToResize->GetPrimaryFrame(); + if (!frame) { + return Nothing(); + } + return Some(frame->StylePosition()->mBoxSizing == StyleBoxSizing::Content + ? frame->GetContentRect().Size() + : frame->GetRect().Size()); +} + +void XULResizerElement::PostHandleEventInternal( + EventChainPostVisitor& aVisitor) { + bool doDefault = true; + const WidgetEvent& event = *aVisitor.mEvent; + switch (event.mMessage) { + case eTouchStart: + case eMouseDown: { + if (event.mClass == eTouchEventClass || + (event.mClass == eMouseEventClass && + event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { + auto size = GetCurrentSize(); + if (!size) { + break; // don't do anything if there's nothing to resize + } + // cache the content rectangle for the frame to resize + mMouseDownSize = *size; + + // remember current mouse coordinates + auto* guiEvent = event.AsGUIEvent(); + if (!GetEventPoint(guiEvent, mMouseDownPoint)) { + break; + } + mTrackingMouseMove = true; + PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState); + doDefault = false; + } + } break; + + case eTouchMove: + case eMouseMove: { + if (mTrackingMouseMove) { + nsCOMPtr<nsIContent> contentToResize = GetContentToResize(); + if (!contentToResize) { + break; // don't do anything if there's nothing to resize + } + nsIFrame* frame = contentToResize->GetPrimaryFrame(); + if (!frame) { + break; + } + + // both MouseMove and direction are negative when pointing to the + // top and left, and positive when pointing to the bottom and right + + // retrieve the offset of the mousemove event relative to the mousedown. + // The difference is how much the resize needs to be + LayoutDeviceIntPoint refPoint; + auto* guiEvent = event.AsGUIEvent(); + if (!GetEventPoint(guiEvent, refPoint)) { + break; + } + + const nsPoint oldPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( + guiEvent->mWidget, mMouseDownPoint, RelativeTo{frame}); + const nsPoint newPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( + guiEvent->mWidget, refPoint, RelativeTo{frame}); + + nsPoint mouseMove(newPos - oldPos); + + // Determine which direction to resize by checking the dir attribute. + // For windows and menus, ensure that it can be resized in that + // direction. + Direction direction = GetDirection(); + + const CSSIntSize newSize = [&] { + nsSize newAuSize = mMouseDownSize; + // Check if there are any size constraints on this window. + newAuSize.width += direction.mHorizontal * mouseMove.x; + newAuSize.height += direction.mVertical * mouseMove.y; + if (newAuSize.width < AppUnitsPerCSSPixel() && mouseMove.x != 0) { + newAuSize.width = AppUnitsPerCSSPixel(); + } + if (newAuSize.height < AppUnitsPerCSSPixel() && mouseMove.y != 0) { + newAuSize.height = AppUnitsPerCSSPixel(); + } + + // When changing the size in a direction, don't allow the new size to + // be less that the resizer's size. This ensures that content isn't + // resized too small as to make the resizer invisible. + if (auto* resizerFrame = GetPrimaryFrame()) { + nsRect resizerRect = resizerFrame->GetRect(); + if (newAuSize.width < resizerRect.width && mouseMove.x != 0) { + newAuSize.width = resizerRect.width; + } + if (newAuSize.height < resizerRect.height && mouseMove.y != 0) { + newAuSize.height = resizerRect.height; + } + } + + // Convert the rectangle into css pixels. + return CSSIntSize::FromAppUnitsRounded(newAuSize); + }(); + + // Only resize in a given direction if the new size doesn't match the + // current size. + if (auto currentSize = GetCurrentSize()) { + auto newAuSize = CSSIntSize::ToAppUnits(newSize); + if (newAuSize.width == currentSize->width) { + direction.mHorizontal = 0; + } + if (newAuSize.height == currentSize->height) { + direction.mVertical = 0; + } + } + + SizeInfo sizeInfo, originalSizeInfo; + sizeInfo.width.AppendInt(newSize.width); + sizeInfo.height.AppendInt(newSize.height); + ResizeContent(contentToResize, direction, sizeInfo, &originalSizeInfo); + MaybePersistOriginalSize(contentToResize, originalSizeInfo); + + doDefault = false; + } + } break; + + case eMouseClick: { + auto* mouseEvent = event.AsMouseEvent(); + if (mouseEvent->IsLeftClickEvent()) { + // Execute the oncommand event handler. + nsContentUtils::DispatchXULCommand( + this, false, nullptr, nullptr, mouseEvent->IsControl(), + mouseEvent->IsAlt(), mouseEvent->IsShift(), mouseEvent->IsMeta(), + mouseEvent->mInputSource, mouseEvent->mButton); + } + } break; + + case eTouchEnd: + case eMouseUp: { + if (event.mClass == eTouchEventClass || + (event.mClass == eMouseEventClass && + event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { + mTrackingMouseMove = false; + PresShell::ReleaseCapturingContent(); + doDefault = false; + } + } break; + + case eMouseDoubleClick: { + if (event.AsMouseEvent()->mButton == MouseButton::ePrimary) { + if (nsIContent* contentToResize = GetContentToResize()) { + RestoreOriginalSize(contentToResize); + } + } + } break; + + default: + break; + } + + if (!doDefault) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } +} + +nsIContent* XULResizerElement::GetContentToResize() const { + if (!IsInComposedDoc()) { + return nullptr; + } + // Return the parent, but skip over native anonymous content + nsIContent* parent = GetParent(); + return parent ? parent->FindFirstNonChromeOnlyAccessContent() : nullptr; +} + +/* static */ +void XULResizerElement::ResizeContent(nsIContent* aContent, + const Direction& aDirection, + const SizeInfo& aSizeInfo, + SizeInfo* aOriginalSizeInfo) { + RefPtr inlineStyleContent = nsStyledElement::FromNode(aContent); + if (!inlineStyleContent) { + return; + } + nsCOMPtr<nsICSSDeclaration> decl = inlineStyleContent->Style(); + if (aOriginalSizeInfo) { + decl->GetPropertyValue("width"_ns, aOriginalSizeInfo->width); + decl->GetPropertyValue("height"_ns, aOriginalSizeInfo->height); + } + + // only set the property if the element could have changed in that + // direction + if (aDirection.mHorizontal) { + nsAutoCString widthstr(aSizeInfo.width); + if (!widthstr.IsEmpty() && !StringEndsWith(widthstr, "px"_ns)) { + widthstr.AppendLiteral("px"); + } + decl->SetProperty("width"_ns, widthstr, ""_ns, IgnoreErrors()); + } + + if (aDirection.mVertical) { + nsAutoCString heightstr(aSizeInfo.height); + if (!heightstr.IsEmpty() && !StringEndsWith(heightstr, "px"_ns)) { + heightstr.AppendLiteral("px"); + } + decl->SetProperty("height"_ns, heightstr, ""_ns, IgnoreErrors()); + } +} + +/* static */ +void XULResizerElement::MaybePersistOriginalSize(nsIContent* aContent, + const SizeInfo& aSizeInfo) { + nsresult rv; + aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv); + if (rv != NS_PROPTABLE_PROP_NOT_THERE) { + return; + } + + UniquePtr<SizeInfo> sizeInfo(new SizeInfo(aSizeInfo)); + rv = aContent->SetProperty( + nsGkAtoms::_moz_original_size, sizeInfo.get(), + nsINode::DeleteProperty<XULResizerElement::SizeInfo>); + if (NS_SUCCEEDED(rv)) { + Unused << sizeInfo.release(); + } +} + +/* static */ +void XULResizerElement::RestoreOriginalSize(nsIContent* aContent) { + nsresult rv; + SizeInfo* sizeInfo = static_cast<SizeInfo*>( + aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv)); + if (NS_FAILED(rv)) { + return; + } + + NS_ASSERTION(sizeInfo, "We set a null sizeInfo!?"); + Direction direction = {1, 1}; + ResizeContent(aContent, direction, *sizeInfo, nullptr); + aContent->RemoveProperty(nsGkAtoms::_moz_original_size); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULResizerElement.h b/dom/xul/XULResizerElement.h new file mode 100644 index 0000000000..f7c9e4d75d --- /dev/null +++ b/dom/xul/XULResizerElement.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef mozilla_dom_XULResizerElement_h +#define mozilla_dom_XULResizerElement_h + +#include "nsXULElement.h" +#include "Units.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULResizerElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + +class XULResizerElement final : public nsXULElement { + public: + explicit XULResizerElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEvent(EventChainPostVisitor&) override; + + private: + virtual ~XULResizerElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; + + MOZ_CAN_RUN_SCRIPT + void PostHandleEventInternal(EventChainPostVisitor&); + + struct Direction { + int8_t mHorizontal; + int8_t mVertical; + }; + Direction GetDirection(); + + nsIContent* GetContentToResize() const; + // The current size of the content to resize (if available). + Maybe<nsSize> GetCurrentSize() const; + + struct SizeInfo { + nsCString width, height; + }; + static void SizeInfoDtorFunc(void* aObject, nsAtom* aPropertyName, + void* aPropertyValue, void* aData); + static void ResizeContent(nsIContent* aContent, const Direction& aDirection, + const SizeInfo& aSizeInfo, + SizeInfo* aOriginalSizeInfo); + static void MaybePersistOriginalSize(nsIContent* aContent, + const SizeInfo& aSizeInfo); + static void RestoreOriginalSize(nsIContent* aContent); + + nsSize mMouseDownSize; + LayoutDeviceIntPoint mMouseDownPoint; + bool mTrackingMouseMove = false; +}; + +} // namespace mozilla::dom + +#endif // XULResizerElement_h diff --git a/dom/xul/XULTextElement.cpp b/dom/xul/XULTextElement.cpp new file mode 100644 index 0000000000..04edfabc09 --- /dev/null +++ b/dom/xul/XULTextElement.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "mozilla/dom/Element.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsCOMPtr.h" +#include "nsChangeHint.h" +#include "nsIContent.h" +#include "nsPresContext.h" +#include "nsIScrollableFrame.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/XULTextElement.h" +#include "mozilla/dom/XULTextElementBinding.h" + +namespace mozilla::dom { + +nsChangeHint XULTextElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + const bool reframe = [&] { + if (aAttribute == nsGkAtoms::value) { + // If we have an accesskey we need to recompute our -moz-label-content. + // Otherwise this is handled by either the attribute text node, or + // nsTextBoxFrame for crop="center". + return aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL || + HasAttr(nsGkAtoms::accesskey); + } + if (aAttribute == nsGkAtoms::crop || aAttribute == nsGkAtoms::accesskey) { + // value attr + crop="center" still uses nsTextBoxFrame. accesskey + // requires reframing as per the above comment. + return HasAttr(nsGkAtoms::value); + } + return false; + }(); + if (reframe) { + return nsChangeHint_ReconstructFrame; + } + return nsXULElement::GetAttributeChangeHint(aAttribute, aModType); +} + +JSObject* XULTextElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULTextElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTextElement.h b/dom/xul/XULTextElement.h new file mode 100644 index 0000000000..0c6f086b5e --- /dev/null +++ b/dom/xul/XULTextElement.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULTextElement_h__ +#define XULTextElement_h__ + +#include "nsXULElement.h" + +namespace mozilla::dom { + +class XULTextElement final : public nsXULElement { + public: + explicit XULTextElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)) {} + + bool Disabled() { return GetXULBoolAttr(nsGkAtoms::disabled); } + MOZ_CAN_RUN_SCRIPT void SetDisabled(bool aValue) { + SetXULBoolAttr(nsGkAtoms::disabled, aValue, mozilla::IgnoreErrors()); + } + void GetValue(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::value, aValue); + } + MOZ_CAN_RUN_SCRIPT void SetValue(const nsAString& aValue) { + SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true); + } + void GetAccessKey(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::accesskey, aValue); + } + MOZ_CAN_RUN_SCRIPT void SetAccessKey(const nsAString& aValue) { + SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, aValue, true); + } + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + + NS_IMPL_FROMNODE_HELPER(XULTextElement, + IsAnyOfXULElements(nsGkAtoms::label, + nsGkAtoms::description)); + + protected: + virtual ~XULTextElement() = default; + JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; +}; + +} // namespace mozilla::dom + +#endif // XULTextElement_h diff --git a/dom/xul/XULTooltipElement.cpp b/dom/xul/XULTooltipElement.cpp new file mode 100644 index 0000000000..f18ab92304 --- /dev/null +++ b/dom/xul/XULTooltipElement.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/XULTooltipElement.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/EventDispatcher.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCTooltipTextProvider.h" +#include "nsITooltipTextProvider.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULTooltipElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + RefPtr<XULTooltipElement> tooltip = + new (nim) XULTooltipElement(nodeInfo.forget()); + NS_ENSURE_SUCCESS(tooltip->Init(), nullptr); + return tooltip; +} + +nsresult XULTooltipElement::Init() { + // Create the default child label node that will contain the text of the + // tooltip. + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfo->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::description, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE); + nsCOMPtr<Element> description; + nsresult rv = NS_NewXULElement(getter_AddRefs(description), nodeInfo.forget(), + dom::NOT_FROM_PARSER); + NS_ENSURE_SUCCESS(rv, rv); + description->SetAttr(kNameSpaceID_None, nsGkAtoms::_class, + u"tooltip-label"_ns, false); + ErrorResult error; + AppendChild(*description, error); + + return error.StealNSResult(); +} + +nsresult XULTooltipElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::label) { + // When the label attribute of this node changes propagate the text down + // into child description element. + nsCOMPtr<nsIContent> description = GetFirstChild(); + if (description && description->IsXULElement(nsGkAtoms::description)) { + nsAutoString value; + if (aValue) { + aValue->ToString(value); + } + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "XULTooltipElement::AfterSetAttr", [description, value]() { + Element* descriptionElement = description->AsElement(); + descriptionElement->SetTextContent(value, IgnoreErrors()); + })); + } + } + return nsXULElement::AfterSetAttr(aNameSpaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +nsresult XULTooltipElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eXULPopupShowing && + aVisitor.mEvent->IsTrusted() && !aVisitor.mEvent->DefaultPrevented() && + AttrValueIs(kNameSpaceID_None, nsGkAtoms::page, nsGkAtoms::_true, + eCaseMatters) && + !AttrValueIs(kNameSpaceID_None, nsGkAtoms::titletip, nsGkAtoms::_true, + eCaseMatters)) { + // When the tooltip node has the "page" attribute set to "true" the + // tooltip text provider is used to find the tooltip text from page where + // mouse is hovering over. + nsCOMPtr<nsITooltipTextProvider> textProvider = + do_GetService(NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID); + nsString text; + nsString direction; + bool shouldChange = false; + if (textProvider) { + textProvider->GetNodeText(GetTriggerNode(), getter_Copies(text), + getter_Copies(direction), &shouldChange); + } + if (shouldChange) { + SetAttr(kNameSpaceID_None, nsGkAtoms::label, text, true); + SetAttr(kNameSpaceID_None, nsGkAtoms::direction, direction, true); + } else { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + aVisitor.mEvent->PreventDefault(); + } + } + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTooltipElement.h b/dom/xul/XULTooltipElement.h new file mode 100644 index 0000000000..b8c3c08b6a --- /dev/null +++ b/dom/xul/XULTooltipElement.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULTooltipElement_h__ +#define XULTooltipElement_h__ + +#include "XULPopupElement.h" + +namespace mozilla::dom { + +nsXULElement* NS_NewXULTooltipElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + +class XULTooltipElement final : public XULPopupElement { + public: + explicit XULTooltipElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : XULPopupElement(std::move(aNodeInfo)) {} + nsresult Init(); + + nsresult AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + protected: + virtual ~XULTooltipElement() = default; +}; + +} // namespace mozilla::dom + +#endif // XULPopupElement_h diff --git a/dom/xul/XULTreeElement.cpp b/dom/xul/XULTreeElement.cpp new file mode 100644 index 0000000000..e190b22556 --- /dev/null +++ b/dom/xul/XULTreeElement.cpp @@ -0,0 +1,411 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsCOMPtr.h" +#include "nsTreeContentView.h" +#include "nsITreeSelection.h" +#include "ChildIterator.h" +#include "nsError.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/XULTreeElement.h" +#include "mozilla/dom/XULTreeElementBinding.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULTreeElement, nsXULElement) +NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeElement, nsXULElement, mView) + +JSObject* XULTreeElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return XULTreeElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void XULTreeElement::UnbindFromTree(bool aNullParent) { + // Drop the view's ref to us. + if (mView) { + nsCOMPtr<nsITreeSelection> sel; + mView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + mView->SetTree(nullptr); // Break the circular ref between the view and us. + } + mView = nullptr; + + nsXULElement::UnbindFromTree(aNullParent); +} + +void XULTreeElement::DestroyContent() { + // Drop the view's ref to us. + if (mView) { + nsCOMPtr<nsITreeSelection> sel; + mView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + mView->SetTree(nullptr); // Break the circular ref between the view and us. + } + mView = nullptr; + + nsXULElement::DestroyContent(); +} + +static nsIContent* FindBodyElement(nsIContent* aParent) { + mozilla::dom::FlattenedChildIterator iter(aParent); + for (nsIContent* content = iter.GetNextChild(); content; + content = iter.GetNextChild()) { + mozilla::dom::NodeInfo* ni = content->NodeInfo(); + if (ni->Equals(nsGkAtoms::treechildren, kNameSpaceID_XUL)) { + return content; + } else if (ni->Equals(nsGkAtoms::tree, kNameSpaceID_XUL)) { + // There are nesting tree elements. Only the innermost should + // find the treechilren. + return nullptr; + } else if (content->IsElement() && + !ni->Equals(nsGkAtoms::_template, kNameSpaceID_XUL)) { + nsIContent* result = FindBodyElement(content); + if (result) return result; + } + } + + return nullptr; +} + +nsTreeBodyFrame* XULTreeElement::GetTreeBodyFrame(FlushType aFlushType) { + MOZ_ASSERT(aFlushType == FlushType::Frames || + aFlushType == FlushType::Layout || aFlushType == FlushType::None); + nsCOMPtr<nsIContent> kungFuDeathGrip = this; // keep a reference + + // Make sure our frames are up to date, and layout as needed. We + // have to do this before checking for our cached mTreeBody, since + // it might go away on style flush, and in any case if aFlushLayout + // is true we need to make sure to flush no matter what. + if (aFlushType != FlushType::None) { + if (RefPtr<Document> doc = GetComposedDoc()) { + doc->FlushPendingNotifications(aFlushType); + } + } + + if (mTreeBody) { + // Have one cached already. + return mTreeBody; + } + + if (nsCOMPtr<nsIContent> tree = FindBodyElement(this)) { + mTreeBody = do_QueryFrame(tree->GetPrimaryFrame()); + } + + return mTreeBody; +} + +already_AddRefed<nsITreeView> XULTreeElement::GetView(FlushType aFlushType) { + if (!mTreeBody) { + if (!GetTreeBodyFrame(aFlushType)) { + return nullptr; + } + + if (mView) { + nsCOMPtr<nsITreeView> view; + // Our new frame needs to initialise itself + mTreeBody->GetView(getter_AddRefs(view)); + return view.forget(); + } + } + if (!mView) { + // No tree builder, create a tree content view. + if (NS_FAILED(NS_NewTreeContentView(getter_AddRefs(mView)))) { + return nullptr; + } + + // Initialise the frame and view + mTreeBody->SetView(mView); + } + + return do_AddRef(mView); +} + +void XULTreeElement::SetView(nsITreeView* aView, CallerType aCallerType, + ErrorResult& aRv) { + if (aCallerType != CallerType::System) { + // Don't trust views coming from random places. + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + mView = aView; + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->SetView(aView); + } +} + +bool XULTreeElement::Focused() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->GetFocused(); + } + return false; +} + +void XULTreeElement::SetFocused(bool aFocused) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->SetFocused(aFocused); + } +} + +already_AddRefed<Element> XULTreeElement::GetTreeBody() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsCOMPtr<Element> element; + body->GetTreeBody(getter_AddRefs(element)); + return element.forget(); + } + + return nullptr; +} + +already_AddRefed<nsTreeColumns> XULTreeElement::GetColumns( + FlushType aFlushType) { + if (nsTreeBodyFrame* body = GetTreeBodyFrame(aFlushType)) { + return body->Columns(); + } + return nullptr; +} + +int32_t XULTreeElement::RowHeight() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->RowHeight(); + } + return 0; +} + +int32_t XULTreeElement::RowWidth() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->RowWidth(); + } + return 0; +} + +int32_t XULTreeElement::GetFirstVisibleRow() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->FirstVisibleRow(); + } + return 0; +} + +int32_t XULTreeElement::GetLastVisibleRow() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->LastVisibleRow(); + } + return 0; +} + +int32_t XULTreeElement::HorizontalPosition() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->GetHorizontalPosition(); + } + return 0; +} + +int32_t XULTreeElement::GetPageLength() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + return body->PageLength(); + } + return 0; +} + +void XULTreeElement::EnsureRowIsVisible(int32_t aRow) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->EnsureRowIsVisible(aRow); + } +} + +void XULTreeElement::EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol, + ErrorResult& aRv) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsresult rv = body->EnsureCellIsVisible(aRow, aCol); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } + } +} + +void XULTreeElement::ScrollToRow(int32_t aRow) { + nsTreeBodyFrame* body = GetTreeBodyFrame(FlushType::Layout); + if (!body) { + return; + } + + body->ScrollToRow(aRow); +} + +void XULTreeElement::ScrollByLines(int32_t aNumLines) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (!body) { + return; + } + body->ScrollByLines(aNumLines); +} + +void XULTreeElement::ScrollByPages(int32_t aNumPages) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->ScrollByPages(aNumPages); + } +} + +void XULTreeElement::Invalidate() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->Invalidate(); + } +} + +void XULTreeElement::InvalidateColumn(nsTreeColumn* aCol) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateColumn(aCol); + } +} + +void XULTreeElement::InvalidateRow(int32_t aIndex) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateRow(aIndex); + } +} + +void XULTreeElement::InvalidateCell(int32_t aRow, nsTreeColumn* aCol) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateCell(aRow, aCol); + } +} + +void XULTreeElement::InvalidateRange(int32_t aStart, int32_t aEnd) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->InvalidateRange(aStart, aEnd); + } +} + +int32_t XULTreeElement::GetRowAt(int32_t x, int32_t y) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (!body) { + return 0; + } + return body->GetRowAt(x, y); +} + +void XULTreeElement::GetCellAt(int32_t aX, int32_t aY, TreeCellInfo& aRetVal, + ErrorResult& aRv) { + aRetVal.mRow = 0; + aRetVal.mCol = nullptr; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + nsAutoCString element; + body->GetCellAt(aX, aY, &aRetVal.mRow, getter_AddRefs(aRetVal.mCol), + element); + CopyUTF8toUTF16(element, aRetVal.mChildElt); + } +} + +nsIntRect XULTreeElement::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aElement, + nsresult& rv) { + rv = NS_OK; + nsIntRect rect; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + NS_ConvertUTF16toUTF8 element(aElement); + if (body) { + rv = body->GetCoordsForCellItem(aRow, aCol, element, &rect.x, &rect.y, + &rect.width, &rect.height); + } + + return rect; +} + +already_AddRefed<DOMRect> XULTreeElement::GetCoordsForCellItem( + int32_t aRow, nsTreeColumn& aCol, const nsAString& aElement, + ErrorResult& aRv) { + nsresult rv; + nsIntRect rect = GetCoordsForCellItem(aRow, &aCol, aElement, rv); + aRv = rv; + + RefPtr<DOMRect> domRect = new DOMRect(ToSupports(OwnerDoc()), rect.x, rect.y, + rect.width, rect.height); + return domRect.forget(); +} + +bool XULTreeElement::IsCellCropped(int32_t aRow, nsTreeColumn* aCol, + ErrorResult& aRv) { + bool cropped = false; + + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + aRv = body->IsCellCropped(aRow, aCol, &cropped); + } + + return cropped; +} + +void XULTreeElement::RowCountChanged(int32_t aIndex, int32_t aDelta) { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->RowCountChanged(aIndex, aDelta); + } +} + +void XULTreeElement::BeginUpdateBatch() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->BeginUpdateBatch(); + } +} + +void XULTreeElement::EndUpdateBatch() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->EndUpdateBatch(); + } +} + +void XULTreeElement::ClearStyleAndImageCaches() { + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->ClearStyleAndImageCaches(); + } +} + +void XULTreeElement::RemoveImageCacheEntry(int32_t aRowIndex, + nsTreeColumn& aCol, + ErrorResult& aRv) { + if (NS_WARN_IF(aRowIndex < 0)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + nsTreeBodyFrame* body = GetTreeBodyFrame(); + if (body) { + body->RemoveImageCacheEntry(aRowIndex, &aCol); + } +} + +} // namespace mozilla::dom diff --git a/dom/xul/XULTreeElement.h b/dom/xul/XULTreeElement.h new file mode 100644 index 0000000000..381ae88f41 --- /dev/null +++ b/dom/xul/XULTreeElement.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#ifndef XULTreeElement_h__ +#define XULTreeElement_h__ + +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsString.h" +#include "nsXULElement.h" +#include "nsITreeView.h" + +class nsTreeBodyFrame; +class nsTreeColumn; +class nsTreeColumns; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct TreeCellInfo; +class DOMRect; +enum class CallerType : uint32_t; + +class XULTreeElement final : public nsXULElement { + public: + explicit XULTreeElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsXULElement(std::move(aNodeInfo)), + mCachedFirstVisibleRow(0), + mTreeBody(nullptr) {} + + NS_IMPL_FROMNODE_WITH_TAG(XULTreeElement, kNameSpaceID_XUL, tree) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeElement, nsXULElement) + + nsTreeBodyFrame* GetTreeBodyFrame(FlushType = FlushType::Frames); + nsTreeBodyFrame* GetCachedTreeBodyFrame() { return mTreeBody; } + + already_AddRefed<nsTreeColumns> GetColumns(FlushType = FlushType::Frames); + + already_AddRefed<nsITreeView> GetView(CallerType /* unused */) { + return GetView(); + } + already_AddRefed<nsITreeView> GetView(FlushType = FlushType::Frames); + + void SetView(nsITreeView* arg, CallerType aCallerType, ErrorResult& aRv); + + bool Focused(); + + already_AddRefed<Element> GetTreeBody(); + + int32_t RowHeight(); + + int32_t RowWidth(); + + int32_t HorizontalPosition(); + + void EnsureCellIsVisible(int32_t row, nsTreeColumn* col, ErrorResult& aRv); + + void ScrollToRow(int32_t aRow); + + void ScrollByLines(int32_t aNumLines); + + void ScrollByPages(int32_t aNumPages); + + int32_t GetFirstVisibleRow(); + + int32_t GetLastVisibleRow(); + + int32_t GetPageLength(); + + int32_t GetRowAt(int32_t x, int32_t y); + + void GetCellAt(int32_t x, int32_t y, TreeCellInfo& aRetVal, ErrorResult& aRv); + + nsIntRect GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aElement, nsresult& rv); + already_AddRefed<DOMRect> GetCoordsForCellItem(int32_t row, nsTreeColumn& col, + const nsAString& element, + ErrorResult& aRv); + + bool IsCellCropped(int32_t row, nsTreeColumn* col, ErrorResult& aRv); + + void RemoveImageCacheEntry(int32_t row, nsTreeColumn& col, ErrorResult& aRv); + + void SetFocused(bool aFocused); + void EnsureRowIsVisible(int32_t index); + void Invalidate(void); + void InvalidateColumn(nsTreeColumn* col); + void InvalidateRow(int32_t index); + void InvalidateCell(int32_t row, nsTreeColumn* col); + void InvalidateRange(int32_t startIndex, int32_t endIndex); + void RowCountChanged(int32_t index, int32_t count); + void BeginUpdateBatch(void); + void EndUpdateBatch(void); + void ClearStyleAndImageCaches(void); + + virtual void UnbindFromTree(bool aNullParent) override; + virtual void DestroyContent() override; + + void BodyDestroyed(int32_t aFirstVisibleRow) { + mTreeBody = nullptr; + mCachedFirstVisibleRow = aFirstVisibleRow; + } + + int32_t GetCachedTopVisibleRow() { return mCachedFirstVisibleRow; } + + protected: + int32_t mCachedFirstVisibleRow; + + nsTreeBodyFrame* mTreeBody; + nsCOMPtr<nsITreeView> mView; + + virtual ~XULTreeElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/xul/crashtests/107518-1.xml b/dom/xul/crashtests/107518-1.xml new file mode 100644 index 0000000000..58e652624c --- /dev/null +++ b/dom/xul/crashtests/107518-1.xml @@ -0,0 +1,52 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<!DOCTYPE window>
+
+<window
+ id = "xulnote-main-window"
+ xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html = "http://www.w3.org/1999/xhtml"
+ onload = "init();"
+>
+ <scrollbox>
+ <vbox style="background-color:white;">
+ <label value="hahaha 0"/>
+ <label value="hahaha 1"/>
+ <label value="hahaha 2"/>
+ <label value="hahaha 3"/>
+ <label value="hahaha 4"/>
+ <label value="hahaha 5"/>
+ <label value="hahaha 6"/>
+ <label value="hahaha 7"/>
+ <label value="hahaha 8"/>
+ <label value="hahaha 9"/>
+ <label value="hahaha 10"/>
+ <label value="hahaha 11"/>
+ <label value="hahaha 12"/>
+ <label value="hahaha 13"/>
+ <label value="hahaha 14"/>
+ <label value="hahaha 15"/>
+ <label value="hahaha 16"/>
+ <label value="hahaha 17"/>
+ <label value="hahaha 18"/>
+ <label value="hahaha 19"/>
+ </vbox>
+<scrollbar
+ id="identifier"
+ align="horizontal"
+ curpos="20"
+ maxpos="100"
+ increment="1"
+ pageincrement="10"/>
+
+ </scrollbox>
+
+ <script type="application/x-javascript">
+ <![CDATA[
+ function init()
+ {
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/dom/xul/crashtests/253479-1.xhtml b/dom/xul/crashtests/253479-1.xhtml new file mode 100644 index 0000000000..5860d2200d --- /dev/null +++ b/dom/xul/crashtests/253479-1.xhtml @@ -0,0 +1,6 @@ +<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+<html:div style="position:fixed;"/>
+</window>
\ No newline at end of file diff --git a/dom/xul/crashtests/253479-2.xhtml b/dom/xul/crashtests/253479-2.xhtml new file mode 100644 index 0000000000..43a8c17b84 --- /dev/null +++ b/dom/xul/crashtests/253479-2.xhtml @@ -0,0 +1,4 @@ +<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <x style="position:fixed; display: block;"/>
+</window>
diff --git a/dom/xul/crashtests/326204-1.xhtml b/dom/xul/crashtests/326204-1.xhtml new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/xul/crashtests/326204-1.xhtml diff --git a/dom/xul/crashtests/326644-1-inner.xhtml b/dom/xul/crashtests/326644-1-inner.xhtml new file mode 100644 index 0000000000..63ed9b7212 --- /dev/null +++ b/dom/xul/crashtests/326644-1-inner.xhtml @@ -0,0 +1,34 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testcase bug 326644 - Crash when changing enumerated properties of objects in xul">
+
+<html:script><![CDATA[
+var timers=0;
+function doe(aObj, aNested, aCurrentTimer){
+var temp =0;
+for (var i in aObj) {
+try {
+if (typeof aObj[i] == 'object') {
+ if (aNested >= 19 || aObj[i] == window.location)
+ continue;
+ setTimeout(doe,500, aObj[i], ++aNested, timers);
+ timers++;
+}
+}
+catch(e){}
+try {
+ //if (temp == 68 && aNested == 21 && aCurrentTimer >= 116) {
+ // alert(i + '-'+ aObj[i]);
+ // return;
+ // }
+ aObj[i]= i;
+ temp+=1;
+}
+catch (e) {
+
+}
+}
+}
+var s=document.getElementsByTagName('window')[0];
+setTimeout(doe,100, s, 0);
+]]></html:script>
+</window>
diff --git a/dom/xul/crashtests/326644-1.html b/dom/xul/crashtests/326644-1.html new file mode 100644 index 0000000000..6f1d88a5eb --- /dev/null +++ b/dom/xul/crashtests/326644-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 1000); +</script> +<body> +<iframe src="326644-1-inner.xhtml"></iframe> +</body> +</html> diff --git a/dom/xul/crashtests/326875-1.xhtml b/dom/xul/crashtests/326875-1.xhtml new file mode 100644 index 0000000000..9dca9a0d53 --- /dev/null +++ b/dom/xul/crashtests/326875-1.xhtml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + + +<script> + + +function init() { + + var m = document.getElementById("m"); + m.parentNode.removeChild(m); + m.controllers; +}; + + +window.addEventListener("load", init, false); + +</script> + + + +<hbox id="m" /> + +</window> diff --git a/dom/xul/crashtests/329982-1.xhtml b/dom/xul/crashtests/329982-1.xhtml new file mode 100644 index 0000000000..43f374a57e --- /dev/null +++ b/dom/xul/crashtests/329982-1.xhtml @@ -0,0 +1,42 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + +<script> + +function init() +{ + var A = document.getElementById("z"); + var B = A.nextSibling; + var C = B.nextSibling; + var P = A.parentNode; + + document.addEventListener("DOMNodeRemoved", fizzy, false); + P.removeChild(B); + document.removeEventListener("DOMNodeRemoved", fizzy, false); + + function fizzy() + { + document.removeEventListener("DOMNodeRemoved", fizzy, false); // avoid recursion + P.removeChild(A); + } + + document.documentElement.appendChild(C); +} + + +window.addEventListener("load", init, false); + +</script> + +</head> + +<body> + +<hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<menupopup y="x">
<menuitem id="z"/>
<menuitem/>
<menuitem/>
</menupopup>
+ +</hbox> + +</body> +</html>
\ No newline at end of file diff --git a/dom/xul/crashtests/336096-1.xhtml b/dom/xul/crashtests/336096-1.xhtml new file mode 100644 index 0000000000..e15691d88d --- /dev/null +++ b/dom/xul/crashtests/336096-1.xhtml @@ -0,0 +1,41 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function init() +{ + var targetWindow = window.frames[0]; + var targetDocument = targetWindow.document; + + targetDocument.body.appendChild(document.getElementById('rootish')); + targetDocument.designMode = 'on'; + + var r = targetDocument.createRange(); + r.setStart(targetDocument.getElementById("start"), 0); + r.setEnd (targetDocument.getElementById("end"), 0); + targetWindow.getSelection().addRange(r); + + targetDocument.execCommand('bold', false, null); +} + +]]> +</script> +</head> + +<body onload="setTimeout(init, 200);"> + +<iframe src="data:text/html," style="width: 95%; height: 500px;"></iframe> + +<div id="rootish"> + <div id="start"></div> + <hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <hbox/> + <vbox id="end"> + <hbox/> + </vbox> + </hbox> +</div> + +</body> +</html> diff --git a/dom/xul/crashtests/344215-1.xhtml b/dom/xul/crashtests/344215-1.xhtml new file mode 100644 index 0000000000..6443c22d6c --- /dev/null +++ b/dom/xul/crashtests/344215-1.xhtml @@ -0,0 +1,7 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<observes/> + +<description>You should not see any assertions in a debug build.</description> + +</window>
\ No newline at end of file diff --git a/dom/xul/crashtests/354611-1.html b/dom/xul/crashtests/354611-1.html new file mode 100644 index 0000000000..fe25de3660 --- /dev/null +++ b/dom/xul/crashtests/354611-1.html @@ -0,0 +1,20 @@ +<html> +<head> +<script> + +var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function boom() +{ + var z = document.createElementNS(XUL_NS, "window"); + document.body.appendChild(z); + z.setAttribute("hidechrome", "true"); +} + +</script> + +<body onload="boom();"> + +</body> + +</html>
\ No newline at end of file diff --git a/dom/xul/crashtests/360078-1.xhtml b/dom/xul/crashtests/360078-1.xhtml new file mode 100644 index 0000000000..a29087014d --- /dev/null +++ b/dom/xul/crashtests/360078-1.xhtml @@ -0,0 +1,42 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> + +<style> +<![CDATA[ +#baz { -moz-binding: url(360078-1xbl.xml#foo); } +]]> +</style> + +<script> +<![CDATA[ + +function stuff() +{ + var baz = document.getElementById("baz"); + var count = 0; + + setTimeout(step, 30); + + function step() + { + ++count; + if (count < 15) { + baz.cloneNode(true); + setTimeout(step, 30); + } + else { + document.documentElement.removeAttribute("class"); + } + } + +} + +]]> +</script> +</head> +<body onload="stuff()"> + +<hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="baz"></hbox> + +</body> +</html> diff --git a/dom/xul/crashtests/363791-1.xhtml b/dom/xul/crashtests/363791-1.xhtml new file mode 100644 index 0000000000..0786960f23 --- /dev/null +++ b/dom/xul/crashtests/363791-1.xhtml @@ -0,0 +1,44 @@ +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="setTimeout(boom, 30);" class="reftest-wait"> + +<script> + +function boom() +{ + var tree = document.getElementById("tree"); + var treecols = document.getElementById("treecols"); + var treechildren = document.getElementById("treechildren"); + + tree.appendChild(treechildren); // no real change + + function boom2() { + treecols.parentNode.removeChild(treecols); + document.documentElement.removeAttribute("class"); + } + + setTimeout(boom2, 30); +} + +</script> + + +<tree rows="6" id="tree"> + + <treecols id="treecols"> + <treecol id="firstname" label="First Name"/> + </treecols> + + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Bob"/> + </treerow> + </treeitem> + </treechildren> + +</tree> + + +</window> diff --git a/dom/xul/crashtests/384877-1-inner.xhtml b/dom/xul/crashtests/384877-1-inner.xhtml new file mode 100644 index 0000000000..bd75aa87a7 --- /dev/null +++ b/dom/xul/crashtests/384877-1-inner.xhtml @@ -0,0 +1,12 @@ +<menupopup xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="d" popup="d"> +<script> +function doe() { +var d = document.getElementById('d'); +if (d.openPopup) { + d.openPopup(document.documentElement, 'before_start', 0, 0, false, false); +// alert(d.state); +} + +setTimeout(doe, 200); +</script> +</menupopup> diff --git a/dom/xul/crashtests/384877-1.html b/dom/xul/crashtests/384877-1.html new file mode 100644 index 0000000000..b4ecd1b5f8 --- /dev/null +++ b/dom/xul/crashtests/384877-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 1000); +</script> +<body> +<iframe src="384877-1-inner.xhtml"></iframe> +</body> +</html> diff --git a/dom/xul/crashtests/386914-1-inner.xhtml b/dom/xul/crashtests/386914-1-inner.xhtml new file mode 100644 index 0000000000..909889d8b5 --- /dev/null +++ b/dom/xul/crashtests/386914-1-inner.xhtml @@ -0,0 +1,10 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="a"> + +<box id="b" observes="a"/> + +<html:script xmlns:html="http://www.w3.org/1999/xhtml"> +document.getElementById('b').addEventListener('DOMAttrModified', function(e) {document.removeChild(document.documentElement);}, true); +setTimeout(function() {document.getElementById('a').setAttribute('tabindex', '1') ;}, 100); +</html:script> + +</window>
\ No newline at end of file diff --git a/dom/xul/crashtests/386914-1.html b/dom/xul/crashtests/386914-1.html new file mode 100644 index 0000000000..2bb1224ab7 --- /dev/null +++ b/dom/xul/crashtests/386914-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 1000); +</script> +<body> +<iframe src="386914-1-inner.xhtml"></iframe> +</body> +</html> diff --git a/dom/xul/crashtests/425821-1.xhtml b/dom/xul/crashtests/425821-1.xhtml new file mode 100644 index 0000000000..9764e64aad --- /dev/null +++ b/dom/xul/crashtests/425821-1.xhtml @@ -0,0 +1,15 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="boom();"> +<script type="text/javascript"> + +function boom() +{ + var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var wiz = document.createElementNS(XUL_NS, "wizard"); + var btn = document.createElementNS(XUL_NS, "hbox"); + btn.setAttribute("anonid", "Buttons"); + wiz.appendChild(btn); + wiz.cloneNode(true); +} + +</script> +</window> diff --git a/dom/xul/crashtests/428951-1.xhtml b/dom/xul/crashtests/428951-1.xhtml new file mode 100644 index 0000000000..41dd353f3d --- /dev/null +++ b/dom/xul/crashtests/428951-1.xhtml @@ -0,0 +1,21 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:mathml="http://www.w3.org/1998/Math/MathML"> +<box> + <box style="background: initial;" id="f"> + <box style="margin-top: -9999999px;"/> + </box> + <mathml:divergence> + <box/> + </mathml:divergence> + <mathml:moment command="f"/> +</box> + +<script id="script" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[ +function init() { + var f = document.getElementsByTagName('mathml:divergence')[0]; + window.addEventListener('DOMAttrModified',function() { f.remove();}, true); + var x=document.getElementsByTagName('mathml:moment')[0]; + x.remove(); +} +window.addEventListener("load", init, false); +]]></script> +</window> diff --git a/dom/xul/crashtests/431906-1-inner.xhtml b/dom/xul/crashtests/431906-1-inner.xhtml new file mode 100644 index 0000000000..367f621b2f --- /dev/null +++ b/dom/xul/crashtests/431906-1-inner.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<colgroup id="a" command="a"> +<box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<box id="a" command="a"/> +<box id="a" command="a"/> +</box> +</colgroup> +<script><![CDATA[ +function doe(){ +document.getElementsByTagName('*')[1].setAttribute('id',''); +document.getElementsByTagName('*')[1].setAttribute('xmlns', ''); +document.getElementsByTagName('*')[3].setAttribute('id',''); + +document.getElementsByTagName('*')[4].removeAttribute('xmlns'); +document.getElementsByTagName('*')[4].setAttribute('width', '1px'); +} +setTimeout(doe,100); +]]></script> +</html>
\ No newline at end of file diff --git a/dom/xul/crashtests/431906-1.html b/dom/xul/crashtests/431906-1.html new file mode 100644 index 0000000000..a8570b1bc0 --- /dev/null +++ b/dom/xul/crashtests/431906-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 1000); +</script> +<body> +<iframe src="431906-1-inner.xhtml"></iframe> +</body> +</html> diff --git a/dom/xul/crashtests/461917-1.xhtml b/dom/xul/crashtests/461917-1.xhtml new file mode 100644 index 0000000000..15792f6f0f --- /dev/null +++ b/dom/xul/crashtests/461917-1.xhtml @@ -0,0 +1,6 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head></head> +<body> +<tabs xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onselect="#"><wizard/></tabs> +</body> +</html> diff --git a/dom/xul/crashtests/crashtests.list b/dom/xul/crashtests/crashtests.list new file mode 100644 index 0000000000..970d57b457 --- /dev/null +++ b/dom/xul/crashtests/crashtests.list @@ -0,0 +1,17 @@ +load 107518-1.xml +load chrome://reftest/content/crashtests/dom/xul/crashtests/253479-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/253479-2.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/326204-1.xhtml +load 326644-1.html +load chrome://reftest/content/crashtests/dom/xul/crashtests/326875-1.xhtml +load 329982-1.xhtml +load 336096-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/344215-1.xhtml +load 354611-1.html +skip-if(Android) load chrome://reftest/content/crashtests/dom/xul/crashtests/363791-1.xhtml +load 384877-1.html +load 386914-1.html +load chrome://reftest/content/crashtests/dom/xul/crashtests/425821-1.xhtml +load chrome://reftest/content/crashtests/dom/xul/crashtests/428951-1.xhtml +load 431906-1.html +load 461917-1.xhtml diff --git a/dom/xul/moz.build b/dom/xul/moz.build new file mode 100644 index 0000000000..c243aee856 --- /dev/null +++ b/dom/xul/moz.build @@ -0,0 +1,91 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +if CONFIG["MOZ_BUILD_APP"] == "browser": + DEFINES["MOZ_BREAK_XUL_OVERLAYS"] = True + +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +EXPORTS += [ + "nsXULCommandDispatcher.h", + "nsXULElement.h", + "nsXULPrototypeDocument.h", + "nsXULSortService.h", +] + +EXPORTS.mozilla.dom += [ + "XULBroadcastManager.h", + "XULButtonElement.h", + "XULFrameElement.h", + "XULMenuElement.h", + "XULMenuParentElement.h", + "XULPersist.h", + "XULPopupElement.h", + "XULResizerElement.h", + "XULTextElement.h", + "XULTooltipElement.h", + "XULTreeElement.h", +] + +UNIFIED_SOURCES += [ + "nsXULCommandDispatcher.cpp", + "nsXULContentSink.cpp", + "nsXULContentUtils.cpp", + "nsXULElement.cpp", + "nsXULPopupListener.cpp", + "nsXULPrototypeCache.cpp", + "nsXULPrototypeDocument.cpp", + "nsXULSortService.cpp", + "XULBroadcastManager.cpp", + "XULButtonElement.cpp", + "XULFrameElement.cpp", + "XULMenuElement.cpp", + "XULMenuParentElement.cpp", + "XULPersist.cpp", + "XULPopupElement.cpp", + "XULResizerElement.cpp", + "XULTextElement.cpp", + "XULTooltipElement.cpp", + "XULTreeElement.cpp", +] + +XPIDL_SOURCES += [ + "nsIBrowserController.idl", + "nsIController.idl", + "nsIControllers.idl", +] + +XPIDL_MODULE = "xul" + +EXPORTS.mozilla.dom += [ + "ChromeObserver.h", +] + + +UNIFIED_SOURCES += [ + "ChromeObserver.cpp", + "nsXULControllers.cpp", +] + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/dom/html", + "/dom/xml", + "/layout/base", + "/layout/generic", + "/layout/style", + "/layout/xul", + "/layout/xul/tree", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/xul/nsIBrowserController.idl b/dom/xul/nsIBrowserController.idl new file mode 100644 index 0000000000..5a7b77529b --- /dev/null +++ b/dom/xul/nsIBrowserController.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * This interface is used to accompany the nsIController for a + * <browser> element. It is used to update the commands in the + * parent process when the set of child command have changed. + */ +[scriptable, uuid(5bb3d56b-e733-4a2c-8a53-058123df65e2)] +interface nsIBrowserController : nsISupports +{ + // Update the commands for a given action in the parent process. + void enableDisableCommands(in AString action, + in Array<ACString> enabledCommands, + in Array<ACString> disabledCommands); +}; diff --git a/dom/xul/nsIController.idl b/dom/xul/nsIController.idl new file mode 100644 index 0000000000..c3249c5749 --- /dev/null +++ b/dom/xul/nsIController.idl @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +[scriptable, uuid(D5B61B82-1DA4-11d3-BF87-00105A1B0627)] +interface nsIController : nsISupports { + boolean isCommandEnabled(in string command); + boolean supportsCommand(in string command); + + [can_run_script] + void doCommand(in string command); + + void onEvent(in string eventName); +}; + + +/* + + Enhanced controller interface that allows for passing of parameters + to commands. + +*/ + +interface nsICommandParams; + +[scriptable, uuid(EEC0B435-7F53-44FE-B00A-CF3EED65C01A)] +interface nsICommandController : nsISupports +{ + + void getCommandStateWithParams( in string command, in nsICommandParams aCommandParams); + + [can_run_script] + void doCommandWithParams(in string command, in nsICommandParams aCommandParams); + + Array<ACString> getSupportedCommands(); +}; diff --git a/dom/xul/nsIControllers.idl b/dom/xul/nsIControllers.idl new file mode 100644 index 0000000000..2f272faec1 --- /dev/null +++ b/dom/xul/nsIControllers.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIController; +interface nsIDOMXULCommandDispatcher; + +[scriptable, uuid(f36e3ec1-9197-4ad8-8d4c-d3b1927fd6df)] +interface nsIControllers : nsISupports +{ + nsIController getControllerForCommand(in string command); + + void insertControllerAt(in unsigned long index, in nsIController controller); + nsIController removeControllerAt(in unsigned long index); + nsIController getControllerAt(in unsigned long index); + + void appendController(in nsIController controller); + void removeController(in nsIController controller); + + /* + Return an ID for this controller which is unique to this + nsIControllers. + */ + unsigned long getControllerId(in nsIController controller); + /* + Get the controller specified by the given ID. + */ + nsIController getControllerById(in unsigned long controllerID); + + unsigned long getControllerCount(); +}; diff --git a/dom/xul/nsXULCommandDispatcher.cpp b/dom/xul/nsXULCommandDispatcher.cpp new file mode 100644 index 0000000000..0b07086914 --- /dev/null +++ b/dom/xul/nsXULCommandDispatcher.cpp @@ -0,0 +1,435 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 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/. */ + +/* + + This file provides the implementation for the XUL Command Dispatcher. + + */ + +#include "nsIContent.h" +#include "nsFocusManager.h" +#include "nsIControllers.h" +#include "mozilla/dom/Document.h" +#include "nsPresContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsXULCommandDispatcher.h" +#include "mozilla/Logging.h" +#include "nsContentUtils.h" +#include "nsReadableUtils.h" +#include "nsCRT.h" +#include "nsError.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ElementBinding.h" + +using namespace mozilla; +using mozilla::dom::Document; +using mozilla::dom::Element; + +#ifdef DEBUG +static LazyLogModule gCommandLog("nsXULCommandDispatcher"); +#endif + +//////////////////////////////////////////////////////////////////////// + +nsXULCommandDispatcher::nsXULCommandDispatcher(Document* aDocument) + : mDocument(aDocument), mUpdaters(nullptr), mLocked(false) {} + +nsXULCommandDispatcher::~nsXULCommandDispatcher() { Disconnect(); } + +// QueryInterface implementation for nsXULCommandDispatcher + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULCommandDispatcher) + NS_INTERFACE_MAP_ENTRY(nsIDOMXULCommandDispatcher) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMXULCommandDispatcher) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULCommandDispatcher) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULCommandDispatcher) + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULCommandDispatcher) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULCommandDispatcher) + tmp->Disconnect(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULCommandDispatcher) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + Updater* updater = tmp->mUpdaters; + while (updater) { + cb.NoteXPCOMChild(updater->mElement); + updater = updater->mNext; + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void nsXULCommandDispatcher::Disconnect() { + while (mUpdaters) { + Updater* doomed = mUpdaters; + mUpdaters = mUpdaters->mNext; + delete doomed; + } + mDocument = nullptr; +} + +already_AddRefed<nsPIWindowRoot> nsXULCommandDispatcher::GetWindowRoot() { + if (mDocument) { + if (nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow()) { + return window->GetTopWindowRoot(); + } + } + + return nullptr; +} + +Element* nsXULCommandDispatcher::GetRootFocusedContentAndWindow( + nsPIDOMWindowOuter** aWindow) { + *aWindow = nullptr; + + if (!mDocument) { + return nullptr; + } + + if (nsCOMPtr<nsPIDOMWindowOuter> win = mDocument->GetWindow()) { + if (nsCOMPtr<nsPIDOMWindowOuter> rootWindow = win->GetPrivateRoot()) { + return nsFocusManager::GetFocusedDescendant( + rootWindow, nsFocusManager::eIncludeAllDescendants, aWindow); + } + } + + return nullptr; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetFocusedElement(Element** aElement) { + *aElement = nullptr; + + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + RefPtr<Element> focusedContent = + GetRootFocusedContentAndWindow(getter_AddRefs(focusedWindow)); + if (focusedContent) { + // Make sure the caller can access the focused element. + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller()->Subsumes( + focusedContent->NodePrincipal())) { + // XXX This might want to return null, but we use that return value + // to mean "there is no focused element," so to be clear, throw an + // exception. + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + focusedContent.forget(aElement); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetFocusedWindow(mozIDOMWindowProxy** aWindow) { + *aWindow = nullptr; + + nsCOMPtr<nsPIDOMWindowOuter> window; + GetRootFocusedContentAndWindow(getter_AddRefs(window)); + if (!window) return NS_OK; + + // Make sure the caller can access this window. The caller can access this + // window iff it can access the document. + nsCOMPtr<Document> doc = window->GetDoc(); + + // Note: If there is no document, then this window has been cleared and + // there's nothing left to protect, so let the window pass through. + if (doc && !nsContentUtils::CanCallerAccess(doc)) + return NS_ERROR_DOM_SECURITY_ERR; + + window.forget(aWindow); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::SetFocusedElement(Element* aElement) { + RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_FAILURE); + + if (aElement) { + return fm->SetFocus(aElement, 0); + } + + // if aElement is null, clear the focus in the currently focused child window + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + GetRootFocusedContentAndWindow(getter_AddRefs(focusedWindow)); + return fm->ClearFocus(focusedWindow); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::SetFocusedWindow(mozIDOMWindowProxy* aWindow) { + NS_ENSURE_TRUE(aWindow, NS_OK); // do nothing if set to null + + nsCOMPtr<nsPIDOMWindowOuter> window = nsPIDOMWindowOuter::From(aWindow); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_FAILURE); + + // get the containing frame for the window, and set it as focused. This will + // end up focusing whatever is currently focused inside the frame. Since + // setting the command dispatcher's focused window doesn't raise the window, + // setting it to a top-level window doesn't need to do anything. + RefPtr<Element> frameElement = window->GetFrameElementInternal(); + if (frameElement) { + return fm->SetFocus(frameElement, 0); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AdvanceFocus() { + return AdvanceFocusIntoSubtree(nullptr); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AdvanceFocusIntoSubtree(Element* aElt) { + return MoveFocusIntoSubtree(aElt, /* aForward = */ true); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::RewindFocus() { + return MoveFocusIntoSubtree(nullptr, /* aForward = */ false); +} + +nsresult nsXULCommandDispatcher::MoveFocusIntoSubtree(Element* aElt, + bool aForward) { + nsCOMPtr<nsPIDOMWindowOuter> win; + GetRootFocusedContentAndWindow(getter_AddRefs(win)); + + RefPtr<Element> result; + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) { + return NS_OK; + } + auto flags = nsFocusManager::ProgrammaticFocusFlags(dom::FocusOptions()) | + nsIFocusManager::FLAG_BYMOVEFOCUS; + auto type = aForward ? nsIFocusManager::MOVEFOCUS_FORWARD + : nsIFocusManager::MOVEFOCUS_BACKWARD; + return fm->MoveFocus(win, aElt, type, flags, getter_AddRefs(result)); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::AddCommandUpdater(Element* aElement, + const nsAString& aEvents, + const nsAString& aTargets) { + MOZ_ASSERT(aElement != nullptr, "null ptr"); + if (!aElement) return NS_ERROR_NULL_POINTER; + + NS_ENSURE_TRUE(mDocument, NS_ERROR_UNEXPECTED); + + nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, aElement); + + if (NS_FAILED(rv)) { + return rv; + } + + Updater* updater = mUpdaters; + Updater** link = &mUpdaters; + + while (updater) { + if (updater->mElement == aElement) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString eventsC, targetsC, aeventsC, atargetsC; + LossyCopyUTF16toASCII(updater->mEvents, eventsC); + LossyCopyUTF16toASCII(updater->mTargets, targetsC); + CopyUTF16toUTF8(aEvents, aeventsC); + CopyUTF16toUTF8(aTargets, atargetsC); + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] replace %p(events=%s targets=%s) with (events=%s " + "targets=%s)", + this, aElement, eventsC.get(), targetsC.get(), aeventsC.get(), + atargetsC.get())); + } +#endif + + // If the updater was already in the list, then replace + // (?) the 'events' and 'targets' filters with the new + // specification. + updater->mEvents = aEvents; + updater->mTargets = aTargets; + return NS_OK; + } + + link = &(updater->mNext); + updater = updater->mNext; + } +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString aeventsC, atargetsC; + CopyUTF16toUTF8(aEvents, aeventsC); + CopyUTF16toUTF8(aTargets, atargetsC); + + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] add %p(events=%s targets=%s)", this, aElement, + aeventsC.get(), atargetsC.get())); + } +#endif + + // If we get here, this is a new updater. Append it to the list. + *link = new Updater(aElement, aEvents, aTargets); + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::RemoveCommandUpdater(Element* aElement) { + MOZ_ASSERT(aElement != nullptr, "null ptr"); + if (!aElement) return NS_ERROR_NULL_POINTER; + + Updater* updater = mUpdaters; + Updater** link = &mUpdaters; + + while (updater) { + if (updater->mElement == aElement) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString eventsC, targetsC; + LossyCopyUTF16toASCII(updater->mEvents, eventsC); + LossyCopyUTF16toASCII(updater->mTargets, targetsC); + MOZ_LOG(gCommandLog, LogLevel::Debug, + ("xulcmd[%p] remove %p(events=%s targets=%s)", this, aElement, + eventsC.get(), targetsC.get())); + } +#endif + + *link = updater->mNext; + delete updater; + return NS_OK; + } + + link = &(updater->mNext); + updater = updater->mNext; + } + + // Hmm. Not found. Oh well. + return NS_OK; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::UpdateCommands(const nsAString& aEventName) { + if (mLocked) { + if (!mPendingUpdates.Contains(aEventName)) { + mPendingUpdates.AppendElement(aEventName); + } + + return NS_OK; + } + + nsAutoString id; + RefPtr<Element> element; + GetFocusedElement(getter_AddRefs(element)); + if (element) { + element->GetAttr(nsGkAtoms::id, id); + } + + nsCOMArray<nsIContent> updaters; + + for (Updater* updater = mUpdaters; updater != nullptr; + updater = updater->mNext) { + // Skip any nodes that don't match our 'events' or 'targets' + // filters. + if (!Matches(updater->mEvents, aEventName)) continue; + + if (!Matches(updater->mTargets, id)) continue; + + nsIContent* content = updater->mElement; + NS_ASSERTION(content != nullptr, "mElement is null"); + if (!content) return NS_ERROR_UNEXPECTED; + + updaters.AppendObject(content); + } + + for (nsIContent* content : updaters) { +#ifdef DEBUG + if (MOZ_LOG_TEST(gCommandLog, LogLevel::Debug)) { + nsAutoCString aeventnameC; + CopyUTF16toUTF8(aEventName, aeventnameC); + MOZ_LOG( + gCommandLog, LogLevel::Debug, + ("xulcmd[%p] update %p event=%s", this, content, aeventnameC.get())); + } +#endif + + WidgetEvent event(true, eXULCommandUpdate); + EventDispatcher::Dispatch(MOZ_KnownLive(content), nullptr, &event); + } + return NS_OK; +} + +bool nsXULCommandDispatcher::Matches(const nsString& aList, + const nsAString& aElement) { + if (aList.EqualsLiteral("*")) return true; // match _everything_! + + int32_t indx = aList.Find(PromiseFlatString(aElement)); + if (indx == -1) return false; // not in the list at all + + // okay, now make sure it's not a substring snafu; e.g., 'ur' + // found inside of 'blur'. + if (indx > 0) { + char16_t ch = aList[indx - 1]; + if (!nsCRT::IsAsciiSpace(ch) && ch != char16_t(',')) return false; + } + + if (indx + aElement.Length() < aList.Length()) { + char16_t ch = aList[indx + aElement.Length()]; + if (!nsCRT::IsAsciiSpace(ch) && ch != char16_t(',')) return false; + } + + return true; +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetControllers(nsIControllers** aResult) { + nsCOMPtr<nsPIWindowRoot> root = GetWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + return root->GetControllers(false /* for any window */, aResult); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::GetControllerForCommand(const char* aCommand, + nsIController** _retval) { + nsCOMPtr<nsPIWindowRoot> root = GetWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + return root->GetControllerForCommand(aCommand, false /* for any window */, + _retval); +} + +NS_IMETHODIMP +nsXULCommandDispatcher::Lock() { + // Since locking is used only as a performance optimization, we don't worry + // about nested lock calls. If that does happen, it just means we will unlock + // and process updates earlier. + mLocked = true; + return NS_OK; +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULCommandDispatcher::Unlock() { + if (mLocked) { + mLocked = false; + + // Handle any pending updates one at a time. In the unlikely case where a + // lock is added during the update, break out. + while (!mLocked && mPendingUpdates.Length() > 0) { + nsString name = mPendingUpdates.ElementAt(0); + mPendingUpdates.RemoveElementAt(0); + UpdateCommands(name); + } + } + + return NS_OK; +} diff --git a/dom/xul/nsXULCommandDispatcher.h b/dom/xul/nsXULCommandDispatcher.h new file mode 100644 index 0000000000..1f988c752e --- /dev/null +++ b/dom/xul/nsXULCommandDispatcher.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + This is the focus manager for XUL documents. + +*/ + +#ifndef nsXULCommandDispatcher_h__ +#define nsXULCommandDispatcher_h__ + +#include "nsCOMPtr.h" +#include "nsIDOMXULCommandDispatcher.h" +#include "nsWeakReference.h" +#include "nsString.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" + +class nsPIDOMWindowOuter; +class nsPIWindowRoot; + +namespace mozilla::dom { +class Document; +class Element; +} // namespace mozilla::dom + +class nsXULCommandDispatcher : public nsIDOMXULCommandDispatcher, + public nsSupportsWeakReference { + using Document = mozilla::dom::Document; + using Element = mozilla::dom::Element; + + public: + explicit nsXULCommandDispatcher(Document* aDocument); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULCommandDispatcher, + nsIDOMXULCommandDispatcher) + + // nsIDOMXULCommandDispatcher interface + NS_DECL_NSIDOMXULCOMMANDDISPATCHER + + void Disconnect(); + + protected: + virtual ~nsXULCommandDispatcher(); + + already_AddRefed<nsPIWindowRoot> GetWindowRoot(); + + Element* GetRootFocusedContentAndWindow(nsPIDOMWindowOuter** aWindow); + nsresult MoveFocusIntoSubtree(Element*, bool aForward); + + RefPtr<Document> mDocument; + + class Updater { + public: + Updater(Element* aElement, const nsAString& aEvents, + const nsAString& aTargets) + : mElement(aElement), + mEvents(aEvents), + mTargets(aTargets), + mNext(nullptr) {} + + RefPtr<Element> mElement; + nsString mEvents; + nsString mTargets; + Updater* mNext; + }; + + Updater* mUpdaters; + + bool Matches(const nsString& aList, const nsAString& aElement); + + bool mLocked; + nsTArray<nsString> mPendingUpdates; +}; + +#endif // nsXULCommandDispatcher_h__ diff --git a/dom/xul/nsXULContentSink.cpp b/dom/xul/nsXULContentSink.cpp new file mode 100644 index 0000000000..a4c15c63cd --- /dev/null +++ b/dom/xul/nsXULContentSink.cpp @@ -0,0 +1,869 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +/* + * An implementation for a Gecko-style content sink that knows how + * to build a content model (the "prototype" document) from XUL. + * + * For more information on XUL, + * see http://developer.mozilla.org/en/docs/XUL + */ + +#include "nsXULContentSink.h" + +#include "jsfriendapi.h" + +#include "nsCOMPtr.h" +#include "nsHTMLStyleSheet.h" +#include "nsIContentSink.h" +#include "mozilla/dom/Document.h" +#include "nsIFormControl.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsIScriptContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsNameSpaceManager.h" +#include "nsParserBase.h" +#include "nsViewManager.h" +#include "nsIScriptSecurityManager.h" +#include "nsLayoutCID.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsXULElement.h" +#include "mozilla/Logging.h" +#include "nsCRT.h" + +#include "nsXULPrototypeDocument.h" // XXXbe temporary +#include "mozilla/css/Loader.h" + +#include "nsUnicharUtils.h" +#include "nsGkAtoms.h" +#include "nsContentUtils.h" +#include "nsAttrName.h" +#include "nsXMLContentSink.h" +#include "nsIScriptError.h" +#include "nsContentTypeParser.h" + +static mozilla::LazyLogModule gContentSinkLog("nsXULContentSink"); + +using namespace mozilla; +using namespace mozilla::dom; +//---------------------------------------------------------------------- + +XULContentSinkImpl::ContextStack::ContextStack() : mTop(nullptr), mDepth(0) {} + +XULContentSinkImpl::ContextStack::~ContextStack() { + while (mTop) { + Entry* doomed = mTop; + mTop = mTop->mNext; + delete doomed; + } +} + +void XULContentSinkImpl::ContextStack::Push(RefPtr<nsXULPrototypeNode>&& aNode, + State aState) { + mTop = new Entry(std::move(aNode), aState, mTop); + ++mDepth; +} + +nsresult XULContentSinkImpl::ContextStack::Pop(State* aState) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + Entry* entry = mTop; + mTop = mTop->mNext; + --mDepth; + + *aState = entry->mState; + delete entry; + + return NS_OK; +} + +nsresult XULContentSinkImpl::ContextStack::GetTopNode( + RefPtr<nsXULPrototypeNode>& aNode) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + aNode = mTop->mNode; + return NS_OK; +} + +nsresult XULContentSinkImpl::ContextStack::GetTopChildren( + nsPrototypeArray** aChildren) { + if (mDepth == 0) return NS_ERROR_UNEXPECTED; + + *aChildren = &(mTop->mChildren); + return NS_OK; +} + +void XULContentSinkImpl::ContextStack::Clear() { + Entry* cur = mTop; + while (cur) { + // Release the root element (and its descendants). + Entry* next = cur->mNext; + delete cur; + cur = next; + } + + mTop = nullptr; + mDepth = 0; +} + +void XULContentSinkImpl::ContextStack::Traverse( + nsCycleCollectionTraversalCallback& aCb) { + nsCycleCollectionTraversalCallback& cb = aCb; + for (ContextStack::Entry* tmp = mTop; tmp; tmp = tmp->mNext) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildren) + } +} + +//---------------------------------------------------------------------- + +XULContentSinkImpl::XULContentSinkImpl() + : mText(nullptr), + mTextLength(0), + mTextSize(0), + mConstrainSize(true), + mState(eInProlog) {} + +XULContentSinkImpl::~XULContentSinkImpl() { + // The context stack _should_ be empty, unless something has gone wrong. + NS_ASSERTION(mContextStack.Depth() == 0, "Context stack not empty?"); + mContextStack.Clear(); + + free(mText); +} + +//---------------------------------------------------------------------- +// nsISupports interface + +NS_IMPL_CYCLE_COLLECTION_CLASS(XULContentSinkImpl) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(XULContentSinkImpl) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNodeInfoManager) + tmp->mContextStack.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrototype) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(XULContentSinkImpl) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNodeInfoManager) + tmp->mContextStack.Traverse(cb); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrototype) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULContentSinkImpl) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIXMLContentSink) + NS_INTERFACE_MAP_ENTRY(nsIXMLContentSink) + NS_INTERFACE_MAP_ENTRY(nsIExpatSink) + NS_INTERFACE_MAP_ENTRY(nsIContentSink) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(XULContentSinkImpl) +NS_IMPL_CYCLE_COLLECTING_RELEASE(XULContentSinkImpl) + +//---------------------------------------------------------------------- +// nsIContentSink interface + +NS_IMETHODIMP +XULContentSinkImpl::DidBuildModel(bool aTerminated) { + nsCOMPtr<Document> doc = do_QueryReferent(mDocument); + if (doc) { + mPrototype->NotifyLoadDone(); + mDocument = nullptr; + } + + // Drop our reference to the parser to get rid of a circular + // reference. + mParser = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::WillInterrupt(void) { + // XXX Notify the docshell, if necessary + return NS_OK; +} + +void XULContentSinkImpl::WillResume() { + // XXX Notify the docshell, if necessary +} + +NS_IMETHODIMP +XULContentSinkImpl::SetParser(nsParserBase* aParser) { + mParser = aParser; + return NS_OK; +} + +void XULContentSinkImpl::SetDocumentCharset( + NotNull<const Encoding*> aEncoding) { + nsCOMPtr<Document> doc = do_QueryReferent(mDocument); + if (doc) { + doc->SetDocumentCharacterSet(aEncoding); + } +} + +nsISupports* XULContentSinkImpl::GetTarget() { + nsCOMPtr<Document> doc = do_QueryReferent(mDocument); + return ToSupports(doc); +} + +//---------------------------------------------------------------------- + +nsresult XULContentSinkImpl::Init(Document* aDocument, + nsXULPrototypeDocument* aPrototype) { + MOZ_ASSERT(aDocument != nullptr, "null ptr"); + if (!aDocument) return NS_ERROR_NULL_POINTER; + + mDocument = do_GetWeakReference(aDocument); + mPrototype = aPrototype; + + mDocumentURL = mPrototype->GetURI(); + mNodeInfoManager = aPrototype->GetNodeInfoManager(); + if (!mNodeInfoManager) return NS_ERROR_UNEXPECTED; + + mState = eInProlog; + return NS_OK; +} + +//---------------------------------------------------------------------- +// +// Text buffering +// + +bool XULContentSinkImpl::IsDataInBuffer(char16_t* buffer, int32_t length) { + for (int32_t i = 0; i < length; ++i) { + if (buffer[i] == ' ' || buffer[i] == '\t' || buffer[i] == '\n' || + buffer[i] == '\r') + continue; + + return true; + } + return false; +} + +nsresult XULContentSinkImpl::FlushText(bool aCreateTextNode) { + nsresult rv; + + do { + // Don't do anything if there's no text to create a node from, or + // if they've told us not to create a text node + if (!mTextLength) break; + + if (!aCreateTextNode) break; + + RefPtr<nsXULPrototypeNode> node; + rv = mContextStack.GetTopNode(node); + if (NS_FAILED(rv)) return rv; + + bool stripWhitespace = false; + if (node->mType == nsXULPrototypeNode::eType_Element) { + mozilla::dom::NodeInfo* nodeInfo = + static_cast<nsXULPrototypeElement*>(node.get())->mNodeInfo; + + if (nodeInfo->NamespaceEquals(kNameSpaceID_XUL)) + stripWhitespace = !nodeInfo->Equals(nsGkAtoms::label) && + !nodeInfo->Equals(nsGkAtoms::description); + } + + // Don't bother if there's nothing but whitespace. + if (stripWhitespace && !IsDataInBuffer(mText, mTextLength)) break; + + // Don't bother if we're not in XUL document body + if (mState != eInDocumentElement || mContextStack.Depth() == 0) break; + + RefPtr<nsXULPrototypeText> text = new nsXULPrototypeText(); + text->mValue.Assign(mText, mTextLength); + if (stripWhitespace) text->mValue.Trim(" \t\n\r"); + + // hook it up + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) return rv; + + children->AppendElement(text.forget()); + } while (0); + + // Reset our text buffer + mTextLength = 0; + return NS_OK; +} + +//---------------------------------------------------------------------- + +nsresult XULContentSinkImpl::NormalizeAttributeString( + const char16_t* aExpatName, nsAttrName& aName) { + int32_t nameSpaceID; + RefPtr<nsAtom> prefix, localName; + nsContentUtils::SplitExpatName(aExpatName, getter_AddRefs(prefix), + getter_AddRefs(localName), &nameSpaceID); + + if (nameSpaceID == kNameSpaceID_None) { + aName.SetTo(localName); + + return NS_OK; + } + + RefPtr<mozilla::dom::NodeInfo> ni; + ni = mNodeInfoManager->GetNodeInfo(localName, prefix, nameSpaceID, + nsINode::ATTRIBUTE_NODE); + aName.SetTo(ni); + + return NS_OK; +} + +/**** BEGIN NEW APIs ****/ + +NS_IMETHODIMP +XULContentSinkImpl::HandleStartElement(const char16_t* aName, + const char16_t** aAtts, + uint32_t aAttsCount, + uint32_t aLineNumber, + uint32_t aColumnNumber) { + // XXX Hopefully the parser will flag this before we get here. If + // we're in the epilog, there should be no new elements + MOZ_ASSERT(mState != eInEpilog, "tag in XUL doc epilog"); + MOZ_ASSERT(aAttsCount % 2 == 0, "incorrect aAttsCount"); + + // Adjust aAttsCount so it's the actual number of attributes + aAttsCount /= 2; + + if (mState == eInEpilog) return NS_ERROR_UNEXPECTED; + + if (mState != eInScript) { + FlushText(); + } + + int32_t nameSpaceID; + RefPtr<nsAtom> prefix, localName; + nsContentUtils::SplitExpatName(aName, getter_AddRefs(prefix), + getter_AddRefs(localName), &nameSpaceID); + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo(localName, prefix, nameSpaceID, + nsINode::ELEMENT_NODE); + + nsresult rv = NS_OK; + switch (mState) { + case eInProlog: + // We're the root document element + rv = OpenRoot(aAtts, aAttsCount, nodeInfo); + break; + + case eInDocumentElement: + rv = OpenTag(aAtts, aAttsCount, aLineNumber, nodeInfo); + break; + + case eInEpilog: + case eInScript: + MOZ_LOG( + gContentSinkLog, LogLevel::Warning, + ("xul: warning: unexpected tags in epilog at line %d", aLineNumber)); + rv = NS_ERROR_UNEXPECTED; // XXX + break; + } + + return rv; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleEndElement(const char16_t* aName) { + // Never EVER return anything but NS_OK or + // NS_ERROR_HTMLPARSER_BLOCK from this method. Doing so will blow + // the parser's little mind all over the planet. + nsresult rv; + + RefPtr<nsXULPrototypeNode> node; + rv = mContextStack.GetTopNode(node); + + if (NS_FAILED(rv)) { + return NS_OK; + } + + switch (node->mType) { + case nsXULPrototypeNode::eType_Element: { + // Flush any text _now_, so that we'll get text nodes created + // before popping the stack. + FlushText(); + + // Pop the context stack and do prototype hookup. + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) return rv; + + nsXULPrototypeElement* element = + static_cast<nsXULPrototypeElement*>(node.get()); + + int32_t count = children->Length(); + if (count) { + element->mChildren.SetCapacity(count); + + for (int32_t i = 0; i < count; ++i) + element->mChildren.AppendElement(children->ElementAt(i)); + } + } break; + + case nsXULPrototypeNode::eType_Script: { + nsXULPrototypeScript* script = + static_cast<nsXULPrototypeScript*>(node.get()); + + // If given a src= attribute, we must ignore script tag content. + if (!script->mSrcURI && !script->HasStencil()) { + nsCOMPtr<Document> doc = do_QueryReferent(mDocument); + + script->mOutOfLine = false; + if (doc) { + script->Compile(mText, mTextLength, JS::SourceOwnership::Borrowed, + mDocumentURL, script->mLineNo, doc); + } + } + + FlushText(false); + } break; + + default: + NS_ERROR("didn't expect that"); + break; + } + + rv = mContextStack.Pop(&mState); + NS_ASSERTION(NS_SUCCEEDED(rv), "context stack corrupted"); + if (NS_FAILED(rv)) return rv; + + if (mContextStack.Depth() == 0) { + // The root element should -always- be an element, because + // it'll have been created via XULContentSinkImpl::OpenRoot(). + NS_ASSERTION(node->mType == nsXULPrototypeNode::eType_Element, + "root is not an element"); + if (node->mType != nsXULPrototypeNode::eType_Element) + return NS_ERROR_UNEXPECTED; + + // Now that we're done parsing, set the prototype document's + // root element. This transfers ownership of the prototype + // element tree to the prototype document. + nsXULPrototypeElement* element = + static_cast<nsXULPrototypeElement*>(node.get()); + + mPrototype->SetRootElement(element); + mState = eInEpilog; + } + + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleComment(const char16_t* aName) { + FlushText(); + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleCDataSection(const char16_t* aData, + uint32_t aLength) { + FlushText(); + return AddText(aData, aLength); +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleDoctypeDecl(const nsAString& aSubset, + const nsAString& aName, + const nsAString& aSystemId, + const nsAString& aPublicId, + nsISupports* aCatalogData) { + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleCharacterData(const char16_t* aData, + uint32_t aLength) { + if (aData && mState != eInProlog && mState != eInEpilog) { + return AddText(aData, aLength); + } + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleProcessingInstruction(const char16_t* aTarget, + const char16_t* aData) { + FlushText(); + + const nsDependentString target(aTarget); + const nsDependentString data(aData); + + // Note: the created nsXULPrototypePI has mRefCnt == 1 + RefPtr<nsXULPrototypePI> pi = new nsXULPrototypePI(); + pi->mTarget = target; + pi->mData = data; + + if (mState == eInProlog) { + // Note: passing in already addrefed pi + return mPrototype->AddProcessingInstruction(pi); + } + + nsresult rv; + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + children->AppendElement(pi); + + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::HandleXMLDeclaration(const char16_t* aVersion, + const char16_t* aEncoding, + int32_t aStandalone) { + return NS_OK; +} + +NS_IMETHODIMP +XULContentSinkImpl::ReportError(const char16_t* aErrorText, + const char16_t* aSourceText, + nsIScriptError* aError, bool* _retval) { + MOZ_ASSERT(aError && aSourceText && aErrorText, "Check arguments!!!"); + + // The expat driver should report the error. + *_retval = true; + + nsresult rv = NS_OK; + + // make sure to empty the context stack so that + // <parsererror> could become the root element. + mContextStack.Clear(); + + mState = eInProlog; + + // Clear any buffered-up text we have. It's enough to set the length to 0. + // The buffer itself is allocated when we're created and deleted in our + // destructor, so don't mess with it. + mTextLength = 0; + + // return leaving the document empty if we're asked to not add a <parsererror> + // root node + nsCOMPtr<Document> idoc = do_QueryReferent(mDocument); + if (idoc && idoc->SuppressParserErrorElement()) { + return NS_OK; + }; + + const char16_t* noAtts[] = {0, 0}; + + constexpr auto errorNs = + u"http://www.mozilla.org/newlayout/xml/parsererror.xml"_ns; + + nsAutoString parsererror(errorNs); + parsererror.Append((char16_t)0xFFFF); + parsererror.AppendLiteral("parsererror"); + + rv = HandleStartElement(parsererror.get(), noAtts, 0, 0, 0); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleCharacterData(aErrorText, NS_strlen(aErrorText)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString sourcetext(errorNs); + sourcetext.Append((char16_t)0xFFFF); + sourcetext.AppendLiteral("sourcetext"); + + rv = HandleStartElement(sourcetext.get(), noAtts, 0, 0, 0); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleCharacterData(aSourceText, NS_strlen(aSourceText)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleEndElement(sourcetext.get()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = HandleEndElement(parsererror.get()); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult XULContentSinkImpl::OpenRoot(const char16_t** aAttributes, + const uint32_t aAttrLen, + mozilla::dom::NodeInfo* aNodeInfo) { + NS_ASSERTION(mState == eInProlog, "how'd we get here?"); + if (mState != eInProlog) return NS_ERROR_UNEXPECTED; + + if (aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XHTML) || + aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XUL)) { + MOZ_LOG(gContentSinkLog, LogLevel::Error, + ("xul: script tag not allowed as root content element")); + + return NS_ERROR_UNEXPECTED; + } + + // Create the element + RefPtr<nsXULPrototypeElement> element = new nsXULPrototypeElement(aNodeInfo); + + // Add the attributes + nsresult rv = AddAttributes(aAttributes, aAttrLen, element); + if (NS_FAILED(rv)) return rv; + + // Push the element onto the context stack, so that child + // containers will hook up to us as their parent. + mContextStack.Push(std::move(element), mState); + + mState = eInDocumentElement; + return NS_OK; +} + +nsresult XULContentSinkImpl::OpenTag(const char16_t** aAttributes, + const uint32_t aAttrLen, + const uint32_t aLineNumber, + mozilla::dom::NodeInfo* aNodeInfo) { + // Create the element + RefPtr<nsXULPrototypeElement> element = new nsXULPrototypeElement(aNodeInfo); + + // Link this element to its parent. + nsPrototypeArray* children = nullptr; + nsresult rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + // Add the attributes + rv = AddAttributes(aAttributes, aAttrLen, element); + if (NS_FAILED(rv)) return rv; + + children->AppendElement(element); + + if (aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XHTML) || + aNodeInfo->Equals(nsGkAtoms::script, kNameSpaceID_XUL)) { + // Do scripty things now + rv = OpenScript(aAttributes, aLineNumber); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(mState == eInScript || mState == eInDocumentElement, + "Unexpected state"); + if (mState == eInScript) { + // OpenScript has pushed the nsPrototypeScriptElement onto the + // stack, so we're done. + return NS_OK; + } + } + + // Push the element onto the context stack, so that child + // containers will hook up to us as their parent. + mContextStack.Push(std::move(element), mState); + + mState = eInDocumentElement; + return NS_OK; +} + +nsresult XULContentSinkImpl::OpenScript(const char16_t** aAttributes, + const uint32_t aLineNumber) { + bool isJavaScript = true; + nsresult rv; + + // Look for SRC attribute and look for a LANGUAGE attribute + nsAutoString src; + while (*aAttributes) { + const nsDependentString key(aAttributes[0]); + if (key.EqualsLiteral("src")) { + src.Assign(aAttributes[1]); + } else if (key.EqualsLiteral("type")) { + nsDependentString str(aAttributes[1]); + nsContentTypeParser parser(str); + nsAutoString mimeType; + rv = parser.GetType(mimeType); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_INVALID_ARG) { + // Fail immediately rather than checking if later things + // are okay. + return NS_OK; + } + // We do want the warning here + NS_ENSURE_SUCCESS(rv, rv); + } + + // NOTE(emilio): Module scripts don't pass this test, aren't cached yet. + // If they become cached, then we need to tweak + // PrototypeDocumentContentSink and remove the special cases there. + if (nsContentUtils::IsJavascriptMIMEType(mimeType)) { + isJavaScript = true; + + // Get the version string, and ensure that JavaScript supports it. + nsAutoString versionName; + rv = parser.GetParameter("version", versionName); + + if (NS_SUCCEEDED(rv)) { + nsContentUtils::ReportToConsoleNonLocalized( + u"Versioned JavaScripts are no longer supported. " + "Please remove the version parameter."_ns, + nsIScriptError::errorFlag, "XUL Document"_ns, nullptr, + mDocumentURL, u""_ns, aLineNumber); + isJavaScript = false; + } else if (rv != NS_ERROR_INVALID_ARG) { + return rv; + } + } else { + isJavaScript = false; + } + } else if (key.EqualsLiteral("language")) { + // Language is deprecated, and the impl in ScriptLoader ignores the + // various version strings anyway. So we make no attempt to support + // languages other than JS for language= + nsAutoString lang(aAttributes[1]); + if (nsContentUtils::IsJavaScriptLanguage(lang)) { + isJavaScript = true; + } + } + aAttributes += 2; + } + + // Don't process scripts that aren't JavaScript. + if (!isJavaScript) { + return NS_OK; + } + + nsCOMPtr<Document> doc(do_QueryReferent(mDocument)); + nsCOMPtr<nsIScriptGlobalObject> globalObject; + if (doc) globalObject = do_QueryInterface(doc->GetWindow()); + RefPtr<nsXULPrototypeScript> script = new nsXULPrototypeScript(aLineNumber); + + // If there is a SRC attribute... + if (!src.IsEmpty()) { + // Use the SRC attribute value to load the URL + rv = NS_NewURI(getter_AddRefs(script->mSrcURI), src, nullptr, mDocumentURL); + + // Check if this document is allowed to load a script from this source + // NOTE: if we ever allow scripts added via the DOM to run, we need to + // add a CheckLoadURI call for that as well. + if (NS_SUCCEEDED(rv)) { + if (!mSecMan) + mSecMan = do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<Document> doc = do_QueryReferent(mDocument, &rv); + + if (NS_SUCCEEDED(rv)) { + rv = mSecMan->CheckLoadURIWithPrincipal( + doc->NodePrincipal(), script->mSrcURI, + nsIScriptSecurityManager::ALLOW_CHROME, doc->InnerWindowID()); + } + } + } + + if (NS_FAILED(rv)) { + return rv; + } + + // Attempt to deserialize an out-of-line script from the FastLoad + // file right away. Otherwise we'll end up reloading the script and + // corrupting the FastLoad file trying to serialize it, in the case + // where it's already there. + script->DeserializeOutOfLine(nullptr, mPrototype); + } + + nsPrototypeArray* children = nullptr; + rv = mContextStack.GetTopChildren(&children); + if (NS_FAILED(rv)) { + return rv; + } + + children->AppendElement(script); + + mConstrainSize = false; + + mContextStack.Push(script, mState); + mState = eInScript; + + return NS_OK; +} + +nsresult XULContentSinkImpl::AddAttributes(const char16_t** aAttributes, + const uint32_t aAttrLen, + nsXULPrototypeElement* aElement) { + // Add tag attributes to the element + nsresult rv; + + // Create storage for the attributes + nsXULPrototypeAttribute* attrs = nullptr; + if (aAttrLen > 0) { + attrs = aElement->mAttributes.AppendElements(aAttrLen); + } + + // Copy the attributes into the prototype + uint32_t i; + for (i = 0; i < aAttrLen; ++i) { + rv = NormalizeAttributeString(aAttributes[i * 2], attrs[i].mName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aElement->SetAttrAt(i, nsDependentString(aAttributes[i * 2 + 1]), + mDocumentURL); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gContentSinkLog, LogLevel::Debug)) { + nsAutoString extraWhiteSpace; + int32_t cnt = mContextStack.Depth(); + while (--cnt >= 0) extraWhiteSpace.AppendLiteral(" "); + nsAutoString qnameC, valueC; + qnameC.Assign(aAttributes[0]); + valueC.Assign(aAttributes[1]); + MOZ_LOG(gContentSinkLog, LogLevel::Debug, + ("xul: %.5d. %s %s=%s", + -1, // XXX pass in line number + NS_ConvertUTF16toUTF8(extraWhiteSpace).get(), + NS_ConvertUTF16toUTF8(qnameC).get(), + NS_ConvertUTF16toUTF8(valueC).get())); + } + } + + return NS_OK; +} + +nsresult XULContentSinkImpl::AddText(const char16_t* aText, int32_t aLength) { + // Create buffer when we first need it + if (0 == mTextSize) { + mText = (char16_t*)malloc(sizeof(char16_t) * 4096); + if (nullptr == mText) { + return NS_ERROR_OUT_OF_MEMORY; + } + mTextSize = 4096; + } + + // Copy data from string into our buffer; flush buffer when it fills up + int32_t offset = 0; + while (0 != aLength) { + int32_t amount = mTextSize - mTextLength; + if (amount > aLength) { + amount = aLength; + } + if (0 == amount) { + if (mConstrainSize) { + nsresult rv = FlushText(); + if (NS_OK != rv) { + return rv; + } + } else { + CheckedInt32 size = mTextSize; + size += aLength; + if (!size.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + mTextSize = size.value(); + + mText = (char16_t*)realloc(mText, sizeof(char16_t) * mTextSize); + if (nullptr == mText) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + memcpy(&mText[mTextLength], aText + offset, sizeof(char16_t) * amount); + + mTextLength += amount; + offset += amount; + aLength -= amount; + } + + return NS_OK; +} diff --git a/dom/xul/nsXULContentSink.h b/dom/xul/nsXULContentSink.h new file mode 100644 index 0000000000..0538c013b4 --- /dev/null +++ b/dom/xul/nsXULContentSink.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsXULContentSink_h__ +#define nsXULContentSink_h__ + +#include "mozilla/Attributes.h" +#include "nsIExpatSink.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIXMLContentSink.h" +#include "nsNodeInfoManager.h" +#include "nsXULElement.h" +#include "nsIDTD.h" + +class nsIScriptSecurityManager; +class nsAttrName; +class nsXULPrototypeDocument; +class nsXULPrototypeElement; +class nsXULPrototypeNode; + +class XULContentSinkImpl final : public nsIXMLContentSink, public nsIExpatSink { + public: + XULContentSinkImpl(); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIEXPATSINK + + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(XULContentSinkImpl, + nsIXMLContentSink) + + // nsIContentSink + NS_IMETHOD WillParse(void) override { return NS_OK; } + NS_IMETHOD DidBuildModel(bool aTerminated) override; + NS_IMETHOD WillInterrupt(void) override; + void WillResume() override; + NS_IMETHOD SetParser(nsParserBase* aParser) override; + virtual void FlushPendingNotifications(mozilla::FlushType aType) override {} + virtual void SetDocumentCharset(NotNull<const Encoding*> aEncoding) override; + virtual nsISupports* GetTarget() override; + + /** + * Initialize the content sink, giving it a document with which to communicate + * with the outside world, and an nsXULPrototypeDocument to build. + */ + nsresult Init(mozilla::dom::Document* aDocument, + nsXULPrototypeDocument* aPrototype); + + protected: + virtual ~XULContentSinkImpl(); + + // pseudo-constants + char16_t* mText; + int32_t mTextLength; + int32_t mTextSize; + bool mConstrainSize; + + nsresult AddAttributes(const char16_t** aAttributes, const uint32_t aAttrLen, + nsXULPrototypeElement* aElement); + + nsresult OpenRoot(const char16_t** aAttributes, const uint32_t aAttrLen, + mozilla::dom::NodeInfo* aNodeInfo); + + nsresult OpenTag(const char16_t** aAttributes, const uint32_t aAttrLen, + const uint32_t aLineNumber, + mozilla::dom::NodeInfo* aNodeInfo); + + // If OpenScript returns NS_OK and after it returns our state is eInScript, + // that means that we created a prototype script and stuck it on + // mContextStack. If NS_OK is returned but the state is still + // eInDocumentElement then we didn't create a prototype script (e.g. the + // script had an unknown type), and the caller should create a prototype + // element. + nsresult OpenScript(const char16_t** aAttributes, const uint32_t aLineNumber); + + static bool IsDataInBuffer(char16_t* aBuffer, int32_t aLength); + + // Text management + nsresult FlushText(bool aCreateTextNode = true); + nsresult AddText(const char16_t* aText, int32_t aLength); + + RefPtr<nsNodeInfoManager> mNodeInfoManager; + + nsresult NormalizeAttributeString(const char16_t* aExpatName, + nsAttrName& aName); + + public: + enum State { eInProlog, eInDocumentElement, eInScript, eInEpilog }; + + protected: + State mState; + + // content stack management + class ContextStack { + protected: + struct Entry { + RefPtr<nsXULPrototypeNode> mNode; + // a LOT of nodes have children; preallocate for 8 + nsPrototypeArray mChildren; + State mState; + Entry* mNext; + Entry(RefPtr<nsXULPrototypeNode>&& aNode, State aState, Entry* aNext) + : mNode(std::move(aNode)), + mChildren(8), + mState(aState), + mNext(aNext) {} + }; + + Entry* mTop; + int32_t mDepth; + + public: + ContextStack(); + ~ContextStack(); + + int32_t Depth() { return mDepth; } + + void Push(RefPtr<nsXULPrototypeNode>&& aNode, State aState); + nsresult Pop(State* aState); + + nsresult GetTopNode(RefPtr<nsXULPrototypeNode>& aNode); + nsresult GetTopChildren(nsPrototypeArray** aChildren); + + void Clear(); + + void Traverse(nsCycleCollectionTraversalCallback& aCallback); + }; + + friend class ContextStack; + ContextStack mContextStack; + + nsWeakPtr mDocument; // [OWNER] + nsCOMPtr<nsIURI> mDocumentURL; // [OWNER] + + RefPtr<nsXULPrototypeDocument> mPrototype; // [OWNER] + + RefPtr<nsParserBase> mParser; + nsCOMPtr<nsIScriptSecurityManager> mSecMan; +}; + +#endif /* nsXULContentSink_h__ */ diff --git a/dom/xul/nsXULContentUtils.cpp b/dom/xul/nsXULContentUtils.cpp new file mode 100644 index 0000000000..109aa1975a --- /dev/null +++ b/dom/xul/nsXULContentUtils.cpp @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +/* + + A package of routines shared by the XUL content code. + + */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/intl/Collator.h" + +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsXULContentUtils.h" +#include "nsLayoutCID.h" +#include "nsString.h" +#include "nsGkAtoms.h" + +using namespace mozilla; + +//------------------------------------------------------------------------ + +const mozilla::intl::Collator* nsXULContentUtils::gCollator; + +//------------------------------------------------------------------------ +// Constructors n' stuff +// + +nsresult nsXULContentUtils::Finish() { + if (gCollator) { + delete gCollator; + gCollator = nullptr; + } + + return NS_OK; +} + +const mozilla::intl::Collator* nsXULContentUtils::GetCollator() { + if (!gCollator) { + // Lazily initialize the Collator. + auto result = mozilla::intl::LocaleService::TryCreateComponent< + mozilla::intl::Collator>(); + if (result.isErr()) { + NS_ERROR("couldn't create a mozilla::intl::Collator"); + return nullptr; + } + + auto collator = result.unwrap(); + + // Sort in a case-insensitive way, where "base" letters are considered + // equal, e.g: a = á, a = A, a ≠b. + mozilla::intl::Collator::Options options{}; + options.sensitivity = mozilla::intl::Collator::Sensitivity::Base; + auto optResult = collator->SetOptions(options); + if (optResult.isErr()) { + NS_ERROR("couldn't set options for mozilla::intl::Collator"); + return nullptr; + } + gCollator = collator.release(); + } + + return gCollator; +} + +//------------------------------------------------------------------------ +// + +nsresult nsXULContentUtils::FindChildByTag(nsIContent* aElement, + int32_t aNameSpaceID, nsAtom* aTag, + mozilla::dom::Element** aResult) { + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsElement() && child->NodeInfo()->Equals(aTag, aNameSpaceID)) { + NS_ADDREF(*aResult = child->AsElement()); + return NS_OK; + } + } + + *aResult = nullptr; + return NS_RDF_NO_VALUE; // not found +} diff --git a/dom/xul/nsXULContentUtils.h b/dom/xul/nsXULContentUtils.h new file mode 100644 index 0000000000..c996bcec2b --- /dev/null +++ b/dom/xul/nsXULContentUtils.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + A package of routines shared by the XUL content code. + + */ + +#ifndef nsXULContentUtils_h__ +#define nsXULContentUtils_h__ + +#include "nsISupports.h" + +class nsAtom; +class nsIContent; + +namespace mozilla::dom { +class Element; +} +namespace mozilla::intl { +class Collator; +} + +class nsXULContentUtils { + protected: + const static mozilla::intl::Collator* gCollator; + + static bool gDisableXULCache; + + static int DisableXULCacheChangedCallback(const char* aPrefName, + void* aClosure); + + public: + static nsresult Finish(); + + static nsresult FindChildByTag(nsIContent* aElement, int32_t aNameSpaceID, + nsAtom* aTag, mozilla::dom::Element** aResult); + + static const mozilla::intl::Collator* GetCollator(); +}; + +#endif // nsXULContentUtils_h__ diff --git a/dom/xul/nsXULControllers.cpp b/dom/xul/nsXULControllers.cpp new file mode 100644 index 0000000000..3e96fd6e41 --- /dev/null +++ b/dom/xul/nsXULControllers.cpp @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + This file provides the implementation for the XUL "controllers" + object. + +*/ + +#include "nsString.h" + +#include "nsIControllers.h" +#include "nsXULControllers.h" +#include "nsIController.h" +#include "mozilla/RefPtr.h" + +//---------------------------------------------------------------------- + +nsXULControllerData::nsXULControllerData(uint32_t inControllerID, + nsIController* inController) + : mControllerID(inControllerID), mController(inController) {} + +nsresult nsXULControllerData::GetController(nsIController** outController) { + NS_IF_ADDREF(*outController = mController); + return NS_OK; +} + +nsXULControllers::nsXULControllers() : mCurControllerID(0) {} + +nsXULControllers::~nsXULControllers(void) { DeleteControllers(); } + +void nsXULControllers::DeleteControllers() { + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + delete controllerData; // releases the nsIController + } + + mControllers.Clear(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULControllers) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULControllers) + tmp->DeleteControllers(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULControllers) + { + uint32_t i, count = tmp->mControllers.Length(); + for (i = 0; i < count; ++i) { + nsXULControllerData* controllerData = tmp->mControllers[i]; + if (controllerData) { + cb.NoteXPCOMChild(controllerData->mController); + } + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULControllers) + NS_INTERFACE_MAP_ENTRY(nsIControllers) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIControllers) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULControllers) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULControllers) + +NS_IMETHODIMP +nsXULControllers::GetControllerForCommand(const char* aCommand, + nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr<nsIController> controller; + controllerData->GetController(getter_AddRefs(controller)); + if (controller) { + bool supportsCommand; + controller->SupportsCommand(aCommand, &supportsCommand); + if (supportsCommand) { + controller.forget(_retval); + return NS_OK; + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::InsertControllerAt(uint32_t aIndex, + nsIController* controller) { + nsXULControllerData* controllerData = + new nsXULControllerData(++mCurControllerID, controller); +#ifdef DEBUG + nsXULControllerData** inserted = +#endif + mControllers.InsertElementAt(aIndex, controllerData); + NS_ASSERTION(inserted != nullptr, "Insertion of controller failed"); + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::RemoveControllerAt(uint32_t aIndex, nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsXULControllerData* controllerData = mControllers.SafeElementAt(aIndex); + if (!controllerData) return NS_ERROR_FAILURE; + + mControllers.RemoveElementAt(aIndex); + + controllerData->GetController(_retval); + delete controllerData; + + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::GetControllerAt(uint32_t aIndex, nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = nullptr; + + nsXULControllerData* controllerData = mControllers.SafeElementAt(aIndex); + if (!controllerData) return NS_ERROR_FAILURE; + + return controllerData->GetController(_retval); // does the addref +} + +NS_IMETHODIMP +nsXULControllers::AppendController(nsIController* controller) { + // This assigns controller IDs starting at 1 so we can use 0 to test if an ID + // was obtained + nsXULControllerData* controllerData = + new nsXULControllerData(++mCurControllerID, controller); + +#ifdef DEBUG + nsXULControllerData** appended = +#endif + mControllers.AppendElement(controllerData); + NS_ASSERTION(appended != nullptr, "Appending controller failed"); + return NS_OK; +} + +NS_IMETHODIMP +nsXULControllers::RemoveController(nsIController* controller) { + // first get the identity pointer + nsCOMPtr<nsISupports> controllerSup(do_QueryInterface(controller)); + // then find it + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr<nsIController> thisController; + controllerData->GetController(getter_AddRefs(thisController)); + nsCOMPtr<nsISupports> thisControllerSup( + do_QueryInterface(thisController)); // get identity + if (thisControllerSup == controllerSup) { + mControllers.RemoveElementAt(i); + delete controllerData; + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; // right thing to return if no controller found? +} + +NS_IMETHODIMP +nsXULControllers::GetControllerId(nsIController* controller, + uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData) { + nsCOMPtr<nsIController> thisController; + controllerData->GetController(getter_AddRefs(thisController)); + if (thisController.get() == controller) { + *_retval = controllerData->GetControllerID(); + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; // none found +} + +NS_IMETHODIMP +nsXULControllers::GetControllerById(uint32_t controllerID, + nsIController** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + uint32_t count = mControllers.Length(); + for (uint32_t i = 0; i < count; i++) { + nsXULControllerData* controllerData = mControllers.ElementAt(i); + if (controllerData && controllerData->GetControllerID() == controllerID) { + return controllerData->GetController(_retval); + } + } + return NS_ERROR_FAILURE; // none found +} + +NS_IMETHODIMP +nsXULControllers::GetControllerCount(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mControllers.Length(); + return NS_OK; +} diff --git a/dom/xul/nsXULControllers.h b/dom/xul/nsXULControllers.h new file mode 100644 index 0000000000..495cf64e71 --- /dev/null +++ b/dom/xul/nsXULControllers.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + The XUL "controllers" object. + +*/ + +#ifndef nsXULControllers_h__ +#define nsXULControllers_h__ + +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIControllers.h" +#include "nsCycleCollectionParticipant.h" + +/* non-XPCOM class for holding controllers and their IDs */ +class nsXULControllerData final { + public: + nsXULControllerData(uint32_t inControllerID, nsIController* inController); + ~nsXULControllerData() = default; + + uint32_t GetControllerID() { return mControllerID; } + + nsresult GetController(nsIController** outController); + + uint32_t mControllerID; + nsCOMPtr<nsIController> mController; +}; + +class nsXULControllers final : public nsIControllers { + public: + nsXULControllers(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULControllers, nsIControllers) + NS_DECL_NSICONTROLLERS + + protected: + virtual ~nsXULControllers(void); + + void DeleteControllers(); + + nsTArray<nsXULControllerData*> mControllers; + uint32_t mCurControllerID; +}; + +#endif // nsXULControllers_h__ diff --git a/dom/xul/nsXULElement.cpp b/dom/xul/nsXULElement.cpp new file mode 100644 index 0000000000..d885cc526e --- /dev/null +++ b/dom/xul/nsXULElement.cpp @@ -0,0 +1,2027 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsXULElement.h" + +#include <new> +#include <utility> +#include "AttrArray.h" +#include "MainThreadUtils.h" +#include "ReferrerInfo.h" +#include "Units.h" +#include "XULButtonElement.h" +#include "XULFrameElement.h" +#include "XULMenuElement.h" +#include "XULPopupElement.h" +#include "XULResizerElement.h" +#include "XULTextElement.h" +#include "XULTooltipElement.h" +#include "XULTreeElement.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/experimental/JSStencil.h" +#include "js/OffThreadScriptCompilation.h" +#include "js/SourceText.h" +#include "js/Transcoding.h" +#include "js/Utility.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/ArrayIterator.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/FlushType.h" +#include "mozilla/GlobalKeyListener.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/PresShell.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/URLExtraData.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/BorrowedAttrInfo.h" +#include "mozilla/dom/CSSRuleBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/XULBroadcastManager.h" +#include "mozilla/dom/XULCommandEvent.h" +#include "mozilla/dom/XULElementBinding.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/fallible.h" +#include "nsAtom.h" +#include "nsAttrValueInlines.h" +#include "nsAttrValueOrString.h" +#include "nsCaseTreatment.h" +#include "nsChangeHint.h" +#include "nsCOMPtr.h" +#include "nsCompatibility.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionNoteChild.h" +#include "nsCycleCollectionTraversalCallback.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIControllers.h" +#include "nsID.h" +#include "nsIDOMEventListener.h" +#include "nsIDOMXULControlElement.h" +#include "nsIDOMXULSelectCntrlItemEl.h" +#include "nsIDocShell.h" +#include "nsIFocusManager.h" +#include "nsIFrame.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIRunnable.h" +#include "nsIScriptContext.h" +#include "nsISupportsUtils.h" +#include "nsIURI.h" +#include "nsIXPConnect.h" +#include "nsMenuPopupFrame.h" +#include "nsNodeInfoManager.h" +#include "nsPIDOMWindow.h" +#include "nsPIDOMWindowInlines.h" +#include "nsPresContext.h" +#include "nsQueryFrame.h" +#include "nsString.h" +#include "nsStyledElement.h" +#include "nsThreadUtils.h" +#include "nsXULControllers.h" +#include "nsXULPopupListener.h" +#include "nsXULPopupManager.h" +#include "nsXULPrototypeCache.h" +#include "nsXULTooltipListener.h" +#include "xpcpublic.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#ifdef XUL_PROTOTYPE_ATTRIBUTE_METERING +uint32_t nsXULPrototypeAttribute::gNumElements; +uint32_t nsXULPrototypeAttribute::gNumAttributes; +uint32_t nsXULPrototypeAttribute::gNumCacheTests; +uint32_t nsXULPrototypeAttribute::gNumCacheHits; +uint32_t nsXULPrototypeAttribute::gNumCacheSets; +uint32_t nsXULPrototypeAttribute::gNumCacheFills; +#endif + +#define NS_DISPATCH_XUL_COMMAND (1 << 0) + +//---------------------------------------------------------------------- +// nsXULElement +// + +nsXULElement::nsXULElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsStyledElement(std::move(aNodeInfo)) { + XUL_PROTOTYPE_ATTRIBUTE_METER(gNumElements); +} + +nsXULElement::~nsXULElement() = default; + +/* static */ +nsXULElement* NS_NewBasicXULElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(std::move(aNodeInfo)); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) nsXULElement(nodeInfo.forget()); +} + +/* static */ +nsXULElement* nsXULElement::Construct( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + // NOTE: If you add elements here, you probably also want to change + // mozilla::dom::binding_detail::HTMLConstructor in BindingUtils.cpp to take + // them into account, otherwise you'll start getting "Illegal constructor" + // exceptions in chrome code. + RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo; + if (nodeInfo->Equals(nsGkAtoms::resizer)) { + return NS_NewXULResizerElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::label) || + nodeInfo->Equals(nsGkAtoms::description)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULTextElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menupopup) || + nodeInfo->Equals(nsGkAtoms::popup) || + nodeInfo->Equals(nsGkAtoms::panel)) { + return NS_NewXULPopupElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::tooltip)) { + return NS_NewXULTooltipElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::iframe) || + nodeInfo->Equals(nsGkAtoms::browser) || + nodeInfo->Equals(nsGkAtoms::editor)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULFrameElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menubar)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULMenuParentElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::menu) || + nodeInfo->Equals(nsGkAtoms::menulist)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULMenuElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::tree)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULTreeElement(nodeInfo.forget()); + } + + if (nodeInfo->Equals(nsGkAtoms::checkbox) || + nodeInfo->Equals(nsGkAtoms::radio) || + nodeInfo->Equals(nsGkAtoms::thumb) || + nodeInfo->Equals(nsGkAtoms::button) || + nodeInfo->Equals(nsGkAtoms::menuitem) || + nodeInfo->Equals(nsGkAtoms::toolbarbutton) || + nodeInfo->Equals(nsGkAtoms::toolbarpaletteitem) || + nodeInfo->Equals(nsGkAtoms::scrollbarbutton)) { + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) XULButtonElement(nodeInfo.forget()); + } + + return NS_NewBasicXULElement(nodeInfo.forget()); +} + +/* static */ +already_AddRefed<nsXULElement> nsXULElement::CreateFromPrototype( + nsXULPrototypeElement* aPrototype, mozilla::dom::NodeInfo* aNodeInfo, + bool aIsScriptable, bool aIsRoot) { + RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo; + nsCOMPtr<Element> baseElement; + NS_NewXULElement(getter_AddRefs(baseElement), ni.forget(), + dom::FROM_PARSER_NETWORK, aPrototype->mIsAtom); + + if (baseElement) { + nsXULElement* element = FromNode(baseElement); + + if (aPrototype->mHasIdAttribute) { + element->SetHasID(); + } + if (aPrototype->mHasClassAttribute) { + element->SetMayHaveClass(); + } + if (aPrototype->mHasStyleAttribute) { + element->SetMayHaveStyle(); + } + + element->MakeHeavyweight(aPrototype); + if (aIsScriptable) { + // Check each attribute on the prototype to see if we need to do + // any additional processing and hookup that would otherwise be + // done 'automagically' by SetAttr(). + for (const auto& attribute : aPrototype->mAttributes) { + element->AddListenerForAttributeIfNeeded(attribute.mName); + } + } + + return baseElement.forget().downcast<nsXULElement>(); + } + + return nullptr; +} + +nsresult nsXULElement::CreateFromPrototype(nsXULPrototypeElement* aPrototype, + Document* aDocument, + bool aIsScriptable, bool aIsRoot, + Element** aResult) { + // Create an nsXULElement from a prototype + MOZ_ASSERT(aPrototype != nullptr, "null ptr"); + if (!aPrototype) return NS_ERROR_NULL_POINTER; + + MOZ_ASSERT(aResult != nullptr, "null ptr"); + if (!aResult) return NS_ERROR_NULL_POINTER; + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + if (aDocument) { + mozilla::dom::NodeInfo* ni = aPrototype->mNodeInfo; + nodeInfo = aDocument->NodeInfoManager()->GetNodeInfo( + ni->NameAtom(), ni->GetPrefixAtom(), ni->NamespaceID(), ELEMENT_NODE); + } else { + nodeInfo = aPrototype->mNodeInfo; + } + + RefPtr<nsXULElement> element = + CreateFromPrototype(aPrototype, nodeInfo, aIsScriptable, aIsRoot); + element.forget(aResult); + + return NS_OK; +} + +nsresult NS_NewXULElement(Element** aResult, + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, nsAtom* aIsAtom, + mozilla::dom::CustomElementDefinition* aDefinition) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo; + + MOZ_ASSERT(nodeInfo, "need nodeinfo for non-proto Create"); + + NS_ASSERTION( + nodeInfo->NamespaceEquals(kNameSpaceID_XUL), + "Trying to create XUL elements that don't have the XUL namespace"); + + Document* doc = nodeInfo->GetDocument(); + if (doc && !doc->AllowXULXBL()) { + return NS_ERROR_NOT_AVAILABLE; + } + + return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, + aIsAtom, aDefinition); +} + +void NS_TrustedNewXULElement( + Element** aResult, already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { + RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo; + MOZ_ASSERT(ni, "need nodeinfo for non-proto Create"); + + // Create an nsXULElement with the specified namespace and tag. + NS_ADDREF(*aResult = nsXULElement::Construct(ni.forget())); +} + +//---------------------------------------------------------------------- +// nsISupports interface + +NS_IMPL_CYCLE_COLLECTION_INHERITED(nsXULElement, nsStyledElement) + +NS_IMPL_ADDREF_INHERITED(nsXULElement, nsStyledElement) +NS_IMPL_RELEASE_INHERITED(nsXULElement, nsStyledElement) + +NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(nsXULElement) + NS_ELEMENT_INTERFACE_TABLE_TO_MAP_SEGUE +NS_INTERFACE_MAP_END_INHERITING(nsStyledElement) + +//---------------------------------------------------------------------- +// nsINode interface + +nsresult nsXULElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + + RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo; + RefPtr<nsXULElement> element = Construct(ni.forget()); + + nsresult rv = const_cast<nsXULElement*>(this)->CopyInnerTo( + element, ReparseAttributes::No); + NS_ENSURE_SUCCESS(rv, rv); + + // Note that we're _not_ copying mControllers. + + element.forget(aResult); + return rv; +} + +//---------------------------------------------------------------------- + +EventListenerManager* nsXULElement::GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) { + // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc() + // here, override BindToTree for those classes and munge event + // listeners there? + Document* doc = OwnerDoc(); + + nsPIDOMWindowInner* window; + Element* root = doc->GetRootElement(); + if ((!root || root == this) && (window = doc->GetInnerWindow())) { + nsCOMPtr<EventTarget> piTarget = do_QueryInterface(window); + + *aDefer = false; + return piTarget->GetOrCreateListenerManager(); + } + + return nsStyledElement::GetEventListenerManagerForAttr(aAttrName, aDefer); +} + +// returns true if the element is not a list +static bool IsNonList(mozilla::dom::NodeInfo* aNodeInfo) { + return !aNodeInfo->Equals(nsGkAtoms::tree) && + !aNodeInfo->Equals(nsGkAtoms::richlistbox); +} + +bool nsXULElement::IsFocusableInternal(int32_t* aTabIndex, bool aWithMouse) { + /* + * Returns true if an element may be focused, and false otherwise. The inout + * argument aTabIndex will be set to the tab order index to be used; -1 for + * elements that should not be part of the tab order and a greater value to + * indicate its tab order. + * + * Confusingly, the supplied value for the aTabIndex argument may indicate + * whether the element may be focused as a result of the -moz-user-focus + * property, where -1 means no and 0 means yes. + * + * For controls, the element cannot be focused and is not part of the tab + * order if it is disabled. + * + * -moz-user-focus is overridden if a tabindex (even -1) is specified. + * + * Specifically, the behaviour for all XUL elements is as follows: + * *aTabIndex = -1 no tabindex Not focusable or tabbable + * *aTabIndex = -1 tabindex="-1" Focusable but not tabbable + * *aTabIndex = -1 tabindex=">=0" Focusable and tabbable + * *aTabIndex >= 0 no tabindex Focusable and tabbable + * *aTabIndex >= 0 tabindex="-1" Focusable but not tabbable + * *aTabIndex >= 0 tabindex=">=0" Focusable and tabbable + * + * If aTabIndex is null, then the tabindex is not computed, and + * true is returned for non-disabled controls and false otherwise. + */ + + // elements are not focusable by default + bool shouldFocus = false; + +#ifdef XP_MACOSX + // on Mac, mouse interactions only focus the element if it's a list, + // or if it's a remote target, since the remote target must handle + // the focus. + if (aWithMouse && IsNonList(mNodeInfo) && + !EventStateManager::IsTopLevelRemoteTarget(this)) { + return false; + } +#endif + + nsCOMPtr<nsIDOMXULControlElement> xulControl = AsXULControl(); + if (xulControl) { + // a disabled element cannot be focused and is not part of the tab order + bool disabled; + xulControl->GetDisabled(&disabled); + if (disabled) { + if (aTabIndex) *aTabIndex = -1; + return false; + } + shouldFocus = true; + } + + if (aTabIndex) { + Maybe<int32_t> attrVal = GetTabIndexAttrValue(); + if (attrVal.isSome()) { + // The tabindex attribute was specified, so the element becomes + // focusable. + shouldFocus = true; + *aTabIndex = attrVal.value(); + } else { + // otherwise, if there is no tabindex attribute, just use the value of + // *aTabIndex to indicate focusability. Reset any supplied tabindex to 0. + shouldFocus = *aTabIndex >= 0; + if (shouldFocus) { + *aTabIndex = 0; + } + } + + if (xulControl && shouldFocus && sTabFocusModelAppliesToXUL && + !(sTabFocusModel & eTabFocus_formElementsMask)) { + // By default, the tab focus model doesn't apply to xul element on any + // system but OS X. on OS X we're following it for UI elements (XUL) as + // sTabFocusModel is based on "Full Keyboard Access" system setting (see + // mac/nsILookAndFeel). both textboxes and list elements (i.e. trees and + // list) should always be focusable (textboxes are handled as html:input) + // For compatibility, we only do this for controls, otherwise elements + // like <browser> cannot take this focus. + if (IsNonList(mNodeInfo)) { + *aTabIndex = -1; + } + } + } + + return shouldFocus; +} + +bool nsXULElement::HasMenu() { + if (auto* button = XULButtonElement::FromNode(this)) { + return button->IsMenu(); + } + return false; +} + +void nsXULElement::OpenMenu(bool aOpenFlag) { + // Flush frames first. It's not clear why this is needed, see bug 1704670. + if (Document* doc = GetComposedDoc()) { + doc->FlushPendingNotifications(FlushType::Frames); + } + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) { + return; + } + + if (aOpenFlag) { + // Nothing will happen if this element isn't a menu. + pm->ShowMenu(this, false); + } else { + // Nothing will happen if this element isn't a menu. + pm->HideMenu(this); + } +} + +Result<bool, nsresult> nsXULElement::PerformAccesskey(bool aKeyCausesActivation, + bool aIsTrustedEvent) { + if (IsXULElement(nsGkAtoms::label)) { + nsAutoString control; + GetAttr(kNameSpaceID_None, nsGkAtoms::control, control); + if (control.IsEmpty()) { + return Err(NS_ERROR_UNEXPECTED); + } + + // XXXsmaug Should we use ShadowRoot::GetElementById in case + // element is in Shadow DOM? + RefPtr<Document> document = GetUncomposedDoc(); + if (!document) { + return Err(NS_ERROR_UNEXPECTED); + } + + RefPtr<Element> element = document->GetElementById(control); + if (!element) { + return Err(NS_ERROR_UNEXPECTED); + } + + // XXXedgar, This is mainly for HTMLElement which doesn't do visible + // check in PerformAccesskey. We probably should always do visible + // check on HTMLElement even if the PerformAccesskey is not redirected from + // label XULelement per spec. + nsIFrame* frame = element->GetPrimaryFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors()) { + return Err(NS_ERROR_UNEXPECTED); + } + + return element->PerformAccesskey(aKeyCausesActivation, aIsTrustedEvent); + } + + nsIFrame* frame = GetPrimaryFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors()) { + return Err(NS_ERROR_UNEXPECTED); + } + + bool focused = false; + // Define behavior for each type of XUL element. + if (!IsXULElement(nsGkAtoms::toolbarbutton)) { + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + RefPtr<Element> elementToFocus = this; + // for radio buttons, focus the radiogroup instead + if (IsXULElement(nsGkAtoms::radio)) { + if (nsCOMPtr<nsIDOMXULSelectControlItemElement> controlItem = + AsXULSelectControlItem()) { + bool disabled; + controlItem->GetDisabled(&disabled); + if (!disabled) { + controlItem->GetControl(getter_AddRefs(elementToFocus)); + } + } + } + + if (elementToFocus) { + fm->SetFocus(elementToFocus, nsIFocusManager::FLAG_BYKEY); + + // Return true if the element became focused. + nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); + focused = (window && window->GetFocusedElement() == elementToFocus); + } + } + } + + if (aKeyCausesActivation && !IsXULElement(nsGkAtoms::menulist)) { + ClickWithInputSource(MouseEvent_Binding::MOZ_SOURCE_KEYBOARD, + aIsTrustedEvent); + return focused; + } + + // If the accesskey won't cause the activation and the focus isn't changed, + // either. Return error so EventStateManager would try to find next element + // to handle the accesskey. + return focused ? Result<bool, nsresult>{focused} : Err(NS_ERROR_ABORT); +} + +//---------------------------------------------------------------------- + +void nsXULElement::AddListenerForAttributeIfNeeded(nsAtom* aLocalName) { + // If appropriate, add a popup listener and/or compile the event + // handler. Called when we change the element's document, create a + // new element, change an attribute's value, etc. + // Eventlistenener-attributes are always in the null namespace. + if (aLocalName == nsGkAtoms::menu || aLocalName == nsGkAtoms::contextmenu || + // XXXdwh popup and context are deprecated + aLocalName == nsGkAtoms::popup || aLocalName == nsGkAtoms::context) { + AddPopupListener(aLocalName); + } + if (nsContentUtils::IsEventAttributeName(aLocalName, EventNameType_XUL)) { + nsAutoString value; + GetAttr(kNameSpaceID_None, aLocalName, value); + SetEventHandler(aLocalName, value, true); + } +} + +void nsXULElement::AddListenerForAttributeIfNeeded(const nsAttrName& aName) { + if (aName.IsAtom()) { + AddListenerForAttributeIfNeeded(aName.Atom()); + } +} + +//---------------------------------------------------------------------- +// +// nsIContent interface +// +void nsXULElement::UpdateEditableState(bool aNotify) { + // Don't call through to Element here because the things + // it does don't work for cases when we're an editable control. + nsIContent* parent = GetParent(); + + SetEditableFlag(parent && parent->HasFlag(NODE_IS_EDITABLE)); + UpdateState(aNotify); +} + +class XULInContentErrorReporter : public Runnable { + public: + explicit XULInContentErrorReporter(Document& aDocument) + : mozilla::Runnable("XULInContentErrorReporter"), mDocument(aDocument) {} + + NS_IMETHOD Run() override { + mDocument->WarnOnceAbout(DeprecatedOperations::eImportXULIntoContent, + false); + return NS_OK; + } + + private: + OwningNonNull<Document> mDocument; +}; + +static bool NeedTooltipSupport(const nsXULElement& aXULElement) { + if (aXULElement.NodeInfo()->Equals(nsGkAtoms::treechildren)) { + // treechildren always get tooltip support, since cropped tree cells show + // their full text in a tooltip. + return true; + } + + return aXULElement.GetBoolAttr(nsGkAtoms::tooltip) || + aXULElement.GetBoolAttr(nsGkAtoms::tooltiptext); +} + +nsresult nsXULElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsStyledElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (!IsInComposedDoc()) { + return rv; + } + + Document& doc = aContext.OwnerDoc(); + if (!IsInNativeAnonymousSubtree() && !doc.AllowXULXBL() && + !doc.HasWarnedAbout(DeprecatedOperations::eImportXULIntoContent)) { + nsContentUtils::AddScriptRunner(new XULInContentErrorReporter(doc)); + } + +#ifdef DEBUG + if (!doc.AllowXULXBL() && !doc.IsUnstyledDocument()) { + // To save CPU cycles and memory, non-XUL documents only load the user + // agent style sheet rules for a minimal set of XUL elements such as + // 'scrollbar' that may be created implicitly for their content (those + // rules being in minimal-xul.css). + // + // This assertion makes sure no other XUL element is used in a non-XUL + // document. + nsAtom* tag = NodeInfo()->NameAtom(); + MOZ_ASSERT( + // scrollbar parts + tag == nsGkAtoms::scrollbar || tag == nsGkAtoms::scrollbarbutton || + tag == nsGkAtoms::scrollcorner || tag == nsGkAtoms::slider || + tag == nsGkAtoms::thumb || + // other + tag == nsGkAtoms::resizer || tag == nsGkAtoms::label, + "Unexpected XUL element in non-XUL doc"); + } +#endif + + // Within Bug 1492063 and its dependencies we started to apply a + // CSP to system privileged about pages. Since some about: pages + // are implemented in *.xul files we added this workaround to + // apply a CSP to them. To do so, we check the introduced custom + // attribute 'csp' on the root element. + if (doc.GetRootElement() == this) { + nsAutoString cspPolicyStr; + GetAttr(kNameSpaceID_None, nsGkAtoms::csp, cspPolicyStr); + +#ifdef DEBUG + { + nsCOMPtr<nsIContentSecurityPolicy> docCSP = doc.GetCsp(); + uint32_t policyCount = 0; + if (docCSP) { + docCSP->GetPolicyCount(&policyCount); + } + MOZ_ASSERT(policyCount == 0, "how come we already have a policy?"); + } +#endif + + CSP_ApplyMetaCSPToDoc(doc, cspPolicyStr); + } + + if (NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) { + // Create our XUL key listener and hook it up. + XULKeySetGlobalKeyListener::AttachKeyHandler(this); + } + + RegUnRegAccessKey(true); + + if (NeedTooltipSupport(*this)) { + AddTooltipSupport(); + } + + if (XULBroadcastManager::MayNeedListener(*this)) { + if (!doc.HasXULBroadcastManager()) { + doc.InitializeXULBroadcastManager(); + } + XULBroadcastManager* broadcastManager = doc.GetXULBroadcastManager(); + broadcastManager->AddListener(this); + } + return rv; +} + +void nsXULElement::UnbindFromTree(bool aNullParent) { + if (NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) { + XULKeySetGlobalKeyListener::DetachKeyHandler(this); + } + + RegUnRegAccessKey(false); + + if (NeedTooltipSupport(*this)) { + RemoveTooltipSupport(); + } + + Document* doc = GetComposedDoc(); + if (doc && doc->HasXULBroadcastManager() && + XULBroadcastManager::MayNeedListener(*this)) { + RefPtr<XULBroadcastManager> broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->RemoveListener(this); + } + + // mControllers can own objects that are implemented + // in JavaScript (such as some implementations of + // nsIControllers. These objects prevent their global + // object's script object from being garbage collected, + // which means JS continues to hold an owning reference + // to the nsGlobalWindow, which owns the document, + // which owns this content. That's a cycle, so we break + // it here. (It might be better to break this by releasing + // mDocument in nsGlobalWindow::SetDocShell, but I'm not + // sure whether that would fix all possible cycles through + // mControllers.) + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots) { + slots->mControllers = nullptr; + } + + nsStyledElement::UnbindFromTree(aNullParent); +} + +void nsXULElement::DoneAddingChildren(bool aHaveNotified) { + if (IsXULElement(nsGkAtoms::linkset)) { + Document* doc = GetComposedDoc(); + if (doc) { + doc->OnL10nResourceContainerParsed(); + } + } +} + +void nsXULElement::RegUnRegAccessKey(bool aDoReg) { + // Don't try to register for unsupported elements + if (!SupportsAccessKey()) { + return; + } + + nsStyledElement::RegUnRegAccessKey(aDoReg); +} + +bool nsXULElement::SupportsAccessKey() const { + if (NodeInfo()->Equals(nsGkAtoms::label) && HasAttr(nsGkAtoms::control)) { + return true; + } + + // XXX(ntim): check if description[value] or description[accesskey] are + // actually used, remove `value` from {Before/After}SetAttr if not the case + if (NodeInfo()->Equals(nsGkAtoms::description) && HasAttr(nsGkAtoms::value) && + HasAttr(nsGkAtoms::control)) { + return true; + } + + return IsAnyOfXULElements(nsGkAtoms::button, nsGkAtoms::toolbarbutton, + nsGkAtoms::checkbox, nsGkAtoms::tab, + nsGkAtoms::radio); +} + +nsresult nsXULElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString* aValue, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None && + (aName == nsGkAtoms::accesskey || aName == nsGkAtoms::control || + aName == nsGkAtoms::value)) { + RegUnRegAccessKey(false); + } else if (aNamespaceID == kNameSpaceID_None && + (aName == nsGkAtoms::command || aName == nsGkAtoms::observes) && + IsInUncomposedDoc()) { + // XXX sXBL/XBL2 issue! Owner or current document? + // XXX Why does this not also remove broadcast listeners if the + // "element" attribute was changed on an <observer>? + nsAutoString oldValue; + GetAttr(kNameSpaceID_None, nsGkAtoms::observes, oldValue); + if (oldValue.IsEmpty()) { + GetAttr(kNameSpaceID_None, nsGkAtoms::command, oldValue); + } + + Document* doc = GetUncomposedDoc(); + if (!oldValue.IsEmpty() && doc->HasXULBroadcastManager()) { + RefPtr<XULBroadcastManager> broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->RemoveListener(this); + } + } else if (aNamespaceID == kNameSpaceID_None && aValue && + mNodeInfo->Equals(nsGkAtoms::window) && + aName == nsGkAtoms::chromemargin) { + nsAttrValue attrValue; + // Make sure the margin format is valid first + if (!attrValue.ParseIntMarginValue(aValue->String())) { + return NS_ERROR_INVALID_ARG; + } + } else if (aNamespaceID == kNameSpaceID_None && + aName == nsGkAtoms::usercontextid) { + nsAutoString oldValue; + bool hasAttribute = + GetAttr(kNameSpaceID_None, nsGkAtoms::usercontextid, oldValue); + if (hasAttribute && (!aValue || !aValue->String().Equals(oldValue))) { + MOZ_ASSERT(false, "Changing usercontextid is not allowed."); + return NS_ERROR_INVALID_ARG; + } + } + + return nsStyledElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); +} + +nsresult nsXULElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aValue) { + AddListenerForAttributeIfNeeded(aName); + } + + if (aName == nsGkAtoms::accesskey || aName == nsGkAtoms::control || + aName == nsGkAtoms::value) { + RegUnRegAccessKey(true); + } else if (aName == nsGkAtoms::tooltip || aName == nsGkAtoms::tooltiptext) { + if (!!aValue != !!aOldValue && IsInComposedDoc() && + !NodeInfo()->Equals(nsGkAtoms::treechildren)) { + if (aValue) { + AddTooltipSupport(); + } else { + RemoveTooltipSupport(); + } + } + } + Document* doc = GetComposedDoc(); + if (doc && doc->HasXULBroadcastManager()) { + RefPtr<XULBroadcastManager> broadcastManager = + doc->GetXULBroadcastManager(); + broadcastManager->AttributeChanged(this, aNamespaceID, aName); + } + if (doc && XULBroadcastManager::MayNeedListener(*this)) { + if (!doc->HasXULBroadcastManager()) { + doc->InitializeXULBroadcastManager(); + } + XULBroadcastManager* broadcastManager = doc->GetXULBroadcastManager(); + broadcastManager->AddListener(this); + } + + // XXX need to check if they're changing an event handler: if + // so, then we need to unhook the old one. Or something. + } + + return nsStyledElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +void nsXULElement::AddTooltipSupport() { + nsXULTooltipListener* listener = nsXULTooltipListener::GetInstance(); + if (!listener) { + return; + } + + listener->AddTooltipSupport(this); +} + +void nsXULElement::RemoveTooltipSupport() { + nsXULTooltipListener* listener = nsXULTooltipListener::GetInstance(); + if (!listener) { + return; + } + + listener->RemoveTooltipSupport(this); +} + +bool nsXULElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) { + return aResult.ParseIntValue(aValue); + } + + // Parse into a nsAttrValue + if (!nsStyledElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult)) { + // Fall back to parsing as atom for short values + aResult.ParseStringOrAtom(aValue); + } + + return true; +} + +void nsXULElement::DestroyContent() { + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots) { + slots->mControllers = nullptr; + } + + nsStyledElement::DestroyContent(); +} + +#ifdef MOZ_DOM_LIST +void nsXULElement::List(FILE* out, int32_t aIndent) const { + nsCString prefix("XUL"); + if (HasSlots()) { + prefix.Append('*'); + } + prefix.Append(' '); + + nsStyledElement::List(out, aIndent, prefix); +} +#endif + +bool nsXULElement::IsEventStoppedFromAnonymousScrollbar(EventMessage aMessage) { + return (IsRootOfNativeAnonymousSubtree() && + IsAnyOfXULElements(nsGkAtoms::scrollbar, nsGkAtoms::scrollcorner) && + (aMessage == eMouseClick || aMessage == eMouseDoubleClick || + aMessage == eXULCommand || aMessage == eContextMenu || + aMessage == eDragStart || aMessage == eMouseAuxClick)); +} + +nsresult nsXULElement::DispatchXULCommand(const EventChainVisitor& aVisitor, + nsAutoString& aCommand) { + // XXX sXBL/XBL2 issue! Owner or current document? + nsCOMPtr<Document> doc = GetUncomposedDoc(); + NS_ENSURE_STATE(doc); + RefPtr<Element> commandElt = doc->GetElementById(aCommand); + if (commandElt) { + // Create a new command event to dispatch to the element + // pointed to by the command attribute. The new event's + // sourceEvent will be the original command event that we're + // handling. + RefPtr<Event> event = aVisitor.mDOMEvent; + uint16_t inputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + int16_t button = 0; + while (event) { + NS_ENSURE_STATE(event->GetOriginalTarget() != commandElt); + RefPtr<XULCommandEvent> commandEvent = event->AsXULCommandEvent(); + if (commandEvent) { + event = commandEvent->GetSourceEvent(); + inputSource = commandEvent->InputSource(); + button = commandEvent->Button(); + } else { + event = nullptr; + } + } + WidgetInputEvent* orig = aVisitor.mEvent->AsInputEvent(); + nsContentUtils::DispatchXULCommand( + commandElt, orig->IsTrusted(), MOZ_KnownLive(aVisitor.mDOMEvent), + nullptr, orig->IsControl(), orig->IsAlt(), orig->IsShift(), + orig->IsMeta(), inputSource, button); + } else { + NS_WARNING("A XUL element is attached to a command that doesn't exist!\n"); + } + return NS_OK; +} + +void nsXULElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mForceContentDispatch = true; // FIXME! Bug 329119 + if (IsEventStoppedFromAnonymousScrollbar(aVisitor.mEvent->mMessage)) { + // Don't propagate these events from native anonymous scrollbar. + aVisitor.mCanHandle = true; + aVisitor.SetParentTarget(nullptr, false); + return; + } + if (aVisitor.mEvent->mMessage == eXULCommand && + aVisitor.mEvent->mClass == eInputEventClass && + aVisitor.mEvent->mOriginalTarget == static_cast<nsIContent*>(this) && + !IsXULElement(nsGkAtoms::command)) { + // Check that we really have an xul command event. That will be handled + // in a special way. + // See if we have a command elt. If so, we execute on the command + // instead of on our content element. + if (aVisitor.mDOMEvent && aVisitor.mDOMEvent->AsXULCommandEvent() && + HasNonEmptyAttr(nsGkAtoms::command)) { + // Stop building the event target chain for the original event. + // We don't want it to propagate to any DOM nodes. + aVisitor.mCanHandle = false; + aVisitor.mAutomaticChromeDispatch = false; + // Dispatch XUL command in PreHandleEvent to prevent it breaks event + // target chain creation + aVisitor.mWantsPreHandleEvent = true; + aVisitor.mItemFlags |= NS_DISPATCH_XUL_COMMAND; + return; + } + } + + nsStyledElement::GetEventTargetParent(aVisitor); +} + +nsresult nsXULElement::PreHandleEvent(EventChainVisitor& aVisitor) { + if (aVisitor.mItemFlags & NS_DISPATCH_XUL_COMMAND) { + nsAutoString command; + GetAttr(kNameSpaceID_None, nsGkAtoms::command, command); + MOZ_ASSERT(!command.IsEmpty()); + return DispatchXULCommand(aVisitor, command); + } + return nsStyledElement::PreHandleEvent(aVisitor); +} + +//---------------------------------------------------------------------- +// Implementation methods + +NS_IMETHODIMP_(bool) +nsXULElement::IsAttributeMapped(const nsAtom* aAttribute) const { + return false; +} + +nsIControllers* nsXULElement::GetControllers(ErrorResult& rv) { + if (!Controllers()) { + nsExtendedDOMSlots* slots = ExtendedDOMSlots(); + + slots->mControllers = new nsXULControllers(); + } + + return Controllers(); +} + +void nsXULElement::Click(CallerType aCallerType) { + ClickWithInputSource(MouseEvent_Binding::MOZ_SOURCE_UNKNOWN, + aCallerType == CallerType::System); +} + +void nsXULElement::ClickWithInputSource(uint16_t aInputSource, + bool aIsTrustedEvent) { + if (BoolAttrIsTrue(nsGkAtoms::disabled)) return; + + nsCOMPtr<Document> doc = GetComposedDoc(); // Strong just in case + if (doc) { + RefPtr<nsPresContext> context = doc->GetPresContext(); + if (context) { + // strong ref to PresContext so events don't destroy it + + WidgetMouseEvent eventDown(aIsTrustedEvent, eMouseDown, nullptr, + WidgetMouseEvent::eReal); + WidgetMouseEvent eventUp(aIsTrustedEvent, eMouseUp, nullptr, + WidgetMouseEvent::eReal); + // This helps to avoid commands being dispatched from + // XULButtonElement::PostHandleEventForMenu. + eventUp.mFlags.mMultipleActionsPrevented = true; + WidgetMouseEvent eventClick(aIsTrustedEvent, eMouseClick, nullptr, + WidgetMouseEvent::eReal); + eventDown.mInputSource = eventUp.mInputSource = eventClick.mInputSource = + aInputSource; + + // send mouse down + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(static_cast<nsIContent*>(this), context, + &eventDown, nullptr, &status); + + // send mouse up + status = nsEventStatus_eIgnore; // reset status + EventDispatcher::Dispatch(static_cast<nsIContent*>(this), context, + &eventUp, nullptr, &status); + + // send mouse click + status = nsEventStatus_eIgnore; // reset status + EventDispatcher::Dispatch(static_cast<nsIContent*>(this), context, + &eventClick, nullptr, &status); + + // If the click has been prevented, lets skip the command call + // this is how a physical click works + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + } + + // oncommand is fired when an element is clicked... + DoCommand(); +} + +void nsXULElement::DoCommand() { + nsCOMPtr<Document> doc = GetComposedDoc(); // strong just in case + if (doc) { + RefPtr<nsXULElement> self = this; + nsContentUtils::DispatchXULCommand(self, true); + } +} + +bool nsXULElement::IsNodeOfType(uint32_t aFlags) const { return false; } + +nsresult nsXULElement::AddPopupListener(nsAtom* aName) { + // Add a popup listener to the element + bool isContext = + (aName == nsGkAtoms::context || aName == nsGkAtoms::contextmenu); + uint32_t listenerFlag = isContext ? XUL_ELEMENT_HAS_CONTENTMENU_LISTENER + : XUL_ELEMENT_HAS_POPUP_LISTENER; + + if (HasFlag(listenerFlag)) { + return NS_OK; + } + + nsCOMPtr<nsIDOMEventListener> listener = + new nsXULPopupListener(this, isContext); + + // Add the popup as a listener on this element. + EventListenerManager* manager = GetOrCreateListenerManager(); + SetFlags(listenerFlag); + + if (isContext) { + manager->AddEventListenerByType(listener, u"contextmenu"_ns, + TrustedEventsAtSystemGroupBubble()); + } else { + manager->AddEventListenerByType(listener, u"mousedown"_ns, + TrustedEventsAtSystemGroupBubble()); + } + return NS_OK; +} + +//---------------------------------------------------------------------- + +nsresult nsXULElement::MakeHeavyweight(nsXULPrototypeElement* aPrototype) { + if (!aPrototype) { + return NS_OK; + } + + size_t i; + nsresult rv; + for (i = 0; i < aPrototype->mAttributes.Length(); ++i) { + nsXULPrototypeAttribute* protoattr = &aPrototype->mAttributes[i]; + nsAttrValue attrValue; + + // Style rules need to be cloned. + if (protoattr->mValue.Type() == nsAttrValue::eCSSDeclaration) { + DeclarationBlock* decl = protoattr->mValue.GetCSSDeclarationValue(); + RefPtr<DeclarationBlock> declClone = decl->Clone(); + + nsString stringValue; + protoattr->mValue.ToString(stringValue); + + attrValue.SetTo(declClone.forget(), &stringValue); + } else { + attrValue.SetTo(protoattr->mValue); + } + + bool oldValueSet; + // XXX we might wanna have a SetAndTakeAttr that takes an nsAttrName + if (protoattr->mName.IsAtom()) { + rv = mAttrs.SetAndSwapAttr(protoattr->mName.Atom(), attrValue, + &oldValueSet); + } else { + rv = mAttrs.SetAndSwapAttr(protoattr->mName.NodeInfo(), attrValue, + &oldValueSet); + } + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +bool nsXULElement::BoolAttrIsTrue(nsAtom* aName) const { + const nsAttrValue* attr = GetAttrInfo(kNameSpaceID_None, aName).mValue; + + return attr && attr->Type() == nsAttrValue::eAtom && + attr->GetAtomValue() == nsGkAtoms::_true; +} + +bool nsXULElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName(aName, EventNameType_XUL); +} + +JSObject* nsXULElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::XULElement_Binding::Wrap(aCx, this, aGivenProto); +} + +bool nsXULElement::IsInteractiveHTMLContent() const { + return IsXULElement(nsGkAtoms::menupopup) || + Element::IsInteractiveHTMLContent(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULPrototypeNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULPrototypeNode) + if (tmp->mType == nsXULPrototypeNode::eType_Element) { + static_cast<nsXULPrototypeElement*>(tmp)->Unlink(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULPrototypeNode) + if (tmp->mType == nsXULPrototypeNode::eType_Element) { + nsXULPrototypeElement* elem = static_cast<nsXULPrototypeElement*>(tmp); + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mNodeInfo"); + cb.NoteNativeChild(elem->mNodeInfo, + NS_CYCLE_COLLECTION_PARTICIPANT(NodeInfo)); + size_t i; + for (i = 0; i < elem->mAttributes.Length(); ++i) { + const nsAttrName& name = elem->mAttributes[i].mName; + if (!name.IsAtom()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, + "mAttributes[i].mName.NodeInfo()"); + cb.NoteNativeChild(name.NodeInfo(), + NS_CYCLE_COLLECTION_PARTICIPANT(NodeInfo)); + } + } + ImplCycleCollectionTraverse(cb, elem->mChildren, "mChildren"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsXULPrototypeNode) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +//---------------------------------------------------------------------- +// +// nsXULPrototypeAttribute +// + +nsXULPrototypeAttribute::~nsXULPrototypeAttribute() { + MOZ_COUNT_DTOR(nsXULPrototypeAttribute); +} + +//---------------------------------------------------------------------- +// +// nsXULPrototypeElement +// + +nsresult nsXULPrototypeElement::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv; + + // Write basic prototype data + rv = aStream->Write32(mType); + + // Write Node Info + int32_t index = aNodeInfos->IndexOf(mNodeInfo); + NS_ASSERTION(index >= 0, "unknown mozilla::dom::NodeInfo index"); + nsresult tmp = aStream->Write32(index); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + // Write Attributes + tmp = aStream->Write32(mAttributes.Length()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + nsAutoString attributeValue; + size_t i; + for (i = 0; i < mAttributes.Length(); ++i) { + RefPtr<mozilla::dom::NodeInfo> ni; + if (mAttributes[i].mName.IsAtom()) { + ni = mNodeInfo->NodeInfoManager()->GetNodeInfo( + mAttributes[i].mName.Atom(), nullptr, kNameSpaceID_None, + nsINode::ATTRIBUTE_NODE); + NS_ASSERTION(ni, "the nodeinfo should already exist"); + } else { + ni = mAttributes[i].mName.NodeInfo(); + } + + index = aNodeInfos->IndexOf(ni); + NS_ASSERTION(index >= 0, "unknown mozilla::dom::NodeInfo index"); + tmp = aStream->Write32(index); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + mAttributes[i].mValue.ToString(attributeValue); + tmp = aStream->WriteWStringZ(attributeValue.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + // Now write children + tmp = aStream->Write32(uint32_t(mChildren.Length())); + if (NS_FAILED(tmp)) { + rv = tmp; + } + for (i = 0; i < mChildren.Length(); i++) { + nsXULPrototypeNode* child = mChildren[i].get(); + switch (child->mType) { + case eType_Element: + case eType_Text: + case eType_PI: + tmp = child->Serialize(aStream, aProtoDoc, aNodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + break; + case eType_Script: + tmp = aStream->Write32(child->mType); + if (NS_FAILED(tmp)) { + rv = tmp; + } + nsXULPrototypeScript* script = + static_cast<nsXULPrototypeScript*>(child); + + tmp = aStream->Write8(script->mOutOfLine); + if (NS_FAILED(tmp)) { + rv = tmp; + } + if (!script->mOutOfLine) { + tmp = script->Serialize(aStream, aProtoDoc, aNodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } else { + tmp = aStream->WriteCompoundObject(script->mSrcURI, + NS_GET_IID(nsIURI), true); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + if (script->HasStencil()) { + // This may return NS_OK without muxing script->mSrcURI's + // data into the cache file, in the case where that + // muxed document is already there (written by a prior + // session, or by an earlier cache episode during this + // session). + tmp = script->SerializeOutOfLine(aStream, aProtoDoc); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + } + break; + } + } + + return rv; +} + +nsresult nsXULPrototypeElement::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + MOZ_ASSERT(aNodeInfos, "missing nodeinfo array"); + + // Read Node Info + uint32_t number = 0; + nsresult rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + mNodeInfo = aNodeInfos->SafeElementAt(number, nullptr); + if (!mNodeInfo) { + return NS_ERROR_UNEXPECTED; + } + + // Read Attributes + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + int32_t attributes = int32_t(number); + + if (attributes > 0) { + mAttributes.AppendElements(attributes); + + nsAutoString attributeValue; + for (size_t i = 0; i < mAttributes.Length(); ++i) { + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + mozilla::dom::NodeInfo* ni = aNodeInfos->SafeElementAt(number, nullptr); + if (!ni) { + return NS_ERROR_UNEXPECTED; + } + + mAttributes[i].mName.SetTo(ni); + + rv = aStream->ReadString(attributeValue); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + rv = SetAttrAt(i, attributeValue, aDocumentURI); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + } + + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + uint32_t numChildren = int32_t(number); + + if (numChildren > 0) { + if (!mChildren.SetCapacity(numChildren, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t i = 0; i < numChildren; i++) { + rv = aStream->Read32(&number); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + Type childType = (Type)number; + + RefPtr<nsXULPrototypeNode> child; + + switch (childType) { + case eType_Element: + child = new nsXULPrototypeElement(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_Text: + child = new nsXULPrototypeText(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_PI: + child = new nsXULPrototypePI(); + rv = child->Deserialize(aStream, aProtoDoc, aDocumentURI, aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + break; + case eType_Script: { + // language version/options obtained during deserialization. + RefPtr<nsXULPrototypeScript> script = new nsXULPrototypeScript(0); + + rv = aStream->ReadBoolean(&script->mOutOfLine); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + if (!script->mOutOfLine) { + rv = script->Deserialize(aStream, aProtoDoc, aDocumentURI, + aNodeInfos); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } else { + nsCOMPtr<nsISupports> supports; + rv = aStream->ReadObject(true, getter_AddRefs(supports)); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + script->mSrcURI = do_QueryInterface(supports); + + rv = script->DeserializeOutOfLine(aStream, aProtoDoc); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + + child = std::move(script); + break; + } + default: + MOZ_ASSERT(false, "Unexpected child type!"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(child, "Don't append null to mChildren"); + MOZ_ASSERT(child->mType == childType); + mChildren.AppendElement(child); + + // Oh dear. Something failed during the deserialization. + // We don't know what. But likely consequences of failed + // deserializations included calls to |AbortCaching| which + // shuts down the cache and closes our streams. + // If that happens, next time through this loop, we die a messy + // death. So, let's just fail now, and propagate that failure + // upward so that the ChromeProtocolHandler knows it can't use + // a cached chrome channel for this. + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + } + + return rv; +} + +nsresult nsXULPrototypeElement::SetAttrAt(uint32_t aPos, + const nsAString& aValue, + nsIURI* aDocumentURI) { + MOZ_ASSERT(aPos < mAttributes.Length(), "out-of-bounds"); + + // WARNING!! + // This code is largely duplicated in nsXULElement::SetAttr. + // Any changes should be made to both functions. + + if (!mNodeInfo->NamespaceEquals(kNameSpaceID_XUL)) { + if (mNodeInfo->NamespaceEquals(kNameSpaceID_XHTML) && + mAttributes[aPos].mName.Equals(nsGkAtoms::is)) { + // We still care about the is attribute set on HTML elements. + mAttributes[aPos].mValue.ParseAtom(aValue); + mIsAtom = mAttributes[aPos].mValue.GetAtomValue(); + + return NS_OK; + } + + mAttributes[aPos].mValue.ParseStringOrAtom(aValue); + + return NS_OK; + } + + if (mAttributes[aPos].mName.Equals(nsGkAtoms::id) && !aValue.IsEmpty()) { + mHasIdAttribute = true; + // Store id as atom. + // id="" means that the element has no id. Not that it has + // emptystring as id. + mAttributes[aPos].mValue.ParseAtom(aValue); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::is)) { + // Store is as atom. + mAttributes[aPos].mValue.ParseAtom(aValue); + mIsAtom = mAttributes[aPos].mValue.GetAtomValue(); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::_class)) { + mHasClassAttribute = true; + // Compute the element's class list + mAttributes[aPos].mValue.ParseAtomArray(aValue); + + return NS_OK; + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::style)) { + mHasStyleAttribute = true; + // Parse the element's 'style' attribute + + // This is basically duplicating what nsINode::NodePrincipal() does + nsIPrincipal* principal = mNodeInfo->NodeInfoManager()->DocumentPrincipal(); + // XXX Get correct Base URI (need GetBaseURI on *prototype* element) + // TODO: If we implement Content Security Policy for chrome documents + // as has been discussed, the CSP should be checked here to see if + // inline styles are allowed to be applied. + // XXX No specific specs talk about xul and referrer policy, pass Unset + auto referrerInfo = + MakeRefPtr<ReferrerInfo>(aDocumentURI, ReferrerPolicy::_empty); + auto data = MakeRefPtr<URLExtraData>(aDocumentURI, referrerInfo, principal); + RefPtr<DeclarationBlock> declaration = DeclarationBlock::FromCssText( + aValue, data, eCompatibility_FullStandards, nullptr, + StyleCssRuleType::Style); + if (declaration) { + mAttributes[aPos].mValue.SetTo(declaration.forget(), &aValue); + + return NS_OK; + } + // Don't abort if parsing failed, it could just be malformed css. + } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::tabindex)) { + mAttributes[aPos].mValue.ParseIntValue(aValue); + + return NS_OK; + } + + mAttributes[aPos].mValue.ParseStringOrAtom(aValue); + + return NS_OK; +} + +void nsXULPrototypeElement::Unlink() { + mAttributes.Clear(); + mChildren.Clear(); +} + +//---------------------------------------------------------------------- +// +// nsXULPrototypeScript +// + +nsXULPrototypeScript::nsXULPrototypeScript(uint32_t aLineNo) + : nsXULPrototypeNode(eType_Script), + mLineNo(aLineNo), + mSrcLoading(false), + mOutOfLine(true), + mSrcLoadWaiters(nullptr), + mStencil(nullptr) {} + +static nsresult WriteStencil(nsIObjectOutputStream* aStream, JSContext* aCx, + JS::Stencil* aStencil) { + JS::TranscodeBuffer buffer; + JS::TranscodeResult code; + code = JS::EncodeStencil(aCx, aStencil, buffer); + + if (code != JS::TranscodeResult::Ok) { + if (code == JS::TranscodeResult::Throw) { + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(IsTranscodeFailureResult(code)); + return NS_ERROR_FAILURE; + } + + size_t size = buffer.length(); + if (size > UINT32_MAX) { + return NS_ERROR_FAILURE; + } + nsresult rv = aStream->Write32(size); + if (NS_SUCCEEDED(rv)) { + // Ideally we could just pass "buffer" here. See bug 1566574. + rv = aStream->WriteBytes(Span(buffer.begin(), size)); + } + + return rv; +} + +static nsresult ReadStencil(nsIObjectInputStream* aStream, JSContext* aCx, + const JS::DecodeOptions& aOptions, + JS::Stencil** aStencilOut) { + // We don't serialize mutedError-ness of scripts, which is fine as long as + // we only serialize system and XUL-y things. We can detect this by checking + // where the caller wants us to deserialize. + // + // CompilationScope() could theoretically GC, so get that out of the way + // before comparing to the cx global. + JSObject* loaderGlobal = xpc::CompilationScope(); + MOZ_RELEASE_ASSERT(nsContentUtils::IsSystemCaller(aCx) || + JS::CurrentGlobalOrNull(aCx) == loaderGlobal); + + uint32_t size; + nsresult rv = aStream->Read32(&size); + if (NS_FAILED(rv)) { + return rv; + } + + char* data; + rv = aStream->ReadBytes(size, &data); + if (NS_FAILED(rv)) { + return rv; + } + + // The decoded stencil shouldn't borrow from the XDR buffer. + MOZ_ASSERT(!aOptions.borrowBuffer); + auto cleanupData = MakeScopeExit([&]() { free(data); }); + + JS::TranscodeRange range(reinterpret_cast<uint8_t*>(data), size); + + { + JS::TranscodeResult code; + RefPtr<JS::Stencil> stencil; + code = JS::DecodeStencil(aCx, aOptions, range, getter_AddRefs(stencil)); + if (code != JS::TranscodeResult::Ok) { + if (code == JS::TranscodeResult::Throw) { + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(IsTranscodeFailureResult(code)); + return NS_ERROR_FAILURE; + } + + stencil.forget(aStencilOut); + } + + return rv; +} + +void nsXULPrototypeScript::FillCompileOptions(JS::CompileOptions& options) { + // If the script was inline, tell the JS parser to save source for + // Function.prototype.toSource(). If it's out of line, we retrieve the + // source from the files on demand. + options.setSourceIsLazy(mOutOfLine); +} + +nsresult nsXULPrototypeScript::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + NS_ENSURE_TRUE(aProtoDoc, NS_ERROR_UNEXPECTED); + + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + return NS_ERROR_UNEXPECTED; + } + + NS_ASSERTION(!mSrcLoading || mSrcLoadWaiters != nullptr || !mStencil, + "script source still loading when serializing?!"); + if (!mStencil) return NS_ERROR_FAILURE; + + // Write basic prototype data + nsresult rv; + rv = aStream->Write32(mLineNo); + if (NS_FAILED(rv)) return rv; + + JSContext* cx = jsapi.cx(); + MOZ_ASSERT(xpc::CompilationScope() == JS::CurrentGlobalOrNull(cx)); + + return WriteStencil(aStream, cx, mStencil); +} + +nsresult nsXULPrototypeScript::SerializeOutOfLine( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc) { + if (!mSrcURI->SchemeIs("chrome")) + // Don't cache scripts that don't come from chrome uris. + return NS_ERROR_NOT_IMPLEMENTED; + + nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); + if (!cache) return NS_ERROR_OUT_OF_MEMORY; + + NS_ASSERTION(cache->IsEnabled(), + "writing to the cache file, but the XUL cache is off?"); + bool exists; + cache->HasScript(mSrcURI, &exists); + + /* return will be NS_OK from GetAsciiSpec. + * that makes no sense. + * nor does returning NS_OK from HasMuxedDocument. + * XXX return something meaningful. + */ + if (exists) return NS_OK; + + nsCOMPtr<nsIObjectOutputStream> oos; + nsresult rv = cache->GetScriptOutputStream(mSrcURI, getter_AddRefs(oos)); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult tmp = Serialize(oos, aProtoDoc, nullptr); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = cache->FinishScriptOutputStream(mSrcURI); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + if (NS_FAILED(rv)) cache->AbortCaching(); + return rv; +} + +nsresult nsXULPrototypeScript::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv; + NS_ASSERTION(!mSrcLoading || mSrcLoadWaiters != nullptr || !mStencil, + "prototype script not well-initialized when deserializing?!"); + + // Read basic prototype data + rv = aStream->Read32(&mLineNo); + if (NS_FAILED(rv)) return rv; + + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + + JS::DecodeOptions options; + RefPtr<JS::Stencil> newStencil; + rv = ReadStencil(aStream, cx, options, getter_AddRefs(newStencil)); + NS_ENSURE_SUCCESS(rv, rv); + Set(newStencil); + return NS_OK; +} + +nsresult nsXULPrototypeScript::DeserializeOutOfLine( + nsIObjectInputStream* aInput, nsXULPrototypeDocument* aProtoDoc) { + // Keep track of failure via rv, so we can + // AbortCaching if things look bad. + nsresult rv = NS_OK; + nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); + + nsCOMPtr<nsIObjectInputStream> objectInput = aInput; + if (cache) { + bool useXULCache = true; + if (mSrcURI) { + // NB: we must check the XUL script cache early, to avoid + // multiple deserialization attempts for a given script. + // Note that PrototypeDocumentContentSink::LoadScript + // checks the XUL script cache too, in order to handle the + // serialization case. + // + // We need do this only for <script src='strres.js'> and the + // like, i.e., out-of-line scripts that are included by several + // different XUL documents stored in the cache file. + useXULCache = cache->IsEnabled(); + + if (useXULCache) { + RefPtr<JS::Stencil> newStencil = cache->GetStencil(mSrcURI); + if (newStencil) { + Set(newStencil); + } + } + } + + if (!mStencil) { + if (mSrcURI) { + rv = cache->GetScriptInputStream(mSrcURI, getter_AddRefs(objectInput)); + } + // If !mSrcURI, we have an inline script. We shouldn't have + // to do anything else in that case, I think. + + // We do reflect errors into rv, but our caller may want to + // ignore our return value, because mStencil will be null + // after any error, and that suffices to cause the script to + // be reloaded (from the src= URI, if any) and recompiled. + // We're better off slow-loading than bailing out due to a + // error. + if (NS_SUCCEEDED(rv)) + rv = Deserialize(objectInput, aProtoDoc, nullptr, nullptr); + + if (NS_SUCCEEDED(rv)) { + if (useXULCache && mSrcURI && mSrcURI->SchemeIs("chrome")) { + cache->PutStencil(mSrcURI, GetStencil()); + } + cache->FinishScriptInputStream(mSrcURI); + } else { + // If mSrcURI is not in the cache, + // rv will be NS_ERROR_NOT_AVAILABLE and we'll try to + // update the cache file to hold a serialization of + // this script, once it has finished loading. + if (rv != NS_ERROR_NOT_AVAILABLE) cache->AbortCaching(); + } + } + } + return rv; +} + +class NotifyOffThreadScriptCompletedRunnable : public Runnable { + // An array of all outstanding script receivers. All reference counting of + // these objects happens on the main thread. When we return to the main + // thread from script compilation we make sure our receiver is still in + // this array (still alive) before proceeding. This array is cleared during + // shutdown, potentially before all outstanding script compilations have + // finished. We do not need to worry about pointer replay here, because + // a) we should not be starting script compilation after clearing this + // array and b) in all other cases the receiver will still be alive. + static StaticAutoPtr<nsTArray<nsCOMPtr<nsIOffThreadScriptReceiver>>> + sReceivers; + static bool sSetupClearOnShutdown; + + nsIOffThreadScriptReceiver* mReceiver; + JS::OffThreadToken* mToken; + + public: + NotifyOffThreadScriptCompletedRunnable(nsIOffThreadScriptReceiver* aReceiver, + JS::OffThreadToken* aToken) + : mozilla::Runnable("NotifyOffThreadScriptCompletedRunnable"), + mReceiver(aReceiver), + mToken(aToken) {} + + static void NoteReceiver(nsIOffThreadScriptReceiver* aReceiver) { + if (!sSetupClearOnShutdown) { + ClearOnShutdown(&sReceivers); + sSetupClearOnShutdown = true; + sReceivers = new nsTArray<nsCOMPtr<nsIOffThreadScriptReceiver>>(); + } + + // If we ever crash here, it's because we tried to lazy compile script + // too late in shutdown. + sReceivers->AppendElement(aReceiver); + } + + NS_DECL_NSIRUNNABLE +}; + +StaticAutoPtr<nsTArray<nsCOMPtr<nsIOffThreadScriptReceiver>>> + NotifyOffThreadScriptCompletedRunnable::sReceivers; +bool NotifyOffThreadScriptCompletedRunnable::sSetupClearOnShutdown = false; + +NS_IMETHODIMP +NotifyOffThreadScriptCompletedRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<JS::Stencil> stencil; + { + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + // Now what? I guess we just leak... this should probably never + // happen. + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + stencil = JS::FinishOffThreadStencil(cx, mToken); + } + + if (!sReceivers) { + // We've already shut down. + return NS_OK; + } + + auto index = sReceivers->IndexOf(mReceiver); + MOZ_RELEASE_ASSERT(index != sReceivers->NoIndex); + nsCOMPtr<nsIOffThreadScriptReceiver> receiver = + std::move((*sReceivers)[index]); + sReceivers->RemoveElementAt(index); + + return receiver->OnScriptCompileComplete(stencil, + stencil ? NS_OK : NS_ERROR_FAILURE); +} + +static void OffThreadScriptReceiverCallback(JS::OffThreadToken* aToken, + void* aCallbackData) { + // Be careful not to adjust the refcount on the receiver, as this callback + // may be invoked off the main thread. + nsIOffThreadScriptReceiver* aReceiver = + static_cast<nsIOffThreadScriptReceiver*>(aCallbackData); + RefPtr<NotifyOffThreadScriptCompletedRunnable> notify = + new NotifyOffThreadScriptCompletedRunnable(aReceiver, aToken); + NS_DispatchToMainThread(notify); +} + +template <typename Unit> +nsresult nsXULPrototypeScript::Compile( + const Unit* aText, size_t aTextLength, JS::SourceOwnership aOwnership, + nsIURI* aURI, uint32_t aLineNo, Document* aDocument, + nsIOffThreadScriptReceiver* aOffThreadReceiver /* = nullptr */) { + // We'll compile the script in the compilation scope. + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::CompilationScope())) { + if (aOwnership == JS::SourceOwnership::TakeOwnership) { + // In this early-exit case -- before the |srcBuf.init| call will + // own |aText| -- we must relinquish ownership manually. + js_free(const_cast<Unit*>(aText)); + } + + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + + JS::SourceText<Unit> srcBuf; + if (NS_WARN_IF(!srcBuf.init(cx, aText, aTextLength, aOwnership))) { + return NS_ERROR_FAILURE; + } + + nsAutoCString urlspec; + nsresult rv = aURI->GetSpec(urlspec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Ok, compile it to create a prototype script object! + JS::CompileOptions options(cx); + FillCompileOptions(options); + options.setIntroductionType(mOutOfLine ? "srcScript" : "inlineScript") + .setFileAndLine(urlspec.get(), mOutOfLine ? 1 : aLineNo); + + JS::Rooted<JSObject*> scope(cx, JS::CurrentGlobalOrNull(cx)); + + if (aOffThreadReceiver && JS::CanCompileOffThread(cx, options, aTextLength)) { + if (!JS::CompileToStencilOffThread( + cx, options, srcBuf, OffThreadScriptReceiverCallback, + static_cast<void*>(aOffThreadReceiver))) { + JS_ClearPendingException(cx); + return NS_ERROR_OUT_OF_MEMORY; + } + NotifyOffThreadScriptCompletedRunnable::NoteReceiver(aOffThreadReceiver); + } else { + RefPtr<JS::Stencil> stencil = + JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + if (!stencil) { + return NS_ERROR_OUT_OF_MEMORY; + } + Set(stencil); + } + return NS_OK; +} + +template nsresult nsXULPrototypeScript::Compile<char16_t>( + const char16_t* aText, size_t aTextLength, JS::SourceOwnership aOwnership, + nsIURI* aURI, uint32_t aLineNo, Document* aDocument, + nsIOffThreadScriptReceiver* aOffThreadReceiver); +template nsresult nsXULPrototypeScript::Compile<Utf8Unit>( + const Utf8Unit* aText, size_t aTextLength, JS::SourceOwnership aOwnership, + nsIURI* aURI, uint32_t aLineNo, Document* aDocument, + nsIOffThreadScriptReceiver* aOffThreadReceiver); + +nsresult nsXULPrototypeScript::InstantiateScript( + JSContext* aCx, JS::MutableHandle<JSScript*> aScript) { + MOZ_ASSERT(mStencil); + + JS::CompileOptions options(aCx); + FillCompileOptions(options); + JS::InstantiateOptions instantiateOptions(options); + aScript.set(JS::InstantiateGlobalStencil(aCx, instantiateOptions, mStencil)); + if (!aScript) { + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void nsXULPrototypeScript::Set(JS::Stencil* aStencil) { mStencil = aStencil; } + +//---------------------------------------------------------------------- +// +// nsXULPrototypeText +// + +nsresult nsXULPrototypeText::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv; + + // Write basic prototype data + rv = aStream->Write32(mType); + + nsresult tmp = aStream->WriteWStringZ(mValue.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + return rv; +} + +nsresult nsXULPrototypeText::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv = aStream->ReadString(mValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// +// nsXULPrototypePI +// + +nsresult nsXULPrototypePI::Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv; + + // Write basic prototype data + rv = aStream->Write32(mType); + + nsresult tmp = aStream->WriteWStringZ(mTarget.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = aStream->WriteWStringZ(mData.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + return rv; +} + +nsresult nsXULPrototypePI::Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) { + nsresult rv; + + rv = aStream->ReadString(mTarget); + if (NS_FAILED(rv)) return rv; + rv = aStream->ReadString(mData); + if (NS_FAILED(rv)) return rv; + + return rv; +} diff --git a/dom/xul/nsXULElement.h b/dom/xul/nsXULElement.h new file mode 100644 index 0000000000..532f4cd8c4 --- /dev/null +++ b/dom/xul/nsXULElement.h @@ -0,0 +1,545 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + + The base XUL element class and associates. + +*/ + +#ifndef nsXULElement_h__ +#define nsXULElement_h__ + +#include <stdint.h> +#include <stdio.h> +#include "ErrorList.h" +#include "js/experimental/JSStencil.h" +#include "js/RootingAPI.h" +#include "js/SourceText.h" +#include "js/TracingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsAtom.h" +#include "nsAttrName.h" +#include "nsAttrValue.h" +#include "nsCOMPtr.h" +#include "nsCaseTreatment.h" +#include "nsChangeHint.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsINode.h" +#include "nsISupports.h" +#include "nsLiteralString.h" +#include "nsString.h" +#include "nsStyledElement.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" +#include "nscore.h" + +class JSObject; +class nsAttrValueOrString; +class nsIControllers; +class nsIObjectInputStream; +class nsIObjectOutputStream; +class nsIOffThreadScriptReceiver; +class nsIPrincipal; +class nsIURI; +class nsXULPrototypeDocument; +class nsXULPrototypeNode; +struct JSContext; + +using nsPrototypeArray = nsTArray<RefPtr<nsXULPrototypeNode>>; + +namespace JS { +class CompileOptions; +} + +namespace mozilla { +class ErrorResult; +class EventChainPreVisitor; +class EventListenerManager; +namespace css { +class StyleRule; +} // namespace css +namespace dom { +class Document; +class HTMLIFrameElement; +class PrototypeDocumentContentSink; +enum class CallerType : uint32_t; +} // namespace dom +} // namespace mozilla + +//////////////////////////////////////////////////////////////////////// + +#ifdef XUL_PROTOTYPE_ATTRIBUTE_METERING +# define XUL_PROTOTYPE_ATTRIBUTE_METER(counter) \ + (nsXULPrototypeAttribute::counter++) +#else +# define XUL_PROTOTYPE_ATTRIBUTE_METER(counter) ((void)0) +#endif + +/** + + A prototype attribute for an nsXULPrototypeElement. + + */ + +class nsXULPrototypeAttribute { + public: + nsXULPrototypeAttribute() + : mName(nsGkAtoms::id) // XXX this is a hack, but names have to have a + // value + { + XUL_PROTOTYPE_ATTRIBUTE_METER(gNumAttributes); + MOZ_COUNT_CTOR(nsXULPrototypeAttribute); + } + + ~nsXULPrototypeAttribute(); + + nsAttrName mName; + nsAttrValue mValue; + +#ifdef XUL_PROTOTYPE_ATTRIBUTE_METERING + static uint32_t gNumElements; + static uint32_t gNumAttributes; + static uint32_t gNumCacheTests; + static uint32_t gNumCacheHits; + static uint32_t gNumCacheSets; + static uint32_t gNumCacheFills; +#endif /* !XUL_PROTOTYPE_ATTRIBUTE_METERING */ +}; + +/** + + A prototype content model element that holds the "primordial" values + that have been parsed from the original XUL document. + + */ + +class nsXULPrototypeNode { + public: + enum Type { eType_Element, eType_Script, eType_Text, eType_PI }; + + Type mType; + + virtual nsresult Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) = 0; + virtual nsresult Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) = 0; + + /** + * The prototype document must call ReleaseSubtree when it is going + * away. This makes the parents through the tree stop owning their + * children, whether or not the parent's reference count is zero. + * Individual elements may still own individual prototypes, but + * those prototypes no longer remember their children to allow them + * to be constructed. + */ + virtual void ReleaseSubtree() {} + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(nsXULPrototypeNode) + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(nsXULPrototypeNode) + + protected: + explicit nsXULPrototypeNode(Type aType) : mType(aType) {} + virtual ~nsXULPrototypeNode() = default; +}; + +class nsXULPrototypeElement : public nsXULPrototypeNode { + public: + explicit nsXULPrototypeElement(mozilla::dom::NodeInfo* aNodeInfo = nullptr) + : nsXULPrototypeNode(eType_Element), + mNodeInfo(aNodeInfo), + mHasIdAttribute(false), + mHasClassAttribute(false), + mHasStyleAttribute(false), + mIsAtom(nullptr) {} + + private: + virtual ~nsXULPrototypeElement() { Unlink(); } + + public: + virtual void ReleaseSubtree() override { + for (int32_t i = mChildren.Length() - 1; i >= 0; i--) { + if (mChildren[i].get()) mChildren[i]->ReleaseSubtree(); + } + mChildren.Clear(); + nsXULPrototypeNode::ReleaseSubtree(); + } + + virtual nsresult Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + virtual nsresult Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + + nsresult SetAttrAt(uint32_t aPos, const nsAString& aValue, + nsIURI* aDocumentURI); + + void Unlink(); + + nsPrototypeArray mChildren; + + RefPtr<mozilla::dom::NodeInfo> mNodeInfo; + + uint32_t mHasIdAttribute : 1; + uint32_t mHasClassAttribute : 1; + uint32_t mHasStyleAttribute : 1; + nsTArray<nsXULPrototypeAttribute> mAttributes; // [OWNER] + RefPtr<nsAtom> mIsAtom; +}; + +class nsXULPrototypeScript : public nsXULPrototypeNode { + public: + explicit nsXULPrototypeScript(uint32_t aLineNo); + + private: + virtual ~nsXULPrototypeScript() = default; + + void FillCompileOptions(JS::CompileOptions& options); + + public: + virtual nsresult Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + nsresult SerializeOutOfLine(nsIObjectOutputStream* aStream, + nsXULPrototypeDocument* aProtoDoc); + virtual nsresult Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + nsresult DeserializeOutOfLine(nsIObjectInputStream* aInput, + nsXULPrototypeDocument* aProtoDoc); + + template <typename Unit> + nsresult Compile(const Unit* aText, size_t aTextLength, + JS::SourceOwnership aOwnership, nsIURI* aURI, + uint32_t aLineNo, mozilla::dom::Document* aDocument, + nsIOffThreadScriptReceiver* aOffThreadReceiver = nullptr); + + void Set(JS::Stencil* aStencil); + + bool HasStencil() { return mStencil; } + + JS::Stencil* GetStencil() { return mStencil.get(); } + + nsresult InstantiateScript(JSContext* aCx, + JS::MutableHandle<JSScript*> aScript); + + nsCOMPtr<nsIURI> mSrcURI; + uint32_t mLineNo; + bool mSrcLoading; + bool mOutOfLine; + mozilla::dom::PrototypeDocumentContentSink* + mSrcLoadWaiters; // [OWNER] but not COMPtr + private: + RefPtr<JS::Stencil> mStencil; +}; + +class nsXULPrototypeText : public nsXULPrototypeNode { + public: + nsXULPrototypeText() : nsXULPrototypeNode(eType_Text) {} + + private: + virtual ~nsXULPrototypeText() = default; + + public: + virtual nsresult Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + virtual nsresult Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + + nsString mValue; +}; + +class nsXULPrototypePI : public nsXULPrototypeNode { + public: + nsXULPrototypePI() : nsXULPrototypeNode(eType_PI) {} + + private: + virtual ~nsXULPrototypePI() = default; + + public: + virtual nsresult Serialize( + nsIObjectOutputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + virtual nsresult Deserialize( + nsIObjectInputStream* aStream, nsXULPrototypeDocument* aProtoDoc, + nsIURI* aDocumentURI, + const nsTArray<RefPtr<mozilla::dom::NodeInfo>>* aNodeInfos) override; + + nsString mTarget; + nsString mData; +}; + +//////////////////////////////////////////////////////////////////////// + +/** + + The XUL element. + + */ + +#define XUL_ELEMENT_FLAG_BIT(n_) \ + NODE_FLAG_BIT(ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_)) + +// XUL element specific bits +enum { + XUL_ELEMENT_HAS_CONTENTMENU_LISTENER = XUL_ELEMENT_FLAG_BIT(0), + XUL_ELEMENT_HAS_POPUP_LISTENER = XUL_ELEMENT_FLAG_BIT(1) +}; + +ASSERT_NODE_FLAGS_SPACE(ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2); + +#undef XUL_ELEMENT_FLAG_BIT + +class nsXULElement : public nsStyledElement { + protected: + using Document = mozilla::dom::Document; + + // Use Construct to construct elements instead of this constructor. + explicit nsXULElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + public: + using Element::Blur; + using Element::Focus; + + static nsresult CreateFromPrototype(nsXULPrototypeElement* aPrototype, + Document* aDocument, bool aIsScriptable, + bool aIsRoot, + mozilla::dom::Element** aResult); + + // This is the constructor for nsXULElements. + static nsXULElement* Construct( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE(nsXULElement, kNameSpaceID_XUL) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsXULElement, nsStyledElement) + + // This doesn't work on XUL elements! You probably want + // GetXULBoolAttr(nsGkAtoms::disabled) or so. + // TODO(emilio): Maybe we should unify HTML and XUL here. + bool IsDisabled() const = delete; + + // nsINode + void GetEventTargetParent(mozilla::EventChainPreVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual nsresult PreHandleEvent( + mozilla::EventChainVisitor& aVisitor) override; + // nsIContent + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent) override; + virtual void DestroyContent() override; + virtual void DoneAddingChildren(bool aHaveNotified) override; + +#ifdef MOZ_DOM_LIST + virtual void List(FILE* out, int32_t aIndent) const override; + virtual void DumpContent(FILE* out, int32_t aIndent, + bool aDumpAll) const override {} +#endif + + MOZ_CAN_RUN_SCRIPT bool HasMenu(); + MOZ_CAN_RUN_SCRIPT void OpenMenu(bool aOpenFlag); + + MOZ_CAN_RUN_SCRIPT + virtual mozilla::Result<bool, nsresult> PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) override; + MOZ_CAN_RUN_SCRIPT void ClickWithInputSource(uint16_t aInputSource, + bool aIsTrustedEvent); + + virtual bool IsNodeOfType(uint32_t aFlags) const override; + virtual bool IsFocusableInternal(int32_t* aTabIndex, + bool aWithMouse) override; + + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + virtual nsresult Clone(mozilla::dom::NodeInfo*, + nsINode** aResult) const override; + + virtual bool IsEventAttributeNameInternal(nsAtom* aName) override; + + using DOMString = mozilla::dom::DOMString; + void GetXULAttr(nsAtom* aName, DOMString& aResult) const { + GetAttr(kNameSpaceID_None, aName, aResult); + } + void SetXULAttr(nsAtom* aName, const nsAString& aValue, + mozilla::ErrorResult& aError) { + SetAttr(aName, aValue, aError); + } + bool GetXULBoolAttr(nsAtom* aName) const { + return AttrValueIs(kNameSpaceID_None, aName, u"true"_ns, eCaseMatters); + } + void SetXULBoolAttr(nsAtom* aName, bool aValue, + mozilla::ErrorResult& aError) { + if (aValue) { + SetAttr(aName, u"true"_ns, aError); + } else { + UnsetAttr(aName, aError); + } + } + + // WebIDL API + bool Autofocus() const { return BoolAttrIsTrue(nsGkAtoms::autofocus); } + void SetAutofocus(bool aAutofocus, ErrorResult& aRv) { + SetXULBoolAttr(nsGkAtoms::autofocus, aAutofocus, aRv); + } + bool Hidden() const { return BoolAttrIsTrue(nsGkAtoms::hidden); } + void SetHidden(bool aHidden) { + SetXULBoolAttr(nsGkAtoms::hidden, aHidden, mozilla::IgnoreErrors()); + } + bool Collapsed() const { return BoolAttrIsTrue(nsGkAtoms::collapsed); } + void SetCollapsed(bool aCollapsed) { + SetXULBoolAttr(nsGkAtoms::collapsed, aCollapsed, mozilla::IgnoreErrors()); + } + void GetObserves(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::observes, aValue); + } + void SetObserves(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::observes, aValue, rv); + } + void GetMenu(DOMString& aValue) const { GetXULAttr(nsGkAtoms::menu, aValue); } + void SetMenu(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::menu, aValue, rv); + } + void GetContextMenu(DOMString& aValue) { + GetXULAttr(nsGkAtoms::contextmenu, aValue); + } + void SetContextMenu(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::contextmenu, aValue, rv); + } + void GetTooltip(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::tooltip, aValue); + } + void SetTooltip(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::tooltip, aValue, rv); + } + void GetTooltipText(DOMString& aValue) const { + GetXULAttr(nsGkAtoms::tooltiptext, aValue); + } + void SetTooltipText(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::tooltiptext, aValue, rv); + } + void GetSrc(DOMString& aValue) const { GetXULAttr(nsGkAtoms::src, aValue); } + void SetSrc(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetXULAttr(nsGkAtoms::src, aValue, rv); + } + nsIControllers* GetControllers(mozilla::ErrorResult& rv); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Click(mozilla::dom::CallerType aCallerType); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DoCommand(); + // Style() inherited from nsStyledElement + + nsINode* GetScopeChainParent() const override { + // For XUL, the parent is the parent element, if any + Element* parent = GetParentElement(); + return parent ? parent : nsStyledElement::GetScopeChainParent(); + } + + bool IsInteractiveHTMLContent() const override; + + protected: + ~nsXULElement(); + + // This can be removed if EnsureContentsGenerated dies. + friend class nsNSElementTearoff; + + // Implementation methods + nsresult EnsureContentsGenerated(void) const; + + nsresult AddPopupListener(nsAtom* aName); + + /** + * Abandon our prototype linkage, and copy all attributes locally + */ + nsresult MakeHeavyweight(nsXULPrototypeElement* aPrototype); + + virtual nsresult BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString* aValue, + bool aNotify) override; + virtual nsresult AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + + virtual void UpdateEditableState(bool aNotify) override; + + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + virtual mozilla::EventListenerManager* GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) override; + + /** + * Add a listener for the specified attribute, if appropriate. + */ + void AddListenerForAttributeIfNeeded(const nsAttrName& aName); + void AddListenerForAttributeIfNeeded(nsAtom* aLocalName); + + protected: + void AddTooltipSupport(); + void RemoveTooltipSupport(); + + // Internal accessor. This shadows the 'Slots', and returns + // appropriate value. + nsIControllers* Controllers() { + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + return slots ? slots->mControllers.get() : nullptr; + } + + bool SupportsAccessKey() const; + void RegUnRegAccessKey(bool aDoReg) override; + bool BoolAttrIsTrue(nsAtom* aName) const; + + friend nsXULElement* NS_NewBasicXULElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + friend nsresult NS_NewXULElement(mozilla::dom::Element** aResult, + mozilla::dom::NodeInfo* aNodeInfo, + mozilla::dom::FromParser aFromParser, + const nsAString* aIs); + friend void NS_TrustedNewXULElement(mozilla::dom::Element** aResult, + mozilla::dom::NodeInfo* aNodeInfo); + + static already_AddRefed<nsXULElement> CreateFromPrototype( + nsXULPrototypeElement* aPrototype, mozilla::dom::NodeInfo* aNodeInfo, + bool aIsScriptable, bool aIsRoot); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool IsEventStoppedFromAnonymousScrollbar(mozilla::EventMessage aMessage); + + MOZ_CAN_RUN_SCRIPT + nsresult DispatchXULCommand(const mozilla::EventChainVisitor& aVisitor, + nsAutoString& aCommand); +}; + +#endif // nsXULElement_h__ diff --git a/dom/xul/nsXULPopupListener.cpp b/dom/xul/nsXULPopupListener.cpp new file mode 100644 index 0000000000..825f7d15ab --- /dev/null +++ b/dom/xul/nsXULPopupListener.cpp @@ -0,0 +1,283 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + This file provides the implementation for xul popup listener which + tracks xul popups and context menus + */ + +#include "nsXULPopupListener.h" +#include "XULButtonElement.h" +#include "nsCOMPtr.h" +#include "nsGkAtoms.h" +#include "nsContentCID.h" +#include "nsContentUtils.h" +#include "nsXULPopupManager.h" +#include "nsIScriptContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsServiceManagerUtils.h" +#include "nsLayoutUtils.h" +#include "mozilla/ReflowInput.h" +#include "nsIObjectLoadingContent.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" // for Event +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/MouseEventBinding.h" + +// for event firing in context menus +#include "nsPresContext.h" +#include "nsFocusManager.h" +#include "nsPIDOMWindow.h" +#include "nsViewManager.h" +#include "nsError.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// on win32 and os/2, context menus come up on mouse up. On other platforms, +// they appear on mouse down. Certain bits of code care about this difference. +#if defined(XP_WIN) +# define NS_CONTEXT_MENU_IS_MOUSEUP 1 +#endif + +nsXULPopupListener::nsXULPopupListener(mozilla::dom::Element* aElement, + bool aIsContext) + : mElement(aElement), mPopupContent(nullptr), mIsContext(aIsContext) {} + +nsXULPopupListener::~nsXULPopupListener(void) { ClosePopup(); } + +NS_IMPL_CYCLE_COLLECTION(nsXULPopupListener, mElement, mPopupContent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener) + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXULPopupListener) + // If the owner, mElement, can be skipped, so can we. + if (tmp->mElement) { + return mozilla::dom::FragmentOrElement::CanSkip(tmp->mElement, true); + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXULPopupListener) + if (tmp->mElement) { + return mozilla::dom::FragmentOrElement::CanSkipInCC(tmp->mElement); + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXULPopupListener) + if (tmp->mElement) { + return mozilla::dom::FragmentOrElement::CanSkipThis(tmp->mElement); + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +nsresult nsXULPopupListener::HandleEvent(Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + + if (!((eventType.EqualsLiteral("mousedown") && !mIsContext) || + (eventType.EqualsLiteral("contextmenu") && mIsContext))) + return NS_OK; + + MouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (!mouseEvent) { + // non-ui event passed in. bad things. + return NS_OK; + } + + // Get the node that was clicked on. + nsCOMPtr<nsIContent> targetContent = + nsIContent::FromEventTargetOrNull(mouseEvent->GetTarget()); + if (!targetContent) { + return NS_OK; + } + + if (nsIContent* content = + nsIContent::FromEventTargetOrNull(mouseEvent->GetOriginalTarget())) { + if (EventStateManager::IsTopLevelRemoteTarget(content)) { + return NS_OK; + } + } + + bool preventDefault = mouseEvent->DefaultPrevented(); + if (preventDefault && mIsContext) { + // Someone called preventDefault on a context menu. + // Let's make sure they are allowed to do so. + bool eventEnabled = + Preferences::GetBool("dom.event.contextmenu.enabled", true); + if (!eventEnabled) { + // The user wants his contextmenus. Let's make sure that this is a + // website and not chrome since there could be places in chrome which + // don't want contextmenus. + if (!targetContent->NodePrincipal()->IsSystemPrincipal()) { + // This isn't chrome. Cancel the preventDefault() and + // let the event go forth. + preventDefault = false; + } + } + } + + if (preventDefault) { + // someone called preventDefault. bail. + return NS_OK; + } + + // prevent popups on menu and menuitems as they handle their own popups + // This was added for bug 96920. + // If a menu item child was clicked on that leads to a popup needing + // to show, we know (guaranteed) that we're dealing with a menu or + // submenu of an already-showing popup. We don't need to do anything at all. + if (!mIsContext && + targetContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem)) { + return NS_OK; + } + + if (!mIsContext && mouseEvent->Button() != 0) { + // Only open popups when the left mouse button is down. + return NS_OK; + } + + // Open the popup. LaunchPopup will call StopPropagation and PreventDefault + // in the right situations. + LaunchPopup(mouseEvent); + + return NS_OK; +} + +// ClosePopup +// +// Do everything needed to shut down the popup. +// +// NOTE: This routine is safe to call even if the popup is already closed. +// +void nsXULPopupListener::ClosePopup() { + if (mPopupContent) { + // this is called when the listener is going away, so make sure that the + // popup is hidden. Use asynchronous hiding just to be safe so we don't + // fire events during destruction. + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) pm->HidePopup(mPopupContent, false, true, true, false); + mPopupContent = nullptr; // release the popup + } +} // ClosePopup + +static already_AddRefed<Element> GetImmediateChild(nsIContent* aContent, + nsAtom* aTag) { + for (nsIContent* child = aContent->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(aTag)) { + RefPtr<Element> ret = child->AsElement(); + return ret.forget(); + } + } + + return nullptr; +} + +// +// LaunchPopup +// +// Given the element on which the event was triggered and the mouse locations in +// Client and widget coordinates, popup a new window showing the appropriate +// content. +// +// aTargetContent is the target of the mouse event aEvent that triggered the +// popup. mElement is the element that the popup menu is attached to. +// aTargetContent may be equal to mElement or it may be a descendant. +// +// This looks for an attribute on |mElement| of the appropriate popup type +// (popup, context) and uses that attribute's value as an ID for +// the popup content in the document. +// +nsresult nsXULPopupListener::LaunchPopup(MouseEvent* aEvent) { + nsresult rv = NS_OK; + + nsAutoString identifier; + nsAtom* type = mIsContext ? nsGkAtoms::context : nsGkAtoms::popup; + bool hasPopupAttr = mElement->GetAttr(kNameSpaceID_None, type, identifier); + + if (identifier.IsEmpty()) { + hasPopupAttr = + mElement->GetAttr(kNameSpaceID_None, + mIsContext ? nsGkAtoms::contextmenu : nsGkAtoms::menu, + identifier) || + hasPopupAttr; + } + + if (hasPopupAttr) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + } + + if (identifier.IsEmpty()) return rv; + + // Try to find the popup content and the document. + nsCOMPtr<Document> document = mElement->GetComposedDoc(); + if (!document) { + NS_WARNING("No document!"); + return NS_ERROR_FAILURE; + } + + // Handle the _child case for popups and context menus + RefPtr<Element> popup; + if (identifier.EqualsLiteral("_child")) { + popup = GetImmediateChild(mElement, nsGkAtoms::menupopup); + } else if (!mElement->IsInUncomposedDoc() || + !(popup = document->GetElementById(identifier))) { + // XXXsmaug Should we try to use ShadowRoot::GetElementById in case + // mElement is in shadow DOM? + // + // Use getElementById to obtain the popup content and gracefully fail if + // we didn't find any popup content in the document. + NS_WARNING("GetElementById had some kind of spasm."); + return rv; + } + + // return if no popup was found or the popup is the element itself. + if (!popup || popup == mElement) { + return NS_OK; + } + + // Submenus can't be used as context menus or popups, bug 288763. + // Similar code also in nsXULTooltipListener::GetTooltipFor. + if (auto* button = XULButtonElement::FromNodeOrNull(popup->GetParent())) { + if (button->IsMenu()) { + return NS_OK; + } + } + + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (!pm) return NS_OK; + + // For left-clicks, if the popup has an position attribute, or both the + // popupanchor and popupalign attributes are used, anchor the popup to the + // element, otherwise just open it at the screen position where the mouse + // was clicked. Context menus always open at the mouse position. + mPopupContent = popup; + if (!mIsContext && + (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::position) || + (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) && + mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) { + pm->ShowPopup(mPopupContent, mElement, u""_ns, 0, 0, false, true, false, + aEvent); + } else { + CSSIntPoint pos = aEvent->ScreenPoint(CallerType::System); + pm->ShowPopupAtScreen(mPopupContent, pos.x, pos.y, mIsContext, aEvent); + } + + return NS_OK; +} diff --git a/dom/xul/nsXULPopupListener.h b/dom/xul/nsXULPopupListener.h new file mode 100644 index 0000000000..ac5a631118 --- /dev/null +++ b/dom/xul/nsXULPopupListener.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/** + * This is the popup listener implementation for popup menus and context menus. + */ + +#ifndef nsXULPopupListener_h___ +#define nsXULPopupListener_h___ + +#include "nsCOMPtr.h" + +#include "nsIDOMEventListener.h" +#include "nsCycleCollectionParticipant.h" + +class nsIContent; + +namespace mozilla::dom { +class Element; +class MouseEvent; +} // namespace mozilla::dom + +class nsXULPopupListener : public nsIDOMEventListener { + public: + // aElement is the element that the popup is attached to. If aIsContext is + // false, the popup opens on left click on aElement or a descendant. If + // aIsContext is true, the popup is a context menu which opens on a + // context menu event. + nsXULPopupListener(mozilla::dom::Element* aElement, bool aIsContext); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_CLASS(nsXULPopupListener) + NS_DECL_NSIDOMEVENTLISTENER + + protected: + virtual ~nsXULPopupListener(void); + + // open the popup. aEvent is the event that triggered the popup such as + // a mouse click and aTargetContent is the target of this event. + virtual nsresult LaunchPopup(mozilla::dom::MouseEvent* aEvent); + + // close the popup when the listener goes away + virtual void ClosePopup(); + + private: + // |mElement| is the node to which this listener is attached. + RefPtr<mozilla::dom::Element> mElement; + + // The popup that is getting shown on top of mElement. + RefPtr<mozilla::dom::Element> mPopupContent; + + // true if a context popup + bool mIsContext; +}; + +#endif // nsXULPopupListener_h___ diff --git a/dom/xul/nsXULPrototypeCache.cpp b/dom/xul/nsXULPrototypeCache.cpp new file mode 100644 index 0000000000..fc58cd28f5 --- /dev/null +++ b/dom/xul/nsXULPrototypeCache.cpp @@ -0,0 +1,501 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsXULPrototypeCache.h" + +#include "plstr.h" +#include "nsXULPrototypeDocument.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIObserverService.h" +#include "nsIStorageStream.h" + +#include "nsAppDirectoryServiceDefs.h" + +#include "js/experimental/JSStencil.h" +#include "js/TracingAPI.h" + +#include "mozilla/StyleSheetInlines.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_nglayout.h" +#include "mozilla/scache/StartupCache.h" +#include "mozilla/scache/StartupCacheUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/intl/LocaleService.h" + +using namespace mozilla; +using namespace mozilla::scache; +using mozilla::intl::LocaleService; + +static const char kXULCacheInfoKey[] = "nsXULPrototypeCache.startupCache"; +#define CACHE_PREFIX(aCompilationTarget) "xulcache/" aCompilationTarget + +static void DisableXULCacheChangedCallback(const char* aPref, void* aClosure) { + if (nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance()) { + if (!cache->IsEnabled()) { + // AbortCaching() calls Flush() for us. + cache->AbortCaching(); + } + } +} + +//---------------------------------------------------------------------- + +nsXULPrototypeCache* nsXULPrototypeCache::sInstance = nullptr; + +nsXULPrototypeCache::nsXULPrototypeCache() = default; + +NS_IMPL_ISUPPORTS(nsXULPrototypeCache, nsIObserver) + +/* static */ +nsXULPrototypeCache* nsXULPrototypeCache::GetInstance() { + if (!sInstance) { + NS_ADDREF(sInstance = new nsXULPrototypeCache()); + + Preferences::RegisterCallback( + DisableXULCacheChangedCallback, + nsDependentCString( + StaticPrefs::GetPrefName_nglayout_debug_disable_xul_cache())); + + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + nsXULPrototypeCache* p = sInstance; + obsSvc->AddObserver(p, "chrome-flush-caches", false); + obsSvc->AddObserver(p, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + obsSvc->AddObserver(p, "startupcache-invalidate", false); + } + } + return sInstance; +} + +//---------------------------------------------------------------------- + +NS_IMETHODIMP +nsXULPrototypeCache::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "chrome-flush-caches") || + !strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Flush(); + } else if (!strcmp(aTopic, "startupcache-invalidate")) { + AbortCaching(); + } else { + NS_WARNING("Unexpected observer topic."); + } + return NS_OK; +} + +nsXULPrototypeDocument* nsXULPrototypeCache::GetPrototype(nsIURI* aURI) { + if (!aURI) return nullptr; + + nsCOMPtr<nsIURI> uriWithoutRef; + NS_GetURIWithoutRef(aURI, getter_AddRefs(uriWithoutRef)); + + nsXULPrototypeDocument* protoDoc = mPrototypeTable.GetWeak(uriWithoutRef); + if (protoDoc) { + return protoDoc; + } + + nsresult rv = BeginCaching(aURI); + if (NS_FAILED(rv)) return nullptr; + + // No prototype in XUL memory cache. Spin up the cache Service. + nsCOMPtr<nsIObjectInputStream> ois; + rv = GetPrototypeInputStream(aURI, getter_AddRefs(ois)); + if (NS_FAILED(rv)) { + return nullptr; + } + + RefPtr<nsXULPrototypeDocument> newProto; + rv = NS_NewXULPrototypeDocument(getter_AddRefs(newProto)); + if (NS_FAILED(rv)) return nullptr; + + rv = newProto->Read(ois); + if (NS_SUCCEEDED(rv)) { + rv = PutPrototype(newProto); + } else { + newProto = nullptr; + } + + mInputStreamTable.Remove(aURI); + return newProto; +} + +nsresult nsXULPrototypeCache::PutPrototype(nsXULPrototypeDocument* aDocument) { + if (!aDocument->GetURI()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIURI> uri; + NS_GetURIWithoutRef(aDocument->GetURI(), getter_AddRefs(uri)); + + // Put() releases any old value + mPrototypeTable.InsertOrUpdate(uri, RefPtr{aDocument}); + + return NS_OK; +} + +JS::Stencil* nsXULPrototypeCache::GetStencil(nsIURI* aURI) { + if (auto* entry = mStencilTable.GetEntry(aURI)) { + return entry->mStencil; + } + return nullptr; +} + +nsresult nsXULPrototypeCache::PutStencil(nsIURI* aURI, JS::Stencil* aStencil) { + MOZ_ASSERT(aStencil, "Need a non-NULL stencil"); + +#ifdef DEBUG_BUG_392650 + if (mStencilTable.Get(aURI)) { + nsAutoCString scriptName; + aURI->GetSpec(scriptName); + nsAutoCString message("Loaded script "); + message += scriptName; + message += " twice (bug 392650)"; + NS_WARNING(message.get()); + } +#endif + + mStencilTable.PutEntry(aURI)->mStencil = aStencil; + + return NS_OK; +} + +void nsXULPrototypeCache::Flush() { + mPrototypeTable.Clear(); + mStencilTable.Clear(); +} + +bool nsXULPrototypeCache::IsEnabled() { + return !StaticPrefs::nglayout_debug_disable_xul_cache(); +} + +void nsXULPrototypeCache::AbortCaching() { + // Flush the XUL cache for good measure, in case we cached a bogus/downrev + // script, somehow. + Flush(); + + // Clear the cache set + mStartupCacheURITable.Clear(); +} + +nsresult nsXULPrototypeCache::WritePrototype( + nsXULPrototypeDocument* aPrototypeDocument) { + nsresult rv = NS_OK, rv2 = NS_OK; + + if (!StartupCache::GetSingleton()) return NS_OK; + + nsCOMPtr<nsIURI> protoURI = aPrototypeDocument->GetURI(); + + nsCOMPtr<nsIObjectOutputStream> oos; + rv = GetPrototypeOutputStream(protoURI, getter_AddRefs(oos)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aPrototypeDocument->Write(oos); + NS_ENSURE_SUCCESS(rv, rv); + FinishPrototypeOutputStream(protoURI); + return NS_FAILED(rv) ? rv : rv2; +} + +static nsresult PathifyURIForType(nsXULPrototypeCache::CacheType cacheType, + nsIURI* in, nsACString& out) { + switch (cacheType) { + case nsXULPrototypeCache::CacheType::Prototype: + return PathifyURI(CACHE_PREFIX("proto"), in, out); + case nsXULPrototypeCache::CacheType::Script: + return PathifyURI(CACHE_PREFIX("script"), in, out); + } + MOZ_ASSERT_UNREACHABLE("unknown cache type?"); + return NS_ERROR_UNEXPECTED; +} + +nsresult nsXULPrototypeCache::GetInputStream(CacheType cacheType, nsIURI* uri, + nsIObjectInputStream** stream) { + nsAutoCString spec; + nsresult rv = PathifyURIForType(cacheType, uri, spec); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + + const char* buf; + uint32_t len; + nsCOMPtr<nsIObjectInputStream> ois; + StartupCache* sc = StartupCache::GetSingleton(); + if (!sc) return NS_ERROR_NOT_AVAILABLE; + + rv = sc->GetBuffer(spec.get(), &buf, &len); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + + rv = NewObjectInputStreamFromBuffer(buf, len, getter_AddRefs(ois)); + NS_ENSURE_SUCCESS(rv, rv); + + mInputStreamTable.InsertOrUpdate(uri, ois); + + ois.forget(stream); + return NS_OK; +} + +nsresult nsXULPrototypeCache::FinishInputStream(nsIURI* uri) { + mInputStreamTable.Remove(uri); + return NS_OK; +} + +nsresult nsXULPrototypeCache::GetOutputStream(nsIURI* uri, + nsIObjectOutputStream** stream) { + nsresult rv; + nsCOMPtr<nsIObjectOutputStream> objectOutput; + nsCOMPtr<nsIStorageStream> storageStream; + bool found = mOutputStreamTable.Get(uri, getter_AddRefs(storageStream)); + if (found) { + // Setting an output stream here causes crashes on Windows. The previous + // version of this code always returned NS_ERROR_OUT_OF_MEMORY here, + // because it used a mistyped contract ID to create its object stream. + return NS_ERROR_NOT_IMPLEMENTED; +#if 0 + nsCOMPtr<nsIOutputStream> outputStream + = do_QueryInterface(storageStream); + objectOutput = NS_NewObjectOutputStream(outputStream); +#endif + } else { + rv = NewObjectOutputWrappedStorageStream( + getter_AddRefs(objectOutput), getter_AddRefs(storageStream), false); + NS_ENSURE_SUCCESS(rv, rv); + mOutputStreamTable.InsertOrUpdate(uri, storageStream); + } + objectOutput.forget(stream); + return NS_OK; +} + +nsresult nsXULPrototypeCache::FinishOutputStream(CacheType cacheType, + nsIURI* uri) { + nsresult rv; + StartupCache* sc = StartupCache::GetSingleton(); + if (!sc) return NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr<nsIStorageStream> storageStream; + bool found = mOutputStreamTable.Get(uri, getter_AddRefs(storageStream)); + if (!found) return NS_ERROR_UNEXPECTED; + nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(storageStream); + outputStream->Close(); + + UniqueFreePtr<char[]> buf; + uint32_t len; + rv = NewBufferFromStorageStream(storageStream, &buf, &len); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mStartupCacheURITable.GetEntry(uri)) { + nsAutoCString spec; + rv = PathifyURIForType(cacheType, uri, spec); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + rv = sc->PutBuffer(spec.get(), std::move(buf), len); + if (NS_SUCCEEDED(rv)) { + mOutputStreamTable.Remove(uri); + mStartupCacheURITable.PutEntry(uri); + } + } + + return rv; +} + +// We have data if we're in the middle of writing it or we already +// have it in the cache. +nsresult nsXULPrototypeCache::HasData(CacheType cacheType, nsIURI* uri, + bool* exists) { + if (mOutputStreamTable.Get(uri, nullptr)) { + *exists = true; + return NS_OK; + } + nsAutoCString spec; + nsresult rv = PathifyURIForType(cacheType, uri, spec); + if (NS_FAILED(rv)) { + *exists = false; + return NS_OK; + } + UniquePtr<char[]> buf; + StartupCache* sc = StartupCache::GetSingleton(); + if (sc) { + *exists = sc->HasEntry(spec.get()); + } else { + *exists = false; + } + return NS_OK; +} + +nsresult nsXULPrototypeCache::BeginCaching(nsIURI* aURI) { + nsresult rv, tmp; + + nsAutoCString path; + aURI->GetPathQueryRef(path); + if (!(StringEndsWith(path, ".xul"_ns) || StringEndsWith(path, ".xhtml"_ns))) { + return NS_ERROR_NOT_AVAILABLE; + } + + StartupCache* startupCache = StartupCache::GetSingleton(); + if (!startupCache) return NS_ERROR_FAILURE; + + if (StaticPrefs::nglayout_debug_disable_xul_cache()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Get the chrome directory to validate against the one stored in the + // cache file, or to store there if we're generating a new file. + nsCOMPtr<nsIFile> chromeDir; + rv = NS_GetSpecialDirectory(NS_APP_CHROME_DIR, getter_AddRefs(chromeDir)); + if (NS_FAILED(rv)) return rv; + nsAutoCString chromePath; + rv = chromeDir->GetPersistentDescriptor(chromePath); + if (NS_FAILED(rv)) return rv; + + // XXXbe we assume the first package's locale is the same as the locale of + // all subsequent packages of cached chrome URIs.... + nsAutoCString package; + rv = aURI->GetHost(package); + if (NS_FAILED(rv)) return rv; + nsAutoCString locale; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale); + + nsAutoCString fileChromePath, fileLocale; + + const char* buf = nullptr; + uint32_t len, amtRead; + nsCOMPtr<nsIObjectInputStream> objectInput; + + rv = startupCache->GetBuffer(kXULCacheInfoKey, &buf, &len); + if (NS_SUCCEEDED(rv)) + rv = NewObjectInputStreamFromBuffer(buf, len, getter_AddRefs(objectInput)); + + if (NS_SUCCEEDED(rv)) { + rv = objectInput->ReadCString(fileLocale); + tmp = objectInput->ReadCString(fileChromePath); + if (NS_FAILED(tmp)) { + rv = tmp; + } + if (NS_FAILED(rv) || + (!fileChromePath.Equals(chromePath) || !fileLocale.Equals(locale))) { + // Our cache won't be valid in this case, we'll need to rewrite. + // XXX This blows away work that other consumers (like + // mozJSModuleLoader) have done, need more fine-grained control. + startupCache->InvalidateCache(); + mStartupCacheURITable.Clear(); + rv = NS_ERROR_UNEXPECTED; + } + } else if (rv != NS_ERROR_NOT_AVAILABLE) + // NS_ERROR_NOT_AVAILABLE is normal, usually if there's no cachefile. + return rv; + + if (NS_FAILED(rv)) { + // Either the cache entry was invalid or it didn't exist, so write it now. + nsCOMPtr<nsIObjectOutputStream> objectOutput; + nsCOMPtr<nsIInputStream> inputStream; + nsCOMPtr<nsIStorageStream> storageStream; + rv = NewObjectOutputWrappedStorageStream( + getter_AddRefs(objectOutput), getter_AddRefs(storageStream), false); + if (NS_SUCCEEDED(rv)) { + rv = objectOutput->WriteStringZ(locale.get()); + tmp = objectOutput->WriteStringZ(chromePath.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = objectOutput->Close(); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = storageStream->NewInputStream(0, getter_AddRefs(inputStream)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + if (NS_SUCCEEDED(rv)) { + uint64_t len64; + rv = inputStream->Available(&len64); + if (NS_SUCCEEDED(rv)) { + if (len64 <= UINT32_MAX) + len = (uint32_t)len64; + else + rv = NS_ERROR_FILE_TOO_BIG; + } + } + + if (NS_SUCCEEDED(rv)) { + auto putBuf = UniqueFreePtr<char[]>( + reinterpret_cast<char*>(malloc(sizeof(char) * len))); + rv = inputStream->Read(putBuf.get(), len, &amtRead); + if (NS_SUCCEEDED(rv) && len == amtRead) + rv = startupCache->PutBuffer(kXULCacheInfoKey, std::move(putBuf), len); + else { + rv = NS_ERROR_UNEXPECTED; + } + } + + // Failed again, just bail. + if (NS_FAILED(rv)) { + startupCache->InvalidateCache(); + mStartupCacheURITable.Clear(); + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +void nsXULPrototypeCache::MarkInCCGeneration(uint32_t aGeneration) { + for (const auto& prototype : mPrototypeTable.Values()) { + prototype->MarkInCCGeneration(aGeneration); + } +} + +MOZ_DEFINE_MALLOC_SIZE_OF(CacheMallocSizeOf) + +static void ReportSize(const nsCString& aPath, size_t aAmount, + const nsCString& aDescription, + nsIHandleReportCallback* aHandleReport, + nsISupports* aData) { + nsAutoCString path("explicit/xul-prototype-cache/"); + path += aPath; + aHandleReport->Callback(""_ns, path, nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, aAmount, aDescription, + aData); +} + +/* static */ +void nsXULPrototypeCache::CollectMemoryReports( + nsIHandleReportCallback* aHandleReport, nsISupports* aData) { + if (!sInstance) { + return; + } + + MallocSizeOf mallocSizeOf = CacheMallocSizeOf; + size_t other = mallocSizeOf(sInstance); + +#define REPORT_SIZE(_path, _amount, _desc) \ + ReportSize(_path, _amount, nsLiteralCString(_desc), aHandleReport, aData) + + other += sInstance->mPrototypeTable.ShallowSizeOfExcludingThis(mallocSizeOf); + // TODO Report content in mPrototypeTable? + + other += sInstance->mStencilTable.ShallowSizeOfExcludingThis(mallocSizeOf); + // TODO Report content inside mStencilTable? + + other += + sInstance->mStartupCacheURITable.ShallowSizeOfExcludingThis(mallocSizeOf); + + other += + sInstance->mOutputStreamTable.ShallowSizeOfExcludingThis(mallocSizeOf); + other += + sInstance->mInputStreamTable.ShallowSizeOfExcludingThis(mallocSizeOf); + + REPORT_SIZE("other"_ns, other, + "Memory used by " + "the instance and tables of the XUL prototype cache."); + +#undef REPORT_SIZE +} diff --git a/dom/xul/nsXULPrototypeCache.h b/dom/xul/nsXULPrototypeCache.h new file mode 100644 index 0000000000..bf5370381a --- /dev/null +++ b/dom/xul/nsXULPrototypeCache.h @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsXULPrototypeCache_h__ +#define nsXULPrototypeCache_h__ + +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsInterfaceHashtable.h" +#include "nsRefPtrHashtable.h" +#include "nsURIHashKey.h" +#include "nsXULPrototypeDocument.h" +#include "nsIStorageStream.h" + +#include "mozilla/scache/StartupCache.h" +#include "js/experimental/JSStencil.h" +#include "mozilla/RefPtr.h" + +class nsIHandleReportCallback; +namespace mozilla { +class StyleSheet; +} // namespace mozilla + +/** + * The XUL prototype cache can be used to store and retrieve shared data for + * XUL documents, style sheets, XBL, and scripts. + * + * The cache has two levels: + * 1. In-memory hashtables + * 2. The on-disk cache file. + */ +class nsXULPrototypeCache : public nsIObserver { + public: + enum class CacheType { Prototype, Script }; + + // nsISupports + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + bool IsCached(nsIURI* aURI) { return GetPrototype(aURI) != nullptr; } + void AbortCaching(); + + /** + * Whether the prototype cache is enabled. + */ + bool IsEnabled(); + + /** + * Flush the cache; remove all XUL prototype documents, style + * sheets, and scripts. + */ + void Flush(); + + // The following methods are used to put and retrive various items into and + // from the cache. + + nsXULPrototypeDocument* GetPrototype(nsIURI* aURI); + nsresult PutPrototype(nsXULPrototypeDocument* aDocument); + + JS::Stencil* GetStencil(nsIURI* aURI); + nsresult PutStencil(nsIURI* aURI, JS::Stencil* aStencil); + + /** + * Write the XUL prototype document to a cache file. The proto must be + * fully loaded. + */ + nsresult WritePrototype(nsXULPrototypeDocument* aPrototypeDocument); + + /** + * This interface allows partial reads and writes from the buffers in the + * startupCache. + */ + + inline nsresult GetPrototypeInputStream(nsIURI* aURI, + nsIObjectInputStream** objectInput) { + return GetInputStream(CacheType::Prototype, aURI, objectInput); + } + inline nsresult GetScriptInputStream(nsIURI* aURI, + nsIObjectInputStream** objectInput) { + return GetInputStream(CacheType::Script, aURI, objectInput); + } + inline nsresult FinishScriptInputStream(nsIURI* aURI) { + return FinishInputStream(aURI); + } + + inline nsresult GetPrototypeOutputStream( + nsIURI* aURI, nsIObjectOutputStream** objectOutput) { + return GetOutputStream(aURI, objectOutput); + } + inline nsresult GetScriptOutputStream(nsIURI* aURI, + nsIObjectOutputStream** objectOutput) { + return GetOutputStream(aURI, objectOutput); + } + + inline nsresult FinishPrototypeOutputStream(nsIURI* aURI) { + return FinishOutputStream(CacheType::Prototype, aURI); + } + inline nsresult FinishScriptOutputStream(nsIURI* aURI) { + return FinishOutputStream(CacheType::Script, aURI); + } + + inline nsresult HasPrototype(nsIURI* aURI, bool* exists) { + return HasData(CacheType::Prototype, aURI, exists); + } + inline nsresult HasScript(nsIURI* aURI, bool* exists) { + return HasData(CacheType::Script, aURI, exists); + } + + private: + nsresult GetInputStream(CacheType cacheType, nsIURI* uri, + nsIObjectInputStream** stream); + nsresult FinishInputStream(nsIURI* aURI); + + nsresult GetOutputStream(nsIURI* aURI, nsIObjectOutputStream** objectOutput); + nsresult FinishOutputStream(CacheType cacheType, nsIURI* aURI); + nsresult HasData(CacheType cacheType, nsIURI* aURI, bool* exists); + + public: + static nsXULPrototypeCache* GetInstance(); + static nsXULPrototypeCache* MaybeGetInstance() { return sInstance; } + + static void ReleaseGlobals() { NS_IF_RELEASE(sInstance); } + + void MarkInCCGeneration(uint32_t aGeneration); + + static void CollectMemoryReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData); + + protected: + friend nsresult NS_NewXULPrototypeCache(REFNSIID aIID, void** aResult); + + nsXULPrototypeCache(); + virtual ~nsXULPrototypeCache() = default; + + static nsXULPrototypeCache* sInstance; + + nsRefPtrHashtable<nsURIHashKey, nsXULPrototypeDocument> + mPrototypeTable; // owns the prototypes + + class StencilHashKey : public nsURIHashKey { + public: + explicit StencilHashKey(const nsIURI* aKey) : nsURIHashKey(aKey) {} + StencilHashKey(StencilHashKey&&) = default; + + RefPtr<JS::Stencil> mStencil; + }; + + nsTHashtable<StencilHashKey> mStencilTable; + + // URIs already written to the startup cache, to prevent double-caching. + nsTHashtable<nsURIHashKey> mStartupCacheURITable; + + nsInterfaceHashtable<nsURIHashKey, nsIStorageStream> mOutputStreamTable; + nsInterfaceHashtable<nsURIHashKey, nsIObjectInputStream> mInputStreamTable; + + // Bootstrap caching service + nsresult BeginCaching(nsIURI* aDocumentURI); +}; + +#endif // nsXULPrototypeCache_h__ diff --git a/dom/xul/nsXULPrototypeDocument.cpp b/dom/xul/nsXULPrototypeDocument.cpp new file mode 100644 index 0000000000..e9d291f532 --- /dev/null +++ b/dom/xul/nsXULPrototypeDocument.cpp @@ -0,0 +1,516 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsXULPrototypeDocument.h" + +#include "nsXULElement.h" +#include "nsAString.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIPrincipal.h" +#include "nsJSPrincipals.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIURI.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsString.h" +#include "nsDOMCID.h" +#include "nsNodeInfoManager.h" +#include "nsContentUtils.h" +#include "nsCCUncollectableMarker.h" +#include "xpcpublic.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/BindingUtils.h" +#include "nsXULPrototypeCache.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Text.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::dom::DestroyProtoAndIfaceCache; + +uint32_t nsXULPrototypeDocument::gRefCnt; + +//---------------------------------------------------------------------- +// +// ctors, dtors, n' stuff +// + +nsXULPrototypeDocument::nsXULPrototypeDocument() + : mRoot(nullptr), mLoaded(false), mCCGeneration(0), mWasL10nCached(false) { + ++gRefCnt; +} + +nsresult nsXULPrototypeDocument::Init() { + mNodeInfoManager = new nsNodeInfoManager(nullptr); + return NS_OK; +} + +nsXULPrototypeDocument::~nsXULPrototypeDocument() { + if (mRoot) mRoot->ReleaseSubtree(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULPrototypeDocument) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULPrototypeDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrototypeWaiters) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULPrototypeDocument) + if (nsCCUncollectableMarker::InGeneration(cb, tmp->mCCGeneration)) { + return NS_SUCCESS_INTERRUPTED_TRAVERSE; + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNodeInfoManager) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPrototypeDocument) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPrototypeDocument) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPrototypeDocument) + +NS_IMETHODIMP +NS_NewXULPrototypeDocument(nsXULPrototypeDocument** aResult) { + *aResult = nullptr; + RefPtr<nsXULPrototypeDocument> doc = new nsXULPrototypeDocument(); + + nsresult rv = doc->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + doc.forget(aResult); + return rv; +} + +//---------------------------------------------------------------------- +// +// nsISerializable methods +// + +NS_IMETHODIMP +nsXULPrototypeDocument::Read(nsIObjectInputStream* aStream) { + nsCOMPtr<nsISupports> supports; + nsresult rv = aStream->ReadObject(true, getter_AddRefs(supports)); + if (NS_FAILED(rv)) { + return rv; + } + mURI = do_QueryInterface(supports); + + // nsIPrincipal mNodeInfoManager->mPrincipal + nsAutoCString JSON; + rv = aStream->ReadCString(JSON); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr<nsIPrincipal> principal = mozilla::BasePrincipal::FromJSON(JSON); + + // Better safe than sorry.... + mNodeInfoManager->SetDocumentPrincipal(principal); + + rv = aStream->ReadBoolean(&mWasL10nCached); + if (NS_FAILED(rv)) { + return rv; + } + + mRoot = new nsXULPrototypeElement(); + + // mozilla::dom::NodeInfo table + nsTArray<RefPtr<mozilla::dom::NodeInfo>> nodeInfos; + + uint32_t count, i; + rv = aStream->Read32(&count); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoString namespaceURI, prefixStr, localName; + bool prefixIsNull; + RefPtr<nsAtom> prefix; + for (i = 0; i < count; ++i) { + rv = aStream->ReadString(namespaceURI); + if (NS_FAILED(rv)) { + return rv; + } + rv = aStream->ReadBoolean(&prefixIsNull); + if (NS_FAILED(rv)) { + return rv; + } + if (prefixIsNull) { + prefix = nullptr; + } else { + rv = aStream->ReadString(prefixStr); + if (NS_FAILED(rv)) { + return rv; + } + prefix = NS_Atomize(prefixStr); + } + rv = aStream->ReadString(localName); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + // Using UINT16_MAX here as we don't know which nodeinfos will be + // used for attributes and which for elements. And that doesn't really + // matter. + rv = mNodeInfoManager->GetNodeInfo(localName, prefix, namespaceURI, + UINT16_MAX, getter_AddRefs(nodeInfo)); + if (NS_FAILED(rv)) { + return rv; + } + nodeInfos.AppendElement(nodeInfo); + } + + // Document contents + uint32_t type; + while (NS_SUCCEEDED(rv)) { + rv = aStream->Read32(&type); + if (NS_FAILED(rv)) { + return rv; + break; + } + + if ((nsXULPrototypeNode::Type)type == nsXULPrototypeNode::eType_PI) { + RefPtr<nsXULPrototypePI> pi = new nsXULPrototypePI(); + + rv = pi->Deserialize(aStream, this, mURI, &nodeInfos); + if (NS_FAILED(rv)) { + return rv; + } + rv = AddProcessingInstruction(pi); + if (NS_FAILED(rv)) { + return rv; + } + } else if ((nsXULPrototypeNode::Type)type == + nsXULPrototypeNode::eType_Element) { + rv = mRoot->Deserialize(aStream, this, mURI, &nodeInfos); + if (NS_FAILED(rv)) { + return rv; + } + break; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected prototype node type"); + return NS_ERROR_FAILURE; + } + } + + return NotifyLoadDone(); +} + +static nsresult GetNodeInfos(nsXULPrototypeElement* aPrototype, + nsTArray<RefPtr<mozilla::dom::NodeInfo>>& aArray) { + if (aArray.IndexOf(aPrototype->mNodeInfo) == aArray.NoIndex) { + aArray.AppendElement(aPrototype->mNodeInfo); + } + + // Search attributes + size_t i; + for (i = 0; i < aPrototype->mAttributes.Length(); ++i) { + RefPtr<mozilla::dom::NodeInfo> ni; + nsAttrName* name = &aPrototype->mAttributes[i].mName; + if (name->IsAtom()) { + ni = aPrototype->mNodeInfo->NodeInfoManager()->GetNodeInfo( + name->Atom(), nullptr, kNameSpaceID_None, nsINode::ATTRIBUTE_NODE); + } else { + ni = name->NodeInfo(); + } + + if (aArray.IndexOf(ni) == aArray.NoIndex) { + aArray.AppendElement(ni); + } + } + + // Search children + for (i = 0; i < aPrototype->mChildren.Length(); ++i) { + nsXULPrototypeNode* child = aPrototype->mChildren[i]; + if (child->mType == nsXULPrototypeNode::eType_Element) { + nsresult rv = + GetNodeInfos(static_cast<nsXULPrototypeElement*>(child), aArray); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsXULPrototypeDocument::Write(nsIObjectOutputStream* aStream) { + nsresult rv; + + rv = aStream->WriteCompoundObject(mURI, NS_GET_IID(nsIURI), true); + + // nsIPrincipal mNodeInfoManager->mPrincipal + nsAutoCString JSON; + mozilla::BasePrincipal::Cast(mNodeInfoManager->DocumentPrincipal()) + ->ToJSON(JSON); + nsresult tmp = aStream->WriteStringZ(JSON.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + +#ifdef DEBUG + // XXX Worrisome if we're caching things without system principal. + if (!mNodeInfoManager->DocumentPrincipal()->IsSystemPrincipal()) { + NS_WARNING("Serializing document without system principal"); + } +#endif + + tmp = aStream->WriteBoolean(mWasL10nCached); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + // mozilla::dom::NodeInfo table + nsTArray<RefPtr<mozilla::dom::NodeInfo>> nodeInfos; + if (mRoot) { + tmp = GetNodeInfos(mRoot, nodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + uint32_t nodeInfoCount = nodeInfos.Length(); + tmp = aStream->Write32(nodeInfoCount); + if (NS_FAILED(tmp)) { + rv = tmp; + } + uint32_t i; + for (i = 0; i < nodeInfoCount; ++i) { + mozilla::dom::NodeInfo* nodeInfo = nodeInfos[i]; + NS_ENSURE_TRUE(nodeInfo, NS_ERROR_FAILURE); + + nsAutoString namespaceURI; + nodeInfo->GetNamespaceURI(namespaceURI); + tmp = aStream->WriteWStringZ(namespaceURI.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + + nsAutoString prefix; + nodeInfo->GetPrefix(prefix); + bool nullPrefix = DOMStringIsNull(prefix); + tmp = aStream->WriteBoolean(nullPrefix); + if (NS_FAILED(tmp)) { + rv = tmp; + } + if (!nullPrefix) { + tmp = aStream->WriteWStringZ(prefix.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + nsAutoString localName; + nodeInfo->GetName(localName); + tmp = aStream->WriteWStringZ(localName.get()); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + // Now serialize the document contents + uint32_t count = mProcessingInstructions.Length(); + for (i = 0; i < count; ++i) { + nsXULPrototypePI* pi = mProcessingInstructions[i]; + tmp = pi->Serialize(aStream, this, &nodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + if (mRoot) { + tmp = mRoot->Serialize(aStream, this, &nodeInfos); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + + return rv; +} + +//---------------------------------------------------------------------- +// + +nsresult nsXULPrototypeDocument::InitPrincipal(nsIURI* aURI, + nsIPrincipal* aPrincipal) { + NS_ENSURE_ARG_POINTER(aURI); + + mURI = aURI; + mNodeInfoManager->SetDocumentPrincipal(aPrincipal); + return NS_OK; +} + +nsIURI* nsXULPrototypeDocument::GetURI() { + NS_ASSERTION(mURI, "null URI"); + return mURI; +} + +nsXULPrototypeElement* nsXULPrototypeDocument::GetRootElement() { + return mRoot; +} + +void nsXULPrototypeDocument::SetRootElement(nsXULPrototypeElement* aElement) { + mRoot = aElement; +} + +nsresult nsXULPrototypeDocument::AddProcessingInstruction( + nsXULPrototypePI* aPI) { + MOZ_ASSERT(aPI, "null ptr"); + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mProcessingInstructions.AppendElement(aPI); + return NS_OK; +} + +const nsTArray<RefPtr<nsXULPrototypePI>>& +nsXULPrototypeDocument::GetProcessingInstructions() const { + return mProcessingInstructions; +} + +nsIPrincipal* nsXULPrototypeDocument::DocumentPrincipal() { + MOZ_ASSERT(mNodeInfoManager, "missing nodeInfoManager"); + return mNodeInfoManager->DocumentPrincipal(); +} + +void nsXULPrototypeDocument::SetDocumentPrincipal(nsIPrincipal* aPrincipal) { + mNodeInfoManager->SetDocumentPrincipal(aPrincipal); +} + +void nsXULPrototypeDocument::MarkInCCGeneration(uint32_t aCCGeneration) { + mCCGeneration = aCCGeneration; +} + +nsNodeInfoManager* nsXULPrototypeDocument::GetNodeInfoManager() { + return mNodeInfoManager; +} + +nsresult nsXULPrototypeDocument::AwaitLoadDone(Callback&& aCallback, + bool* aResult) { + nsresult rv = NS_OK; + + *aResult = mLoaded; + + if (!mLoaded) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mPrototypeWaiters.AppendElement(std::move(aCallback)); + } + + return rv; +} + +nsresult nsXULPrototypeDocument::NotifyLoadDone() { + // Call back to each XUL document that raced to start the same + // prototype document load, lost the race, but hit the XUL + // prototype cache because the winner filled the cache with + // the not-yet-loaded prototype object. + + mLoaded = true; + + for (uint32_t i = mPrototypeWaiters.Length(); i > 0;) { + --i; + mPrototypeWaiters[i](); + } + mPrototypeWaiters.Clear(); + + return NS_OK; +} + +void nsXULPrototypeDocument::SetIsL10nCached(bool aIsCached) { + mWasL10nCached = aIsCached; +} + +void nsXULPrototypeDocument::RebuildPrototypeFromElement( + nsXULPrototypeElement* aPrototype, Element* aElement, bool aDeep) { + aPrototype->mHasIdAttribute = aElement->HasID(); + aPrototype->mHasClassAttribute = aElement->MayHaveClass(); + aPrototype->mHasStyleAttribute = aElement->MayHaveStyle(); + NodeInfo* oldNodeInfo = aElement->NodeInfo(); + RefPtr<NodeInfo> newNodeInfo = mNodeInfoManager->GetNodeInfo( + oldNodeInfo->NameAtom(), oldNodeInfo->GetPrefixAtom(), + oldNodeInfo->NamespaceID(), nsINode::ELEMENT_NODE); + aPrototype->mNodeInfo = newNodeInfo; + + // First replace the prototype attributes with the new ones from this element. + aPrototype->mAttributes.Clear(); + + uint32_t count = aElement->GetAttrCount(); + nsXULPrototypeAttribute* protoAttr = + aPrototype->mAttributes.AppendElements(count); + for (uint32_t index = 0; index < count; index++) { + BorrowedAttrInfo attr = aElement->GetAttrInfoAt(index); + + if (attr.mName->IsAtom()) { + protoAttr->mName.SetTo(attr.mName->Atom()); + } else { + NodeInfo* oldNodeInfo = attr.mName->NodeInfo(); + RefPtr<NodeInfo> newNodeInfo = mNodeInfoManager->GetNodeInfo( + oldNodeInfo->NameAtom(), oldNodeInfo->GetPrefixAtom(), + oldNodeInfo->NamespaceID(), nsINode::ATTRIBUTE_NODE); + protoAttr->mName.SetTo(newNodeInfo); + } + protoAttr->mValue.SetTo(*attr.mValue); + + protoAttr++; + } + + // Make sure the mIsAtom is correct in case this prototype element has been + // completely rebuilt. + CustomElementData* ceData = aElement->GetCustomElementData(); + nsAtom* isAtom = ceData ? ceData->GetIs(aElement) : nullptr; + aPrototype->mIsAtom = isAtom; + + if (aDeep) { + // We have to rebuild the prototype children from this element. + // First release the tree under this element. + aPrototype->ReleaseSubtree(); + + RefPtr<nsXULPrototypeNode>* children = + aPrototype->mChildren.AppendElements(aElement->GetChildCount()); + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsElement()) { + Element* element = child->AsElement(); + RefPtr<nsXULPrototypeElement> elemProto = new nsXULPrototypeElement; + RebuildPrototypeFromElement(elemProto, element, true); + *children = elemProto; + } else if (child->IsText()) { + Text* text = child->AsText(); + RefPtr<nsXULPrototypeText> textProto = new nsXULPrototypeText(); + text->AppendTextTo(textProto->mValue); + *children = textProto; + } else { + MOZ_ASSERT(false, "We handle only elements and text nodes here."); + } + + children++; + } + } +} + +void nsXULPrototypeDocument::RebuildL10nPrototype(Element* aElement, + bool aDeep) { + if (mWasL10nCached) { + return; + } + + Document* doc = aElement->OwnerDoc(); + + nsAutoString id; + MOZ_RELEASE_ASSERT(aElement->GetAttr(nsGkAtoms::datal10nid, id)); + + if (!doc) { + return; + } + + RefPtr<nsXULPrototypeElement> proto = doc->mL10nProtoElements.Get(aElement); + MOZ_RELEASE_ASSERT(proto); + RebuildPrototypeFromElement(proto, aElement, aDeep); +} diff --git a/dom/xul/nsXULPrototypeDocument.h b/dom/xul/nsXULPrototypeDocument.h new file mode 100644 index 0000000000..511f4b3e2b --- /dev/null +++ b/dom/xul/nsXULPrototypeDocument.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsXULPrototypeDocument_h__ +#define nsXULPrototypeDocument_h__ + +#include "js/TracingAPI.h" +#include "mozilla/Attributes.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsISerializable.h" +#include "nsCycleCollectionParticipant.h" +#include <functional> + +class nsAtom; +class nsIPrincipal; +class nsIURI; +class nsNodeInfoManager; +class nsXULPrototypeElement; +class nsXULPrototypePI; + +namespace mozilla::dom { +class Element; +} + +/** + * A "prototype" document that stores shared document information + * for the XUL cache. + * Among other things, stores the tree of nsXULPrototype* + * objects, from which the real DOM tree is built later in + * PrototypeDocumentContentSink::ResumeWalk. + */ +class nsXULPrototypeDocument final : public nsISerializable { + public: + using Callback = std::function<void()>; + + // nsISupports interface + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + // nsISerializable interface + NS_DECL_NSISERIALIZABLE + + nsresult InitPrincipal(nsIURI* aURI, nsIPrincipal* aPrincipal); + nsIURI* GetURI(); + + /** + * Get/set the root nsXULPrototypeElement of the document. + */ + nsXULPrototypeElement* GetRootElement(); + void SetRootElement(nsXULPrototypeElement* aElement); + + /** + * Add a processing instruction to the prolog. Note that only + * PI nodes are currently stored in a XUL prototype document's + * prolog and that they're handled separately from the rest of + * prototype node tree. + * + * @param aPI an already adrefed PI proto to add. This method takes + * ownership of the passed PI. + */ + nsresult AddProcessingInstruction(nsXULPrototypePI* aPI); + /** + * @note GetProcessingInstructions retains the ownership (the PI + * protos only get deleted when the proto document is deleted) + */ + const nsTArray<RefPtr<nsXULPrototypePI> >& GetProcessingInstructions() const; + + nsIPrincipal* DocumentPrincipal(); + void SetDocumentPrincipal(nsIPrincipal* aPrincipal); + + /** + * If current prototype document has not yet finished loading, + * appends aDocument to the list of documents to notify (via + * PrototypeDocumentContentSink::OnPrototypeLoadDone()) and + * sets aLoaded to false. Otherwise sets aLoaded to true. + */ + nsresult AwaitLoadDone(Callback&& aCallback, bool* aResult); + + /** + * Notifies each document registered via AwaitLoadDone on this + * prototype document that the prototype has finished loading. + * The notification is performed by calling + * PrototypeDocumentContentSink::OnPrototypeLoadDone on the + * registered documents. + */ + nsresult NotifyLoadDone(); + + nsNodeInfoManager* GetNodeInfoManager(); + + void MarkInCCGeneration(uint32_t aCCGeneration); + + NS_DECL_CYCLE_COLLECTION_CLASS(nsXULPrototypeDocument) + + bool WasL10nCached() { return mWasL10nCached; }; + + void SetIsL10nCached(bool aIsCached); + void RebuildPrototypeFromElement(nsXULPrototypeElement* aPrototype, + mozilla::dom::Element* aElement, bool aDeep); + void RebuildL10nPrototype(mozilla::dom::Element* aElement, bool aDeep); + + protected: + nsCOMPtr<nsIURI> mURI; + RefPtr<nsXULPrototypeElement> mRoot; + nsTArray<RefPtr<nsXULPrototypePI> > mProcessingInstructions; + + bool mLoaded; + nsTArray<Callback> mPrototypeWaiters; + + RefPtr<nsNodeInfoManager> mNodeInfoManager; + + uint32_t mCCGeneration; + + nsXULPrototypeDocument(); + virtual ~nsXULPrototypeDocument(); + nsresult Init(); + + friend NS_IMETHODIMP NS_NewXULPrototypeDocument( + nsXULPrototypeDocument** aResult); + + static uint32_t gRefCnt; + bool mWasL10nCached; +}; + +#endif // nsXULPrototypeDocument_h__ diff --git a/dom/xul/nsXULSortService.cpp b/dom/xul/nsXULSortService.cpp new file mode 100644 index 0000000000..e73a6c2a1c --- /dev/null +++ b/dom/xul/nsXULSortService.cpp @@ -0,0 +1,395 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + This file provides the implementation for the sort service manager. + */ + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsXULContentUtils.h" +#include "nsString.h" +#include "nsQuickSort.h" +#include "nsWhitespaceTokenizer.h" +#include "nsXULSortService.h" +#include "nsXULElement.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" + +#include "mozilla/dom/Element.h" +#include "mozilla/intl/Collator.h" + +using mozilla::dom::Element; +const unsigned long SORT_COMPARECASE = 0x0001; +const unsigned long SORT_INTEGER = 0x0100; + +enum nsSortState_direction { + nsSortState_descending, + nsSortState_ascending, + nsSortState_natural +}; + +// the sort state holds info about the current sort +struct MOZ_STACK_CLASS nsSortState final { + bool initialized; + MOZ_INIT_OUTSIDE_CTOR bool invertSort; + + uint32_t sortHints; + + MOZ_INIT_OUTSIDE_CTOR nsSortState_direction direction; + nsAutoString sort; + nsTArray<RefPtr<nsAtom>> sortKeys; + + nsCOMPtr<nsIContent> lastContainer; + MOZ_INIT_OUTSIDE_CTOR bool lastWasFirst, lastWasLast; + + nsSortState() : initialized(false), sortHints(0) {} +}; + +// information about a particular item to be sorted +struct contentSortInfo { + nsCOMPtr<nsIContent> content; + nsCOMPtr<nsIContent> parent; + void swap(contentSortInfo& other) { + content.swap(other.content); + parent.swap(other.parent); + } +}; + +/** + * Set sortActive and sortDirection attributes on a tree column when a sort + * is done. The column to change is the one with a sort attribute that + * matches the sort key. The sort attributes are removed from the other + * columns. + */ +static void SetSortColumnHints(nsIContent* content, + const nsAString& sortResource, + const nsAString& sortDirection) { + // set sort info on current column. This ensures that the + // column header sort indicator is updated properly. + for (nsIContent* child = content->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(nsGkAtoms::treecols)) { + SetSortColumnHints(child, sortResource, sortDirection); + } else if (child->IsXULElement(nsGkAtoms::treecol)) { + nsAutoString value; + child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, value); + if (value == sortResource) { + child->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, + u"true"_ns, true); + + child->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, + sortDirection, true); + // Note: don't break out of loop; want to set/unset + // attribs on ALL sort columns + } else if (!value.IsEmpty()) { + child->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, + true); + child->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::sortDirection, true); + } + } + } +} + +/** + * Set sort and sortDirection attributes when a sort is done. + */ +static void SetSortHints(Element* aElement, nsSortState* aSortState) { + // set sort and sortDirection attributes when is sort is done + aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::sort, aSortState->sort, true); + + nsAutoString direction; + if (aSortState->direction == nsSortState_descending) + direction.AssignLiteral("descending"); + else if (aSortState->direction == nsSortState_ascending) + direction.AssignLiteral("ascending"); + aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, direction, + true); + + // for trees, also set the sort info on the currently sorted column + if (aElement->IsXULElement(nsGkAtoms::tree)) { + if (aSortState->sortKeys.Length() >= 1) { + nsAutoString sortkey; + aSortState->sortKeys[0]->ToString(sortkey); + SetSortColumnHints(aElement, sortkey, direction); + } + } +} + +/** + * Determine the list of items which need to be sorted. This is determined + * in the following way: + * - for elements that have a content builder, get its list of generated + * results + * - otherwise, for trees, get the child treeitems + * - otherwise, get the direct children + */ +static nsresult GetItemsToSort(nsIContent* aContainer, nsSortState* aSortState, + nsTArray<contentSortInfo>& aSortItems) { + // Get the children. For trees, get the treechildren element and + // use that as the parent + RefPtr<Element> treechildren; + if (aContainer->IsXULElement(nsGkAtoms::tree)) { + nsXULContentUtils::FindChildByTag(aContainer, kNameSpaceID_XUL, + nsGkAtoms::treechildren, + getter_AddRefs(treechildren)); + if (!treechildren) return NS_OK; + + aContainer = treechildren; + } + + for (nsIContent* child = aContainer->GetFirstChild(); child; + child = child->GetNextSibling()) { + contentSortInfo* cinfo = aSortItems.AppendElement(); + if (!cinfo) return NS_ERROR_OUT_OF_MEMORY; + + cinfo->content = child; + } + + return NS_OK; +} + +/** + * Compares aLeft and aRight and returns < 0, 0, or > 0. The sort + * hints are checked for case matching and integer sorting. + */ +static int32_t CompareValues(const nsAString& aLeft, const nsAString& aRight, + uint32_t aSortHints) { + if (aSortHints & SORT_INTEGER) { + nsresult err; + int32_t leftint = PromiseFlatString(aLeft).ToInteger(&err); + if (NS_SUCCEEDED(err)) { + int32_t rightint = PromiseFlatString(aRight).ToInteger(&err); + if (NS_SUCCEEDED(err)) { + return leftint - rightint; + } + } + // if they aren't integers, just fall through and compare strings + } + + if (aSortHints & SORT_COMPARECASE) { + return ::Compare(aLeft, aRight); + } + + using mozilla::intl::Collator; + const Collator* collator = nsXULContentUtils::GetCollator(); + if (collator) { + return collator->CompareStrings(aLeft, aRight); + } + + return ::Compare(aLeft, aRight, nsCaseInsensitiveStringComparator); +} + +static int testSortCallback(const void* data1, const void* data2, + void* privateData) { + /// Note: testSortCallback is a small C callback stub for NS_QuickSort + contentSortInfo* left = (contentSortInfo*)data1; + contentSortInfo* right = (contentSortInfo*)data2; + nsSortState* sortState = (nsSortState*)privateData; + + int32_t sortOrder = 0; + + int32_t length = sortState->sortKeys.Length(); + for (int32_t t = 0; t < length; t++) { + // compare attributes. Ignore namespaces for now. + nsAutoString leftstr, rightstr; + if (left->content->IsElement()) { + left->content->AsElement()->GetAttr(kNameSpaceID_None, + sortState->sortKeys[t], leftstr); + } + if (right->content->IsElement()) { + right->content->AsElement()->GetAttr(kNameSpaceID_None, + sortState->sortKeys[t], rightstr); + } + + sortOrder = CompareValues(leftstr, rightstr, sortState->sortHints); + } + + if (sortState->direction == nsSortState_descending) sortOrder = -sortOrder; + + return sortOrder; +} + +/** + * Given a list of sortable items, reverse the list. This is done + * when simply changing the sort direction for the same key. + */ +static nsresult InvertSortInfo(nsTArray<contentSortInfo>& aData, int32_t aStart, + int32_t aNumItems) { + if (aNumItems > 1) { + // reverse the items in the array starting from aStart + int32_t upPoint = (aNumItems + 1) / 2 + aStart; + int32_t downPoint = (aNumItems - 2) / 2 + aStart; + int32_t half = aNumItems / 2; + while (half-- > 0) { + aData[downPoint--].swap(aData[upPoint++]); + } + } + return NS_OK; +} + +/** + * Sort a container using the supplied sort state details. + */ +static nsresult SortContainer(nsIContent* aContainer, nsSortState* aSortState) { + nsTArray<contentSortInfo> items; + nsresult rv = GetItemsToSort(aContainer, aSortState, items); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t numResults = items.Length(); + if (!numResults) return NS_OK; + + uint32_t i; + + // if the items are just being inverted, that is, just switching between + // ascending and descending, just reverse the list. + if (aSortState->invertSort) + InvertSortInfo(items, 0, numResults); + else + NS_QuickSort((void*)items.Elements(), numResults, sizeof(contentSortInfo), + testSortCallback, (void*)aSortState); + + // first remove the items from the old positions + for (i = 0; i < numResults; i++) { + nsIContent* child = items[i].content; + nsIContent* parent = child->GetParent(); + + if (parent) { + // remember the parent so that it can be reinserted back + // into the same parent. This is necessary as multiple rules + // may generate results which get placed in different locations. + items[i].parent = parent; + parent->RemoveChildNode(child, true); + } + } + + // now add the items back in sorted order + for (i = 0; i < numResults; i++) { + nsIContent* child = items[i].content; + nsIContent* parent = items[i].parent; + if (parent) { + parent->AppendChildTo(child, true, mozilla::IgnoreErrors()); + + // if it's a container in a tree or menu, find its children, + // and sort those also + if (!child->IsElement() || !child->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters)) + continue; + + for (nsIContent* grandchild = child->GetFirstChild(); grandchild; + grandchild = grandchild->GetNextSibling()) { + mozilla::dom::NodeInfo* ni = grandchild->NodeInfo(); + nsAtom* localName = ni->NameAtom(); + if (ni->NamespaceID() == kNameSpaceID_XUL && + (localName == nsGkAtoms::treechildren || + localName == nsGkAtoms::menupopup)) { + SortContainer(grandchild, aSortState); + } + } + } + } + + return NS_OK; +} + +/** + * Initialize sort information from attributes specified on the container, + * the sort key and sort direction. + * + * @param aRootElement the element that contains sort attributes + * @param aContainer the container to sort, usually equal to aRootElement + * @param aSortKey space separated list of sort keys + * @param aSortDirection direction to sort in + * @param aSortState structure filled in with sort data + */ +static nsresult InitializeSortState(Element* aRootElement, Element* aContainer, + const nsAString& aSortKey, + const nsAString& aSortHints, + nsSortState* aSortState) { + // used as an optimization for the content builder + if (aContainer != aSortState->lastContainer.get()) { + aSortState->lastContainer = aContainer; + aSortState->lastWasFirst = false; + aSortState->lastWasLast = false; + } + + // The sort attribute is of the form: sort="key1 key2 ..." + nsAutoString sort(aSortKey); + aSortState->sortKeys.Clear(); + nsWhitespaceTokenizer tokenizer(sort); + while (tokenizer.hasMoreTokens()) { + RefPtr<nsAtom> keyatom = NS_Atomize(tokenizer.nextToken()); + NS_ENSURE_TRUE(keyatom, NS_ERROR_OUT_OF_MEMORY); + aSortState->sortKeys.AppendElement(keyatom); + } + + aSortState->sort.Assign(sort); + aSortState->direction = nsSortState_natural; + + bool noNaturalState = false; + nsWhitespaceTokenizer hintsTokenizer(aSortHints); + while (hintsTokenizer.hasMoreTokens()) { + const nsDependentSubstring& token(hintsTokenizer.nextToken()); + if (token.EqualsLiteral("comparecase")) + aSortState->sortHints |= SORT_COMPARECASE; + else if (token.EqualsLiteral("integer")) + aSortState->sortHints |= SORT_INTEGER; + else if (token.EqualsLiteral("descending")) + aSortState->direction = nsSortState_descending; + else if (token.EqualsLiteral("ascending")) + aSortState->direction = nsSortState_ascending; + else if (token.EqualsLiteral("twostate")) + noNaturalState = true; + } + + // if the twostate flag was set, the natural order is skipped and only + // ascending and descending are allowed + if (aSortState->direction == nsSortState_natural && noNaturalState) { + aSortState->direction = nsSortState_ascending; + } + + // set up sort order info + aSortState->invertSort = false; + + nsAutoString existingsort; + aRootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, existingsort); + nsAutoString existingsortDirection; + aRootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, + existingsortDirection); + + // if just switching direction, set the invertSort flag + if (sort.Equals(existingsort)) { + if (aSortState->direction == nsSortState_descending) { + if (existingsortDirection.EqualsLiteral("ascending")) + aSortState->invertSort = true; + } else if (aSortState->direction == nsSortState_ascending && + existingsortDirection.EqualsLiteral("descending")) { + aSortState->invertSort = true; + } + } + + aSortState->initialized = true; + + return NS_OK; +} + +nsresult mozilla::XULWidgetSort(Element* aNode, const nsAString& aSortKey, + const nsAString& aSortHints) { + nsSortState sortState; + nsresult rv = + InitializeSortState(aNode, aNode, aSortKey, aSortHints, &sortState); + NS_ENSURE_SUCCESS(rv, rv); + + // store sort info in attributes on content + SetSortHints(aNode, &sortState); + rv = SortContainer(aNode, &sortState); + + return rv; +} diff --git a/dom/xul/nsXULSortService.h b/dom/xul/nsXULSortService.h new file mode 100644 index 0000000000..c087cb572a --- /dev/null +++ b/dom/xul/nsXULSortService.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + This sort service is used to sort content by attribute. + */ + +#ifndef nsXULSortService_h +#define nsXULSortService_h + +#include "nsAString.h" +#include "nsError.h" + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +/** + * Sort the contents of the widget containing <code>aNode</code> + * using <code>aSortKey</code> as the comparison key, and + * <code>aSortDirection</code> as the direction. + * + * @param aNode A node in the XUL widget whose children are to be sorted. + * @param aSortKey The value to be used as the comparison key. + * @param aSortHints One or more hints as to how to sort: + * + * ascending: to sort the contents in ascending order + * descending: to sort the contents in descending order + * comparecase: perform case sensitive comparisons + * integer: treat values as integers, non-integers are compared as strings + * twostate: don't allow the natural (unordered state) + */ +nsresult XULWidgetSort(dom::Element* aNode, const nsAString& aSortKey, + const nsAString& aSortHints); + +} // namespace mozilla + +#endif // nsXULSortService_h diff --git a/dom/xul/test/398289-resource.xhtml b/dom/xul/test/398289-resource.xhtml new file mode 100644 index 0000000000..6702027ef1 --- /dev/null +++ b/dom/xul/test/398289-resource.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog buttonlabelaccept="OK" + buttonlabelcancel="Cancel"> + <style xmlns="http://www.w3.org/1999/xhtml"> + .tab-middle { outline: none !important } + </style> + + <script> + document.addEventListener("dialogaccept", function() { alert('OK') }); + document.addEventListener("dialogcancel", function() { alert('Cancel') }); + </script> + + <tabbox id="test" flex="1" persist="selectedIndex"> + + <tabs> + <tab label="One"/> + <tab label="Two"/> + </tabs> + + <tabpanels flex="1"> + + <vbox flex="1"> + <description>Text for tab ONE</description> + <description class="text-link" + onclick="window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=398289');">(test case for bug 398289)</description> + <tree> + <treecols> + <treecol label="Header" flex="1"/> + </treecols> + </tree> + </vbox> + + <vbox flex="1"> + <description>Text for tab TWO</description> + <description>(When the document is reloaded, this content gets replaced by the one from the first tab.)</description> + </vbox> + + </tabpanels> + + </tabbox> + + <box height="1000"/> <!-- Push dialog buttons out of sight so that the animated default button isn't part of the snapshot --> + +</dialog> +</window> diff --git a/dom/xul/test/chrome.ini b/dom/xul/test/chrome.ini new file mode 100644 index 0000000000..a1313d269e --- /dev/null +++ b/dom/xul/test/chrome.ini @@ -0,0 +1,26 @@ +[DEFAULT] +support-files = + 398289-resource.xhtml + window_bug583948.xhtml + window_bug757137.xhtml + +[test_bug199692.xhtml] +[test_bug311681.xhtml] +[test_bug391002.xhtml] +[test_bug398289.html] +[test_bug403868.xhtml] +[test_bug418216.xhtml] +[test_bug445177.xhtml] +[test_bug468176.xhtml] +[test_bug583948.xhtml] +[test_bug757137.xhtml] +[test_bug775972.xhtml] +[test_bug1070049_throw_from_script.xhtml] +[test_html_template.xhtml] +[test_import_xul_to_content.xhtml] +[test_bug1290965.xhtml] +[test_bug749367.xhtml] +[test_xul_tabindex_focus.xhtml] +[test_bug1686822.xhtml] + support-files = window_bug1686822.xhtml +[test_accesskey.xhtml] diff --git a/dom/xul/test/file_bug236853.rdf b/dom/xul/test/file_bug236853.rdf new file mode 100644 index 0000000000..8d17bf6912 --- /dev/null +++ b/dom/xul/test/file_bug236853.rdf @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:ex="http://www.ex.org/ex-rdf#"> + + <rdf:Description about="urn:root"> + <ex:nodes> + <rdf:Bag about="http://www.ex.org/nodes"> + <rdf:li> + <rdf:Description about="http://www.ex.org/nodes/A"/> + </rdf:li> + </rdf:Bag> + </ex:nodes> + </rdf:Description> +</rdf:RDF> diff --git a/dom/xul/test/mochitest.ini b/dom/xul/test/mochitest.ini new file mode 100644 index 0000000000..9ec3128ab6 --- /dev/null +++ b/dom/xul/test/mochitest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_disable_scroll_frame_plain.html] diff --git a/dom/xul/test/test_accesskey.xhtml b/dom/xul/test/test_accesskey.xhtml new file mode 100644 index 0000000000..c71fcf78a0 --- /dev/null +++ b/dom/xul/test/test_accesskey.xhtml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1699284 +--> +<window title="Mozilla Bug 1699284" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<body> +<label id="label1" accesskey="a">Label 1</label><button id="button1" accesskey="a">Button 1</button> +<label id="label2" accesskey="a" tabindex="0">Label 2</label><button id="button2">Button 2</button> +<label id="label3">Label 3</label><button id="button3" accesskey="a">Button 3</button> +<label id="label4" accesskey="a" control="button4">Label 4</label><button id="button4" disabled="true">Button 4</button> +<label id="label5" accesskey="a" control="button5">Label 5</label><button id="button5">Button 5</button> +<!-- Tests code --> +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 1699284 **/ + +function PerformAccessKey(aKey) { + synthesizeKey(aKey, navigator.platform.includes("Mac") ? { altKey: true, ctrlKey: true } + : { altKey: true, shiftKey: true }); +}; + +add_task(async function test_accesskey() { + let [label1, label2, label3, label4, label5] = document.querySelectorAll("label"); + let [button1, button2, button3, button4, button5] = document.querySelectorAll("button"); + + [ + label1, label2, label3, label4, label5, button1, button2, button3, button4, button5 + ].forEach(function(ele) { + ele.addEventListener("click", function(e) { + ok(false, `${e.target.id} should not receive click event`); + }); + }); + + PerformAccessKey("a"); + is(document.activeElement.id, button1.id, `focus should move to ${button1.id}`); + + PerformAccessKey("a"); + is(document.activeElement.id, button3.id, `focus should move to ${button3.id}`); + + PerformAccessKey("a"); + is(document.activeElement.id, button5.id, `focus should move to ${button5.id}`); + + // Cycle back to first element + PerformAccessKey("a"); + is(document.activeElement.id, button1.id, `focus should move to ${button1.id}`); +}); + +]]> +</script> +</body> +</window> diff --git a/dom/xul/test/test_bug1070049_throw_from_script.xhtml b/dom/xul/test/test_bug1070049_throw_from_script.xhtml new file mode 100644 index 0000000000..b9c635efd6 --- /dev/null +++ b/dom/xul/test/test_bug1070049_throw_from_script.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1070049 +--> +<window title="Mozilla Bug 1070049" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 1070049 **/ + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + // Prevent the test from failing when the exception hits onerror. + SimpleTest.expectUncaughtException(); + + // Tell the test to expect exactly one console error with the given parameters, + // with SimpleTest.finish as a continuation function. + SimpleTest.monitorConsole(SimpleTest.finish, [{errorMessage: new RegExp('flimfniffle')}]); + + // Schedule the console accounting (and continuation) to run next, right + // after we throw (below). + SimpleTest.executeSoon(SimpleTest.endMonitorConsole); + + // Throw. + throw "flimfniffle"; + }); + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1070049" + target="_blank">Mozilla Bug 1070049</a> + </body> +</window> diff --git a/dom/xul/test/test_bug1290965.xhtml b/dom/xul/test/test_bug1290965.xhtml new file mode 100644 index 0000000000..ce1f7a8522 --- /dev/null +++ b/dom/xul/test/test_bug1290965.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:h="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <toolbarbutton oncommand="++countera;" id="a">A</toolbarbutton> + <toolbarbutton oncommand="++counterb;" id="b">B</toolbarbutton> + <script type="text/javascript"> + <![CDATA[ + let aEl = document.getElementById('a'); + let bEl = document.getElementById('b'); + let countera = 0; + let counterb = 0; + + aEl.addEventListener('click', function (aEvent) { + aEvent.preventDefault(); + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey, + aEvent.metaKey, 0, null, aEvent.mozInputSource); + aEvent.currentTarget.dispatchEvent(cmdEvent); + }); + + bEl.addEventListener('click', function (aEvent) { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey, + aEvent.metaKey, 0, null, aEvent.mozInputSource); + aEvent.currentTarget.dispatchEvent(cmdEvent); + }); + + bEl.click(); + aEl.click(); + + is(countera, 1, "Counter should be one as event fires once"); + is(counterb, 2, "Counter should be two as event fires twice"); + ]]> + </script> +</window> diff --git a/dom/xul/test/test_bug1686822.xhtml b/dom/xul/test/test_bug1686822.xhtml new file mode 100644 index 0000000000..cdad585d00 --- /dev/null +++ b/dom/xul/test/test_bug1686822.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <html:script><![CDATA[ + SimpleTest.waitForExplicitFinish(); + let chromeWindow = browsingContext.topChromeWindow; + + let resolve; + let i = 0; + chromeWindow.moduleScriptRan = function() { + ok(true, "Module script executed: " + ++i) + resolve(); + resolve = null; + } + + function testOnce() { + let currentWin; + return new Promise(r => { + currentWin = chromeWindow.open("window_bug1686822.xhtml", "", "chrome"); + resolve = r; + }).then(function() { + currentWin.close(); + }); + } + + (async function() { + // The first and second loads are different so make sure we test both code paths. + await testOnce(); + await testOnce(); + delete chromeWindow.moduleScriptRan; + SimpleTest.finish(); + }()); + ]]></html:script> +</window> diff --git a/dom/xul/test/test_bug199692.xhtml b/dom/xul/test/test_bug199692.xhtml new file mode 100644 index 0000000000..6d8b9efe4e --- /dev/null +++ b/dom/xul/test/test_bug199692.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<window title="Test for Bug 199692" + id="test_bug199692.xhtml" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript"> + <![CDATA[ + customElements.define("test-xul-element", class CustomElement extends XULElement { + constructor() { + super(); + const template = document.getElementById("template"); + this.attachShadow({mode: "open"}) + .appendChild(template.content.cloneNode(true)); + } + }); + ]]> + </script> +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> +<template id="template"> + <xul:label id="anon-label" value="ANON"/> +</template> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=199692">Mozilla Bug 199692</a> + +<vbox id="content" style="position: relative;" +xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <xul:label id="non-anon-label" value="a textbox!:" control="textbox"/> + <html:textarea id="textbox" rows="4"/> + <xul:radiogroup style="outline: 2px solid orange;"> + <xul:radio id="unselected-radio" label="Orange" style="outline: 2px solid red;"/> + <xul:radio id="selected-radio" label="Violet" selected="true"/> + <xul:radio id="disabled-radio" label="Yellow" disabled="true"/> + </xul:radiogroup> + <test-xul-element id="bound" style="border: 2px solid green;"></test-xul-element> +</vbox> +<pre id="test"> + <script class="testbody" type="text/javascript"> +<![CDATA[ + SimpleTest.waitForExplicitFinish(); + + // Before onload, XUL docs have no root frame. + is(document.elementFromPoint(10,10), null, + "Calls to elementFromPoint before onload should return null"); + + var d = 10; + function middle(e) { + return { "x": e.getBoundingClientRect().x + e.getBoundingClientRect().width/2, + "y": e.getBoundingClientRect().y + e.getBoundingClientRect().height/2 }; + } + function lower_right(e) { + return { "x": e.getBoundingClientRect().x + e.getBoundingClientRect().width - d, + "y": e.getBoundingClientRect().y + e.getBoundingClientRect().height - d }; + } + function upper_left(e) { + return { "x": e.getBoundingClientRect().x + d, + "y": e.getBoundingClientRect().y + d }; + } + function scrollbar_button(e) { // a bit down from upper right + return { "x": e.getBoundingClientRect().x + e.getBoundingClientRect().width - d, + "y": e.getBoundingClientRect().y + d + 15 }; + } + + function test(ptFunc, id, message) { + var pt = ptFunc($(id)); + var e = document.elementFromPoint(pt.x, pt.y); + ok(e != null, message + " (returned null)"); + is(e.id, id, message); + } + + function do_test() { + // Avoid hardcoding x,y pixel values, to better deal with differing default + // font sizes or other layout defaults. + + test(middle, 'textbox', "Point within textbox should return textbox element"); + test(lower_right, 'textbox', "Point on textbox's scrollbar should return textbox element"); + test(scrollbar_button, 'textbox', "Point on textbox's scrollbar button should return textbox element"); + test(middle, 'non-anon-label', "Point on label should return label"); + test(upper_left, 'bound', "Point on custom element content should return custom element"); + + SimpleTest.finish(); + } + $("textbox").setAttribute("value", + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet " + + "lorem ipsum dolor sit amet "); // force scrollbars to appear + addLoadEvent(do_test); +]]> + </script> +</pre> +</body> +</window> diff --git a/dom/xul/test/test_bug311681.xhtml b/dom/xul/test/test_bug311681.xhtml new file mode 100644 index 0000000000..8caef2ca5b --- /dev/null +++ b/dom/xul/test/test_bug311681.xhtml @@ -0,0 +1,107 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=311681 +--> +<window title="Mozilla Bug 311681" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=311681">Mozilla Bug 311681</a> +<script class="testbody" type="text/javascript"> +<![CDATA[ + // Setup script + SimpleTest.waitForExplicitFinish(); + + // Make sure to trigger the hashtable case by asking for enough elements + // by ID. + for (var i = 0; i < 256; ++i) { + var x = document.getElementById(i); + } + + // save off the document.getElementById function, since getting it as a + // property off the document it causes a content flush. + var fun = document.getElementById; + + // Slot for our initial element with id "content" + var testNode; + + function getCont() { + return fun.call(document, "content"); + } + + function testClone() { + // Test to make sure that if we have multiple nodes with the same ID in + // a document we don't forget about one of them when the other is + // removed. + var newParent = $("display"); + var node = testNode.cloneNode(true); + isnot(node, testNode, "Clone should be a different node"); + + newParent.appendChild(node); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting new node pre-flush 1") + + // Trigger a layout flush, just in case. + let itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting new node post-flush 1") + + clear(newParent); + + // Check what getElementById returns, no flushing + is(getCont(), testNode, "Should be getting orig node pre-flush 2"); + + // Trigger a layout flush, just in case. + itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), testNode, "Should be getting orig node post-flush 2"); + + node = testNode.cloneNode(true); + newParent.appendChild(node); + testNode.remove(); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting clone pre-flush"); + + // Trigger a layout flush, just in case. + // eslint-disable-next-line no-unused-vars + itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting clone post-flush"); + + } + + function clear(node) { + while (node.hasChildNodes()) { + node.firstChild.remove(); + } + } + + addLoadEvent(testClone); + addLoadEvent(SimpleTest.finish); +]]> +</script> +<p id="display"></p> +<div id="content" style="display: none"> + <script class="testbody" type="text/javascript"> + <![CDATA[ + testNode = fun.call(document, "content"); + // Needs incremental XML parser + isnot(testNode, null, "Should have node here"); + ]]> + </script> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/dom/xul/test/test_bug391002.xhtml b/dom/xul/test/test_bug391002.xhtml new file mode 100644 index 0000000000..da8c8b4d35 --- /dev/null +++ b/dom/xul/test/test_bug391002.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=391002 +--> +<window title="Mozilla Bug 391002" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=391002" + target="_blank">Mozilla Bug 391002</a> + </body> + + <button id="btn1" command="cmd1"/> + + <button id="btn2"> + <observes id="observes" element="cmd1" attribute="label"/> + </button> + + <commandset> + <command id="cmd1" label="cmd1"/> + <command id="cmd2" label="cmd2"/> + </commandset> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + /** Test for Bug 391002 **/ + + $("btn1").setAttribute("command", "cmd2"); + is($("btn1").getAttribute("label"), $("cmd2").getAttribute("label")) + + $("observes").setAttribute("element", "cmd2"); + is($("btn2").getAttribute("label"), $("cmd2").getAttribute("label")) + + ]]></script> +</window> + diff --git a/dom/xul/test/test_bug398289.html b/dom/xul/test/test_bug398289.html new file mode 100644 index 0000000000..c4873fe2b2 --- /dev/null +++ b/dom/xul/test/test_bug398289.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html style="height: 100%"> +<head> + <title>Test for bug 398289</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body style="height: 100%; margin: 0; overflow: hidden;" onload="setTimeout(onBodyLoad, 0);"> + <iframe id="test" style="border: 0; width: 100%; height: 100%" scrolling="no" src="398289-resource.xhtml"></iframe> + + <script class="testbody"> + var snap1, snap2; + SimpleTest.waitForExplicitFinish(); + + async function onBodyLoad() { + window.frames[0].document.getElementById("test").selectedIndex = 0; + window.frames[0].document.getElementById("test").selectedIndex = 1; + + frames[0].scrollTo(0, 0); + scrollTo(0, 0); + snap1 = await snapshotWindow(window); + + document.getElementById("test").onload = onFrameLoad; + window.frames[0].location.reload(); + } + + async function onFrameLoad() { + frames[0].scrollTo(0, 0); + scrollTo(0, 0); + snap2 = await snapshotWindow(window); + + var equal, str1, str2; + [equal, str1, str2] = compareSnapshots(snap1, snap2, true); + + ok(equal, "persistent attribute in tab box broken, expected: "+str1+" got: "+str2); + SimpleTest.finish(); + } + </script> +</body> +</html> diff --git a/dom/xul/test/test_bug403868.xhtml b/dom/xul/test/test_bug403868.xhtml new file mode 100644 index 0000000000..c2427bb644 --- /dev/null +++ b/dom/xul/test/test_bug403868.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=403868 +--> +<window title="Mozilla Bug 403868" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=403868" + target="_blank">Mozilla Bug 403868</a> + <div id="content" style="display: none"/> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + /** Test for Bug 403868 **/ +function createSpan(id, insertionPoint) { + var s = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + s.id = id; + $("content").insertBefore(s, insertionPoint); + return s; +} + +var s1a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Only one span with id=test1 in the tree; should work!"); + +var s2a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Appending span with id=test1 doesn't change which one comes first"); + +var s3a = createSpan("test1", s2a); +is(document.getElementById("test1"), s1a, + "Inserting span with id=test1 not at the beginning; doesn't matter"); + +var s4a = createSpan("test1", s1a); +is(document.getElementById("test1"), s4a, + "Inserting span with id=test1 at the beginning changes which one is first"); + +s4a.remove(); +is(document.getElementById("test1"), s1a, + "First-created span with id=test1 is first again"); + +s1a.remove(); +is(document.getElementById("test1"), s3a, + "Third-created span with id=test1 is first now"); + +// Start the id hashtable +for (var i = 0; i < 256; ++i) { + document.getElementById("no-such-id-in-the-document" + i); +} + +var s1b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Only one span with id=test2 in the tree; should work!"); + +var s2b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Appending span with id=test2 doesn't change which one comes first"); + +var s3b = createSpan("test2", s2b); +is(document.getElementById("test2"), s1b, + "Inserting span with id=test2 not at the beginning; doesn't matter"); + +var s4b = createSpan("test2", s1b); +is(document.getElementById("test2"), s4b, + "Inserting span with id=test2 at the beginning changes which one is first"); + +s4b.remove(); +is(document.getElementById("test2"), s1b, + "First-created span with id=test2 is first again"); + +s1b.remove(); +is(document.getElementById("test2"), s3b, + "Third-created span with id=test2 is first now"); + + ]]></script> +</window> diff --git a/dom/xul/test/test_bug418216.xhtml b/dom/xul/test/test_bug418216.xhtml new file mode 100644 index 0000000000..56e487faf2 --- /dev/null +++ b/dom/xul/test/test_bug418216.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=418216 +--> +<window title="Mozilla Bug 418216" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=418216" + target="_blank">Mozilla Bug 418216</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + /** Test for Bug 418216 **/ + SimpleTest.waitForExplicitFinish(); + window.onload = function onload() { + var element = $("item"); + $("container").removeChild($("item")); + $("broadcaster").removeAttribute("disabled"); + $("container").appendChild(element); + is($("item").hasAttribute("disabled"), $("broadcaster").hasAttribute("disabled")); + SimpleTest.finish(); + } + + + + + ]]></script> + +<box id="container"> +<box id="item"> + <observes element="broadcaster" attribute="disabled"/> +</box> +</box> + +<broadcasterset> + <broadcaster id="broadcaster" disabled="true"/> +</broadcasterset> + +</window> diff --git a/dom/xul/test/test_bug445177.xhtml b/dom/xul/test/test_bug445177.xhtml new file mode 100644 index 0000000000..d188f19241 --- /dev/null +++ b/dom/xul/test/test_bug445177.xhtml @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=445177 +--> +<window title="Test for Bug 445177" + id="test_bug445177.xhtml" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=445177">Mozilla Bug 445177</a> + + +<xul:hbox id="b1" value="foo"/> +<xul:hbox id="o1" observes="b1"/> + +<pre id="test"> + <script class="testbody" type="text/javascript"> +<![CDATA[ + SimpleTest.waitForExplicitFinish(); + function do_test() { + var b1 = document.getElementById("b1"); + var o1 = document.getElementById("o1"); + + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (1)"); + + b1.setAttribute("value", "bar"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (2)"); + + b1.removeAttribute("value"); + is(o1.hasAttribute("value"), b1.hasAttribute("value"), "Wrong value (3)"); + + o1.setAttribute("value", "foo"); + isnot(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (4)"); + + b1.setAttribute("value", "foobar"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (5)"); + + //After removing listener, changes to broadcaster shouldn't have any effect. + o1.remove(); + b1.setAttribute("value", "foo"); + isnot(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (6)"); + + b1.parentNode.appendChild(o1); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (7)"); + + o1.remove(); + o1.removeAttribute("observes"); + o1.removeAttribute("value"); + b1.parentNode.appendChild(o1); + isnot(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (8)"); + + SimpleTest.finish(); + } + + addLoadEvent(do_test); +]]> + </script> +</pre> +</body> +</window> diff --git a/dom/xul/test/test_bug468176.xhtml b/dom/xul/test/test_bug468176.xhtml new file mode 100644 index 0000000000..f21c1bcde3 --- /dev/null +++ b/dom/xul/test/test_bug468176.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=468176 +--> +<window title="Test for Bug 468176" + id="test_bug468176.xhtml" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=468176">Mozilla Bug 468176</a> + +<xul:hbox id="b1" value="foo"/> + +<xul:hbox id="o1"> + <xul:observes id="inner" element="b1" attribute="*"/> +</xul:hbox> + +<pre id="test"> + <script class="testbody" type="text/javascript"> +<![CDATA[ + SimpleTest.waitForExplicitFinish(); + + var broadcastCount = 0; + function b_listener(evt) { + ++broadcastCount; + } + + function do_test() { + var b1 = document.getElementById("b1"); + var o1 = document.getElementById("o1"); + var inner = document.getElementById("inner"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (1)"); + + inner.addEventListener("broadcast", b_listener, true); + b1.setAttribute("value", "bar"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (2)"); + is(broadcastCount, 1, "Wrong value (3)"); + + b1.removeAttribute("value"); + is(o1.hasAttribute("value"), b1.hasAttribute("value"), "Wrong value (4)"); + is(broadcastCount, 2, "Wrong value (5)"); + + o1.setAttribute("value", "foo"); + isnot(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (6)"); + is(broadcastCount, 2, "Wrong value (7)"); + + b1.setAttribute("value", "foobar"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (8)"); + is(broadcastCount, 3, "Wrong value (9)"); + + b1.removeAttribute("value"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (10)"); + is(broadcastCount, 4, "Wrong value (11)"); + + b1.removeAttribute("value"); + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (12)"); + is(broadcastCount, 4, "Wrong value (13)"); + + o1.setAttribute("value", "bar"); + b1.setAttribute("value", "bar"); // This should still dispatch 'broadcast' + is(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (14)"); + is(broadcastCount, 5, "Wrong value (15)"); + + //After removing listener, changes to broadcaster shouldn't have any effect. + o1.remove(); + b1.setAttribute("value", "foo"); + isnot(o1.getAttribute("value"), b1.getAttribute("value"), "Wrong value (16)"); + is(broadcastCount, 5, "Wrong value (17)"); + + SimpleTest.finish(); + } + + addLoadEvent(do_test); +]]> + </script> +</pre> +</body> +</window> diff --git a/dom/xul/test/test_bug583948.xhtml b/dom/xul/test/test_bug583948.xhtml new file mode 100644 index 0000000000..2cab13a4ac --- /dev/null +++ b/dom/xul/test/test_bug583948.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <div id="content" style="display: none"/> +</body> + +<script> +SimpleTest.waitForExplicitFinish(); + +let chromeWindow = window.browsingContext.topChromeWindow; + +var attempts = 0; + +chromeWindow.update = function () { + // without the crash fix, this usually crashes after 2 to 4 reloads + if (++attempts == 6) { + ok(true, "didn't crash after 6 attempts"); + otherWindow.close(); + SimpleTest.waitForFocus(function() { + chromeWindow.update = null; + SimpleTest.finish(); + }); + } else { + otherWindow.document.commandDispatcher.updateCommands(''); + setTimeout(function() { + otherWindow.location.reload() + }, 0); + } +} + +var otherWindow = chromeWindow.open("window_bug583948.xhtml", "_blank", "chrome"); +</script> + +</window> diff --git a/dom/xul/test/test_bug749367.xhtml b/dom/xul/test/test_bug749367.xhtml new file mode 100644 index 0000000000..3dcea24f1c --- /dev/null +++ b/dom/xul/test/test_bug749367.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=749367 +--> +<window title="Mozilla Bug 749367" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="setTimeout(runTests, 0);"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=486990" + target="_blank">Mozilla Bug 486990</a> + </body> + + <!-- test code goes here --> + <script type="text/template"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + function runTests() { + ok(false, "Shouldn't execute"); + SimpleTest.finish(); + } + ]]> + </script> + + <script type="text/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + function runTests() { + ok(true, "Should execute"); + SimpleTest.finish(); + } + ]]> + </script> + +</window> diff --git a/dom/xul/test/test_bug757137.xhtml b/dom/xul/test/test_bug757137.xhtml new file mode 100644 index 0000000000..a08309bd49 --- /dev/null +++ b/dom/xul/test/test_bug757137.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <div id="content" style="display: none"/> +</body> + +<script> +SimpleTest.waitForExplicitFinish(); + +// Force off out-of-process mozbrowser because we need to grab its +// |window| synchronously from here. Out-of-process docshell creation +// for mozbrowser haves entirely differently. +SpecialPowers.pushPrefEnv({"set":[["dom.ipc.tabs.disabled", true]]}).then(startTest); + +function startTest() { + var otherWindow = window.browsingContext.topChromeWindow.open("window_bug757137.xhtml", "", "chrome"); + ok(otherWindow.isChromeWindow, 'XUL window should be a ChromeWindow'); + + otherWindow.onload = function () { + var w = otherWindow.document.getElementById('f').contentWindow; + ok(w !== null, 'got the |window| for a mozbrowser iframe'); + ok(!w.isChromeWindow, 'mozbrowser iframe should not be a ChromeWindow'); + + otherWindow.close(); + SimpleTest.waitForFocus(SimpleTest.finish); + }; +} +</script> + +</window> diff --git a/dom/xul/test/test_bug775972.xhtml b/dom/xul/test/test_bug775972.xhtml new file mode 100644 index 0000000000..571bf6a6c3 --- /dev/null +++ b/dom/xul/test/test_bug775972.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=775972 +--> +<window title="Mozilla Bug 775972" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=775972" + target="_blank">Mozilla Bug 775972</a> + </body> + + <hbox id="container"><label value="test" id=""/></hbox> + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 775972 **/ + + function test() { + var c = document.getElementById("container"); + var clone = c.cloneNode(true); + document.documentElement.appendChild(clone); + ok(true, "This shouldn't crash!"); + } + + + ]]> + </script> +</window> diff --git a/dom/xul/test/test_disable_scroll_frame_plain.html b/dom/xul/test/test_disable_scroll_frame_plain.html new file mode 100644 index 0000000000..8104f55721 --- /dev/null +++ b/dom/xul/test/test_disable_scroll_frame_plain.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html scrolling="false"> +<head> + <meta charset="utf-8"> + <title>disable scroll frame exposed</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<div style="height: 300vh"></div> +<script> + // Ensure that disabling the scroll frame isn't exposed to content. + ok(document.scrollingElement.scrollTopMax > 0, "Scrolling should still work."); +</script> +</body> +</html> diff --git a/dom/xul/test/test_html_template.xhtml b/dom/xul/test/test_html_template.xhtml new file mode 100644 index 0000000000..7e6029486e --- /dev/null +++ b/dom/xul/test/test_html_template.xhtml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<window title="HTML template in XUL" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <body xmlns="http://www.w3.org/1999/xhtml"> + <template id="template">Content<span>Content</span></template> +<script type="application/javascript"><![CDATA[ + add_task(async function test_template() { + let template = document.getElementById("template"); + ok("content" in template, "Template has shadow root."); + is(template.childNodes.length, 0, "Template should have no children."); + is(template.content.childNodes.length, 2, "Template content should have two children."); + }); +]]></script> + </body> +</window> diff --git a/dom/xul/test/test_import_xul_to_content.xhtml b/dom/xul/test/test_import_xul_to_content.xhtml new file mode 100644 index 0000000000..c3475f5920 --- /dev/null +++ b/dom/xul/test/test_import_xul_to_content.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Mozilla Importing XUL into Content" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1027299" + target="_blank">Mozilla Bug 1027299</a> + </body> + + <browser id="browserelt" src="about:blank" type="content"/> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function expectWarning(expected, when, f) { + Services.console.reset(); + + f(); + + var sawWarning = false; + var msgs = Services.console.getMessageArray(); + for (var i = 0; i < msgs.length; i++) { + var msg = msgs[i]; + if (!msg || !(msg instanceof Ci.nsIScriptError)) { + continue; + } + + if (msg.category.includes("DOM") && msg.errorMessage.includes("Importing XUL")) { + sawWarning = true; + } + } + + ok(sawWarning == expected, "correct warning condition when " + when); + } + + var browser = document.getElementById("browserelt"); + browser.addEventListener("load", function() { + var doc = browser.contentDocument; + + // We add a <video> element, which contains anonymous XUL content. This should not warn. + var video = doc.createElement("video"); + expectWarning(false, "appending video", function() { + doc.documentElement.appendChild(video); + // Force a layout flush to make sure the anonymous content is added. + // eslint-disable-next-line no-unused-vars + let dummy = doc.documentElement.offsetLeft; + }); + + // We add some XUL to a content document. This should generate a warning. + var elt = document.createXULElement("label"); + var newElt = doc.importNode(elt, false); + expectWarning(true, "appending XUL", function() { + doc.documentElement.appendChild(newElt); + }); + + SimpleTest.finish(); + }); + + ]]> + </script> +</window> diff --git a/dom/xul/test/test_xul_tabindex_focus.xhtml b/dom/xul/test/test_xul_tabindex_focus.xhtml new file mode 100644 index 0000000000..1363d9b1e1 --- /dev/null +++ b/dom/xul/test/test_xul_tabindex_focus.xhtml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 +--> +<window title="Mozilla Bug 1128054" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<body> +<!-- Test default focusability --> +<label></label> +<!-- Test tabindex=0 focusability --> +<label tabindex="0"></label> +<!-- Test tabindex=-1 focusability --> +<label tabindex="-1"></label> +<!-- Test tabindex=invalid focusability --> +<label tabindex="invalid"></label> +<!-- Tests code --> +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 1128054 **/ + +add_task(function test_xul_tabindex_focus() { + for (let element of document.querySelectorAll("label")) { + let desc = "xul element"; + let focusable = false; + if (element.hasAttribute("tabindex")) { + let attr = element.getAttribute("tabindex"); + focusable = Number.isInteger(Number.parseInt(attr)); + desc += ` with tabindex=${attr}`; + } + + element.focus(); + focusable ? is(document.activeElement, element, desc + " should focusable") + : isnot(document.activeElement, element, desc + " should not focusable"); + } +}); + +]]> +</script> +</body> +</window> diff --git a/dom/xul/test/window_bug1686822.xhtml b/dom/xul/test/window_bug1686822.xhtml new file mode 100644 index 0000000000..1a2cf25636 --- /dev/null +++ b/dom/xul/test/window_bug1686822.xhtml @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + <html:script type="module"><![CDATA[ + window.opener.moduleScriptRan(); + ]]></html:script> +</window> diff --git a/dom/xul/test/window_bug583948.xhtml b/dom/xul/test/window_bug583948.xhtml new file mode 100644 index 0000000000..d0f6a26926 --- /dev/null +++ b/dom/xul/test/window_bug583948.xhtml @@ -0,0 +1,8 @@ +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="opener.update()"> + +<command oncommandupdate="document.removeChild(document.documentElement)" commandupdater="true"/> +<box command="c"/> +<iframe/> + +</window> diff --git a/dom/xul/test/window_bug757137.xhtml b/dom/xul/test/window_bug757137.xhtml new file mode 100644 index 0000000000..16f38ef20f --- /dev/null +++ b/dom/xul/test/window_bug757137.xhtml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <html:iframe id="f" mozbrowser="true" + src="data:text/html;charset=utf-8,%3C!DOCTYPE html>Hi" /> +</window> |