/* * Copyright (C) 2013 Google Inc. All rights reserved. * Copyright (C) 2013-2017 Apple Inc. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "EventPath.h" #include "DOMWindow.h" #include "Event.h" #include "EventContext.h" #include "EventNames.h" #include "FullscreenManager.h" #include "HTMLSlotElement.h" #include "MouseEvent.h" #include "Node.h" #include "PseudoElement.h" #include "ShadowRoot.h" #include "TouchEvent.h" namespace WebCore { static inline bool shouldEventCrossShadowBoundary(Event& event, ShadowRoot& shadowRoot, EventTarget& target) { #if ENABLE(FULLSCREEN_API) && ENABLE(VIDEO) // Video-only full screen is a mode where we use the shadow DOM as an implementation // detail that should not be detectable by the web content. if (is(target)) { if (auto* element = downcast(target).document().fullscreenManager().currentFullscreenElement()) { // FIXME: We assume that if the full screen element is a media element that it's // the video-only full screen. Both here and elsewhere. But that is probably wrong. if (element->isMediaElement() && shadowRoot.host() == element) return false; } } #endif bool targetIsInShadowRoot = is(target) && &downcast(target).treeScope().rootNode() == &shadowRoot; return !targetIsInShadowRoot || event.composed(); } static Node* nodeOrHostIfPseudoElement(Node* node) { return is(*node) ? downcast(*node).hostElement() : node; } class RelatedNodeRetargeter { public: RelatedNodeRetargeter(Node& relatedNode, Node& target); Node* currentNode(Node& currentTreeScope); void moveToNewTreeScope(TreeScope* previousTreeScope, TreeScope& newTreeScope); private: Node* nodeInLowestCommonAncestor(); void collectTreeScopes(); void checkConsistency(Node& currentTarget); Node& m_relatedNode; Node* m_retargetedRelatedNode; Vector m_ancestorTreeScopes; unsigned m_lowestCommonAncestorIndex { 0 }; bool m_hasDifferentTreeRoot { false }; }; EventPath::EventPath(Node& originalTarget, Event& event) { buildPath(originalTarget, event); if (auto* relatedTarget = event.relatedTarget(); is(relatedTarget) && !m_path.isEmpty()) setRelatedTarget(originalTarget, downcast(*relatedTarget)); #if ENABLE(TOUCH_EVENTS) if (is(event)) retargetTouchLists(downcast(event)); #endif } void EventPath::buildPath(Node& originalTarget, Event& event) { EventContext::Type contextType = [&]() { if (is(event) || event.isFocusEvent()) return EventContext::Type::MouseOrFocus; #if ENABLE(TOUCH_EVENTS) if (is(event)) return EventContext::Type::Touch; #endif return EventContext::Type::Normal; }(); Node* node = nodeOrHostIfPseudoElement(&originalTarget); Node* target = node ? eventTargetRespectingTargetRules(*node) : nullptr; int closedShadowDepth = 0; // Depths are used to decided which nodes are excluded in event.composedPath when the tree is mutated during event dispatching. // They could be negative for nodes outside the shadow tree of the target node. while (node) { while (node) { m_path.append(EventContext { contextType, *node, eventTargetRespectingTargetRules(*node), target, closedShadowDepth }); if (is(*node)) break; ContainerNode* parent = node->parentNode(); if (UNLIKELY(!parent)) { // https://dom.spec.whatwg.org/#interface-document if (is(*node) && event.type() != eventNames().loadEvent) { ASSERT(target); if (target) { if (auto* window = downcast(*node).domWindow()) m_path.append(EventContext { EventContext::Type::Window, node, window, target, closedShadowDepth }); } } return; } if (auto* shadowRootOfParent = parent->shadowRoot(); UNLIKELY(shadowRootOfParent)) { if (auto* assignedSlot = shadowRootOfParent->findAssignedSlot(*node)) { if (shadowRootOfParent->mode() != ShadowRootMode::Open) closedShadowDepth++; // node is assigned to a slot. Continue dispatching the event at this slot. parent = assignedSlot; } } node = parent; } bool exitingShadowTreeOfTarget = &target->treeScope() == &node->treeScope(); ShadowRoot& shadowRoot = downcast(*node); if (!shouldEventCrossShadowBoundary(event, shadowRoot, originalTarget)) return; node = shadowRoot.host(); if (shadowRoot.mode() != ShadowRootMode::Open) closedShadowDepth--; if (exitingShadowTreeOfTarget) target = eventTargetRespectingTargetRules(*node); } } void EventPath::setRelatedTarget(Node& origin, Node& relatedNode) { RelatedNodeRetargeter retargeter(relatedNode, *m_path[0].node()); bool originIsRelatedTarget = &origin == &relatedNode; Node& rootNodeInOriginTreeScope = origin.treeScope().rootNode(); TreeScope* previousTreeScope = nullptr; size_t originalEventPathSize = m_path.size(); for (unsigned contextIndex = 0; contextIndex < originalEventPathSize; contextIndex++) { auto& context = m_path[contextIndex]; if (!context.isMouseOrFocusEventContext()) { ASSERT(context.isWindowContext()); continue; } Node& currentTarget = *context.node(); TreeScope& currentTreeScope = currentTarget.treeScope(); if (UNLIKELY(previousTreeScope && ¤tTreeScope != previousTreeScope)) retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope); Node* currentRelatedNode = retargeter.currentNode(currentTarget); if (UNLIKELY(!originIsRelatedTarget && context.target() == currentRelatedNode)) { m_path.shrink(contextIndex); break; } context.setRelatedTarget(currentRelatedNode); if (UNLIKELY(originIsRelatedTarget && context.node() == &rootNodeInOriginTreeScope)) { m_path.shrink(contextIndex + 1); break; } previousTreeScope = ¤tTreeScope; } } #if ENABLE(TOUCH_EVENTS) void EventPath::retargetTouch(EventContext::TouchListType type, const Touch& touch) { auto* eventTarget = touch.target(); if (!is(eventTarget)) return; RelatedNodeRetargeter retargeter(downcast(*eventTarget), *m_path[0].node()); TreeScope* previousTreeScope = nullptr; for (auto& context : m_path) { Node& currentTarget = *context.node(); TreeScope& currentTreeScope = currentTarget.treeScope(); if (UNLIKELY(previousTreeScope && ¤tTreeScope != previousTreeScope)) retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope); if (context.isTouchEventContext()) { Node* currentRelatedNode = retargeter.currentNode(currentTarget); context.touchList(type).append(touch.cloneWithNewTarget(currentRelatedNode)); } else ASSERT(context.isWindowContext()); previousTreeScope = ¤tTreeScope; } } void EventPath::retargetTouchList(EventContext::TouchListType type, const TouchList* list) { for (unsigned i = 0, length = list ? list->length() : 0; i < length; ++i) retargetTouch(type, *list->item(i)); } void EventPath::retargetTouchLists(const TouchEvent& event) { retargetTouchList(EventContext::TouchListType::Touches, event.touches()); retargetTouchList(EventContext::TouchListType::TargetTouches, event.targetTouches()); retargetTouchList(EventContext::TouchListType::ChangedTouches, event.changedTouches()); } #endif // https://dom.spec.whatwg.org/#dom-event-composedpath // Any node whose depth computed in EventPath::buildPath is greater than the context object is excluded. // Because we can exit out of a closed shadow tree and re-enter another closed shadow tree via a slot, // we decrease the *allowed depth* whenever we moved to a "shallower" (closer-to-document) tree. Vector EventPath::computePathUnclosedToTarget(const EventTarget& target) const { Vector path; auto pathSize = m_path.size(); RELEASE_ASSERT(pathSize); path.reserveInitialCapacity(pathSize); auto currentTargetIndex = m_path.findMatching([&target] (auto& context) { return context.currentTarget() == ⌖ }); RELEASE_ASSERT(currentTargetIndex != notFound); auto currentTargetDepth = m_path[currentTargetIndex].closedShadowDepth(); auto appendTargetWithLesserDepth = [&path] (const EventContext& currentContext, int& currentDepthAllowed) { auto depth = currentContext.closedShadowDepth(); bool contextIsInsideInnerShadowTree = depth > currentDepthAllowed; if (contextIsInsideInnerShadowTree) return; bool movedOutOfShadowTree = depth < currentDepthAllowed; if (movedOutOfShadowTree) currentDepthAllowed = depth; path.uncheckedAppend(currentContext.currentTarget()); }; auto currentDepthAllowed = currentTargetDepth; auto i = currentTargetIndex; do { appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); } while (i--); path.reverse(); currentDepthAllowed = currentTargetDepth; for (auto i = currentTargetIndex + 1; i < pathSize; ++i) appendTargetWithLesserDepth(m_path[i], currentDepthAllowed); return path; } EventPath::EventPath(const Vector& targets) { for (auto* target : targets) { ASSERT(target); ASSERT(!is(target)); m_path.append(EventContext { EventContext::Type::Normal, nullptr, target, *targets.begin(), 0 }); } } static Node* moveOutOfAllShadowRoots(Node& startingNode) { Node* node = &startingNode; while (node->isInShadowTree()) node = downcast(node->treeScope().rootNode()).host(); return node; } RelatedNodeRetargeter::RelatedNodeRetargeter(Node& relatedNode, Node& target) : m_relatedNode(relatedNode) , m_retargetedRelatedNode(&relatedNode) { auto& targetTreeScope = target.treeScope(); TreeScope* currentTreeScope = &m_relatedNode.treeScope(); if (LIKELY(currentTreeScope == &targetTreeScope && target.isConnected() && m_relatedNode.isConnected())) return; if (¤tTreeScope->documentScope() != &targetTreeScope.documentScope()) { m_hasDifferentTreeRoot = true; m_retargetedRelatedNode = nullptr; return; } if (relatedNode.isConnected() != target.isConnected()) { m_hasDifferentTreeRoot = true; m_retargetedRelatedNode = moveOutOfAllShadowRoots(relatedNode); return; } collectTreeScopes(); // FIXME: We should collect this while constructing the event path. Vector targetTreeScopeAncestors; for (TreeScope* currentTreeScope = &targetTreeScope; currentTreeScope; currentTreeScope = currentTreeScope->parentTreeScope()) targetTreeScopeAncestors.append(currentTreeScope); ASSERT_WITH_SECURITY_IMPLICATION(!targetTreeScopeAncestors.isEmpty()); unsigned i = m_ancestorTreeScopes.size(); unsigned j = targetTreeScopeAncestors.size(); ASSERT_WITH_SECURITY_IMPLICATION(m_ancestorTreeScopes.last() == targetTreeScopeAncestors.last()); while (m_ancestorTreeScopes[i - 1] == targetTreeScopeAncestors[j - 1]) { i--; j--; if (!i || !j) break; } bool lowestCommonAncestorIsDocumentScope = i + 1 == m_ancestorTreeScopes.size(); if (lowestCommonAncestorIsDocumentScope && !relatedNode.isConnected() && !target.isConnected()) { Node& relatedNodeAncestorInDocumentScope = i ? *downcast(m_ancestorTreeScopes[i - 1]->rootNode()).shadowHost() : relatedNode; Node& targetAncestorInDocumentScope = j ? *downcast(targetTreeScopeAncestors[j - 1]->rootNode()).shadowHost() : target; if (&targetAncestorInDocumentScope.rootNode() != &relatedNodeAncestorInDocumentScope.rootNode()) { m_hasDifferentTreeRoot = true; m_retargetedRelatedNode = moveOutOfAllShadowRoots(relatedNode); return; } } m_lowestCommonAncestorIndex = i; m_retargetedRelatedNode = nodeInLowestCommonAncestor(); } inline Node* RelatedNodeRetargeter::currentNode(Node& currentTarget) { checkConsistency(currentTarget); return m_retargetedRelatedNode; } void RelatedNodeRetargeter::moveToNewTreeScope(TreeScope* previousTreeScope, TreeScope& newTreeScope) { if (m_hasDifferentTreeRoot) return; auto& currentRelatedNodeScope = m_retargetedRelatedNode->treeScope(); if (previousTreeScope != ¤tRelatedNodeScope) { // currentRelatedNode is still outside our shadow tree. New tree scope may contain currentRelatedNode // but there is no need to re-target it. Moving into a slot (thereby a deeper shadow tree) doesn't matter. return; } bool enteredSlot = newTreeScope.parentTreeScope() == previousTreeScope; if (enteredSlot) { if (m_lowestCommonAncestorIndex) { if (m_ancestorTreeScopes.isEmpty()) collectTreeScopes(); bool relatedNodeIsInSlot = m_ancestorTreeScopes[m_lowestCommonAncestorIndex - 1] == &newTreeScope; if (relatedNodeIsInSlot) { m_lowestCommonAncestorIndex--; m_retargetedRelatedNode = nodeInLowestCommonAncestor(); ASSERT(&newTreeScope == &m_retargetedRelatedNode->treeScope()); } } else ASSERT(m_retargetedRelatedNode == &m_relatedNode); } else { ASSERT(previousTreeScope->parentTreeScope() == &newTreeScope); m_lowestCommonAncestorIndex++; ASSERT_WITH_SECURITY_IMPLICATION(m_ancestorTreeScopes.isEmpty() || m_lowestCommonAncestorIndex < m_ancestorTreeScopes.size()); m_retargetedRelatedNode = downcast(currentRelatedNodeScope.rootNode()).host(); ASSERT(&newTreeScope == &m_retargetedRelatedNode->treeScope()); } } inline Node* RelatedNodeRetargeter::nodeInLowestCommonAncestor() { if (!m_lowestCommonAncestorIndex) return &m_relatedNode; auto& rootNode = m_ancestorTreeScopes[m_lowestCommonAncestorIndex - 1]->rootNode(); return downcast(rootNode).host(); } void RelatedNodeRetargeter::collectTreeScopes() { ASSERT(m_ancestorTreeScopes.isEmpty()); for (TreeScope* currentTreeScope = &m_relatedNode.treeScope(); currentTreeScope; currentTreeScope = currentTreeScope->parentTreeScope()) m_ancestorTreeScopes.append(currentTreeScope); ASSERT_WITH_SECURITY_IMPLICATION(!m_ancestorTreeScopes.isEmpty()); } #if !ASSERT_ENABLED inline void RelatedNodeRetargeter::checkConsistency(Node&) { } #else // ASSERT_ENABLED void RelatedNodeRetargeter::checkConsistency(Node& currentTarget) { if (!m_retargetedRelatedNode) return; ASSERT(!currentTarget.isClosedShadowHidden(*m_retargetedRelatedNode)); ASSERT(m_retargetedRelatedNode == currentTarget.treeScope().retargetToScope(m_relatedNode).ptr()); } #endif // ASSERT_ENABLED }