/* * Copyright (C) 2011-2020 Apple Inc. All rights reserved. * Copyright (C) 2014 Yusuke Suzuki * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "SelectorQuery.h" #include "CSSParser.h" #include "ElementIterator.h" #include "HTMLNames.h" #include "SelectorChecker.h" #include "StaticNodeList.h" #include "StyledElement.h" namespace WebCore { #if ASSERT_ENABLED static bool isSingleTagNameSelector(const CSSSelector& selector) { return selector.isLastInTagHistory() && selector.match() == CSSSelector::Tag; } static bool isSingleClassNameSelector(const CSSSelector& selector) { return selector.isLastInTagHistory() && selector.match() == CSSSelector::Class; } #endif // ASSERT_ENABLED enum class IdMatchingType : uint8_t { None, Rightmost, Filter }; static bool canBeUsedForIdFastPath(const CSSSelector& selector) { return selector.match() == CSSSelector::Id || (selector.match() == CSSSelector::Exact && selector.attribute() == HTMLNames::idAttr && !selector.attributeValueMatchingIsCaseInsensitive()); } static IdMatchingType findIdMatchingType(const CSSSelector& firstSelector) { bool inRightmost = true; for (const CSSSelector* selector = &firstSelector; selector; selector = selector->tagHistory()) { if (canBeUsedForIdFastPath(*selector)) { if (inRightmost) return IdMatchingType::Rightmost; return IdMatchingType::Filter; } if (selector->relation() != CSSSelector::Subselector) inRightmost = false; } return IdMatchingType::None; } SelectorDataList::SelectorDataList(const CSSSelectorList& selectorList) { unsigned selectorCount = 0; for (const CSSSelector* selector = selectorList.first(); selector; selector = CSSSelectorList::next(selector)) selectorCount++; m_selectors.reserveInitialCapacity(selectorCount); for (const CSSSelector* selector = selectorList.first(); selector; selector = CSSSelectorList::next(selector)) m_selectors.uncheckedAppend({ selector }); if (selectorCount == 1) { const CSSSelector& selector = *m_selectors.first().selector; if (selector.isLastInTagHistory()) { switch (selector.match()) { case CSSSelector::Tag: m_matchType = TagNameMatch; break; case CSSSelector::Class: m_matchType = ClassNameMatch; break; default: if (canBeUsedForIdFastPath(selector)) m_matchType = RightMostWithIdMatch; else m_matchType = CompilableSingle; break; } } else { switch (findIdMatchingType(selector)) { case IdMatchingType::None: m_matchType = CompilableSingle; break; case IdMatchingType::Rightmost: m_matchType = RightMostWithIdMatch; break; case IdMatchingType::Filter: m_matchType = CompilableSingleWithRootFilter; break; } } } else m_matchType = CompilableMultipleSelectorMatch; } inline bool SelectorDataList::selectorMatches(const SelectorData& selectorData, Element& element, const ContainerNode& rootNode) const { SelectorChecker selectorChecker(element.document()); SelectorChecker::CheckingContext selectorCheckingContext(SelectorChecker::Mode::QueryingRules); selectorCheckingContext.scope = rootNode.isDocumentNode() ? nullptr : &rootNode; return selectorChecker.match(*selectorData.selector, element, selectorCheckingContext); } inline Element* SelectorDataList::selectorClosest(const SelectorData& selectorData, Element& element, const ContainerNode& rootNode) const { SelectorChecker selectorChecker(element.document()); SelectorChecker::CheckingContext selectorCheckingContext(SelectorChecker::Mode::QueryingRules); selectorCheckingContext.scope = rootNode.isDocumentNode() ? nullptr : &rootNode; if (!selectorChecker.match(*selectorData.selector, element, selectorCheckingContext)) return nullptr; return &element; } bool SelectorDataList::matches(Element& targetElement) const { for (auto& selctor : m_selectors) { if (selectorMatches(selctor, targetElement, targetElement)) return true; } return false; } Element* SelectorDataList::closest(Element& targetElement) const { for (auto& currentElement : lineageOfType(targetElement)) { for (auto& selector : m_selectors) { if (auto* candidateElement = selectorClosest(selector, currentElement, targetElement)) return candidateElement; } } return nullptr; } struct AllElementExtractorSelectorQueryTrait { typedef Vector> OutputType; static const bool shouldOnlyMatchFirstElement = false; ALWAYS_INLINE static void appendOutputForElement(OutputType& output, Element* element) { ASSERT(element); output.append(*element); } }; Ref SelectorDataList::queryAll(ContainerNode& rootNode) const { Vector> result; execute(rootNode, result); return StaticElementList::create(WTFMove(result)); } struct SingleElementExtractorSelectorQueryTrait { typedef Element* OutputType; static const bool shouldOnlyMatchFirstElement = true; ALWAYS_INLINE static void appendOutputForElement(OutputType& output, Element* element) { ASSERT(element); ASSERT(!output); output = element; } }; Element* SelectorDataList::queryFirst(ContainerNode& rootNode) const { Element* result = nullptr; execute(rootNode, result); return result; } static const CSSSelector* selectorForIdLookup(const ContainerNode& rootNode, const CSSSelector& firstSelector) { if (!rootNode.isConnected()) return nullptr; if (rootNode.document().inQuirksMode()) return nullptr; for (const CSSSelector* selector = &firstSelector; selector; selector = selector->tagHistory()) { if (canBeUsedForIdFastPath(*selector)) return selector; if (selector->relation() != CSSSelector::Subselector) break; } return nullptr; } static inline bool isTreeScopeRoot(const ContainerNode& node) { return node.isDocumentNode() || node.isShadowRoot(); } template ALWAYS_INLINE void SelectorDataList::executeFastPathForIdSelector(const ContainerNode& rootNode, const SelectorData& selectorData, const CSSSelector* idSelector, typename SelectorQueryTrait::OutputType& output) const { ASSERT(m_selectors.size() == 1); ASSERT(idSelector); const AtomString& idToMatch = idSelector->value(); if (UNLIKELY(rootNode.treeScope().containsMultipleElementsWithId(idToMatch))) { const Vector* elements = rootNode.treeScope().getAllElementsById(idToMatch); ASSERT(elements); bool rootNodeIsTreeScopeRoot = isTreeScopeRoot(rootNode); for (auto& element : *elements) { if ((rootNodeIsTreeScopeRoot || element->isDescendantOf(rootNode)) && selectorMatches(selectorData, *element, rootNode)) { SelectorQueryTrait::appendOutputForElement(output, element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } return; } Element* element = rootNode.treeScope().getElementById(idToMatch); if (!element || !(isTreeScopeRoot(rootNode) || element->isDescendantOf(rootNode))) return; if (selectorMatches(selectorData, *element, rootNode)) SelectorQueryTrait::appendOutputForElement(output, element); } static ContainerNode& filterRootById(ContainerNode& rootNode, const CSSSelector& firstSelector) { if (!rootNode.isConnected()) return rootNode; if (rootNode.document().inQuirksMode()) return rootNode; // If there was an Id match in the rightmost Simple Selector, we should be in a RightMostWithIdMatch, not in filter. // Thus we can skip the rightmost match. const CSSSelector* selector = &firstSelector; do { ASSERT(!canBeUsedForIdFastPath(*selector)); if (selector->relation() != CSSSelector::Subselector) break; selector = selector->tagHistory(); } while (selector); bool inAdjacentChain = false; for (; selector; selector = selector->tagHistory()) { if (canBeUsedForIdFastPath(*selector)) { const AtomString& idToMatch = selector->value(); if (ContainerNode* searchRoot = rootNode.treeScope().getElementById(idToMatch)) { if (LIKELY(!rootNode.treeScope().containsMultipleElementsWithId(idToMatch))) { if (inAdjacentChain) searchRoot = searchRoot->parentNode(); if (searchRoot && (isTreeScopeRoot(rootNode) || searchRoot == &rootNode || searchRoot->isDescendantOf(rootNode))) return *searchRoot; } } } if (selector->relation() == CSSSelector::Subselector) continue; if (selector->relation() == CSSSelector::DirectAdjacent || selector->relation() == CSSSelector::IndirectAdjacent) inAdjacentChain = true; else inAdjacentChain = false; } return rootNode; } static ALWAYS_INLINE bool localNameMatches(const Element& element, const AtomString& localName, const AtomString& lowercaseLocalName) { if (element.isHTMLElement() && element.document().isHTMLDocument()) return element.localName() == lowercaseLocalName; return element.localName() == localName; } template static inline void elementsForLocalName(const ContainerNode& rootNode, const AtomString& localName, const AtomString& lowercaseLocalName, typename SelectorQueryTrait::OutputType& output) { if (localName == lowercaseLocalName) { for (auto& element : descendantsOfType(const_cast(rootNode))) { if (element.tagQName().localName() == localName) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } else { for (auto& element : descendantsOfType(const_cast(rootNode))) { if (localNameMatches(element, localName, lowercaseLocalName)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } } template static inline void anyElement(const ContainerNode& rootNode, typename SelectorQueryTrait::OutputType& output) { for (auto& element : descendantsOfType(const_cast(rootNode))) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } template ALWAYS_INLINE void SelectorDataList::executeSingleTagNameSelectorData(const ContainerNode& rootNode, const SelectorData& selectorData, typename SelectorQueryTrait::OutputType& output) const { ASSERT(m_selectors.size() == 1); ASSERT(isSingleTagNameSelector(*selectorData.selector)); const QualifiedName& tagQualifiedName = selectorData.selector->tagQName(); const AtomString& selectorLocalName = tagQualifiedName.localName(); const AtomString& selectorLowercaseLocalName = selectorData.selector->tagLowercaseLocalName(); const AtomString& selectorNamespaceURI = tagQualifiedName.namespaceURI(); if (selectorNamespaceURI == starAtom()) { if (selectorLocalName != starAtom()) { // Common case: name defined, selectorNamespaceURI is a wildcard. elementsForLocalName(rootNode, selectorLocalName, selectorLowercaseLocalName, output); } else { // Other fairly common case: both are wildcards. anyElement(rootNode, output); } } else { // Fallback: NamespaceURI is set, selectorLocalName may be starAtom(). for (auto& element : descendantsOfType(const_cast(rootNode))) { if (element.namespaceURI() == selectorNamespaceURI && localNameMatches(element, selectorLocalName, selectorLowercaseLocalName)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } } template ALWAYS_INLINE void SelectorDataList::executeSingleClassNameSelectorData(const ContainerNode& rootNode, const SelectorData& selectorData, typename SelectorQueryTrait::OutputType& output) const { ASSERT(m_selectors.size() == 1); ASSERT(isSingleClassNameSelector(*selectorData.selector)); const AtomString& className = selectorData.selector->value(); for (auto& element : descendantsOfType(const_cast(rootNode))) { if (element.hasClass() && element.classNames().contains(className)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } template ALWAYS_INLINE void SelectorDataList::executeSingleSelectorData(const ContainerNode& rootNode, const ContainerNode& searchRootNode, const SelectorData& selectorData, typename SelectorQueryTrait::OutputType& output) const { ASSERT(m_selectors.size() == 1); for (auto& element : descendantsOfType(const_cast(searchRootNode))) { if (selectorMatches(selectorData, element, rootNode)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } template ALWAYS_INLINE void SelectorDataList::executeSingleMultiSelectorData(const ContainerNode& rootNode, typename SelectorQueryTrait::OutputType& output) const { for (auto& element : descendantsOfType(const_cast(rootNode))) { for (auto& selector : m_selectors) { if (selectorMatches(selector, element, rootNode)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; break; } } } } #if ENABLE(CSS_SELECTOR_JIT) template ALWAYS_INLINE void SelectorDataList::executeCompiledSimpleSelectorChecker(const ContainerNode& searchRootNode, Checker selectorChecker, typename SelectorQueryTrait::OutputType& output, const SelectorData& selectorData) const { for (auto& element : descendantsOfType(const_cast(searchRootNode))) { selectorData.compiledSelector.wasUsed(); if (selectorChecker(&element)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } template ALWAYS_INLINE void SelectorDataList::executeCompiledSelectorCheckerWithCheckingContext(const ContainerNode& rootNode, const ContainerNode& searchRootNode, Checker selectorChecker, typename SelectorQueryTrait::OutputType& output, const SelectorData& selectorData) const { SelectorChecker::CheckingContext checkingContext(SelectorChecker::Mode::QueryingRules); checkingContext.scope = rootNode.isDocumentNode() ? nullptr : &rootNode; for (auto& element : descendantsOfType(const_cast(searchRootNode))) { selectorData.compiledSelector.wasUsed(); if (selectorChecker(&element, &checkingContext)) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; } } } template ALWAYS_INLINE void SelectorDataList::executeCompiledSingleMultiSelectorData(const ContainerNode& rootNode, typename SelectorQueryTrait::OutputType& output) const { SelectorChecker::CheckingContext checkingContext(SelectorChecker::Mode::QueryingRules); checkingContext.scope = rootNode.isDocumentNode() ? nullptr : &rootNode; for (auto& element : descendantsOfType(const_cast(rootNode))) { for (auto& selector : m_selectors) { selector.compiledSelector.wasUsed(); bool matched = false; if (selector.compiledSelector.status == SelectorCompilationStatus::SimpleSelectorChecker) matched = SelectorCompiler::querySelectorSimpleSelectorChecker(selector.compiledSelector, &element); else { ASSERT(selector.compiledSelector.status == SelectorCompilationStatus::SelectorCheckerWithCheckingContext); matched = SelectorCompiler::querySelectorSelectorCheckerWithCheckingContext(selector.compiledSelector, &element, &checkingContext); } if (matched) { SelectorQueryTrait::appendOutputForElement(output, &element); if (SelectorQueryTrait::shouldOnlyMatchFirstElement) return; break; } } } } bool SelectorDataList::compileSelector(const SelectorData& selectorData) { auto& compiledSelector = selectorData.compiledSelector; if (compiledSelector.status == SelectorCompilationStatus::NotCompiled) SelectorCompiler::compileSelector(compiledSelector, selectorData.selector, SelectorCompiler::SelectorContext::QuerySelector); return compiledSelector.status != SelectorCompilationStatus::CannotCompile; } #endif // ENABLE(CSS_SELECTOR_JIT) template ALWAYS_INLINE void SelectorDataList::execute(ContainerNode& rootNode, typename SelectorQueryTrait::OutputType& output) const { ContainerNode* searchRootNode = &rootNode; switch (m_matchType) { case RightMostWithIdMatch: { const SelectorData& selectorData = m_selectors.first(); if (const CSSSelector* idSelector = selectorForIdLookup(*searchRootNode, *selectorData.selector)) { executeFastPathForIdSelector(*searchRootNode, m_selectors.first(), idSelector, output); break; } #if ENABLE(CSS_SELECTOR_JIT) if (compileSelector(selectorData)) goto CompiledSingleCase; #endif // ENABLE(CSS_SELECTOR_JIT) goto SingleSelectorCase; ASSERT_NOT_REACHED(); } case CompilableSingleWithRootFilter: case CompilableSingle: { #if ENABLE(CSS_SELECTOR_JIT) const SelectorData& selectorData = m_selectors.first(); ASSERT(selectorData.compiledSelector.status == SelectorCompilationStatus::NotCompiled); ASSERT(m_matchType == CompilableSingle || m_matchType == CompilableSingleWithRootFilter); if (compileSelector(selectorData)) { if (m_matchType == CompilableSingle) { m_matchType = CompiledSingle; goto CompiledSingleCase; } ASSERT(m_matchType == CompilableSingleWithRootFilter); m_matchType = CompiledSingleWithRootFilter; goto CompiledSingleWithRootFilterCase; } #endif // ENABLE(CSS_SELECTOR_JIT) if (m_matchType == CompilableSingle) { m_matchType = SingleSelector; goto SingleSelectorCase; } ASSERT(m_matchType == CompilableSingleWithRootFilter); m_matchType = SingleSelectorWithRootFilter; goto SingleSelectorWithRootFilterCase; ASSERT_NOT_REACHED(); } #if ENABLE(CSS_SELECTOR_JIT) case CompiledSingleWithRootFilter: CompiledSingleWithRootFilterCase: searchRootNode = &filterRootById(*searchRootNode, *m_selectors.first().selector); FALLTHROUGH; case CompiledSingle: { CompiledSingleCase: const SelectorData& selectorData = m_selectors.first(); if (selectorData.compiledSelector.status == SelectorCompilationStatus::SimpleSelectorChecker) { executeCompiledSimpleSelectorChecker(*searchRootNode, [&] (const Element* element) { return SelectorCompiler::querySelectorSimpleSelectorChecker(selectorData.compiledSelector, element); }, output, selectorData); } else { ASSERT(selectorData.compiledSelector.status == SelectorCompilationStatus::SelectorCheckerWithCheckingContext); executeCompiledSelectorCheckerWithCheckingContext(rootNode, *searchRootNode, [&] (const Element* element, const SelectorChecker::CheckingContext* context) { return SelectorCompiler::querySelectorSelectorCheckerWithCheckingContext(selectorData.compiledSelector, element, context); }, output, selectorData); } break; } #else case CompiledSingleWithRootFilter: case CompiledSingle: ASSERT_NOT_REACHED(); #if !ASSERT_ENABLED FALLTHROUGH; #endif #endif // ENABLE(CSS_SELECTOR_JIT) case SingleSelectorWithRootFilter: SingleSelectorWithRootFilterCase: searchRootNode = &filterRootById(*searchRootNode, *m_selectors.first().selector); FALLTHROUGH; case SingleSelector: SingleSelectorCase: executeSingleSelectorData(rootNode, *searchRootNode, m_selectors.first(), output); break; case TagNameMatch: executeSingleTagNameSelectorData(*searchRootNode, m_selectors.first(), output); break; case ClassNameMatch: executeSingleClassNameSelectorData(*searchRootNode, m_selectors.first(), output); break; case CompilableMultipleSelectorMatch: #if ENABLE(CSS_SELECTOR_JIT) { for (auto& selector : m_selectors) { if (!compileSelector(selector)) { m_matchType = MultipleSelectorMatch; goto MultipleSelectorMatch; } } m_matchType = CompiledMultipleSelectorMatch; goto CompiledMultipleSelectorMatch; } #else FALLTHROUGH; #endif // ENABLE(CSS_SELECTOR_JIT) case CompiledMultipleSelectorMatch: #if ENABLE(CSS_SELECTOR_JIT) CompiledMultipleSelectorMatch: executeCompiledSingleMultiSelectorData(*searchRootNode, output); break; #else FALLTHROUGH; #endif // ENABLE(CSS_SELECTOR_JIT) case MultipleSelectorMatch: #if ENABLE(CSS_SELECTOR_JIT) MultipleSelectorMatch: #endif executeSingleMultiSelectorData(*searchRootNode, output); break; } } SelectorQuery::SelectorQuery(CSSSelectorList&& selectorList) : m_selectorList(WTFMove(selectorList)) , m_selectors(m_selectorList) { } ExceptionOr SelectorQueryCache::add(const String& selectors, Document& document) { if (auto* entry = m_entries.get(selectors)) return *entry; CSSParser parser(document); auto selectorList = parser.parseSelector(selectors); if (!selectorList || selectorList->hasInvalidSelector()) return Exception { SyntaxError }; if (selectorList->selectorsNeedNamespaceResolution()) return Exception { SyntaxError }; const int maximumSelectorQueryCacheSize = 256; if (m_entries.size() == maximumSelectorQueryCacheSize) m_entries.remove(m_entries.random()); return *m_entries.add(selectors, makeUnique(WTFMove(*selectorList))).iterator->value; } }