diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /dom/base | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/base')
111 files changed, 4888 insertions, 807 deletions
diff --git a/dom/base/AbstractRange.cpp b/dom/base/AbstractRange.cpp index 91234bf0a7..c9138a19d2 100644 --- a/dom/base/AbstractRange.cpp +++ b/dom/base/AbstractRange.cpp @@ -10,6 +10,7 @@ #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/RangeUtils.h" +#include "mozilla/dom/ChildIterator.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/dom/Selection.h" @@ -87,6 +88,29 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRegisteredClosestCommonInclusiveAncestor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +// When aMarkDesendants is true, Set +// DescendantOfClosestCommonInclusiveAncestorForRangeInSelection flag for the +// flattened children of aNode. When aMarkDesendants is false, unset that flag +// for the flattened children of aNode. +void UpdateDescendantsInFlattenedTree(const nsIContent& aNode, + bool aMarkDesendants) { + if (!aNode.IsElement() || aNode.IsHTMLElement(nsGkAtoms::slot)) { + return; + } + + FlattenedChildIterator iter(&aNode); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (aMarkDesendants) { + child->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); + } else { + child + ->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); + } + UpdateDescendantsInFlattenedTree(*child, aMarkDesendants); + } +} + void AbstractRange::MarkDescendants(const nsINode& aNode) { // Set NodeIsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection on // aNode's descendants unless aNode is already marked as a range common @@ -95,10 +119,22 @@ void AbstractRange::MarkDescendants(const nsINode& aNode) { if (!aNode.IsMaybeSelected()) { // don't set the Descendant bit on |aNode| itself nsINode* node = aNode.GetNextNode(&aNode); + if (!node) { + if (aNode.GetShadowRootForSelection()) { + UpdateDescendantsInFlattenedTree(*aNode.AsContent(), true); + } + return; + } while (node) { node->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) { - node = node->GetNextNode(&aNode); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + UpdateDescendantsInFlattenedTree(*node->AsContent(), true); + // sub-tree of node has been marked already + node = node->GetNextNonChildNode(&aNode); + } else { + node = node->GetNextNode(&aNode); + } } else { // optimize: skip this sub-tree since it's marked already. node = node->GetNextNonChildNode(&aNode); @@ -116,10 +152,22 @@ void AbstractRange::UnmarkDescendants(const nsINode& aNode) { .IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) { // we know |aNode| doesn't have any bit set nsINode* node = aNode.GetNextNode(&aNode); + if (!node) { + if (aNode.GetShadowRootForSelection()) { + UpdateDescendantsInFlattenedTree(*aNode.AsContent(), false); + } + return; + } while (node) { node->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) { - node = node->GetNextNode(&aNode); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + UpdateDescendantsInFlattenedTree(*node->AsContent(), false); + // sub-tree has been marked already + node = node->GetNextNonChildNode(&aNode); + } else { + node = node->GetNextNode(&aNode); + } } else { // We found an ancestor of an overlapping range, skip its descendants. node = node->GetNextNonChildNode(&aNode); @@ -185,10 +233,62 @@ bool AbstractRange::MaybeCacheToReuse(RangeType& aInstance) { return true; } -nsINode* AbstractRange::GetClosestCommonInclusiveAncestor() const { - return mIsPositioned ? nsContentUtils::GetClosestCommonInclusiveAncestor( - mStart.Container(), mEnd.Container()) - : nullptr; +nsINode* AbstractRange::GetClosestCommonInclusiveAncestor( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { + if (!mIsPositioned) { + return nullptr; + } + nsINode* startContainer = + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? GetMayCrossShadowBoundaryStartContainer() + : GetStartContainer(); + nsINode* endContainer = + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? GetMayCrossShadowBoundaryEndContainer() + : GetEndContainer(); + + if (MayCrossShadowBoundary() && + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + // Since both the start container and the end container are + // guaranteed to be in the same composed document. + // If one of the boundary is a document, use that document + // as the common ancestor since both nodes. + const bool oneBoundaryIsDocument = + (startContainer && startContainer->IsDocument()) || + (endContainer && endContainer->IsDocument()); + if (oneBoundaryIsDocument) { + MOZ_ASSERT_IF( + startContainer && startContainer->IsDocument(), + !endContainer || endContainer->GetComposedDoc() == startContainer); + MOZ_ASSERT_IF( + endContainer && endContainer->IsDocument(), + !startContainer || startContainer->GetComposedDoc() == endContainer); + + return startContainer ? startContainer->GetComposedDoc() + : endContainer->GetComposedDoc(); + } + + const auto rescope = [](nsINode*& aContainer) { + if (!aContainer) { + return; + } + // RangeBoundary allows the container to be shadow roots; When + // this happens, we should use the shadow host here. + if (auto* shadowRoot = ShadowRoot::FromNode(aContainer)) { + aContainer = shadowRoot->GetHost(); + return; + } + }; + + rescope(startContainer); + rescope(endContainer); + + return nsContentUtils::GetCommonFlattenedTreeAncestorForSelection( + startContainer ? startContainer->AsContent() : nullptr, + endContainer ? endContainer->AsContent() : nullptr); + } + return nsContentUtils::GetClosestCommonInclusiveAncestor(startContainer, + endContainer); } // static @@ -237,9 +337,28 @@ nsresult AbstractRange::SetStartAndEndInternal( return NS_ERROR_DOM_INDEX_SIZE_ERR; } - // If they have different root, this should be collapsed at the end point. + // Different root if (newStartRoot != newEndRoot) { - aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot); + if (aRange->IsStaticRange()) { + // StaticRange allows nodes in different trees, so set start and end + // accordingly + aRange->DoSetRange(aStartBoundary, aEndBoundary, newEndRoot); + } else { + MOZ_ASSERT(aRange->IsDynamicRange()); + // In contrast, nsRange keeps both. It has a pair of start and end + // which they have been collapsed to one end, and it also may have a pair + // of start and end which are the original value. + aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot); + + // Don't create the cross shadow bounday range if the one of the roots is + // an UA widget regardless whether the boundaries are allowed to cross + // shadow boundary or not. + if (!IsRootUAWidget(newStartRoot) && !IsRootUAWidget(newEndRoot)) { + aRange->AsDynamicRange() + ->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(aStartBoundary, + aEndBoundary); + } + } return NS_OK; } @@ -274,7 +393,10 @@ void AbstractRange::RegisterSelection(Selection& aSelection) { bool isFirstSelection = mSelections.IsEmpty(); mSelections.AppendElement(&aSelection); if (isFirstSelection && !mRegisteredClosestCommonInclusiveAncestor) { - nsINode* commonAncestor = GetClosestCommonInclusiveAncestor(); + nsINode* commonAncestor = GetClosestCommonInclusiveAncestor( + StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() + ? AllowRangeCrossShadowBoundary::Yes + : AllowRangeCrossShadowBoundary::No); MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes"); RegisterClosestCommonInclusiveAncestor(commonAncestor); } @@ -358,7 +480,8 @@ void AbstractRange::UnregisterClosestCommonInclusiveAncestor( void AbstractRange::UpdateCommonAncestorIfNecessary() { nsINode* oldCommonAncestor = mRegisteredClosestCommonInclusiveAncestor; - nsINode* newCommonAncestor = GetClosestCommonInclusiveAncestor(); + nsINode* newCommonAncestor = + GetClosestCommonInclusiveAncestor(AllowRangeCrossShadowBoundary::Yes); if (newCommonAncestor != oldCommonAncestor) { if (oldCommonAncestor) { UnregisterClosestCommonInclusiveAncestor(oldCommonAncestor, false); @@ -379,6 +502,59 @@ void AbstractRange::UpdateCommonAncestorIfNecessary() { } } +const RangeBoundary& AbstractRange::MayCrossShadowBoundaryStartRef() const { + return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryStartRef() + : mStart; +} + +const RangeBoundary& AbstractRange::MayCrossShadowBoundaryEndRef() const { + return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryEndRef() + : mEnd; +} + +nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtStartOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtStartOffset() + : mStart.GetChildAtOffset(); +} + +nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtEndOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtEndOffset() + : mEnd.GetChildAtOffset(); +} + +nsINode* AbstractRange::GetMayCrossShadowBoundaryStartContainer() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryStartContainer() + : mStart.Container(); +} + +nsINode* AbstractRange::GetMayCrossShadowBoundaryEndContainer() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryEndContainer() + : mEnd.Container(); +} + +bool AbstractRange::MayCrossShadowBoundary() const { + return IsDynamicRange() ? !!AsDynamicRange()->GetCrossShadowBoundaryRange() + : false; +} + +uint32_t AbstractRange::MayCrossShadowBoundaryStartOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->MayCrossShadowBoundaryStartOffset() + : static_cast<uint32_t>(*mStart.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); +} + +uint32_t AbstractRange::MayCrossShadowBoundaryEndOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->MayCrossShadowBoundaryEndOffset() + : static_cast<uint32_t>(*mEnd.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); +} + nsINode* AbstractRange::GetParentObject() const { return mOwner; } JSObject* AbstractRange::WrapObject(JSContext* aCx, @@ -395,4 +571,12 @@ void AbstractRange::ClearForReuse() { mCalledByJS = false; } +/*static*/ +bool AbstractRange::IsRootUAWidget(const nsINode* aRoot) { + MOZ_ASSERT(aRoot); + if (const ShadowRoot* shadowRoot = ShadowRoot::FromNode(aRoot)) { + return shadowRoot->IsUAWidget(); + } + return false; +} } // namespace mozilla::dom diff --git a/dom/base/AbstractRange.h b/dom/base/AbstractRange.h index c70aaf19ec..2f9b59158a 100644 --- a/dom/base/AbstractRange.h +++ b/dom/base/AbstractRange.h @@ -31,10 +31,15 @@ class Document; class Selection; class StaticRange; +enum class AllowRangeCrossShadowBoundary : bool { No, Yes }; + class AbstractRange : public nsISupports, public nsWrapperCache, // For linking together selection-associated ranges. public mozilla::LinkedListElement<AbstractRange> { + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; + protected: explicit AbstractRange(nsINode* aNode, bool aIsDynamicRange); virtual ~AbstractRange(); @@ -51,18 +56,33 @@ class AbstractRange : public nsISupports, NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AbstractRange) + /** + * All of the MayCrossShadowBoundary* methods are used to get the boundary + * endpoints that cross shadow boundaries. They would return + * the same value as the non-MayCrossShadowBoundary* methods if the range + * boundaries don't cross shadow boundaries. + */ const RangeBoundary& StartRef() const { return mStart; } + const RangeBoundary& MayCrossShadowBoundaryStartRef() const; + const RangeBoundary& EndRef() const { return mEnd; } + const RangeBoundary& MayCrossShadowBoundaryEndRef() const; nsIContent* GetChildAtStartOffset() const { return mStart.GetChildAtOffset(); } + nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const; + nsIContent* GetChildAtEndOffset() const { return mEnd.GetChildAtOffset(); } + nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const; + bool IsPositioned() const { return mIsPositioned; } /** * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor */ - nsINode* GetClosestCommonInclusiveAncestor() const; + nsINode* GetClosestCommonInclusiveAncestor( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; // WebIDL @@ -75,7 +95,12 @@ class AbstractRange : public nsISupports, // `IsPositioned()` directly. nsINode* GetStartContainer() const { return mStart.Container(); } + nsINode* GetMayCrossShadowBoundaryStartContainer() const; + nsINode* GetEndContainer() const { return mEnd.Container(); } + nsINode* GetMayCrossShadowBoundaryEndContainer() const; + + bool MayCrossShadowBoundary() const; Document* GetComposedDocOfContainers() const { return mStart.Container() ? mStart.Container()->GetComposedDoc() : nullptr; @@ -86,12 +111,15 @@ class AbstractRange : public nsISupports, return static_cast<uint32_t>( *mStart.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); } + uint32_t MayCrossShadowBoundaryStartOffset() const; // FYI: Returns 0 if it's not positioned. uint32_t EndOffset() const { return static_cast<uint32_t>( *mEnd.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); } + uint32_t MayCrossShadowBoundaryEndOffset() const; + bool Collapsed() const { return !mIsPositioned || (mStart.Container() == mEnd.Container() && StartOffset() == EndOffset()); @@ -132,6 +160,11 @@ class AbstractRange : public nsISupports, */ bool IsInSelection(const mozilla::dom::Selection& aSelection) const; + /** + * Return true if aRoot is a UA shadow root. + */ + static bool IsRootUAWidget(const nsINode* aRoot); + protected: template <typename SPT, typename SRT, typename EPT, typename ERT, typename RangeType> diff --git a/dom/base/BodyUtil.cpp b/dom/base/BodyUtil.cpp index e8de3d18ec..45ca58e2d9 100644 --- a/dom/base/BodyUtil.cpp +++ b/dom/base/BodyUtil.cpp @@ -422,9 +422,10 @@ already_AddRefed<FormData> BodyUtil::ConsumeFormData( if (isValidUrlEncodedMimeType) { RefPtr<FormData> fd = new FormData(aParent); DebugOnly<bool> status = URLParams::Parse( - aStr, true, [&fd](const nsAString& aName, const nsAString& aValue) { - ErrorResult rv; - fd->Append(aName, aValue, rv); + aStr, true, [&fd](const nsACString& aName, const nsACString& aValue) { + IgnoredErrorResult rv; + fd->Append(NS_ConvertUTF8toUTF16(aName), + NS_ConvertUTF8toUTF16(aValue), rv); MOZ_ASSERT(!rv.Failed()); return true; }); diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp index 0df1cd3c9b..407f33e044 100644 --- a/dom/base/ChromeUtils.cpp +++ b/dom/base/ChromeUtils.cpp @@ -1289,9 +1289,10 @@ void ChromeUtils::GetBaseDomainFromPartitionKey(dom::GlobalObject& aGlobal, nsString scheme; nsString pkBaseDomain; int32_t port; + bool ancestor; - if (!mozilla::OriginAttributes::ParsePartitionKey(aPartitionKey, scheme, - pkBaseDomain, port)) { + if (!mozilla::OriginAttributes::ParsePartitionKey( + aPartitionKey, scheme, pkBaseDomain, port, ancestor)) { aRv.Throw(NS_ERROR_FAILURE); return; } @@ -1317,7 +1318,10 @@ void ChromeUtils::GetPartitionKeyFromURL(dom::GlobalObject& aGlobal, } mozilla::OriginAttributes attrs; - attrs.SetPartitionKey(uri); + // For now, uses assume the partition key is cross-site. + // We will need to not make this assumption to allow access + // to same-site partitioned cookies in the cookie extension API. + attrs.SetPartitionKey(uri, false); aPartitionKey = attrs.mPartitionKey; } @@ -1515,10 +1519,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal, // DOM windows. /* aUtilityInfo = */ std::move(utilityActors), /* aChild = */ 0 // Without a ContentProcess, no ChildId. -#ifdef XP_MACOSX +#ifdef XP_DARWIN , /* aChildTask = */ aGeckoProcess->GetChildTask() -#endif // XP_MACOSX +#endif // XP_DARWIN ); }); @@ -1617,10 +1621,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal, /* aWindowInfo = */ std::move(windows), /* aUtilityInfo = */ nsTArray<UtilityInfo>(), /* aChild = */ contentParent->ChildID() -#ifdef XP_MACOSX +#ifdef XP_DARWIN , /* aChildTask = */ contentParent->Process()->GetChildTask() -#endif // XP_MACOSX +#endif // XP_DARWIN ); } diff --git a/dom/base/ContentIterator.cpp b/dom/base/ContentIterator.cpp index 6819353520..0b405f0348 100644 --- a/dom/base/ContentIterator.cpp +++ b/dom/base/ContentIterator.cpp @@ -7,6 +7,7 @@ #include "ContentIterator.h" #include "mozilla/Assertions.h" +#include "mozilla/dom/ShadowRoot.h" #include "mozilla/DebugOnly.h" #include "mozilla/RangeBoundary.h" #include "mozilla/RangeUtils.h" @@ -26,6 +27,69 @@ using namespace dom; __VA_ARGS__); \ template aResultType ContentIteratorBase<nsINode*>::aMethodName(__VA_ARGS__) +/** + * IteratorHelpers contains the static methods to help extra values + * based on whether or not the iterator allows to iterate nodes cross the shadow + * boundary. + */ +struct IteratorHelpers { + IteratorHelpers() = delete; + + static nsINode* GetStartContainer(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->GetMayCrossShadowBoundaryStartContainer() + : aRange->GetStartContainer(); + } + + static int32_t StartOffset(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->MayCrossShadowBoundaryStartOffset() + : aRange->StartOffset(); + } + + static nsINode* GetEndContainer(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->GetMayCrossShadowBoundaryEndContainer() + : aRange->GetEndContainer(); + } + + static int32_t EndOffset(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->MayCrossShadowBoundaryEndOffset() + : aRange->EndOffset(); + } + + // FIXME(sefeng): This doesn't work with slots / flattened tree. + static nsINode* GetParentNode(nsINode& aNode, + bool aAllowCrossShadowBoundary) { + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aNode.GetParentOrShadowHostNode() + : aNode.GetParentNode(); + } + + static ShadowRoot* GetShadowRoot(const nsINode* aNode, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aNode); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aNode->GetShadowRootForSelection() + : nullptr; + } +}; + static bool ComparePostMode(const RawRangeBoundary& aStart, const RawRangeBoundary& aEnd, nsINode& aNode) { nsINode* parent = aNode.GetParentNode(); @@ -541,17 +605,45 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepFirstChild(nsINode* aRoot) { // static template <typename NodeType> nsIContent* ContentIteratorBase<NodeType>::GetDeepFirstChild( - nsIContent* aRoot) { + nsIContent* aRoot, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aRoot)) { return nullptr; } nsIContent* node = aRoot; - nsIContent* child = node->GetFirstChild(); + nsIContent* child = nullptr; + + if (ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) { + // When finding the deepest child of node, if this node has a + // web exposed shadow root, we use this shadow root to find the deepest + // child. + // If the first candidate should be a slotted content, + // shadowRoot->GetFirstChild() should be able to return the <slot> element. + // It's probably correct I think. Then it's up to the caller of this + // iterator to decide whether to use the slot's assigned nodes or not. + MOZ_ASSERT(aAllowCrossShadowBoundary); + child = shadowRoot->GetFirstChild(); + } else { + child = node->GetFirstChild(); + } while (child) { node = child; - child = node->GetFirstChild(); + if (ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) { + // When finding the deepest child of node, if this node has a + // web exposed shadow root, we use this shadow root to find the deepest + // child. + // If the first candidate should be a slotted content, + // shadowRoot->GetFirstChild() should be able to return the <slot> + // element. It's probably correct I think. Then it's up to the caller of + // this iterator to decide whether to use the slot's assigned nodes or + // not. + child = shadowRoot->GetFirstChild(); + } else { + child = node->GetFirstChild(); + } } return node; @@ -569,23 +661,41 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepLastChild(nsINode* aRoot) { // static template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild(nsIContent* aRoot) { +nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild( + nsIContent* aRoot, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aRoot)) { return nullptr; } nsIContent* node = aRoot; - while (node->HasChildren()) { - nsIContent* child = node->GetLastChild(); - node = child; + + ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary); + // FIXME(sefeng): This doesn't work with slots / flattened tree. + while (node->HasChildren() || (shadowRoot && shadowRoot->HasChildren())) { + if (node->HasChildren()) { + node = node->GetLastChild(); + } else { + MOZ_ASSERT(shadowRoot); + // If this node doesn't have a child, but it's also a shadow host + // that can be selected, we go into this shadow tree. + node = shadowRoot->GetLastChild(); + } + shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary); } return node; } -// Get the next sibling, or parent's next sibling, or grandpa's next sibling... +// Get the next sibling, or parent's next sibling, or shadow host's next +// sibling (when aAllowCrossShadowBoundary is true), or grandpa's next +// sibling... +// // static +// template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) { +nsIContent* ContentIteratorBase<NodeType>::GetNextSibling( + nsINode* aNode, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aNode)) { return nullptr; } @@ -594,18 +704,32 @@ nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) { return next; } - nsINode* parent = aNode->GetParentNode(); + nsINode* parent = + IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary); if (NS_WARN_IF(!parent)) { return nullptr; } - return ContentIteratorBase::GetNextSibling(parent); + if (aAllowCrossShadowBoundary) { + // This is temporary solution. + // For shadow root, instead of getting to the sibling of the parent + // directly, we need to get into the light tree of the parent to handle + // slotted contents. + if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(aNode)) { + if (nsIContent* child = parent->GetFirstChild()) { + return child; + } + } + } + + return ContentIteratorBase::GetNextSibling(parent, aAllowCrossShadowBoundary); } -// Get the prev sibling, or parent's prev sibling, or grandpa's prev sibling... -// static +// Get the prev sibling, or parent's prev sibling, or shadow host's prev sibling +// (when aAllowCrossShadowBoundary is true), or grandpa's prev sibling... static template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) { +nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling( + nsINode* aNode, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aNode)) { return nullptr; } @@ -614,12 +738,13 @@ nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) { return prev; } - nsINode* parent = aNode->GetParentNode(); + nsINode* parent = + IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary); if (NS_WARN_IF(!parent)) { return nullptr; } - return ContentIteratorBase::GetPrevSibling(parent); + return ContentIteratorBase::GetPrevSibling(parent, aAllowCrossShadowBoundary); } template <typename NodeType> @@ -860,19 +985,41 @@ nsresult ContentSubtreeIterator::Init(const RawRangeBoundary& aStartBoundary, return InitWithRange(); } +nsresult ContentSubtreeIterator::InitWithAllowCrossShadowBoundary( + AbstractRange* aRange) { + MOZ_ASSERT(aRange); + + if (NS_WARN_IF(!aRange->IsPositioned())) { + return NS_ERROR_INVALID_ARG; + } + + mRange = aRange; + + mAllowCrossShadowBoundary = AllowRangeCrossShadowBoundary::Yes; + return InitWithRange(); +} + void ContentSubtreeIterator::CacheInclusiveAncestorsOfEndContainer() { mInclusiveAncestorsOfEndContainer.Clear(); - nsINode* const endContainer = mRange->GetEndContainer(); + nsINode* const endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); nsIContent* endNode = endContainer->IsContent() ? endContainer->AsContent() : nullptr; while (endNode) { mInclusiveAncestorsOfEndContainer.AppendElement(endNode); - endNode = endNode->GetParent(); + // Cross the boundary for contents in shadow tree. + nsINode* parent = IteratorHelpers::GetParentNode( + *endNode, IterAllowCrossShadowBoundary()); + if (!parent || !parent->IsContent()) { + break; + } + endNode = parent->AsContent(); } } nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { - nsINode* startContainer = mRange->GetStartContainer(); + nsINode* startContainer = IteratorHelpers::GetStartContainer( + mRange, IterAllowCrossShadowBoundary()); nsIContent* firstCandidate = nullptr; // find first node in range nsINode* node = nullptr; @@ -880,9 +1027,14 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { // no children, start at the node itself node = startContainer; } else { - nsIContent* child = mRange->GetChildAtStartOffset(); - MOZ_ASSERT(child == - startContainer->GetChildAt_Deprecated(mRange->StartOffset())); + nsIContent* child = + IterAllowCrossShadowBoundary() + ? mRange->GetMayCrossShadowBoundaryChildAtStartOffset() + : mRange->GetChildAtStartOffset(); + + MOZ_ASSERT(child == startContainer->GetChildAt_Deprecated( + IteratorHelpers::StartOffset( + mRange, IterAllowCrossShadowBoundary()))); if (!child) { // offset after last child node = startContainer; @@ -893,11 +1045,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { if (!firstCandidate) { // then firstCandidate is next node after node - firstCandidate = ContentIteratorBase::GetNextSibling(node); + firstCandidate = ContentIteratorBase::GetNextSibling( + node, IterAllowCrossShadowBoundary()); } if (firstCandidate) { - firstCandidate = ContentIteratorBase::GetDeepFirstChild(firstCandidate); + firstCandidate = ContentIteratorBase::GetDeepFirstChild( + firstCandidate, IterAllowCrossShadowBoundary()); } return firstCandidate; @@ -926,9 +1080,12 @@ nsIContent* ContentSubtreeIterator::DetermineFirstContent() const { nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { nsIContent* lastCandidate{nullptr}; - nsINode* endContainer = mRange->GetEndContainer(); + nsINode* endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); // now to find the last node - int32_t offset = mRange->EndOffset(); + int32_t offset = + IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary()); + int32_t numChildren = endContainer->GetChildCount(); nsINode* node = nullptr; @@ -939,7 +1096,9 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { if (!offset || !numChildren) { node = endContainer; } else { - lastCandidate = mRange->EndRef().Ref(); + lastCandidate = IterAllowCrossShadowBoundary() + ? mRange->MayCrossShadowBoundaryEndRef().Ref() + : mRange->EndRef().Ref(); MOZ_ASSERT(lastCandidate == endContainer->GetChildAt_Deprecated(--offset)); NS_ASSERTION(lastCandidate, "tree traversal trouble in ContentSubtreeIterator::Init"); @@ -947,11 +1106,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { if (!lastCandidate) { // then lastCandidate is prev node before node - lastCandidate = ContentIteratorBase::GetPrevSibling(node); + lastCandidate = ContentIteratorBase::GetPrevSibling( + node, IterAllowCrossShadowBoundary()); } if (lastCandidate) { - lastCandidate = ContentIteratorBase::GetDeepLastChild(lastCandidate); + lastCandidate = ContentIteratorBase::GetDeepLastChild( + lastCandidate, IterAllowCrossShadowBoundary()); } return lastCandidate; @@ -962,11 +1123,17 @@ nsresult ContentSubtreeIterator::InitWithRange() { MOZ_ASSERT(mRange->IsPositioned()); // get the start node and offset, convert to nsINode - mClosestCommonInclusiveAncestor = mRange->GetClosestCommonInclusiveAncestor(); - nsINode* startContainer = mRange->GetStartContainer(); - const int32_t startOffset = mRange->StartOffset(); - nsINode* endContainer = mRange->GetEndContainer(); - const int32_t endOffset = mRange->EndOffset(); + mClosestCommonInclusiveAncestor = + mRange->GetClosestCommonInclusiveAncestor(mAllowCrossShadowBoundary); + + nsINode* startContainer = IteratorHelpers::GetStartContainer( + mRange, IterAllowCrossShadowBoundary()); + const int32_t startOffset = + IteratorHelpers::StartOffset(mRange, IterAllowCrossShadowBoundary()); + nsINode* endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); + const int32_t endOffset = + IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary()); MOZ_ASSERT(mClosestCommonInclusiveAncestor && startContainer && endContainer); // Bug 767169 MOZ_ASSERT(uint32_t(startOffset) <= startContainer->Length() && @@ -1044,14 +1211,23 @@ void ContentSubtreeIterator::Next() { return; } - nsINode* nextNode = ContentIteratorBase::GetNextSibling(mCurNode); + nsINode* nextNode = ContentIteratorBase::GetNextSibling( + mCurNode, IterAllowCrossShadowBoundary()); + NS_ASSERTION(nextNode, "No next sibling!?! This could mean deadlock!"); int32_t i = mInclusiveAncestorsOfEndContainer.IndexOf(nextNode); while (i != -1) { // as long as we are finding ancestors of the endpoint of the range, // dive down into their children - nextNode = nextNode->GetFirstChild(); + ShadowRoot* root = IteratorHelpers::GetShadowRoot( + Element::FromNode(nextNode), IterAllowCrossShadowBoundary()); + if (!root) { + nextNode = nextNode->GetFirstChild(); + } else { + nextNode = mRange->MayCrossShadowBoundary() ? root->GetFirstChild() + : nextNode->GetFirstChild(); + } NS_ASSERTION(nextNode, "Iterator error, expected a child node!"); // should be impossible to get a null pointer. If we went all the way @@ -1098,7 +1274,8 @@ nsresult ContentSubtreeIterator::PositionAt(nsINode* aCurNode) { nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( nsINode* aNode) const { - if (!aNode || !aNode->GetParentNode()) { + if (!aNode || + !IteratorHelpers::GetParentNode(*aNode, IterAllowCrossShadowBoundary())) { return nullptr; } @@ -1114,15 +1291,23 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( return nullptr; } + nsIContent* lastContentInShadowTree = nullptr; while (content) { - nsIContent* parent = content->GetParent(); + nsINode* parent = IteratorHelpers::GetParentNode( + *content, IterAllowCrossShadowBoundary()); + // content always has a parent. If its parent is the root, however -- // i.e., either it's not content, or it is content but its own parent is // null -- then we're finished, since we don't go up to the root. // + // Caveat: If iteration crossing shadow boundary is allowed + // and the root is a shadow root, we keep going up to the + // shadow host and continue. + // // We have to special-case this because CompareNodeToRange treats the root // node differently -- see bug 765205. - if (!parent || !parent->GetParentNode()) { + if (!parent || !IteratorHelpers::GetParentNode( + *parent, IterAllowCrossShadowBoundary())) { return content; } @@ -1130,10 +1315,28 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( RangeUtils::IsNodeContainedInRange(*parent, mRange); MOZ_ALWAYS_TRUE(isNodeContainedInRange); if (!isNodeContainedInRange.value()) { + if (IterAllowCrossShadowBoundary() && content->IsShadowRoot()) { + MOZ_ASSERT(parent->GetShadowRoot() == content); + // host element is not in range, the last content in tree + // should be the ancestor. + MOZ_ASSERT(lastContentInShadowTree); + return lastContentInShadowTree; + } return content; } - content = parent; + // When we cross the boundary, we keep a reference to the + // last content that is in tree, because if we later + // find the shadow host element is not in the range, that means + // the last content in the tree should be top ancestor in range. + // + // Using shadow root doesn't make sense here because it doesn't + // represent a actual content. + if (IterAllowCrossShadowBoundary() && parent->IsShadowRoot()) { + lastContentInShadowTree = content; + } + + content = parent->AsContent(); } MOZ_CRASH("This should only be possible if aNode was null"); diff --git a/dom/base/ContentIterator.h b/dom/base/ContentIterator.h index b645c4147e..67962d41d5 100644 --- a/dom/base/ContentIterator.h +++ b/dom/base/ContentIterator.h @@ -82,15 +82,26 @@ class ContentIteratorBase { // Recursively get the deepest first/last child of aRoot. This will return // aRoot itself if it has no children. static nsINode* GetDeepFirstChild(nsINode* aRoot); - static nsIContent* GetDeepFirstChild(nsIContent* aRoot); + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree + // when it reaches to a shadow host. + static nsIContent* GetDeepFirstChild(nsIContent* aRoot, + bool aAllowCrossShadowBoundary); static nsINode* GetDeepLastChild(nsINode* aRoot); - static nsIContent* GetDeepLastChild(nsIContent* aRoot); + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree + // when it reaches to a shadow host. + static nsIContent* GetDeepLastChild(nsIContent* aRoot, + bool aAllowCrossShadowBoundary); // Get the next/previous sibling of aNode, or its parent's, or grandparent's, // etc. Returns null if aNode and all its ancestors have no next/previous // sibling. - static nsIContent* GetNextSibling(nsINode* aNode); - static nsIContent* GetPrevSibling(nsINode* aNode); + // + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow host + // when it reaches to a shadow root. + static nsIContent* GetNextSibling(nsINode* aNode, + bool aAllowCrossShadowBoundary = false); + static nsIContent* GetPrevSibling(nsINode* aNode, + bool aAllowCrossShadowBoundary = false); nsINode* NextNode(nsINode* aNode); nsINode* PrevNode(nsINode* aNode); @@ -219,6 +230,29 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase { virtual nsresult Init(nsINode* aRoot) override; virtual nsresult Init(dom::AbstractRange* aRange) override; + + /** + * Initialize the iterator with aRange that does correct things + * when the aRange's start and/or the end containers are + * in shadow dom. + * + * If both start and end containers are in light dom, the iterator + * won't do anything special. + * + * When the start container is in shadow dom, the iterator can + * find the correct start node by crossing the shadow + * boundary when needed. + * + * When the end container is in shadow dom, the iterator can find + * the correct end node by crossing the shadow boundary when + * needed. Also when the next node is an ancestor of + * the end node, it can correctly iterate into the + * subtree of it by crossing the shadow boundary. + * + * Examples of what nodes will be returned can be found + * at test_content_iterator_subtree_shadow_tree.html. + */ + nsresult InitWithAllowCrossShadowBoundary(dom::AbstractRange* aRange); virtual nsresult Init(nsINode* aStartContainer, uint32_t aStartOffset, nsINode* aEndContainer, uint32_t aEndOffset) override; virtual nsresult Init(const RawRangeBoundary& aStartBoundary, @@ -276,10 +310,18 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase { // the range's start and end nodes will never be considered "in" it. nsIContent* GetTopAncestorInRange(nsINode* aNode) const; + bool IterAllowCrossShadowBoundary() const { + return mAllowCrossShadowBoundary == dom::AllowRangeCrossShadowBoundary::Yes; + } + RefPtr<dom::AbstractRange> mRange; // See <https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor>. AutoTArray<nsIContent*, 8> mInclusiveAncestorsOfEndContainer; + + // Whether this iterator allows to iterate nodes across shadow boundary. + dom::AllowRangeCrossShadowBoundary mAllowCrossShadowBoundary = + dom::AllowRangeCrossShadowBoundary::No; }; } // namespace mozilla diff --git a/dom/base/ContentProcessMessageManager.cpp b/dom/base/ContentProcessMessageManager.cpp index 7661d1036f..9723341782 100644 --- a/dom/base/ContentProcessMessageManager.cpp +++ b/dom/base/ContentProcessMessageManager.cpp @@ -104,6 +104,7 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() { jsapi.Init(); if (!GetOrCreateDOMReflectorNoWrap(jsapi.cx(), this, &val)) { + JS_ClearPendingException(jsapi.cx()); return nullptr; } } @@ -111,11 +112,15 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() { return &val.toObject(); } -void ContentProcessMessageManager::LoadScript(const nsAString& aURL) { +bool ContentProcessMessageManager::LoadScript(const nsAString& aURL) { Init(); - JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(), - GetOrCreateWrapper()); - LoadScriptInternal(messageManager, aURL, true); + JSObject* wrapper = GetOrCreateWrapper(); + if (wrapper) { + JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(), wrapper); + LoadScriptInternal(messageManager, aURL, true); + return true; + } + return false; } void ContentProcessMessageManager::SetInitialProcessData( diff --git a/dom/base/ContentProcessMessageManager.h b/dom/base/ContentProcessMessageManager.h index b7c54ba452..0d437b5e50 100644 --- a/dom/base/ContentProcessMessageManager.h +++ b/dom/base/ContentProcessMessageManager.h @@ -58,7 +58,7 @@ class ContentProcessMessageManager : public nsIMessageSender, virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; - JSObject* GetOrCreateWrapper(); + [[nodiscard]] JSObject* GetOrCreateWrapper(); using MessageManagerGlobal::AddMessageListener; using MessageManagerGlobal::AddWeakMessageListener; @@ -84,7 +84,7 @@ class ContentProcessMessageManager : public nsIMessageSender, return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); } - virtual void LoadScript(const nsAString& aURL); + [[nodiscard]] virtual bool LoadScript(const nsAString& aURL); bool IsProcessScoped() const override { return true; } diff --git a/dom/base/DirectionalityUtils.cpp b/dom/base/DirectionalityUtils.cpp index dd427c61b1..2e4ada4800 100644 --- a/dom/base/DirectionalityUtils.cpp +++ b/dom/base/DirectionalityUtils.cpp @@ -191,10 +191,8 @@ static bool ParticipatesInAutoDirection(const nsIContent* aContent) { if (aContent->IsShadowRoot()) { return true; } - dom::NodeInfo* ni = aContent->NodeInfo(); - return ni->NamespaceID() == kNameSpaceID_XHTML && - !ni->Equals(nsGkAtoms::script) && !ni->Equals(nsGkAtoms::style) && - !ni->Equals(nsGkAtoms::input) && !ni->Equals(nsGkAtoms::textarea); + return !aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style, + nsGkAtoms::input, nsGkAtoms::textarea); } /** diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 4e9286a91e..8cbf8b8075 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -172,6 +172,8 @@ #include "mozilla/dom/FeaturePolicy.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/FontFaceSet.h" +#include "mozilla/dom/FragmentDirective.h" +#include "mozilla/dom/fragmentdirectives_ffi_generated.h" #include "mozilla/dom/FromParser.h" #include "mozilla/dom/HighlightRegistry.h" #include "mozilla/dom/HTMLAllCollection.h" @@ -198,6 +200,7 @@ #include "mozilla/dom/NetErrorInfoBinding.h" #include "mozilla/dom/NodeInfo.h" #include "mozilla/dom/NodeIterator.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" #include "mozilla/dom/PContentChild.h" #include "mozilla/dom/PWindowGlobalChild.h" #include "mozilla/dom/PageTransitionEvent.h" @@ -1437,7 +1440,6 @@ Document::Document(const char* aContentType) mThrowOnDynamicMarkupInsertionCounter(0), mIgnoreOpensDuringUnloadCounter(0), mSavedResolution(1.0f), - mSavedResolutionBeforeMVM(1.0f), mGeneration(0), mCachedTabSizeGeneration(0), mNextFormNumber(0), @@ -2484,6 +2486,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyForIdle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFragmentDirective) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHighlightRegistry) // Traverse all Document nsCOMPtrs. @@ -2631,6 +2634,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet) NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFragmentDirective) NS_IMPL_CYCLE_COLLECTION_UNLINK(mHighlightRegistry) NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser) NS_IMPL_CYCLE_COLLECTION_UNLINK(mOnloadBlocker) @@ -4065,6 +4069,21 @@ void Document::StopDocumentLoad() { void Document::SetDocumentURI(nsIURI* aURI) { nsCOMPtr<nsIURI> oldBase = GetDocBaseURI(); mDocumentURI = aURI; + // This loosely implements §3.4.1 of Text Fragments + // https://wicg.github.io/scroll-to-text-fragment/#invoking-text-directives + // Unlike specified in the spec, the fragment directive is not stripped from + // the URL in the session history entry. Instead it is removed when the URL is + // set in the `Document`. Also, instead of storing the `uninvokedDirective` in + // `Document` as mentioned in the spec, the extracted directives are moved to + // the `FragmentDirective` object which deals with finding the ranges to + // highlight in `ScrollToRef()`. + // XXX(:jjaschke): This is only a temporary solution. + // https://bugzil.la/1881429 is filed for revisiting this. + nsTArray<TextDirective> textDirectives; + FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment( + mDocumentURI, &textDirectives); + FragmentDirective()->SetTextDirectives(std::move(textDirectives)); + nsIURI* newBase = GetDocBaseURI(); mChromeRulesEnabled = URLExtraData::ChromeRulesEnabled(aURI); @@ -4103,10 +4122,11 @@ void Document::SetDocumentURI(nsIURI* aURI) { } } -static void GetFormattedTimeString(PRTime aTime, +static void GetFormattedTimeString(PRTime aTime, bool aUniversal, nsAString& aFormattedTimeString) { PRExplodedTime prtime; - PR_ExplodeTime(aTime, PR_LocalTimeParameters, &prtime); + PR_ExplodeTime(aTime, aUniversal ? PR_GMTParameters : PR_LocalTimeParameters, + &prtime); // "MM/DD/YYYY hh:mm:ss" char formatedTime[24]; if (SprintfLiteral(formatedTime, "%02d/%02d/%04d %02d:%02d:%02d", @@ -4124,7 +4144,9 @@ void Document::GetLastModified(nsAString& aLastModified) const { if (!mLastModified.IsEmpty()) { aLastModified.Assign(mLastModified); } else { - GetFormattedTimeString(PR_Now(), aLastModified); + GetFormattedTimeString(PR_Now(), + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + aLastModified); } } @@ -6401,7 +6423,7 @@ void Document::SetLastFocusTime(const TimeStamp& aFocusTime) { mLastFocusTime = aFocusTime; } -void Document::GetReferrer(nsAString& aReferrer) const { +void Document::GetReferrer(nsACString& aReferrer) const { aReferrer.Truncate(); if (!mReferrerInfo) { return; @@ -6412,13 +6434,7 @@ void Document::GetReferrer(nsAString& aReferrer) const { return; } - nsAutoCString uri; - nsresult rv = URLDecorationStripper::StripTrackingIdentifiers(referrer, uri); - if (NS_WARN_IF(NS_FAILED(rv))) { - return; - } - - CopyUTF8toUTF16(uri, aReferrer); + URLDecorationStripper::StripTrackingIdentifiers(referrer, aReferrer); } void Document::GetCookie(nsAString& aCookie, ErrorResult& aRv) { @@ -7683,6 +7699,10 @@ static void NotifyActivityChangedCallback(nsISupports* aSupports) { void Document::NotifyActivityChanged() { EnumerateActivityObservers(NotifyActivityChangedCallback); + // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-full-activity + if (!IsActive()) { + UnlockAllWakeLocks(WakeLockType::Screen); + } } void Document::SetContainer(nsDocShell* aContainer) { @@ -11111,7 +11131,9 @@ void Document::RetrieveRelevantHeaders(nsIChannel* aChannel) { mLastModified.Truncate(); if (modDate != 0) { - GetFormattedTimeString(modDate, mLastModified); + GetFormattedTimeString(modDate, + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + mLastModified); } } @@ -12537,14 +12559,6 @@ void Document::UpdateDocumentStates(DocumentState aMaybeChangedStates, } } - if (aMaybeChangedStates.HasAtLeastOneOfStates(DocumentState::LWTHEME)) { - if (ComputeDocumentLWTheme()) { - mState |= DocumentState::LWTHEME; - } else { - mState &= ~DocumentState::LWTHEME; - } - } - if (aMaybeChangedStates.HasState(DocumentState::WINDOW_INACTIVE)) { BrowsingContext* bc = GetBrowsingContext(); if (!bc || !bc->GetIsActiveBrowserWindow()) { @@ -13089,25 +13103,29 @@ void Document::SetScrollToRef(nsIURI* aDocumentURI) { // https://html.spec.whatwg.org/#scrolling-to-a-fragment void Document::ScrollToRef() { - if (mScrolledToRefAlready) { - RefPtr<PresShell> presShell = GetPresShell(); - if (presShell) { - presShell->ScrollToAnchor(); - } + RefPtr<PresShell> presShell = GetPresShell(); + if (!presShell) { return; } - - // 2. If fragment is the empty string, then return the special value top of - // the document. - if (mScrollToRef.IsEmpty()) { + if (mScrolledToRefAlready) { + presShell->ScrollToAnchor(); return; } - RefPtr<PresShell> presShell = GetPresShell(); - if (!presShell) { + // If text directives is non-null, then highlight the text directives and + // scroll to the last one. + // XXX(:jjaschke): Document policy integration should happen here + // as soon as https://bugzil.la/1860915 lands. + // XXX(:jjaschke): Same goes for User Activation and security aspects, + // tracked in https://bugzil.la/1888756. + const bool didScrollToTextFragment = + presShell->HighlightAndGoToTextFragment(true); + + // 2. If fragment is the empty string and no text directives have been + // scrolled to, then return the special value top of the document. + if (didScrollToTextFragment || mScrollToRef.IsEmpty()) { return; } - // 3. Let potentialIndicatedElement be the result of finding a potential // indicated element given document and fragment. NS_ConvertUTF8toUTF16 ref(mScrollToRef); @@ -15089,11 +15107,6 @@ void Document::HideAllPopoversUntil(nsINode& aEndpoint, } while (repeatingHide); } -MOZ_CAN_RUN_SCRIPT_BOUNDARY void -Document::HideAllPopoversWithoutRunningScript() { - return HideAllPopoversUntil(*this, false, false); -} - void Document::HidePopover(Element& aPopover, bool aFocusPreviousElement, bool aFireEvents, ErrorResult& aRv) { RefPtr<nsGenericHTMLElement> popoverHTMLEl = @@ -15548,13 +15561,21 @@ bool Document::HasPendingFullscreenRequests() { return !iter.AtEnd(); } +MOZ_CAN_RUN_SCRIPT_BOUNDARY bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) { if (!FullscreenElementReadyCheck(*aRequest)) { return false; } + Element* elem = aRequest->Element(); + + RefPtr<nsINode> hideUntil = elem->GetTopmostPopoverAncestor(nullptr, false); + if (!hideUntil) { + hideUntil = OwnerDoc(); + } + RefPtr<Document> doc = aRequest->Document(); - doc->HideAllPopoversWithoutRunningScript(); + doc->HideAllPopoversUntil(*hideUntil, false, true); // Stash a reference to any existing fullscreen doc, we'll use this later // to detect if the origin which is fullscreen has changed. @@ -15580,7 +15601,6 @@ bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) { // Set the fullscreen element. This sets the fullscreen style on the // element, and the fullscreen-ancestor styles on ancestors of the element // in this document. - Element* elem = aRequest->Element(); SetFullscreenElement(*elem); // Set the iframe fullscreen flag. if (auto* iframe = HTMLIFrameElement::FromNode(elem)) { @@ -15698,6 +15718,11 @@ void Document::UpdateVisibilityState(DispatchVisibilityChange aDispatchEvent) { for (auto* listener : mWorkerListeners) { listener->OnVisible(visible); } + + // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-visibility + if (!visible) { + UnlockAllWakeLocks(WakeLockType::Screen); + } } } @@ -16558,16 +16583,6 @@ void Document::SetStateObject(nsIStructuredCloneContainer* scContainer) { mCachedStateObjectValid = false; } -bool Document::ComputeDocumentLWTheme() const { - if (!NodePrincipal()->IsSystemPrincipal()) { - return false; - } - - Element* element = GetRootElement(); - return element && element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::lwtheme, - nsGkAtoms::_true, eCaseMatters); -} - already_AddRefed<Element> Document::CreateHTMLElement(nsAtom* aTag) { RefPtr<mozilla::dom::NodeInfo> nodeInfo; nodeInfo = mNodeInfoManager->GetNodeInfo(aTag, nullptr, kNameSpaceID_XHTML, @@ -17525,6 +17540,18 @@ Document::CreatePermissionGrantPromise( p = new StorageAccessAPIHelper::StorageAccessPermissionGrantPromise:: Private(__func__); + // Before we prompt, see if we are same-site + if (aFrameOnly) { + nsIChannel* channel = self->GetChannel(); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (!loadInfo->GetIsThirdPartyContextToTopWindow()) { + p->Resolve(StorageAccessAPIHelper::eAllow, __func__); + return p; + } + } + } + RefPtr<PWindowGlobalChild::GetStorageAccessPermissionPromise> promise; // Test the permission MOZ_ASSERT(XRE_IsContentProcess()); @@ -18357,9 +18384,13 @@ class UnlockAllWakeLockRunnable final : public Runnable { void Document::UnlockAllWakeLocks(WakeLockType aType) { // Perform unlock in a runnable to prevent UnlockAll being MOZ_CAN_RUN_SCRIPT - RefPtr<UnlockAllWakeLockRunnable> runnable = - MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this); - NS_DispatchToMainThread(runnable); + if (!ActiveWakeLocks(aType).IsEmpty()) { + RefPtr<UnlockAllWakeLockRunnable> runnable = + MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this); + nsresult rv = NS_DispatchToMainThread(runnable); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + } } RefPtr<Document::AutomaticStorageAccessPermissionGrantPromise> @@ -18637,12 +18668,24 @@ nsICookieJarSettings* Document::CookieJarSettings() { net::CookieJarSettings::Cast(mCookieJarSettings) ->SetFingerprintingRandomizationKey(randomKey); } + + // Inerit the top level windowContext id from the parent. + net::CookieJarSettings::Cast(mCookieJarSettings) + ->SetTopLevelWindowContextId( + net::CookieJarSettings::Cast(inProcessParent->CookieJarSettings()) + ->GetTopLevelWindowContextId()); } else { mCookieJarSettings = net::CookieJarSettings::Create(NodePrincipal()); + + if (IsTopLevelContentDocument()) { + net::CookieJarSettings::Cast(mCookieJarSettings) + ->SetTopLevelWindowContextId(InnerWindowID()); + } } if (auto* wgc = GetWindowGlobalChild()) { net::CookieJarSettingsArgs csArgs; + net::CookieJarSettings::Cast(mCookieJarSettings)->Serialize(csArgs); // Update cookie settings in the parent process if (!wgc->SendUpdateCookieJarSettings(csArgs)) { @@ -19042,6 +19085,13 @@ HighlightRegistry& Document::HighlightRegistry() { return *mHighlightRegistry; } +FragmentDirective* Document::FragmentDirective() { + if (!mFragmentDirective) { + mFragmentDirective = MakeRefPtr<class FragmentDirective>(this); + } + return mFragmentDirective; +} + RadioGroupContainer& Document::OwnedRadioGroupContainer() { if (!mRadioGroupContainer) { mRadioGroupContainer = MakeUnique<RadioGroupContainer>(); diff --git a/dom/base/Document.h b/dom/base/Document.h index a52c61addf..0b0d0ca3d0 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -244,6 +244,7 @@ class EventListener; struct FailedCertSecurityInfo; class FeaturePolicy; class FontFaceSet; +class FragmentDirective; class FrameRequestCallback; class ImageTracker; class HighlightRegistry; @@ -2992,15 +2993,6 @@ class Document : public nsINode, SetStateObject(aDocument->mStateObjectContainer); } - /** - * Returns true if there is a lightweight theme specified. This is used to - * determine the state of the :-moz-lwtheme pseudo-class. - */ - bool ComputeDocumentLWTheme() const; - void ResetDocumentLWTheme() { - UpdateDocumentStates(DocumentState::LWTHEME, true); - } - // Whether we're a media document or not. enum class MediaDocumentKind { NotMedia, @@ -3281,7 +3273,7 @@ class Document : public nsINode, void SetDomain(const nsAString& aDomain, mozilla::ErrorResult& rv); void GetCookie(nsAString& aCookie, mozilla::ErrorResult& rv); void SetCookie(const nsAString& aCookie, mozilla::ErrorResult& rv); - void GetReferrer(nsAString& aReferrer) const; + void GetReferrer(nsACString& aReferrer) const; void GetLastModified(nsAString& aLastModified) const; void GetReadyState(nsAString& aReadyState) const; @@ -3377,7 +3369,6 @@ class Document : public nsINode, bool aFocusPreviousElement, bool aFireEvents); - MOZ_CAN_RUN_SCRIPT_BOUNDARY void HideAllPopoversWithoutRunningScript(); // Hides the given popover element, see // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm MOZ_CAN_RUN_SCRIPT void HidePopover(Element& popover, @@ -4100,6 +4091,13 @@ class Document : public nsINode, */ class HighlightRegistry& HighlightRegistry(); + /** + * @brief Returns the `FragmentDirective` object which contains information + * and functionality to extract or create text directives. + * Guaranteed to be non-null. + */ + class FragmentDirective* FragmentDirective(); + bool ShouldResistFingerprinting(RFPTarget aTarget) const; bool IsInPrivateBrowsing() const; @@ -4159,7 +4157,8 @@ class Document : public nsINode, // Apply the fullscreen state to the document, and trigger related // events. It returns false if the fullscreen element ready check // fails and nothing gets changed. - bool ApplyFullscreen(UniquePtr<FullscreenRequest>); + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ApplyFullscreen( + UniquePtr<FullscreenRequest>); void RemoveDocStyleSheetsFromStyleSets(); void ResetStylesheetsToURI(nsIURI* aURI); @@ -5314,9 +5313,6 @@ class Document : public nsINode, // Pres shell resolution saved before entering fullscreen mode. float mSavedResolution; - // Pres shell resolution saved before creating a MobileViewportManager. - float mSavedResolutionBeforeMVM; - nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; bool mHasStoragePermission; @@ -5381,6 +5377,7 @@ class Document : public nsINode, nsTArray<CanvasUsage> mCanvasUsage; uint64_t mLastCanvasUsage = 0; + RefPtr<class FragmentDirective> mFragmentDirective; UniquePtr<RadioGroupContainer> mRadioGroupContainer; public: @@ -5392,11 +5389,6 @@ class Document : public nsINode, nsRefPtrHashtable<nsRefPtrHashKey<Element>, nsXULPrototypeElement> mL10nProtoElements; - float GetSavedResolutionBeforeMVM() { return mSavedResolutionBeforeMVM; } - void SetSavedResolutionBeforeMVM(float aResolution) { - mSavedResolutionBeforeMVM = aResolution; - } - void LoadEventFired(); RadioGroupContainer& OwnedRadioGroupContainer(); diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp index be31000278..b6f5d5c3be 100644 --- a/dom/base/Element.cpp +++ b/dom/base/Element.cpp @@ -1055,6 +1055,14 @@ already_AddRefed<nsIScreen> Element::GetScreen() { return nullptr; } +double Element::CurrentCSSZoom() { + nsIFrame* f = GetPrimaryFrame(FlushType::Frames); + if (!f) { + return 1.0; + } + return f->Style()->EffectiveZoom().ToFloat(); +} + already_AddRefed<DOMRect> Element::GetBoundingClientRect() { RefPtr<DOMRect> rect = new DOMRect(ToSupports(OwnerDoc())); @@ -1233,15 +1241,14 @@ bool Element::CanAttachShadowDOM() const { return true; } -// https://dom.spec.whatwg.org/commit-snapshots/1eadf0a4a271acc92013d1c0de8c730ac96204f9/#dom-element-attachshadow -already_AddRefed<ShadowRoot> Element::AttachShadow( - const ShadowRootInit& aInit, ErrorResult& aError, - ShadowRootDeclarative aNewShadowIsDeclarative) { +// https://dom.spec.whatwg.org/#dom-element-attachshadow +already_AddRefed<ShadowRoot> Element::AttachShadow(const ShadowRootInit& aInit, + ErrorResult& aError) { /** * Step 1, 2, and 3. */ if (!CanAttachShadowDOM()) { - aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + aError.ThrowNotSupportedError("Unable to attach ShadowDOM"); return nullptr; } @@ -1249,21 +1256,27 @@ already_AddRefed<ShadowRoot> Element::AttachShadow( * 4. If element is a shadow host, then: */ if (RefPtr<ShadowRoot> root = GetShadowRoot()) { - /* - * 1. If element’s shadow root’s declarative is false, then throw an - * "NotSupportedError" DOMException. + /** + * 1. Let currentShadowRoot be element’s shadow root. + * + * 2. If any of the following are true: + * currentShadowRoot’s declarative is false; or + * currentShadowRoot’s mode is not mode, + * then throw a "NotSupportedError" DOMException. */ - if (!root->IsDeclarative()) { - aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + if (!root->IsDeclarative() || root->Mode() != aInit.mMode) { + aError.ThrowNotSupportedError( + "Unable to re-attach to existing ShadowDOM"); return nullptr; } - // https://github.com/whatwg/dom/issues/1235 - root->SetIsDeclarative(aNewShadowIsDeclarative); - /* - * 2. Otherwise, remove all of element’s shadow root’s children, in tree - * order, and return. + /** + * 3. Otherwise: + * 1. Remove all of currentShadowRoot’s children, in tree order. + * 2. Set currentShadowRoot’s declarative to false. + * 3. Return. */ root->ReplaceChildren(nullptr, aError); + root->SetIsDeclarative(ShadowRootDeclarative::No); return root.forget(); } @@ -1273,14 +1286,12 @@ already_AddRefed<ShadowRoot> Element::AttachShadow( return AttachShadowWithoutNameChecks( aInit.mMode, DelegatesFocus(aInit.mDelegatesFocus), aInit.mSlotAssignment, - ShadowRootClonable(aInit.mClonable), - ShadowRootDeclarative(aNewShadowIsDeclarative)); + ShadowRootClonable(aInit.mClonable)); } already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( ShadowRootMode aMode, DelegatesFocus aDelegatesFocus, - SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable, - ShadowRootDeclarative aDeclarative) { + SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable) { nsAutoScriptBlocker scriptBlocker; auto* nim = mNodeInfo->NodeInfoManager(); @@ -1304,9 +1315,9 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( * context object's node document, host is context object, * and mode is init's mode. */ - RefPtr<ShadowRoot> shadowRoot = - new (nim) ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment, - aClonable, aDeclarative, nodeInfo.forget()); + RefPtr<ShadowRoot> shadowRoot = new (nim) + ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment, aClonable, + ShadowRootDeclarative::No, nodeInfo.forget()); if (NodeOrAncestorHasDirAuto()) { shadowRoot->SetAncestorHasDirAuto(); @@ -1336,6 +1347,22 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( dispatcher->PostDOMEvent(); } + const LinkedList<AbstractRange>* ranges = + GetExistingClosestCommonInclusiveAncestorRanges(); + if (ranges) { + for (const AbstractRange* range : *ranges) { + if (range->MayCrossShadowBoundary()) { + MOZ_ASSERT(range->IsDynamicRange()); + StaticRange* crossBoundaryRange = + range->AsDynamicRange()->GetCrossShadowBoundaryRange(); + MOZ_ASSERT(crossBoundaryRange); + // We may have previously selected this node before it + // becomes a shadow host, so we need to reset the values + // in RangeBoundaries to accommodate the change. + crossBoundaryRange->NotifyNodeBecomesShadowHost(this); + } + } + } /** * 10. Return shadow. */ @@ -1460,13 +1487,7 @@ void Element::GetAttribute(const nsAString& aName, DOMString& aReturn) { if (val) { val->ToString(aReturn); } else { - if (IsXULElement()) { - // XXX should be SetDOMStringToNull(aReturn); - // See bug 232598 - // aReturn is already empty - } else { - aReturn.SetNull(); - } + aReturn.SetNull(); } } @@ -1930,9 +1951,7 @@ nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) { // This has to be here, rather than in nsGenericHTMLElement::BindToTree, // because it has to happen after updating the parent pointer, but before // recursively binding the kids. - if (IsHTMLElement()) { - SetDirOnBind(this, nsIContent::FromNode(aParent)); - } + SetDirOnBind(this, nsIContent::FromNode(aParent)); UpdateEditableState(false); @@ -2145,9 +2164,7 @@ void Element::UnbindFromTree(UnbindContext& aContext) { // This has to be here, rather than in nsGenericHTMLElement::UnbindFromTree, // because it has to happen after unsetting the parent pointer, but before // recursively unbinding the kids. - if (IsHTMLElement()) { - ResetDir(this); - } + ResetDir(this); for (nsIContent* child = GetFirstChild(); child; child = child->GetNextSibling()) { diff --git a/dom/base/Element.h b/dom/base/Element.h index 40a8052aef..fb88274a88 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -257,7 +257,8 @@ class Element : public FragmentOrElement { #ifdef MOZILLA_INTERNAL_API explicit Element(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) : FragmentOrElement(std::move(aNodeInfo)), - mState(ElementState::READONLY | ElementState::DEFINED) { + mState(ElementState::READONLY | ElementState::DEFINED | + ElementState::LTR) { MOZ_ASSERT(mNodeInfo->NodeType() == ELEMENT_NODE, "Bad NodeType in aNodeInfo"); SetIsElement(); @@ -1352,10 +1353,8 @@ class Element : public FragmentOrElement { enum class ShadowRootDeclarative : bool { No, Yes }; MOZ_CAN_RUN_SCRIPT_BOUNDARY - already_AddRefed<ShadowRoot> AttachShadow( - const ShadowRootInit& aInit, ErrorResult& aError, - ShadowRootDeclarative aNewShadowIsDeclarative = - ShadowRootDeclarative::No); + already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit, + ErrorResult& aError); bool CanAttachShadowDOM() const; enum class DelegatesFocus : bool { No, Yes }; @@ -1364,8 +1363,7 @@ class Element : public FragmentOrElement { already_AddRefed<ShadowRoot> AttachShadowWithoutNameChecks( ShadowRootMode aMode, DelegatesFocus = DelegatesFocus::No, SlotAssignmentMode aSlotAssignmentMode = SlotAssignmentMode::Named, - ShadowRootClonable aClonable = ShadowRootClonable::No, - ShadowRootDeclarative aDeclarative = ShadowRootDeclarative::No); + ShadowRootClonable aClonable = ShadowRootClonable::No); // Attach UA Shadow Root if it is not attached. enum class NotifyUAWidgetSetup : bool { No, Yes }; @@ -1499,6 +1497,8 @@ class Element : public FragmentOrElement { return CSSPixel::FromAppUnits(GetClientAreaRect().Width()); } + MOZ_CAN_RUN_SCRIPT double CurrentCSSZoom(); + // This function will return the block size of first line box, no matter if // the box is 'block' or 'inline'. The return unit is pixel. If the element // can't get a primary frame, we will return be zero. diff --git a/dom/base/EventSource.cpp b/dom/base/EventSource.cpp index f70db487dd..def3c90ec0 100644 --- a/dom/base/EventSource.cpp +++ b/dom/base/EventSource.cpp @@ -568,7 +568,15 @@ nsresult EventSourceImpl::ParseURL(const nsAString& aURL) { NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIURI> srcURI; - rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI); + nsCOMPtr<Document> doc = + mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr; + if (doc) { + rv = NS_NewURI(getter_AddRefs(srcURI), aURL, doc->GetDocumentCharacterSet(), + baseURI); + } else { + rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI); + } + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); nsAutoString origin; diff --git a/dom/base/FragmentDirective.cpp b/dom/base/FragmentDirective.cpp new file mode 100644 index 0000000000..3300a85751 --- /dev/null +++ b/dom/base/FragmentDirective.cpp @@ -0,0 +1,879 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "FragmentDirective.h" +#include <cstdint> +#include "RangeBoundary.h" +#include "mozilla/Assertions.h" +#include "Document.h" +#include "mozilla/dom/FragmentDirectiveBinding.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/NodeBinding.h" +#include "mozilla/dom/Text.h" +#include "mozilla/intl/WordBreaker.h" +#include "nsComputedDOMStyle.h" +#include "nsContentUtils.h" +#include "nsDOMAttributeMap.h" +#include "nsGkAtoms.h" +#include "nsICSSDeclaration.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsIURIMutator.h" +#include "nsRange.h" +#include "nsString.h" + +namespace mozilla::dom { +static LazyLogModule sFragmentDirectiveLog("FragmentDirective"); + +/** Converts a `TextDirective` into a percent-encoded string. */ +nsCString ToString(const TextDirective& aTextDirective) { + nsCString str; + create_text_directive(&aTextDirective, &str); + return str; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FragmentDirective, mDocument) +NS_IMPL_CYCLE_COLLECTING_ADDREF(FragmentDirective) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FragmentDirective) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FragmentDirective) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +FragmentDirective::FragmentDirective(Document* aDocument) + : mDocument(aDocument) {} + +JSObject* FragmentDirective::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FragmentDirective_Binding::Wrap(aCx, this, aGivenProto); +} + +void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment( + nsCOMPtr<nsIURI>& aURI, nsTArray<TextDirective>* aTextDirectives) { + if (!aURI || !StaticPrefs::dom_text_fragments_enabled()) { + return; + } + bool hasRef = false; + aURI->GetHasRef(&hasRef); + if (!hasRef) { + return; + } + + nsAutoCString hash; + aURI->GetRef(hash); + + ParsedFragmentDirectiveResult fragmentDirective; + const bool hasRemovedFragmentDirective = + parse_fragment_directive(&hash, &fragmentDirective); + if (!hasRemovedFragmentDirective) { + return; + } + Unused << NS_MutateURI(aURI) + .SetRef(fragmentDirective.url_without_fragment_directive) + .Finalize(aURI); + if (aTextDirectives) { + aTextDirectives->SwapElements(fragmentDirective.text_directives); + } +} + +nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() { + MOZ_ASSERT(mDocument); + mDocument->FlushPendingNotifications(FlushType::Frames); + nsTArray<RefPtr<nsRange>> textDirectiveRanges; + for (const TextDirective& textDirective : mUninvokedTextDirectives) { + if (RefPtr<nsRange> range = FindRangeForTextDirective(textDirective)) { + textDirectiveRanges.AppendElement(range); + } + } + mUninvokedTextDirectives.Clear(); + return textDirectiveRanges; +} + +/** + * @brief Determine if `aNode` should be considered when traversing the DOM. + * + * A node is "search invisible" if it is an element in the HTML namespace and + * 1. The computed value of its `display` property is `none` + * 2. It serializes as void + * 3. It is one of the following types: + * - HTMLIFrameElement + * - HTMLImageElement + * - HTMLMeterElement + * - HTMLObjectElement + * - HTMLProgressElement + * - HTMLStyleElement + * - HTMLScriptElement + * - HTMLVideoElement + * - HTMLAudioElement + * 4. It is a `select` element whose `multiple` content attribute is absent + * + * see https://wicg.github.io/scroll-to-text-fragment/#search-invisible + */ +bool NodeIsSearchInvisible(nsINode& aNode) { + if (!aNode.IsElement()) { + return false; + } + // 2. If the node serializes as void. + nsAtom* nodeNameAtom = aNode.NodeInfo()->NameAtom(); + if (FragmentOrElement::IsHTMLVoid(nodeNameAtom)) { + return true; + } + // 3. Is any of the following types: HTMLIFrameElement, HTMLImageElement, + // HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement, + // HTMLScriptElement, HTMLVideoElement, HTMLAudioElement + if (aNode.IsAnyOfHTMLElements( + nsGkAtoms::iframe, nsGkAtoms::image, nsGkAtoms::meter, + nsGkAtoms::object, nsGkAtoms::progress, nsGkAtoms::style, + nsGkAtoms::script, nsGkAtoms::video, nsGkAtoms::audio)) { + return true; + } + // 4. Is a select element whose multiple content attribute is absent. + if (aNode.IsHTMLElement(nsGkAtoms::select)) { + return aNode.GetAttributes()->GetNamedItem(u"multiple"_ns) == nullptr; + } + // This is tested last because it's the most expensive check. + // 1. The computed value of its 'display' property is 'none'. + const Element* nodeAsElement = Element::FromNode(aNode); + const RefPtr<const ComputedStyle> computedStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); + return !computedStyle || + computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None; +} + +/** + * @brief Returns true if `aNode` has block-level display. + * A node has block-level display if it is an element and the computed value + * of its display property is any of + * - block + * - table + * - flow-root + * - grid + * - flex + * - list-item + * + * See https://wicg.github.io/scroll-to-text-fragment/#has-block-level-display + */ +bool NodeHasBlockLevelDisplay(nsINode& aNode) { + if (!aNode.IsElement()) { + return false; + } + const Element* nodeAsElement = Element::FromNode(aNode); + const RefPtr<const ComputedStyle> computedStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); + if (!computedStyle) { + return false; + } + const StyleDisplay& styleDisplay = computedStyle->StyleDisplay()->mDisplay; + return styleDisplay == StyleDisplay::Block || + styleDisplay == StyleDisplay::Table || + styleDisplay == StyleDisplay::FlowRoot || + styleDisplay == StyleDisplay::Grid || + styleDisplay == StyleDisplay::Flex || styleDisplay.IsListItem(); +} + +/** + * @brief Get the Block Ancestor For `aNode`. + * + * see https://wicg.github.io/scroll-to-text-fragment/#nearest-block-ancestor + */ +nsINode* GetBlockAncestorForNode(nsINode* aNode) { + // 1. Let curNode be node. + RefPtr<nsINode> curNode = aNode; + // 2. While curNode is non-null + while (curNode) { + // 2.1. If curNode is not a Text node and it has block-level display then + // return curNode. + if (!curNode->IsText() && NodeHasBlockLevelDisplay(*curNode)) { + return curNode; + } + // 2.2. Otherwise, set curNode to curNode’s parent. + curNode = curNode->GetParentNode(); + } + // 3.Return node’s node document's document element. + return aNode->GetOwnerDocument(); +} + +/** + * @brief Returns true if `aNode` is part of a non-searchable subtree. + * + * A node is part of a non-searchable subtree if it is or has a shadow-including + * ancestor that is search invisible. + * + * see https://wicg.github.io/scroll-to-text-fragment/#non-searchable-subtree + */ +bool NodeIsPartOfNonSearchableSubTree(nsINode& aNode) { + nsINode* node = &aNode; + do { + if (NodeIsSearchInvisible(*node)) { + return true; + } + } while ((node = node->GetParentOrShadowHostNode())); + return false; +} + +/** + * @brief Return true if `aNode` is a visible Text node. + * + * A node is a visible text node if it is a Text node, the computed value of + * its parent element's visibility property is visible, and it is being + * rendered. + * + * see https://wicg.github.io/scroll-to-text-fragment/#visible-text-node + */ +bool NodeIsVisibleTextNode(const nsINode& aNode) { + const Text* text = Text::FromNode(aNode); + if (!text) { + return false; + } + const nsIFrame* frame = text->GetPrimaryFrame(); + return frame && frame->StyleVisibility()->IsVisible(); +} + +enum class TextScanDirection { Left = -1, Right = 1 }; + +/** + * @brief Tests if there is whitespace at the given position and direction. + * + * This algorithm tests for whitespaces and ` ` at `aPos`. + * It returns the size of the whitespace found at the position, i.e. 5/6 for + * ` /;` and 1 otherwise. + * + * This function follows a subsection of this section of the spec, but has been + * adapted to be able to scan in both directions: + * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position + */ +uint32_t IsWhitespaceAtPosition(nsString& aText, uint32_t aPos, + TextScanDirection aDirection) { + if (aText.Length() == 0) { + return 0; + } + if (aDirection == TextScanDirection::Right) { + if (aText.Length() > (aPos + 5)) { + if (Substring(aText, aPos, 5).Equals(u" ")) { + return aText.Length() > (aPos + 6) && aText.CharAt(aPos + 6) == u';' + ? 6 + : 5; + } + } + } else { + if (aPos > 6 && Substring(aText, aPos - 6, 6).Equals(u" ")) { + return 6; + } + if (aPos > 5 && Substring(aText, aPos - 5, 5).Equals(u" ")) { + return 5; + } + } + return uint32_t(IsSpaceCharacter(aText.CharAt(aPos))); +} + +/** Advances the start of `aRange` to the next non-whitespace position. + * The function follows this section of the spec: + * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position + */ +void AdvanceStartToNextNonWhitespacePosition(nsRange& aRange) { + // 1. While range is not collapsed: + while (!aRange.Collapsed()) { + // 1.1. Let node be range's start node. + RefPtr<nsINode> node = aRange.GetStartContainer(); + MOZ_ASSERT(node); + // 1.2. Let offset be range's start offset. + const uint32_t offset = aRange.StartOffset(); + // 1.3. If node is part of a non-searchable subtree or if node is not a + // visible text node or if offset is equal to node's length then: + if (NodeIsPartOfNonSearchableSubTree(*node) || + !NodeIsVisibleTextNode(*node) || offset == node->Length()) { + // 1.3.1. Set range's start node to the next node, in shadow-including + // tree order. + // 1.3.2. Set range's start offset to 0. + if (NS_FAILED(aRange.SetStart(node->GetNextNode(), 0))) { + return; + } + // 1.3.3. Continue. + continue; + } + const Text* text = Text::FromNode(node); + nsAutoString textData; + text->GetData(textData); + // These steps are moved to `IsWhitespaceAtPosition()`. + // 1.4. If the substring data of node at offset offset and count 6 is equal + // to the string " " then: + // 1.4.1. Add 6 to range’s start offset. + // 1.5. Otherwise, if the substring data of node at offset offset and count + // 5 is equal to the string " " then: + // 1.5.1. Add 5 to range’s start offset. + // 1.6. Otherwise: + // 1.6.1 Let cp be the code point at the offset index in node’s data. + // 1.6.2 If cp does not have the White_Space property set, return. + // 1.6.3 Add 1 to range’s start offset. + const uint32_t whitespace = + IsWhitespaceAtPosition(textData, offset, TextScanDirection::Right); + if (whitespace == 0) { + return; + } + + aRange.SetStart(node, offset + whitespace); + } +} + +/** + * @brief Moves `aRangeBoundary` one word in `aDirection`. + * + * Word boundaries are determined using `intl::WordBreaker::FindWord()`. + * + * + * @param aRangeBoundary[in] The range boundary that should be moved. + * Must be set and valid. + * @param aDirection[in] The direction into which to move. + * @return A new `RangeBoundary` which is moved to the next word. + */ +RangeBoundary MoveRangeBoundaryOneWord(const RangeBoundary& aRangeBoundary, + TextScanDirection aDirection) { + MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); + RefPtr<nsINode> curNode = aRangeBoundary.Container(); + uint32_t offset = *aRangeBoundary.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); + + const int offsetIncrement = int(aDirection); + // Get the text node of the start of the range and the offset. + // This is the current position of the start of the range. + nsAutoString text; + if (NodeIsVisibleTextNode(*curNode)) { + const Text* textNode = Text::FromNode(curNode); + textNode->GetData(text); + + // Assuming that the current position might not be at a word boundary, + // advance to the word boundary at word begin/end. + if (!IsWhitespaceAtPosition(text, offset, aDirection)) { + const intl::WordRange wordRange = + intl::WordBreaker::FindWord(text, offset); + if (aDirection == TextScanDirection::Right && + offset != wordRange.mBegin) { + offset = wordRange.mEnd; + } else if (aDirection == TextScanDirection::Left && + offset != wordRange.mEnd) { + // The additional -1 is necessary to move to offset to *before* the + // start of the word. + offset = wordRange.mBegin - 1; + } + } + } + // Now, skip any whitespace, so that `offset` points to the word boundary of + // the next word (which is the one this algorithm actually aims to move over). + while (curNode) { + if (!NodeIsVisibleTextNode(*curNode) || NodeIsSearchInvisible(*curNode) || + offset >= curNode->Length()) { + curNode = aDirection == TextScanDirection::Left ? curNode->GetPrevNode() + : curNode->GetNextNode(); + if (!curNode) { + break; + } + offset = + aDirection == TextScanDirection::Left ? curNode->Length() - 1 : 0; + if (const Text* textNode = Text::FromNode(curNode)) { + textNode->GetData(text); + } + continue; + } + if (const uint32_t whitespace = + IsWhitespaceAtPosition(text, offset, aDirection)) { + offset += offsetIncrement * whitespace; + continue; + } + + // At this point, the caret has been moved to the next non-whitespace + // position. + // find word boundaries at the current position + const intl::WordRange wordRange = intl::WordBreaker::FindWord(text, offset); + offset = aDirection == TextScanDirection::Left ? wordRange.mBegin + : wordRange.mEnd; + + return {curNode, offset}; + } + return {}; +} + +RefPtr<nsRange> FragmentDirective::FindRangeForTextDirective( + const TextDirective& aTextDirective) { + MOZ_LOG(sFragmentDirectiveLog, LogLevel::Info, + ("FragmentDirective::%s(): Find range for text directive '%s'.", + __FUNCTION__, ToString(aTextDirective).Data())); + // 1. Let searchRange be a range with start (document, 0) and end (document, + // document’s length) + ErrorResult rv; + RefPtr<nsRange> searchRange = + nsRange::Create(mDocument, 0, mDocument, mDocument->Length(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2. While searchRange is not collapsed: + while (!searchRange->Collapsed()) { + // 2.1. Let potentialMatch be null. + RefPtr<nsRange> potentialMatch; + // 2.2. If parsedValues’s prefix is not null: + if (!aTextDirective.prefix.IsEmpty()) { + // 2.2.1. Let prefixMatch be the the result of running the find a string + // in range steps with query parsedValues’s prefix, searchRange + // searchRange, wordStartBounded true and wordEndBounded false. + RefPtr<nsRange> prefixMatch = + FindStringInRange(searchRange, aTextDirective.prefix, true, false); + // 2.2.2. If prefixMatch is null, return null. + if (!prefixMatch) { + return nullptr; + } + // 2.2.3. Set searchRange’s start to the first boundary point after + // prefixMatch’s start + const RangeBoundary boundaryPoint = MoveRangeBoundaryOneWord( + {prefixMatch->GetStartContainer(), prefixMatch->StartOffset()}, + TextScanDirection::Right); + if (!boundaryPoint.IsSetAndValid()) { + return nullptr; + } + searchRange->SetStart(boundaryPoint.AsRaw(), rv); + if (rv.Failed()) { + return nullptr; + } + + // 2.2.4. Let matchRange be a range whose start is prefixMatch’s end and + // end is searchRange’s end. + RefPtr<nsRange> matchRange = nsRange::Create( + prefixMatch->GetEndContainer(), prefixMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.2.5. Advance matchRange’s start to the next non-whitespace position. + AdvanceStartToNextNonWhitespacePosition(*matchRange); + // 2.2.6. If matchRange is collapsed return null. + // (This can happen if prefixMatch’s end or its subsequent non-whitespace + // position is at the end of the document.) + if (matchRange->Collapsed()) { + return nullptr; + } + // 2.2.7. Assert: matchRange’s start node is a Text node. + // (matchRange’s start now points to the next non-whitespace text data + // following a matched prefix.) + MOZ_ASSERT(matchRange->GetStartContainer()->IsText()); + + // 2.2.8. Let mustEndAtWordBoundary be true if parsedValues’s end is + // non-null or parsedValues’s suffix is null, false otherwise. + const bool mustEndAtWordBoundary = + !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty(); + // 2.2.9. Set potentialMatch to the result of running the find a string in + // range steps with query parsedValues’s start, searchRange matchRange, + // wordStartBounded false, and wordEndBounded mustEndAtWordBoundary. + potentialMatch = FindStringInRange(matchRange, aTextDirective.start, + false, mustEndAtWordBoundary); + // 2.2.10. If potentialMatch is null, return null. + if (!potentialMatch) { + return nullptr; + } + // 2.2.11. If potentialMatch’s start is not matchRange’s start, then + // continue. + // (In this case, we found a prefix but it was followed by something other + // than a matching text so we’ll continue searching for the next instance + // of prefix.) + if (potentialMatch->GetStartContainer() != + matchRange->GetStartContainer()) { + continue; + } + } + // 2.3. Otherwise: + else { + // 2.3.1. Let mustEndAtWordBoundary be true if parsedValues’s end is + // non-null or parsedValues’s suffix is null, false otherwise. + const bool mustEndAtWordBoundary = + !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty(); + // 2.3.2. Set potentialMatch to the result of running the find a string in + // range steps with query parsedValues’s start, searchRange searchRange, + // wordStartBounded true, and wordEndBounded mustEndAtWordBoundary. + potentialMatch = FindStringInRange(searchRange, aTextDirective.start, + true, mustEndAtWordBoundary); + // 2.3.3. If potentialMatch is null, return null. + if (!potentialMatch) { + return nullptr; + } + // 2.3.4. Set searchRange’s start to the first boundary point after + // potentialMatch’s start + RangeBoundary newRangeBoundary = MoveRangeBoundaryOneWord( + {potentialMatch->GetStartContainer(), potentialMatch->StartOffset()}, + TextScanDirection::Right); + if (!newRangeBoundary.IsSetAndValid()) { + return nullptr; + } + searchRange->SetStart(newRangeBoundary.AsRaw(), rv); + if (rv.Failed()) { + return nullptr; + } + } + // 2.4. Let rangeEndSearchRange be a range whose start is potentialMatch’s + // end and whose end is searchRange’s end. + RefPtr<nsRange> rangeEndSearchRange = nsRange::Create( + potentialMatch->GetEndContainer(), potentialMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.5. While rangeEndSearchRange is not collapsed: + while (!rangeEndSearchRange->Collapsed()) { + // 2.5.1. If parsedValues’s end item is non-null, then: + if (!aTextDirective.end.IsEmpty()) { + // 2.5.1.1. Let mustEndAtWordBoundary be true if parsedValues’s suffix + // is null, false otherwise. + const bool mustEndAtWordBoundary = aTextDirective.suffix.IsEmpty(); + // 2.5.1.2. Let endMatch be the result of running the find a string in + // range steps with query parsedValues’s end, searchRange + // rangeEndSearchRange, wordStartBounded true, and wordEndBounded + // mustEndAtWordBoundary. + RefPtr<nsRange> endMatch = + FindStringInRange(rangeEndSearchRange, aTextDirective.end, true, + mustEndAtWordBoundary); + // 2.5.1.3. If endMatch is null then return null. + if (!endMatch) { + return nullptr; + } + // 2.5.1.4. Set potentialMatch’s end to endMatch’s end. + potentialMatch->SetEnd(endMatch->GetEndContainer(), + endMatch->EndOffset()); + } + // 2.5.2. Assert: potentialMatch is non-null, not collapsed and represents + // a range exactly containing an instance of matching text. + MOZ_ASSERT(potentialMatch && !potentialMatch->Collapsed()); + + // 2.5.3. If parsedValues’s suffix is null, return potentialMatch. + if (aTextDirective.suffix.IsEmpty()) { + return potentialMatch; + } + // 2.5.4. Let suffixRange be a range with start equal to potentialMatch’s + // end and end equal to searchRange’s end. + RefPtr<nsRange> suffixRange = nsRange::Create( + potentialMatch->GetEndContainer(), potentialMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.5.5. Advance suffixRange's start to the next non-whitespace position. + AdvanceStartToNextNonWhitespacePosition(*suffixRange); + + // 2.5.6. Let suffixMatch be result of running the find a string in range + // steps with query parsedValue's suffix, searchRange suffixRange, + // wordStartBounded false, and wordEndBounded true. + RefPtr<nsRange> suffixMatch = + FindStringInRange(suffixRange, aTextDirective.suffix, false, true); + + // 2.5.7. If suffixMatch is null, return null. + // (If the suffix doesn't appear in the remaining text of the document, + // there's no possible way to make a match.) + if (!suffixMatch) { + return nullptr; + } + // 2.5.8. If suffixMatch's start is suffixRange's start, return + // potentialMatch. + if (suffixMatch->GetStartContainer() == + suffixRange->GetStartContainer() && + suffixMatch->StartOffset() == suffixRange->StartOffset()) { + return potentialMatch; + } + // 2.5.9. If parsedValue's end item is null then break; + // (If this is an exact match and the suffix doesn’t match, start + // searching for the next range start by breaking out of this loop without + // rangeEndSearchRange being collapsed. If we’re looking for a range + // match, we’ll continue iterating this inner loop since the range start + // will already be correct.) + if (aTextDirective.end.IsEmpty()) { + break; + } + // 2.5.10. Set rangeEndSearchRange's start to potentialMatch's end. + // (Otherwise, it is possible that we found the correct range start, but + // not the correct range end. Continue the inner loop to keep searching + // for another matching instance of rangeEnd.) + rangeEndSearchRange->SetStart(potentialMatch->GetEndContainer(), + potentialMatch->EndOffset()); + } + // 2.6. If rangeEndSearchRange is collapsed then: + if (rangeEndSearchRange->Collapsed()) { + // 2.6.1. Assert parsedValue's end item is non-null. + // (This can only happen for range matches due to the break for exact + // matches in step 9 of the above loop. If we couldn’t find a valid + // rangeEnd+suffix pair anywhere in the doc then there’s no possible way + // to make a match.) + // XXX(:jjaschke): should this really assert? + MOZ_ASSERT(!aTextDirective.end.IsEmpty()); + } + } + // 3. Return null. + return nullptr; +} + +/** + * @brief Convenience function that returns true if the given position in a + * string is a word boundary. + * + * This is a thin wrapper around the `WordBreaker::FindWord()` function. + * + * @param aText The text input. + * @param aPosition The position to check. + * @return true if there is a word boundary at `aPosition`. + * @return false otherwise. + */ +bool IsAtWordBoundary(const nsAString& aText, uint32_t aPosition) { + const intl::WordRange wordRange = + intl::WordBreaker::FindWord(aText, aPosition); + return wordRange.mBegin == aPosition || wordRange.mEnd == aPosition; +} + +enum class IsEndIndex : bool { No, Yes }; +RangeBoundary GetBoundaryPointAtIndex( + uint32_t aIndex, const nsTArray<RefPtr<Text>>& aTextNodeList, + IsEndIndex aIsEndIndex) { + // 1. Let counted be 0. + uint32_t counted = 0; + // 2. For each curNode of nodes: + for (Text* curNode : aTextNodeList) { + // 2.1. Let nodeEnd be counted + curNode’s length. + uint32_t nodeEnd = counted + curNode->Length(); + // 2.2. If isEnd is true, add 1 to nodeEnd. + if (aIsEndIndex == IsEndIndex::Yes) { + ++nodeEnd; + } + // 2.3. If nodeEnd is greater than index then: + if (nodeEnd > aIndex) { + // 2.3.1. Return the boundary point (curNode, index − counted). + return RangeBoundary(curNode->AsNode(), aIndex - counted); + } + // 2.4. Increment counted by curNode’s length. + counted += curNode->Length(); + } + return {}; +} + +RefPtr<nsRange> FindRangeFromNodeList( + nsRange* aSearchRange, const nsAString& aQuery, + const nsTArray<RefPtr<Text>>& aTextNodeList, bool aWordStartBounded, + bool aWordEndBounded) { + // 1. Let searchBuffer be the concatenation of the data of each item in nodes. + // XXX(:jjaschke): There's an open issue here that deals with what + // data is supposed to be (text data vs. rendered text) + // https://github.com/WICG/scroll-to-text-fragment/issues/98 + uint32_t bufferLength = 0; + for (const Text* text : aTextNodeList) { + bufferLength += text->Length(); + } + // bail out if the search query is longer than the text data. + if (bufferLength < aQuery.Length()) { + return nullptr; + } + nsAutoString searchBuffer; + searchBuffer.SetCapacity(bufferLength); + for (Text* text : aTextNodeList) { + text->AppendTextTo(searchBuffer); + } + // 2. Let searchStart be 0. + // 3. If the first item in nodes is searchRange’s start node then set + // searchStart to searchRange’s start offset. + uint32_t searchStart = + aTextNodeList.SafeElementAt(0) == aSearchRange->GetStartContainer() + ? aSearchRange->StartOffset() + : 0; + + // 4. Let start and end be boundary points, initially null. + RangeBoundary start, end; + // 5. Let matchIndex be null. + // "null" here doesn't mean 0, instead "not set". 0 would be a valid index. + // Therefore, "null" is represented by the value -1. + int32_t matchIndex = -1; + + // 6. While matchIndex is null + // As explained above, "null" == -1 in this algorithm. + while (matchIndex == -1) { + // 6.1. Set matchIndex to the index of the first instance of queryString in + // searchBuffer, starting at searchStart. The string search must be + // performed using a base character comparison, or the primary level, as + // defined in [UTS10]. + // [UTS10] + // Ken Whistler; Markus Scherer.Unicode Collation Algorithm.26 August 2022. + // Unicode Technical Standard #10. + // URL : https://www.unicode.org/reports/tr10/tr10-47.html + + // XXX(:jjaschke): For the initial implementation, a standard case-sensitive + // find-in-string is used. + // See: https://github.com/WICG/scroll-to-text-fragment/issues/233 + matchIndex = searchBuffer.Find(aQuery, searchStart); + // 6.2. If matchIndex is null, return null. + if (matchIndex == -1) { + return nullptr; + } + + // 6.3. Let endIx be matchIndex + queryString’s length. + // endIx is the index of the last character in the match + 1. + const uint32_t endIx = matchIndex + aQuery.Length(); + + // 6.4. Set start to the boundary point result of get boundary point at + // index matchIndex run over nodes with isEnd false. + start = GetBoundaryPointAtIndex(matchIndex, aTextNodeList, IsEndIndex::No); + // 6.5. Set end to the boundary point result of get boundary point at index + // endIx run over nodes with isEnd true. + end = GetBoundaryPointAtIndex(endIx, aTextNodeList, IsEndIndex::Yes); + + // 6.6. If wordStartBounded is true and matchIndex is not at a word boundary + // in searchBuffer, given the language from start’s node as the locale; or + // wordEndBounded is true and matchIndex + queryString’s length is not at a + // word boundary in searchBuffer, given the language from end’s node as the + // locale: + if ((aWordStartBounded && !IsAtWordBoundary(searchBuffer, matchIndex)) || + (aWordEndBounded && !IsAtWordBoundary(searchBuffer, endIx))) { + // 6.6.1. Set searchStart to matchIndex + 1. + searchStart = matchIndex + 1; + // 6.6.2. Set matchIndex to null. + matchIndex = -1; + } + } + // 7. Let endInset be 0. + // 8. If the last item in nodes is searchRange’s end node then set endInset + // to (searchRange’s end node's length − searchRange’s end offset) + // (endInset is the offset from the last position in the last node in the + // reverse direction. Alternatively, it is the length of the node that’s not + // included in the range.) + uint32_t endInset = + aTextNodeList.LastElement() == aSearchRange->GetEndContainer() + ? aSearchRange->GetEndContainer()->Length() - + aSearchRange->EndOffset() + : 0; + + // 9. If matchIndex + queryString’s length is greater than searchBuffer’s + // length − endInset return null. + // (If the match runs past the end of the search range, return null.) + if (matchIndex + aQuery.Length() > searchBuffer.Length() - endInset) { + return nullptr; + } + + // 10. Assert: start and end are non-null, valid boundary points in + // searchRange. + MOZ_ASSERT(start.IsSetAndValid()); + MOZ_ASSERT(end.IsSetAndValid()); + + // 11. Return a range with start start and end end. + ErrorResult rv; + RefPtr<nsRange> range = nsRange::Create(start, end, rv); + if (rv.Failed()) { + return nullptr; + } + + return range; +} + +RefPtr<nsRange> FragmentDirective::FindStringInRange(nsRange* aSearchRange, + const nsAString& aQuery, + bool aWordStartBounded, + bool aWordEndBounded) { + MOZ_ASSERT(aSearchRange); + RefPtr<nsRange> searchRange = aSearchRange->CloneRange(); + // 1. While searchRange is not collapsed + while (searchRange && !searchRange->Collapsed()) { + // 1.1. Let curNode be searchRange’s start node. + RefPtr<nsINode> curNode = searchRange->GetStartContainer(); + + // 1.2. If curNode is part of a non-searchable subtree: + if (NodeIsPartOfNonSearchableSubTree(*curNode)) { + // 1.2.1. Set searchRange’s start node to the next node, in + // shadow-including tree order, that isn’t a shadow-including descendant + // of curNode. + RefPtr<nsINode> next = curNode; + while ((next = next->GetNextNode())) { + if (!next->IsShadowIncludingInclusiveDescendantOf(curNode)) { + break; + } + } + if (!next) { + return nullptr; + } + // 1.2.2. Set `searchRange`s `start offset` to 0 + searchRange->SetStart(next, 0); + // 1.2.3. continue. + continue; + } + // 1.3. If curNode is not a visible TextNode: + if (!NodeIsVisibleTextNode(*curNode)) { + // 1.3.1. Set searchRange’s start node to the next node, in + // shadow-including tree order, that is not a doctype. + RefPtr<nsINode> next = curNode; + while ((next = next->GetNextNode())) { + if (next->NodeType() != Node_Binding::DOCUMENT_TYPE_NODE) { + break; + } + } + if (!next) { + return nullptr; + } + // 1.3.2. Set searchRange’s start offset to 0. + searchRange->SetStart(next, 0); + // 1.3.3. continue. + continue; + } + // 1.4. Let blockAncestor be the nearest block ancestor of `curNode` + RefPtr<nsINode> blockAncestor = GetBlockAncestorForNode(curNode); + + // 1.5. Let textNodeList be a list of Text nodes, initially empty. + nsTArray<RefPtr<Text>> textNodeList; + // 1.6. While curNode is a shadow-including descendant of blockAncestor and + // the position of the boundary point (curNode,0) is not after searchRange's + // end: + while (curNode && + curNode->IsShadowIncludingInclusiveDescendantOf(blockAncestor)) { + Maybe<int32_t> comp = nsContentUtils::ComparePoints( + curNode, 0, searchRange->GetEndContainer(), searchRange->EndOffset()); + if (comp) { + if (*comp >= 0) { + break; + } + } else { + // This means that the compared nodes are disconnected. + return nullptr; + } + // 1.6.1. If curNode has block-level display, then break. + if (NodeHasBlockLevelDisplay(*curNode)) { + break; + } + // 1.6.2. If curNode is search invisible: + if (NodeIsSearchInvisible(*curNode)) { + // 1.6.2.1. Set curNode to the next node, in shadow-including tree + // order, that isn't a shadow-including descendant of curNode. + curNode = curNode->GetNextNode(); + // 1.6.2.2. Continue. + continue; + } + // 1.6.3. If curNode is a visible text node then append it to + // textNodeList. + if (NodeIsVisibleTextNode(*curNode)) { + textNodeList.AppendElement(curNode->AsText()); + } + // 1.6.4. Set curNode to the next node in shadow-including + // tree order. + curNode = curNode->GetNextNode(); + } + // 1.7. Run the find a range from a node list steps given + // query, searchRange, textNodeList, wordStartBounded, wordEndBounded as + // input. If the resulting Range is not null, then return it. + if (RefPtr<nsRange> range = + FindRangeFromNodeList(searchRange, aQuery, textNodeList, + aWordStartBounded, aWordEndBounded)) { + return range; + } + + // 1.8. If curNode is null, then break. + if (!curNode) { + break; + } + + // 1.9. Assert: curNode follows searchRange's start node. + + // 1.10. Set searchRange's start to the boundary point (curNode,0). + searchRange->SetStart(curNode, 0); + } + + // 2. Return null. + return nullptr; +} +} // namespace mozilla::dom diff --git a/dom/base/FragmentDirective.h b/dom/base/FragmentDirective.h new file mode 100644 index 0000000000..8972556d6c --- /dev/null +++ b/dom/base/FragmentDirective.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_FRAGMENTDIRECTIVE_H_ +#define DOM_FRAGMENTDIRECTIVE_H_ + +#include "js/TypeDecls.h" +#include "mozilla/dom/BindingDeclarations.h" + +#include "mozilla/dom/fragmentdirectives_ffi_generated.h" +#include "nsCycleCollectionParticipant.h" +#include "nsStringFwd.h" +#include "nsWrapperCache.h" + +class nsINode; +class nsIURI; +class nsRange; +namespace mozilla::dom { +class Document; +class Text; + +/** + * @brief The `FragmentDirective` class is the C++ representation of the + * `Document.fragmentDirective` webidl property. + * + * This class also serves as the main interface to interact with the fragment + * directive from the C++ side. It allows to find text fragment ranges from a + * given list of `TextDirective`s using + * `FragmentDirective::FindTextFragmentsInDocument()`. + * To avoid Text Directives being applied multiple times, this class implements + * the `uninvoked directive` mechanism, which in the spec is defined to be part + * of the `Document` [0]. + * + * [0] + * https://wicg.github.io/scroll-to-text-fragment/#document-uninvoked-directives + */ +class FragmentDirective final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FragmentDirective) + + public: + explicit FragmentDirective(Document* aDocument); + FragmentDirective(Document* aDocument, + nsTArray<TextDirective>&& aTextDirectives) + : mDocument(aDocument), + mUninvokedTextDirectives(std::move(aTextDirectives)) {} + + protected: + ~FragmentDirective() = default; + + public: + Document* GetParentObject() const { return mDocument; }; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + /** + * @brief Sets Text Directives as "uninvoked directive". + */ + void SetTextDirectives(nsTArray<TextDirective>&& aTextDirectives) { + mUninvokedTextDirectives = std::move(aTextDirectives); + } + + /** Returns true if there are Text Directives that have not been applied to + * the `Document`. + */ + bool HasUninvokedDirectives() const { + return !mUninvokedTextDirectives.IsEmpty(); + }; + + /** Searches for the current uninvoked text directives and creates a range for + * each one that is found. + * + * When this method returns, the uninvoked directives for this document are + * cleared. + * + * This method tries to follow the specification as close as possible in how + * to find a matching range for a text directive. However, instead of using + * collator-based search, a standard case-insensitive search is used + * (`nsString::find()`). + */ + nsTArray<RefPtr<nsRange>> FindTextFragmentsInDocument(); + + /** Utility function which parses the fragment directive and removes it from + * the hash of the given URI. This operation happens in-place. + * + * If aTextDirectives is nullptr, the parsed fragment directive is discarded. + */ + static void ParseAndRemoveFragmentDirectiveFromFragment( + nsCOMPtr<nsIURI>& aURI, + nsTArray<TextDirective>* aTextDirectives = nullptr); + + private: + RefPtr<nsRange> FindRangeForTextDirective( + const TextDirective& aTextDirective); + RefPtr<nsRange> FindStringInRange(nsRange* aSearchRange, + const nsAString& aQuery, + bool aWordStartBounded, + bool aWordEndBounded); + + RefPtr<Document> mDocument; + nsTArray<TextDirective> mUninvokedTextDirectives; +}; + +} // namespace mozilla::dom + +#endif // DOM_FRAGMENTDIRECTIVE_H_ diff --git a/dom/base/Link.cpp b/dom/base/Link.cpp index 296a584ed1..1465ea5773 100644 --- a/dom/base/Link.cpp +++ b/dom/base/Link.cpp @@ -6,26 +6,21 @@ #include "Link.h" -#include "mozilla/MemoryReporting.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/SVGAElement.h" #include "mozilla/dom/HTMLDNSPrefetch.h" #include "mozilla/IHistory.h" -#include "mozilla/StaticPrefs_layout.h" #include "nsLayoutUtils.h" -#include "nsIURL.h" #include "nsIURIMutator.h" #include "nsISizeOf.h" -#include "nsEscape.h" #include "nsGkAtoms.h" #include "nsString.h" -#include "mozAutoDocUpdate.h" #include "mozilla/Components.h" #include "nsAttrValueInlines.h" -#include "HTMLLinkElement.h" namespace mozilla::dom { @@ -133,7 +128,7 @@ nsIURI* Link::GetURI() const { return mCachedURI; } -void Link::SetProtocol(const nsAString& aProtocol) { +void Link::SetProtocol(const nsACString& aProtocol) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. @@ -146,111 +141,100 @@ void Link::SetProtocol(const nsAString& aProtocol) { SetHrefAttribute(uri); } -void Link::SetPassword(const nsAString& aPassword) { +void Link::SetPassword(const nsACString& aPassword) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetPassword(NS_ConvertUTF16toUTF8(aPassword)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetPassword(aPassword).Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } -void Link::SetUsername(const nsAString& aUsername) { +void Link::SetUsername(const nsACString& aUsername) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetUsername(NS_ConvertUTF16toUTF8(aUsername)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetUsername(aUsername).Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } -void Link::SetHost(const nsAString& aHost) { +void Link::SetHost(const nsACString& aHost) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetHostPort(NS_ConvertUTF16toUTF8(aHost)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetHostPort(aHost).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetHostname(const nsAString& aHostname) { +void Link::SetHostname(const nsACString& aHostname) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetHost(NS_ConvertUTF16toUTF8(aHostname)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetHost(aHostname).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetPathname(const nsAString& aPathname) { +void Link::SetPathname(const nsACString& aPathname) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetFilePath(NS_ConvertUTF16toUTF8(aPathname)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetFilePath(aPathname).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetSearch(const nsAString& aSearch) { +void Link::SetSearch(const nsACString& aSearch) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetQuery(NS_ConvertUTF16toUTF8(aSearch)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetQuery(aSearch).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetPort(const nsAString& aPort) { +void Link::SetPort(const nsACString& aPort) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv; - nsAutoString portStr(aPort); - // nsIURI uses -1 as default value. + nsresult rv; int32_t port = -1; if (!aPort.IsEmpty()) { - port = portStr.ToInteger(&rv); + port = aPort.ToInteger(&rv); if (NS_FAILED(rv)) { return; } @@ -263,15 +247,14 @@ void Link::SetPort(const nsAString& aPort) { SetHrefAttribute(uri); } -void Link::SetHash(const nsAString& aHash) { +void Link::SetHash(const nsACString& aHash) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetRef(aHash).Finalize(uri); if (NS_FAILED(rv)) { return; } @@ -279,121 +262,102 @@ void Link::SetHash(const nsAString& aHash) { SetHrefAttribute(uri); } -void Link::GetOrigin(nsAString& aOrigin) { +void Link::GetOrigin(nsACString& aOrigin) { aOrigin.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsString origin; - nsContentUtils::GetWebExposedOriginSerialization(uri, origin); - aOrigin.Assign(origin); + nsContentUtils::GetWebExposedOriginSerialization(uri, aOrigin); } -void Link::GetProtocol(nsAString& _protocol) { - nsCOMPtr<nsIURI> uri(GetURI()); - if (uri) { - nsAutoCString scheme; - (void)uri->GetScheme(scheme); - CopyASCIItoUTF16(scheme, _protocol); +void Link::GetProtocol(nsACString& aProtocol) { + if (nsIURI* uri = GetURI()) { + (void)uri->GetScheme(aProtocol); } - _protocol.Append(char16_t(':')); + aProtocol.Append(':'); } -void Link::GetUsername(nsAString& aUsername) { +void Link::GetUsername(nsACString& aUsername) { aUsername.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsAutoCString username; - uri->GetUsername(username); - CopyASCIItoUTF16(username, aUsername); + uri->GetUsername(aUsername); } -void Link::GetPassword(nsAString& aPassword) { +void Link::GetPassword(nsACString& aPassword) { aPassword.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsAutoCString password; - uri->GetPassword(password); - CopyASCIItoUTF16(password, aPassword); + uri->GetPassword(aPassword); } -void Link::GetHost(nsAString& _host) { - _host.Truncate(); +void Link::GetHost(nsACString& aHost) { + aHost.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsAutoCString hostport; - nsresult rv = uri->GetHostPort(hostport); - if (NS_SUCCEEDED(rv)) { - CopyUTF8toUTF16(hostport, _host); - } + uri->GetHostPort(aHost); } -void Link::GetHostname(nsAString& _hostname) { - _hostname.Truncate(); +void Link::GetHostname(nsACString& aHostname) { + aHostname.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsContentUtils::GetHostOrIPv6WithBrackets(uri, _hostname); + nsContentUtils::GetHostOrIPv6WithBrackets(uri, aHostname); } -void Link::GetPathname(nsAString& _pathname) { - _pathname.Truncate(); +void Link::GetPathname(nsACString& aPathname) { + aPathname.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsAutoCString file; - nsresult rv = uri->GetFilePath(file); - if (NS_SUCCEEDED(rv)) { - CopyUTF8toUTF16(file, _pathname); - } + uri->GetFilePath(aPathname); } -void Link::GetSearch(nsAString& _search) { - _search.Truncate(); +void Link::GetSearch(nsACString& aSearch) { + aSearch.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI or URL should result in an empty // string. return; } - nsAutoCString search; - nsresult rv = uri->GetQuery(search); - if (NS_SUCCEEDED(rv) && !search.IsEmpty()) { - _search.Assign(u'?'); - AppendUTF8toUTF16(search, _search); + nsresult rv = uri->GetQuery(aSearch); + if (NS_SUCCEEDED(rv) && !aSearch.IsEmpty()) { + aSearch.Insert('?', 0); } } -void Link::GetPort(nsAString& _port) { - _port.Truncate(); +void Link::GetPort(nsACString& aPort) { + aPort.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; @@ -404,27 +368,23 @@ void Link::GetPort(nsAString& _port) { // Note that failure to get the port from the URI is not necessarily a bad // thing. Some URIs do not have a port. if (NS_SUCCEEDED(rv) && port != -1) { - nsAutoString portStr; - portStr.AppendInt(port, 10); - _port.Assign(portStr); + aPort.AppendInt(port, 10); } } -void Link::GetHash(nsAString& _hash) { - _hash.Truncate(); +void Link::GetHash(nsACString& aHash) { + aHash.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty // string. return; } - nsAutoCString ref; - nsresult rv = uri->GetRef(ref); - if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) { - _hash.Assign(char16_t('#')); - AppendUTF8toUTF16(ref, _hash); + nsresult rv = uri->GetRef(aHash); + if (NS_SUCCEEDED(rv) && !aHash.IsEmpty()) { + aHash.Insert('#', 0); } } diff --git a/dom/base/Link.h b/dom/base/Link.h index b604457126..cee1e1c5ca 100644 --- a/dom/base/Link.h +++ b/dom/base/Link.h @@ -64,25 +64,25 @@ class Link : public nsISupports { /** * Helper methods for modifying and obtaining parts of the URI of the Link. */ - void SetProtocol(const nsAString& aProtocol); - void SetUsername(const nsAString& aUsername); - void SetPassword(const nsAString& aPassword); - void SetHost(const nsAString& aHost); - void SetHostname(const nsAString& aHostname); - void SetPathname(const nsAString& aPathname); - void SetSearch(const nsAString& aSearch); - void SetPort(const nsAString& aPort); - void SetHash(const nsAString& aHash); - void GetOrigin(nsAString& aOrigin); - void GetProtocol(nsAString& _protocol); - void GetUsername(nsAString& aUsername); - void GetPassword(nsAString& aPassword); - void GetHost(nsAString& _host); - void GetHostname(nsAString& _hostname); - void GetPathname(nsAString& _pathname); - void GetSearch(nsAString& _search); - void GetPort(nsAString& _port); - void GetHash(nsAString& _hash); + void SetProtocol(const nsACString& aProtocol); + void SetUsername(const nsACString& aUsername); + void SetPassword(const nsACString& aPassword); + void SetHost(const nsACString& aHost); + void SetHostname(const nsACString& aHostname); + void SetPathname(const nsACString& aPathname); + void SetSearch(const nsACString& aSearch); + void SetPort(const nsACString& aPort); + void SetHash(const nsACString& aHash); + void GetOrigin(nsACString& aOrigin); + void GetProtocol(nsACString& aProtocol); + void GetUsername(nsACString& aUsername); + void GetPassword(nsACString& aPassword); + void GetHost(nsACString& aHost); + void GetHostname(nsACString& aHostname); + void GetPathname(nsACString& aPathname); + void GetSearch(nsACString& aSearch); + void GetPort(nsACString& aPort); + void GetHash(nsACString& aHash); /** * Invalidates any link caching, and resets the state to the default. diff --git a/dom/base/Location.cpp b/dom/base/Location.cpp index cdf25abf56..9edb9e0b6f 100644 --- a/dom/base/Location.cpp +++ b/dom/base/Location.cpp @@ -34,6 +34,7 @@ #include "mozilla/Unused.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/FragmentDirective.h" #include "mozilla/dom/LocationBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "ReferrerInfo.h" @@ -105,6 +106,9 @@ nsresult Location::GetURI(nsIURI** aURI, bool aGetInnermostURI) { } NS_ASSERTION(uri, "nsJARURI screwed up?"); + + // Remove the fragment directive from the url hash. + FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(uri); nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri); exposableURI.forget(aURI); return NS_OK; @@ -549,26 +553,6 @@ void Location::Reload(bool aForceget, nsIPrincipal& aSubjectPrincipal, return aRv.Throw(NS_ERROR_FAILURE); } - if (StaticPrefs::dom_block_reload_from_resize_event_handler()) { - nsCOMPtr<nsPIDOMWindowOuter> window = docShell->GetWindow(); - if (window && window->IsHandlingResizeEvent()) { - // location.reload() was called on a window that is handling a - // resize event. Sites do this since Netscape 4.x needed it, but - // we don't, and it's a horrible experience for nothing. In stead - // of reloading the page, just clear style data and reflow the - // page since some sites may use this trick to work around gecko - // reflow bugs, and this should have the same effect. - RefPtr<Document> doc = window->GetExtantDoc(); - - nsPresContext* pcx; - if (doc && (pcx = doc->GetPresContext())) { - pcx->RebuildAllStyleData(NS_STYLE_HINT_REFLOW, - RestyleHint::RestyleSubtree()); - } - return; - } - } - RefPtr<BrowsingContext> bc = GetBrowsingContext(); if (!bc || bc->IsDiscarded()) { return; diff --git a/dom/base/PlacesEventCounts.cpp b/dom/base/PlacesEventCounts.cpp new file mode 100644 index 0000000000..be02e4c39b --- /dev/null +++ b/dom/base/PlacesEventCounts.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "PlacesEventCounts.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/PlacesEventCounts.h" +#include "mozilla/dom/PlacesEventBinding.h" +#include "mozilla/dom/PlacesObserversBinding.h" + +namespace mozilla::dom { + +// Only needed for refcounted objects. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(PlacesEventCounts) +NS_IMPL_CYCLE_COLLECTING_ADDREF(PlacesEventCounts) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PlacesEventCounts) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlacesEventCounts) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +PlacesEventCounts::PlacesEventCounts() { + ErrorResult rv; + for (auto eventType : MakeWebIDLEnumeratedRange<PlacesEventType>()) { + PlacesEventCounts_Binding::MaplikeHelpers::Set( + this, NS_ConvertUTF8toUTF16(GetEnumString(eventType)), 0, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + } +} + +nsresult PlacesEventCounts::Increment(PlacesEventType aEventType) { + ErrorResult rv; + nsAutoCString eventName(GetEnumString(aEventType)); + uint64_t count = PlacesEventCounts_Binding::MaplikeHelpers::Get( + this, NS_ConvertUTF8toUTF16(eventName), rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return rv.StealNSResult(); + } + PlacesEventCounts_Binding::MaplikeHelpers::Set( + this, NS_ConvertUTF8toUTF16(eventName), ++count, rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return rv.StealNSResult(); + } + return NS_OK; +} + +JSObject* PlacesEventCounts::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PlacesEventCounts_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/base/PlacesEventCounts.h b/dom/base/PlacesEventCounts.h new file mode 100644 index 0000000000..69abeac7b3 --- /dev/null +++ b/dom/base/PlacesEventCounts.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_PLACESEVENTCOUNTS_H_ +#define DOM_PLACESEVENTCOUNTS_H_ + +#include "js/TypeDecls.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/PlacesEventBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class PlacesEventCounts final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PlacesEventCounts) + + public: + PlacesEventCounts(); + + nsresult Increment(PlacesEventType aEventType); + + nsISupports* GetParentObject() const { return nullptr; }; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~PlacesEventCounts() = default; +}; + +} // namespace mozilla::dom + +#endif // DOM_PLACESEVENTCOUNTS_H_ diff --git a/dom/base/PlacesObservers.cpp b/dom/base/PlacesObservers.cpp index bb92e4a072..b5211733ea 100644 --- a/dom/base/PlacesObservers.cpp +++ b/dom/base/PlacesObservers.cpp @@ -22,7 +22,7 @@ struct Flagged { Flagged(const Flagged& aOther) = default; ~Flagged() = default; - uint32_t flags; + uint32_t flags = 0; T value; }; @@ -123,6 +123,14 @@ MOZ_CAN_RUN_SCRIPT void CallListeners( } } +StaticRefPtr<PlacesEventCounts> PlacesObservers::sCounts; +static void EnsureCountsInitialized() { + if (!PlacesObservers::sCounts) { + PlacesObservers::sCounts = new PlacesEventCounts(); + ClearOnShutdown(&PlacesObservers::sCounts); + } +} + void PlacesObservers::AddListener(GlobalObject& aGlobal, const nsTArray<PlacesEventType>& aEventTypes, PlacesEventCallback& aCallback, @@ -304,7 +312,11 @@ void PlacesObservers::NotifyListeners( if (aEvents.Length() == 0) { return; } - + EnsureCountsInitialized(); + for (const auto& event : aEvents) { + DebugOnly<nsresult> rv = sCounts->Increment(event->Type()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } #ifdef DEBUG if (!gNotificationQueue.IsEmpty()) { NS_WARNING( @@ -389,4 +401,10 @@ void PlacesObservers::NotifyNext() { } } +already_AddRefed<PlacesEventCounts> PlacesObservers::Counts( + const GlobalObject& global) { + EnsureCountsInitialized(); + return do_AddRef(sCounts); +}; + } // namespace mozilla::dom diff --git a/dom/base/PlacesObservers.h b/dom/base/PlacesObservers.h index e7aca305d8..434380e11f 100644 --- a/dom/base/PlacesObservers.h +++ b/dom/base/PlacesObservers.h @@ -11,6 +11,7 @@ #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/PlacesObserversBinding.h" #include "mozilla/dom/PlacesEvent.h" +#include "mozilla/dom/PlacesEventCounts.h" #include "mozilla/places/INativePlacesEventCallback.h" #include "nsIWeakReferenceUtils.h" @@ -49,6 +50,9 @@ class PlacesObservers { static void NotifyListeners( const Sequence<OwningNonNull<PlacesEvent>>& aEvents); + static StaticRefPtr<PlacesEventCounts> sCounts; + static already_AddRefed<PlacesEventCounts> Counts(const GlobalObject& global); + private: static void RemoveListener(uint32_t aFlags, PlacesEventCallback& aCallback); static void RemoveListener(uint32_t aFlags, diff --git a/dom/base/RangeBoundary.h b/dom/base/RangeBoundary.h index 2e4ab42397..4c5a70fb6a 100644 --- a/dom/base/RangeBoundary.h +++ b/dom/base/RangeBoundary.h @@ -9,6 +9,7 @@ #include "nsCOMPtr.h" #include "nsIContent.h" +#include "mozilla/dom/ShadowRoot.h" #include "mozilla/Assertions.h" #include "mozilla/Maybe.h" @@ -351,6 +352,34 @@ class RangeBoundaryBase { } public: + void NotifyParentBecomesShadowHost() { + MOZ_ASSERT(mParent); + MOZ_ASSERT(mParent->IsContainerNode(), + "Range is positioned on a text node!"); + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return; + } + + if (!mIsMutationObserved) { + // RangeBoundaries that are not used in the context of a + // `MutationObserver` use the offset as main source of truth to compute + // `mRef`. Therefore, it must not be updated or invalidated. + return; + } + + if (!mRef) { + MOZ_ASSERT(mOffset.isSome() && mOffset.value() == 0, + "Invalidating offset of invalid RangeBoundary?"); + return; + } + + if (dom::ShadowRoot* shadowRoot = mParent->GetShadowRootForSelection()) { + mParent = shadowRoot; + } + + mOffset = Some(0); + } + bool IsSet() const { return mParent && (mRef || mOffset.isSome()); } bool IsSetAndValid() const { diff --git a/dom/base/RangeUtils.cpp b/dom/base/RangeUtils.cpp index 28283054b8..2383cc18aa 100644 --- a/dom/base/RangeUtils.cpp +++ b/dom/base/RangeUtils.cpp @@ -147,9 +147,10 @@ nsresult RangeUtils::CompareNodeToRange(nsINode* aNode, NS_WARN_IF(!aAbstractRange->IsPositioned())) { return NS_ERROR_INVALID_ARG; } - return CompareNodeToRangeBoundaries(aNode, aAbstractRange->StartRef(), - aAbstractRange->EndRef(), - aNodeIsBeforeRange, aNodeIsAfterRange); + return CompareNodeToRangeBoundaries( + aNode, aAbstractRange->MayCrossShadowBoundaryStartRef(), + aAbstractRange->MayCrossShadowBoundaryEndRef(), aNodeIsBeforeRange, + aNodeIsAfterRange); } template <typename SPT, typename SRT, typename EPT, typename ERT> nsresult RangeUtils::CompareNodeToRangeBoundaries( diff --git a/dom/base/ScriptableContentIterator.cpp b/dom/base/ScriptableContentIterator.cpp index d10c4e68f8..ab04053c29 100644 --- a/dom/base/ScriptableContentIterator.cpp +++ b/dom/base/ScriptableContentIterator.cpp @@ -109,6 +109,22 @@ ScriptableContentIterator::InitWithRange(IteratorType aType, nsRange* aRange) { } NS_IMETHODIMP +ScriptableContentIterator::InitWithRangeAllowCrossShadowBoundary( + IteratorType aType, nsRange* aRange) { + if (aType == NOT_INITIALIZED || + (mIteratorType != NOT_INITIALIZED && aType != mIteratorType) || + aType != SUBTREE_ITERATOR) { + return NS_ERROR_INVALID_ARG; + } + + mIteratorType = aType; + MOZ_ASSERT(mIteratorType == SUBTREE_ITERATOR); + EnsureContentIterator(); + return static_cast<ContentSubtreeIterator*>(mContentIterator.get()) + ->InitWithAllowCrossShadowBoundary(aRange); +} + +NS_IMETHODIMP ScriptableContentIterator::InitWithPositions(IteratorType aType, nsINode* aStartContainer, uint32_t aStartOffset, diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp index 69986e6b78..7983ef98f9 100644 --- a/dom/base/Selection.cpp +++ b/dom/base/Selection.cpp @@ -22,9 +22,11 @@ #include "mozilla/CaretAssociationHint.h" #include "mozilla/ContentIterator.h" #include "mozilla/dom/Element.h" +#include "mozilla/dom/ChildIterator.h" #include "mozilla/dom/SelectionBinding.h" #include "mozilla/dom/ShadowRoot.h" #include "mozilla/dom/StaticRange.h" +#include "mozilla/dom/ShadowIncludingTreeIterator.h" #include "mozilla/ErrorResult.h" #include "mozilla/HTMLEditor.h" #include "mozilla/IntegerRange.h" @@ -778,30 +780,39 @@ NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(Selection) NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Selection, Disconnect()) -const RangeBoundary& Selection::AnchorRef() const { +const RangeBoundary& Selection::AnchorRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { if (!mAnchorFocusRange) { static RangeBoundary sEmpty; return sEmpty; } if (GetDirection() == eDirNext) { - return mAnchorFocusRange->StartRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef() + : mAnchorFocusRange->StartRef(); } - return mAnchorFocusRange->EndRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef() + : mAnchorFocusRange->EndRef(); } -const RangeBoundary& Selection::FocusRef() const { +const RangeBoundary& Selection::FocusRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { if (!mAnchorFocusRange) { static RangeBoundary sEmpty; return sEmpty; } if (GetDirection() == eDirNext) { - return mAnchorFocusRange->EndRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef() + : mAnchorFocusRange->EndRef(); } - - return mAnchorFocusRange->StartRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef() + : mAnchorFocusRange->StartRef(); } void Selection::SetAnchorFocusRange(size_t aIndex) { @@ -818,8 +829,8 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode, uint32_t aCompareOffset, const AbstractRange& aRange, nsContentUtils::NodeIndexCache* aCache) { - MOZ_ASSERT(aRange.GetStartContainer()); - nsINode* start = aRange.GetStartContainer(); + MOZ_ASSERT(aRange.GetMayCrossShadowBoundaryStartContainer()); + nsINode* start = aRange.GetMayCrossShadowBoundaryStartContainer(); // If the nodes that we're comparing are not in the same document, assume that // aCompareNode will fall at the end of the ranges. if (aCompareNode.GetComposedDoc() != start->GetComposedDoc() || @@ -830,8 +841,9 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode, } // The points are in the same subtree, hence there has to be an order. - return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, start, - aRange.StartOffset(), aCache); + return *nsContentUtils::ComparePoints( + &aCompareNode, aCompareOffset, start, + aRange.MayCrossShadowBoundaryStartOffset(), aCache); } static int32_t CompareToRangeStart(const nsINode& aCompareNode, @@ -844,7 +856,7 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode, uint32_t aCompareOffset, const AbstractRange& aRange) { MOZ_ASSERT(aRange.IsPositioned()); - nsINode* end = aRange.GetEndContainer(); + nsINode* end = aRange.GetMayCrossShadowBoundaryEndContainer(); // If the nodes that we're comparing are not in the same document or in the // same subtree, assume that aCompareNode will fall at the end of the ranges. if (aCompareNode.GetComposedDoc() != end->GetComposedDoc() || @@ -855,8 +867,9 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode, } // The points are in the same subtree, hence there has to be an order. - return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, end, - aRange.EndOffset()); + return *nsContentUtils::ComparePoints( + &aCompareNode, aCompareOffset, end, + aRange.MayCrossShadowBoundaryEndOffset()); } // static @@ -1323,7 +1336,18 @@ nsresult Selection::RemoveCollapsedRanges() { nsresult Selection::StyledRanges::RemoveCollapsedRanges() { uint32_t i = 0; while (i < mRanges.Length()) { - if (mRanges[i].mRange->Collapsed()) { + const AbstractRange* range = mRanges[i].mRange; + // If nsRange::mCrossShadowBoundaryRange exists, it means + // there's a cross boundary selection, so obviously + // we shouldn't remove this range. + const bool collapsed = + range->Collapsed() && !range->MayCrossShadowBoundary(); + // Cross boundary range should always be uncollapsed. + MOZ_ASSERT_IF( + range->MayCrossShadowBoundary(), + !range->AsDynamicRange()->CrossShadowBoundaryRangeCollapsed()); + + if (collapsed) { nsresult rv = RemoveRangeAndUnregisterSelection(*mRanges[i].mRange); NS_ENSURE_SUCCESS(rv, rv); } else { @@ -1616,7 +1640,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval( // the given interval's start point, but that range isn't collapsed (a // collapsed range should be included in the returned results). const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange; - if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset) && + if (beginRange->MayCrossShadowBoundaryEndRef().Equals(aBeginNode, + aBeginOffset) && !beginRange->Collapsed()) { beginsAfterIndex++; } @@ -1627,7 +1652,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval( // included if (endsBeforeIndex < mRanges.Length()) { const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange; - if (endRange->StartRef().Equals(aEndNode, aEndOffset) && + if (endRange->MayCrossShadowBoundaryStartRef().Equals(aEndNode, + aEndOffset) && endRange->Collapsed()) { endsBeforeIndex++; } @@ -1710,6 +1736,16 @@ nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent( return NS_OK; } +void Selection::SelectFramesOfShadowIncludingDescendantsOfContent( + nsIContent* aContent, bool aSelected) const { + MOZ_ASSERT(aContent); + MOZ_ASSERT(StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()); + for (nsINode* node : ShadowIncludingTreeIterator(*aContent)) { + nsIContent* innercontent = node->IsContent() ? node->AsContent() : nullptr; + SelectFramesOf(innercontent, aSelected); + } +} + void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) { // this method is currently only called in a user-initiated context. // therefore it is safe to assume that we are not in a Highlight selection @@ -1748,16 +1784,22 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, if (mFrameSelection->IsInTableSelectionMode()) { const nsIContent* const commonAncestorContent = - nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor()); + nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor( + StaticPrefs::dom_select_events_textcontrols_selectstart_enabled() + ? AllowRangeCrossShadowBoundary::Yes + : AllowRangeCrossShadowBoundary::No)); nsIFrame* const frame = commonAncestorContent ? commonAncestorContent->GetPrimaryFrame() : aPresContext->PresShell()->GetRootFrame(); if (frame) { if (frame->IsTextFrame()) { - MOZ_ASSERT(commonAncestorContent == aRange.GetStartContainer()); - MOZ_ASSERT(commonAncestorContent == aRange.GetEndContainer()); + MOZ_ASSERT(commonAncestorContent == + aRange.GetMayCrossShadowBoundaryStartContainer()); + MOZ_ASSERT(commonAncestorContent == + aRange.GetMayCrossShadowBoundaryEndContainer()); static_cast<nsTextFrame*>(frame)->SelectionStateChanged( - aRange.StartOffset(), aRange.EndOffset(), aSelect, mSelectionType); + aRange.MayCrossShadowBoundaryStartOffset(), + aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType); } else { frame->SelectionStateChanged(); } @@ -1768,8 +1810,8 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // Loop through the content iterator for each content node; for each text // node, call SetSelected on it: - nsIContent* const startContent = - nsIContent::FromNodeOrNull(aRange.GetStartContainer()); + nsIContent* const startContent = nsIContent::FromNodeOrNull( + aRange.GetMayCrossShadowBoundaryStartContainer()); if (MOZ_UNLIKELY(!startContent)) { // Don't warn, bug 1055722 // XXX The range can start from a document node and such range can be @@ -1780,7 +1822,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, MOZ_DIAGNOSTIC_ASSERT(startContent->IsInComposedDoc()); // We must call first one explicitly - nsINode* const endNode = aRange.GetEndContainer(); + nsINode* const endNode = aRange.GetMayCrossShadowBoundaryEndContainer(); if (NS_WARN_IF(!endNode)) { // We null-checked start node above, therefore, end node should also be // non-null here. @@ -1792,10 +1834,10 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // The frame could be an SVG text frame, in which case we don't treat it // as a text frame. if (frame->IsTextFrame()) { - const uint32_t startOffset = aRange.StartOffset(); - const uint32_t endOffset = endNode == startContent - ? aRange.EndOffset() - : startContent->Length(); + const uint32_t startOffset = aRange.MayCrossShadowBoundaryStartOffset(); + const uint32_t endOffset = + endNode == startContent ? aRange.MayCrossShadowBoundaryEndOffset() + : startContent->Length(); static_cast<nsTextFrame*>(frame)->SelectionStateChanged( startOffset, endOffset, aSelect, mSelectionType); } else { @@ -1806,7 +1848,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // If the range is in a node and the node is a leaf node, we don't need to // walk the subtree. - if (aRange.Collapsed() || + if ((aRange.Collapsed() && !aRange.MayCrossShadowBoundary()) || (startContent == endNode && !startContent->HasChildren())) { if (!isFirstContentTextNode) { SelectFramesOf(startContent, aSelect); @@ -1815,7 +1857,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, } ContentSubtreeIterator subtreeIter; - subtreeIter.Init(&aRange); + subtreeIter.InitWithAllowCrossShadowBoundary(&aRange); if (isFirstContentTextNode && !subtreeIter.IsDone() && subtreeIter.GetCurrentNode() == startContent) { subtreeIter.Next(); // first content has already been handled. @@ -1825,8 +1867,12 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, MOZ_DIAGNOSTIC_ASSERT(subtreeIter.GetCurrentNode()); if (nsIContent* const content = nsIContent::FromNodeOrNull(subtreeIter.GetCurrentNode())) { - SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content, - aSelect); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + SelectFramesOfShadowIncludingDescendantsOfContent(content, aSelect); + } else { + SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content, + aSelect); + } } } @@ -1839,7 +1885,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // The frame could be an SVG text frame, in which case we'll ignore it. if (frame->IsTextFrame()) { static_cast<nsTextFrame*>(frame)->SelectionStateChanged( - 0, aRange.EndOffset(), aSelect, mSelectionType); + 0, aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType); } } return NS_OK; @@ -1901,10 +1947,11 @@ UniquePtr<SelectionDetails> Selection::LookUpSelection( if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) { continue; } - nsINode* startNode = range->GetStartContainer(); - nsINode* endNode = range->GetEndContainer(); - uint32_t startOffset = range->StartOffset(); - uint32_t endOffset = range->EndOffset(); + + nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer(); + nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer(); + uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset(); + uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset(); Maybe<uint32_t> start, end; if (startNode == aContent && endNode == aContent) { @@ -2184,6 +2231,67 @@ void Selection::RemoveAllRanges(ErrorResult& aRv) { RemoveAllRangesInternal(aRv); } +already_AddRefed<StaticRange> Selection::GetComposedRange( + const AbstractRange* aRange, + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const { + // If aIsEndNode is true, this method does the Step 5.1 and 5.2 + // in https://www.w3.org/TR/selection-api/#dom-selection-getcomposedranges, + // otherwise it does the Step 3.1 and 3.2. + auto reScope = [&aShadowRoots](nsINode*& aNode, uint32_t& aOffset, + bool aIsEndNode) { + MOZ_ASSERT(aNode); + while (aNode) { + const ShadowRoot* shadowRootOfNode = aNode->GetContainingShadow(); + if (!shadowRootOfNode) { + return; + } + + for (const OwningNonNull<ShadowRoot>& shadowRoot : aShadowRoots) { + if (shadowRoot->IsShadowIncludingInclusiveDescendantOf( + shadowRootOfNode)) { + return; + } + } + + const nsIContent* host = aNode->GetContainingShadowHost(); + const Maybe<uint32_t> maybeIndex = host->ComputeIndexInParentContent(); + MOZ_ASSERT(maybeIndex.isSome(), "not parent or anonymous child?"); + if (MOZ_UNLIKELY(maybeIndex.isNothing())) { + // Unlikely to happen, but still set aNode to nullptr to avoid + // leaking information about the shadow tree. + aNode = nullptr; + return; + } + aOffset = maybeIndex.value(); + if (aIsEndNode) { + aOffset += 1; + } + aNode = host->GetParentNode(); + } + }; + + nsINode* startNode = aRange->GetMayCrossShadowBoundaryStartContainer(); + uint32_t startOffset = aRange->MayCrossShadowBoundaryStartOffset(); + nsINode* endNode = aRange->GetMayCrossShadowBoundaryEndContainer(); + uint32_t endOffset = aRange->MayCrossShadowBoundaryEndOffset(); + + reScope(startNode, startOffset, false /* aIsEndNode */); + reScope(endNode, endOffset, true /* aIsEndNode */); + + RefPtr<StaticRange> composedRange = StaticRange::Create( + startNode, startOffset, endNode, endOffset, IgnoreErrors()); + return composedRange.forget(); +} + +void Selection::GetComposedRanges( + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots, + nsTArray<RefPtr<StaticRange>>& aComposedRanges) { + aComposedRanges.SetCapacity(mStyledRanges.mRanges.Length()); + for (const auto& range : mStyledRanges.mRanges) { + aComposedRanges.AppendElement(GetComposedRange(range.mRange, aShadowRoots)); + } +} + void Selection::RemoveAllRangesInternal(ErrorResult& aRv) { if (!mFrameSelection) { aRv.Throw(NS_ERROR_NOT_INITIALIZED); @@ -2501,11 +2609,15 @@ void Selection::CollapseInternal(InLimiter aInLimiter, // Hack to display the caret on the right line (bug 1237236). if (frameSelection->GetHint() == CaretAssociationHint::Before && aPoint.Container()->IsContent()) { - int32_t frameOffset; - nsTextFrame* f = do_QueryFrame(nsCaret::GetFrameAndOffset( - this, aPoint.Container(), - *aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets), - &frameOffset)); + const nsCaret::CaretPosition pos{ + aPoint.Container(), + int32_t(*aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)), + frameSelection->GetHint(), frameSelection->GetCaretBidiLevel()}; + CaretFrameData frameData = nsCaret::GetFrameAndOffset(pos); + if (frameData.mFrame) { + frameSelection->SetHint(frameData.mHint); + } + nsTextFrame* f = do_QueryFrame(frameData.mFrame); if (f && f->IsAtEndOfLine() && f->HasSignificantTerminalNewline()) { // RawRangeBounary::Offset() causes computing offset if it's not been // done yet. However, it's called only when the container is a text @@ -2672,6 +2784,19 @@ AbstractRange* Selection::GetAbstractRangeAt(uint32_t aIndex) const { return mStyledRanges.mRanges.SafeElementAt(aIndex, empty).mRange; } +void Selection::GetDirection(nsAString& aDirection) const { + if (mStyledRanges.mRanges.IsEmpty() || + (mFrameSelection && (mFrameSelection->IsDoubleClickSelection() || + mFrameSelection->IsTripleClickSelection()))) { + // Empty range and double/triple clicks result a directionless selection. + aDirection.AssignLiteral("none"); + } else if (mDirection == nsDirection::eDirPrevious) { + aDirection.AssignLiteral("backward"); + } else { + aDirection.AssignLiteral("forward"); + } +} + nsRange* Selection::GetRangeAt(uint32_t aIndex) const { // This method per IDL spec returns a dynamic range. // Therefore, it must be ensured that it is only called @@ -2836,17 +2961,17 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, #ifdef DEBUG_SELECTION nsDirection oldDirection = GetDirection(); #endif - nsINode* anchorNode = GetAnchorNode(); - nsINode* focusNode = GetFocusNode(); - const uint32_t anchorOffset = AnchorOffset(); - const uint32_t focusOffset = FocusOffset(); + nsINode* anchorNode = GetMayCrossShadowBoundaryAnchorNode(); + nsINode* focusNode = GetMayCrossShadowBoundaryFocusNode(); + const uint32_t anchorOffset = MayCrossShadowBoundaryAnchorOffset(); + const uint32_t focusOffset = MayCrossShadowBoundaryFocusOffset(); RefPtr<nsRange> range = mAnchorFocusRange->CloneRange(); - nsINode* startNode = range->GetStartContainer(); - nsINode* endNode = range->GetEndContainer(); - const uint32_t startOffset = range->StartOffset(); - const uint32_t endOffset = range->EndOffset(); + nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer(); + nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer(); + const uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset(); + const uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset(); bool shouldClearRange = false; const Maybe<int32_t> anchorOldFocusOrder = nsContentUtils::ComparePoints( @@ -2882,7 +3007,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, (*anchorOldFocusOrder <= 0 && *oldFocusNewFocusOrder < 0)) { // a1,2 a,1,2 // select from 1 to 2 unless they are collapsed - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2903,7 +3029,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, *anchorNewFocusOrder > 0) { // 2, a1 // select from 2 to 1a SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2923,7 +3050,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2933,27 +3061,33 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SelectFrames(presContext, *difRange, false); // deselect now - difRange->SetEnd(range->GetEndContainer(), range->EndOffset()); + difRange->SetEnd(range->GetMayCrossShadowBoundaryEndContainer(), + range->MayCrossShadowBoundaryEndOffset(), + AllowRangeCrossShadowBoundary::Yes); SelectFrames(presContext, *difRange, true); // must reselect last node // maybe more } else if (*anchorOldFocusOrder >= 0 && *anchorNewFocusOrder <= 0) { // 1,a,2 or 1a,2 or 1,a2 or 1a2 if (GetDirection() == eDirPrevious) { - res = range->SetStart(endNode, endOffset); + res = range->SetStart(endNode, endOffset, + AllowRangeCrossShadowBoundary::Yes); if (NS_FAILED(res)) { aRv.Throw(res); return; } } SetDirection(eDirNext); - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } if (focusNode != anchorNode || focusOffset != anchorOffset) { // if collapsed diff dont do anything - res = difRange->SetStart(focusNode, focusOffset); - nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset); + res = difRange->SetStart(focusNode, focusOffset, + AllowRangeCrossShadowBoundary::Yes); + nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset, + AllowRangeCrossShadowBoundary::Yes); if (NS_FAILED(tmp)) { res = tmp; } @@ -2987,7 +3121,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2998,15 +3133,19 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SelectFrames(presContext, *difRange, false); - difRange->SetStart(range->GetStartContainer(), range->StartOffset()); + difRange->SetStart(range->GetMayCrossShadowBoundaryStartContainer(), + range->MayCrossShadowBoundaryStartOffset(), + AllowRangeCrossShadowBoundary::Yes); SelectFrames(presContext, *difRange, true); // must reselect last node } else if (*anchorNewFocusOrder >= 0 && *anchorOldFocusOrder <= 0) { // 2,a,1 or 2a,1 or 2,a1 or 2a1 if (GetDirection() == eDirNext) { - range->SetEnd(startNode, startOffset); + range->SetEnd(startNode, startOffset, + AllowRangeCrossShadowBoundary::Yes); } SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -3036,7 +3175,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, } else if (*oldFocusNewFocusOrder >= 0 && *anchorOldFocusOrder >= 0) { // 2,1,a or 21,a or 2,1a or 21a // select from 2 to 1 - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -3604,9 +3744,10 @@ void Selection::NotifySelectionListeners() { RefPtr<nsFrameSelection> frameSelection = mFrameSelection; - // This flag will be set to true if a selection by double click is detected. - // As soon as the selection is modified, it needs to be set to false. - frameSelection->SetIsDoubleClickSelection(false); + // This flag will be set to Double or Triple if a selection by double click or + // triple click is detected. As soon as the selection is modified, it needs to + // be reset to NotApplicable. + frameSelection->SetClickSelectionType(ClickSelectionType::NotApplicable); if (frameSelection->IsBatching()) { frameSelection->SetChangesDuringBatchingFlag(); diff --git a/dom/base/Selection.h b/dom/base/Selection.h index 9f031ab3cf..08563993ac 100644 --- a/dom/base/Selection.h +++ b/dom/base/Selection.h @@ -64,6 +64,9 @@ namespace dom { class Selection final : public nsSupportsWeakReference, public nsWrapperCache, public SupportsWeakPtr { + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; + protected: virtual ~Selection(); @@ -205,6 +208,10 @@ class Selection final : public nsSupportsWeakReference, nsRange* aRange, Maybe<size_t>* aOutIndex, DispatchSelectstartEvent aDispatchSelectstartEvent); + already_AddRefed<StaticRange> GetComposedRange( + const AbstractRange* aRange, + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const; + public: nsresult RemoveCollapsedRanges(); void Clear(nsPresContext* aPresContext); @@ -246,6 +253,8 @@ class Selection final : public nsSupportsWeakReference, // anchor and which end is focus. const nsRange* GetAnchorFocusRange() const { return mAnchorFocusRange; } + void GetDirection(nsAString& aDirection) const; + nsDirection GetDirection() const { return mDirection; } void SetDirection(nsDirection aDir) { mDirection = aDir; } @@ -321,6 +330,30 @@ class Selection final : public nsSupportsWeakReference, return offset ? *offset : 0; } + nsINode* GetMayCrossShadowBoundaryAnchorNode() const { + const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes); + return anchor.IsSet() ? anchor.Container() : nullptr; + } + + uint32_t MayCrossShadowBoundaryAnchorOffset() const { + const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes); + const Maybe<uint32_t> offset = + anchor.Offset(RangeBoundary::OffsetFilter::kValidOffsets); + return offset ? *offset : 0; + } + + nsINode* GetMayCrossShadowBoundaryFocusNode() const { + const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes); + return focus.IsSet() ? focus.Container() : nullptr; + } + + uint32_t MayCrossShadowBoundaryFocusOffset() const { + const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes); + const Maybe<uint32_t> offset = + focus.Offset(RangeBoundary::OffsetFilter::kValidOffsets); + return offset ? *offset : 0; + } + nsIContent* GetChildAtAnchorOffset() { const RangeBoundary& anchor = AnchorRef(); return anchor.IsSet() ? anchor.GetChildAtOffset() : nullptr; @@ -330,8 +363,12 @@ class Selection final : public nsSupportsWeakReference, return focus.IsSet() ? focus.GetChildAtOffset() : nullptr; } - const RangeBoundary& AnchorRef() const; - const RangeBoundary& FocusRef() const; + const RangeBoundary& AnchorRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; + const RangeBoundary& FocusRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; /* * IsCollapsed -- is the whole selection just one point, or unset? @@ -385,6 +422,10 @@ class Selection final : public nsSupportsWeakReference, MOZ_CAN_RUN_SCRIPT void RemoveAllRanges(mozilla::ErrorResult& aRv); + void GetComposedRanges( + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots, + nsTArray<RefPtr<StaticRange>>& aComposedRanges); + /** * Whether Stringify should flush layout or not. */ @@ -810,6 +851,12 @@ class Selection final : public nsSupportsWeakReference, PostContentIterator& aPostOrderIter, nsIContent* aContent, bool aSelected) const; + /** + * https://dom.spec.whatwg.org/#concept-shadow-including-descendant + */ + void SelectFramesOfShadowIncludingDescendantsOfContent(nsIContent* aContent, + bool aSelected) const; + nsresult SelectFrames(nsPresContext* aPresContext, AbstractRange& aRange, bool aSelect) const; diff --git a/dom/base/StaticRange.cpp b/dom/base/StaticRange.cpp index 0946e8f9bf..73ff04c038 100644 --- a/dom/base/StaticRange.cpp +++ b/dom/base/StaticRange.cpp @@ -100,6 +100,13 @@ bool StaticRange::IsValid() const { return false; } + MOZ_ASSERT(mAreStartAndEndInSameTree == + (RangeUtils::ComputeRootNode(mStart.Container()) == + RangeUtils::ComputeRootNode(mEnd.Container()))); + if (!mAreStartAndEndInSameTree) { + return false; + } + const Maybe<int32_t> pointOrder = nsContentUtils::ComparePoints(mStart, mEnd); return pointOrder.isSome() && *pointOrder <= 0; } @@ -119,6 +126,9 @@ void StaticRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, if (checkCommonAncestor) { UpdateCommonAncestorIfNecessary(); } + + mAreStartAndEndInSameTree = RangeUtils::ComputeRootNode(mStart.Container()) == + RangeUtils::ComputeRootNode(mEnd.Container()); } /* static */ diff --git a/dom/base/StaticRange.h b/dom/base/StaticRange.h index a6f677130d..af7054f843 100644 --- a/dom/base/StaticRange.h +++ b/dom/base/StaticRange.h @@ -8,6 +8,7 @@ #define mozilla_dom_StaticRange_h #include "mozilla/RangeBoundary.h" +#include "mozilla/RangeUtils.h" #include "mozilla/dom/AbstractRange.h" #include "mozilla/dom/StaticRangeBinding.h" #include "nsTArray.h" @@ -70,6 +71,21 @@ class StaticRange final : public AbstractRange { */ bool IsValid() const; + void NotifyNodeBecomesShadowHost(nsINode* aNode) { + if (aNode == mStart.Container()) { + mStart.NotifyParentBecomesShadowHost(); + } + + if (aNode == mEnd.Container()) { + mEnd.NotifyParentBecomesShadowHost(); + } + } + + private: + // Whether the start and end points are in the same tree. + // They could be in different trees, i.e, cross shadow boundaries. + bool mAreStartAndEndInSameTree = false; + protected: explicit StaticRange(nsINode* aNode) : AbstractRange(aNode, /* aIsDynamicRange = */ false) {} diff --git a/dom/base/ThirdPartyUtil.cpp b/dom/base/ThirdPartyUtil.cpp index 2c71ab3d94..0ced8aa7d8 100644 --- a/dom/base/ThirdPartyUtil.cpp +++ b/dom/base/ThirdPartyUtil.cpp @@ -200,9 +200,9 @@ ThirdPartyUtil::IsThirdPartyWindow(mozIDOMWindowProxy* aWindow, nsIURI* aURI, bool result; - // Ignore about:blank URIs here since they have no domain and attempting to - // compare against them will fail. - if (aURI && !NS_IsAboutBlank(aURI)) { + // Ignore about:blank and about:srcdoc URIs here since they have no domain + // and attempting to compare against them will fail. + if (aURI && !NS_IsAboutBlank(aURI) && !NS_IsAboutSrcdoc(aURI)) { nsCOMPtr<nsIPrincipal> prin; nsresult rv = GetPrincipalFromWindow(aWindow, getter_AddRefs(prin)); NS_ENSURE_SUCCESS(rv, rv); @@ -320,10 +320,10 @@ ThirdPartyUtil::IsThirdPartyChannel(nsIChannel* aChannel, nsIURI* aURI, } } - // Special consideration must be done for about:blank URIs because those - // inherit the principal from the parent context. For them, let's consider the - // principal URI. - if (NS_IsAboutBlank(channelURI)) { + // Special consideration must be done for about:blank and about:srcdoc URIs + // because those inherit the principal from the parent context. For them, + // let's consider the principal URI. + if (NS_IsAboutBlank(channelURI) || NS_IsAboutSrcdoc(channelURI)) { nsCOMPtr<nsIPrincipal> principalToInherit = loadInfo->FindPrincipalToInherit(aChannel); if (!principalToInherit) { diff --git a/dom/base/UseCounters.conf b/dom/base/UseCounters.conf index 86d782b476..66507b6d49 100644 --- a/dom/base/UseCounters.conf +++ b/dom/base/UseCounters.conf @@ -62,11 +62,6 @@ method DataTransfer.mozGetDataAt attribute DataTransfer.mozUserCancelled attribute DataTransfer.mozSourceNode -// Marquee events -custom onstart sets a <marquee> onstart event listener -custom onbounce sets a <marquee> onbounce event listener -custom onfinish sets a <marquee> onfinish event listener - // Element non-standard events custom onoverflow sets an element onoverflow event listener custom onunderflow sets an element onunderflow event listener diff --git a/dom/base/crashtests/1697256.html b/dom/base/crashtests/1697256.html index 25024083e3..3d6634e952 100644 --- a/dom/base/crashtests/1697256.html +++ b/dom/base/crashtests/1697256.html @@ -4,8 +4,9 @@ <script> window.onload = () => { window.requestIdleCallback(() => { - SpecialPowers.wrap(self).printPreview() + let pp = SpecialPowers.wrap(self).printPreview() setTimeout(() => { + try { pp.close(); } catch (e) { } document.documentElement.classList.remove("reftest-wait"); }, 250) }) diff --git a/dom/base/crashtests/1887930.html b/dom/base/crashtests/1887930.html new file mode 100644 index 0000000000..04a89de8d2 --- /dev/null +++ b/dom/base/crashtests/1887930.html @@ -0,0 +1,7 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + document.getSelection().extend(a) +}) +</script> +<dialog id="a"></dialog> +<input type="datetime-local" autofocus="autofocus"> diff --git a/dom/base/crashtests/1887963_1.html b/dom/base/crashtests/1887963_1.html new file mode 100644 index 0000000000..fbc30dc587 --- /dev/null +++ b/dom/base/crashtests/1887963_1.html @@ -0,0 +1,15 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + let c = a.attachShadow({mode: "open"}) + let b = document.getElementById("host").shadowRoot.getElementById("b"); + window.parent.getSelection().setBaseAndExtent(b, 0, c, 0) +}) +</script> +<div id="a">A</div> +<video> + <div id="host"> + <template shadowrootmode="open"> + <video id="b"> + </template> + </div> +</video> diff --git a/dom/base/crashtests/1887963_2.html b/dom/base/crashtests/1887963_2.html new file mode 100644 index 0000000000..53f512173b --- /dev/null +++ b/dom/base/crashtests/1887963_2.html @@ -0,0 +1,15 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + let c = a.attachShadow({mode: "open"}) + let b = document.getElementById("not-slotted"); + window.parent.getSelection().setBaseAndExtent(b, 0, c, 0) +}) +</script> +<div id="a">A</div> +<video> + <div id="host"> + <template shadowrootmode="open"> + </template> + <span id="not-slotted">NotSlotted</span> + </div> +</video> diff --git a/dom/base/crashtests/1887974.html b/dom/base/crashtests/1887974.html new file mode 100644 index 0000000000..85ffd2b02a --- /dev/null +++ b/dom/base/crashtests/1887974.html @@ -0,0 +1,18 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + c.add(d, 1) + b.addEventListener("DOMNodeRemoved", () => { + a.appendChild(f) + }) + let r = document.createRange() + r.setEndBefore(d) + r.deleteContents() +}) +</script> +<audio> +<canvas id="b"> +</canvas> +<select id="c"> +<option id="d">A</option> +<optgroup id="f"> +<input id="a"> diff --git a/dom/base/crashtests/1890888.html b/dom/base/crashtests/1890888.html new file mode 100644 index 0000000000..006768ae14 --- /dev/null +++ b/dom/base/crashtests/1890888.html @@ -0,0 +1,13 @@ +<script> +window.addEventListener("DOMContentLoaded", () => { + o1.prepend(o4) + document.getSelection().setBaseAndExtent(o5.attachShadow({mode: "closed"}), 0, o3, 0) + o5.replaceWith(o2) + setTimeout(window.close, 500) +}) +</script> +<iframe id="o1"></iframe> +<picture id="o2"></picture> +<area id="o3"> +<meter id="o4"> +<p id="o5"> diff --git a/dom/base/crashtests/crashtests.list b/dom/base/crashtests/crashtests.list index 864538ddf5..22aaf50e6b 100644 --- a/dom/base/crashtests/crashtests.list +++ b/dom/base/crashtests/crashtests.list @@ -271,3 +271,8 @@ load 1835886.html load 1836824.html skip-if(Android) load 1838484.html load 1840191.html +load 1887930.html +load 1887963_1.html +load 1887963_2.html +asserts(0-1) load 1887974.html +load 1890888.html diff --git a/dom/base/fragmentdirectives/Cargo.toml b/dom/base/fragmentdirectives/Cargo.toml new file mode 100644 index 0000000000..7b3b589668 --- /dev/null +++ b/dom/base/fragmentdirectives/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dom_fragmentdirectives" +version = "0.1.0" +authors = ["Jan Jaeschke <jjaschke@mozilla.com>"] +edition = "2021" +license = "MPL-2.0" + +[dependencies] +nsstring = { path = "../../../xpcom/rust/nsstring/" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +percent-encoding = { version = "2.3.1" } +[lib] +path = "lib.rs" diff --git a/dom/base/fragmentdirectives/cbindgen.toml b/dom/base/fragmentdirectives/cbindgen.toml new file mode 100644 index 0000000000..ec54ebc02d --- /dev/null +++ b/dom/base/fragmentdirectives/cbindgen.toml @@ -0,0 +1,15 @@ +header = """/* 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/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +include_guard = "fragmentdirectives_ffi_generated_h" +includes = ["nsStringFwd.h", "nsTArrayForwardDeclare.h"] + +[export.rename] +"ThinVec" = "nsTArray" diff --git a/dom/base/fragmentdirectives/fragment_directive_impl.rs b/dom/base/fragmentdirectives/fragment_directive_impl.rs new file mode 100644 index 0000000000..dfbdb37415 --- /dev/null +++ b/dom/base/fragmentdirectives/fragment_directive_impl.rs @@ -0,0 +1,342 @@ +/* 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/. */ +use percent_encoding::{percent_decode, percent_encode, NON_ALPHANUMERIC}; +use std::str; + +/// The `FragmentDirectiveParameter` represents one of +/// `[prefix-,]start[,end][,-suffix]` without any surrounding `-` or `,`. +/// +/// The token is stored as percent-decoded string. +/// Therefore, interfaces exist to +/// - create a `FragmentDirectiveParameter` from a percent-encoded string. +/// This function will determine from occurrence and position of a dash +/// if the token represents a `prefix`, `suffix` or either `start` or `end`. +/// - create a percent-encoded string from the value the token holds. +pub enum TextDirectiveParameter { + Prefix(String), + StartOrEnd(String), + Suffix(String), +} + +impl TextDirectiveParameter { + /// Creates a token from a percent-encoded string. + /// Based on position of a dash the correct token type is determined. + /// Returns `None` in case of an ill-formed token: + /// - starts and ends with a dash (i.e. `-token-`) + /// - only consists of a dash (i.e. `-`) or is empty + /// - conversion from percent-encoded string to utf8 fails. + pub fn from_percent_encoded(token: &[u8]) -> Option<Self> { + if token.is_empty() { + return None; + } + let starts_with_dash = *token.first().unwrap() == b'-'; + let ends_with_dash = *token.last().unwrap() == b'-'; + if starts_with_dash && ends_with_dash { + // `-token-` is not valid. + return None; + } + if token.len() == 1 && starts_with_dash { + // `-` is not valid. + return None; + } + // Note: Trimming of the raw strings is currently not mentioned in the spec. + // However, it looks as it is implicitly expected. + if starts_with_dash { + if let Ok(decoded_suffix) = percent_decode(&token[1..]).decode_utf8() { + return Some(TextDirectiveParameter::Suffix(String::from( + decoded_suffix.trim(), + ))); + } + return None; + } + if ends_with_dash { + if let Ok(decoded_prefix) = percent_decode(&token[..token.len() - 1]).decode_utf8() { + return Some(TextDirectiveParameter::Prefix(String::from( + decoded_prefix.trim(), + ))); + } + return None; + } + if let Ok(decoded_text) = percent_decode(&token).decode_utf8() { + return Some(TextDirectiveParameter::StartOrEnd(String::from( + decoded_text.trim(), + ))); + } + None + } + + /// Returns the value of the token as percent-decoded `String`. + pub fn value(&self) -> &String { + match self { + TextDirectiveParameter::Prefix(value) => &value, + TextDirectiveParameter::StartOrEnd(value) => &value, + TextDirectiveParameter::Suffix(value) => &value, + } + } + + /// Creates a percent-encoded string of the token's value. + /// This includes placing a dash appropriately + /// to indicate whether this token is prefix, suffix or start/end. + /// + /// This method always returns a new object. + pub fn to_percent_encoded_string(&self) -> String { + let encode = |text: &String| percent_encode(text.as_bytes(), NON_ALPHANUMERIC).to_string(); + match self { + Self::Prefix(text) => encode(text) + "-", + Self::StartOrEnd(text) => encode(text), + Self::Suffix(text) => { + let encoded = encode(text); + let mut result = String::with_capacity(encoded.len() + 1); + result.push_str("-"); + result.push_str(&encoded); + result + } + } + } +} + +/// This struct represents one parsed text directive using Rust types. +/// +/// A text fragment is encoded into a URL fragment like this: +/// `text=[prefix-,]start[,end][,-suffix]` +/// +/// The text directive is considered valid if at least `start` is not None. +/// (see `Self::is_valid()`). +#[derive(Default)] +pub struct TextDirective { + prefix: Option<TextDirectiveParameter>, + start: Option<TextDirectiveParameter>, + end: Option<TextDirectiveParameter>, + suffix: Option<TextDirectiveParameter>, +} +impl TextDirective { + /// Creates an instance from string parts. + /// This function is intended to be used when a fragment directive string should be created. + /// Returns `None` if `start` is empty. + pub fn from_parts(prefix: String, start: String, end: String, suffix: String) -> Option<Self> { + if !start.is_empty() { + Some(Self { + prefix: if !prefix.is_empty() { + Some(TextDirectiveParameter::Prefix(prefix.trim().into())) + } else { + None + }, + start: Some(TextDirectiveParameter::StartOrEnd(start.trim().into())), + end: if !end.is_empty() { + Some(TextDirectiveParameter::StartOrEnd(end.trim().into())) + } else { + None + }, + suffix: if !suffix.is_empty() { + Some(TextDirectiveParameter::Suffix(suffix.trim().into())) + } else { + None + }, + }) + } else { + None + } + } + + /// Creates an instance from a percent-encoded string + /// that originates from a fragment directive. + /// + /// `text_fragment` is supposed to have this format: + /// ``` + /// text=[prefix-,]start[,end][,-suffix] + /// ``` + /// This function returns `None` if `text_fragment` + /// does not start with `text=`, it contains 0 or more + /// than 4 elements or prefix/suffix/start or end + /// occur too many times. + /// It also returns `None` if any of the tokens parses to fail. + pub fn from_percent_encoded_string(text_directive: &str) -> Option<Self> { + // first check if the string starts with `text=` + if text_directive.len() < 6 { + return None; + } + if !text_directive.starts_with("text=") { + return None; + } + + let mut parsed_text_directive = Self::default(); + let valid = text_directive[5..] + .split(",") + // Parse the substrings into `TextDirectiveParameter`s. This will determine + // for each substring if it is a Prefix, Suffix or Start/End, + // or if it is invalid. + .map(|token| TextDirectiveParameter::from_percent_encoded(token.as_bytes())) + // populate `parsed_text_directive` and check its validity by inserting the parameters + // one by one. Given that the parameters are sorted by their position in the source, + // the validity of the text directive can be determined while adding the parameters. + .map(|token| match token { + Some(TextDirectiveParameter::Prefix(..)) => { + if !parsed_text_directive.is_empty() { + // `prefix-` must be the first result. + return false; + } + parsed_text_directive.prefix = token; + return true; + } + Some(TextDirectiveParameter::StartOrEnd(..)) => { + if parsed_text_directive.suffix.is_some() { + // start or end must come before `-suffix`. + return false; + } + if parsed_text_directive.start.is_none() { + parsed_text_directive.start = token; + return true; + } + if parsed_text_directive.end.is_none() { + parsed_text_directive.end = token; + return true; + } + // if `start` and `end` is already filled, + // this is invalid as well. + return false; + } + Some(TextDirectiveParameter::Suffix(..)) => { + if parsed_text_directive.start.is_some() + && parsed_text_directive.suffix.is_none() + { + // `start` must be present and `-suffix` must not be present. + // `end` may be present. + parsed_text_directive.suffix = token; + return true; + } + return false; + } + // empty or invalid token renders the whole text directive invalid. + None => false, + }) + .all(|valid| valid); + if valid { + return Some(parsed_text_directive); + } + None + } + + /// Creates a percent-encoded string for the current `TextDirective`. + /// In the unlikely case that the `TextDirective` is invalid (i.e. `start` is None), + /// which should have been caught earlier,this method returns an empty string. + pub fn to_percent_encoded_string(&self) -> String { + if !self.is_valid() { + return String::default(); + } + String::from("text=") + + &[&self.prefix, &self.start, &self.end, &self.suffix] + .iter() + .filter_map(|&token| token.as_ref()) + .map(|token| token.to_percent_encoded_string()) + .collect::<Vec<_>>() + .join(",") + } + + pub fn start(&self) -> &Option<TextDirectiveParameter> { + &self.start + } + + pub fn end(&self) -> &Option<TextDirectiveParameter> { + &self.end + } + + pub fn prefix(&self) -> &Option<TextDirectiveParameter> { + &self.prefix + } + + pub fn suffix(&self) -> &Option<TextDirectiveParameter> { + &self.suffix + } + + fn is_empty(&self) -> bool { + self.prefix.is_none() && self.start.is_none() && self.end.is_none() && self.suffix.is_none() + } + + /// A `TextDirective` object is valid if it contains the `start` token. + /// All other tokens are optional. + fn is_valid(&self) -> bool { + self.start.is_some() + } +} +/// Parses a fragment directive into a list of `TextDirective` objects and removes +/// the fragment directive from the input url. +/// +/// If the hash does not contain a fragment directive, `url` is not modified +/// and this function returns `None`. +/// Otherwise, the fragment directive is removed from `url` and parsed. +/// If parsing fails, this function returns `None`. +pub fn parse_fragment_directive_and_remove_it_from_hash( + url: &str, +) -> Option<(&str, &str, Vec<TextDirective>)> { + // The Fragment Directive is preceded by a `:~:`, + // which is only allowed to appear in the hash once. + // However (even if unlikely), it might appear outside of the hash, + // so this code only considers it when it is after the #. + let maybe_first_hash_pos = url.find("#"); + // If there is no # in url, it is considered to be only the hash (and not a full url). + let first_hash_pos = maybe_first_hash_pos.unwrap_or_default(); + let mut fragment_directive_iter = url[first_hash_pos..].split(":~:"); + let url_with_stripped_fragment_directive = + &url[..first_hash_pos + fragment_directive_iter.next().unwrap_or_default().len()]; + + if let Some(fragment_directive) = fragment_directive_iter.next() { + if fragment_directive_iter.next().is_some() { + // There are multiple occurrences of `:~:`, which is not allowed. + return None; + } + // - fragments are separated by `&`. + // - if a fragment does not start with `text=`, it is not a text fragment and will be ignored. + // - if parsing of the text fragment fails (for whatever reason), it will be ignored. + let text_directives: Vec<_> = fragment_directive + .split("&") + .map(|maybe_text_fragment| { + TextDirective::from_percent_encoded_string(&maybe_text_fragment) + }) + .filter_map(|maybe_text_directive| maybe_text_directive) + .collect(); + if !text_directives.is_empty() { + return Some(( + url_with_stripped_fragment_directive + .strip_suffix("#") + .unwrap_or(url_with_stripped_fragment_directive), + fragment_directive, + text_directives, + )); + } + } + None +} + +/// Creates a percent-encoded text fragment string. +/// +/// The returned string starts with `:~:`, so that it can be appended +/// to a normal fragment. +/// Text directives which are not valid (ie., they are missing the `start` parameter), +/// are skipped. +/// +/// Returns `None` if `fragment_directives` is empty. +pub fn create_fragment_directive_string(text_directives: &Vec<TextDirective>) -> Option<String> { + if text_directives.is_empty() { + return None; + } + let encoded_fragment_directives: Vec<_> = text_directives + .iter() + .filter(|&fragment_directive| fragment_directive.is_valid()) + .map(|fragment_directive| fragment_directive.to_percent_encoded_string()) + .filter(|text_directive| !text_directive.is_empty()) + .collect(); + if encoded_fragment_directives.is_empty() { + return None; + } + Some(String::from(":~:") + &encoded_fragment_directives.join("&")) +} + +/// Creates the percent-encoded text directive string for a single text directive. +pub fn create_text_directive_string(text_directive: &TextDirective) -> Option<String> { + if text_directive.is_valid() { + Some(text_directive.to_percent_encoded_string()) + } else { + None + } +} diff --git a/dom/base/fragmentdirectives/lib.rs b/dom/base/fragmentdirectives/lib.rs new file mode 100644 index 0000000000..0003849eb7 --- /dev/null +++ b/dom/base/fragmentdirectives/lib.rs @@ -0,0 +1,158 @@ +/* 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/. */ + +use nsstring::{nsCString, nsString}; +use thin_vec::ThinVec; +pub mod fragment_directive_impl; +mod test; + +/// This struct contains the percent-decoded parts of a text directive. +/// All parts besides `start` are optional (which is indicated by an empty string). +/// +/// This struct uses Gecko String types, whereas the parser internally uses Rust types. +/// Therefore, conversion functions are provided. +#[repr(C)] +pub struct TextDirective { + prefix: nsString, + start: nsString, + end: nsString, + suffix: nsString, +} + +impl TextDirective { + /// Creates a `FragmentDirectiveElement` object from a `FragmentDirectiveElementInternal` object + /// (which uses Rust string types). + fn from_rust_type(element: &fragment_directive_impl::TextDirective) -> Self { + Self { + prefix: element + .prefix() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + start: element + .start() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + end: element + .end() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + suffix: element + .suffix() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + } + } + + /// Converts the contents of this object into Rust types. + /// Returns `None` if the given fragment is not valid. + /// The only invalid condition is a fragment that is missing the `start` token. + fn to_rust_type(&self) -> Option<fragment_directive_impl::TextDirective> { + fragment_directive_impl::TextDirective::from_parts( + self.prefix.to_string(), + self.start.to_string(), + self.end.to_string(), + self.suffix.to_string(), + ) + } +} + +/// Result of the `parse_fragment_directive()` function. +/// +/// The result contains the original given URL without the fragment directive, +/// a unsanitized string version of the extracted fragment directive, +/// and an array of the parsed text directives. +#[repr(C)] +pub struct ParsedFragmentDirectiveResult { + url_without_fragment_directive: nsCString, + fragment_directive: nsCString, + text_directives: ThinVec<TextDirective>, +} + +/// Parses the fragment directive from a given URL. +/// +/// This function writes the result data into `result`. +/// The result consists of +/// - the input url without the fragment directive, +/// - the fragment directive as unparsed string, +/// - a list of the parsed and percent-decoded text directives. +/// +/// Directives which are unknown will be ignored. +/// If new directive types are added in the future, they should also be considered here. +/// This function returns false if no fragment directive is found, or it could not be parsed. +#[no_mangle] +pub extern "C" fn parse_fragment_directive( + url: &nsCString, + result: &mut ParsedFragmentDirectiveResult, +) -> bool { + // sanitize inputs + result.url_without_fragment_directive = nsCString::new(); + result.fragment_directive = nsCString::new(); + result.text_directives.clear(); + + let url_as_rust_string = url.to_utf8(); + if let Some((stripped_url, fragment_directive, text_directives)) = + fragment_directive_impl::parse_fragment_directive_and_remove_it_from_hash( + &url_as_rust_string, + ) + { + result + .url_without_fragment_directive + .assign(&stripped_url); + result.fragment_directive.assign(&fragment_directive); + result.text_directives.extend( + text_directives + .iter() + .map(|text_directive| TextDirective::from_rust_type(text_directive)), + ); + return true; + } + false +} + +/// Creates a percent-encoded fragment directive string from a given list of `FragmentDirectiveElement`s. +/// +/// The returned string has this form: +/// `:~:text=[prefix1-,]start1[,end1][,-suffix1]&text=[prefix2-,]start2[,end2][,-suffix2]` +/// +/// Invalid `FragmentDirectiveElement`s are ignored, where "invalid" means that no `start` token is provided. +/// If there are no valid `FragmentDirectiveElement`s, an empty string is returned. +#[no_mangle] +pub extern "C" fn create_fragment_directive( + text_directives: &ThinVec<TextDirective>, + fragment_directive: &mut nsCString, +) -> bool { + let directives_rust = Vec::from_iter( + text_directives + .iter() + .filter_map(|fragment| fragment.to_rust_type()), + ); + if let Some(fragment_directive_rust) = + fragment_directive_impl::create_fragment_directive_string(&directives_rust) + { + fragment_directive.assign(&fragment_directive_rust); + return true; + } + + false +} + +/// Creates a percent-encoded text directive string for a single text directive. +/// The returned string has the form `text=[prefix-,]start[,end][,-suffix]`. +/// If the provided `TextDirective` is invalid (i.e. it has no `start` attribute), +/// the outparam `directive_string` is empty and the function returns false. +#[no_mangle] +pub extern "C" fn create_text_directive( + text_directive: &TextDirective, + directive_string: &mut nsCString, +) -> bool { + if let Some(text_directive_rust) = text_directive.to_rust_type() { + if let Some(text_directive_string_rust) = + fragment_directive_impl::create_text_directive_string(&text_directive_rust) + { + directive_string.assign(&text_directive_string_rust); + return true; + } + } + false +} diff --git a/dom/base/fragmentdirectives/test.rs b/dom/base/fragmentdirectives/test.rs new file mode 100644 index 0000000000..d4509cb033 --- /dev/null +++ b/dom/base/fragmentdirectives/test.rs @@ -0,0 +1,599 @@ +/* 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/. */ + +#[cfg(test)] +mod test { + use crate::fragment_directive_impl::{ + create_fragment_directive_string, parse_fragment_directive_and_remove_it_from_hash, + TextDirective, + }; + + /// This test verifies that valid combinations of [prefix-,]start[,end][,-suffix] are parsed correctly. + #[test] + fn test_parse_fragment_directive_with_one_text_directive() { + let test_cases = vec![ + ("#:~:text=start", (None, Some("start"), None, None)), + ( + "#:~:text=start,end", + (None, Some("start"), Some("end"), None), + ), + ( + "#:~:text=prefix-,start", + (Some("prefix"), Some("start"), None, None), + ), + ( + "#:~:text=prefix-,start,end", + (Some("prefix"), Some("start"), Some("end"), None), + ), + ( + "#:~:text=prefix-,start,end,-suffix", + (Some("prefix"), Some("start"), Some("end"), Some("suffix")), + ), + ( + "#:~:text=start,-suffix", + (None, Some("start"), None, Some("suffix")), + ), + ( + "#:~:text=start,end,-suffix", + (None, Some("start"), Some("end"), Some("suffix")), + ), + ("#:~:text=text=", (None, Some("text="), None, None)), + ]; + for (url, (prefix, start, end, suffix)) in test_cases { + let (stripped_url, fragment_directive, result) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string + should be unsanitized and therefore match the input string." + ); + assert_eq!(result.len(), 1, "There must be one parsed text fragment."); + assert_eq!( + stripped_url, "", + "The fragment directive must be removed from the url hash." + ); + let text_directive = result.first().unwrap(); + if prefix.is_none() { + assert!( + text_directive.prefix().is_none(), + "There must be no `prefix` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .prefix() + .as_ref() + .expect("There must be a `prefix` token.") + .value() + == prefix.unwrap(), + "Wrong value for `prefix` (test case `{}`).", + url + ); + } + if start.is_none() { + assert!( + text_directive.start().is_none(), + "There must be no `start` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value() + == start.unwrap(), + "Wrong value for `start` (test case `{}`).", + url + ); + } + if end.is_none() { + assert!( + text_directive.end().is_none(), + "There must be no `end` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .end() + .as_ref() + .expect("There must be a `end` token.") + .value() + == end.unwrap(), + "Wrong value for `end` (test case `{}`).", + url + ); + } + if suffix.is_none() { + assert!( + text_directive.suffix().is_none(), + "There must be no `suffix` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value() + == suffix.unwrap(), + "Wrong value for `suffix` (test case `{}`).", + url + ); + } + } + } + + #[test] + fn test_parse_full_url() { + for (url, stripped_url_ref) in [ + ("https://example.com#:~:text=foo", "https://example.com"), + ( + "https://example.com/some/page.html?query=answer#:~:text=foo", + "https://example.com/some/page.html?query=answer", + ), + ( + "https://example.com/some/page.html?query=answer#fragment:~:text=foo", + "https://example.com/some/page.html?query=answer#fragment", + ), + ( + "http://example.com/page.html?query=irrelevant:~:#bar:~:text=foo", + "http://example.com/page.html?query=irrelevant:~:#bar" + ) + ] { + let (stripped_url, fragment_directive, _) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result"); + assert_eq!(stripped_url, stripped_url_ref, "The stripped url is not correct."); + assert_eq!(fragment_directive, "text=foo"); + } + } + + /// This test verifies that a text fragment is parsed correctly if it is preceded + /// or followed by a fragment (i.e. `#foo:~:text=bar`). + #[test] + fn test_parse_text_fragment_after_fragments() { + let url = "#foo:~:text=start"; + let (stripped_url, fragment_directive, result) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + result.len(), + 1, + "There must be exactly one parsed text fragment." + ); + assert_eq!( + stripped_url, "#foo", + "The fragment directive was not removed correctly." + ); + assert_eq!( + fragment_directive, "text=start", + "The fragment directive was not extracted correctly." + ); + let fragment = result.first().unwrap(); + assert!(fragment.prefix().is_none(), "There is no `prefix` token."); + assert_eq!( + fragment + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start" + ); + assert!(fragment.end().is_none(), "There is no `end` token."); + assert!(fragment.suffix().is_none(), "There is no `suffix` token."); + } + + /// Ensure that multiple text fragments are parsed correctly. + #[test] + fn test_parse_multiple_text_fragments() { + let url = "#:~:text=prefix-,start,-suffix&text=foo&text=bar,-suffix"; + let (_, _, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + text_directives.len(), + 3, + "There must be exactly two parsed text fragments." + ); + let first_text_directive = &text_directives[0]; + assert_eq!( + first_text_directive + .prefix() + .as_ref() + .expect("There must be a `prefix` token.") + .value(), + "prefix" + ); + assert_eq!( + first_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start" + ); + assert!( + first_text_directive.end().is_none(), + "There is no `end` token." + ); + assert_eq!( + first_text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value(), + "suffix" + ); + + let second_text_directive = &text_directives[1]; + assert!( + second_text_directive.prefix().is_none(), + "There is no `prefix` token." + ); + assert_eq!( + second_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "foo" + ); + assert!( + second_text_directive.end().is_none(), + "There is no `end` token." + ); + assert!( + second_text_directive.suffix().is_none(), + "There is no `suffix` token." + ); + let third_text_directive = &text_directives[2]; + assert!( + third_text_directive.prefix().is_none(), + "There is no `prefix` token." + ); + assert_eq!( + third_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "bar" + ); + assert!( + third_text_directive.end().is_none(), + "There is no `end` token." + ); + assert_eq!( + third_text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value(), + "suffix" + ); + } + + /// Multiple text directives should be parsed correctly + /// if they are surrounded or separated by unknown directives. + #[test] + fn test_parse_multiple_text_directives_with_unknown_directive_in_between() { + for url in [ + "#:~:foo&text=start1&text=start2", + "#:~:text=start1&foo&text=start2", + "#:~:text=start1&text=start2&foo", + ] { + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain the unknown directive." + ); + assert_eq!( + text_directives.len(), + 2, + "There must be exactly two parsed text fragments." + ); + let first_text_directive = &text_directives[0]; + assert_eq!( + first_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start1" + ); + let second_text_directive = &text_directives[1]; + assert_eq!( + second_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start2" + ); + } + } + + /// Ensures that input that doesn't contain a text fragment does not produce a result. + /// This includes the use of partial identifying tokens necessary for a text fragment + /// (e.g. `:~:` without `text=`, `text=foo` without the `:~:` or multiple occurrences of `:~:`) + /// In these cases, the parser must return `None` to indicate that there are no valid text fragments. + #[test] + fn test_parse_invalid_or_unknown_fragment_directive() { + for url in [ + "#foo", + "#foo:", + "#foo:~:", + "#foo:~:bar", + "text=prefix-,start", + "#:~:text=foo-,bar,-baz:~:text=foo", + ] { + let text_directives = + parse_fragment_directive_and_remove_it_from_hash(&url); + assert!( + text_directives.is_none(), + "The fragment `{}` does not contain a valid or known fragment directive.", + url + ); + } + } + + /// Ensures that ill-formed text directives (but valid fragment directives) + /// (starting correctly with `:~:text=`) are not parsed. + /// Instead `None` must be returned. + /// Test cases include invalid combinations of `prefix`/`suffix`es, + /// additional `,`s, too many `start`/`end` tokens, or empty text fragments. + #[test] + fn test_parse_invalid_text_fragments() { + for url in [ + "#:~:text=start,start,start", + "#:~:text=prefix-,prefix-", + "#:~:text=prefix-,-suffix", + "#:~:text=prefix-,start,start,start", + "#:~:text=prefix-,start,start,start,-suffix", + "#:~:text=start,start,start,-suffix", + "#:~:text=prefix-,start,end,-suffix,foo", + "#:~:text=foo,prefix-,start", + "#:~:text=prefix-,,start,", + "#:~:text=,prefix,start", + "#:~:text=", + ] { + let text_directives = + parse_fragment_directive_and_remove_it_from_hash(&url); + assert!( + text_directives.is_none(), + "The fragment directive `{}` does not contain a valid text directive.", + url + ); + } + } + + /// Ensure that out of multiple text fragments only the invalid ones are ignored + /// while valid text fragments are still returned. + /// Since correct parsing of multiple text fragments as well as + /// several forms of invalid text fragments are already tested in + /// `test_parse_multiple_text_fragments` and `test_parse_invalid_text_fragments()`, + /// it should be enough to test this with only one fragment directive + /// that contains two text fragments, one of them being invalid. + #[test] + fn test_valid_and_invalid_text_directives() { + for url in [ + "#:~:text=start&text=,foo,", + "#:~:text=foo,foo,foo&text=start", + ] { + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain invalid text directives." + ); + assert_eq!( + text_directives.len(), + 1, + "There must be exactly one parsed text fragment." + ); + let text_directive = text_directives.first().unwrap(); + assert_eq!( + text_directive + .start() + .as_ref() + .expect("There must be a `start` value.") + .value(), + "start", + "The `start` value of the text directive has the wrong value." + ); + } + } + + /// Ensures that a fragment directive that contains percent-encoded characters + /// is decoded correctly. This explicitly includes characters which are used + /// for identifying text fragments, i.e. `#`, `, `, `&`, `:`, `~` and `-`. + #[test] + fn test_parse_percent_encoding_tokens() { + let url = "#:~:text=prefix%26-,start%20and%2C,end%23,-%26suffix%2D"; + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain the original and percent-decoded string." + ); + let text_directive = text_directives.first().unwrap(); + assert_eq!( + text_directive + .prefix() + .as_ref() + .expect("There must be a prefix.") + .value(), + "prefix&", + "" + ); + assert_eq!( + text_directive + .start() + .as_ref() + .expect("There must be a prefix.") + .value(), + "start and,", + "" + ); + assert_eq!( + text_directive + .end() + .as_ref() + .expect("There must be a prefix.") + .value(), + "end#", + "" + ); + assert_eq!( + text_directive + .suffix() + .as_ref() + .expect("There must be a prefix.") + .value(), + "&suffix-", + "" + ); + } + + /// Ensures that a text fragment is created correctly, + /// based on a given combination of tokens. + /// This includes all sorts of combinations of + /// `prefix`, `suffix`, `start` and `end`, + /// als well as values for these tokens which contain + /// characters that need to be encoded because they are + /// identifiers for text fragments + /// (#`, `, `, `&`, `:`, `~` and `-`). + #[test] + fn test_create_fragment_directive() { + for (text_directive, expected_fragment_directive) in [ + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::new(), + String::new(), + ) + .unwrap(), + ":~:text=start", + ), + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::from("end"), + String::new(), + ) + .unwrap(), + ":~:text=start,end", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::from("end"), + String::new(), + ) + .unwrap(), + ":~:text=prefix-,start,end", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::from("end"), + String::from("suffix"), + ) + .unwrap(), + ":~:text=prefix-,start,end,-suffix", + ), + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::from("end"), + String::from("suffix"), + ) + .unwrap(), + ":~:text=start,end,-suffix", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::new(), + String::from("suffix"), + ) + .unwrap(), + ":~:text=prefix-,start,-suffix", + ), + ( + TextDirective::from_parts( + String::from("prefix-"), + String::from("start and,"), + String::from("&end"), + String::from("#:~:suffix"), + ) + .unwrap(), + ":~:text=prefix%2D-,start%20and%2C,%26end,-%23%3A%7E%3Asuffix", + ), + ] { + let fragment_directive = create_fragment_directive_string(&vec![text_directive]) + .expect("The given input must produce a valid fragment directive."); + assert_eq!(fragment_directive, expected_fragment_directive); + } + } + + /// Ensures that a fragment directive is created correctly if multiple text fragments are given. + /// The resulting fragment must start with `:~:` + /// and each text fragment must be separated using `&text=`. + #[test] + fn test_create_fragment_directive_from_multiple_text_directives() { + let text_directives = vec![ + TextDirective::from_parts( + String::new(), + String::from("start1"), + String::new(), + String::new(), + ) + .unwrap(), + TextDirective::from_parts( + String::new(), + String::from("start2"), + String::new(), + String::new(), + ) + .unwrap(), + TextDirective::from_parts( + String::new(), + String::from("start3"), + String::new(), + String::new(), + ) + .unwrap(), + ]; + let fragment_directive = create_fragment_directive_string(&text_directives) + .expect("The given input must produce a valid fragment directive."); + assert_eq!( + fragment_directive, ":~:text=start1&text=start2&text=start3", + "The created fragment directive is wrong for multiple fragments." + ); + } +} diff --git a/dom/base/moz.build b/dom/base/moz.build index ef1780f161..ffcfb0aaf6 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -194,6 +194,7 @@ EXPORTS.mozilla.dom += [ "External.h", "FilteredNodeIterator.h", "FormData.h", + "FragmentDirective.h", "FragmentOrElement.h", "FromParser.h", "GeneratedImageContent.h", @@ -237,6 +238,7 @@ EXPORTS.mozilla.dom += [ "PlacesBookmarkTitle.h", "PlacesBookmarkUrl.h", "PlacesEvent.h", + "PlacesEventCounts.h", "PlacesFavicon.h", "PlacesHistoryCleared.h", "PlacesObservers.h", @@ -296,6 +298,7 @@ if CONFIG["FUZZING"]: if CONFIG["COMPILE_ENVIRONMENT"]: EXPORTS.mozilla.dom += [ + "!fragmentdirectives_ffi_generated.h", "!GeneratedElementDocumentState.h", "RustTypes.h", ] @@ -305,6 +308,11 @@ if CONFIG["COMPILE_ENVIRONMENT"]: inputs=["rust"], ) + CbindgenHeader( + "fragmentdirectives_ffi_generated.h", + inputs=["fragmentdirectives"], + ) + UNIFIED_SOURCES += [ "!UseCounterMetrics.cpp", "AbstractRange.cpp", @@ -349,6 +357,7 @@ UNIFIED_SOURCES += [ "EventSourceEventService.cpp", "External.cpp", "FormData.cpp", + "FragmentDirective.cpp", "FragmentOrElement.cpp", "GeneratedImageContent.cpp", "GlobalTeardownObserver.cpp", @@ -489,6 +498,7 @@ if CONFIG["FUZZING"]: if CONFIG["MOZ_PLACES"]: UNIFIED_SOURCES += [ "PlacesEvent.cpp", + "PlacesEventCounts.cpp", "PlacesObservers.cpp", "PlacesWeakCallbackWrapper.cpp", ] diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index c6f1687f73..d2c863bd65 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -1767,6 +1767,17 @@ bool nsContentUtils::IsAlphanumericOrSymbol(uint32_t aChar) { cat == nsUGenCategory::kSymbol; } +// static +bool nsContentUtils::IsHyphen(uint32_t aChar) { + // Characters treated as hyphens for the purpose of "emergency" breaking + // when the content would otherwise overflow. + return aChar == uint32_t('-') || // HYPHEN-MINUS + aChar == 0x2010 || // HYPHEN + aChar == 0x2012 || // FIGURE DASH + aChar == 0x2013 || // EN DASH + aChar == 0x058A; // ARMENIAN HYPHEN +} + /* static */ bool nsContentUtils::IsHTMLWhitespace(char16_t aChar) { return aChar == char16_t(0x0009) || aChar == char16_t(0x000A) || @@ -3008,6 +3019,15 @@ nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorHelper( } /* static */ +nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorForSelection( + nsIContent* aContent1, nsIContent* aContent2) { + return GetCommonAncestorInternal( + aContent1, aContent2, [](nsIContent* aContent) { + return aContent->GetFlattenedTreeParentNodeForSelection(); + }); +} + +/* static */ Element* nsContentUtils::GetCommonFlattenedTreeAncestorForStyle( Element* aElement1, Element* aElement2) { return GetCommonAncestorInternal(aElement1, aElement2, [](Element* aElement) { @@ -11341,7 +11361,7 @@ int32_t nsContentUtils::CompareTreePosition(const nsINode* aNode1, MOZ_ASSERT(aNode1, "aNode1 must not be null"); MOZ_ASSERT(aNode2, "aNode2 must not be null"); - if (MOZ_UNLIKELY(NS_WARN_IF(aNode1 == aNode2))) { + if (NS_WARN_IF(aNode1 == aNode2)) { return 0; } @@ -11439,7 +11459,8 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost, bool aIsClonable, bool aDelegatesFocus) { RefPtr<Element> host = mozilla::dom::Element::FromNodeOrNull(aHost); - if (!host) { + if (!host || host->GetShadowRoot()) { + // https://html.spec.whatwg.org/#parsing-main-inhead:shadow-host return nullptr; } @@ -11449,9 +11470,10 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost, init.mSlotAssignment = SlotAssignmentMode::Named; init.mClonable = aIsClonable; - RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors(), - Element::ShadowRootDeclarative::Yes); + RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors()); if (shadowRoot) { + shadowRoot->SetIsDeclarative( + nsGenericHTMLFormControlElement::ShadowRootDeclarative::Yes); // https://html.spec.whatwg.org/#parsing-main-inhead:available-to-element-internals shadowRoot->SetAvailableToElementInternals(); } diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 338fc097de..4291d2c5d1 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -225,7 +225,6 @@ enum EventNameType { EventNameType_SVGSVG = 0x0008, // the svg element EventNameType_SMIL = 0x0010, // smil elements EventNameType_HTMLBodyOrFramesetOnly = 0x0020, - EventNameType_HTMLMarqueeOnly = 0x0040, EventNameType_HTMLXUL = 0x0003, EventNameType_All = 0xFFFF @@ -516,6 +515,13 @@ class nsContentUtils { } /** + * Returns the common flattened tree ancestor from the point of view of + * the selection system, if any, for two given content nodes. + */ + static nsIContent* GetCommonFlattenedTreeAncestorForSelection( + nsIContent* aContent1, nsIContent* aContent2); + + /** * Returns the common flattened tree ancestor from the point of view of the * style system, if any, for two given content nodes. */ @@ -781,6 +787,10 @@ class nsContentUtils { * Returns true if aChar is of class L*, N* or S* (for first-letter). */ static bool IsAlphanumericOrSymbol(uint32_t aChar); + /** + * Returns true if aChar is a kind of hyphen. + */ + static bool IsHyphen(uint32_t aChar); /* * Is the character an HTML whitespace character? diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp index 5a4cf78d65..4e2d604693 100644 --- a/dom/base/nsFocusManager.cpp +++ b/dom/base/nsFocusManager.cpp @@ -846,24 +846,27 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument, NS_ENSURE_ARG(aDocument); NS_ENSURE_ARG(aContent); - RefPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow(); - if (!window) { + nsPIDOMWindowOuter* windowPtr = aDocument->GetWindow(); + if (!windowPtr) { return NS_OK; } // if the content is currently focused in the window, or is an // shadow-including inclusive ancestor of the currently focused element, // reset the focus within that window. - RefPtr<Element> previousFocusedElement = window->GetFocusedElement(); - if (!previousFocusedElement) { + Element* previousFocusedElementPtr = windowPtr->GetFocusedElement(); + if (!previousFocusedElementPtr) { return NS_OK; } if (!nsContentUtils::ContentIsHostIncludingDescendantOf( - previousFocusedElement, aContent)) { + previousFocusedElementPtr, aContent)) { return NS_OK; } + RefPtr<nsPIDOMWindowOuter> window = windowPtr; + RefPtr<Element> previousFocusedElement = previousFocusedElementPtr; + RefPtr<Element> newFocusedElement = [&]() -> Element* { if (auto* sr = ShadowRoot::FromNode(aContent)) { if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) { @@ -1802,7 +1805,8 @@ Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent, // focus, update the node in the window, and raise the window if desired. if (allowFrameSwitch) { AdjustWindowFocus(newBrowsingContext, true, IsWindowVisible(newWindow), - actionId); + actionId, false /* aShouldClearAncestorFocus */, + nullptr /* aAncestorBrowsingContextToFocus */); } // set the focus node and method as needed @@ -1975,7 +1979,9 @@ mozilla::dom::BrowsingContext* nsFocusManager::GetCommonAncestor( bool nsFocusManager::AdjustInProcessWindowFocus( BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible, - uint64_t aActionId) { + uint64_t aActionId, bool aShouldClearAncestorFocus, + BrowsingContext* aAncestorBrowsingContextToFocus) { + MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus); if (ActionIdComparableAndLower(aActionId, mActionIdForFocusedBrowsingContextInContent)) { LOGFOCUS( @@ -2026,6 +2032,17 @@ bool nsFocusManager::AdjustInProcessWindowFocus( break; } + if (aShouldClearAncestorFocus) { + // This is the BrowsingContext that receives the focus, no need to clear + // its focused element and the rest of the ancestors. + if (window->GetBrowsingContext() == aAncestorBrowsingContextToFocus) { + break; + } + + window->SetFocusedElement(nullptr); + continue; + } + if (frameElement != window->GetFocusedElement()) { window->SetFocusedElement(frameElement); @@ -2041,18 +2058,22 @@ bool nsFocusManager::AdjustInProcessWindowFocus( return needToNotifyOtherProcess; } -void nsFocusManager::AdjustWindowFocus(BrowsingContext* aBrowsingContext, - bool aCheckPermission, bool aIsVisible, - uint64_t aActionId) { +void nsFocusManager::AdjustWindowFocus( + BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible, + uint64_t aActionId, bool aShouldClearAncestorFocus, + BrowsingContext* aAncestorBrowsingContextToFocus) { + MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus); if (AdjustInProcessWindowFocus(aBrowsingContext, aCheckPermission, aIsVisible, - aActionId)) { + aActionId, aShouldClearAncestorFocus, + aAncestorBrowsingContextToFocus)) { // Some ancestors of aBrowsingContext isn't in this process, so notify other // processes to adjust their focused element. mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); MOZ_ASSERT(contentChild); - contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, - aActionId); + contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, aActionId, + aShouldClearAncestorFocus, + aAncestorBrowsingContextToFocus); } } @@ -2423,6 +2444,22 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, if (ancestorWindowToFocus) { ancestorWindowToFocus->SetFocusedElement(nullptr, 0, true); } + + // When the focus of aBrowsingContextToClear is cleared, it should + // also clear its ancestors's focus because ancestors should no longer + // be considered aBrowsingContextToClear is focused. + // + // We don't need to do this when aBrowsingContextToClear and + // aAncestorBrowsingContextToFocus is equal because ancestors don't + // care about this. + if (aBrowsingContextToClear && + aBrowsingContextToClear != aAncestorBrowsingContextToFocus) { + AdjustWindowFocus( + aBrowsingContextToClear, false, + IsWindowVisible(aBrowsingContextToClear->GetDOMWindow()), aActionId, + true /* aShouldClearAncestorFocus */, + aAncestorBrowsingContextToFocus); + } } SetFocusedWindowInternal(nullptr, aActionId); @@ -2569,7 +2606,9 @@ void nsFocusManager::Focus( // focus can be traversed from the top level down to the newly focused // window. RefPtr<BrowsingContext> bc = aWindow->GetBrowsingContext(); - AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId); + AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId, + false /* aShouldClearAncestorFocus */, + nullptr /* aAncestorBrowsingContextToFocus */); } // indicate that the window has taken focus. diff --git a/dom/base/nsFocusManager.h b/dom/base/nsFocusManager.h index 4fb9d05e1c..9815ab9b98 100644 --- a/dom/base/nsFocusManager.h +++ b/dom/base/nsFocusManager.h @@ -349,17 +349,21 @@ class nsFocusManager final : public nsIFocusManager, nsPIDOMWindowOuter* aWindow, mozilla::dom::BrowsingContext* aContext); /** - * When aBrowsingContext is focused, adjust the ancestors of aBrowsingContext - * so that they also have their corresponding frames focused. Thus, one can - * start at the active top-level window and navigate down the currently - * focused elements for each frame in the tree to get to aBrowsingContext. + * When aBrowsingContext is focused or blurred, adjust the ancestors of + * aBrowsingContext so that they also have their corresponding frames focused + * or blurred. Thus, one can start at the active top-level window and navigate + * down the currently focused elements for each frame in the tree to get to + * aBrowsingContext. */ MOZ_CAN_RUN_SCRIPT bool AdjustInProcessWindowFocus( mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission, - bool aIsVisible, uint64_t aActionId); + bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus, + mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus); + MOZ_CAN_RUN_SCRIPT void AdjustWindowFocus( mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission, - bool aIsVisible, uint64_t aActionId); + bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus, + mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus); /** * Returns true if aWindow is visible. diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp index eca528f258..1e3fb93aa8 100644 --- a/dom/base/nsFrameLoader.cpp +++ b/dom/base/nsFrameLoader.cpp @@ -3388,7 +3388,8 @@ already_AddRefed<Promise> nsFrameLoader::PrintPreview( /* aListener = */ nullptr, docShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, rv); + [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, + nullptr, rv); if (NS_WARN_IF(rv.Failed())) { promise->MaybeReject(std::move(rv)); } diff --git a/dom/base/nsFrameLoaderOwner.cpp b/dom/base/nsFrameLoaderOwner.cpp index 03945975dd..0e67f723d2 100644 --- a/dom/base/nsFrameLoaderOwner.cpp +++ b/dom/base/nsFrameLoaderOwner.cpp @@ -81,13 +81,7 @@ nsFrameLoaderOwner::ShouldPreserveBrowsingContext( } } - // We will preserve our browsing context if either fission is enabled, or the - // `preserve_browsing_contexts` pref is active. - if (UseRemoteSubframes() || - StaticPrefs::fission_preserve_browsing_contexts()) { - return ChangeRemotenessContextType::PRESERVE; - } - return ChangeRemotenessContextType::DONT_PRESERVE; + return ChangeRemotenessContextType::PRESERVE; } void nsFrameLoaderOwner::ChangeRemotenessCommon( diff --git a/dom/base/nsFrameMessageManager.cpp b/dom/base/nsFrameMessageManager.cpp index da275095b1..4c110cc55e 100644 --- a/dom/base/nsFrameMessageManager.cpp +++ b/dom/base/nsFrameMessageManager.cpp @@ -506,7 +506,7 @@ void nsFrameMessageManager::SendSyncMessage(JSContext* aCx, "Should not have parent manager in content!"); AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING( - "nsFrameMessageManager::SendMessage", OTHER, aMessageName); + "nsFrameMessageManager::SendSyncMessage", OTHER, aMessageName); profiler_add_marker("SendSyncMessage", geckoprofiler::category::IPC, {}, FrameMessageMarker{}, aMessageName, true); @@ -1428,8 +1428,7 @@ class SameParentProcessMessageManagerCallback : public MessageManagerCallback { bool aRunInGlobalScope) override { auto* global = ContentProcessMessageManager::Get(); MOZ_ASSERT(!aRunInGlobalScope); - global->LoadScript(aURL); - return true; + return global && global->LoadScript(aURL); } nsresult DoSendAsyncMessage(const nsAString& aMessage, diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 5337e1588f..7dcd265ca4 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -3703,7 +3703,7 @@ bool nsGlobalWindowInner::Confirm(const nsAString& aMessage, } already_AddRefed<Promise> nsGlobalWindowInner::Fetch( - const RequestOrUSVString& aInput, const RequestInit& aInit, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { return FetchRequest(this, aInput, aInit, aCallerType, aRv); } @@ -3752,7 +3752,7 @@ Nullable<WindowProxyHolder> nsGlobalWindowInner::PrintPreview( /* aRemotePrintJob = */ nullptr, aListener, aDocShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, aError), + /* aPrintPreviewCallback = */ nullptr, nullptr, aError), aError, nullptr); } diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h index 15fbc4259f..215e362dad 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h @@ -120,7 +120,7 @@ class OwningExternalOrWindowProxy; class Promise; class PostMessageEvent; struct RequestInit; -class RequestOrUSVString; +class RequestOrUTF8String; class SharedWorker; class Selection; struct SizeToContentConstraints; @@ -671,7 +671,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, already_AddRefed<mozilla::dom::cache::CacheStorage> GetCaches( mozilla::ErrorResult& aRv); already_AddRefed<mozilla::dom::Promise> Fetch( - const mozilla::dom::RequestOrUSVString& aInput, + const mozilla::dom::RequestOrUTF8String& aInput, const mozilla::dom::RequestInit& aInit, mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aRv); MOZ_CAN_RUN_SCRIPT void Print(mozilla::ErrorResult& aError); diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index e28dcdb092..c678a0a941 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -2414,6 +2414,11 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, MOZ_RELEASE_ASSERT(newInnerWindow->mDoc == aDocument); + if (mBrowsingContext->IsTopContent()) { + net::CookieJarSettings::Cast(aDocument->CookieJarSettings()) + ->SetTopLevelWindowContextId(aDocument->InnerWindowID()); + } + newInnerWindow->RefreshReduceTimerPrecisionCallerType(); if (!aState) { @@ -3468,9 +3473,17 @@ nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) { aSize = CSSPixel::FromAppUnits(viewportSize); - if (StaticPrefs::dom_innerSize_rounded()) { - aSize.width = std::roundf(aSize.width); - aSize.height = std::roundf(aSize.height); + switch (StaticPrefs::dom_innerSize_rounding()) { + case 1: + aSize.width = std::roundf(aSize.width); + aSize.height = std::roundf(aSize.height); + break; + case 2: + aSize.width = std::truncf(aSize.width); + aSize.height = std::truncf(aSize.height); + break; + default: + break; } return NS_OK; @@ -4993,7 +5006,7 @@ void nsGlobalWindowOuter::PrintOuter(ErrorResult& aError) { const bool forPreview = !StaticPrefs::print_always_print_silent(); Print(nullptr, nullptr, nullptr, nullptr, IsPreview(forPreview), - IsForWindowDotPrint::Yes, nullptr, aError); + IsForWindowDotPrint::Yes, nullptr, nullptr, aError); #endif } @@ -5015,7 +5028,8 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( nsIPrintSettings* aPrintSettings, RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener* aListener, nsIDocShell* aDocShellToCloneInto, IsPreview aIsPreview, IsForWindowDotPrint aForWindowDotPrint, - PrintPreviewResolver&& aPrintPreviewCallback, ErrorResult& aError) { + PrintPreviewResolver&& aPrintPreviewCallback, + RefPtr<BrowsingContext>* aCachedBrowsingContext, ErrorResult& aError) { #ifdef NS_PRINTING nsCOMPtr<nsIPrintSettingsService> printSettingsService = do_GetService("@mozilla.org/gfx/printsettings-service;1"); @@ -5051,16 +5065,36 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( nsCOMPtr<nsIDocumentViewer> viewer; RefPtr<BrowsingContext> bc; bool hasPrintCallbacks = false; - if (docToPrint->IsStaticDocument()) { + bool wasStaticDocument = docToPrint->IsStaticDocument(); + bool usingCachedBrowsingContext = false; + if (aCachedBrowsingContext && *aCachedBrowsingContext) { + MOZ_ASSERT(!wasStaticDocument, + "Why pass in non-empty aCachedBrowsingContext if original " + "document is already static?"); + if (!wasStaticDocument) { + // The passed in document is not a static clone and the caller passed in a + // static clone to reuse, so swap it in. + docToPrint = (*aCachedBrowsingContext)->GetDocument(); + MOZ_ASSERT(docToPrint); + MOZ_ASSERT(docToPrint->IsStaticDocument()); + wasStaticDocument = true; + usingCachedBrowsingContext = true; + } + } + if (wasStaticDocument) { if (aForWindowDotPrint == IsForWindowDotPrint::Yes) { aError.ThrowNotSupportedError( "Calling print() from a print preview is unsupported, did you intend " "to call printPreview() instead?"); return nullptr; } - // We're already a print preview window, just reuse our browsing context / - // content viewer. - bc = sourceBC; + if (usingCachedBrowsingContext) { + bc = docToPrint->GetBrowsingContext(); + } else { + // We're already a print preview window, just reuse our browsing context / + // content viewer. + bc = sourceBC; + } nsCOMPtr<nsIDocShell> docShell = bc->GetDocShell(); if (!docShell) { aError.ThrowNotSupportedError("No docshell"); @@ -5102,6 +5136,10 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( if (NS_WARN_IF(aError.Failed())) { return nullptr; } + if (aCachedBrowsingContext) { + MOZ_ASSERT(!*aCachedBrowsingContext); + *aCachedBrowsingContext = bc; + } } if (!bc) { aError.ThrowNotAllowedError("No browsing context"); @@ -5157,6 +5195,24 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( "Content viewer didn't implement nsIWebBrowserPrint"); return nullptr; } + bool closeWindowAfterPrint; + if (wasStaticDocument) { + // Here the document was a static clone to begin with that this code did not + // create, so we should not clean it up. + // The exception is if we're using the passed-in aCachedBrowsingContext, in + // which case this is the second print with this static document clone that + // we created the first time through, and we are responsible for cleaning it + // up. + closeWindowAfterPrint = usingCachedBrowsingContext; + } else { + // In this case the document was not a static clone, so we made a static + // clone for printing purposes and must clean it up after the print is done. + // The exception is if aCachedBrowsingContext is non-NULL, meaning the + // caller is intending to print this document again, so we need to defer the + // cleanup until after the second print. + closeWindowAfterPrint = !aCachedBrowsingContext; + } + webBrowserPrint->SetCloseWindowAfterPrint(closeWindowAfterPrint); // For window.print(), we postpone making these calls until the round-trip to // the parent process (triggered by the OpenInternal call above) calls us @@ -5953,10 +6009,11 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) { if (!allowClose) { // We're blocking the close operation // report localized error msg in JS console - nsContentUtils::ReportToConsole( - nsIScriptError::warningFlag, "DOM Window"_ns, - mDoc, // Better name for the category? - nsContentUtils::eDOM_PROPERTIES, "WindowCloseBlockedWarning"); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "DOM Window"_ns, + mDoc, // Better name for the category? + nsContentUtils::eDOM_PROPERTIES, + "WindowCloseByScriptBlockedWarning"); return; } diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h index 8337a7353f..3c26344c3d 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -105,7 +105,6 @@ class PostMessageData; class PostMessageEvent; class PrintPreviewResultInfo; struct RequestInit; -class RequestOrUSVString; class Selection; struct SizeToContentConstraints; class SpeechSynthesis; @@ -581,7 +580,8 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, Print(nsIPrintSettings*, mozilla::layout::RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener*, nsIDocShell*, IsPreview, IsForWindowDotPrint, - PrintPreviewResolver&&, mozilla::ErrorResult&); + PrintPreviewResolver&&, RefPtr<mozilla::dom::BrowsingContext>*, + mozilla::ErrorResult&); mozilla::dom::Selection* GetSelectionOuter(); already_AddRefed<mozilla::dom::Selection> GetSelection() override; nsScreen* GetScreen(); diff --git a/dom/base/nsIContentInlines.h b/dom/base/nsIContentInlines.h index 89cc7b9c30..04e73b75e1 100644 --- a/dom/base/nsIContentInlines.h +++ b/dom/base/nsIContentInlines.h @@ -82,10 +82,27 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) { return parent; } - if (parentAsContent->GetShadowRoot()) { - // If it's not assigned to any slot it's not part of the flat tree, and thus - // we return null. - return content->GetAssignedSlot(); + // Use GetShadowRootForSelection for the selection case such that + // if the content is slotted into a UA shadow tree, use + // the parent of content as the flattened tree parent (instead of + // the slot element). + const nsINode* shadowRootForParent = + aType == nsINode::eForSelection + ? parentAsContent->GetShadowRootForSelection() + : parentAsContent->GetShadowRoot(); + + if (shadowRootForParent) { + // When aType is not nsINode::eForSelection, If it's not assigned to any + // slot it's not part of the flat tree, and thus we return null. + auto* assignedSlot = content->GetAssignedSlot(); + if (assignedSlot || aType != nsINode::eForSelection) { + return assignedSlot; + } + + MOZ_ASSERT(aType == nsINode::eForSelection); + // When aType is nsINode::eForSelection, we use the parent of the + // content even if it's not assigned to any slot. + return parent; } if (parentAsContent->IsInShadowTree()) { @@ -106,7 +123,7 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) { } inline nsINode* nsINode::GetFlattenedTreeParentNode() const { - return ::GetFlattenedTreeParentNode<nsINode::eNotForStyle>(this); + return ::GetFlattenedTreeParentNode<nsINode::eNormal>(this); } inline nsIContent* nsIContent::GetFlattenedTreeParent() const { @@ -127,6 +144,11 @@ inline nsINode* nsINode::GetFlattenedTreeParentNodeForStyle() const { return ::GetFlattenedTreeParentNode<nsINode::eForStyle>(this); } +inline nsIContent* nsINode::GetFlattenedTreeParentNodeForSelection() const { + nsINode* parent = ::GetFlattenedTreeParentNode<nsINode::eForSelection>(this); + return (parent && parent->IsContent()) ? parent->AsContent() : nullptr; +} + inline bool nsINode::NodeOrAncestorHasDirAuto() const { return AncestorHasDirAuto() || (IsElement() && AsElement()->HasDirAuto()); } diff --git a/dom/base/nsIEventSourceEventService.idl b/dom/base/nsIEventSourceEventService.idl index 6131424669..be96d58260 100644 --- a/dom/base/nsIEventSourceEventService.idl +++ b/dom/base/nsIEventSourceEventService.idl @@ -31,5 +31,5 @@ interface nsIEventSourceEventService : nsISupports [must_use] void removeListener(in unsigned long long aInnerWindowID, in nsIEventSourceEventListener aListener); - [must_use] bool hasListenerFor(in unsigned long long aInnerWindowID); + [must_use] boolean hasListenerFor(in unsigned long long aInnerWindowID); }; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index d5455e5596..6c77574df7 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -287,11 +287,17 @@ static const nsINode* GetClosestCommonInclusiveAncestorForRangeInSelection( const nsINode* aNode) { while (aNode && !aNode->IsClosestCommonInclusiveAncestorForRangeInSelection()) { + const bool isNodeInShadowTree = + StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aNode->IsInShadowTree(); if (!aNode - ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) { + ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection() && + !isNodeInShadowTree) { return nullptr; } - aNode = aNode->GetParentNode(); + aNode = StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() + ? aNode->GetParentOrShadowHostNode() + : aNode->GetParentNode(); } return aNode; } @@ -315,12 +321,12 @@ class IsItemInRangeComparator { int operator()(const AbstractRange* const aRange) const { int32_t cmp = nsContentUtils::ComparePoints_Deprecated( - &mNode, mEndOffset, aRange->GetStartContainer(), aRange->StartOffset(), - nullptr, mCache); + &mNode, mEndOffset, aRange->GetMayCrossShadowBoundaryStartContainer(), + aRange->MayCrossShadowBoundaryStartOffset(), nullptr, mCache); if (cmp == 1) { cmp = nsContentUtils::ComparePoints_Deprecated( - &mNode, mStartOffset, aRange->GetEndContainer(), aRange->EndOffset(), - nullptr, mCache); + &mNode, mStartOffset, aRange->GetMayCrossShadowBoundaryEndContainer(), + aRange->MayCrossShadowBoundaryEndOffset(), nullptr, mCache); if (cmp == -1) { return 0; } @@ -386,6 +392,18 @@ bool nsINode::IsSelected(const uint32_t aStartOffset, return true; } + if (range->MayCrossShadowBoundary()) { + MOZ_ASSERT(range->IsDynamicRange(), + "range->MayCrossShadowBoundary() can only return true for " + "dynamic range"); + StaticRange* crossBoundaryRange = + range->AsDynamicRange()->GetCrossShadowBoundaryRange(); + MOZ_ASSERT(crossBoundaryRange); + if (!crossBoundaryRange->Collapsed()) { + return true; + } + } + const AbstractRange* middlePlus1; const AbstractRange* middleMinus1; // if node end > start of middle+1, result = 1 @@ -552,7 +570,8 @@ static nsIContent* GetRootForContentSubtree(nsIContent* aContent) { return nsIContent::FromNode(aContent->SubtreeRoot()); } -nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { +nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell, + bool aAllowCrossShadowBoundary) { NS_ENSURE_TRUE(aPresShell, nullptr); if (IsDocument()) return AsDocument()->GetRootElement(); @@ -596,7 +615,7 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { } RefPtr<nsFrameSelection> fs = aPresShell->FrameSelection(); - nsIContent* content = fs->GetLimiter(); + nsCOMPtr<nsIContent> content = fs->GetLimiter(); if (!content) { content = fs->GetAncestorLimiter(); if (!content) { @@ -616,6 +635,10 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { // Use the host as the root. if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(content)) { content = shadowRoot->GetHost(); + if (content && aAllowCrossShadowBoundary) { + content = content->GetSelectionRootContent(aPresShell, + aAllowCrossShadowBoundary); + } } } @@ -3826,6 +3849,33 @@ void nsINode::FireNodeRemovedForChildren() { } } +ShadowRoot* nsINode::GetShadowRoot() const { + return IsContent() ? AsContent()->GetShadowRoot() : nullptr; +} + +ShadowRoot* nsINode::GetShadowRootForSelection() const { + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return nullptr; + } + + ShadowRoot* shadowRoot = GetShadowRoot(); + if (!shadowRoot) { + return nullptr; + } + + // ie. <details> and <video> + if (shadowRoot->IsUAWidget()) { + return nullptr; + } + + // ie. <use> element + if (IsElement() && !AsElement()->CanAttachShadowDOM()) { + return nullptr; + } + + return shadowRoot; +} + NS_IMPL_ISUPPORTS(nsNodeWeakReference, nsIWeakReference) nsNodeWeakReference::nsNodeWeakReference(nsINode* aNode) diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h index 3a47992cc8..d2a2fd008d 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h @@ -1099,7 +1099,7 @@ class nsINode : public mozilla::dom::EventTarget { : nullptr; } - enum FlattenedParentType { eNotForStyle, eForStyle }; + enum FlattenedParentType { eNormal, eForStyle, eForSelection }; /** * Returns the node that is the parent of this node in the flattened @@ -1121,6 +1121,15 @@ class nsINode : public mozilla::dom::EventTarget { */ inline nsINode* GetFlattenedTreeParentNodeForStyle() const; + /** + * Similar to GetFlattenedTreeParentNode, it does two things differently + * 1. For contents that are not in the flattened tree, use its + * parent rather than nullptr. + * 2. For contents that are slotted into a UA shadow tree, use its + * parent rather than the slot element. + */ + inline nsIContent* GetFlattenedTreeParentNodeForSelection() const; + inline mozilla::dom::Element* GetFlattenedTreeParentElement() const; inline mozilla::dom::Element* GetFlattenedTreeParentElementForStyle() const; @@ -1629,7 +1638,7 @@ class nsINode : public mozilla::dom::EventTarget { * not in same subtree, this returns the root content of the closeset subtree. */ MOZ_CAN_RUN_SCRIPT nsIContent* GetSelectionRootContent( - mozilla::PresShell* aPresShell); + mozilla::PresShell* aPresShell, bool aAllowCrossShadowBoundary = false); nsINodeList* ChildNodes(); @@ -2074,6 +2083,14 @@ class nsINode : public mozilla::dom::EventTarget { ClearBoolFlag(ElementCreatedFromPrototypeAndHasUnmodifiedL10n); } + mozilla::dom::ShadowRoot* GetShadowRoot() const; + + // Return the shadow root of the node if it is a shadow host and + // it meets the requirements for being a shadow host of a selection. + // For example, <details>, <video> and <use> elements are not valid + // shadow host for selection. + mozilla::dom::ShadowRoot* GetShadowRootForSelection() const; + protected: void SetParentIsContent(bool aValue) { SetBoolFlag(ParentIsContent, aValue); } void SetIsInDocument() { SetBoolFlag(IsInDocument); } diff --git a/dom/base/nsIScriptableContentIterator.idl b/dom/base/nsIScriptableContentIterator.idl index 370cd8c8a7..80208ac983 100644 --- a/dom/base/nsIScriptableContentIterator.idl +++ b/dom/base/nsIScriptableContentIterator.idl @@ -38,6 +38,11 @@ interface nsIScriptableContentIterator : nsISupports void initWithRange(in nsIScriptableContentIterator_IteratorType aType, in Range aRange); + // See ContentSubtreeIterator::InitWithAllowCrossShadowBoundary(nsRange*) + void initWithRangeAllowCrossShadowBoundary( + in nsIScriptableContentIterator_IteratorType aType, + in Range aRange); + // See ContentIteratorBase::Init(nsINode*, uint32_t, nsINode*, uint32_t) void initWithPositions(in nsIScriptableContentIterator_IteratorType aType, in Node aStartContainer, in unsigned long aStartOffset, @@ -59,7 +64,7 @@ interface nsIScriptableContentIterator : nsISupports readonly attribute Node currentNode; // See ContentIteratorBase::IsDone() - readonly attribute bool isDone; + readonly attribute boolean isDone; // See ContentIteratorBase::PositionAt(nsINode*) void positionAt(in Node aNode); diff --git a/dom/base/nsISelectionController.idl b/dom/base/nsISelectionController.idl index f2d5ebe65f..9c76e45f43 100644 --- a/dom/base/nsISelectionController.idl +++ b/dom/base/nsISelectionController.idl @@ -41,11 +41,12 @@ interface nsISelectionController : nsISelectionDisplay const short SELECTION_FIND = 8; const short SELECTION_URLSECONDARY = 9; const short SELECTION_URLSTRIKEOUT = 10; + const short SELECTION_TARGET_TEXT = 11; // Custom Highlight API // (see https://drafts.csswg.org/css-highlight-api-1/#enumdef-highlighttype) - const short SELECTION_HIGHLIGHT = 11; + const short SELECTION_HIGHLIGHT = 12; // End of RawSelectionType values. - const short NUM_SELECTIONTYPES = 12; + const short NUM_SELECTIONTYPES = 13; // SelectionRegion values: const short SELECTION_ANCHOR_REGION = 0; @@ -311,6 +312,7 @@ enum class SelectionType : RawSelectionType eFind = nsISelectionController::SELECTION_FIND, eURLSecondary = nsISelectionController::SELECTION_URLSECONDARY, eURLStrikeout = nsISelectionController::SELECTION_URLSTRIKEOUT, + eTargetText = nsISelectionController::SELECTION_TARGET_TEXT, eHighlight = nsISelectionController::SELECTION_HIGHLIGHT, }; @@ -327,6 +329,7 @@ static const SelectionType kPresentSelectionTypes[] = { SelectionType::eFind, SelectionType::eURLSecondary, SelectionType::eURLStrikeout, + SelectionType::eTargetText, SelectionType::eHighlight, }; diff --git a/dom/base/nsJSEnvironment.cpp b/dom/base/nsJSEnvironment.cpp index 1397bd25b5..a14a22bcf0 100644 --- a/dom/base/nsJSEnvironment.cpp +++ b/dom/base/nsJSEnvironment.cpp @@ -1707,38 +1707,38 @@ void nsJSContext::MaybeRunNextCollectorSlice(nsIDocShell* aDocShell, return; } - if (!sScheduler->IsUserActive()) { - if (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles()) { - Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint(); - if (next.isSome()) { - // Try to not delay the next RefreshDriver tick, so give a reasonable - // deadline for collectors. - sScheduler->RunNextCollectorTimer(aReason, next.value()); - } - } else { - nsCOMPtr<nsIDocShell> shell = aDocShell; - NS_DispatchToCurrentThreadQueue( - NS_NewRunnableFunction( - "nsJSContext::MaybeRunNextCollectorSlice", - [shell] { - nsIDocShell::BusyFlags busyFlags = nsIDocShell::BUSY_FLAGS_NONE; - shell->GetBusyFlags(&busyFlags); - if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { - return; - } - - // In order to improve performance on the next page, run a minor - // GC. The 16ms limit ensures it isn't called all the time if - // there are for example multiple iframes loading at the same - // time. - JS::RunNurseryCollection( - CycleCollectedJSRuntime::Get()->Runtime(), - JS::GCReason::PREPARE_FOR_PAGELOAD, - mozilla::TimeDuration::FromMilliseconds(16)); - }), - EventQueuePriority::Idle); + if (!sScheduler->IsUserActive() && + (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles())) { + Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint(); + if (next.isSome()) { + // Try to not delay the next RefreshDriver tick, so give a reasonable + // deadline for collectors. + sScheduler->RunNextCollectorTimer(aReason, next.value()); } } + + nsCOMPtr<nsIDocShell> shell = aDocShell; + NS_DispatchToCurrentThreadQueue( + NS_NewRunnableFunction("nsJSContext::MaybeRunNextCollectorSlice", + [shell] { + nsIDocShell::BusyFlags busyFlags = + nsIDocShell::BUSY_FLAGS_NONE; + shell->GetBusyFlags(&busyFlags); + if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { + return; + } + + // In order to improve performance on the next + // page, run a minor GC. The 16ms limit ensures + // it isn't called all the time if there are for + // example multiple iframes loading at the same + // time. + JS::RunNurseryCollection( + CycleCollectedJSRuntime::Get()->Runtime(), + JS::GCReason::PREPARE_FOR_PAGELOAD, + mozilla::TimeDuration::FromMilliseconds(16)); + }), + EventQueuePriority::Idle); } // static @@ -2121,6 +2121,13 @@ void nsJSContext::EnsureStatics() { "javascript.options.mem.gc_compacting", (void*)JSGC_COMPACTING_ENABLED); +#ifdef NIGHTLY_BUILD + Preferences::RegisterCallbackAndCall( + SetMemoryPrefChangedCallbackBool, + "javascript.options.mem.gc_experimental_semispace_nursery", + (void*)JSGC_SEMISPACE_NURSERY_ENABLED); +#endif + Preferences::RegisterCallbackAndCall( SetMemoryPrefChangedCallbackBool, "javascript.options.mem.gc_parallel_marking", diff --git a/dom/base/nsMimeTypeArray.cpp b/dom/base/nsMimeTypeArray.cpp index 4d15e1d8a2..376c7e1065 100644 --- a/dom/base/nsMimeTypeArray.cpp +++ b/dom/base/nsMimeTypeArray.cpp @@ -94,3 +94,7 @@ JSObject* nsMimeType::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return MimeType_Binding::Wrap(aCx, this, aGivenProto); } + +already_AddRefed<nsPluginElement> nsMimeType::EnabledPlugin() const { + return do_AddRef(mPluginElement); +} diff --git a/dom/base/nsMimeTypeArray.h b/dom/base/nsMimeTypeArray.h index aaeedba3ae..207f5dc542 100644 --- a/dom/base/nsMimeTypeArray.h +++ b/dom/base/nsMimeTypeArray.h @@ -83,9 +83,7 @@ class nsMimeType final : public nsWrapperCache { retval.SetKnownLiveString(kMimeDescription); } - already_AddRefed<nsPluginElement> EnabledPlugin() const { - return do_AddRef(mPluginElement); - } + already_AddRefed<nsPluginElement> EnabledPlugin() const; void GetSuffixes(mozilla::dom::DOMString& retval) const { retval.SetKnownLiveString(kMimeSuffix); diff --git a/dom/base/nsRange.cpp b/dom/base/nsRange.cpp index 3121ed86c3..cf15f239c5 100644 --- a/dom/base/nsRange.cpp +++ b/dom/base/nsRange.cpp @@ -105,16 +105,30 @@ template nsresult nsRange::SetStartAndEnd( template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); + +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RawRangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RawRangeBoundary& aStartBoundary, + const RawRangeBoundary& aEndBoundary); JSObject* nsRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { @@ -171,7 +185,7 @@ nsRange::nsRange(nsINode* aNode) mNextEndRef(nullptr) { // printf("Size of nsRange: %zu\n", sizeof(nsRange)); - static_assert(sizeof(nsRange) <= 240, + static_assert(sizeof(nsRange) <= 248, "nsRange size shouldn't be increased as far as possible"); } @@ -201,6 +215,109 @@ already_AddRefed<nsRange> nsRange::Create( return range.forget(); } +/* + * When a new boundary is given to a nsRange, compare its position with other + * existing boundaries to see if we need to collapse the end points. + * + * aRange: The nsRange that aNewBoundary is being set to. + * aNewRoot: The shadow-including root of the container of aNewBoundary + * aNewBoundary: The new boundary + * aIsSetStart: true if ShouldCollapseBoundary is called by nsRange::SetStart, + * false otherwise + * aAllowCrossShadowBoundary: Indicates whether the boundaries allowed to cross + * shadow boundary or not + */ +static CollapsePolicy ShouldCollapseBoundary( + const nsRange* aRange, const nsINode* aNewRoot, + const RawRangeBoundary& aNewBoundary, const bool aIsSetStart, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { + if (!aRange->IsPositioned()) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + MOZ_ASSERT(aRange->GetRoot()); + if (aNewRoot != aRange->GetRoot()) { + // Boundaries are in different document (or not connected), so collapse + // the both the default range and the crossBoundaryRange range. + if (aNewRoot->GetComposedDoc() != aRange->GetRoot()->GetComposedDoc()) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + // Always collapse both ranges if the one of the roots is an UA widget + // regardless whether the boundaries are allowed to cross shadow boundary + // or not. + if (AbstractRange::IsRootUAWidget(aNewRoot) || + AbstractRange::IsRootUAWidget(aRange->GetRoot())) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + // Different root, but same document. So we only collapse the + // default range if boundaries are allowed to cross shadow boundary. + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? CollapsePolicy::DefaultRange + : CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + const RangeBoundary& otherSideExistingBoundary = + aIsSetStart ? aRange->EndRef() : aRange->StartRef(); + + // Both bondaries are in the same root, now check for their position + const Maybe<int32_t> order = + aIsSetStart ? nsContentUtils::ComparePoints(aNewBoundary, + otherSideExistingBoundary) + : nsContentUtils::ComparePoints(otherSideExistingBoundary, + aNewBoundary); + + if (order) { + if (*order != 1) { + // aNewBoundary is at a valid position. + // + // If aIsSetStart is true, this means + // aNewBoundary <= otherSideExistingBoundary which is + // good because aNewBoundary intends to be the start. + // + // If aIsSetStart is false, this means + // otherSideExistingBoundary <= aNewBoundary which is good because + // aNewBoundary intends to be the end. + // + // So no collapse for above cases. + return CollapsePolicy::No; + } + + if (!aRange->MayCrossShadowBoundary() || + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::No) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + const RangeBoundary& otherSideExistingCrossShadowBoundaryBoundary = + aIsSetStart ? aRange->MayCrossShadowBoundaryEndRef() + : aRange->MayCrossShadowBoundaryStartRef(); + + // Please see the comment for (*order != 1) to see what "valid" means. + // + // We reach to this line when (*order == 1), it means aNewBoundary is + // at an invalid position, so we need to collapse aNewBoundary with + // otherSideExistingBoundary. However, it's possible that aNewBoundary + // is valid with the otherSideExistingCrossShadowBoundaryBoundary. + const Maybe<int32_t> withCrossShadowBoundaryOrder = + aIsSetStart + ? nsContentUtils::ComparePoints( + aNewBoundary, otherSideExistingCrossShadowBoundaryBoundary) + : nsContentUtils::ComparePoints( + otherSideExistingCrossShadowBoundaryBoundary, aNewBoundary); + + // Valid to the cross boundary boundary. + if (withCrossShadowBoundaryOrder && *withCrossShadowBoundaryOrder != 1) { + return CollapsePolicy::DefaultRange; + } + + // Not valid to both existing boundaries. + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + MOZ_ASSERT_UNREACHABLE(); + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; +} /****************************************************** * nsISupports ******************************************************/ @@ -219,11 +336,13 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange) // `Reset()` unlinks `mStart`, `mEnd` and `mRoot`. + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrossShadowBoundaryRange); tmp->Reset(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrossShadowBoundaryRange); NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange) @@ -231,6 +350,7 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_END bool nsRange::MaybeInterruptLastRelease() { bool interrupt = AbstractRange::MaybeCacheToReuse(*this); + ResetCrossShadowBoundaryRange(); MOZ_ASSERT(!interrupt || IsCleared()); return interrupt; } @@ -573,6 +693,21 @@ void nsRange::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) { nsINode* startContainer = mStart.Container(); nsINode* endContainer = mEnd.Container(); + // FIXME(sefeng): Temporary Solution for ContentRemoved + // editing/crashtests/removeformat-from-DOMNodeRemoved.html can be used to + // verify this. + if (mCrossShadowBoundaryRange) { + if (mCrossShadowBoundaryRange->GetStartContainer() == aChild || + mCrossShadowBoundaryRange->GetEndContainer() == aChild) { + ResetCrossShadowBoundaryRange(); + } else if (ShadowRoot* shadowRoot = aChild->GetShadowRoot()) { + if (mCrossShadowBoundaryRange->GetStartContainer() == shadowRoot || + mCrossShadowBoundaryRange->GetEndContainer() == shadowRoot) { + ResetCrossShadowBoundaryRange(); + } + } + } + RawRangeBoundary newStart; RawRangeBoundary newEnd; Maybe<bool> gravitateStart; @@ -881,10 +1016,12 @@ void nsRange::AssertIfMismatchRootAndRangeBoundaries( // Calling DoSetRange with either parent argument null will collapse // the range to have both endpoints point to the other node template <typename SPT, typename SRT, typename EPT, typename ERT> -void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, - const RangeBoundaryBase<EPT, ERT>& aEndBoundary, - nsINode* aRootNode, - bool aNotInsertedYet /* = false */) { +void nsRange::DoSetRange( + const RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const RangeBoundaryBase<EPT, ERT>& aEndBoundary, nsINode* aRootNode, + bool aNotInsertedYet /* = false */, + CollapsePolicy + aCollapsePolicy /* = DEFAULT_RANGE_AND_CROSS_BOUNDARY_RANGES */) { mIsPositioned = aStartBoundary.IsSetAndValid() && aEndBoundary.IsSetAndValid() && aRootNode; MOZ_ASSERT_IF(!mIsPositioned, !aStartBoundary.IsSet()); @@ -913,6 +1050,11 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, mStart.CopyFrom(aStartBoundary, RangeBoundaryIsMutationObserved::Yes); mEnd.CopyFrom(aEndBoundary, RangeBoundaryIsMutationObserved::Yes); + if (aCollapsePolicy == + CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges) { + ResetCrossShadowBoundaryRange(); + } + if (checkCommonAncestor) { UpdateCommonAncestorIfNecessary(); } @@ -965,17 +1107,21 @@ bool nsRange::CanAccess(const nsINode& aNode) const { return nsContentUtils::CanCallerAccess(&aNode); } -void nsRange::SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) { +void nsRange::SetStart( + nsINode& aNode, uint32_t aOffset, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } AutoInvalidateSelection atEndOfBlock(this); - SetStart(RawRangeBoundary(&aNode, aOffset), aRv); + SetStart(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary); } -void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) { +void nsRange::SetStart( + const RawRangeBoundary& aPoint, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container()); if (!newRoot) { aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR); @@ -987,28 +1133,46 @@ void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) { return; } - // Collapse if not positioned yet, if positioned in another doc or - // if the new start is after end. - const bool collapse = [&]() { - if (!mIsPositioned || (newRoot != mRoot)) { - return true; - } - - const Maybe<int32_t> order = nsContentUtils::ComparePoints(aPoint, mEnd); - if (order) { - return *order == 1; - } - - MOZ_ASSERT_UNREACHABLE(); - return true; - }(); + CollapsePolicy policy = + ShouldCollapseBoundary(this, newRoot, aPoint, true /* aIsSetStart= */, + aAllowCrossShadowBoundary); - if (collapse) { - DoSetRange(aPoint, aPoint, newRoot); - return; + switch (policy) { + case CollapsePolicy::No: + // EndRef(..) may be same as mStart or not, depends on + // the value of mCrossShadowBoundaryRange->mEnd, We need to update + // mCrossShadowBoundaryRange and the default boundaries separately + if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + if (MayCrossShadowBoundaryEndRef() != mEnd) { + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + aPoint, MayCrossShadowBoundaryEndRef()); + } else { + // The normal range is good enough for this case, just use that. + ResetCrossShadowBoundaryRange(); + } + } + DoSetRange(aPoint, mEnd, mRoot, false, policy); + break; + case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges: + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + case CollapsePolicy::DefaultRange: + MOZ_ASSERT(aAllowCrossShadowBoundary == + AllowRangeCrossShadowBoundary::Yes); + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + aPoint, MayCrossShadowBoundaryEndRef()); + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + default: + MOZ_ASSERT_UNREACHABLE(); } +} - DoSetRange(aPoint, mEnd, mRoot); +void nsRange::SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr) { + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; + SetStart(aNode, aOffset, aErr, AllowRangeCrossShadowBoundary::Yes); } void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) { @@ -1017,7 +1181,9 @@ void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) { SetStartBefore(aNode, aErr); } -void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) { +void nsRange::SetStartBefore( + nsINode& aNode, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; @@ -1027,7 +1193,8 @@ void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) { // If the node is being removed from its parent, GetRawRangeBoundaryBefore() // returns unset instance. Then, SetStart() will throw // NS_ERROR_DOM_INVALID_NODE_TYPE_ERR. - SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv); + SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv, + aAllowCrossShadowBoundary); } void nsRange::SetStartAfterJS(nsINode& aNode, ErrorResult& aErr) { @@ -1055,16 +1222,18 @@ void nsRange::SetEndJS(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr) { SetEnd(aNode, aOffset, aErr); } -void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) { +void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } AutoInvalidateSelection atEndOfBlock(this); - SetEnd(RawRangeBoundary(&aNode, aOffset), aRv); + SetEnd(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary); } -void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) { +void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container()); if (!newRoot) { aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR); @@ -1076,28 +1245,47 @@ void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) { return; } - // Collapse if not positioned yet, if positioned in another doc or - // if the new end is before start. - const bool collapse = [&]() { - if (!mIsPositioned || (newRoot != mRoot)) { - return true; - } + CollapsePolicy policy = + ShouldCollapseBoundary(this, newRoot, aPoint, false /* aIsStartStart */, + aAllowCrossShadowBoundary); - const Maybe<int32_t> order = nsContentUtils::ComparePoints(mStart, aPoint); - if (order) { - return *order == 1; - } - - MOZ_ASSERT_UNREACHABLE(); - return true; - }(); - - if (collapse) { - DoSetRange(aPoint, aPoint, newRoot); - return; + switch (policy) { + case CollapsePolicy::No: + // StartRef(..) may be same as mStart or not, depends on + // the value of mCrossShadowBoundaryRange->mStart, so we need to update + // mCrossShadowBoundaryRange and the default boundaries separately + if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + if (MayCrossShadowBoundaryStartRef() != mStart) { + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + MayCrossShadowBoundaryStartRef(), aPoint); + } else { + // The normal range is good enough for this case, just use that. + ResetCrossShadowBoundaryRange(); + } + } + DoSetRange(mStart, aPoint, mRoot, false, policy); + break; + case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges: + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + case CollapsePolicy::DefaultRange: + MOZ_ASSERT(aAllowCrossShadowBoundary == + AllowRangeCrossShadowBoundary::Yes); + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + MayCrossShadowBoundaryStartRef(), aPoint); + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + default: + MOZ_ASSERT_UNREACHABLE(); } +} - DoSetRange(mStart, aPoint, mRoot); +void nsRange::SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr) { + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; + SetEnd(aNode, aOffset, aErr, + AllowRangeCrossShadowBoundary::Yes /* aAllowCrossShadowBoundary */); } void nsRange::SelectNodesInContainer(nsINode* aContainer, @@ -1127,7 +1315,9 @@ void nsRange::SetEndBeforeJS(nsINode& aNode, ErrorResult& aErr) { SetEndBefore(aNode, aErr); } -void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) { +void nsRange::SetEndBefore( + nsINode& aNode, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; @@ -1137,7 +1327,8 @@ void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) { // If the node is being removed from its parent, GetRawRangeBoundaryBefore() // returns unset instance. Then, SetEnd() will throw // NS_ERROR_DOM_INVALID_NODE_TYPE_ERR. - SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv); + SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv, + aAllowCrossShadowBoundary); } void nsRange::SetEndAfterJS(nsINode& aNode, ErrorResult& aErr) { @@ -2201,6 +2392,11 @@ already_AddRefed<DocumentFragment> nsRange::CloneContents(ErrorResult& aRv) { already_AddRefed<nsRange> nsRange::CloneRange() const { RefPtr<nsRange> range = nsRange::Create(mOwner); range->DoSetRange(mStart, mEnd, mRoot); + if (mCrossShadowBoundaryRange) { + range->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + mCrossShadowBoundaryRange->StartRef(), + mCrossShadowBoundaryRange->EndRef()); + } return range.forget(); } @@ -2640,13 +2836,14 @@ static nsresult GetPartialTextRect(RectCallback* aCallback, static void CollectClientRectsForSubtree( nsINode* aNode, RectCallback* aCollector, Sequence<nsString>* aTextList, nsINode* aStartContainer, uint32_t aStartOffset, nsINode* aEndContainer, - uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout) { + uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout, bool aTextOnly) { auto* content = nsIContent::FromNode(aNode); if (!content) { return; } - if (content->IsText()) { + const bool isText = content->IsText(); + if (isText) { if (aNode == aStartContainer) { int32_t offset = aStartContainer == aEndContainer ? static_cast<int32_t>(aEndOffset) @@ -2665,19 +2862,30 @@ static void CollectClientRectsForSubtree( } } - if (aNode->IsElement() && aNode->AsElement()->IsDisplayContents()) { - FlattenedChildIterator childIter(content); - - for (nsIContent* child = childIter.GetNextChild(); child; - child = childIter.GetNextChild()) { - CollectClientRectsForSubtree(child, aCollector, aTextList, - aStartContainer, aStartOffset, aEndContainer, - aEndOffset, aClampToEdge, aFlushLayout); + if (nsIFrame* frame = content->GetPrimaryFrame()) { + if (!aTextOnly || isText) { + nsLayoutUtils::GetAllInFlowRectsAndTexts( + frame, nsLayoutUtils::GetContainingBlockForClientRect(frame), + aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + if (isText) { + return; + } + aTextOnly = true; + // We just get the text when calling GetAllInFlowRectsAndTexts, so we + // don't need to call it again when visiting the children. + aTextList = nullptr; } - } else if (nsIFrame* frame = content->GetPrimaryFrame()) { - nsLayoutUtils::GetAllInFlowRectsAndTexts( - frame, nsLayoutUtils::GetContainingBlockForClientRect(frame), - aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + } else if (!content->IsElement() || + !content->AsElement()->IsDisplayContents()) { + return; + } + + FlattenedChildIterator childIter(content); + for (nsIContent* child = childIter.GetNextChild(); child; + child = childIter.GetNextChild()) { + CollectClientRectsForSubtree(child, aCollector, aTextList, aStartContainer, + aStartOffset, aEndContainer, aEndOffset, + aClampToEdge, aFlushLayout, aTextOnly); } } @@ -2745,7 +2953,7 @@ void nsRange::CollectClientRectsAndText( CollectClientRectsForSubtree(node, aCollector, aTextList, aStartContainer, aStartOffset, aEndContainer, aEndOffset, - aClampToEdge, aFlushLayout); + aClampToEdge, aFlushLayout, false); } while (!iter.IsDone()); } @@ -2994,7 +3202,7 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) { // This is the initial range and all its nodes until now are // non-selectable so just trim them from the start. IgnoredErrorResult err; - range->SetStartBefore(*node, err); + range->SetStartBefore(*node, err, AllowRangeCrossShadowBoundary::Yes); if (err.Failed()) { return; } @@ -3008,7 +3216,8 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) { // Truncate the current range before the first non-selectable node. IgnoredErrorResult err; - range->SetEndBefore(*firstNonSelectableContent, err); + range->SetEndBefore(*firstNonSelectableContent, err, + AllowRangeCrossShadowBoundary::Yes); // Store it in the result (strong ref) - do this before creating // a new range in |newRange| below so we don't drop the last ref @@ -3252,3 +3461,52 @@ void nsRange::GetInnerTextNoFlush(DOMString& aValue, ErrorResult& aError, // Do not flush trailing line breaks! Required breaks at the end of the text // are suppressed. } + +template <typename SPT, typename SRT, typename EPT, typename ERT> +void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary) { + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return; + } + + MOZ_ASSERT(aStartBoundary.IsSetAndValid() && aEndBoundary.IsSetAndValid()); + + nsINode* startNode = aStartBoundary.Container(); + nsINode* endNode = aEndBoundary.Container(); + + if (!startNode && !endNode) { + ResetCrossShadowBoundaryRange(); + return; + } + + auto CanBecomeCrossShadowBoundaryPoint = [](nsINode* aContainer) -> bool { + if (!aContainer) { + return true; + } + + // Unlike normal ranges, shadow cross ranges don't work + // when the nodes aren't in document. + if (!aContainer->IsInComposedDoc()) { + return false; + } + + // AbstractRange::GetClosestCommonInclusiveAncestor only supports + // Document and Content nodes. + return aContainer->IsDocument() || aContainer->IsContent(); + }; + + if (!CanBecomeCrossShadowBoundaryPoint(startNode) || + !CanBecomeCrossShadowBoundaryPoint(endNode)) { + ResetCrossShadowBoundaryRange(); + return; + } + + if (!mCrossShadowBoundaryRange) { + mCrossShadowBoundaryRange = + StaticRange::Create(aStartBoundary, aEndBoundary, IgnoreErrors()); + return; + } + + mCrossShadowBoundaryRange->SetStartAndEnd(aStartBoundary, aEndBoundary); +} diff --git a/dom/base/nsRange.h b/dom/base/nsRange.h index 97756d3afc..94459087cb 100644 --- a/dom/base/nsRange.h +++ b/dom/base/nsRange.h @@ -13,6 +13,7 @@ #include "nsCOMPtr.h" #include "mozilla/dom/AbstractRange.h" +#include "mozilla/dom/StaticRange.h" #include "prmon.h" #include "nsStubMutationObserver.h" #include "nsWrapperCache.h" @@ -31,6 +32,13 @@ class DOMRect; class DOMRectList; class InspectorFontFace; class Selection; + +enum class CollapsePolicy : uint8_t { + No, // Don't need to collapse + DefaultRange, // Collapse the default range + DefaultRangeAndCrossShadowBoundaryRanges // Collapse both the default range + // and the cross boundary range +}; } // namespace dom } // namespace mozilla @@ -43,6 +51,8 @@ class nsRange final : public mozilla::dom::AbstractRange, using DOMRectList = mozilla::dom::DOMRectList; using RangeBoundary = mozilla::RangeBoundary; using RawRangeBoundary = mozilla::RawRangeBoundary; + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; virtual ~nsRange(); explicit nsRange(nsINode* aNode); @@ -111,14 +121,20 @@ class nsRange final : public mozilla::dom::AbstractRange, * When you set both start and end of a range, you should use * SetStartAndEnd() instead. */ - nsresult SetStart(nsINode* aContainer, uint32_t aOffset) { + nsresult SetStart(nsINode* aContainer, uint32_t aOffset, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) { ErrorResult error; - SetStart(RawRangeBoundary(aContainer, aOffset), error); + SetStart(RawRangeBoundary(aContainer, aOffset), error, + aAllowCrossShadowBoundary); return error.StealNSResult(); } - nsresult SetEnd(nsINode* aContainer, uint32_t aOffset) { + nsresult SetEnd(nsINode* aContainer, uint32_t aOffset, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) { ErrorResult error; - SetEnd(RawRangeBoundary(aContainer, aOffset), error); + SetEnd(RawRangeBoundary(aContainer, aOffset), error, + aAllowCrossShadowBoundary); return error.StealNSResult(); } @@ -224,6 +240,11 @@ class nsRange final : public mozilla::dom::AbstractRange, void SetStartAfterJS(nsINode& aNode, ErrorResult& aErr); void SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr); + void SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr); + void SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr); + void SurroundContents(nsINode& aNode, ErrorResult& aErr); already_AddRefed<DOMRect> GetBoundingClientRect(bool aClampToEdge = true, bool aFlushLayout = true); @@ -235,14 +256,26 @@ class nsRange final : public mozilla::dom::AbstractRange, // Following methods should be used for internal use instead of *JS(). void SelectNode(nsINode& aNode, ErrorResult& aErr); void SelectNodeContents(nsINode& aNode, ErrorResult& aErr); - void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr); - void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr); + void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void SetEndAfter(nsINode& aNode, ErrorResult& aErr); - void SetEndBefore(nsINode& aNode, ErrorResult& aErr); - void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr); - void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr); + void SetEndBefore(nsINode& aNode, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void SetStartAfter(nsINode& aNode, ErrorResult& aErr); - void SetStartBefore(nsINode& aNode, ErrorResult& aErr); + void SetStartBefore(nsINode& aNode, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void Collapse(bool aToStart); static void GetInnerTextNoFlush(mozilla::dom::DOMString& aValue, @@ -351,6 +384,82 @@ class nsRange final : public mozilla::dom::AbstractRange, */ nsINode* GetRegisteredClosestCommonInclusiveAncestor(); + template <typename SPT, typename SRT, typename EPT, typename ERT> + void CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary); + + void ResetCrossShadowBoundaryRange() { mCrossShadowBoundaryRange = nullptr; } + +#ifdef DEBUG + bool CrossShadowBoundaryRangeCollapsed() const { + MOZ_ASSERT(mCrossShadowBoundaryRange); + + return !mCrossShadowBoundaryRange->IsPositioned() || + (mCrossShadowBoundaryRange->GetStartContainer() == + mCrossShadowBoundaryRange->GetEndContainer() && + mCrossShadowBoundaryRange->StartOffset() == + mCrossShadowBoundaryRange->EndOffset()); + } +#endif + + /* + * The methods marked with MayCrossShadowBoundary[..] additionally check for + * the existence of mCrossShadowBoundaryRange, which indicates a range that + * crosses a shadow DOM boundary (i.e. mStart and mEnd are in different + * trees). If the caller can guarantee that this does not happen, there are + * additional variants of these methods named without MayCrossShadowBoundary, + * which provide a slightly faster implementation. + * */ + + nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetChildAtStartOffset() + : mStart.GetChildAtOffset(); + } + + nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetChildAtEndOffset() + : mEnd.GetChildAtOffset(); + } + + mozilla::dom::StaticRange* GetCrossShadowBoundaryRange() const { + return mCrossShadowBoundaryRange; + } + + nsINode* GetMayCrossShadowBoundaryStartContainer() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetStartContainer() + : mStart.Container(); + } + + nsINode* GetMayCrossShadowBoundaryEndContainer() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetEndContainer() + : mEnd.Container(); + } + + uint32_t MayCrossShadowBoundaryStartOffset() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartOffset() + : StartOffset(); + } + + uint32_t MayCrossShadowBoundaryEndOffset() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndOffset() + : EndOffset(); + } + + const RangeBoundary& MayCrossShadowBoundaryStartRef() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartRef() + : StartRef(); + } + + const RangeBoundary& MayCrossShadowBoundaryEndRef() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndRef() + : EndRef(); + } + protected: /** * DoSetRange() is called when `AbstractRange::SetStartAndEndInternal()` sets @@ -372,7 +481,9 @@ class nsRange final : public mozilla::dom::AbstractRange, MOZ_CAN_RUN_SCRIPT_BOUNDARY void DoSetRange( const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet = false); + nsINode* aRootNode, bool aNotInsertedYet = false, + mozilla::dom::CollapsePolicy aCollapsePolicy = mozilla::dom:: + CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges); // Assume that this is guaranteed that this is held by the caller when // this is used. (Note that we cannot use AutoRestore for mCalledByJS @@ -424,6 +535,22 @@ class nsRange final : public mozilla::dom::AbstractRange, static nsTArray<RefPtr<nsRange>>* sCachedRanges; + // Used to keep track of the real start and end for a + // selection where the start and the end are in different trees. + // It's NULL when the nodes are in the same tree. + // + // mCrossShadowBoundaryRange doesn't deal with DOM mutations, because + // it's still an open question about how it should be handled. + // Spec: https://github.com/w3c/selection-api/issues/168. + // As a result, it'll be set to NULL if that happens. + // + // Theoretically, mCrossShadowBoundaryRange isn't really needed because + // we should be able to always store the real start and end, and + // just return one point when a collapse is needed. + // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1886028 is going + // to be used to improve mCrossShadowBoundaryRange. + RefPtr<mozilla::dom::StaticRange> mCrossShadowBoundaryRange; + friend class mozilla::dom::AbstractRange; }; namespace mozilla::dom { diff --git a/dom/base/nsWrapperCache.h b/dom/base/nsWrapperCache.h index f1c4e17fa3..4410b64ddf 100644 --- a/dom/base/nsWrapperCache.h +++ b/dom/base/nsWrapperCache.h @@ -12,6 +12,7 @@ #include "mozilla/ServoUtils.h" #include "mozilla/RustCell.h" #include "js/HeapAPI.h" +#include "js/RootingAPI.h" #include "js/TracingAPI.h" #include "js/TypeDecls.h" #include "nsISupports.h" @@ -196,6 +197,11 @@ class JS_HAZ_ROOTED nsWrapperCache { if (mWrapper) { MOZ_ASSERT(mWrapper == aOldObject); mWrapper = aNewObject; + if (PreservingWrapper() && !JS::ObjectIsTenured(mWrapper)) { + // Can pass prevp as null here since a previous store buffer entry has + // been cleared by the current nursery collection. + JS::HeapObjectPostWriteBarrier(&mWrapper, nullptr, mWrapper); + } } } diff --git a/dom/base/rust/lib.rs b/dom/base/rust/lib.rs index 1c155e062b..467eeeb832 100644 --- a/dom/base/rust/lib.rs +++ b/dom/base/rust/lib.rs @@ -156,8 +156,6 @@ bitflags! { const RTL_LOCALE = 1 << 1; /// LTR locale: specific to the XUL localedir attribute const LTR_LOCALE = 1 << 2; - /// LWTheme status - const LWTHEME = 1 << 3; const ALL_LOCALEDIR_BITS = Self::LTR_LOCALE.bits() | Self::RTL_LOCALE.bits(); } diff --git a/dom/base/test/browser.toml b/dom/base/test/browser.toml index a68bd2e873..04eca5d1aa 100644 --- a/dom/base/test/browser.toml +++ b/dom/base/test/browser.toml @@ -104,7 +104,7 @@ skip-if = ["fission"] # Fails with Fission, and we're unlikely to spend time to ["browser_multiple_popups.js"] skip-if = [ "os == 'win' && !debug", # Bug 1505235 - "os == 'mac' && !debug", # Bug 1661132 (osx) + "apple_catalina", # Bug 1661132 (osx), Bug 1866073 "socketprocess_networking", ] support-files = ["browser_multiple_popups.html"] diff --git a/dom/base/test/browser_aboutnewtab_process_selection.js b/dom/base/test/browser_aboutnewtab_process_selection.js index ad59077105..db0e80acdf 100644 --- a/dom/base/test/browser_aboutnewtab_process_selection.js +++ b/dom/base/test/browser_aboutnewtab_process_selection.js @@ -33,7 +33,7 @@ add_task(async function () { // Open 3 tabs using the preloaded browser. let tabs = []; for (let i = 0; i < 3; i++) { - BrowserOpenTab(); + BrowserCommands.openTab(); tabs.unshift(gBrowser.selectedTab); await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser); @@ -114,7 +114,7 @@ add_task(async function preloaded_state_attribute() { "Sanity check that the first preloaded browser has the correct attribute" ); - BrowserOpenTab(); + BrowserCommands.openTab(); await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser); // Now check that the tabs have the correct browser attributes set diff --git a/dom/base/test/file_window_close.html b/dom/base/test/file_window_close.html index 5adec04ec4..b3bb20d499 100644 --- a/dom/base/test/file_window_close.html +++ b/dom/base/test/file_window_close.html @@ -13,7 +13,7 @@ if (history.length == 4) { // We're coming back from history. function listener(m) { - if (m.message.includes("Scripts may not close windows that were not opened by script.")) { + if (m.message.includes("Scripts may only close windows that were opened by a script.")) { SpecialPowers.postConsoleSentinel(); SpecialPowers.pushPrefEnv({ set: [["dom.allow_scripts_to_close_windows", true]]}).then( function() { diff --git a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js index 2aef23b042..6764ff2009 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js +++ b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js @@ -52,7 +52,7 @@ async function waitAndCheckFullscreenState(aWindow) { add_task(async () => { const URL = - "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html"; + "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html"; // We need this dummy tab which load the same URL as test tab to keep the // original content process alive after test page navigates away. let dummyTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); @@ -90,7 +90,7 @@ add_task(async () => { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html", + url: "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html", }, async function (browser) { // Open a new window to run the tests, the original window will keep the diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js index 49a48c3177..ab1651fd74 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js +++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js @@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html", + url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html", }, async function (browser) { // Maybe prevent BFCache on initial page. @@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { // Navigate to fullscreen page. const url = crossOrigin ? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html" - : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; + : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; const loaded = BrowserTestUtils.browserLoaded(browser, false, url); BrowserTestUtils.startLoadingURIString(browser, url); await loaded; diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js index c4feb7f641..1b344b7a2f 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js +++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js @@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html", + url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html", }, async function (browser) { // Maybe prevent BFCache on initial page. @@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { // Navigate to fullscreen page. const url = crossOrigin ? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html" - : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; + : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; const loaded = BrowserTestUtils.browserLoaded(browser, false, url); BrowserTestUtils.startLoadingURIString(browser, url); await loaded; diff --git a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js index 4cf8a3d8c7..95668db542 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js +++ b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js @@ -27,7 +27,7 @@ add_setup(async function () { add_task(async () => { const url = - "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html"; + "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html"; const name = "foo"; await BrowserTestUtils.withNewTab( diff --git a/dom/base/test/fullscreen/file_MozDomFullscreen.html b/dom/base/test/fullscreen/file_MozDomFullscreen.html index f954892706..600d335501 100644 --- a/dom/base/test/fullscreen/file_MozDomFullscreen.html +++ b/dom/base/test/fullscreen/file_MozDomFullscreen.html @@ -3,6 +3,6 @@ </head> <body style="background-color: blue;"> <p>Outer doc</p> -<iframe id="innerFrame" src="http://mochi.test:8888/"></iframe> +<iframe id="innerFrame" src="https://example.com/"></iframe> </body> </html> diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html index b60dea43bf..35b07cfa11 100644 --- a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html +++ b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html @@ -1,5 +1,5 @@ <div name="div" id="div" style="width: 100px; height: 100px; background: blue;"> <iframe id="iframe" allowfullscreen="yes" - src="http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"> + src="https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"> </iframe> </div><br> diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html index dddf4930c2..b61cdc02c3 100644 --- a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html +++ b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html @@ -1,5 +1,5 @@ <div name="div" id="div" style="width: 100px; height: 100px; background: red;"> <iframe id="iframe" allowfullscreen="yes" - src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"> + src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"> </iframe> </div><br> diff --git a/dom/base/test/fullscreen/fullscreen_helpers.js b/dom/base/test/fullscreen/fullscreen_helpers.js index f097ae316e..41548cf8aa 100644 --- a/dom/base/test/fullscreen/fullscreen_helpers.js +++ b/dom/base/test/fullscreen/fullscreen_helpers.js @@ -8,15 +8,14 @@ const TEST_URLS = [ `data:text/html, <div name="div" id="div" style="width: 100px; height: 100px; background: red;"> <iframe id="iframe" allowfullscreen="yes" - src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe> + src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe> </div>`, // toplevel and inner most iframe are in same process, and middle iframe is // in a different process. - // eslint-disable-next-line @microsoft/sdl/no-insecure-url - `http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, + `https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, // toplevel and middle iframe are in same process, and inner most iframe is // in a different process. - `http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, + `https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, ]; function waitRemoteFullscreenExitEvents(aBrowsingContexts) { diff --git a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml index 3041d851ac..6b3d8fada2 100644 --- a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml +++ b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml @@ -17,7 +17,7 @@ var gPrevTrusted = SpecialPowers.getBoolPref("full-screen-api.allow-trusted-requ var newwindow; // Ensure "fullscreen" permissions are not present on the test URI. -var uri = Services.io.newURI("http://mochi.test:8888"); +var uri = Services.io.newURI("https://example.com"); var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); Services.perms.removeFromPrincipal(principal, "fullscreen"); diff --git a/dom/base/test/jsmodules/chrome.toml b/dom/base/test/jsmodules/chrome.toml index 82d02ad4df..8b8a614bfb 100644 --- a/dom/base/test/jsmodules/chrome.toml +++ b/dom/base/test/jsmodules/chrome.toml @@ -3,11 +3,21 @@ support-files = [ "ambiguous_export.mjs", "import_ambiguous.mjs", "import_ambiguous_indirect_export.mjs", + "import_ambiguous_export.mjs", + "import_ambiguous_export_star.mjs", + "import_circular.mjs", + "import_circular_1.mjs", "import_no_export.mjs", "import_no_indirect_export.mjs", "exportA1.mjs", "exportA2.mjs", "export_ambiguous.mjs", + "export_star_ambiguous.mjs", + "module_a.mjs", + "module_b.mjs", + "module_c.mjs", + "module_d.mjs", + "module_e.mjs", "module_setRan.mjs", "module_testSyntax.mjs", "module_badSyntax.mjs", @@ -47,6 +57,8 @@ support-files = [ ["test_import_errorMessage.html"] +["test_import_errorMessage2.html"] + ["test_import_meta_resolve.html"] ["test_importedModuleMemoization.html"] diff --git a/dom/base/test/jsmodules/export_star_ambiguous.mjs b/dom/base/test/jsmodules/export_star_ambiguous.mjs new file mode 100644 index 0000000000..35cc979dee --- /dev/null +++ b/dom/base/test/jsmodules/export_star_ambiguous.mjs @@ -0,0 +1 @@ +export * from "./ambiguous_export.mjs"; diff --git a/dom/base/test/jsmodules/import_ambiguous_export.mjs b/dom/base/test/jsmodules/import_ambiguous_export.mjs new file mode 100644 index 0000000000..f5c12ff086 --- /dev/null +++ b/dom/base/test/jsmodules/import_ambiguous_export.mjs @@ -0,0 +1 @@ +import { a } from "./ambiguous_export.mjs"; diff --git a/dom/base/test/jsmodules/import_ambiguous_export_star.mjs b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs new file mode 100644 index 0000000000..1ee2a56d37 --- /dev/null +++ b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs @@ -0,0 +1 @@ +import { a } from "./export_star_ambiguous.mjs"; diff --git a/dom/base/test/jsmodules/import_circular.mjs b/dom/base/test/jsmodules/import_circular.mjs new file mode 100644 index 0000000000..bb3a46bd5e --- /dev/null +++ b/dom/base/test/jsmodules/import_circular.mjs @@ -0,0 +1 @@ +export { a } from "./import_circular_1.mjs"; diff --git a/dom/base/test/jsmodules/import_circular_1.mjs b/dom/base/test/jsmodules/import_circular_1.mjs new file mode 100644 index 0000000000..eb7c038c77 --- /dev/null +++ b/dom/base/test/jsmodules/import_circular_1.mjs @@ -0,0 +1 @@ +export { a } from "./import_circular.mjs"; diff --git a/dom/base/test/jsmodules/import_no_export.mjs b/dom/base/test/jsmodules/import_no_export.mjs index 47cabac557..d989498c88 100644 --- a/dom/base/test/jsmodules/import_no_export.mjs +++ b/dom/base/test/jsmodules/import_no_export.mjs @@ -1 +1 @@ -import x from "./no_export.mjs"; +import { x } from "./no_export.mjs"; diff --git a/dom/base/test/jsmodules/import_no_indirect_export.mjs b/dom/base/test/jsmodules/import_no_indirect_export.mjs index dd1ca847fc..bfcdcedbc5 100644 --- a/dom/base/test/jsmodules/import_no_indirect_export.mjs +++ b/dom/base/test/jsmodules/import_no_indirect_export.mjs @@ -1,2 +1,2 @@ /* eslint-disable import/default */ -import x from "./no_indirect_export.mjs"; +import { a } from "./no_indirect_export.mjs"; diff --git a/dom/base/test/jsmodules/importmaps/classic_script.js b/dom/base/test/jsmodules/importmaps/classic_script.js new file mode 100644 index 0000000000..d7ae0be054 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/classic_script.js @@ -0,0 +1 @@ +// Empty script. diff --git a/dom/base/test/jsmodules/importmaps/mochitest.toml b/dom/base/test/jsmodules/importmaps/mochitest.toml index 4229455722..1f95b155ac 100644 --- a/dom/base/test/jsmodules/importmaps/mochitest.toml +++ b/dom/base/test/jsmodules/importmaps/mochitest.toml @@ -3,6 +3,9 @@ support-files = [ "bug_1865410_module_a.mjs", "bug_1865410_module_b.mjs", "bug_1873417.mjs", + "classic_script.js", + "module_chain_1.mjs", + "module_chain_2.mjs", "module_importMap_with_external_script_0.mjs", "module_importMap_with_external_script_1.mjs", "module_importMap_with_external_script_2.mjs", @@ -13,6 +16,7 @@ support-files = [ "module_importMap_with_external_script_6.mjs", "module_importMap_with_external_script_6.mjs^headers^", "module_importMap_with_external_script_7.mjs", + "module_importMap_with_nonexisting_module.mjs", "bad/module_2.mjs", "bad/module_3.mjs", "bad/module_4.mjs", @@ -31,3 +35,6 @@ support-files = [ ["test_bug_1873417.html"] ["test_importMap_with_external_script.html"] +["test_importMap_with_nonexisting_module.html"] +["test_dynamic_importMap_with_external_script.html"] +["test_dynamic_importMap_load_completes.html"] diff --git a/dom/base/test/jsmodules/importmaps/module_chain_1.mjs b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs new file mode 100644 index 0000000000..d9515fab7f --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/no-unassigned-import +import {} from "./module_chain_2.mjs"; diff --git a/dom/base/test/jsmodules/importmaps/module_chain_2.mjs b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs new file mode 100644 index 0000000000..ce12406a76 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs @@ -0,0 +1 @@ +loaded = true; diff --git a/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs new file mode 100644 index 0000000000..4f9981bbe3 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs @@ -0,0 +1,4 @@ +/* eslint-disable import/no-unassigned-import, import/no-unresolved */ +// Bareword specifier should be mapped to ./good/module_0.mjs. +import {} from "bare"; +import * as test from "nonexistingmodule"; diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html new file mode 100644 index 0000000000..da354c1ca0 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> +<title>Test script loading complets when there's a dynamicly inserted import map</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<!-- + This test case used to fail intermittently with some modules never + being loaded. +--> + +<script> + let loaded = false; // Set by module_chain_2.mjs. +</script> + +<script src="classic_script.js"></script> + +<script> + const script = document.createElement('script'); + script.type = 'importmap'; + script.textContent = `{}`; + document.head.appendChild(script); +</script> + +<script src="module_chain_1.mjs" type="module"></script> + +<script type="module"> + ok(loaded, "Expected all modules loaded"); +</script> + +<body></body> diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html new file mode 100644 index 0000000000..b78992fb87 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> +<title>Test speculative preload of external script doesn't conflict with import map</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<!-- + These tests check that speculative preloading, which could happen before + the import map is installed, doesn't load the wrong modules. This version + dynamically inserts the import map script element after speculative + preloading has started. +--> + +<script> + const script = document.createElement('script'); + script.type = 'importmap'; + script.textContent = + `{ + "imports": { + "bare": "./good/module_0.mjs", + "./bad/module_1.mjs": "./good/module_1.mjs", + "./bad/module_2.mjs": "./good/module_2.mjs", + "./bad/module_3.mjs": "./good/module_3.mjs", + "./bad/module_4.mjs": "./good/module_4.mjs", + "./bad/module_7.mjs": "./good/module_7.mjs" + } + }`; + document.head.appendChild(script); +</script> + +<!-- +Test bareword import (not supported before import map installed). +--> +<script type="module" src="module_importMap_with_external_script_0.mjs"></script> + +<!-- +Test mapping from missing resource to existing resource (not found before +import map installed). +--> +<script type="module" src="module_importMap_with_external_script_1.mjs"></script> + +<!-- +Test mapping from one existing resource to another (would load wrong resource before +import map installed). +--> +<script type="module" src="module_importMap_with_external_script_2.mjs"></script> + +<!-- +Test mapping from one existing resource to another with circular dependency. +--> +<script type="module" src="module_importMap_with_external_script_3.mjs"></script> + +<!-- +Test with redirect, script_6.mjs -> script_5.mjs -> script_4.mjs. +We redirect twice here, as sometimes one redirect can't reproduce the crash +from bug 1835468. +--> +<script type="module" src="module_importMap_with_external_script_6.mjs"></script> + +<!-- +Test with async attribute +--> +<script type="module" async src="module_importMap_with_external_script_7.mjs"></script> + +<script> + SimpleTest.waitForExplicitFinish(); + + let passCount = 0; + const expectedCount = 6; + + function success(name) { + ok(true, "Test passed, loaded " + name); + passCount++; + if (passCount == expectedCount) { + SimpleTest.finish(); + } + } +</script> +<body></body> diff --git a/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html new file mode 100644 index 0000000000..57cfd5e5a1 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test an import map with an nonexisting module specifier</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="importmap"> +{ + "imports": { + "bare": "./good/module_0.mjs" + } +} +</script> + + +<script> + SimpleTest.waitForExplicitFinish(); + + window.onerror = (event, src, lineno, colno, error) => { + ok(error instanceof TypeError, "Should be a TypeError"); + SimpleTest.finish(); + }; + +</script> +<script type="module" src="module_importMap_with_nonexisting_module.mjs"></script> diff --git a/dom/base/test/jsmodules/module_a.mjs b/dom/base/test/jsmodules/module_a.mjs new file mode 100644 index 0000000000..0b0c3304ff --- /dev/null +++ b/dom/base/test/jsmodules/module_a.mjs @@ -0,0 +1,2 @@ +export var a = 1; +export var b = 2; diff --git a/dom/base/test/jsmodules/module_b.mjs b/dom/base/test/jsmodules/module_b.mjs new file mode 100644 index 0000000000..79da84736d --- /dev/null +++ b/dom/base/test/jsmodules/module_b.mjs @@ -0,0 +1,2 @@ +export var b = 3; +export var c = 4; diff --git a/dom/base/test/jsmodules/module_c.mjs b/dom/base/test/jsmodules/module_c.mjs new file mode 100644 index 0000000000..5a2a7e3e09 --- /dev/null +++ b/dom/base/test/jsmodules/module_c.mjs @@ -0,0 +1,3 @@ +/* eslint-disable import/export */ +export * from "./module_a.mjs"; +export * from "./module_b.mjs"; diff --git a/dom/base/test/jsmodules/module_d.mjs b/dom/base/test/jsmodules/module_d.mjs new file mode 100644 index 0000000000..04dc02d27a --- /dev/null +++ b/dom/base/test/jsmodules/module_d.mjs @@ -0,0 +1 @@ +import { a } from "./module_c.mjs"; diff --git a/dom/base/test/jsmodules/module_e.mjs b/dom/base/test/jsmodules/module_e.mjs new file mode 100644 index 0000000000..544c424fcb --- /dev/null +++ b/dom/base/test/jsmodules/module_e.mjs @@ -0,0 +1 @@ +import { b } from "./module_c.mjs"; diff --git a/dom/base/test/jsmodules/test_import_errorMessage.html b/dom/base/test/jsmodules/test_import_errorMessage.html index 6ab0b1dd74..2330b46dd9 100644 --- a/dom/base/test/jsmodules/test_import_errorMessage.html +++ b/dom/base/test/jsmodules/test_import_errorMessage.html @@ -8,40 +8,38 @@ let count = 0; window.onerror = function (event, src, lineno, colno, error) { - info("window.onerror :" + error.message); + info("window.onerror: message: " + error.message); + info("window.onerror: src: " + src); ok(error instanceof SyntaxError, "Should be a SyntaxError."); - // import_no_indirect_export.mjs and import_ambiguous_indirect_export.mjs - // are related to indirect import/export. - if (count < 2) { - ok(error.message.match("indirect"), "Should contain 'indirect'"); - } - - // import_ambiguous_indirect_export.mjs and import_ambiguous.mjs both - // have ambiguous import/export. - if (count % 2 === 1) { - ok(error.message.match("ambiguous"), "Should contain 'ambiguous'"); - } - - if (count === 2) { - ok(!error.message.match("ambiguous") && !error.message.match("indirect"), - "Should NOT contain 'indirect' nor 'ambiguous'"); + if (src.match("no_indirect_export.mjs") || + src.match("import_no_export.mjs")) { + ok(error.message.match("doesn't provide an export named")); + } else if(src.match("export_ambiguous.mjs") || + src.match("import_ambiguous_export_star.mjs") || + src.match("import_ambiguous_export.mjs") || + src.match("import_ambiguous.mjs")) { + ok(error.message.match("contains ambiguous star export")); + } else if (src.match("import_circular_1.mjs")) { + ok(error.message.match("contains circular import")); + } else { + ok(false, "unknown src " + src); } count++; }; function testLoaded() { - ok(count === 4, "Should have 4 SynaxErrors thrown."); + ok(count === 7, "Should have 7 SynaxErrors thrown."); SimpleTest.finish(); } + </script> -<!-- -In window.onerror will test the error messages, so if the order is changed, -the code in window.onerror should be updated as well. ---> <script type="module" src="import_no_indirect_export.mjs"></script> <script type="module" src="import_ambiguous_indirect_export.mjs"></script> +<script type="module" src="import_ambiguous_export_star.mjs"></script> +<script type="module" src="import_ambiguous_export.mjs"></script> <script type="module" src="import_no_export.mjs"></script> <script type="module" src="import_ambiguous.mjs"></script> +<script type="module" src="import_circular.mjs"></script> <body onload='testLoaded()'></body> diff --git a/dom/base/test/jsmodules/test_import_errorMessage2.html b/dom/base/test/jsmodules/test_import_errorMessage2.html new file mode 100644 index 0000000000..ed4227362e --- /dev/null +++ b/dom/base/test/jsmodules/test_import_errorMessage2.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test to get the filename of the requested module after it has been evaluated</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script> + SimpleTest.waitForExplicitFinish(); + + let count = 0; + + window.onerror = function (event, src, lineno, colno, error) { + info("window.onerror: message: " + error.message); + info("window.onerror: src: " + src); + ok(error instanceof SyntaxError, "Should be a SyntaxError."); + + if (src.match("module_e.mjs")) { + ok(error.message.match("contains ambiguous star export")); + } else { + ok(false, "unknown src " + src); + } + count++; + }; + + function testLoaded() { + ok(count === 1, "Should have 1 SynaxError thrown."); + SimpleTest.finish(); + } + +</script> + +<!--module_c.mjs will be evaluated here--> +<script type="module" src="module_d.mjs"></script> + +<!--module_c.mjs will be linked again here--> +<script type="module" src="module_e.mjs"></script> +<body onload='testLoaded()'></body> diff --git a/dom/base/test/mochitest.toml b/dom/base/test/mochitest.toml index fb6724e497..a1e7b31a19 100644 --- a/dom/base/test/mochitest.toml +++ b/dom/base/test/mochitest.toml @@ -1170,6 +1170,8 @@ skip-if = [ ["test_content_iterator_subtree.html"] +["test_content_iterator_subtree_shadow_tree.html"] + ["test_copyimage.html"] skip-if = [ "os == 'android'", diff --git a/dom/base/test/test_bug564863-2.xhtml b/dom/base/test/test_bug564863-2.xhtml index 6f338f612d..c860a7f6d7 100644 --- a/dom/base/test/test_bug564863-2.xhtml +++ b/dom/base/test/test_bug564863-2.xhtml @@ -55,11 +55,11 @@ SimpleTest.waitForExplicitFinish(); // not when run as a Mochitest plain. //is(xul_cs.color, "rgb(0, 0, 0)", "xul color " + test); - attrValue = removed ? null : ""; + let attrValue = removed ? null : ""; is(xul.id, "", "xul id " + test); - is(xul.getAttribute("id"), "", "xul getAttribute " + test); + is(xul.getAttribute("id"), attrValue, "xul getAttribute " + test); is($("xul_id"), null, "xul getElementById " + test); } @@ -149,7 +149,7 @@ SimpleTest.waitForExplicitFinish(); await new Promise(SimpleTest.executeSoon); if (mutation) { is(mutation.target, xul, "target is xul"); - is(xul.getAttribute("id"), "", "xul no longer has id attr"); + is(xul.getAttribute("id"), null, "xul no longer has id attr"); is(xul.id, "", "xul no longer has id"); xul.id = "other_xul_id"; } else { diff --git a/dom/base/test/test_content_iterator_subtree_shadow_tree.html b/dom/base/test/test_content_iterator_subtree_shadow_tree.html new file mode 100644 index 0000000000..033aaf80a7 --- /dev/null +++ b/dom/base/test/test_content_iterator_subtree_shadow_tree.html @@ -0,0 +1,290 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for content subtree iterator with ShadowDOM involved</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +var Cc = SpecialPowers.Cc; +var Ci = SpecialPowers.Ci; +function finish() { + // The SimpleTest may require usual elements in the template, but they shouldn't be during test. + // So, let's create them at end of the test. + document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>'; + SimpleTest.finish(); +} + +function createContentIterator() { + return Cc["@mozilla.org/scriptable-content-iterator;1"] + .createInstance(Ci.nsIScriptableContentIterator); +} + +function getNodeDescription(aNode) { + if (aNode === undefined) { + return "undefine"; + } + if (aNode === null) { + return "null"; + } + function getElementDescription(aElement) { + if (aElement.host) { + aElement = aElement.host; + } + if (aElement.tagName === "BR") { + if (aElement.previousSibling) { + return `<br> element after ${getNodeDescription(aElement.previousSibling)}`; + } + return `<br> element in ${getElementDescription(aElement.parentElement)}`; + } + let hasHint = aElement == document.body; + let tag = `<${aElement.tagName.toLowerCase()}`; + if (aElement.getAttribute("id")) { + tag += ` id="${aElement.getAttribute("id")}"`; + hasHint = true; + } + if (aElement.getAttribute("class")) { + tag += ` class="${aElement.getAttribute("class")}"`; + hasHint = true; + } + if (aElement.getAttribute("type")) { + tag += ` type="${aElement.getAttribute("type")}"`; + } + if (aElement.getAttribute("name")) { + tag += ` name="${aElement.getAttribute("name")}"`; + } + if (aElement.getAttribute("value")) { + tag += ` value="${aElement.getAttribute("value")}"`; + hasHint = true; + } + if (aElement.getAttribute("style")) { + tag += ` style="${aElement.getAttribute("style")}"`; + hasHint = true; + } + if (hasHint) { + return tag + ">"; + } + + return `${tag}> in ${getElementDescription(aElement.parentElement || aElement.parentNode)}`; + } + switch (aNode.nodeType) { + case aNode.TEXT_NODE: + return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`; + case aNode.COMMENT_NODE: + return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`; + case aNode.ELEMENT_NODE: + return getElementDescription(SpecialPowers.unwrap(aNode)); + default: + return "unknown node"; + } +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function () { + let iter = createContentIterator(); + + function runTest() { + /** + * Basic tests with complicated tree. + */ + function check(aIter, aExpectedResult, aDescription) { + if (aExpectedResult.length) { + is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], + `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`); + + aIter.first(); + is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], + `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`); + + for (let expected of aExpectedResult) { + is(SpecialPowers.unwrap(aIter.currentNode), expected, + `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`); + aIter.next(); + } + + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`); + ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`); + } else { + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`); + ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`); + + aIter.first(); + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`); + ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`); + } + } + + // Structure + // <div>OuterText1</div> + // <div #host1> + // #ShadowRoot + // InnerText1 + // <div>OuterText2</div> + // <div #host2> + // #ShadowRoot + // <div>InnerText2</div> + // <div>InnerText3</div> + // <div #host3> + // #ShadowRoot + // <div #host4> + // #ShadowRoot + // InnerText4 + // OuterText3 + + document.body.innerHTML = `<div id="outerText1">OuterText1</div>` + + `<div id="host1"></div>` + + `<div id="outerText2">OuterText2</div>` + + `<div id="host2"></div>` + + `<div id="host3"></div>` + + `OuterText3`; + const outerText1 = document.getElementById("outerText1"); + const outerText2 = document.getElementById("outerText2"); + + const host1 = document.getElementById("host1"); + const root1 = host1.attachShadow({mode: "open"}); + root1.innerHTML = "InnerText1"; + + const host2 = document.getElementById("host2"); + const root2 = host2.attachShadow({mode: "open"}); + root2.innerHTML = "<div>InnerText2</div><div>InnerText3</div>"; + + const host3 = document.getElementById("host3"); + const root3 = host3.attachShadow({mode: "open"}); + root3.innerHTML = `<div id="host4"></div>`; + + const host4 = root3.getElementById("host4"); + const root4 = host4.attachShadow({mode: "open"}); + root4.innerHTML = "InnerText4"; + + /** + * Selects the <body> with a range. + */ + range = document.createRange(); + range.selectNode(document.body); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + check(iter, [document.body], "Initialized with range selecting the <body>"); + + /** + * Selects all children in the <body> with a range. + */ + range = document.createRange(); + range.selectNodeContents(document.body); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + check(iter, [outerText1, host1, + outerText2, host2, + host3, // host4 is a child of host3 + document.body.lastChild], + "Initialized with range selecting all children in the <body>"); + + /** + * range around elements. + */ + range = document.createRange(); + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // outerText1.firstChild is a node without children, so the + // next candidate is root1.firstChild, given root1.firstChild + // is also the end container which isn't fully contained + // by this range, so the iterator returns nothing. + check(iter, [], "Initialized with range selecting 'OuterText1 and InnerText1'"); + + // From light DOM to Shadow DOM #1 + range = document.createRange(); + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText1 is a container and it has children, so the first node + // is the topmost descendant, which is outerText.firstChild. + // [end] The end point of this iteration is also outerText1.firstChild because + // it is also the topmost element in the previous node of root1.firstChild. + // Iteration #1: outerText1.firstChild as it is the start node + check(iter, [outerText1.firstChild], "Initialized with range selecting 'OuterText1 and InnerText1'"); + + // From light DOM to Shadow DOM #2 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2, root2.childNodes.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText1 is a container and it has children, so the first node + // is the topmost descendant, which is outerText.firstChild. + // [end] root2 is the container and it has children, so the end node is + // the last node of root2, which is root2.lastChild + // Iteration #1: outerText1.firstChild, as it's the start node + // Iteration #2: host1, as it's next available node after outerText1.firstChild + // Iteration #3: outerText2, as it's the next sibiling of host1 + // Iteration #4: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + // Iteration #5: root2.lastChild, as it's the next sibling of root2.firstChild + check(iter, [outerText1.firstChild, host1, outerText2, root2.firstChild, root2.lastChild], + "Initialized with range selecting 'OuterText1, InnerText1, OuterText2 and InnerText2'"); + + // From Shadow DOM to Shadow DOM #1 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText2 is the start because root1.firstChild doesn't have children, + // so we look for next available node which is outerText2. + // [end] root2.lastChild is the end container, so we look for previous + // nodes and get root2.firstChild + // Iteration #1: outerText2, as it's the start node + // Iteration #2: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + check(iter, [outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); + + // From Shadow DOM to Shadow DOM #2 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] root1 is the start container and it has children, so the first node + // is the topmost descendant, which is root1.firstChild. + // [end] root2.lastChild is the end container, so we look for previous + // nodes and get root2.firstChild + // Iteration #1: root1.firstChild, as it's the start node + // Iteration #2: outerText2, as it's the next available node + // Iteration #3: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + check(iter, [root1.firstChild, outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); + + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 1); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root4.firstChild, root4.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText2 is the start because root1.firstChild doesn't have children, + // so we look for next available node which is outerText2. + // [end] host2 is the end container, so we look for previous + // nodes root4.firstChild and eventually get host2. + // Iteration #1: outerText2, as it's the start node + // Iteration #2: host2, as it's the next sibling of outerText2 + check(iter, [outerText2, host2], "Initialized with range selecting 'InnerText1, OuterText2, InnerText2 and InnerText3'"); + + // From light to light + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(document.body.lastChild, document.body.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] host1 is the start because it's the next available node of + // outerText1.firstChild. + // [end] host3 is the end because the previous node of document.body.lastChild is host3. + // Iteration #1: host1, as it's the start node + // Iteration #2: outerText2, as it's the next sibling of host1 + // Iteration #3: host2, as it's the next sibling of outerText2 + // Iteration #4: host3, as it's the next sibling of host2 + check(iter, [host1, outerText2, host2, host3], + "Initialized with range selecting 'OuterText1, InnerText1, OuterText2, InnerText2, InnerText3 and OuterText3'"); + + finish(); + } + + SpecialPowers.pushPrefEnv({"set": [["dom.shadowdom.selection_across_boundary.enabled", true]]}, runTest); +}); +</script> +</head> +<body></body> +</html> diff --git a/dom/base/test/test_embed_xorigin_document.html b/dom/base/test/test_embed_xorigin_document.html index 7d4c29aacf..4566d48534 100644 --- a/dom/base/test/test_embed_xorigin_document.html +++ b/dom/base/test/test_embed_xorigin_document.html @@ -12,11 +12,6 @@ const testPath = window.location.href.replace("http://mochi.test:8888", ""); const testDir = testPath.substring(0, testPath.lastIndexOf('/') + 1); add_task(async function() { - // FIXME: Remove when bug 1658342 is fixed - await SpecialPowers.pushPrefEnv({ - set: [["fission.remoteObjectEmbed", true]], - }); - info("Loading image in embed"); let embed = document.createElement("embed"); document.body.appendChild(embed); diff --git a/dom/base/test/test_range_bounds.html b/dom/base/test/test_range_bounds.html index 657d315198..dba687eead 100644 --- a/dom/base/test/test_range_bounds.html +++ b/dom/base/test/test_range_bounds.html @@ -38,6 +38,22 @@ function isEmptyRect(rect, name) { is(rect.height, 0, name+'empty rect should have height = 0'); } +function getTextBoundingClientRect(node) { + const quads = node.getBoxQuads()[0]; + return DOMRect.fromRect({ + x: quads.p1.x, + y: quads.p1.y, + width: quads.p2.x - quads.p1.x, + height: quads.p3.y - quads.p2.y + }); +} + +function sortRectList(rectlist) { + return Array.prototype.slice.call(rectlist, 0).sort(function(a, b) { + return a.top - b.top || a.left - b.left; + }); +} + function isEmptyRectList(rectlist, name) { name = annotateName(name); is(rectlist.length, 0, name + 'empty rectlist should have zero rects'); @@ -90,6 +106,9 @@ function runATest(obj) { //convert RectList to a real array obj.rectList=Array.prototype.slice.call(obj.rectList, 0); } + if (obj.mustSortBeforeComparing) { + rectlist = sortRectList(rectlist); + } obj.rectList.forEach(function(r,i) { is(_getRect(rectlist[i]),_getRect(r), annotateName(testname+": item at "+i)); @@ -109,11 +128,16 @@ function doTest(){ thirdDiv = root.childNodes[5]; var firstPRect = firstP.getBoundingClientRect(), spanInFirstPRect = spanInFirstP.getBoundingClientRect(), + textInFirstPRect = getTextBoundingClientRect(firstP.firstChild), + textInSpanInFirstPRect = getTextBoundingClientRect(spanInFirstP.firstChild), firstDivRect = firstDiv.getBoundingClientRect(), + textInFirstDivRect = getTextBoundingClientRect(firstDiv.firstChild), spanInFirstDivRect = spanInFirstDiv.getBoundingClientRect(), + textInSpanInFirstDivRect = getTextBoundingClientRect(spanInFirstDiv.firstChild), secondPRect = secondP.getBoundingClientRect(), secondDivRect = secondDiv.getBoundingClientRect(), spanInSecondPRect = spanInSecondP.getBoundingClientRect(), + textInSpanInSecondPRect = getTextBoundingClientRect(spanInSecondP.firstChild), spanInSecondDivRect = spanInSecondDiv.getBoundingClientRect(), spanInSecondDivRectList = spanInSecondDiv.getClientRects(); var widthPerchar = spanInSecondPRect.width / spanInSecondP.firstChild.length; @@ -132,12 +156,14 @@ function doTest(){ {name:'collapsedAtEndOfTextNode', range:[firstP.firstChild, 6], rect:[spanInFirstPRect.left, spanInFirstPRect.left, spanInFirstPRect.top, spanInFirstPRect.bottom, 0, spanInFirstPRect.height]}, - {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect}, + {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect, + rectList:[firstPRect, textInFirstPRect, spanInFirstPRect]}, {name:'twoBlockNodes', range:[root, 1, root, 3], rect:[firstPRect.left, firstPRect.right, firstPRect.top, firstDivRect.bottom, firstPRect.width, firstDivRect.bottom - firstPRect.top], - rectList:[firstPRect, firstDivRect]}, + rectList:[firstPRect, textInFirstPRect, textInSpanInFirstPRect, + firstDivRect, textInFirstDivRect, textInSpanInFirstDivRect]}, {name:'endOfTextNodeToEndOfAnotherTextNodeInAnotherBlock', range:[spanInFirstP.firstChild, 1, firstDiv.firstChild, 5], rect:[spanInFirstDivRect.left - 5*widthPerchar, spanInFirstDivRect.left, @@ -163,7 +189,7 @@ function doTest(){ rectList:[[spanInSecondPRect.left - 3*widthPerchar, spanInSecondPRect.left, spanInSecondPRect.top, spanInSecondPRect.bottom, 3 * widthPerchar, spanInSecondPRect.height], - spanInSecondPRect, + spanInSecondPRect, textInSpanInSecondPRect, [spanInSecondPRect.right, spanInSecondPRect.right + widthPerchar, spanInSecondPRect.top, spanInSecondPRect.bottom, widthPerchar, spanInSecondPRect.height]]} @@ -228,10 +254,17 @@ function doTest(){ } function testMixedDir(){ var root = document.getElementById('mixeddir'); + var bdo = document.getElementById('bdo'); var firstSpan = root.firstElementChild, firstSpanRect=firstSpan.getBoundingClientRect(), - firstSpanRectList = firstSpan.getClientRects(); + firstSpanWithInnerTextRectList = Array.from(firstSpan.getClientRects()); + firstSpanWithInnerTextRectList.push(...bdo.getClientRects()); + + // Depending on the font rendering, the order of the rects composing the bdo + // element may vary. We need to sort the list of rects before comparing it to + // the expected list. + firstSpanWithInnerTextRectList = sortRectList(firstSpanWithInnerTextRectList); runATest({name:'mixeddir',range:[firstSpan.firstChild,0,firstSpan.lastChild,firstSpan.lastChild.length], - rect: firstSpanRect, rectList:firstSpanRectList}); + rect: firstSpanRect, rectList:firstSpanWithInnerTextRectList, mustSortBeforeComparing: true}); root = document.getElementById('mixeddir2'); firstSpan = root.firstElementChild; @@ -271,7 +304,10 @@ function testShadowDOM() { isnot(rect.height, 0, "Div element inside shadow shouldn't have zero size."); } -function test(){ +async function test(){ + // We use getBoxQuads to get some text nodes bounding rects. + await SpecialPowers.pushPrefEnv({"set": [["layout.css.getBoxQuads.enabled", true]]}); + //test ltr doTest(); diff --git a/dom/base/test/unit/test_xhr_standalone.js b/dom/base/test/unit/test_xhr_standalone.js index 94f2d7d642..100027f38a 100644 --- a/dom/base/test/unit/test_xhr_standalone.js +++ b/dom/base/test/unit/test_xhr_standalone.js @@ -6,6 +6,10 @@ // in non-window non-Worker context function run_test() { + Services.prefs.setBoolPref( + "network.fetch.systemDefaultsToOmittingCredentials", + false + ); var xhr = new XMLHttpRequest(); xhr.open("GET", "data:,", false); var exceptionThrown = false; @@ -13,6 +17,7 @@ function run_test() { xhr.responseType = ""; xhr.withCredentials = false; } catch (e) { + console.error(e); exceptionThrown = true; } Assert.equal(false, exceptionThrown); diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml index 211d3e1344..11b569e6d4 100644 --- a/dom/base/use_counter_metrics.yaml +++ b/dom/base/use_counter_metrics.yaml @@ -107,8 +107,8 @@ use.counter: send_in_pings: - use-counters -# Total of 2307 use counter metrics (excludes denominators). -# Total of 358 'page' use counters. +# Total of 2301 use counter metrics (excludes denominators). +# Total of 354 'page' use counters. use.counter.page: svgsvgelement_getelementbyid: type: counter @@ -450,57 +450,6 @@ use.counter.page: send_in_pings: - use-counters - onstart: - type: counter - description: > - Whether a page sets a <marquee> onstart event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onbounce: - type: counter - description: > - Whether a page sets a <marquee> onbounce event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onfinish: - type: counter - description: > - Whether a page sets a <marquee> onfinish event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - onoverflow: type: counter description: > @@ -569,23 +518,6 @@ use.counter.page: send_in_pings: - use-counters - js_late_weekday: - type: counter - description: > - Whether a page parses a Date with day of week in an unexpected position. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - js_wasm_legacy_exceptions: type: counter description: > @@ -6196,7 +6128,7 @@ use.counter.page: send_in_pings: - use-counters -# Total of 358 'document' use counters. +# Total of 354 'document' use counters. use.counter.doc: svgsvgelement_getelementbyid: type: counter @@ -6538,57 +6470,6 @@ use.counter.doc: send_in_pings: - use-counters - onstart: - type: counter - description: > - Whether a document sets a <marquee> onstart event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onbounce: - type: counter - description: > - Whether a document sets a <marquee> onbounce event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onfinish: - type: counter - description: > - Whether a document sets a <marquee> onfinish event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - onoverflow: type: counter description: > @@ -6657,23 +6538,6 @@ use.counter.doc: send_in_pings: - use-counters - js_late_weekday: - type: counter - description: > - Whether a document parses a Date with day of week in an unexpected position. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - js_wasm_legacy_exceptions: type: counter description: > @@ -15677,7 +15541,7 @@ use.counter.deprecated_ops.doc: send_in_pings: - use-counters -# Total of 696 'CSS (page)' use counters. +# Total of 697 'CSS (page)' use counters. use.counter.css.page: css_align_content: type: counter @@ -27528,7 +27392,7 @@ use.counter.css.page: send_in_pings: - use-counters -# Total of 696 'CSS (document)' use counters. +# Total of 697 'CSS (document)' use counters. use.counter.css.doc: css_align_content: type: counter |