Bug Summary

File:Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore/page/text-extraction/TextExtraction.cpp
Warning:line 1393, column 24
Call argument for 'this' parameter is unchecked and unsafe

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple arm64-apple-ios26.2.0-simulator -O3 -Wundef-prefix=TARGET_OS_ -Werror=undef-prefix -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -Werror=implicit-function-declaration -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name UnifiedSource317.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=osx -analyzer-checker=security.insecureAPI.decodeValueOfObjCType -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -analyzer-config-compatibility-mode=true -mrelocation-model pic -pic-level 2 -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -target-sdk-version=26.2 -fdepfile-entry=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/SDKSettings.json -fcompatibility-qualified-id-block-type-checking -fvisibility-inlines-hidden-static-local-var -fno-modulemap-allow-subdirectory-search -fdefine-target-os-macros -enable-tlsdesc -target-cpu apple-m1 -target-feature +v8.4a -target-feature +aes -target-feature +altnzcv -target-feature +ccdp -target-feature +ccpp -target-feature +complxnum -target-feature +crc -target-feature +dotprod -target-feature +flagm -target-feature +fp-armv8 -target-feature +fp16fml -target-feature +fptoint -target-feature +fullfp16 -target-feature +jsconv -target-feature +lse -target-feature +neon -target-feature +pauth -target-feature +perfmon -target-feature +predres -target-feature +ras -target-feature +rcpc -target-feature +rdm -target-feature +sb -target-feature +sha2 -target-feature +sha3 -target-feature +specrestrict -target-feature +ssbs -target-abi darwinpcs -dwarf-ext-refs -fmodule-format=obj -debug-info-kind=standalone -dwarf-version=5 -debugger-tuning=lldb -fdebug-compilation-dir=/Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore -target-linker-version 1230.1 -fcoverage-compilation-dir=/Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore -resource-dir /Users/buildbot/Library/Developer/Toolchains/swift-webkit.xctoolchain/usr/lib/clang/21 -dependency-file /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/scan-build-output/StaticAnalyzer/WebCore/WebCore/normal/arm64/UnifiedSource317.d -skip-unused-modulemap-deps -MT dependencies -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk -iquote /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/WebCore-generated-files.hmap -iquote /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/WebCore-project-headers.hmap -isystem /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore/PAL/ThirdParty/dav1d/include -isystem /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/SDKAdditions/usr/include -iframework /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/SDKAdditions/System/Library/Frameworks -iframework /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitLibraries/SDKs/iphonesimulator26.0-additions.sdk/System/Library/PrivateFrameworks -iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/System/Library/Frameworks -include-pch /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/PrecompiledHeaders/SharedPrecompiledHeaders/11721353671419897710/WebCorePrefix.h.gch -D _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE -D NDEBUG -D BUILDING_WEBKIT -D GL_SILENCE_DEPRECATION=1 -D GLES_SILENCE_DEPRECATION=1 -D __clang_analyzer__ -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/WebCore-own-target-headers.hmap -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/WebCore-all-target-headers.hmap -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/include -I PAL -I ForwardingHeaders -I /usr/include/libxslt -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/include/libxml2 -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/DerivedSources/WebCore -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/usr/local/include -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/usr/local/include/WebKitAdditions -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/local/include/WebKitAdditions -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/usr/local/include -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitLibraries/SDKs/iphonesimulator26.0-additions.sdk/usr/local/include -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/local/include -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/usr/local/include/webrtc -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/local/include/webrtc -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/usr/local/include/webm -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/local/include/webm -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/PAL.build/PAL.build/DerivedSources -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/PAL.build/Release-iphonesimulator/PAL.build/DerivedSources -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/DerivedSources-normal/arm64 -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/DerivedSources/arm64 -I /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release-iphonesimulator/WebCore.build/DerivedSources -F/Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator -F/Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator -I/usr/local/include -stdlib=libc++ -internal-isystem /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/include/c++/v1 -internal-isystem /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/local/include -internal-isystem /Users/buildbot/Library/Developer/Toolchains/swift-webkit.xctoolchain/usr/lib/clang/21/include -internal-externc-isystem /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/usr/include -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/System/Library/Frameworks -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/System/Library/SubFrameworks -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.2.sdk/Library/Frameworks -Wno-trigraphs -Werror -Wno-missing-field-initializers -Wmissing-prototypes -Wunreachable-code -Wnon-virtual-dtor -Wno-overloaded-virtual -Wno-exit-time-destructors -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wuninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wsign-compare -Wno-shorten-64-to-32 -Wnewline-eof -Wno-c++11-extensions -Wno-implicit-fallthrough -Wdeprecated-declarations -Winvalid-offsetof -Wno-sign-conversion -Winfinite-recursion -Wmove -Wcomma -Wblock-capture-autoreleasing -Wstrict-prototypes -Wrange-loop-analysis -Wno-semicolon-before-method-body -Wall -Wc99-designator -Wconditional-uninitialized -Wextra -Wdeprecated-enum-enum-conversion -Wdeprecated-enum-float-conversion -Wenum-float-conversion -Wfinal-dtor-non-final-class -Wformat=2 -Wmisleading-indentation -Wpointer-to-int-cast -Wreorder-init-list -Wundef -Wunused-but-set-variable -Wvla -Werror=unguarded-availability -Wno-elaborated-enum-base -Wcast-qual -Wchar-subscripts -Wextra-tokens -Winit-self -Wmissing-noreturn -Wpacked -Wpointer-arith -Wredundant-decls -Wwrite-strings -Wexit-time-destructors -Wglobal-constructors -Wtautological-compare -Wimplicit-fallthrough -Wvla -Wno-unknown-warning-option -Wliteral-conversion -Wthread-safety -Wno-profile-instr-out-of-date -Wno-profile-instr-unprofiled -Wno-backend-plugin -Wunsafe-buffer-usage -std=c++2b -fdeprecated-macro -ferror-limit 19 -fmacro-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden -stack-protector 1 -fblocks -fencode-extended-block-signature -fno-rtti -fregister-global-dtors-with-atexit -fgnuc-version=4.2.1 -fno-cxx-modules -fno-implicit-modules -fskip-odr-check-in-gmf -fpascal-strings -fmax-type-align=16 -fdiagnostics-show-note-include-stack -vectorize-loops -vectorize-slp -fsafe-buffer-usage-suggestions -analyzer-output=html -analyzer-config report-in-main-source-file=true -analyzer-config nullability:NoDiagnoseCallsToSystemHeaders=true -analyzer-checker optin.osx.cocoa.localizability.NonLocalizedStringChecker -analyzer-checker security.insecureAPI.UncheckedReturn -analyzer-checker security.insecureAPI.getpw -analyzer-checker security.insecureAPI.gets -analyzer-checker security.insecureAPI.mkstemp -analyzer-checker security.insecureAPI.mktemp -analyzer-disable-checker security.insecureAPI.rand -analyzer-disable-checker security.insecureAPI.strcpy -analyzer-checker security.insecureAPI.vfork -analyzer-disable-checker alpha,apiModeling,core,cplusplus,deadcode,debug,fuchsia,nullability,optin,osx,security,unix,webkit -analyzer-checker alpha.webkit.ForwardDeclChecker,alpha.webkit.MemoryUnsafeCastChecker,alpha.webkit.NoDeleteChecker,alpha.webkit.NoUncheckedPtrMemberChecker,alpha.webkit.NoUnretainedMemberChecker,alpha.webkit.RetainPtrCtorAdoptChecker,alpha.webkit.UncheckedCallArgsChecker,alpha.webkit.UncheckedLocalVarsChecker,alpha.webkit.UncountedCallArgsChecker,alpha.webkit.UncountedLocalVarsChecker,alpha.webkit.UnretainedCallArgsChecker,alpha.webkit.UnretainedLambdaCapturesChecker,alpha.webkit.UnretainedLocalVarsChecker,webkit.NoUncountedMemberChecker,webkit.RefCntblBaseVirtualDtor,webkit.UncountedLambdaCapturesChecker -analyzer-config max-nodes=10000000 -analyzer-config verbose-report-filename=true -fdwarf2-cfi-asm -o /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/scan-build-output/StaticAnalyzer/WebCore/WebCore/normal/arm64/UnifiedSource317.plist -x c++ /Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/WebKitBuild/Release-iphonesimulator/DerivedSources/WebCore/unified-sources/UnifiedSource317.cpp
1/*
2 * Copyright (C) 2024-2025 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "TextExtraction.h"
28
29#include "AXObjectCache.h"
30#include "AccessibilityObject.h"
31#include "BoundaryPointInlines.h"
32#include "CommonVM.h"
33#include "ComposedTreeIterator.h"
34#include "ContainerNodeInlines.h"
35#include "DocumentPage.h"
36#include "DocumentSecurityOrigin.h"
37#include "DocumentView.h"
38#include "Editing.h"
39#include "Editor.h"
40#include "ElementAncestorIteratorInlines.h"
41#include "ElementInlines.h"
42#include "EventHandler.h"
43#include "EventListenerMap.h"
44#include "EventNames.h"
45#include "EventTargetInlines.h"
46#include "ExceptionCode.h"
47#include "ExceptionOr.h"
48#include "FocusController.h"
49#include "FrameSelection.h"
50#include "GeometryUtilities.h"
51#include "HTMLAnchorElement.h"
52#include "HTMLBodyElement.h"
53#include "HTMLButtonElement.h"
54#include "HTMLCanvasElement.h"
55#include "HTMLFrameOwnerElement.h"
56#include "HTMLIFrameElement.h"
57#include "HTMLImageElement.h"
58#include "HTMLInputElement.h"
59#include "HTMLNames.h"
60#include "HTMLOptionElement.h"
61#include "HTMLSelectElement.h"
62#include "HandleUserInputEventResult.h"
63#include "HighlightRegistry.h"
64#include "HitTestResult.h"
65#include "Image.h"
66#include "ImageOverlay.h"
67#include "JSNode.h"
68#include "LocalFrame.h"
69#include "NodeList.h"
70#include "Page.h"
71#include "PlatformKeyboardEvent.h"
72#include "PlatformMouseEvent.h"
73#include "PositionInlines.h"
74#include "RenderBox.h"
75#include "RenderDescendantIterator.h"
76#include "RenderElementInlines.h"
77#include "RenderIFrame.h"
78#include "RenderLayer.h"
79#include "RenderLayerModelObject.h"
80#include "RenderLayerScrollableArea.h"
81#include "RenderObjectInlines.h"
82#include "RenderView.h"
83#include "RunJavaScriptParameters.h"
84#include "ScriptController.h"
85#include "Settings.h"
86#include "SimpleRange.h"
87#include "StaticRange.h"
88#include "StringEntropyHelpers.h"
89#include "StyleTextDecorationLine.h"
90#include "Text.h"
91#include "TextIterator.h"
92#include "TypedElementDescendantIteratorInlines.h"
93#include "UserGestureIndicator.h"
94#include "UserTypingGestureIndicator.h"
95#include "VisibleSelection.h"
96#include "WritingMode.h"
97#include <JavaScriptCore/JSCInlines.h>
98#include <JavaScriptCore/JSCJSValue.h>
99#include <JavaScriptCore/JSLock.h>
100#include <JavaScriptCore/JSString.h>
101#include <JavaScriptCore/RegularExpression.h>
102#include <ranges>
103#include <unicode/uchar.h>
104#include <wtf/CallbackAggregator.h>
105#include <wtf/Scope.h>
106#include <wtf/text/MakeString.h>
107#include <wtf/text/StringBuilder.h>
108
109#if ENABLE(DATA_DETECTION)(defined 1 && 1)
110#include "DataDetection.h"
111#endif
112
113namespace WebCore {
114namespace TextExtraction {
115
116using namespace JSC;
117
118static String normalizeText(const String& string, unsigned maxDescriptionLength = 512)
119{
120 auto result = foldQuoteMarks(string);
121 result = makeStringByReplacingAll(result, '"', "'"_s);
122 result = makeStringByReplacingAll(result, '\r', ""_s);
123 result = makeStringByReplacingAll(result, '\n', " "_s);
124 result = result.simplifyWhiteSpace(isASCIIWhitespace);
125 if (result.length() <= maxDescriptionLength)
126 return result;
127
128 return makeString(result.left(maxDescriptionLength / 2 - 2), "..."_s, result.right(maxDescriptionLength / 2 - 1));
129}
130
131static constexpr auto minOpacityToConsiderVisible = 0.05;
132
133enum class IncludeTextInAutoFilledControls : bool { No, Yes };
134
135using TextNodesAndText = Vector<std::pair<Ref<Text>, String>>;
136using TextAndSelectedRange = std::pair<String, std::optional<CharacterRange>>;
137using TextAndSelectedRangeMap = HashMap<Ref<Text>, TextAndSelectedRange>;
138
139static bool hasEnclosingAutoFilledInput(Node& node)
140{
141 RefPtr input = dynamicDowncast<HTMLInputElement>(node.shadowHost());
142 if (!input)
143 return false;
144
145 return input->autofilled() || input->autofilledAndViewable() || input->autofilledAndObscured();
146}
147
148static inline TextNodesAndText collectText(const SimpleRange& range, IncludeTextInAutoFilledControls includeTextInAutoFilledControls)
149{
150 TextNodesAndText nodesAndText;
151 RefPtr<Text> lastTextNode;
152 StringBuilder textForLastTextNode;
153
154 auto emitTextForLastNode = [&] {
155 auto text = makeStringByReplacingAll(textForLastTextNode.toString(), noBreakSpace, ' ');
156 if (text.isEmpty())
157 return;
158 nodesAndText.append({ lastTextNode.releaseNonNull(), WTF::move(text) });
159 };
160
161 for (TextIterator iterator { range, TextIteratorBehavior::EntersTextControls }; !iterator.atEnd(); iterator.advance()) {
162 if (iterator.text().isEmpty())
163 continue;
164
165 RefPtr node = iterator.node();
166 if (!node) {
167 textForLastTextNode.append(iterator.text());
168 continue;
169 }
170
171 if (includeTextInAutoFilledControls == IncludeTextInAutoFilledControls::No && hasEnclosingAutoFilledInput(*node))
172 continue;
173
174 RefPtr textNode = dynamicDowncast<Text>(*node);
175 if (!textNode) {
176 textForLastTextNode.append(iterator.text());
177 continue;
178 }
179
180 if (!lastTextNode)
181 lastTextNode = textNode;
182
183 if (lastTextNode == textNode) {
184 textForLastTextNode.append(iterator.text());
185 continue;
186 }
187
188 emitTextForLastNode();
189 textForLastTextNode.clear();
190 textForLastTextNode.append(iterator.text());
191 lastTextNode = textNode;
192 }
193
194 if (lastTextNode)
195 emitTextForLastNode();
196
197 return nodesAndText;
198}
199
200static String stringOnlyIfHumanReadable(const String& string)
201{
202 if (StringEntropyHelpers::isProbablyHumanReadable(string))
203 return string;
204 return { };
205}
206
207static void addBoxShadowIfNeeded(Node& node, const String& colorAsString)
208{
209 Ref document = node.document();
210 if (!document->settings().textExtractionDebugUIEnabled())
211 return;
212
213 RefPtr element = lineageOfType<HTMLElement>(node).first();
214 if (!element)
215 return;
216
217 if (element == document->body())
218 return;
219
220 auto styleValue = makeString("0 0 20px "_s, colorAsString, ", inset 0 0 8px "_s, colorAsString);
221 element->setInlineStyleProperty(CSSPropertyBoxShadow, styleValue, IsImportant::Yes);
222
223 auto relatedElementAttributes = std::array {
224 HTMLNames::aria_labeledbyAttr.get(),
225 HTMLNames::aria_labelledbyAttr.get(),
226 HTMLNames::aria_describedbyAttr.get()
227 };
228
229 for (auto& attributeName : relatedElementAttributes) {
230 if (RefPtr otherElement = dynamicDowncast<HTMLElement>(element->elementForAttributeInternal(attributeName).get()))
231 otherElement->setInlineStyleProperty(CSSPropertyBoxShadow, styleValue, IsImportant::Yes);
232 }
233}
234
235using ClientNodeAttributesMap = WeakHashMap<Node, HashMap<String, String>, WeakPtrImplWithEventTargetData>;
236
237struct TraversalContext {
238 const Request originalRequest;
239 const ClientNodeAttributesMap clientNodeAttributes;
240 const TextAndSelectedRangeMap visibleText;
241 const WeakHashSet<Node, WeakPtrImplWithEventTargetData> nodesToSkip;
242 const std::optional<FloatRect> rectInRootView;
243 const FrameIdentifier frameIdentifier;
244 Vector<WeakPtr<Node, WeakPtrImplWithEventTargetData>> enclosingBlocks;
245 WeakHashMap<Node, unsigned, WeakPtrImplWithEventTargetData> enclosingBlockNumberMap;
246 WeakHashSet<Node, WeakPtrImplWithEventTargetData> additionalContainersToCollect;
247 unsigned inAdditionalContainerToCollectCount { 0 };
248 Vector<bool, 1> hasOverflowItemsStack;
249 unsigned onlyCollectTextAndLinksCount { 0 };
250 bool mergeParagraphs { false };
251 bool skipNearlyTransparentContent { false };
252 NodeIdentifierInclusion nodeIdentifierInclusion { NodeIdentifierInclusion::None };
253 bool includeAccessibilityAttributes { false };
254 unsigned visibleTextLength { 0 };
255
256 inline bool shouldIncludeNodeWithRect(const FloatRect& rect) const
257 {
258 return !rectInRootView || rectInRootView->intersects(rect);
259 }
260
261 void pushEnclosingBlock(const Node& node)
262 {
263 enclosingBlocks.append(node);
264 enclosingBlockNumberMap.add(node, 1 + enclosingBlockNumberMap.computeSize());
265 }
266
267 unsigned enclosingBlockNumber() const
268 {
269 if (enclosingBlocks.isEmpty())
270 return 0;
271
272 return enclosingBlockNumberMap.get(*enclosingBlocks.last());
273 }
274
275 void NODELETE[[clang::annotate_type("webkit.nodelete")]] popEnclosingBlock()
276 {
277 enclosingBlocks.removeLast();
278 }
279};
280
281static inline TextAndSelectedRangeMap collectText(Node& node, IncludeTextInAutoFilledControls includeTextInAutoFilledControls)
282{
283 auto nodeRange = makeRangeSelectingNodeContents(node);
284 auto selection = node.document().selection().selection();
285 TextNodesAndText textBeforeRangedSelection;
286 TextNodesAndText textInRangedSelection;
287 TextNodesAndText textAfterRangedSelection;
288 bool populatedRangesAroundSelection = [&] {
289 if (!selection.isRange())
290 return false;
291
292 auto selectionStart = makeBoundaryPoint(selection.start());
293 auto selectionEnd = makeBoundaryPoint(selection.end());
294 if (!selectionStart || !selectionEnd)
295 return false;
296
297 if (is_lt(treeOrder(*selectionStart, nodeRange.start)))
298 selectionStart = { nodeRange.start };
299
300 if (is_gt(treeOrder(*selectionEnd, nodeRange.end)))
301 selectionEnd = { nodeRange.end };
302
303 auto rangeBeforeSelection = makeSimpleRange(nodeRange.start, *selectionStart);
304 auto selectionRange = makeSimpleRange(*selectionStart, *selectionEnd);
305 auto rangeAfterSelection = makeSimpleRange(*selectionEnd, nodeRange.end);
306 textBeforeRangedSelection = collectText(rangeBeforeSelection, includeTextInAutoFilledControls);
307 textInRangedSelection = collectText(selectionRange, includeTextInAutoFilledControls);
308 textAfterRangedSelection = collectText(rangeAfterSelection, includeTextInAutoFilledControls);
309 return true;
310 }();
311
312 if (!populatedRangesAroundSelection) {
313 // Fall back to collecting the full contents of the node.
314 textBeforeRangedSelection = collectText(nodeRange, includeTextInAutoFilledControls);
315 }
316
317 TextAndSelectedRangeMap result;
318 for (auto& [node, text] : textBeforeRangedSelection)
319 result.add(node, TextAndSelectedRange { text, { } });
320
321 bool isFirstSelectedNode = true;
322 for (auto& [node, text] : textInRangedSelection) {
323 if (std::exchange(isFirstSelectedNode, false)) {
324 if (auto entry = result.find(node.ptr()); entry != result.end() && entry->key.ptr() == node.ptr()) {
325 entry->value = std::make_pair(
326 makeString(entry->value.first, text),
327 CharacterRange { entry->value.first.length(), text.length() }
328 );
329 continue;
330 }
331 }
332 result.add(node, TextAndSelectedRange { text, CharacterRange { 0, text.length() } });
333 }
334
335 bool isFirstNodeAfterSelection = true;
336 for (auto& [node, text] : textAfterRangedSelection) {
337 if (std::exchange(isFirstNodeAfterSelection, false)) {
338 if (auto entry = result.find(node.ptr()); entry != result.end() && entry->key.ptr() == node.ptr()) {
339 entry->value.first = makeString(entry->value.first, text);
340 continue;
341 }
342 }
343 result.add(node, TextAndSelectedRange { text, std::nullopt });
344 }
345
346 return result;
347}
348
349static inline bool canMerge(const TraversalContext& context, const Item& destinationItem, const Item& sourceItem)
350{
351 if (!destinationItem.children.isEmpty() || !sourceItem.children.isEmpty())
352 return false;
353
354 if (!context.mergeParagraphs && destinationItem.enclosingBlockNumber != sourceItem.enclosingBlockNumber)
355 return false;
356
357 if (!std::holds_alternative<TextItemData>(destinationItem.data) || !std::holds_alternative<TextItemData>(sourceItem.data))
358 return false;
359
360 if (destinationItem.hasLineThrough != sourceItem.hasLineThrough)
361 return false;
362
363 // Don't merge adjacent text runs if they represent two different editable roots.
364 auto& destination = std::get<TextItemData>(destinationItem.data);
365 auto& source = std::get<TextItemData>(sourceItem.data);
366 return !destination.editable && !source.editable;
367}
368
369static inline void merge(Item& destinationItem, Item&& sourceItem)
370{
371 auto& destination = std::get<TextItemData>(destinationItem.data);
372 auto& source = std::get<TextItemData>(sourceItem.data);
373
374 destinationItem.rectInRootView.unite(sourceItem.rectInRootView);
375
376 auto originalContentLength = destination.content.length();
377 destination.content = makeString(destination.content, WTF::move(source.content));
378
379 if (source.selectedRange) {
380 CharacterRange newSelectedRange;
381 if (destination.selectedRange)
382 newSelectedRange = { destination.selectedRange->location, destination.selectedRange->length + source.selectedRange->length };
383 else
384 newSelectedRange = { originalContentLength + source.selectedRange->location, source.selectedRange->length };
385 destination.selectedRange = WTF::move(newSelectedRange);
386 }
387
388 if (!source.links.isEmpty()) {
389 for (auto& [url, range] : source.links)
390 range.location += originalContentLength;
391 destination.links.appendVector(WTF::move(source.links));
392 }
393}
394
395static inline FloatRect rootViewBounds(Node& node)
396{
397 RefPtr view = node.document().view();
398 if (!view) [[unlikely]]
399 return { };
400
401 CheckedPtr renderer = node.renderer();
402 if (!renderer)
403 return { };
404
405 IntRect absoluteRect;
406 if (CheckedPtr renderElement = dynamicDowncast<RenderElement>(*renderer); renderElement && renderElement->firstChild())
407 absoluteRect = renderer->pixelSnappedAbsoluteClippedOverflowRect();
408
409 if (absoluteRect.isEmpty())
410 absoluteRect = renderer->absoluteBoundingBoxRect();
411
412 return view->contentsToRootView(absoluteRect);
413}
414
415static inline String labelText(HTMLElement& element)
416{
417 auto labels = element.labels();
418 if (!labels)
419 return { };
420
421 RefPtr<Element> firstRenderedLabel;
422 for (unsigned index = 0; index < labels->length(); ++index) {
423 if (RefPtr label = dynamicDowncast<Element>(labels->item(index)); label && label->renderer())
424 firstRenderedLabel = WTF::move(label);
425 }
426
427 if (firstRenderedLabel)
428 return firstRenderedLabel->textContent().simplifyWhiteSpace(isASCIIWhitespace);
429
430 return { };
431}
432
433template<typename T>
434RefPtr<T> shadowHostOrSelfInclusiveParent(Node& node)
435{
436 if (node.isInUserAgentShadowTree())
437 return dynamicDowncast<T>(node.shadowHost());
438
439 if (RefPtr element = dynamicDowncast<T>(node))
440 return element;
441
442 return dynamicDowncast<T>(node.parentElement());
443}
444
445static bool canBeEdited(Node& node)
446{
447 if (RefPtr control = shadowHostOrSelfInclusiveParent<HTMLTextFormControlElement>(node))
448 return !control->isReadOnly();
449
450 return node.isContentEditable() || node.isRootEditableElement();
451}
452
453static bool isInDisabledFormControl(Node& node)
454{
455 RefPtr control = shadowHostOrSelfInclusiveParent<HTMLFormControlElement>(node);
456 return control && control->isDisabledFormControl();
457}
458
459enum class SkipExtraction : bool {
460 Self,
461 SelfAndSubtree
462};
463
464static bool shouldTreatAsPasswordField(const Element* element)
465{
466 RefPtr input = dynamicDowncast<HTMLInputElement>(element);
467 return input && input->hasEverBeenPasswordField();
468}
469
470enum class FallbackPolicy : bool { Skip, Extract };
471
472static inline Variant<SkipExtraction, ItemData, URL, Editable> extractItemData(Node& node, FallbackPolicy policy, TraversalContext& context)
473{
474 CheckedPtr renderer = node.renderer();
475
476 RefPtr element = dynamicDowncast<Element>(node);
477 if (element && element->hasDisplayContents())
478 return { SkipExtraction::Self };
479
480 if (!renderer)
481 return { SkipExtraction::SelfAndSubtree };
482
483 if (context.skipNearlyTransparentContent && renderer->style().opacity() < minOpacityToConsiderVisible)
484 return { SkipExtraction::SelfAndSubtree };
485
486 if (renderer->style().usedVisibility() == Visibility::Hidden)
487 return { SkipExtraction::Self };
488
489 if (RefPtr textNode = dynamicDowncast<Text>(node)) {
490 if (shouldTreatAsPasswordField(protect(textNode->shadowHost())))
491 return { SkipExtraction::Self };
492
493 if (auto iterator = context.visibleText.find(*textNode); iterator != context.visibleText.end()) {
494 auto& [textContent, selectedRange] = iterator->value;
495 context.visibleTextLength += textContent.length();
496 return { TextItemData { { }, selectedRange, textContent, { } } };
497 }
498 return { SkipExtraction::Self };
499 }
500
501 if (!element)
502 return { SkipExtraction::Self };
503
504 if (element->isLink()) {
505 if (auto href = element->attributeWithoutSynchronization(HTMLNames::hrefAttr); !href.isEmpty()) {
506 if (auto url = protect(element->document())->completeURL(href); !url.isEmpty()) {
507 if (context.mergeParagraphs)
508 return { WTF::move(url) };
509
510 auto shortenedURLString = [&] {
511 auto shortenedURL = StringEntropyHelpers::removeHighEntropyComponents(url);
512 if (shortenedURL.protocolIsFile()) {
513 String lastComponent;
514 String secondToLastComponent;
515 for (auto component : shortenedURL.path().split('/')) {
516 std::swap(secondToLastComponent, lastComponent);
517 lastComponent = component.toString();
518 }
519
520 if (!secondToLastComponent.isEmpty())
521 shortenedURL.setPath(makeString(WTF::move(secondToLastComponent), '/', WTF::move(lastComponent)));
522
523 return shortenedURL.path().toString();
524 }
525
526 auto shortenedString = shortenedURL.string();
527 if (!shortenedURL.protocolIsInHTTPFamily())
528 return shortenedString;
529
530 if (auto endOfProtocol = shortenedString.find("://"_s); endOfProtocol != notFound)
531 shortenedString = shortenedString.substring(endOfProtocol + 3);
532
533 if (shortenedString.endsWith('/'))
534 shortenedString = shortenedString.left(shortenedString.length() - 1);
535
536 return shortenedString;
537 }();
538
539 String target;
540 if (RefPtr anchor = dynamicDowncast<HTMLAnchorElement>(*element))
541 target = anchor->target();
542
543 return { LinkItemData {
544 WTF::move(target),
545 WTF::move(url),
546 WTF::move(shortenedURLString)
547 } };
548 }
549 }
550 }
551
552 if (context.onlyCollectTextAndLinksCount) {
553 // FIXME: This isn't quite right in the case where a richly contenteditable element
554 // contains more nested editable containers underneath it (for instance, a textarea
555 // element inside of a Mail compose draft).
556 return { SkipExtraction::Self };
557 }
558
559 if (!element->isInUserAgentShadowTree() && element->isRootEditableElement()) {
560 if (context.mergeParagraphs)
561 return { Editable { } };
562
563 return { ContentEditableData {
564 .isPlainTextOnly = !element->hasRichlyEditableStyle(),
565 .isFocused = protect(element->document())->activeElement() == element,
566 } };
567 }
568
569 if (RefPtr image = dynamicDowncast<HTMLImageElement>(element)) {
570 auto completedSourceURL = image->getURLAttribute(HTMLNames::srcAttr);
571 String mimeType;
572 if (RefPtr underlyingImage = image->image())
573 mimeType = underlyingImage->mimeType();
574
575 if (mimeType.isEmpty())
576 mimeType = "image/png"_s;
577
578 return { ImageItemData {
579 .completedSource = completedSourceURL,
580 .shortenedName = StringEntropyHelpers::lowEntropyLastPathComponent(completedSourceURL, "image"_s, mimeType),
581 .altText = image->altText(),
582 } };
583 }
584
585 if (RefPtr iframe = dynamicDowncast<HTMLIFrameElement>(element)) {
586 if (RefPtr contentFrame = iframe->contentFrame()) {
587 if (RefPtr frameOrigin = contentFrame->frameDocumentSecurityOrigin()) {
588 return { IFrameData {
589 .origin = frameOrigin->toString(),
590 .identifier = contentFrame->frameID(),
591 } };
592 }
593 }
594 }
595
596 if (RefPtr form = dynamicDowncast<HTMLFormElement>(element)) {
597 return { FormData {
598 .autocomplete = form->autocomplete(),
599 .name = stringOnlyIfHumanReadable(form->name()),
600 } };
601 }
602
603 if (RefPtr control = dynamicDowncast<HTMLTextFormControlElement>(element)) {
604 RefPtr input = dynamicDowncast<HTMLInputElement>(control);
605 Editable editable {
606 labelText(*control),
607 input ? input->placeholder() : nullString(),
608 shouldTreatAsPasswordField(element.get()),
609 protect(element->document())->activeElement() == control
610 };
611
612 if (context.mergeParagraphs && control->isTextField())
613 return { WTF::move(editable) };
614
615 if (!context.mergeParagraphs) {
616 auto wholeNumberOrNull = [](int value) -> std::optional<int> {
617 if (value == -1)
618 return std::nullopt;
619 return value;
620 };
621 RefPtr input = dynamicDowncast<HTMLInputElement>(*control);
622 return { TextFormControlData {
623 .editable = WTF::move(editable),
624 .controlType = control->type(),
625 .autocomplete = control->autocomplete(),
626 .pattern = control->attributeWithoutSynchronization(HTMLNames::patternAttr),
627 .name = input ? stringOnlyIfHumanReadable(input->name()) : String { },
628 .minLength = input ? wholeNumberOrNull(input->minLength()) : std::optional<int> { },
629 .maxLength = input ? wholeNumberOrNull(input->maxLength()) : std::optional<int> { },
630 .isRequired = control->isRequired(),
631 .isReadonly = input && input->isReadOnly(),
632 .isDisabled = control->isDisabled(),
633 .isChecked = input && input->checked(),
634 } };
635 }
636 }
637
638 if (RefPtr select = dynamicDowncast<HTMLSelectElement>(element)) {
639 SelectData selectData;
640 for (WeakPtr weakItem : select->listItems()) {
641 RefPtr item = weakItem.get();
642 if (!item)
643 continue;
644
645 if (RefPtr option = dynamicDowncast<HTMLOptionElement>(*item)) {
646 selectData.options.append({
647 .value = option->value(),
648 .label = option->label(),
649 .isSelected = option->selected(),
650 });
651 }
652 }
653 selectData.isMultiple = select->multiple();
654 return selectData;
655 }
656
657 if (RefPtr button = dynamicDowncast<HTMLButtonElement>(element))
658 return { ItemData { ContainerType::Button } };
659
660 if (RefPtr input = dynamicDowncast<HTMLInputElement>(element)) {
661 if (input->isTextButton())
662 return { ItemData { ContainerType::Button } };
663 }
664
665 if (is<HTMLCanvasElement>(element))
666 return { ItemData { ContainerType::Canvas } };
667
668 if (CheckedPtr box = dynamicDowncast<RenderBox>(node.renderer()); box && box->canBeScrolledAndHasScrollableArea()) {
669 if (CheckedPtr layer = box->layer()) {
670 if (CheckedPtr scrollableArea = layer->scrollableArea()) {
671 return { ScrollableItemData {
672 .contentSize = scrollableArea->totalContentsSize(),
673 .scrollPosition = scrollableArea->scrollPosition(),
674 .isRoot = false,
675 .hasOverflowItems = false,
676 } };
677 }
678 }
679 }
680
681 if (element->hasTagName(HTMLNames::olTag) || element->hasTagName(HTMLNames::ulTag))
682 return { ItemData { ContainerType::List } };
683
684 if (element->hasTagName(HTMLNames::liTag))
685 return { ItemData { ContainerType::ListItem } };
686
687 if (element->hasTagName(HTMLNames::blockquoteTag))
688 return { ItemData { ContainerType::BlockQuote } };
689
690 if (element->hasTagName(HTMLNames::articleTag))
691 return { ItemData { ContainerType::Article } };
692
693 if (element->hasTagName(HTMLNames::sectionTag))
694 return { ItemData { ContainerType::Section } };
695
696 if (element->hasTagName(HTMLNames::navTag))
697 return { ItemData { ContainerType::Nav } };
698
699 if (element->hasTagName(HTMLNames::supTag))
700 return { ItemData { ContainerType::Superscript } };
701
702 if (element->hasTagName(HTMLNames::subTag))
703 return { ItemData { ContainerType::Subscript } };
704
705 if (element->hasTagName(HTMLNames::delTag) || element->hasTagName(HTMLNames::sTag) || element->hasTagName(HTMLNames::strikeTag))
706 return { ItemData { ContainerType::Strikethrough } };
707
708 if (CheckedPtr renderElement = dynamicDowncast<RenderBox>(*renderer); renderElement && renderElement->style().hasViewportConstrainedPosition())
709 return { ItemData { ContainerType::ViewportConstrained } };
710
711 if (policy == FallbackPolicy::Extract) {
712 // As a last resort, if the element doesn't fall into any of the other buckets above,
713 // we still need to extract it to preserve data about event listeners and accessibility
714 // attributes.
715 return { ItemData { ContainerType::Generic } };
716 }
717
718 return { SkipExtraction::Self };
719}
720
721static inline bool shouldIncludeNodeIdentifier(NodeIdentifierInclusion inclusion, OptionSet<EventListenerCategory> eventListeners, AccessibilityRole role, const ItemData& data)
722{
723 using enum NodeIdentifierInclusion;
724 switch (inclusion) {
725 case None:
726 return false;
727 case AllContainers:
728 return !std::holds_alternative<TextItemData>(data);
729 default:
730 break;
731 }
732
733 return WTF::switchOn(data,
734 [inclusion, eventListeners, role](ContainerType type) {
735 if (inclusion != Interactive)
736 return false;
737
738 switch (type) {
739 case ContainerType::Article:
740 case ContainerType::ViewportConstrained:
741 case ContainerType::List:
742 case ContainerType::ListItem:
743 case ContainerType::BlockQuote:
744 case ContainerType::Section:
745 case ContainerType::Nav:
746 case ContainerType::Subscript:
747 case ContainerType::Superscript:
748 case ContainerType::Strikethrough:
749 case ContainerType::Generic:
750 return eventListeners || AccessibilityObject::isARIAControl(role);
751 case ContainerType::Button:
752 case ContainerType::Canvas:
753 return true;
754 }
755 ASSERT_NOT_REACHED()((void)0);
756 return false;
757 },
758 [](const TextItemData&) {
759 return false;
760 },
761 [](const TextFormControlData&) {
762 return true;
763 },
764 [](const ContentEditableData&) {
765 return true;
766 },
767 [](const SelectData&) {
768 return true;
769 },
770 [inclusion](const ScrollableItemData& scrollableData) {
771 if (scrollableData.isRoot)
772 return false;
773 return inclusion == Interactive;
774 },
775 [inclusion](auto&) {
776 return inclusion == Interactive;
777 });
778}
779
780static bool areSameOrigin(Document& document, Document& other)
781{
782 return protect(document.securityOrigin())->isSameOriginAs(protect(other.securityOrigin()));
783}
784
785static inline void extractRecursive(Node& node, Item& parentItem, TraversalContext& context)
786{
787 if (context.nodesToSkip.contains(node))
788 return;
789
790 bool isBlock = WebCore::isBlock(node);
791 if (isBlock)
792 context.pushEnclosingBlock(node);
793
794 bool isAdditionalContainerToCollect = context.additionalContainersToCollect.contains(node);
795 if (isAdditionalContainerToCollect)
796 context.inAdditionalContainerToCollectCount++;
797
798 auto extractionScope = makeScopeExit([&] {
799 if (isAdditionalContainerToCollect)
800 context.inAdditionalContainerToCollectCount--;
801 if (isBlock)
802 context.popEnclosingBlock();
803 });
804
805 auto enclosingBlockNumber = context.enclosingBlockNumber();
806 std::optional<Item> item;
807 std::optional<Editable> editable;
808 std::optional<URL> linkURL;
809 bool shouldSkipSubtree = false;
810
811 OptionSet<EventListenerCategory> eventListeners;
812 if (auto requestedCategories = context.originalRequest.eventListenerCategories) {
813 node.enumerateEventListenerTypes([&](auto& type, uint16_t, uint16_t) {
814 auto typeInfo = eventNames().typeInfoForEvent(type);
815 if (typeInfo.isInCategory(EventCategory::Wheel) && requestedCategories.contains(EventListenerCategory::Wheel))
816 eventListeners.add(EventListenerCategory::Wheel);
817 else if (typeInfo.isInCategory(EventCategory::MouseClickRelated) && requestedCategories.contains(EventListenerCategory::Click))
818 eventListeners.add(EventListenerCategory::Click);
819 else if (typeInfo.isInCategory(EventCategory::MouseMoveRelated) && requestedCategories.contains(EventListenerCategory::Hover))
820 eventListeners.add(EventListenerCategory::Hover);
821 else if (typeInfo.isInCategory(EventCategory::TouchRelated) && requestedCategories.contains(EventListenerCategory::Touch))
822 eventListeners.add(EventListenerCategory::Touch);
823
824 switch (typeInfo.type()) {
825 case EventType::keydown:
826 case EventType::keypress:
827 case EventType::keyup:
828 if (requestedCategories.contains(EventListenerCategory::Keyboard))
829 eventListeners.add(EventListenerCategory::Keyboard);
830 break;
831
832 default:
833 break;
834 }
835 });
836 }
837
838 auto clientAttributes = context.clientNodeAttributes.get(node);
839
840 HashMap<String, String> ariaAttributes;
841 String role;
842 String title;
843 if (RefPtr element = dynamicDowncast<Element>(node); element && context.includeAccessibilityAttributes) {
844 auto attributesToExtract = std::array {
845 HTMLNames::aria_labelAttr.get(),
846 HTMLNames::aria_expandedAttr.get(),
847 HTMLNames::aria_modalAttr.get(),
848 HTMLNames::aria_disabledAttr.get(),
849 HTMLNames::aria_checkedAttr.get(),
850 HTMLNames::aria_selectedAttr.get(),
851 HTMLNames::aria_readonlyAttr.get(),
852 HTMLNames::aria_haspopupAttr.get(),
853 HTMLNames::aria_descriptionAttr.get(),
854 HTMLNames::aria_multilineAttr.get(),
855 HTMLNames::aria_valueminAttr.get(),
856 HTMLNames::aria_valuemaxAttr.get(),
857 HTMLNames::aria_valuenowAttr.get(),
858 HTMLNames::aria_valuetextAttr.get(),
859 };
860 for (auto& attributeName : attributesToExtract) {
861 if (auto value = element->attributeWithoutSynchronization(attributeName); !value.isEmpty())
862 ariaAttributes.set(attributeName.toString(), WTF::move(value));
863 }
864 role = element->attributeWithoutSynchronization(HTMLNames::roleAttr);
865 title = element->attributeWithoutSynchronization(HTMLNames::titleAttr);
866
867 auto elementAttributesToExtract = std::array { HTMLNames::aria_labeledbyAttr.get(), HTMLNames::aria_labelledbyAttr.get(), HTMLNames::aria_describedbyAttr.get() };
868 for (auto& attributeName : elementAttributesToExtract) {
869 RefPtr elementForAttribute = element->elementForAttributeInternal(attributeName);
870 if (!elementForAttribute)
871 continue;
872
873 static constexpr auto maximumLengthForAttributeText = 32;
874 auto rawText = plainText(makeRangeSelectingNodeContents(*elementForAttribute)).simplifyWhiteSpace(isASCIIWhitespace);
875 auto elementText = normalizeText(WTF::move(rawText), maximumLengthForAttributeText);
876 if (elementText.isEmpty())
877 continue;
878
879 ariaAttributes.set(attributeName.toString(), WTF::move(elementText));
880 }
881 }
882
883 auto policy = [&] {
884 if (eventListeners)
885 return FallbackPolicy::Extract;
886
887 if (!ariaAttributes.isEmpty())
888 return FallbackPolicy::Extract;
889
890 if (!title.isEmpty())
891 return FallbackPolicy::Extract;
892
893 if (!role.isEmpty())
894 return FallbackPolicy::Extract;
895
896 if (!clientAttributes.isEmpty())
897 return FallbackPolicy::Extract;
898
899 return FallbackPolicy::Skip;
900 }();
901
902 bool isScrollable = false;
903
904 WTF::switchOn(extractItemData(node, policy, context),
905 [&](SkipExtraction skipExtraction) {
906 switch (skipExtraction) {
907 case SkipExtraction::Self:
908 return;
909 case SkipExtraction::SelfAndSubtree:
910 shouldSkipSubtree = true;
911 return;
912 }
913 },
914 [&](URL&& result) {
915 ASSERT(context.mergeParagraphs)((void)0);
916 linkURL = WTF::move(result);
917 },
918 [&](Editable&& result) {
919 ASSERT(context.mergeParagraphs)((void)0);
920 editable = WTF::move(result);
921 },
922 [&](ItemData&& result) {
923 auto bounds = rootViewBounds(node);
924 if (!context.inAdditionalContainerToCollectCount && !context.shouldIncludeNodeWithRect(bounds)) {
925 if (context.hasOverflowItemsStack.isEmpty()) {
926 ASSERT_NOT_REACHED()((void)0);
927 return;
928 }
929
930 context.hasOverflowItemsStack.last() = true;
931 return;
932 }
933
934 isScrollable = std::holds_alternative<ScrollableItemData>(result);
935
936 std::optional<NodeIdentifier> nodeIdentifier;
937 if (shouldIncludeNodeIdentifier(context.nodeIdentifierInclusion, eventListeners, AccessibilityObject::ariaRoleToWebCoreRole(role), result))
938 nodeIdentifier = node.nodeIdentifier();
939
940 item = { {
941 WTF::move(result),
942 WTF::move(bounds),
943 { },
944 node.nodeName(),
945 WTF::move(nodeIdentifier),
946 { context.frameIdentifier },
947 eventListeners,
948 WTF::move(ariaAttributes),
949 WTF::move(role),
950 WTF::move(title),
951 WTF::move(clientAttributes),
952 enclosingBlockNumber,
953 } };
954 });
955
956 if (shouldSkipSubtree)
957 return;
958
959 bool onlyCollectTextAndLinks = linkURL || editable;
960 if (onlyCollectTextAndLinks) {
961 if (auto bounds = rootViewBounds(node); context.shouldIncludeNodeWithRect(bounds)) {
962 item = {
963 TextItemData { { }, { }, emptyString(), { } },
964 WTF::move(bounds),
965 { },
966 { },
967 { },
968 { context.frameIdentifier },
969 eventListeners,
970 WTF::move(ariaAttributes),
971 WTF::move(role),
972 WTF::move(title),
973 { },
974 enclosingBlockNumber,
975 };
976 }
977 context.onlyCollectTextAndLinksCount++;
978 }
979
980 if (CheckedPtr renderer = node.renderer(); renderer && item)
981 item->hasLineThrough = renderer->style().textDecorationLineInEffect().hasLineThrough();
982
983 ASSERT_IMPLIES(isScrollable, item)((void)0);
984
985 if (isScrollable)
986 context.hasOverflowItemsStack.append(false);
987
988 if (RefPtr container = dynamicDowncast<ContainerNode>(node)) {
989 for (Ref child : composedTreeChildren<0>(*container))
990 extractRecursive(child.get(), item ? *item : parentItem, context);
991
992 if (RefPtr iframe = dynamicDowncast<HTMLIFrameElement>(node); iframe && item) {
993 if (RefPtr frame = dynamicDowncast<LocalFrame>(iframe->contentFrame())) {
994 if (RefPtr document = frame->document(); document && areSameOrigin(*document, protect(node.document()))) {
995 auto [rootItem, textLength] = extractItem(Request { context.originalRequest }, *frame);
996 context.visibleTextLength += textLength;
997 item->children.appendVector(WTF::move(rootItem.children));
998 }
999 }
1000 }
1001 }
1002
1003 if (isScrollable)
1004 std::get<ScrollableItemData>(item->data).hasOverflowItems = context.hasOverflowItemsStack.takeLast();
1005
1006 if (onlyCollectTextAndLinks) {
1007 if (item) {
1008 if (linkURL) {
1009 auto& text = std::get<TextItemData>(item->data);
1010 text.links.append({ WTF::move(*linkURL), CharacterRange { 0, text.content.length() } });
1011 }
1012 if (editable) {
1013 auto& text = std::get<TextItemData>(item->data);
1014 text.editable = WTF::move(editable);
1015 }
1016 }
1017 context.onlyCollectTextAndLinksCount--;
1018 }
1019
1020 if (!item)
1021 return;
1022
1023 if (context.mergeParagraphs && parentItem.children.isEmpty()) {
1024 if (canMerge(context, parentItem, *item))
1025 return merge(parentItem, WTF::move(*item));
1026 }
1027
1028 if (!parentItem.children.isEmpty()) {
1029 if (auto& lastChild = parentItem.children.last(); canMerge(context, lastChild, *item))
1030 return merge(lastChild, WTF::move(*item));
1031 }
1032
1033 parentItem.children.append(WTF::move(*item));
1034}
1035
1036static void pruneWhitespaceRecursive(Item& item)
1037{
1038 item.children.removeAllMatching([](auto& child) {
1039 if (!child.children.isEmpty() || !std::holds_alternative<TextItemData>(child.data))
1040 return false;
1041
1042 auto& text = std::get<TextItemData>(child.data);
1043 return !text.editable && text.content.template containsOnly<isASCIIWhitespace>();
1044 });
1045
1046 for (auto& child : item.children)
1047 pruneWhitespaceRecursive(child);
1048}
1049
1050static void pruneEmptyContainersRecursive(Item& item)
1051{
1052 for (auto& child : item.children)
1053 pruneEmptyContainersRecursive(child);
1054
1055 item.children.removeAllMatching([](auto& child) {
1056 if (!child.children.isEmpty())
1057 return false;
1058
1059 if (!child.eventListeners.isEmpty())
1060 return false;
1061
1062 if (!child.ariaAttributes.isEmpty())
1063 return false;
1064
1065 if (!child.accessibilityRole.isEmpty())
1066 return false;
1067
1068 if (!child.title.isEmpty())
1069 return false;
1070
1071 if (!std::holds_alternative<ContainerType>(child.data))
1072 return false;
1073
1074 switch (std::get<ContainerType>(child.data)) {
1075 case ContainerType::Button:
1076 case ContainerType::Canvas:
1077 return false;
1078 default:
1079 break;
1080 }
1081 return true;
1082 });
1083}
1084
1085static Node* nodeFromJSHandle(JSHandleIdentifier identifier)
1086{
1087 auto* object = WebKitJSHandle::objectForIdentifier(identifier);
1088 if (!object)
1089 return nullptr;
1090
1091 if (auto* jsNode = jsDynamicCast<JSNode*>(object))
1092 return &jsNode->wrapped();
1093
1094 return nullptr;
1095}
1096
1097static RefPtr<ContainerNode> findLargeContainerAboveNode(Node& node, FloatSize minimumSize, Node* belowRootNode = nullptr)
1098{
1099 for (CheckedPtr renderer = node.renderer(); renderer; renderer = renderer->parent()) {
1100 auto bounds = renderer->absoluteBoundingBoxRect();
1101 CheckedPtr renderElement = dynamicDowncast<RenderElement>(*renderer);
1102 bool isFixedOrSticky = renderElement && (renderElement->isFixedPositioned() || renderElement->isStickilyPositioned());
1103 if ((bounds.width() < minimumSize.width() || bounds.height() < minimumSize.height()) && !isFixedOrSticky)
1104 continue;
1105
1106 RefPtr node = renderer->node();
1107 if (RefPtr containerNode = dynamicDowncast<ContainerNode>(node))
1108 return containerNode;
1109
1110 if (belowRootNode && belowRootNode == node)
1111 break;
1112 }
1113
1114 return { };
1115}
1116
1117// FIXME: Consider making this size threshold client-configurable in the future.
1118static constexpr FloatSize minimumSizeForLargeContainer { 280, 300 };
1119
1120#if ENABLE(DATA_DETECTION)(defined 1 && 1)
1121
1122static RefPtr<ContainerNode> findContainerNodeForDataDetectorResults(Node& rootNode, OptionSet<DataDetectorType> types)
1123{
1124 struct RangeAndArea {
1125 SimpleRange range;
1126 double area;
1127 };
1128
1129 std::optional<RangeAndArea> largestRange;
1130 for (auto range : DataDetection::detectRanges(makeRangeSelectingNodeContents(rootNode), types)) {
1131 double areaAboveMaximumYOffset = 0;
1132 for (auto rect : RenderObject::absoluteTextRects(range)) {
1133 static constexpr auto maximumYOffset = 3000;
1134 if (rect.y() < maximumYOffset)
1135 areaAboveMaximumYOffset += FloatRect { rect }.area();
1136 }
1137
1138 if (areaAboveMaximumYOffset <= 0)
1139 continue;
1140
1141 if (!largestRange || largestRange->area < areaAboveMaximumYOffset)
1142 largestRange = { range, areaAboveMaximumYOffset };
1143 }
1144
1145 if (!largestRange)
1146 return { };
1147
1148 RefPtr commonAncestor = commonInclusiveAncestor<ComposedTree>(largestRange->range);
1149 if (!commonAncestor)
1150 return { };
1151
1152 return findLargeContainerAboveNode(*commonAncestor, minimumSizeForLargeContainer, &rootNode);
1153}
1154
1155#endif // ENABLE(DATA_DETECTION)
1156
1157Result extractItem(Request&& request, LocalFrame& frame)
1158{
1159 auto frameID = frame.frameID();
1160 Item root { ScrollableItemData { }, { }, { }, { }, { }, frameID, { }, { }, { }, { }, { }, 0 };
1161 RefPtr document = frame.document();
1162 if (!document)
1163 return { root, 0 };
1164
1165 RefPtr bodyElement = document->body();
1166 if (!bodyElement)
1167 return { root, 0 };
1168
1169 document->updateLayoutIgnorePendingStylesheets();
1170
1171 RefPtr extractionRootNode = [&] -> Node* {
1172 if (!request.targetNodeHandleIdentifier)
1173 return bodyElement.get();
1174
1175 return nodeFromJSHandle(*request.targetNodeHandleIdentifier);
1176 }();
1177
1178 bool extractingWithDataDetectors = false;
1179#if ENABLE(DATA_DETECTION)(defined 1 && 1)
1180 if (request.dataDetectorTypes && extractionRootNode) {
1181 extractionRootNode = findContainerNodeForDataDetectorResults(*extractionRootNode, request.dataDetectorTypes);
1182 extractingWithDataDetectors = true;
1183 }
1184#endif
1185
1186 if (extractionRootNode)
1187 addBoxShadowIfNeeded(*extractionRootNode, extractingWithDataDetectors ? "#0088FF"_s : "#ff8d28"_s);
1188
1189 if (!extractionRootNode)
1190 return { root, 0 };
1191
1192 RefPtr view = frame.view();
1193 if (!view)
1194 return { root, 0 };
1195
1196 root.data = { ScrollableItemData {
1197 .contentSize = view->contentsSize(),
1198 .scrollPosition = view->scrollPosition(),
1199 .isRoot = true,
1200 .hasOverflowItems = false,
1201 } };
1202
1203 root.rectInRootView = view->contentsToRootView(IntRect { IntPoint::zero(), view->contentsSize() });
1204 if (root.rectInRootView.isEmpty())
1205 return { root, 0 };
1206
1207 unsigned visibleTextLength = 0;
1208 {
1209 ClientNodeAttributesMap clientNodeAttributes;
1210 for (auto& [attribute, values] : request.clientNodeAttributes) {
1211 for (auto& [identifier, value] : values) {
1212 RefPtr node = nodeFromJSHandle(identifier);
1213 if (!node)
1214 continue;
1215
1216 clientNodeAttributes.ensure(*node, [] {
1217 return HashMap<String, String> { };
1218 }).iterator->value.set(attribute, value);
1219 }
1220 }
1221
1222 auto includeTextInAutoFilledControls = request.includeTextInAutoFilledControls ? IncludeTextInAutoFilledControls::Yes : IncludeTextInAutoFilledControls::No;
1223
1224 WeakHashSet<Node, WeakPtrImplWithEventTargetData> nodesToSkip;
1225 for (auto identifier : request.handleIdentifiersOfNodesToSkip) {
1226 if (RefPtr node = nodeFromJSHandle(identifier))
1227 nodesToSkip.add(node.releaseNonNull());
1228 }
1229
1230 WeakHashSet<Node, WeakPtrImplWithEventTargetData> additionalContainersToCollect;
1231 RefPtr extractionRoot = dynamicDowncast<ContainerNode>(*extractionRootNode);
1232 if (extractionRoot && request.includeOffscreenPasswordFields && request.collectionRectInRootView) {
1233 Vector<Ref<HTMLInputElement>> passwordFields;
1234 for (Ref input : descendantsOfType<HTMLInputElement>(*extractionRoot)) {
1235 if (input->isPasswordField())
1236 passwordFields.append(input);
1237 }
1238
1239 for (Ref passwordField : passwordFields) {
1240 static constexpr FloatSize minimumSize { 280, 200 };
1241 if (RefPtr container = findLargeContainerAboveNode(passwordField, minimumSize)) {
1242 addBoxShadowIfNeeded(*container, "#cb30e0"_s);
1243 additionalContainersToCollect.add(container.releaseNonNull());
1244 }
1245 }
1246 }
1247
1248 TraversalContext context {
1249 .originalRequest = { request },
1250 .clientNodeAttributes = WTF::move(clientNodeAttributes),
1251 .visibleText = collectText(*extractionRootNode, includeTextInAutoFilledControls),
1252 .nodesToSkip = WTF::move(nodesToSkip),
1253 .rectInRootView = request.collectionRectInRootView,
1254 .frameIdentifier = WTF::move(frameID),
1255 .enclosingBlocks = { },
1256 .enclosingBlockNumberMap = { },
1257 .additionalContainersToCollect = WTF::move(additionalContainersToCollect),
1258 .inAdditionalContainerToCollectCount = 0,
1259 .hasOverflowItemsStack = { false },
1260 .onlyCollectTextAndLinksCount = 0,
1261 .mergeParagraphs = request.mergeParagraphs,
1262 .skipNearlyTransparentContent = request.skipNearlyTransparentContent,
1263 .nodeIdentifierInclusion = request.nodeIdentifierInclusion,
1264 .includeAccessibilityAttributes = request.includeAccessibilityAttributes,
1265 };
1266 extractRecursive(*extractionRootNode, root, context);
1267
1268 ASSERT(context.hasOverflowItemsStack.size() == 1)((void)0);
1269 if (!context.hasOverflowItemsStack.isEmpty())
1270 std::get<ScrollableItemData>(root.data).hasOverflowItems = context.hasOverflowItemsStack.takeLast();
1271
1272 visibleTextLength = context.visibleTextLength;
1273 }
1274
1275 pruneWhitespaceRecursive(root);
1276 pruneEmptyContainersRecursive(root);
1277
1278 return { WTF::move(root), visibleTextLength };
1279}
1280
1281using Token = Variant<String, IntSize>;
1282struct TokenAndBlockOffset {
1283 Vector<Token> tokens;
1284 int offset { 0 };
1285};
1286
1287static IntSize reducePrecision(FloatSize size)
1288{
1289 static constexpr auto resolution = 10;
1290 return {
1291 static_cast<int>(std::round(size.width() / resolution)) * resolution,
1292 static_cast<int>(std::round(size.height() / resolution)) * resolution
1293 };
1294}
1295
1296static void extractRenderedTokens(Vector<TokenAndBlockOffset>& tokensAndOffsets, ContainerNode& node, FlowDirection direction)
1297{
1298 CheckedPtr renderer = node.renderer();
1299 if (!renderer)
1300 return;
1301
1302 auto appendTokens = [&](Vector<Token>&& tokens, IntRect bounds) mutable {
1303 static constexpr auto minPixelDistanceForNearbyText = 5;
1304 if (tokens.isEmpty() || bounds.width() <= minPixelDistanceForNearbyText || bounds.height() <= minPixelDistanceForNearbyText)
1305 return;
1306
1307 auto offset = [&] {
1308 switch (direction) {
1309 case FlowDirection::TopToBottom:
1310 return bounds.y();
1311 case FlowDirection::BottomToTop:
1312 return bounds.maxY();
1313 case FlowDirection::LeftToRight:
1314 return bounds.x();
1315 case FlowDirection::RightToLeft:
1316 return bounds.maxX();
1317 }
1318 ASSERT_NOT_REACHED()((void)0);
1319 return 0;
1320 }();
1321
1322 auto foundIndex = tokensAndOffsets.reverseFindIf([&](auto& item) {
1323 return std::abs(offset - item.offset) <= minPixelDistanceForNearbyText;
1324 });
1325
1326 if (foundIndex == notFound) {
1327 tokensAndOffsets.append({ WTF::move(tokens), offset });
1328 return;
1329 }
1330
1331 tokensAndOffsets[foundIndex].tokens.appendVector(WTF::move(tokens));
1332 };
1333
1334 if (CheckedPtr frameRenderer = dynamicDowncast<RenderIFrame>(*renderer)) {
1335 if (RefPtr contentDocument = frameRenderer->iframeElement().contentDocument())
1336 extractRenderedTokens(tokensAndOffsets, *contentDocument, direction);
1337 return;
1338 }
1339
1340 RefPtr frameView = renderer->view().frameView();
1341 auto appendReplacedContentOrBackgroundImage = [&](auto& renderer) {
1342 if (!Style::hasImageInAnyLayer(renderer.style().backgroundLayers()) && !is<RenderReplaced>(renderer))
1343 return;
1344
1345 auto absoluteRect = renderer.absoluteBoundingBoxRect();
1346 auto roundedSize = reducePrecision(frameView->absoluteToDocumentRect(absoluteRect).size());
1347 appendTokens({ { roundedSize } }, frameView->contentsToRootView(absoluteRect));
1348 };
1349
1350 appendReplacedContentOrBackgroundImage(*renderer);
1351
1352 for (CheckedRef descendant : descendantsOfType<RenderObject>(*renderer)) {
1353 if (descendant->style().usedVisibility() == Visibility::Hidden)
1354 continue;
1355
1356 if (descendant->style().opacity() < minOpacityToConsiderVisible)
1357 continue;
1358
1359 if (RefPtr node = descendant->node(); node && ImageOverlay::isInsideOverlay(*node))
1360 continue;
1361
1362 if (CheckedPtr textRenderer = dynamicDowncast<RenderText>(descendant)) {
1363 if (textRenderer->hasRenderedText()) {
1364 Vector<Token> tokens;
1365 for (auto token : textRenderer->text().simplifyWhiteSpace(isASCIIWhitespace).split(' ')) {
1366 auto candidate = token.removeCharacters([](char16_t character) {
1367 return !u_isalpha(character) && !u_isdigit(character);
1368 });
1369 if (!candidate.isEmpty())
1370 tokens.append({ WTF::move(candidate) });
1371 }
1372 appendTokens(WTF::move(tokens), frameView->contentsToRootView(descendant->absoluteBoundingBoxRect()));
1373 }
1374 continue;
1375 }
1376
1377 if (CheckedPtr frameRenderer = dynamicDowncast<RenderIFrame>(descendant)) {
1378 if (RefPtr contentDocument = frameRenderer->iframeElement().contentDocument())
1379 extractRenderedTokens(tokensAndOffsets, *contentDocument, direction);
1380 continue;
1381 }
1382
1383 appendReplacedContentOrBackgroundImage(downcast<RenderElement>(descendant));
1384 }
1385}
1386
1387RenderedText extractRenderedText(Element& element)
1388{
1389 CheckedPtr renderer = element.renderer();
1390 if (!renderer)
1391 return { };
1392
1393 RefPtr frameView = renderer->view().frameView();
Call argument for 'this' parameter is unchecked and unsafe
1394 auto direction = renderer->writingMode().blockDirection();
1395 auto elementRectInDocument = frameView->absoluteToDocumentRect(renderer->absoluteBoundingBoxRect());
1396
1397 Vector<TokenAndBlockOffset> allTokensAndOffsets;
1398 extractRenderedTokens(allTokensAndOffsets, element, direction);
1399
1400 bool ascendingOrder = [&] {
1401 switch (direction) {
1402 case FlowDirection::TopToBottom:
1403 case FlowDirection::LeftToRight:
1404 return true;
1405 case FlowDirection::BottomToTop:
1406 case FlowDirection::RightToLeft:
1407 return false;
1408 }
1409 ASSERT_NOT_REACHED()((void)0);
1410 return true;
1411 }();
1412
1413 if (ascendingOrder)
1414 std::ranges::sort(allTokensAndOffsets, std::ranges::less { }, &TokenAndBlockOffset::offset);
1415 else
1416 std::ranges::sort(allTokensAndOffsets, std::ranges::greater { }, &TokenAndBlockOffset::offset);
1417
1418 bool hasLargeReplacedDescendant = false;
1419 StringBuilder textWithReplacedContent;
1420 StringBuilder textWithoutReplacedContent;
1421 auto appendText = [](StringBuilder& builder, const String& string) {
1422 if (!builder.isEmpty())
1423 builder.append(' ');
1424 builder.append(string);
1425 };
1426
1427 for (auto& [tokens, offset] : allTokensAndOffsets) {
1428 for (auto& token : tokens) {
1429 switchOn(token, [&](const String& text) {
1430 appendText(textWithReplacedContent, text);
1431 appendText(textWithoutReplacedContent, text);
1432 }, [&](const IntSize& size) {
1433 constexpr auto ratioToConsiderLengthAsLarge = 0.9;
1434 if (size.width() > ratioToConsiderLengthAsLarge * elementRectInDocument.width() && size.height() > ratioToConsiderLengthAsLarge * elementRectInDocument.height())
1435 hasLargeReplacedDescendant = true;
1436 appendText(textWithReplacedContent, makeString('{', size.width(), ',', size.height(), '}'));
1437 });
1438 }
1439 }
1440
1441 return { textWithReplacedContent.toString(), textWithoutReplacedContent.toString(), hasLargeReplacedDescendant };
1442}
1443
1444static Vector<std::pair<String, FloatRect>> extractAllTextAndRectsRecursive(Document& document)
1445{
1446 RefPtr bodyElement = document.body();
1447 if (!bodyElement)
1448 return { };
1449
1450 RefPtr view = document.view();
1451 if (!view)
1452 return { };
1453
1454 ListHashSet<Ref<HTMLFrameOwnerElement>> frameOwners;
1455 Vector<std::pair<String, FloatRect>> result;
1456 auto fullRange = makeRangeSelectingNodeContents(*bodyElement);
1457 for (TextIterator iterator { fullRange, TextIteratorBehavior::EntersTextControls }; !iterator.atEnd(); iterator.advance()) {
1458 RefPtr node = iterator.node();
1459 if (!node)
1460 continue;
1461
1462 if (RefPtr frameOwner = dynamicDowncast<HTMLFrameOwnerElement>(*node))
1463 frameOwners.add(frameOwner.releaseNonNull());
1464
1465 auto trimmedText = iterator.text().trim(isASCIIWhitespace<char16_t>);
1466 if (trimmedText.isEmpty())
1467 continue;
1468
1469 CheckedPtr renderer = node->renderer();
1470 if (!renderer)
1471 continue;
1472
1473 FloatRect absoluteBounds;
1474 auto textRange = iterator.range();
1475 if (!textRange.collapsed()) {
1476 absoluteBounds = enclosingIntRect(unionRectIgnoringZeroRects(RenderObject::absoluteBorderAndTextRects(textRange, {
1477 RenderObject::BoundingRectBehavior::IgnoreTinyRects,
1478 RenderObject::BoundingRectBehavior::IgnoreEmptyTextSelections,
1479 RenderObject::BoundingRectBehavior::UseSelectionHeight,
1480 })));
1481 }
1482
1483 if (absoluteBounds.isEmpty())
1484 absoluteBounds = renderer->absoluteBoundingBoxRect();
1485
1486 result.append({ trimmedText.toString(), view->contentsToRootView(absoluteBounds) });
1487 }
1488
1489 for (auto& frameOwner : frameOwners) {
1490 RefPtr contentDocument = frameOwner->contentDocument();
1491 if (!contentDocument)
1492 continue;
1493
1494 result.appendVector(extractAllTextAndRectsRecursive(*contentDocument));
1495 }
1496
1497 return result;
1498}
1499
1500Vector<std::pair<String, FloatRect>> extractAllTextAndRects(Page& page)
1501{
1502 RefPtr mainFrame = dynamicDowncast<LocalFrame>(page.mainFrame());
1503 if (!mainFrame)
1504 return { };
1505
1506 RefPtr document = mainFrame->document();
1507 if (!document)
1508 return { };
1509
1510 return extractAllTextAndRectsRecursive(*document);
1511}
1512
1513static std::optional<SimpleRange> searchForClickTarget(Node& container, const String& searchText)
1514{
1515 if (searchText.isEmpty())
1516 return std::nullopt;
1517
1518 Vector<SimpleRange> candidateRanges;
1519 auto remainingRange = makeRangeSelectingNodeContents(container);
1520 while (is_lt(treeOrder(remainingRange.start, remainingRange.end))) {
1521 auto foundRange = findPlainText(remainingRange, searchText, {
1522 FindOption::DoNotRevealSelection,
1523 FindOption::DoNotSetSelection,
1524 FindOption::CaseInsensitive,
1525 });
1526
1527 if (foundRange.collapsed())
1528 break;
1529
1530 remainingRange.start = foundRange.end;
1531 candidateRanges.append(WTF::move(foundRange));
1532
1533 if (remainingRange.collapsed())
1534 break;
1535 }
1536
1537 if (candidateRanges.isEmpty())
1538 return std::nullopt;
1539
1540 if (candidateRanges.size() == 1)
1541 return candidateRanges.first();
1542
1543 bool bestRangeIsExactMatch = false;
1544 bool bestRangeIsClickable = false;
1545 std::optional<SimpleRange> bestRange;
1546
1547 for (auto& range : candidateRanges) {
1548 RefPtr ancestor = commonInclusiveAncestor<ComposedTree>(range);
1549 if (!ancestor)
1550 continue;
1551
1552 RefPtr target = lineageOfType<Element>(*ancestor).first();
1553 if (!target)
1554 continue;
1555
1556 bool isClickable = target->willRespondToMouseClickEvents() || target->isLink() || target->isFormListedElement();
1557 bool isExactMatch = normalizeText(plainText(makeRangeSelectingNodeContents(*ancestor))) == searchText;
1558
1559 if (!bestRangeIsClickable && isClickable) {
1560 bestRangeIsClickable = true;
1561 bestRangeIsExactMatch = isExactMatch;
1562 bestRange = range;
1563 } else if (!bestRangeIsExactMatch && isExactMatch) {
1564 bestRangeIsExactMatch = true;
1565 bestRangeIsClickable = isClickable;
1566 bestRange = range;
1567 }
1568
1569 if (isExactMatch && isClickable)
1570 break;
1571 }
1572
1573 return bestRange;
1574}
1575
1576static std::optional<SimpleRange> searchForText(Node& node, const String& searchText)
1577{
1578 if (searchText.isEmpty())
1579 return std::nullopt;
1580
1581 auto searchRange = makeRangeSelectingNodeContents(node);
1582 auto foundRange = findPlainText(searchRange, searchText, {
1583 FindOption::DoNotRevealSelection,
1584 FindOption::DoNotSetSelection,
1585 FindOption::CaseInsensitive,
1586 });
1587
1588 if (foundRange.collapsed())
1589 return { };
1590
1591 return { WTF::move(foundRange) };
1592}
1593
1594static String invalidNodeIdentifierDescription(std::optional<NodeIdentifier>&& identifier)
1595{
1596 if (!identifier)
1597 return "Missing nodeIdentifier"_s;
1598
1599 return makeString("Failed to resolve nodeIdentifier "_s, identifier->loggingString());
1600}
1601
1602static String searchTextNotFoundDescription(const String& searchText)
1603{
1604 return makeString('\'', searchText, "' not found inside the target node"_s);
1605}
1606
1607static constexpr auto nullFrameDescription = "Browsing context has been detached"_s;
1608static constexpr auto interactedWithSelectElementDescription = "Successfully updated option in select element"_s;
1609
1610static void dispatchSimulatedClick(LocalFrame& frame, IntPoint location, CompletionHandler<void(bool, String&&)>&& completion)
1611{
1612 frame.eventHandler().handleMouseMoveEvent({
1613 location, location, MouseButton::Left, PlatformEvent::Type::MouseMoved, 0, { }, MonotonicTime::now(), ForceAtClick, SyntheticClickType::NoTap, MouseEventInputSource::UserDriven
1614 });
1615
1616 frame.eventHandler().handleMousePressEvent({
1617 location, location, MouseButton::Left, PlatformEvent::Type::MousePressed, 1, { }, MonotonicTime::now(), ForceAtClick, SyntheticClickType::NoTap, MouseEventInputSource::UserDriven
1618 });
1619
1620 frame.eventHandler().handleMouseReleaseEvent({
1621 location, location, MouseButton::Left, PlatformEvent::Type::MouseReleased, 1, { }, MonotonicTime::now(), ForceAtClick, SyntheticClickType::NoTap, MouseEventInputSource::UserDriven
1622 });
1623
1624 completion(true, { });
1625}
1626
1627static Node* findNodeAtRootViewLocation(const LocalFrameView& view, Document& document, FloatPoint locationInRootView)
1628{
1629 static constexpr OptionSet defaultHitTestOptions {
1630 HitTestRequest::Type::ReadOnly,
1631 HitTestRequest::Type::DisallowUserAgentShadowContent,
1632 };
1633
1634 HitTestResult result { view.rootViewToContents(roundedIntPoint(locationInRootView)) };
1635 return document.hitTest(defaultHitTestOptions, result) ? result.innerNode() : nullptr;
1636}
1637
1638static void dispatchSimulatedClick(Node& targetNode, const String& searchText, CompletionHandler<void(bool, String&&)>&& completion)
1639{
1640 RefPtr element = dynamicDowncast<Element>(targetNode);
1641 if (!element)
1642 element = targetNode.parentElementInComposedTree();
1643
1644 if (!element || !element->isConnected())
1645 return completion(false, "Target has been disconnected from the DOM"_s);
1646
1647 {
1648 CheckedPtr renderer = element->renderer();
1649 if (!renderer)
1650 return completion(false, "Target is not rendered (possibly display: none)"_s);
1651
1652 if (renderer->style().usedVisibility() != Visibility::Visible)
1653 return completion(false, "Target is hidden via CSS visibility"_s);
1654 }
1655
1656 Ref document = element->document();
1657 RefPtr view = document->view();
1658 if (!view)
1659 return completion(false, "Document is not visible to the user"_s);
1660
1661 RefPtr frame = document->frame();
1662 if (!frame)
1663 return completion(false, nullFrameDescription);
1664
1665 addBoxShadowIfNeeded(targetNode, "#34c759"_s);
1666
1667 std::optional<FloatRect> targetRectInRootView;
1668 if (!searchText.isEmpty()) {
1669 auto foundRange = searchForClickTarget(*element, searchText);
1670 if (!foundRange) {
1671 // Err on the side of failing, if the text has changed since the interaction was triggered.
1672 return completion(false, searchTextNotFoundDescription(searchText));
1673 }
1674
1675 if (auto absoluteQuads = RenderObject::absoluteTextQuads(*foundRange); !absoluteQuads.isEmpty()) {
1676 // If the text match wraps across multiple lines, arbitrarily click over the first rect to avoid
1677 // missing the text node altogether.
1678 targetRectInRootView = view->contentsToRootView(absoluteQuads.first().boundingBox());
1679 }
1680 }
1681
1682 if (isInDisabledFormControl(*element))
1683 return completion(false, "Click target is disabled"_s);
1684
1685 if (!targetRectInRootView)
1686 targetRectInRootView = rootViewBounds(*element);
1687
1688 auto centerInRootView = roundedIntPoint(targetRectInRootView->center());
1689 if (RefPtr target = findNodeAtRootViewLocation(*view, document, centerInRootView); target && (target == element || target->isShadowIncludingDescendantOf(*element))) {
1690 // Dispatch mouse events over the center of the element, if possible.
1691 return dispatchSimulatedClick(*frame, centerInRootView, WTF::move(completion));
1692 }
1693
1694 UserGestureIndicator indicator { IsProcessingUserGesture::Yes, protect(element->document()).ptr() };
1695
1696 // Fall back to dispatching a programmatic click.
1697 if (element->dispatchSimulatedClick(nullptr, SendMouseUpDownEvents))
1698 completion(true, { });
1699 else
1700 completion(false, "Failed to click (tried falling back to dispatching programmatic click since target could not be hit-tested)"_s);
1701}
1702
1703static void dispatchSimulatedClick(NodeIdentifier identifier, const String& searchText, CompletionHandler<void(bool, String&&)>&& completion)
1704{
1705 RefPtr foundNode = Node::fromIdentifier(identifier);
1706 if (!foundNode)
1707 return completion(false, invalidNodeIdentifierDescription(identifier));
1708
1709 dispatchSimulatedClick(*foundNode, searchText, WTF::move(completion));
1710}
1711
1712struct SelectOptionResult {
1713 bool handled { false };
1714 String failureReason;
1715};
1716
1717static SelectOptionResult selectOptionByValue(NodeIdentifier identifier, const String& optionText)
1718{
1719 RefPtr foundNode = Node::fromIdentifier(identifier);
1720 if (!foundNode)
1721 return { };
1722
1723 if (RefPtr select = dynamicDowncast<HTMLSelectElement>(*foundNode)) {
1724 if (select->isDisabledFormControl())
1725 return { true, "Select is disabled"_s };
1726
1727 if (optionText.isEmpty())
1728 return { };
1729
1730 addBoxShadowIfNeeded(*select, "#34c759"_s);
1731 select->setValue(optionText);
1732 return { select->selectedIndex() != -1, { } };
1733 }
1734
1735 return { };
1736}
1737
1738static HTMLElement* documentBodyElement(const LocalFrame& frame)
1739{
1740 if (RefPtr document = frame.document())
1741 return document->body();
1742
1743 return nullptr;
1744}
1745
1746static RefPtr<Node> resolveNodeWithBodyAsFallback(const LocalFrame& frame, std::optional<NodeIdentifier> identifier)
1747{
1748 if (identifier)
1749 return Node::fromIdentifier(WTF::move(*identifier));
1750
1751 return documentBodyElement(frame);
1752}
1753
1754static std::optional<SimpleRange> rangeForTextInContainer(const String& searchText, Ref<Node>&& node)
1755{
1756 if (searchText.isEmpty() && !is<HTMLBodyElement>(node))
1757 return makeRangeSelectingNodeContents(node);
1758
1759 return searchForText(node, searchText);
1760}
1761
1762static void selectText(LocalFrame& frame, std::optional<NodeIdentifier>&& identifier, const String& searchText, bool revealText, CompletionHandler<void(bool, String&&)>&& completion)
1763{
1764 RefPtr foundNode = resolveNodeWithBodyAsFallback(frame, identifier);
1765 if (!foundNode)
1766 return completion(false, invalidNodeIdentifierDescription(WTF::move(identifier)));
1767
1768 if (RefPtr control = dynamicDowncast<HTMLTextFormControlElement>(*foundNode)) {
1769 // FIXME: This should probably honor `searchText`.
1770 control->select();
1771 addBoxShadowIfNeeded(*control, "#34c759"_s);
1772 return completion(true, { });
1773 }
1774
1775 auto targetRange = rangeForTextInContainer(searchText, *foundNode);
1776 if (!targetRange)
1777 return completion(false, searchTextNotFoundDescription(searchText));
1778
1779 Ref document = foundNode->document();
1780 if (!document->selection().setSelectedRange(*targetRange, Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::Yes))
1781 return completion(false, "Failed to set selected range"_s);
1782
1783 if (revealText)
1784 document->selection().revealSelection();
1785
1786 return completion(true, { });
1787}
1788
1789static void highlightText(LocalFrame& frame, std::optional<NodeIdentifier>&& identifier, const String& searchText, bool scrollToVisible, CompletionHandler<void(bool, String&&)>&& completion)
1790{
1791 RefPtr foundNode = resolveNodeWithBodyAsFallback(frame, identifier);
1792 if (!foundNode)
1793 return completion(false, invalidNodeIdentifierDescription(WTF::move(identifier)));
1794
1795 auto range = rangeForTextInContainer(searchText, *foundNode);
1796 if (!range)
1797 return completion(false, searchTextNotFoundDescription(searchText));
1798
1799 Ref document = foundNode->document();
1800 RefPtr view = document->view();
1801 if (!view)
1802 return completion(false, nullFrameDescription);
1803
1804 document->textExtractionHighlightRegistry().addAnnotationHighlightWithRange(StaticRange::create(*range));
1805
1806 if (scrollToVisible)
1807 view->revealRangeWithTemporarySelection(*range);
1808
1809 return completion(true, { });
1810}
1811
1812static void scrollBy(LocalFrame& frame, std::optional<NodeIdentifier>&& identifier, FloatSize scrollDelta, CompletionHandler<void(bool, String&&)>&& completion)
1813{
1814 RefPtr foundNode = resolveNodeWithBodyAsFallback(frame, identifier);
1815 if (!foundNode)
1816 return completion(false, invalidNodeIdentifierDescription(WTF::move(identifier)));
1817
1818 WeakPtr scroller = CheckedRef { frame.eventHandler() }->enclosingScrollableArea(foundNode.get());
1819 if (!scroller)
1820 return completion(false, "No scrollable area found"_s);
1821
1822 addBoxShadowIfNeeded(*foundNode, "#34c759"_s);
1823 scroller->scrollToOffsetWithoutAnimation(FloatPoint { scroller->scrollOffset() } + scrollDelta);
1824 completion(true, { });
1825}
1826
1827static void scrollToReveal(LocalFrame& frame, std::optional<NodeIdentifier>&& identifier, String&& searchText, CompletionHandler<void(bool, String&&)>&& completion)
1828{
1829 RefPtr searchScope = resolveNodeWithBodyAsFallback(frame, identifier);
1830 if (!searchScope)
1831 return completion(false, invalidNodeIdentifierDescription(WTF::move(identifier)));
1832
1833 auto foundRange = searchForText(*searchScope, searchText);
1834 if (!foundRange)
1835 return completion(false, searchTextNotFoundDescription(searchText));
1836
1837 RefPtr elementToReveal = lineageOfType<Element>(foundRange->startContainer()).first();
1838 if (!elementToReveal)
1839 return completion(false, searchTextNotFoundDescription(searchText));
1840
1841 elementToReveal->scrollIntoView();
1842 completion(true, { });
1843}
1844
1845static bool simulateKeyPress(LocalFrame& frame, const String& key)
1846{
1847 auto keyDown = PlatformKeyboardEvent::syntheticEventFromText(PlatformEvent::Type::KeyDown, key);
1848 if (!keyDown)
1849 return false;
1850
1851 auto keyUp = PlatformKeyboardEvent::syntheticEventFromText(PlatformEvent::Type::KeyUp, key);
1852 if (!keyUp)
1853 return false;
1854
1855 frame.eventHandler().keyEvent(*keyDown);
1856 frame.eventHandler().keyEvent(*keyUp);
1857 return true;
1858}
1859
1860static void simulateKeyPress(LocalFrame& targetFrame, std::optional<NodeIdentifier>&& identifier, const String& text, CompletionHandler<void(bool, String&&)>&& completion)
1861{
1862 if (identifier) {
1863 RefPtr focusTarget = dynamicDowncast<Element>(Node::fromIdentifier(*identifier));
1864 if (!focusTarget)
1865 return completion(false, makeString(identifier->loggingString()));
1866
1867 if (focusTarget != protect(focusTarget->document())->activeElement())
1868 focusTarget->focus();
1869 }
1870
1871 String canonicalKey = text;
1872 if (text == "\n"_s || text == "Return"_s)
1873 canonicalKey = "Enter"_s;
1874 else if (text == "Left"_s || text == "Right"_s || text == "Up"_s || text == "Down"_s)
1875 canonicalKey = makeString("Arrow"_s, text);
1876
1877 if (simulateKeyPress(targetFrame, canonicalKey))
1878 return completion(true, { });
1879
1880 if (!text.is8Bit()) {
1881 // FIXME: Consider falling back to simulating text insertion.
1882 return completion(false, "Only 8-bit strings are supported"_s);
1883 }
1884
1885 bool succeeded = true;
1886 for (auto character : text.span8()) {
1887 if (!simulateKeyPress(targetFrame, { std::span { &character, 1 } }))
1888 succeeded = false;
1889 }
1890
1891 completion(succeeded, succeeded
1892 ? makeString('\'', text, "' is not a valid key, but we successfully fell back to typing each character in the string separately"_s)
1893 : makeString("One or more key events failed (tried to input '"_s, text, "' character by character"_s));
1894}
1895
1896static void focusAndInsertText(NodeIdentifier identifier, String&& text, bool replaceAll, CompletionHandler<void(bool, String&&)>&& completion)
1897{
1898 RefPtr foundNode = Node::fromIdentifier(identifier);
1899 if (!foundNode)
1900 return completion(false, invalidNodeIdentifierDescription(identifier));
1901
1902 if (!canBeEdited(*foundNode))
1903 return completion(false, "Target is readonly"_s);
1904
1905 RefPtr<Element> elementToFocus;
1906 if (RefPtr element = dynamicDowncast<Element>(*foundNode); element && element->isTextField())
1907 elementToFocus = element;
1908 else if (RefPtr host = foundNode->shadowHost(); host && host->isTextField()) {
1909 if (RefPtr formControl = dynamicDowncast<HTMLTextFormControlElement>(host.get()))
1910 elementToFocus = WTF::move(formControl);
1911 }
1912
1913 if (!elementToFocus)
1914 elementToFocus = foundNode->isRootEditableElement() ? dynamicDowncast<Element>(*foundNode) : foundNode->rootEditableElement();
1915
1916 if (!elementToFocus)
1917 return completion(false, makeString(identifier.loggingString(), " cannot be edited (requires text field or contentEditable)"_s));
1918
1919 Ref document = elementToFocus->document();
1920 RefPtr frame = document->frame();
1921 if (!frame)
1922 return completion(false, nullFrameDescription);
1923
1924 // First, attempt to dispatch a click over the editable area (and fall back to programmatically setting focus).
1925 dispatchSimulatedClick(*elementToFocus, { }, [document = document.copyRef(), elementToFocus, frame, replaceAll, text = WTF::move(text), completion = WTF::move(completion)](bool clicked, String&&) mutable {
1926 if (!clicked || elementToFocus != document->activeElement())
1927 elementToFocus->focus();
1928
1929 if (replaceAll) {
1930 if (elementToFocus->isRootEditableElement())
1931 document->selection().setSelectedRange(makeRangeSelectingNodeContents(*elementToFocus), Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::Yes);
1932 else
1933 document->selection().selectAll();
1934 }
1935
1936 UserTypingGestureIndicator indicator { *frame };
1937
1938 protect(document->editor())->pasteAsPlainText(text, false);
1939 completion(true, "Inserted text by simulating paste with plain text"_s);
1940 });
1941}
1942
1943void handleInteraction(Interaction&& interaction, LocalFrame& frame, CompletionHandler<void(bool, String&&)>&& completion)
1944{
1945 switch (interaction.action) {
1946 case Action::Click: {
1947 if (auto location = interaction.locationInRootView)
1948 return dispatchSimulatedClick(frame, roundedIntPoint(*location), WTF::move(completion));
1949
1950 if (auto identifier = interaction.nodeIdentifier)
1951 return dispatchSimulatedClick(*identifier, WTF::move(interaction.text), WTF::move(completion));
1952
1953 if (RefPtr body = documentBodyElement(frame); body && !interaction.text.isEmpty())
1954 return dispatchSimulatedClick(*body, WTF::move(interaction.text), WTF::move(completion));
1955
1956 return completion(false, "Missing nodeIdentifier and/or text"_s);
1957 }
1958 case Action::SelectMenuItem: {
1959 if (auto identifier = interaction.nodeIdentifier) {
1960 if (auto [handled, failureReason] = selectOptionByValue(*identifier, interaction.text); handled) {
1961 if (failureReason.isEmpty())
1962 return completion(true, interactedWithSelectElementDescription);
1963
1964 return completion(false, WTF::move(failureReason));
1965 }
1966
1967 return dispatchSimulatedClick(*identifier, interaction.text, WTF::move(completion));
1968 }
1969
1970 return completion(false, "Missing nodeIdentifier"_s);
1971 }
1972 case Action::SelectText: {
1973 if (auto identifier = interaction.nodeIdentifier) {
1974 if (auto [handled, failureReason] = selectOptionByValue(WTF::move(*identifier), interaction.text); handled) {
1975 if (failureReason.isEmpty())
1976 return completion(true, interactedWithSelectElementDescription);
1977
1978 return completion(false, WTF::move(failureReason));
1979 }
1980 }
1981
1982 if (interaction.text.isEmpty() && !interaction.nodeIdentifier)
1983 return completion(false, "Missing nodeIdentifier and/or text"_s);
1984
1985 return selectText(frame, WTF::move(interaction.nodeIdentifier), WTF::move(interaction.text), interaction.scrollToVisible, WTF::move(completion));
1986 }
1987 case Action::TextInput: {
1988 if (auto identifier = interaction.nodeIdentifier)
1989 return focusAndInsertText(*identifier, WTF::move(interaction.text), interaction.replaceAll, WTF::move(completion));
1990
1991 return completion(false, "Missing nodeIdentifier"_s);
1992 }
1993 case Action::KeyPress:
1994 return simulateKeyPress(frame, WTF::move(interaction.nodeIdentifier), interaction.text, WTF::move(completion));
1995 case Action::HighlightText: {
1996 if (interaction.text.isEmpty() && !interaction.nodeIdentifier)
1997 return completion(false, "Missing nodeIdentifier and/or text"_s);
1998
1999 return highlightText(frame, WTF::move(interaction.nodeIdentifier), WTF::move(interaction.text), interaction.scrollToVisible, WTF::move(completion));
2000 }
2001 case Action::Scroll:
2002 if (!interaction.text.isEmpty())
2003 return scrollToReveal(frame, WTF::move(interaction.nodeIdentifier), WTF::move(interaction.text), WTF::move(completion));
2004
2005 if (interaction.scrollDelta.isZero())
2006 return completion(false, "Scroll delta is zero"_s);
2007
2008 return scrollBy(frame, WTF::move(interaction.nodeIdentifier), interaction.scrollDelta, WTF::move(completion));
2009 default:
2010 ASSERT_NOT_REACHED()((void)0);
2011 break;
2012 }
2013 completion(false, "Invalid action"_s);
2014}
2015
2016static String normalizedLabelText(const Element& element)
2017{
2018 for (auto attribute : { HTMLNames::aria_labelAttr.get(), HTMLNames::labelAttr.get() }) {
2019 auto text = normalizeText(element.attributeWithoutSynchronization(attribute));
2020 if (!text.isEmpty())
2021 return text;
2022 }
2023
2024 return { };
2025}
2026
2027static String wrapWithDoubleQuotes(StringView text)
2028{
2029 return makeString(u"“", text, u"”");
2030}
2031
2032static String textDescription(const Element& element, Vector<String>& stringsToValidate, bool isTargetElement = true)
2033{
2034 StringBuilder description;
2035
2036 if (element.hasEditableStyle())
2037 description.append("editable "_s);
2038
2039 auto tagName = element.tagName().convertToASCIILowercase();
2040 if (element.isLink())
2041 description.append("link"_s);
2042 else
2043 description.append(tagName);
2044
2045 bool needsParentContext = true;
2046
2047 if (element.isLink()) {
2048 if (auto text = normalizeText(element.attributeWithoutSynchronization(HTMLNames::hrefAttr)); !text.isEmpty()) {
2049 description.append(makeString(" with href "_s, wrapWithDoubleQuotes(WTF::move(text))));
2050 stringsToValidate.append(WTF::move(text));
2051 needsParentContext = false;
2052 }
2053 }
2054
2055 if (auto text = normalizeText(element.attributeWithoutSynchronization(HTMLNames::roleAttr)); !text.isEmpty() && text != tagName) {
2056 description.append(makeString(" with role "_s, wrapWithDoubleQuotes(WTF::move(text))));
2057 needsParentContext = false;
2058 }
2059
2060 if (auto text = normalizedLabelText(element); !text.isEmpty()) {
2061 description.append(makeString(" labeled "_s, wrapWithDoubleQuotes(WTF::move(text))));
2062 stringsToValidate.append(WTF::move(text));
2063 needsParentContext = false;
2064 }
2065
2066 if (auto text = normalizeText(element.attributeWithoutSynchronization(HTMLNames::titleAttr)); !text.isEmpty()) {
2067 description.append(makeString(" titled "_s, wrapWithDoubleQuotes(WTF::move(text))));
2068 stringsToValidate.append(WTF::move(text));
2069 needsParentContext = false;
2070 }
2071
2072 if (auto text = element.attributeWithoutSynchronization(HTMLNames::typeAttr); !text.isEmpty() && text != tagName)
2073 description.append(makeString(" of type "_s, text));
2074
2075 if (auto text = normalizeText(element.attributeWithoutSynchronization(HTMLNames::placeholderAttr)); !text.isEmpty()) {
2076 description.append(makeString(" with placeholder "_s, wrapWithDoubleQuotes(WTF::move(text))));
2077 stringsToValidate.append(WTF::move(text));
2078 needsParentContext = false;
2079 }
2080
2081 static constexpr auto maximumNumberOfClasses = 3;
2082 static constexpr auto minimumClassOrIdLength = 6;
2083 static constexpr auto maximumClassOrIdLength = 20;
2084
2085 auto isCandidateClassOrId = [](StringView text) {
2086 if (text.length() < minimumClassOrIdLength)
2087 return false;
2088
2089 if (text.length() > maximumClassOrIdLength)
2090 return false;
2091
2092 if (!StringEntropyHelpers::isProbablyHumanReadable(text))
2093 return false;
2094
2095 return true;
2096 };
2097
2098 if (auto text = element.attributeWithoutSynchronization(HTMLNames::idAttr); isCandidateClassOrId(text)) {
2099 description.append(makeString(" with id "_s, wrapWithDoubleQuotes(text)));
2100 needsParentContext = false;
2101 }
2102
2103 if (auto classValue = element.attributeWithoutSynchronization(HTMLNames::classAttr); !classValue.isEmpty()) {
2104 Vector<String, maximumNumberOfClasses> humanReadableClassNames;
2105 for (auto className : StringView { classValue }.split(' ')) {
2106 if (!isCandidateClassOrId(className))
2107 continue;
2108
2109 humanReadableClassNames.append(className.toString());
2110 if (humanReadableClassNames.size() >= maximumNumberOfClasses)
2111 break;
2112 }
2113
2114 if (!humanReadableClassNames.isEmpty()) {
2115 auto classOrClasses = humanReadableClassNames.size() > 1 ? " with classes "_s : " with class "_s;
2116 description.append(makeString(classOrClasses, wrapWithDoubleQuotes(makeStringByJoining(humanReadableClassNames, " "_s))));
2117 needsParentContext = false;
2118 }
2119 }
2120
2121 auto elementDescription = description.toString();
2122 if (!needsParentContext)
2123 return elementDescription;
2124
2125 RefPtr parent = element.parentElementInComposedTree();
2126 if (!parent)
2127 return elementDescription;
2128
2129 auto parentDescription = textDescription(*parent, stringsToValidate, false);
2130 if (parentDescription.isEmpty())
2131 return elementDescription;
2132
2133 if (isTargetElement)
2134 return makeString(WTF::move(elementDescription), " under "_s, WTF::move(parentDescription));
2135
2136 return parentDescription;
2137}
2138
2139static String textDescription(Node* node, Vector<String>& stringsToValidate)
2140{
2141 if (!node)
2142 return { };
2143
2144 addBoxShadowIfNeeded(*node, "#ff383c"_s);
2145
2146 auto addRenderedTextOrLabeledChild = [&](const String& description) {
2147 StringBuilder extendedDescription;
2148 extendedDescription.append(description);
2149
2150 String renderedTextSuffix;
2151 auto range = makeRangeSelectingNodeContents(*node);
2152 if (auto text = normalizeText(plainText(range, TextIteratorBehavior::EntersTextControls)); !text.isEmpty()) {
2153 stringsToValidate.append(text);
2154 extendedDescription.append(makeString(", with rendered text "_s, wrapWithDoubleQuotes(WTF::move(text))));
2155 }
2156
2157 String labeledChildSuffix;
2158 if (RefPtr container = dynamicDowncast<ContainerNode>(node)) {
2159 for (Ref child : descendantsOfType<Element>(*container)) {
2160 auto label = normalizedLabelText(child);
2161 if (label.isEmpty())
2162 continue;
2163
2164 stringsToValidate.append(label);
2165 extendedDescription.append(makeString(", containing child labeled "_s, wrapWithDoubleQuotes(WTF::move(label))));
2166 break;
2167 }
2168 }
2169
2170 return extendedDescription.toString();
2171 };
2172
2173 if (RefPtr element = dynamicDowncast<Element>(*node))
2174 return addRenderedTextOrLabeledChild(textDescription(*element, stringsToValidate));
2175
2176 if (RefPtr parentElement = node->parentElementInComposedTree())
2177 return addRenderedTextOrLabeledChild(makeString("child node of "_s, textDescription(*parentElement, stringsToValidate)));
2178
2179 return { };
2180}
2181
2182static String textDescription(std::optional<NodeIdentifier> identifier, Vector<String>& stringsToValidate)
2183{
2184 if (!identifier)
2185 return { };
2186
2187 return textDescription(RefPtr { Node::fromIdentifier(*identifier) }.get(), stringsToValidate);
2188}
2189
2190static String textDescription(LocalFrame& frame, std::optional<NodeIdentifier> identifier, const String& searchText, Action action, Vector<String>& stringsToValidate)
2191{
2192 if (!identifier && searchText.isEmpty())
2193 return { };
2194
2195 RefPtr target = resolveNodeWithBodyAsFallback(frame, identifier);
2196 auto searchTextPrefix = emptyString();
2197 if (!searchText.isEmpty()) {
2198 auto range = action == Action::Click ? searchForClickTarget(*target, searchText) : searchForText(*target, searchText);
2199 if (!range)
2200 return { };
2201
2202 target = commonInclusiveAncestor<ComposedTree>(*range);
2203
2204 auto escapedSearchText = normalizeText(searchText);
2205 stringsToValidate.append(escapedSearchText);
2206 searchTextPrefix = makeString(wrapWithDoubleQuotes(escapedSearchText), " in "_s);
2207 }
2208
2209 if (!target)
2210 return { };
2211
2212 return makeString(WTF::move(searchTextPrefix), textDescription(target.get(), stringsToValidate));
2213}
2214
2215static String textDescription(LocalFrame& frame, FloatPoint locationInRootView, Vector<String>& stringsToValidate)
2216{
2217 RefPtr document = frame.document();
2218 if (!document)
2219 return { };
2220
2221 RefPtr view = frame.view();
2222 if (!view)
2223 return { };
2224
2225 RefPtr targetNode = findNodeAtRootViewLocation(*view, *document, locationInRootView);
2226 if (!targetNode)
2227 return { };
2228
2229 return textDescription(targetNode.get(), stringsToValidate);
2230}
2231
2232InteractionDescription interactionDescription(const Interaction& interaction, LocalFrame& frame)
2233{
2234 auto action = interaction.action;
2235 bool isSingleKeyPress = action == Action::KeyPress && PlatformKeyboardEvent::syntheticEventFromText(PlatformEvent::Type::KeyUp, interaction.text);
2236
2237 StringBuilder description;
2238 description.append([&] -> String {
2239 if (isSingleKeyPress)
2240 return makeString("Press the "_s, interaction.text, " key"_s);
2241
2242 switch (action) {
2243 case Action::Click:
2244 return "Click"_s;
2245 case Action::SelectText:
2246 return "Select text"_s;
2247 case Action::SelectMenuItem:
2248 return "Select menu item"_s;
2249 case Action::TextInput:
2250 case Action::KeyPress:
2251 return "Enter text"_s;
2252 case Action::HighlightText:
2253 return "Highlight text"_s;
2254 case Action::Scroll:
2255 return "Scroll"_s;
2256 }
2257 ASSERT_NOT_REACHED()((void)0);
2258 return { };
2259 }());
2260
2261 bool usesSearchText = [&] {
2262 switch (action) {
2263 case Action::SelectText:
2264 case Action::Click:
2265 case Action::HighlightText:
2266 case Action::Scroll:
2267 return true;
2268 case Action::SelectMenuItem:
2269 case Action::TextInput:
2270 case Action::KeyPress:
2271 return false;
2272 }
2273 ASSERT_NOT_REACHED()((void)0);
2274 return false;
2275 }();
2276
2277 Vector<String> stringsToValidate;
2278 if (!usesSearchText) {
2279 if (auto escapedString = normalizeText(interaction.text); !escapedString.isEmpty()) {
2280 description.append(makeString(" "_s, wrapWithDoubleQuotes(String { escapedString })));
2281 stringsToValidate.append(WTF::move(escapedString));
2282 }
2283 }
2284
2285 if (action == Action::Scroll && interaction.text.isEmpty()) {
2286 auto delta = roundedIntSize(interaction.scrollDelta);
2287 description.append(makeString(" by ("_s, delta.width(), ", "_s, delta.height(), ')'));
2288 }
2289
2290 auto appendElementString = [&]<typename... T>(T&&... args) {
2291 auto elementString = textDescription(std::forward<T>(args)...);
2292 if (elementString.isEmpty())
2293 return;
2294
2295 auto elementPrefix = [&interaction] -> String {
2296 switch (interaction.action) {
2297 case Action::Click:
2298 return " on "_s;
2299 case Action::SelectText:
2300 return " inside "_s;
2301 case Action::SelectMenuItem:
2302 case Action::KeyPress:
2303 case Action::HighlightText:
2304 return " in "_s;
2305 case Action::Scroll:
2306 return interaction.text.isEmpty() ? " in "_s : " to reveal "_s;
2307 case Action::TextInput:
2308 return " into "_s;
2309 }
2310 ASSERT_NOT_REACHED()((void)0);
2311 return { };
2312 }();
2313
2314 description.append(makeString(WTF::move(elementPrefix), WTF::move(elementString)));
2315 };
2316
2317 if (auto location = interaction.locationInRootView) {
2318 auto roundedLocation = roundedIntPoint(*location);
2319 description.append(" at coordinates ("_s, roundedLocation.x(), ", "_s, roundedLocation.y(), ')');
2320 appendElementString(frame, *location, stringsToValidate);
2321 } else if (usesSearchText)
2322 appendElementString(frame, interaction.nodeIdentifier, interaction.text, interaction.action, stringsToValidate);
2323 else
2324 appendElementString(interaction.nodeIdentifier, stringsToValidate);
2325
2326 bool appendedReplaceTextDescription = false;
2327 if ((action == Action::KeyPress || action == Action::TextInput) && interaction.replaceAll) {
2328 appendedReplaceTextDescription = true;
2329 description.append(", replacing any existing content"_s);
2330 }
2331
2332 if ((action == Action::SelectText || action == Action::HighlightText) && interaction.scrollToVisible)
2333 description.append(makeString(appendedReplaceTextDescription ? " and"_s : ","_s, " scrolling the targeted range into view"_s));
2334
2335 return { description.toString(), WTF::move(stringsToValidate) };
2336}
2337
2338RefPtr<Element> elementForExtractedText(const LocalFrame& frame, ExtractedText&& extractedText)
2339{
2340 auto range = rangeForExtractedText(frame, WTF::move(extractedText));
2341 if (!range)
2342 return { };
2343
2344 RefPtr node = commonInclusiveAncestor<ComposedTree>(*range);
2345 if (!node)
2346 return { };
2347
2348 RefPtr element = dynamicDowncast<Element>(node);
2349 return element ? element : RefPtr { node->parentElementInComposedTree() };
2350}
2351
2352RefPtr<Element> containerElementForExtractedText(const LocalFrame& frame, ExtractedText&& extractedText)
2353{
2354 RefPtr element = elementForExtractedText(frame, WTF::move(extractedText));
2355 if (!element)
2356 return { };
2357
2358 RefPtr container = findLargeContainerAboveNode(*element, minimumSizeForLargeContainer);
2359 if (!container)
2360 return element;
2361
2362 if (RefPtr containerElement = dynamicDowncast<Element>(container))
2363 return containerElement;
2364
2365 if (RefPtr containerElement = container->parentElementInComposedTree())
2366 return containerElement;
2367
2368 return element;
2369}
2370
2371std::optional<SimpleRange> rangeForExtractedText(const LocalFrame& frame, ExtractedText&& extractedText)
2372{
2373 auto [text, nodeIdentifier] = extractedText;
2374
2375 RefPtr node = resolveNodeWithBodyAsFallback(frame, nodeIdentifier);
2376 if (text.isEmpty())
2377 return { makeRangeSelectingNodeContents(*node) };
2378
2379 return searchForText(*node, text);
2380}
2381
2382Vector<FilterRule> extractRules(Vector<FilterRuleData>&& data)
2383{
2384 return WTF::map(WTF::move(data), [](auto&& data) -> FilterRule {
2385 auto&& [name, urlPattern, scriptSource] = WTF::move(data);
2386 if (urlPattern.isEmpty())
2387 return { WTF::move(name), { FilterRulePattern::Global }, WTF::move(scriptSource) };
2388
2389 auto regex = Yarr::RegularExpression { urlPattern, { Yarr::Flags::IgnoreCase } };
2390 if (!regex.isValid())
2391 return { WTF::move(name), { FilterRulePattern::Global }, WTF::move(scriptSource) };
2392
2393 return { WTF::move(name), { WTF::move(regex) }, WTF::move(scriptSource) };
2394 });
2395}
2396
2397static DOMWrapperWorld& filteringWorld()
2398{
2399 static NeverDestroyed<RefPtr<DOMWrapperWorld>> world = DOMWrapperWorld::create(commonVM(), DOMWrapperWorld::Type::Internal, "Text Extraction Filtering Rules"_s);
2400 return *world.get();
2401}
2402
2403void applyRules(const String& input, std::optional<NodeIdentifier>&& containerNodeID, const Vector<FilterRule>& rules, Page& page, CompletionHandler<void(const String&)>&& completion)
2404{
2405 if (rules.isEmpty())
2406 return completion(input);
2407
2408 RefPtr mainFrame = page.localMainFrame();
2409 if (!mainFrame)
2410 return completion(input);
2411
2412 RefPtr document = mainFrame->document();
2413 if (!document)
2414 return completion(input);
2415
2416 RefPtr containerNode = resolveNodeWithBodyAsFallback(*mainFrame, WTF::move(containerNodeID));
2417 if (!containerNode)
2418 return completion(input);
2419
2420 Ref world = filteringWorld();
2421 auto makeArguments = [&] {
2422 ArgumentMap argumentMap;
2423 argumentMap.reserveInitialCapacity(2);
2424 argumentMap.add("input"_s, [input](auto& lexicalGlobalObject) {
2425 JSLockHolder lock { &lexicalGlobalObject };
2426 return JSValue { jsString(commonVM(), input) };
2427 });
2428 argumentMap.add("containerNode"_s, [containerNode, mainFrame, world = world.copyRef()](auto& lexicalGlobalObject) {
2429 if (!containerNode)
2430 return jsNull();
2431
2432 JSLockHolder lock { &lexicalGlobalObject };
2433 return toJS(&lexicalGlobalObject, protect(mainFrame->script())->globalObject(world), *containerNode);
2434 });
2435 return std::make_optional(WTF::move(argumentMap));
2436 };
2437
2438 auto filteredStrings = Box<Vector<String>>::create();
2439 auto aggregator = MainRunLoopCallbackAggregator::create([completion = WTF::move(completion), input, filteredStrings] mutable {
2440 if (filteredStrings->isEmpty())
2441 return completion(input);
2442
2443 auto shortestFilteredString = std::ranges::min(*filteredStrings, { }, [](auto& string) {
2444 return string.length();
2445 });
2446 completion(WTF::move(shortestFilteredString));
2447 });
2448
2449 auto urlString = document->url().string();
2450 for (auto& [name, urlPattern, source] : rules) {
2451 bool shouldApplyRule = WTF::switchOn(urlPattern, [](FilterRulePattern pattern) {
2452 return pattern == FilterRulePattern::Global;
2453 }, [&](const Yarr::RegularExpression& regex) {
2454 return regex.match(urlString) >= 0;
2455 });
2456
2457 if (!shouldApplyRule)
2458 continue;
2459
2460 auto parameters = RunJavaScriptParameters {
2461 source,
2462 SourceTaintedOrigin::Untainted,
2463 { },
2464 true, // runAsAsyncFunction
2465 makeArguments(),
2466 false, // forceUserGesture
2467 RemoveTransientActivation::No
2468 };
2469
2470 JSLockHolder lock(commonVM());
2471 protect(mainFrame->script())->executeAsynchronousUserAgentScriptInWorld(world, WTF::move(parameters), [document, aggregator, filteredStrings](auto valueOrException) {
2472 if (!valueOrException)
2473 return;
2474
2475 auto jsValue = valueOrException.value();
2476 if (!jsValue.isString())
2477 return;
2478
2479 filteredStrings->append(jsValue.getString(document->globalObject()));
2480 });
2481 }
2482}
2483
2484} // namespace TextExtraction
2485} // namespace WebCore