1 | |
2 | /* |
3 | * Copyright (C) 2016, 2017 Apple Inc. All rights reserved. |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions |
7 | * are met: |
8 | * 1. Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * 2. Redistributions in binary form must reproduce the above copyright |
11 | * notice, this list of conditions and the following disclaimer in the |
12 | * documentation and/or other materials provided with the distribution. |
13 | * |
14 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
16 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
17 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
18 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
24 | * THE POSSIBILITY OF SUCH DAMAGE. |
25 | */ |
26 | |
27 | #include "config.h" |
28 | #include "WebAutomationSession.h" |
29 | |
30 | #include "APIArray.h" |
31 | #include "APIAutomationSessionClient.h" |
32 | #include "APINavigation.h" |
33 | #include "APIOpenPanelParameters.h" |
34 | #include "AutomationProtocolObjects.h" |
35 | #include "CoordinateSystem.h" |
36 | #include "WebAutomationSessionMacros.h" |
37 | #include "WebAutomationSessionMessages.h" |
38 | #include "WebAutomationSessionProxyMessages.h" |
39 | #include "WebCookieManagerProxy.h" |
40 | #include "WebFullScreenManagerProxy.h" |
41 | #include "WebInspectorProxy.h" |
42 | #include "WebOpenPanelResultListenerProxy.h" |
43 | #include "WebProcessPool.h" |
44 | #include <JavaScriptCore/InspectorBackendDispatcher.h> |
45 | #include <JavaScriptCore/InspectorFrontendRouter.h> |
46 | #include <WebCore/MIMETypeRegistry.h> |
47 | #include <algorithm> |
48 | #include <wtf/HashMap.h> |
49 | #include <wtf/Optional.h> |
50 | #include <wtf/URL.h> |
51 | #include <wtf/UUID.h> |
52 | #include <wtf/text/StringConcatenate.h> |
53 | |
54 | namespace WebKit { |
55 | |
56 | using namespace Inspector; |
57 | using namespace WebCore; |
58 | |
59 | String AutomationCommandError::toProtocolString() |
60 | { |
61 | String protocolErrorName = Inspector::Protocol::AutomationHelpers::getEnumConstantValue(type); |
62 | if (!message.hasValue()) |
63 | return protocolErrorName; |
64 | |
65 | return makeString(protocolErrorName, errorNameAndDetailsSeparator, message.value()); |
66 | } |
67 | |
68 | // §8. Sessions |
69 | // https://www.w3.org/TR/webdriver/#dfn-session-page-load-timeout |
70 | static const Seconds defaultPageLoadTimeout = 300_s; |
71 | // https://www.w3.org/TR/webdriver/#dfn-page-loading-strategy |
72 | static const Inspector::Protocol::Automation::PageLoadStrategy defaultPageLoadStrategy = Inspector::Protocol::Automation::PageLoadStrategy::Normal; |
73 | |
74 | WebAutomationSession::WebAutomationSession() |
75 | : m_client(std::make_unique<API::AutomationSessionClient>()) |
76 | , m_frontendRouter(FrontendRouter::create()) |
77 | , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) |
78 | , m_domainDispatcher(AutomationBackendDispatcher::create(m_backendDispatcher, this)) |
79 | , m_domainNotifier(std::make_unique<AutomationFrontendDispatcher>(m_frontendRouter)) |
80 | , m_loadTimer(RunLoop::main(), this, &WebAutomationSession::loadTimerFired) |
81 | { |
82 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
83 | // Set up canonical input sources to be used for 'performInteractionSequence' and 'cancelInteractionSequence'. |
84 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
85 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Touch)); |
86 | #endif |
87 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
88 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Mouse)); |
89 | #endif |
90 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
91 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Keyboard)); |
92 | #endif |
93 | m_inputSources.add(SimulatedInputSource::create(SimulatedInputSourceType::Null)); |
94 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
95 | } |
96 | |
97 | WebAutomationSession::~WebAutomationSession() |
98 | { |
99 | ASSERT(!m_client); |
100 | ASSERT(!m_processPool); |
101 | } |
102 | |
103 | void WebAutomationSession::setClient(std::unique_ptr<API::AutomationSessionClient>&& client) |
104 | { |
105 | m_client = WTFMove(client); |
106 | } |
107 | |
108 | void WebAutomationSession::setProcessPool(WebKit::WebProcessPool* processPool) |
109 | { |
110 | if (m_processPool) |
111 | m_processPool->removeMessageReceiver(Messages::WebAutomationSession::messageReceiverName()); |
112 | |
113 | m_processPool = processPool; |
114 | |
115 | if (m_processPool) |
116 | m_processPool->addMessageReceiver(Messages::WebAutomationSession::messageReceiverName(), *this); |
117 | } |
118 | |
119 | // NOTE: this class could be split at some point to support local and remote automation sessions. |
120 | // For now, it only works with a remote automation driver over a RemoteInspector connection. |
121 | |
122 | #if ENABLE(REMOTE_INSPECTOR) |
123 | |
124 | // Inspector::RemoteAutomationTarget API |
125 | |
126 | void WebAutomationSession::dispatchMessageFromRemote(const String& message) |
127 | { |
128 | m_backendDispatcher->dispatch(message); |
129 | } |
130 | |
131 | void WebAutomationSession::connect(Inspector::FrontendChannel& channel, bool isAutomaticConnection, bool immediatelyPause) |
132 | { |
133 | UNUSED_PARAM(isAutomaticConnection); |
134 | UNUSED_PARAM(immediatelyPause); |
135 | |
136 | m_remoteChannel = &channel; |
137 | m_frontendRouter->connectFrontend(channel); |
138 | |
139 | setIsPaired(true); |
140 | } |
141 | |
142 | void WebAutomationSession::disconnect(Inspector::FrontendChannel& channel) |
143 | { |
144 | ASSERT(&channel == m_remoteChannel); |
145 | terminate(); |
146 | } |
147 | |
148 | #endif // ENABLE(REMOTE_INSPECTOR) |
149 | |
150 | void WebAutomationSession::terminate() |
151 | { |
152 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
153 | for (auto& identifier : copyToVector(m_pendingKeyboardEventsFlushedCallbacksPerPage.keys())) { |
154 | auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(identifier); |
155 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(InternalError)); |
156 | } |
157 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
158 | |
159 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
160 | for (auto& identifier : copyToVector(m_pendingMouseEventsFlushedCallbacksPerPage.keys())) { |
161 | auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(identifier); |
162 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(InternalError)); |
163 | } |
164 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
165 | |
166 | #if ENABLE(REMOTE_INSPECTOR) |
167 | if (Inspector::FrontendChannel* channel = m_remoteChannel) { |
168 | m_remoteChannel = nullptr; |
169 | m_frontendRouter->disconnectFrontend(*channel); |
170 | } |
171 | |
172 | setIsPaired(false); |
173 | #endif |
174 | |
175 | if (m_client) |
176 | m_client->didDisconnectFromRemote(*this); |
177 | } |
178 | |
179 | WebPageProxy* WebAutomationSession::webPageProxyForHandle(const String& handle) |
180 | { |
181 | auto iter = m_handleWebPageMap.find(handle); |
182 | if (iter == m_handleWebPageMap.end()) |
183 | return nullptr; |
184 | return WebProcessProxy::webPage(iter->value); |
185 | } |
186 | |
187 | String WebAutomationSession::handleForWebPageProxy(const WebPageProxy& webPageProxy) |
188 | { |
189 | auto iter = m_webPageHandleMap.find(webPageProxy.pageID()); |
190 | if (iter != m_webPageHandleMap.end()) |
191 | return iter->value; |
192 | |
193 | String handle = "page-" + createCanonicalUUIDString().convertToASCIIUppercase(); |
194 | |
195 | auto firstAddResult = m_webPageHandleMap.add(webPageProxy.pageID(), handle); |
196 | RELEASE_ASSERT(firstAddResult.isNewEntry); |
197 | |
198 | auto secondAddResult = m_handleWebPageMap.add(handle, webPageProxy.pageID()); |
199 | RELEASE_ASSERT(secondAddResult.isNewEntry); |
200 | |
201 | return handle; |
202 | } |
203 | |
204 | Optional<uint64_t> WebAutomationSession::webFrameIDForHandle(const String& handle) |
205 | { |
206 | if (handle.isEmpty()) |
207 | return 0; |
208 | |
209 | auto iter = m_handleWebFrameMap.find(handle); |
210 | if (iter == m_handleWebFrameMap.end()) |
211 | return WTF::nullopt; |
212 | |
213 | return iter->value; |
214 | } |
215 | |
216 | String WebAutomationSession::handleForWebFrameID(uint64_t frameID) |
217 | { |
218 | if (!frameID) |
219 | return emptyString(); |
220 | |
221 | for (auto& process : m_processPool->processes()) { |
222 | if (WebFrameProxy* frame = process->webFrame(frameID)) { |
223 | if (frame->isMainFrame()) |
224 | return emptyString(); |
225 | break; |
226 | } |
227 | } |
228 | |
229 | auto iter = m_webFrameHandleMap.find(frameID); |
230 | if (iter != m_webFrameHandleMap.end()) |
231 | return iter->value; |
232 | |
233 | String handle = "frame-" + createCanonicalUUIDString().convertToASCIIUppercase(); |
234 | |
235 | auto firstAddResult = m_webFrameHandleMap.add(frameID, handle); |
236 | RELEASE_ASSERT(firstAddResult.isNewEntry); |
237 | |
238 | auto secondAddResult = m_handleWebFrameMap.add(handle, frameID); |
239 | RELEASE_ASSERT(secondAddResult.isNewEntry); |
240 | |
241 | return handle; |
242 | } |
243 | |
244 | String WebAutomationSession::handleForWebFrameProxy(const WebFrameProxy& webFrameProxy) |
245 | { |
246 | return handleForWebFrameID(webFrameProxy.frameID()); |
247 | } |
248 | |
249 | Ref<Inspector::Protocol::Automation::BrowsingContext> WebAutomationSession::buildBrowsingContextForPage(WebPageProxy& page, WebCore::FloatRect windowFrame) |
250 | { |
251 | auto originObject = Inspector::Protocol::Automation::Point::create() |
252 | .setX(windowFrame.x()) |
253 | .setY(windowFrame.y()) |
254 | .release(); |
255 | |
256 | auto sizeObject = Inspector::Protocol::Automation::Size::create() |
257 | .setWidth(windowFrame.width()) |
258 | .setHeight(windowFrame.height()) |
259 | .release(); |
260 | |
261 | bool isActive = page.isViewVisible() && page.isViewFocused() && page.isViewWindowActive(); |
262 | String handle = handleForWebPageProxy(page); |
263 | |
264 | return Inspector::Protocol::Automation::BrowsingContext::create() |
265 | .setHandle(handle) |
266 | .setActive(isActive) |
267 | .setUrl(page.pageLoadState().activeURL()) |
268 | .setWindowOrigin(WTFMove(originObject)) |
269 | .setWindowSize(WTFMove(sizeObject)) |
270 | .release(); |
271 | } |
272 | |
273 | // Platform-independent Commands. |
274 | |
275 | void WebAutomationSession::getNextContext(Ref<WebAutomationSession>&& protectedThis, Vector<Ref<WebPageProxy>>&& pages, Ref<JSON::ArrayOf<Inspector::Protocol::Automation::BrowsingContext>> contexts, Ref<WebAutomationSession::GetBrowsingContextsCallback>&& callback) |
276 | { |
277 | if (pages.isEmpty()) { |
278 | callback->sendSuccess(WTFMove(contexts)); |
279 | return; |
280 | } |
281 | auto page = pages.takeLast(); |
282 | auto& webPageProxy = page.get(); |
283 | webPageProxy.getWindowFrameWithCallback([this, protectedThis = WTFMove(protectedThis), callback = WTFMove(callback), pages = WTFMove(pages), contexts = WTFMove(contexts), page = WTFMove(page)](WebCore::FloatRect windowFrame) mutable { |
284 | contexts->addItem(protectedThis->buildBrowsingContextForPage(page.get(), windowFrame)); |
285 | getNextContext(WTFMove(protectedThis), WTFMove(pages), WTFMove(contexts), WTFMove(callback)); |
286 | }); |
287 | } |
288 | |
289 | void WebAutomationSession::getBrowsingContexts(Ref<GetBrowsingContextsCallback>&& callback) |
290 | { |
291 | Vector<Ref<WebPageProxy>> pages; |
292 | for (auto& process : m_processPool->processes()) { |
293 | for (auto* page : process->pages()) { |
294 | ASSERT(page); |
295 | if (!page->isControlledByAutomation()) |
296 | continue; |
297 | pages.append(*page); |
298 | } |
299 | } |
300 | |
301 | getNextContext(makeRef(*this), WTFMove(pages), JSON::ArrayOf<Inspector::Protocol::Automation::BrowsingContext>::create(), WTFMove(callback)); |
302 | } |
303 | |
304 | void WebAutomationSession::getBrowsingContext(const String& handle, Ref<GetBrowsingContextCallback>&& callback) |
305 | { |
306 | WebPageProxy* page = webPageProxyForHandle(handle); |
307 | if (!page) |
308 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
309 | |
310 | page->getWindowFrameWithCallback([protectedThis = makeRef(*this), page = makeRef(*page), callback = WTFMove(callback)](WebCore::FloatRect windowFrame) mutable { |
311 | callback->sendSuccess(protectedThis->buildBrowsingContextForPage(page.get(), windowFrame)); |
312 | }); |
313 | } |
314 | |
315 | static Inspector::Protocol::Automation::BrowsingContextPresentation toProtocol(API::AutomationSessionClient::BrowsingContextPresentation value) |
316 | { |
317 | switch (value) { |
318 | case API::AutomationSessionClient::BrowsingContextPresentation::Tab: |
319 | return Inspector::Protocol::Automation::BrowsingContextPresentation::Tab; |
320 | case API::AutomationSessionClient::BrowsingContextPresentation::Window: |
321 | return Inspector::Protocol::Automation::BrowsingContextPresentation::Window; |
322 | } |
323 | |
324 | RELEASE_ASSERT_NOT_REACHED(); |
325 | } |
326 | |
327 | void WebAutomationSession::createBrowsingContext(const String* optionalPresentationHint, Ref<CreateBrowsingContextCallback>&& callback) |
328 | { |
329 | ASSERT(m_client); |
330 | if (!m_client) |
331 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote session could not request a new browsing context." ); |
332 | |
333 | uint16_t options = 0; |
334 | |
335 | if (optionalPresentationHint) { |
336 | auto parsedPresentationHint = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::BrowsingContextPresentation>(*optionalPresentationHint); |
337 | if (parsedPresentationHint.hasValue() && parsedPresentationHint.value() == Inspector::Protocol::Automation::BrowsingContextPresentation::Tab) |
338 | options |= API::AutomationSessionBrowsingContextOptionsPreferNewTab; |
339 | } |
340 | |
341 | m_client->requestNewPageWithOptions(*this, static_cast<API::AutomationSessionBrowsingContextOptions>(options), [protectedThis = makeRef(*this), callback = WTFMove(callback)](WebPageProxy* page) { |
342 | if (!page) |
343 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote session failed to create a new browsing context." ); |
344 | |
345 | callback->sendSuccess(protectedThis->handleForWebPageProxy(*page), toProtocol(protectedThis->m_client->currentPresentationOfPage(protectedThis.get(), *page))); |
346 | }); |
347 | } |
348 | |
349 | void WebAutomationSession::closeBrowsingContext(Inspector::ErrorString& errorString, const String& handle) |
350 | { |
351 | WebPageProxy* page = webPageProxyForHandle(handle); |
352 | if (!page) |
353 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
354 | |
355 | page->closePage(false); |
356 | } |
357 | |
358 | void WebAutomationSession::switchToBrowsingContext(const String& browsingContextHandle, const String* optionalFrameHandle, Ref<SwitchToBrowsingContextCallback>&& callback) |
359 | { |
360 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
361 | if (!page) |
362 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
363 | |
364 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
365 | if (!frameID) |
366 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
367 | |
368 | |
369 | m_client->requestSwitchToPage(*this, *page, [frameID, page = makeRef(*page), callback = WTFMove(callback)]() { |
370 | page->setFocus(true); |
371 | page->process().send(Messages::WebAutomationSessionProxy::FocusFrame(page->pageID(), frameID.value()), 0); |
372 | |
373 | callback->sendSuccess(); |
374 | }); |
375 | } |
376 | |
377 | void WebAutomationSession::setWindowFrameOfBrowsingContext(const String& handle, const JSON::Object* optionalOriginObject, const JSON::Object* optionalSizeObject, Ref<SetWindowFrameOfBrowsingContextCallback>&& callback) |
378 | { |
379 | Optional<float> x; |
380 | Optional<float> y; |
381 | if (optionalOriginObject) { |
382 | if (!(x = optionalOriginObject->getNumber<float>("x"_s ))) |
383 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'x' parameter was not found or invalid." ); |
384 | |
385 | if (!(y = optionalOriginObject->getNumber<float>("y"_s ))) |
386 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'y' parameter was not found or invalid." ); |
387 | |
388 | if (x.value() < 0) |
389 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'x' parameter had an invalid value." ); |
390 | |
391 | if (y.value() < 0) |
392 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'y' parameter had an invalid value." ); |
393 | } |
394 | |
395 | Optional<float> width; |
396 | Optional<float> height; |
397 | if (optionalSizeObject) { |
398 | if (!(width = optionalSizeObject->getNumber<float>("width"_s ))) |
399 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'width' parameter was not found or invalid." ); |
400 | |
401 | if (!(height = optionalSizeObject->getNumber<float>("height"_s ))) |
402 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The 'height' parameter was not found or invalid." ); |
403 | |
404 | if (width.value() < 0) |
405 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'width' parameter had an invalid value." ); |
406 | |
407 | if (height.value() < 0) |
408 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The 'height' parameter had an invalid value." ); |
409 | } |
410 | |
411 | WebPageProxy* page = webPageProxyForHandle(handle); |
412 | if (!page) |
413 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
414 | |
415 | exitFullscreenWindowForPage(*page, [this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page), width, height, x, y]() mutable { |
416 | auto& webPage = *page; |
417 | this->restoreWindowForPage(webPage, [callback = WTFMove(callback), page = WTFMove(page), width, height, x, y]() mutable { |
418 | auto& webPage = *page; |
419 | webPage.getWindowFrameWithCallback([callback = WTFMove(callback), page = WTFMove(page), width, height, x, y](WebCore::FloatRect originalFrame) mutable { |
420 | WebCore::FloatRect newFrame = WebCore::FloatRect(WebCore::FloatPoint(x.valueOr(originalFrame.location().x()), y.valueOr(originalFrame.location().y())), WebCore::FloatSize(width.valueOr(originalFrame.size().width()), height.valueOr(originalFrame.size().height()))); |
421 | if (newFrame != originalFrame) |
422 | page->setWindowFrame(newFrame); |
423 | |
424 | callback->sendSuccess(); |
425 | }); |
426 | }); |
427 | }); |
428 | } |
429 | |
430 | static Optional<Inspector::Protocol::Automation::PageLoadStrategy> pageLoadStrategyFromStringParameter(const String* optionalPageLoadStrategyString) |
431 | { |
432 | if (!optionalPageLoadStrategyString) |
433 | return defaultPageLoadStrategy; |
434 | |
435 | auto parsedPageLoadStrategy = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::PageLoadStrategy>(*optionalPageLoadStrategyString); |
436 | if (!parsedPageLoadStrategy) |
437 | return WTF::nullopt; |
438 | return parsedPageLoadStrategy; |
439 | } |
440 | |
441 | void WebAutomationSession::waitForNavigationToComplete(const String& browsingContextHandle, const String* optionalFrameHandle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<WaitForNavigationToCompleteCallback>&& callback) |
442 | { |
443 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
444 | if (!page) |
445 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
446 | |
447 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
448 | if (!pageLoadStrategy) |
449 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
450 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
451 | |
452 | // If page is loading and there's an active JavaScript dialog is probably because the |
453 | // dialog was started in an onload handler, so in case of normal page load strategy the |
454 | // load will not finish until the dialog is dismissed. Instead of waiting for the timeout, |
455 | // we return without waiting since we know it will timeout for sure. We want to check |
456 | // arguments first, though. |
457 | bool shouldTimeoutDueToUnexpectedAlert = pageLoadStrategy.value() == Inspector::Protocol::Automation::PageLoadStrategy::Normal |
458 | && page->pageLoadState().isLoading() && m_client->isShowingJavaScriptDialogOnPage(*this, *page); |
459 | |
460 | if (optionalFrameHandle && !optionalFrameHandle->isEmpty()) { |
461 | Optional<uint64_t> frameID = webFrameIDForHandle(*optionalFrameHandle); |
462 | if (!frameID) |
463 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
464 | WebFrameProxy* frame = page->process().webFrame(frameID.value()); |
465 | if (!frame) |
466 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
467 | if (!shouldTimeoutDueToUnexpectedAlert) |
468 | waitForNavigationToCompleteOnFrame(*frame, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
469 | } else { |
470 | if (!shouldTimeoutDueToUnexpectedAlert) |
471 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
472 | } |
473 | |
474 | if (shouldTimeoutDueToUnexpectedAlert) { |
475 | // §9 Navigation. |
476 | // 7. If the previous step completed by the session page load timeout being reached and the browser does not |
477 | // have an active user prompt, return error with error code timeout. |
478 | // 8. Return success with data null. |
479 | // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-wait-for-navigation-to-complete |
480 | callback->sendSuccess(); |
481 | } |
482 | } |
483 | |
484 | void WebAutomationSession::waitForNavigationToCompleteOnPage(WebPageProxy& page, Inspector::Protocol::Automation::PageLoadStrategy loadStrategy, Seconds timeout, Ref<Inspector::BackendDispatcher::CallbackBase>&& callback) |
485 | { |
486 | ASSERT(!m_loadTimer.isActive()); |
487 | if (loadStrategy == Inspector::Protocol::Automation::PageLoadStrategy::None || (!page.pageLoadState().isLoading() && !page.pageLoadState().hasUncommittedLoad())) { |
488 | callback->sendSuccess(JSON::Object::create()); |
489 | return; |
490 | } |
491 | |
492 | m_loadTimer.startOneShot(timeout); |
493 | switch (loadStrategy) { |
494 | case Inspector::Protocol::Automation::PageLoadStrategy::Normal: |
495 | m_pendingNormalNavigationInBrowsingContextCallbacksPerPage.set(page.pageID(), WTFMove(callback)); |
496 | break; |
497 | case Inspector::Protocol::Automation::PageLoadStrategy::Eager: |
498 | m_pendingEagerNavigationInBrowsingContextCallbacksPerPage.set(page.pageID(), WTFMove(callback)); |
499 | break; |
500 | case Inspector::Protocol::Automation::PageLoadStrategy::None: |
501 | ASSERT_NOT_REACHED(); |
502 | } |
503 | } |
504 | |
505 | void WebAutomationSession::waitForNavigationToCompleteOnFrame(WebFrameProxy& frame, Inspector::Protocol::Automation::PageLoadStrategy loadStrategy, Seconds timeout, Ref<Inspector::BackendDispatcher::CallbackBase>&& callback) |
506 | { |
507 | ASSERT(!m_loadTimer.isActive()); |
508 | if (loadStrategy == Inspector::Protocol::Automation::PageLoadStrategy::None || frame.frameLoadState().state() == FrameLoadState::State::Finished) { |
509 | callback->sendSuccess(JSON::Object::create()); |
510 | return; |
511 | } |
512 | |
513 | m_loadTimer.startOneShot(timeout); |
514 | switch (loadStrategy) { |
515 | case Inspector::Protocol::Automation::PageLoadStrategy::Normal: |
516 | m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame.set(frame.frameID(), WTFMove(callback)); |
517 | break; |
518 | case Inspector::Protocol::Automation::PageLoadStrategy::Eager: |
519 | m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame.set(frame.frameID(), WTFMove(callback)); |
520 | break; |
521 | case Inspector::Protocol::Automation::PageLoadStrategy::None: |
522 | ASSERT_NOT_REACHED(); |
523 | } |
524 | } |
525 | |
526 | void WebAutomationSession::respondToPendingPageNavigationCallbacksWithTimeout(HashMap<PageIdentifier, RefPtr<Inspector::BackendDispatcher::CallbackBase>>& map) |
527 | { |
528 | Inspector::ErrorString timeoutError = STRING_FOR_PREDEFINED_ERROR_NAME(Timeout); |
529 | for (auto id : copyToVector(map.keys())) { |
530 | auto page = WebProcessProxy::webPage(id); |
531 | auto callback = map.take(id); |
532 | if (page && m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
533 | callback->sendSuccess(JSON::Object::create()); |
534 | else |
535 | callback->sendFailure(timeoutError); |
536 | } |
537 | } |
538 | |
539 | static WebPageProxy* findPageForFrameID(const WebProcessPool& processPool, uint64_t frameID) |
540 | { |
541 | for (auto& process : processPool.processes()) { |
542 | if (auto* frame = process->webFrame(frameID)) |
543 | return frame->page(); |
544 | } |
545 | return nullptr; |
546 | } |
547 | |
548 | void WebAutomationSession::respondToPendingFrameNavigationCallbacksWithTimeout(HashMap<uint64_t, RefPtr<Inspector::BackendDispatcher::CallbackBase>>& map) |
549 | { |
550 | Inspector::ErrorString timeoutError = STRING_FOR_PREDEFINED_ERROR_NAME(Timeout); |
551 | for (auto id : copyToVector(map.keys())) { |
552 | auto* page = findPageForFrameID(*m_processPool, id); |
553 | auto callback = map.take(id); |
554 | if (page && m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
555 | callback->sendSuccess(JSON::Object::create()); |
556 | else |
557 | callback->sendFailure(timeoutError); |
558 | } |
559 | } |
560 | |
561 | void WebAutomationSession::loadTimerFired() |
562 | { |
563 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame); |
564 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame); |
565 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerPage); |
566 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingEagerNavigationInBrowsingContextCallbacksPerPage); |
567 | } |
568 | |
569 | void WebAutomationSession::maximizeWindowOfBrowsingContext(const String& browsingContextHandle, Ref<MaximizeWindowOfBrowsingContextCallback>&& callback) |
570 | { |
571 | auto* page = webPageProxyForHandle(browsingContextHandle); |
572 | if (!page) |
573 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
574 | |
575 | exitFullscreenWindowForPage(*page, [this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page)]() mutable { |
576 | auto& webPage = *page; |
577 | restoreWindowForPage(webPage, [this, callback = WTFMove(callback), page = WTFMove(page)]() mutable { |
578 | maximizeWindowForPage(*page, [callback = WTFMove(callback)]() { |
579 | callback->sendSuccess(); |
580 | }); |
581 | }); |
582 | }); |
583 | } |
584 | |
585 | void WebAutomationSession::hideWindowOfBrowsingContext(const String& browsingContextHandle, Ref<HideWindowOfBrowsingContextCallback>&& callback) |
586 | { |
587 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
588 | if (!page) |
589 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
590 | |
591 | exitFullscreenWindowForPage(*page, [protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRefPtr(page)]() mutable { |
592 | protectedThis->hideWindowForPage(*page, [callback = WTFMove(callback)]() mutable { |
593 | callback->sendSuccess(); |
594 | }); |
595 | }); |
596 | } |
597 | |
598 | void WebAutomationSession::exitFullscreenWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
599 | { |
600 | #if ENABLE(FULLSCREEN_API) |
601 | ASSERT(!m_windowStateTransitionCallback); |
602 | if (!page.fullScreenManager() || !page.fullScreenManager()->isFullScreen()) { |
603 | completionHandler(); |
604 | return; |
605 | } |
606 | |
607 | m_windowStateTransitionCallback = WTF::Function<void(WindowTransitionedToState)> { [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](WindowTransitionedToState state) mutable { |
608 | // If fullscreen exited and we didn't request that, just ignore it. |
609 | if (state != WindowTransitionedToState::Unfullscreen) |
610 | return; |
611 | |
612 | // Keep this callback in scope so completionHandler does not get destroyed before we call it. |
613 | auto protectedCallback = WTFMove(m_windowStateTransitionCallback); |
614 | completionHandler(); |
615 | } }; |
616 | |
617 | page.fullScreenManager()->requestExitFullScreen(); |
618 | #else |
619 | completionHandler(); |
620 | #endif |
621 | } |
622 | |
623 | void WebAutomationSession::restoreWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
624 | { |
625 | m_client->requestRestoreWindowOfPage(*this, page, WTFMove(completionHandler)); |
626 | } |
627 | |
628 | void WebAutomationSession::maximizeWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
629 | { |
630 | m_client->requestMaximizeWindowOfPage(*this, page, WTFMove(completionHandler)); |
631 | } |
632 | |
633 | void WebAutomationSession::hideWindowForPage(WebPageProxy& page, WTF::CompletionHandler<void()>&& completionHandler) |
634 | { |
635 | m_client->requestHideWindowOfPage(*this, page, WTFMove(completionHandler)); |
636 | } |
637 | |
638 | void WebAutomationSession::willShowJavaScriptDialog(WebPageProxy& page) |
639 | { |
640 | // Wait until the next run loop iteration to give time for the client to show the dialog, |
641 | // then check if the dialog is still present. If the page is loading, the dialog will block |
642 | // the load in case of normal strategy, so we want to dispatch all pending navigation callbacks. |
643 | // If the dialog was shown during a script execution, we want to finish the evaluateJavaScriptFunction |
644 | // operation with an unexpected alert open error. |
645 | RunLoop::main().dispatch([this, protectedThis = makeRef(*this), page = makeRef(page)] { |
646 | if (!page->hasRunningProcess() || !m_client || !m_client->isShowingJavaScriptDialogOnPage(*this, page)) |
647 | return; |
648 | |
649 | if (page->pageLoadState().isLoading()) { |
650 | m_loadTimer.stop(); |
651 | respondToPendingFrameNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame); |
652 | respondToPendingPageNavigationCallbacksWithTimeout(m_pendingNormalNavigationInBrowsingContextCallbacksPerPage); |
653 | } |
654 | |
655 | if (!m_evaluateJavaScriptFunctionCallbacks.isEmpty()) { |
656 | for (auto key : copyToVector(m_evaluateJavaScriptFunctionCallbacks.keys())) { |
657 | auto callback = m_evaluateJavaScriptFunctionCallbacks.take(key); |
658 | callback->sendSuccess("null"_s ); |
659 | } |
660 | } |
661 | |
662 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
663 | if (!m_pendingMouseEventsFlushedCallbacksPerPage.isEmpty()) { |
664 | for (auto key : copyToVector(m_pendingMouseEventsFlushedCallbacksPerPage.keys())) { |
665 | auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(key); |
666 | callback(WTF::nullopt); |
667 | } |
668 | } |
669 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
670 | |
671 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
672 | if (!m_pendingKeyboardEventsFlushedCallbacksPerPage.isEmpty()) { |
673 | for (auto key : copyToVector(m_pendingKeyboardEventsFlushedCallbacksPerPage.keys())) { |
674 | auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(key); |
675 | callback(WTF::nullopt); |
676 | } |
677 | } |
678 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
679 | }); |
680 | } |
681 | |
682 | void WebAutomationSession::didEnterFullScreenForPage(const WebPageProxy&) |
683 | { |
684 | if (m_windowStateTransitionCallback) |
685 | m_windowStateTransitionCallback(WindowTransitionedToState::Fullscreen); |
686 | } |
687 | |
688 | void WebAutomationSession::didExitFullScreenForPage(const WebPageProxy&) |
689 | { |
690 | if (m_windowStateTransitionCallback) |
691 | m_windowStateTransitionCallback(WindowTransitionedToState::Unfullscreen); |
692 | } |
693 | |
694 | void WebAutomationSession::navigateBrowsingContext(const String& handle, const String& url, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<NavigateBrowsingContextCallback>&& callback) |
695 | { |
696 | WebPageProxy* page = webPageProxyForHandle(handle); |
697 | if (!page) |
698 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
699 | |
700 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
701 | if (!pageLoadStrategy) |
702 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
703 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
704 | |
705 | page->loadRequest(URL(URL(), url)); |
706 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
707 | } |
708 | |
709 | void WebAutomationSession::goBackInBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<GoBackInBrowsingContextCallback>&& callback) |
710 | { |
711 | WebPageProxy* page = webPageProxyForHandle(handle); |
712 | if (!page) |
713 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
714 | |
715 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
716 | if (!pageLoadStrategy) |
717 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
718 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
719 | |
720 | page->goBack(); |
721 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
722 | } |
723 | |
724 | void WebAutomationSession::goForwardInBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<GoForwardInBrowsingContextCallback>&& callback) |
725 | { |
726 | WebPageProxy* page = webPageProxyForHandle(handle); |
727 | if (!page) |
728 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
729 | |
730 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
731 | if (!pageLoadStrategy) |
732 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
733 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
734 | |
735 | page->goForward(); |
736 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
737 | } |
738 | |
739 | void WebAutomationSession::reloadBrowsingContext(const String& handle, const String* optionalPageLoadStrategyString, const int* optionalPageLoadTimeout, Ref<ReloadBrowsingContextCallback>&& callback) |
740 | { |
741 | WebPageProxy* page = webPageProxyForHandle(handle); |
742 | if (!page) |
743 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
744 | |
745 | auto pageLoadStrategy = pageLoadStrategyFromStringParameter(optionalPageLoadStrategyString); |
746 | if (!pageLoadStrategy) |
747 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'pageLoadStrategy' is invalid." ); |
748 | auto pageLoadTimeout = optionalPageLoadTimeout ? Seconds::fromMilliseconds(*optionalPageLoadTimeout) : defaultPageLoadTimeout; |
749 | |
750 | page->reload({ }); |
751 | waitForNavigationToCompleteOnPage(*page, pageLoadStrategy.value(), pageLoadTimeout, WTFMove(callback)); |
752 | } |
753 | |
754 | void WebAutomationSession::navigationOccurredForFrame(const WebFrameProxy& frame) |
755 | { |
756 | if (frame.isMainFrame()) { |
757 | // New page loaded, clear frame handles previously cached. |
758 | m_handleWebFrameMap.clear(); |
759 | m_webFrameHandleMap.clear(); |
760 | if (auto callback = m_pendingNormalNavigationInBrowsingContextCallbacksPerPage.take(frame.page()->pageID())) { |
761 | m_loadTimer.stop(); |
762 | callback->sendSuccess(JSON::Object::create()); |
763 | } |
764 | m_domainNotifier->browsingContextCleared(handleForWebPageProxy(*frame.page())); |
765 | } else { |
766 | if (auto callback = m_pendingNormalNavigationInBrowsingContextCallbacksPerFrame.take(frame.frameID())) { |
767 | m_loadTimer.stop(); |
768 | callback->sendSuccess(JSON::Object::create()); |
769 | } |
770 | } |
771 | } |
772 | |
773 | void WebAutomationSession::documentLoadedForFrame(const WebFrameProxy& frame) |
774 | { |
775 | if (frame.isMainFrame()) { |
776 | if (auto callback = m_pendingEagerNavigationInBrowsingContextCallbacksPerPage.take(frame.page()->pageID())) { |
777 | m_loadTimer.stop(); |
778 | callback->sendSuccess(JSON::Object::create()); |
779 | } |
780 | } else { |
781 | if (auto callback = m_pendingEagerNavigationInBrowsingContextCallbacksPerFrame.take(frame.frameID())) { |
782 | m_loadTimer.stop(); |
783 | callback->sendSuccess(JSON::Object::create()); |
784 | } |
785 | } |
786 | } |
787 | |
788 | void WebAutomationSession::inspectorFrontendLoaded(const WebPageProxy& page) |
789 | { |
790 | if (auto callback = m_pendingInspectorCallbacksPerPage.take(page.pageID())) |
791 | callback->sendSuccess(JSON::Object::create()); |
792 | } |
793 | |
794 | void WebAutomationSession::mouseEventsFlushedForPage(const WebPageProxy& page) |
795 | { |
796 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
797 | if (auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(page.pageID())) |
798 | callback(WTF::nullopt); |
799 | #else |
800 | UNUSED_PARAM(page); |
801 | #endif |
802 | } |
803 | |
804 | void WebAutomationSession::keyboardEventsFlushedForPage(const WebPageProxy& page) |
805 | { |
806 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
807 | if (auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID())) |
808 | callback(WTF::nullopt); |
809 | #else |
810 | UNUSED_PARAM(page); |
811 | #endif |
812 | } |
813 | |
814 | void WebAutomationSession::willClosePage(const WebPageProxy& page) |
815 | { |
816 | String handle = handleForWebPageProxy(page); |
817 | m_domainNotifier->browsingContextCleared(handle); |
818 | |
819 | // Cancel pending interactions on this page. By providing an error, this will cause subsequent |
820 | // actions to be aborted and the SimulatedInputDispatcher::run() call will unwind and fail. |
821 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
822 | if (auto callback = m_pendingMouseEventsFlushedCallbacksPerPage.take(page.pageID())) |
823 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(WindowNotFound)); |
824 | #endif |
825 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
826 | if (auto callback = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID())) |
827 | callback(AUTOMATION_COMMAND_ERROR_WITH_NAME(WindowNotFound)); |
828 | #endif |
829 | |
830 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
831 | // Then tell the input dispatcher to cancel so timers are stopped, and let it go out of scope. |
832 | Optional<Ref<SimulatedInputDispatcher>> inputDispatcher = m_inputDispatchersByPage.take(page.pageID()); |
833 | if (inputDispatcher.hasValue()) |
834 | inputDispatcher.value()->cancel(); |
835 | #endif |
836 | } |
837 | |
838 | static bool fileCanBeAcceptedForUpload(const String& filename, const HashSet<String>& allowedMIMETypes, const HashSet<String>& allowedFileExtensions) |
839 | { |
840 | if (!FileSystem::fileExists(filename)) |
841 | return false; |
842 | |
843 | if (allowedMIMETypes.isEmpty() && allowedFileExtensions.isEmpty()) |
844 | return true; |
845 | |
846 | // We can't infer a MIME type from a file without an extension, just give up. |
847 | auto dotOffset = filename.reverseFind('.'); |
848 | if (dotOffset == notFound) |
849 | return false; |
850 | |
851 | String extension = filename.substring(dotOffset + 1).convertToASCIILowercase(); |
852 | if (extension.isEmpty()) |
853 | return false; |
854 | |
855 | if (allowedFileExtensions.contains(extension)) |
856 | return true; |
857 | |
858 | String mappedMIMEType = WebCore::MIMETypeRegistry::getMIMETypeForExtension(extension).convertToASCIILowercase(); |
859 | if (mappedMIMEType.isEmpty()) |
860 | return false; |
861 | |
862 | if (allowedMIMETypes.contains(mappedMIMEType)) |
863 | return true; |
864 | |
865 | // Fall back to checking for a MIME type wildcard if an exact match is not found. |
866 | Vector<String> components = mappedMIMEType.split('/'); |
867 | if (components.size() != 2) |
868 | return false; |
869 | |
870 | String wildcardedMIMEType = makeString(components[0], "/*" ); |
871 | if (allowedMIMETypes.contains(wildcardedMIMEType)) |
872 | return true; |
873 | |
874 | return false; |
875 | } |
876 | |
877 | void WebAutomationSession::handleRunOpenPanel(const WebPageProxy& page, const WebFrameProxy&, const API::OpenPanelParameters& parameters, WebOpenPanelResultListenerProxy& resultListener) |
878 | { |
879 | String browsingContextHandle = handleForWebPageProxy(page); |
880 | if (!m_filesToSelectForFileUpload.size()) { |
881 | resultListener.cancel(); |
882 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
883 | return; |
884 | } |
885 | |
886 | if (m_filesToSelectForFileUpload.size() > 1 && !parameters.allowMultipleFiles()) { |
887 | resultListener.cancel(); |
888 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
889 | return; |
890 | } |
891 | |
892 | HashSet<String> allowedMIMETypes; |
893 | auto acceptMIMETypes = parameters.acceptMIMETypes(); |
894 | for (auto type : acceptMIMETypes->elementsOfType<API::String>()) |
895 | allowedMIMETypes.add(type->string()); |
896 | |
897 | HashSet<String> allowedFileExtensions; |
898 | auto acceptFileExtensions = parameters.acceptFileExtensions(); |
899 | for (auto type : acceptFileExtensions->elementsOfType<API::String>()) { |
900 | // WebCore vends extensions with leading periods. Strip these to simplify matching later. |
901 | String extension = type->string(); |
902 | ASSERT(extension.characterAt(0) == '.'); |
903 | allowedFileExtensions.add(extension.substring(1)); |
904 | } |
905 | |
906 | // Per §14.3.10.5 in the W3C spec, if at least one file cannot be accepted, the command should fail. |
907 | // The REST API service can tell that this failed by checking the "files" attribute of the input element. |
908 | for (const String& filename : m_filesToSelectForFileUpload) { |
909 | if (!fileCanBeAcceptedForUpload(filename, allowedMIMETypes, allowedFileExtensions)) { |
910 | resultListener.cancel(); |
911 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, true); |
912 | return; |
913 | } |
914 | } |
915 | |
916 | resultListener.chooseFiles(m_filesToSelectForFileUpload); |
917 | m_domainNotifier->fileChooserDismissed(browsingContextHandle, false); |
918 | } |
919 | |
920 | void WebAutomationSession::evaluateJavaScriptFunction(const String& browsingContextHandle, const String* optionalFrameHandle, const String& function, const JSON::Array& arguments, const bool* optionalExpectsImplicitCallbackArgument, const int* optionalCallbackTimeout, Ref<EvaluateJavaScriptFunctionCallback>&& callback) |
921 | { |
922 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
923 | if (!page) |
924 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
925 | |
926 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
927 | if (!frameID) |
928 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
929 | |
930 | Vector<String> argumentsVector; |
931 | argumentsVector.reserveCapacity(arguments.length()); |
932 | |
933 | for (auto& argument : arguments) { |
934 | String argumentString; |
935 | argument->asString(argumentString); |
936 | argumentsVector.uncheckedAppend(argumentString); |
937 | } |
938 | |
939 | bool expectsImplicitCallbackArgument = optionalExpectsImplicitCallbackArgument ? *optionalExpectsImplicitCallbackArgument : false; |
940 | int callbackTimeout = optionalCallbackTimeout ? *optionalCallbackTimeout : 0; |
941 | |
942 | uint64_t callbackID = m_nextEvaluateJavaScriptCallbackID++; |
943 | m_evaluateJavaScriptFunctionCallbacks.set(callbackID, WTFMove(callback)); |
944 | |
945 | page->process().send(Messages::WebAutomationSessionProxy::EvaluateJavaScriptFunction(page->pageID(), frameID.value(), function, argumentsVector, expectsImplicitCallbackArgument, callbackTimeout, callbackID), 0); |
946 | } |
947 | |
948 | void WebAutomationSession::didEvaluateJavaScriptFunction(uint64_t callbackID, const String& result, const String& errorType) |
949 | { |
950 | auto callback = m_evaluateJavaScriptFunctionCallbacks.take(callbackID); |
951 | if (!callback) |
952 | return; |
953 | |
954 | if (!errorType.isEmpty()) |
955 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE_AND_DETAILS(errorType, result)); |
956 | else |
957 | callback->sendSuccess(result); |
958 | } |
959 | |
960 | void WebAutomationSession::resolveChildFrameHandle(const String& browsingContextHandle, const String* optionalFrameHandle, const int* optionalOrdinal, const String* optionalName, const String* optionalNodeHandle, Ref<ResolveChildFrameHandleCallback>&& callback) |
961 | { |
962 | if (!optionalOrdinal && !optionalName && !optionalNodeHandle) |
963 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "Command must specify a child frame by ordinal, name, or element handle." ); |
964 | |
965 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
966 | if (!page) |
967 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
968 | |
969 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
970 | if (!frameID) |
971 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
972 | |
973 | WTF::CompletionHandler<void(Optional<String>, uint64_t)> completionHandler = [this, protectedThis = makeRef(*this), callback = callback.copyRef()](Optional<String> errorType, uint64_t frameID) mutable { |
974 | if (errorType) { |
975 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
976 | return; |
977 | } |
978 | |
979 | callback->sendSuccess(handleForWebFrameID(frameID)); |
980 | }; |
981 | |
982 | if (optionalNodeHandle) { |
983 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithNodeHandle(page->pageID(), frameID.value(), *optionalNodeHandle), WTFMove(completionHandler)); |
984 | return; |
985 | } |
986 | |
987 | if (optionalName) { |
988 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithName(page->pageID(), frameID.value(), *optionalName), WTFMove(completionHandler)); |
989 | return; |
990 | } |
991 | |
992 | if (optionalOrdinal) { |
993 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveChildFrameWithOrdinal(page->pageID(), frameID.value(), *optionalOrdinal), WTFMove(completionHandler)); |
994 | return; |
995 | } |
996 | |
997 | ASSERT_NOT_REACHED(); |
998 | } |
999 | |
1000 | void WebAutomationSession::resolveParentFrameHandle(const String& browsingContextHandle, const String& frameHandle, Ref<ResolveParentFrameHandleCallback>&& callback) |
1001 | { |
1002 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1003 | if (!page) |
1004 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1005 | |
1006 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
1007 | if (!frameID) |
1008 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1009 | |
1010 | WTF::CompletionHandler<void(Optional<String>, uint64_t)> completionHandler = [this, protectedThis = makeRef(*this), callback = callback.copyRef()](Optional<String> errorType, uint64_t frameID) mutable { |
1011 | if (errorType) { |
1012 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
1013 | return; |
1014 | } |
1015 | |
1016 | callback->sendSuccess(handleForWebFrameID(frameID)); |
1017 | }; |
1018 | |
1019 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ResolveParentFrame(page->pageID(), frameID.value()), WTFMove(completionHandler)); |
1020 | } |
1021 | |
1022 | static Optional<CoordinateSystem> protocolStringToCoordinateSystem(const String& coordinateSystemString) |
1023 | { |
1024 | if (coordinateSystemString == "Page" ) |
1025 | return CoordinateSystem::Page; |
1026 | if (coordinateSystemString == "LayoutViewport" ) |
1027 | return CoordinateSystem::LayoutViewport; |
1028 | return WTF::nullopt; |
1029 | } |
1030 | |
1031 | void WebAutomationSession::computeElementLayout(const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, const bool* optionalScrollIntoViewIfNeeded, const String& coordinateSystemString, Ref<ComputeElementLayoutCallback>&& callback) |
1032 | { |
1033 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1034 | if (!page) |
1035 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1036 | |
1037 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
1038 | if (!frameID) |
1039 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1040 | |
1041 | Optional<CoordinateSystem> coordinateSystem = protocolStringToCoordinateSystem(coordinateSystemString); |
1042 | if (!coordinateSystem) |
1043 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'coordinateSystem' is invalid." ); |
1044 | |
1045 | WTF::CompletionHandler<void(Optional<String>, WebCore::IntRect, Optional<WebCore::IntPoint>, bool)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType, WebCore::IntRect rect, Optional<WebCore::IntPoint> inViewCenterPoint, bool isObscured) mutable { |
1046 | if (errorType) { |
1047 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
1048 | return; |
1049 | } |
1050 | |
1051 | auto originObject = Inspector::Protocol::Automation::Point::create() |
1052 | .setX(rect.x()) |
1053 | .setY(rect.y()) |
1054 | .release(); |
1055 | |
1056 | auto sizeObject = Inspector::Protocol::Automation::Size::create() |
1057 | .setWidth(rect.width()) |
1058 | .setHeight(rect.height()) |
1059 | .release(); |
1060 | |
1061 | auto rectObject = Inspector::Protocol::Automation::Rect::create() |
1062 | .setOrigin(WTFMove(originObject)) |
1063 | .setSize(WTFMove(sizeObject)) |
1064 | .release(); |
1065 | |
1066 | if (!inViewCenterPoint) { |
1067 | callback->sendSuccess(WTFMove(rectObject), nullptr, isObscured); |
1068 | return; |
1069 | } |
1070 | |
1071 | auto inViewCenterPointObject = Inspector::Protocol::Automation::Point::create() |
1072 | .setX(inViewCenterPoint.value().x()) |
1073 | .setY(inViewCenterPoint.value().y()) |
1074 | .release(); |
1075 | |
1076 | callback->sendSuccess(WTFMove(rectObject), WTFMove(inViewCenterPointObject), isObscured); |
1077 | }; |
1078 | |
1079 | bool scrollIntoViewIfNeeded = optionalScrollIntoViewIfNeeded ? *optionalScrollIntoViewIfNeeded : false; |
1080 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ComputeElementLayout(page->pageID(), frameID.value(), nodeHandle, scrollIntoViewIfNeeded, coordinateSystem.value()), WTFMove(completionHandler)); |
1081 | } |
1082 | |
1083 | void WebAutomationSession::selectOptionElement(const String& browsingContextHandle, const String& frameHandle, const String& nodeHandle, Ref<SelectOptionElementCallback>&& callback) |
1084 | { |
1085 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1086 | if (!page) |
1087 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1088 | |
1089 | Optional<uint64_t> frameID = webFrameIDForHandle(frameHandle); |
1090 | if (!frameID) |
1091 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1092 | |
1093 | WTF::CompletionHandler<void(Optional<String>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType) mutable { |
1094 | if (errorType) { |
1095 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
1096 | return; |
1097 | } |
1098 | |
1099 | callback->sendSuccess(); |
1100 | }; |
1101 | |
1102 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::SelectOptionElement(page->pageID(), frameID.value(), nodeHandle), WTFMove(completionHandler)); |
1103 | } |
1104 | |
1105 | void WebAutomationSession::isShowingJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle, bool* result) |
1106 | { |
1107 | ASSERT(m_client); |
1108 | if (!m_client) |
1109 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
1110 | |
1111 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1112 | if (!page) |
1113 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1114 | |
1115 | *result = m_client->isShowingJavaScriptDialogOnPage(*this, *page); |
1116 | } |
1117 | |
1118 | void WebAutomationSession::dismissCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle) |
1119 | { |
1120 | ASSERT(m_client); |
1121 | if (!m_client) |
1122 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
1123 | |
1124 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1125 | if (!page) |
1126 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1127 | |
1128 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
1129 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
1130 | |
1131 | m_client->dismissCurrentJavaScriptDialogOnPage(*this, *page); |
1132 | } |
1133 | |
1134 | void WebAutomationSession::acceptCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle) |
1135 | { |
1136 | ASSERT(m_client); |
1137 | if (!m_client) |
1138 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
1139 | |
1140 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1141 | if (!page) |
1142 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1143 | |
1144 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
1145 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
1146 | |
1147 | m_client->acceptCurrentJavaScriptDialogOnPage(*this, *page); |
1148 | } |
1149 | |
1150 | void WebAutomationSession::messageOfCurrentJavaScriptDialog(Inspector::ErrorString& errorString, const String& browsingContextHandle, String* text) |
1151 | { |
1152 | ASSERT(m_client); |
1153 | if (!m_client) |
1154 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
1155 | |
1156 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1157 | if (!page) |
1158 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1159 | |
1160 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
1161 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
1162 | |
1163 | *text = m_client->messageOfCurrentJavaScriptDialogOnPage(*this, *page); |
1164 | } |
1165 | |
1166 | void WebAutomationSession::setUserInputForCurrentJavaScriptPrompt(Inspector::ErrorString& errorString, const String& browsingContextHandle, const String& promptValue) |
1167 | { |
1168 | ASSERT(m_client); |
1169 | if (!m_client) |
1170 | SYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
1171 | |
1172 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1173 | if (!page) |
1174 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1175 | |
1176 | if (!m_client->isShowingJavaScriptDialogOnPage(*this, *page)) |
1177 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NoJavaScriptDialog); |
1178 | |
1179 | // §18.4 Send Alert Text. |
1180 | // https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text |
1181 | // 3. Run the substeps of the first matching current user prompt: |
1182 | auto scriptDialogType = m_client->typeOfCurrentJavaScriptDialogOnPage(*this, *page); |
1183 | ASSERT(scriptDialogType); |
1184 | switch (scriptDialogType.value()) { |
1185 | case API::AutomationSessionClient::JavaScriptDialogType::Alert: |
1186 | case API::AutomationSessionClient::JavaScriptDialogType::Confirm: |
1187 | // Return error with error code element not interactable. |
1188 | SYNC_FAIL_WITH_PREDEFINED_ERROR(ElementNotInteractable); |
1189 | case API::AutomationSessionClient::JavaScriptDialogType::Prompt: |
1190 | // Do nothing. |
1191 | break; |
1192 | case API::AutomationSessionClient::JavaScriptDialogType::BeforeUnloadConfirm: |
1193 | // Return error with error code unsupported operation. |
1194 | SYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
1195 | } |
1196 | |
1197 | m_client->setUserInputForCurrentJavaScriptPromptOnPage(*this, *page, promptValue); |
1198 | } |
1199 | |
1200 | void WebAutomationSession::setFilesToSelectForFileUpload(ErrorString& errorString, const String& browsingContextHandle, const JSON::Array& filenames, const JSON::Array* fileContents) |
1201 | { |
1202 | Vector<String> newFileList; |
1203 | newFileList.reserveInitialCapacity(filenames.length()); |
1204 | |
1205 | if (fileContents && fileContents->length() != filenames.length()) |
1206 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameters 'filenames' and 'fileContents' must have equal length." ); |
1207 | |
1208 | for (size_t i = 0; i < filenames.length(); ++i) { |
1209 | String filename; |
1210 | if (!filenames.get(i)->asString(filename)) |
1211 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameter 'filenames' contains a non-string value." ); |
1212 | |
1213 | if (!fileContents) { |
1214 | newFileList.append(filename); |
1215 | continue; |
1216 | } |
1217 | |
1218 | String fileData; |
1219 | if (!fileContents->get(i)->asString(fileData)) |
1220 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The parameter 'fileContents' contains a non-string value." ); |
1221 | |
1222 | Optional<String> localFilePath = platformGenerateLocalFilePathForRemoteFile(filename, fileData); |
1223 | if (!localFilePath) |
1224 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "The remote file could not be saved to a local temporary directory." ); |
1225 | |
1226 | newFileList.append(localFilePath.value()); |
1227 | } |
1228 | |
1229 | m_filesToSelectForFileUpload.swap(newFileList); |
1230 | } |
1231 | |
1232 | static Ref<Inspector::Protocol::Automation::Cookie> buildObjectForCookie(const WebCore::Cookie& cookie) |
1233 | { |
1234 | return Inspector::Protocol::Automation::Cookie::create() |
1235 | .setName(cookie.name) |
1236 | .setValue(cookie.value) |
1237 | .setDomain(cookie.domain) |
1238 | .setPath(cookie.path) |
1239 | .setExpires(cookie.expires ? cookie.expires / 1000 : 0) |
1240 | .setSize((cookie.name.length() + cookie.value.length())) |
1241 | .setHttpOnly(cookie.httpOnly) |
1242 | .setSecure(cookie.secure) |
1243 | .setSession(cookie.session) |
1244 | .release(); |
1245 | } |
1246 | |
1247 | static Ref<JSON::ArrayOf<Inspector::Protocol::Automation::Cookie>> buildArrayForCookies(Vector<WebCore::Cookie>& cookiesList) |
1248 | { |
1249 | auto cookies = JSON::ArrayOf<Inspector::Protocol::Automation::Cookie>::create(); |
1250 | |
1251 | for (const auto& cookie : cookiesList) |
1252 | cookies->addItem(buildObjectForCookie(cookie)); |
1253 | |
1254 | return cookies; |
1255 | } |
1256 | |
1257 | void WebAutomationSession::getAllCookies(const String& browsingContextHandle, Ref<GetAllCookiesCallback>&& callback) |
1258 | { |
1259 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1260 | if (!page) |
1261 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1262 | |
1263 | WTF::CompletionHandler<void(Optional<String>, Vector<WebCore::Cookie>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType, Vector<WebCore::Cookie> cookies) mutable { |
1264 | if (errorType) { |
1265 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
1266 | return; |
1267 | } |
1268 | |
1269 | callback->sendSuccess(buildArrayForCookies(cookies)); |
1270 | }; |
1271 | |
1272 | // Always send the main frame ID as 0 so it is resolved on the WebProcess side. This avoids a race when page->mainFrame() is null still. |
1273 | const uint64_t mainFrameID = 0; |
1274 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::GetCookiesForFrame(page->pageID(), mainFrameID), WTFMove(completionHandler)); |
1275 | } |
1276 | |
1277 | void WebAutomationSession::deleteSingleCookie(const String& browsingContextHandle, const String& cookieName, Ref<DeleteSingleCookieCallback>&& callback) |
1278 | { |
1279 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1280 | if (!page) |
1281 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1282 | |
1283 | WTF::CompletionHandler<void(Optional<String>)> completionHandler = [callback = callback.copyRef()](Optional<String> errorType) mutable { |
1284 | if (errorType) { |
1285 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(*errorType)); |
1286 | return; |
1287 | } |
1288 | |
1289 | callback->sendSuccess(); |
1290 | }; |
1291 | |
1292 | // Always send the main frame ID as 0 so it is resolved on the WebProcess side. This avoids a race when page->mainFrame() is null still. |
1293 | const uint64_t mainFrameID = 0; |
1294 | page->process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::DeleteCookie(page->pageID(), mainFrameID, cookieName), WTFMove(completionHandler)); |
1295 | } |
1296 | |
1297 | static String domainByAddingDotPrefixIfNeeded(String domain) |
1298 | { |
1299 | if (domain[0] != '.') { |
1300 | // RFC 2965: If an explicitly specified value does not start with a dot, the user agent supplies a leading dot. |
1301 | // Assume that any host that ends with a digit is trying to be an IP address. |
1302 | if (!URL::hostIsIPAddress(domain)) |
1303 | return makeString('.', domain); |
1304 | } |
1305 | |
1306 | return domain; |
1307 | } |
1308 | |
1309 | void WebAutomationSession::addSingleCookie(const String& browsingContextHandle, const JSON::Object& cookieObject, Ref<AddSingleCookieCallback>&& callback) |
1310 | { |
1311 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1312 | if (!page) |
1313 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1314 | |
1315 | URL activeURL = URL(URL(), page->pageLoadState().activeURL()); |
1316 | ASSERT(activeURL.isValid()); |
1317 | |
1318 | WebCore::Cookie cookie; |
1319 | |
1320 | if (!cookieObject.getString("name"_s , cookie.name)) |
1321 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'name' was not found." ); |
1322 | |
1323 | if (!cookieObject.getString("value"_s , cookie.value)) |
1324 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'value' was not found." ); |
1325 | |
1326 | String domain; |
1327 | if (!cookieObject.getString("domain"_s , domain)) |
1328 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'domain' was not found." ); |
1329 | |
1330 | // Inherit the domain/host from the main frame's URL if it is not explicitly set. |
1331 | if (domain.isEmpty()) |
1332 | cookie.domain = activeURL.host().toString(); |
1333 | else |
1334 | cookie.domain = domainByAddingDotPrefixIfNeeded(domain); |
1335 | |
1336 | if (!cookieObject.getString("path"_s , cookie.path)) |
1337 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'path' was not found." ); |
1338 | |
1339 | double expires; |
1340 | if (!cookieObject.getDouble("expires"_s , expires)) |
1341 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'expires' was not found." ); |
1342 | |
1343 | cookie.expires = expires * 1000.0; |
1344 | |
1345 | if (!cookieObject.getBoolean("secure"_s , cookie.secure)) |
1346 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'secure' was not found." ); |
1347 | |
1348 | if (!cookieObject.getBoolean("session"_s , cookie.session)) |
1349 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'session' was not found." ); |
1350 | |
1351 | if (!cookieObject.getBoolean("httpOnly"_s , cookie.httpOnly)) |
1352 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'httpOnly' was not found." ); |
1353 | |
1354 | WebCookieManagerProxy* cookieManager = m_processPool->supplement<WebCookieManagerProxy>(); |
1355 | cookieManager->setCookies(page->websiteDataStore().sessionID(), { cookie }, [callback = callback.copyRef()](CallbackBase::Error error) { |
1356 | if (error == CallbackBase::Error::None) |
1357 | callback->sendSuccess(); |
1358 | else |
1359 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_NAME(InternalError)); |
1360 | }); |
1361 | } |
1362 | |
1363 | void WebAutomationSession::deleteAllCookies(ErrorString& errorString, const String& browsingContextHandle) |
1364 | { |
1365 | WebPageProxy* page = webPageProxyForHandle(browsingContextHandle); |
1366 | if (!page) |
1367 | SYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1368 | |
1369 | URL activeURL = URL(URL(), page->pageLoadState().activeURL()); |
1370 | ASSERT(activeURL.isValid()); |
1371 | |
1372 | String host = activeURL.host().toString(); |
1373 | |
1374 | WebCookieManagerProxy* cookieManager = m_processPool->supplement<WebCookieManagerProxy>(); |
1375 | cookieManager->deleteCookiesForHostnames(page->websiteDataStore().sessionID(), { host, domainByAddingDotPrefixIfNeeded(host) }); |
1376 | } |
1377 | |
1378 | void WebAutomationSession::getSessionPermissions(ErrorString&, RefPtr<JSON::ArrayOf<Inspector::Protocol::Automation::SessionPermissionData>>& out_permissions) |
1379 | { |
1380 | auto permissionsObjectArray = JSON::ArrayOf<Inspector::Protocol::Automation::SessionPermissionData>::create(); |
1381 | auto getUserMediaPermissionObject = Inspector::Protocol::Automation::SessionPermissionData::create() |
1382 | .setPermission(Inspector::Protocol::Automation::SessionPermission::GetUserMedia) |
1383 | .setValue(m_permissionForGetUserMedia) |
1384 | .release(); |
1385 | |
1386 | permissionsObjectArray->addItem(WTFMove(getUserMediaPermissionObject)); |
1387 | out_permissions = WTFMove(permissionsObjectArray); |
1388 | } |
1389 | |
1390 | void WebAutomationSession::setSessionPermissions(ErrorString& errorString, const JSON::Array& permissions) |
1391 | { |
1392 | for (auto it = permissions.begin(); it != permissions.end(); ++it) { |
1393 | RefPtr<JSON::Object> permission; |
1394 | if (!it->get()->asObject(permission)) |
1395 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permissions' is invalid." ); |
1396 | |
1397 | String permissionName; |
1398 | if (!permission->getString("permission"_s , permissionName)) |
1399 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permission' is missing or invalid." ); |
1400 | |
1401 | auto parsedPermissionName = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::SessionPermission>(permissionName); |
1402 | if (!parsedPermissionName) |
1403 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'permission' has an unknown value." ); |
1404 | |
1405 | bool permissionValue; |
1406 | if (!permission->getBoolean("value"_s , permissionValue)) |
1407 | SYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'value' is missing or invalid." ); |
1408 | |
1409 | switch (parsedPermissionName.value()) { |
1410 | case Inspector::Protocol::Automation::SessionPermission::GetUserMedia: |
1411 | m_permissionForGetUserMedia = permissionValue; |
1412 | break; |
1413 | } |
1414 | } |
1415 | } |
1416 | |
1417 | bool WebAutomationSession::shouldAllowGetUserMediaForPage(const WebPageProxy&) const |
1418 | { |
1419 | return m_permissionForGetUserMedia; |
1420 | } |
1421 | |
1422 | bool WebAutomationSession::isSimulatingUserInteraction() const |
1423 | { |
1424 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1425 | if (!m_pendingMouseEventsFlushedCallbacksPerPage.isEmpty()) |
1426 | return true; |
1427 | #endif |
1428 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1429 | if (!m_pendingKeyboardEventsFlushedCallbacksPerPage.isEmpty()) |
1430 | return true; |
1431 | #endif |
1432 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
1433 | if (m_simulatingTouchInteraction) |
1434 | return true; |
1435 | #endif |
1436 | return false; |
1437 | } |
1438 | |
1439 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
1440 | SimulatedInputDispatcher& WebAutomationSession::inputDispatcherForPage(WebPageProxy& page) |
1441 | { |
1442 | return m_inputDispatchersByPage.ensure(page.pageID(), [&] { |
1443 | return SimulatedInputDispatcher::create(page, *this); |
1444 | }).iterator->value; |
1445 | } |
1446 | |
1447 | SimulatedInputSource* WebAutomationSession::inputSourceForType(SimulatedInputSourceType type) const |
1448 | { |
1449 | // FIXME: this should use something like Vector's findMatching(). |
1450 | for (auto& inputSource : m_inputSources) { |
1451 | if (inputSource->type == type) |
1452 | return &inputSource.get(); |
1453 | } |
1454 | |
1455 | return nullptr; |
1456 | } |
1457 | |
1458 | // MARK: SimulatedInputDispatcher::Client API |
1459 | void WebAutomationSession::viewportInViewCenterPointOfElement(WebPageProxy& page, uint64_t frameID, const String& nodeHandle, Function<void (Optional<WebCore::IntPoint>, Optional<AutomationCommandError>)>&& completionHandler) |
1460 | { |
1461 | WTF::CompletionHandler<void(Optional<String>, WebCore::IntRect, Optional<WebCore::IntPoint>, bool)> didComputeElementLayoutHandler = [completionHandler = WTFMove(completionHandler)](Optional<String> errorType, WebCore::IntRect, Optional<WebCore::IntPoint> inViewCenterPoint, bool) mutable { |
1462 | if (errorType) { |
1463 | completionHandler(WTF::nullopt, AUTOMATION_COMMAND_ERROR_WITH_MESSAGE(*errorType)); |
1464 | return; |
1465 | } |
1466 | |
1467 | if (!inViewCenterPoint) { |
1468 | completionHandler(WTF::nullopt, AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
1469 | return; |
1470 | } |
1471 | |
1472 | completionHandler(inViewCenterPoint, WTF::nullopt); |
1473 | }; |
1474 | |
1475 | page.process().sendWithAsyncReply(Messages::WebAutomationSessionProxy::ComputeElementLayout(page.pageID(), frameID, nodeHandle, false, CoordinateSystem::LayoutViewport), WTFMove(didComputeElementLayoutHandler)); |
1476 | } |
1477 | |
1478 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1479 | void WebAutomationSession::simulateMouseInteraction(WebPageProxy& page, MouseInteraction interaction, WebMouseEvent::Button mouseButton, const WebCore::IntPoint& locationInViewport, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
1480 | { |
1481 | page.getWindowFrameWithCallback([this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler), page = makeRef(page), interaction, mouseButton, locationInViewport](WebCore::FloatRect windowFrame) mutable { |
1482 | auto clippedX = std::min(std::max(0.0f, (float)locationInViewport.x()), windowFrame.size().width()); |
1483 | auto clippedY = std::min(std::max(0.0f, (float)locationInViewport.y()), windowFrame.size().height()); |
1484 | if (clippedX != locationInViewport.x() || clippedY != locationInViewport.y()) { |
1485 | completionHandler(AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
1486 | return; |
1487 | } |
1488 | |
1489 | // Bridge the flushed callback to our command's completion handler. |
1490 | auto mouseEventsFlushedCallback = [completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
1491 | completionHandler(error); |
1492 | }; |
1493 | |
1494 | auto& callbackInMap = m_pendingMouseEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
1495 | if (callbackInMap) |
1496 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
1497 | callbackInMap = WTFMove(mouseEventsFlushedCallback); |
1498 | |
1499 | platformSimulateMouseInteraction(page, interaction, mouseButton, locationInViewport, OptionSet<WebEvent::Modifier>::fromRaw(m_currentModifiers)); |
1500 | |
1501 | // If the event does not hit test anything in the window, then it may not have been delivered. |
1502 | if (callbackInMap && !page->isProcessingMouseEvents()) { |
1503 | auto callbackToCancel = m_pendingMouseEventsFlushedCallbacksPerPage.take(page->pageID()); |
1504 | callbackToCancel(WTF::nullopt); |
1505 | } |
1506 | |
1507 | // Otherwise, wait for mouseEventsFlushedCallback to run when all events are handled. |
1508 | }); |
1509 | } |
1510 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1511 | |
1512 | #if ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
1513 | void WebAutomationSession::simulateTouchInteraction(WebPageProxy& page, TouchInteraction interaction, const WebCore::IntPoint& locationInViewport, Optional<Seconds> duration, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
1514 | { |
1515 | #if PLATFORM(IOS_FAMILY) |
1516 | WebCore::FloatRect visualViewportBounds = WebCore::FloatRect({ }, page.unobscuredContentRect().size()); |
1517 | if (!visualViewportBounds.contains(locationInViewport)) { |
1518 | completionHandler(AUTOMATION_COMMAND_ERROR_WITH_NAME(TargetOutOfBounds)); |
1519 | return; |
1520 | } |
1521 | #endif |
1522 | |
1523 | m_simulatingTouchInteraction = true; |
1524 | platformSimulateTouchInteraction(page, interaction, locationInViewport, duration, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
1525 | m_simulatingTouchInteraction = false; |
1526 | completionHandler(error); |
1527 | }); |
1528 | } |
1529 | #endif // ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
1530 | |
1531 | #if ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1532 | void WebAutomationSession::simulateKeyboardInteraction(WebPageProxy& page, KeyboardInteraction interaction, WTF::Variant<VirtualKey, CharKey>&& key, CompletionHandler<void(Optional<AutomationCommandError>)>&& completionHandler) |
1533 | { |
1534 | // Bridge the flushed callback to our command's completion handler. |
1535 | auto keyboardEventsFlushedCallback = [completionHandler = WTFMove(completionHandler)](Optional<AutomationCommandError> error) mutable { |
1536 | completionHandler(error); |
1537 | }; |
1538 | |
1539 | auto& callbackInMap = m_pendingKeyboardEventsFlushedCallbacksPerPage.add(page.pageID(), nullptr).iterator->value; |
1540 | if (callbackInMap) |
1541 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
1542 | callbackInMap = WTFMove(keyboardEventsFlushedCallback); |
1543 | |
1544 | platformSimulateKeyboardInteraction(page, interaction, WTFMove(key)); |
1545 | |
1546 | // If the interaction does not generate any events, then do not wait for events to be flushed. |
1547 | // This happens in some corner cases on macOS, such as releasing a key while Command is pressed. |
1548 | if (callbackInMap && !page.isProcessingKeyboardEvents()) { |
1549 | auto callbackToCancel = m_pendingKeyboardEventsFlushedCallbacksPerPage.take(page.pageID()); |
1550 | callbackToCancel(WTF::nullopt); |
1551 | } |
1552 | |
1553 | // Otherwise, wait for keyboardEventsFlushedCallback to run when all events are handled. |
1554 | } |
1555 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1556 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
1557 | |
1558 | #if ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1559 | static WebEvent::Modifier protocolModifierToWebEventModifier(Inspector::Protocol::Automation::KeyModifier modifier) |
1560 | { |
1561 | switch (modifier) { |
1562 | case Inspector::Protocol::Automation::KeyModifier::Alt: |
1563 | return WebEvent::Modifier::AltKey; |
1564 | case Inspector::Protocol::Automation::KeyModifier::Meta: |
1565 | return WebEvent::Modifier::MetaKey; |
1566 | case Inspector::Protocol::Automation::KeyModifier::Control: |
1567 | return WebEvent::Modifier::ControlKey; |
1568 | case Inspector::Protocol::Automation::KeyModifier::Shift: |
1569 | return WebEvent::Modifier::ShiftKey; |
1570 | case Inspector::Protocol::Automation::KeyModifier::CapsLock: |
1571 | return WebEvent::Modifier::CapsLockKey; |
1572 | } |
1573 | |
1574 | RELEASE_ASSERT_NOT_REACHED(); |
1575 | } |
1576 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1577 | |
1578 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
1579 | static WebMouseEvent::Button protocolMouseButtonToWebMouseEventButton(Inspector::Protocol::Automation::MouseButton button) |
1580 | { |
1581 | switch (button) { |
1582 | case Inspector::Protocol::Automation::MouseButton::None: |
1583 | return WebMouseEvent::NoButton; |
1584 | case Inspector::Protocol::Automation::MouseButton::Left: |
1585 | return WebMouseEvent::LeftButton; |
1586 | case Inspector::Protocol::Automation::MouseButton::Middle: |
1587 | return WebMouseEvent::MiddleButton; |
1588 | case Inspector::Protocol::Automation::MouseButton::Right: |
1589 | return WebMouseEvent::RightButton; |
1590 | } |
1591 | |
1592 | RELEASE_ASSERT_NOT_REACHED(); |
1593 | } |
1594 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
1595 | |
1596 | void WebAutomationSession::performMouseInteraction(const String& handle, const JSON::Object& requestedPositionObject, const String& mouseButtonString, const String& mouseInteractionString, const JSON::Array& keyModifierStrings, Ref<PerformMouseInteractionCallback>&& callback) |
1597 | { |
1598 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1599 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
1600 | #else |
1601 | WebPageProxy* page = webPageProxyForHandle(handle); |
1602 | if (!page) |
1603 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1604 | |
1605 | float x; |
1606 | if (!requestedPositionObject.getDouble("x"_s , x)) |
1607 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'x' was not found." ); |
1608 | |
1609 | float y; |
1610 | if (!requestedPositionObject.getDouble("y"_s , y)) |
1611 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "The parameter 'y' was not found." ); |
1612 | |
1613 | OptionSet<WebEvent::Modifier> keyModifiers; |
1614 | for (auto it = keyModifierStrings.begin(); it != keyModifierStrings.end(); ++it) { |
1615 | String modifierString; |
1616 | if (!it->get()->asString(modifierString)) |
1617 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'modifiers' is invalid." ); |
1618 | |
1619 | auto parsedModifier = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::KeyModifier>(modifierString); |
1620 | if (!parsedModifier) |
1621 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A modifier in the 'modifiers' array is invalid." ); |
1622 | keyModifiers.add(protocolModifierToWebEventModifier(parsedModifier.value())); |
1623 | } |
1624 | |
1625 | page->getWindowFrameWithCallback([this, protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRef(*page), x, y, mouseInteractionString, mouseButtonString, keyModifiers](WebCore::FloatRect windowFrame) mutable { |
1626 | |
1627 | x = std::min(std::max(0.0f, x), windowFrame.size().width()); |
1628 | y = std::min(std::max(0.0f, y + page->topContentInset()), windowFrame.size().height()); |
1629 | |
1630 | WebCore::IntPoint positionInView = WebCore::IntPoint(static_cast<int>(x), static_cast<int>(y)); |
1631 | |
1632 | auto parsedInteraction = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseInteraction>(mouseInteractionString); |
1633 | if (!parsedInteraction) |
1634 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'interaction' is invalid." ); |
1635 | |
1636 | auto parsedButton = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseButton>(mouseButtonString); |
1637 | if (!parsedButton) |
1638 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'button' is invalid." ); |
1639 | |
1640 | auto mouseEventsFlushedCallback = [protectedThis = WTFMove(protectedThis), callback = WTFMove(callback), page = page.copyRef(), x, y](Optional<AutomationCommandError> error) { |
1641 | if (error) |
1642 | callback->sendFailure(error.value().toProtocolString()); |
1643 | else { |
1644 | callback->sendSuccess(Inspector::Protocol::Automation::Point::create() |
1645 | .setX(x) |
1646 | .setY(y - page->topContentInset()) |
1647 | .release()); |
1648 | } |
1649 | }; |
1650 | |
1651 | auto& callbackInMap = m_pendingMouseEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
1652 | if (callbackInMap) |
1653 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
1654 | callbackInMap = WTFMove(mouseEventsFlushedCallback); |
1655 | |
1656 | platformSimulateMouseInteraction(page, parsedInteraction.value(), protocolMouseButtonToWebMouseEventButton(parsedButton.value()), positionInView, keyModifiers); |
1657 | |
1658 | // If the event location was previously clipped and does not hit test anything in the window, then it will not be processed. |
1659 | // For compatibility with pre-W3C driver implementations, don't make this a hard error; just do nothing silently. |
1660 | // In W3C-only code paths, we can reject any pointer actions whose coordinates are outside the viewport rect. |
1661 | if (callbackInMap && !page->isProcessingMouseEvents()) { |
1662 | auto callbackToCancel = m_pendingMouseEventsFlushedCallbacksPerPage.take(page->pageID()); |
1663 | callbackToCancel(WTF::nullopt); |
1664 | } |
1665 | }); |
1666 | #endif // ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1667 | } |
1668 | |
1669 | void WebAutomationSession::performKeyboardInteractions(const String& handle, const JSON::Array& interactions, Ref<PerformKeyboardInteractionsCallback>&& callback) |
1670 | { |
1671 | #if !ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1672 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
1673 | #else |
1674 | WebPageProxy* page = webPageProxyForHandle(handle); |
1675 | if (!page) |
1676 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1677 | |
1678 | if (!interactions.length()) |
1679 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'interactions' was not found or empty." ); |
1680 | |
1681 | // Validate all of the parameters before performing any interactions with the browsing context under test. |
1682 | Vector<WTF::Function<void()>> actionsToPerform; |
1683 | actionsToPerform.reserveCapacity(interactions.length()); |
1684 | |
1685 | for (const auto& interaction : interactions) { |
1686 | RefPtr<JSON::Object> interactionObject; |
1687 | if (!interaction->asObject(interactionObject)) |
1688 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter was invalid." ); |
1689 | |
1690 | String interactionTypeString; |
1691 | if (!interactionObject->getString("type"_s , interactionTypeString)) |
1692 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter is missing the 'type' key." ); |
1693 | auto interactionType = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::KeyboardInteractionType>(interactionTypeString); |
1694 | if (!interactionType) |
1695 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'type' key." ); |
1696 | |
1697 | String virtualKeyString; |
1698 | bool foundVirtualKey = interactionObject->getString("key"_s , virtualKeyString); |
1699 | if (foundVirtualKey) { |
1700 | Optional<VirtualKey> virtualKey = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::VirtualKey>(virtualKeyString); |
1701 | if (!virtualKey) |
1702 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'key' value." ); |
1703 | |
1704 | actionsToPerform.uncheckedAppend([this, page, interactionType, virtualKey] { |
1705 | platformSimulateKeyboardInteraction(*page, interactionType.value(), virtualKey.value()); |
1706 | }); |
1707 | } |
1708 | |
1709 | String keySequence; |
1710 | bool foundKeySequence = interactionObject->getString("text"_s , keySequence); |
1711 | if (foundKeySequence) { |
1712 | switch (interactionType.value()) { |
1713 | case Inspector::Protocol::Automation::KeyboardInteractionType::KeyPress: |
1714 | case Inspector::Protocol::Automation::KeyboardInteractionType::KeyRelease: |
1715 | // 'KeyPress' and 'KeyRelease' are meant for a virtual key and are not supported for a string (sequence of codepoints). |
1716 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An interaction in the 'interactions' parameter has an invalid 'key' value." ); |
1717 | |
1718 | case Inspector::Protocol::Automation::KeyboardInteractionType::InsertByKey: |
1719 | actionsToPerform.uncheckedAppend([this, page, keySequence] { |
1720 | platformSimulateKeySequence(*page, keySequence); |
1721 | }); |
1722 | break; |
1723 | } |
1724 | } |
1725 | |
1726 | if (!foundVirtualKey && !foundKeySequence) |
1727 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(MissingParameter, "An interaction in the 'interactions' parameter is missing both 'key' and 'text'. One must be provided." ); |
1728 | } |
1729 | |
1730 | ASSERT(actionsToPerform.size()); |
1731 | if (!actionsToPerform.size()) |
1732 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "No actions to perform." ); |
1733 | |
1734 | auto keyboardEventsFlushedCallback = [protectedThis = makeRef(*this), callback = WTFMove(callback), page = makeRef(*page)](Optional<AutomationCommandError> error) { |
1735 | if (error) |
1736 | callback->sendFailure(error.value().toProtocolString()); |
1737 | else |
1738 | callback->sendSuccess(); |
1739 | }; |
1740 | |
1741 | auto& callbackInMap = m_pendingKeyboardEventsFlushedCallbacksPerPage.add(page->pageID(), nullptr).iterator->value; |
1742 | if (callbackInMap) |
1743 | callbackInMap(AUTOMATION_COMMAND_ERROR_WITH_NAME(Timeout)); |
1744 | callbackInMap = WTFMove(keyboardEventsFlushedCallback); |
1745 | |
1746 | for (auto& action : actionsToPerform) |
1747 | action(); |
1748 | #endif // ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1749 | } |
1750 | |
1751 | #if ENABLE(WEBDRIVER_ACTIONS_API) |
1752 | static SimulatedInputSourceType simulatedInputSourceTypeFromProtocolSourceType(Inspector::Protocol::Automation::InputSourceType protocolType) |
1753 | { |
1754 | switch (protocolType) { |
1755 | case Inspector::Protocol::Automation::InputSourceType::Null: |
1756 | return SimulatedInputSourceType::Null; |
1757 | case Inspector::Protocol::Automation::InputSourceType::Keyboard: |
1758 | return SimulatedInputSourceType::Keyboard; |
1759 | case Inspector::Protocol::Automation::InputSourceType::Mouse: |
1760 | return SimulatedInputSourceType::Mouse; |
1761 | case Inspector::Protocol::Automation::InputSourceType::Touch: |
1762 | return SimulatedInputSourceType::Touch; |
1763 | } |
1764 | |
1765 | RELEASE_ASSERT_NOT_REACHED(); |
1766 | } |
1767 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
1768 | |
1769 | void WebAutomationSession::performInteractionSequence(const String& handle, const String* optionalFrameHandle, const JSON::Array& inputSources, const JSON::Array& steps, Ref<WebAutomationSession::PerformInteractionSequenceCallback>&& callback) |
1770 | { |
1771 | // This command implements WebKit support for §17.5 Perform Actions. |
1772 | |
1773 | #if !ENABLE(WEBDRIVER_ACTIONS_API) |
1774 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
1775 | #else |
1776 | WebPageProxy* page = webPageProxyForHandle(handle); |
1777 | if (!page) |
1778 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1779 | |
1780 | auto frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
1781 | if (!frameID) |
1782 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1783 | |
1784 | HashMap<String, Ref<SimulatedInputSource>> sourceIdToInputSourceMap; |
1785 | HashMap<SimulatedInputSourceType, String, WTF::IntHash<SimulatedInputSourceType>, WTF::StrongEnumHashTraits<SimulatedInputSourceType>> typeToSourceIdMap; |
1786 | |
1787 | // Parse and validate Automation protocol arguments. By this point, the driver has |
1788 | // already performed the steps in §17.3 Processing Actions Requests. |
1789 | if (!inputSources.length()) |
1790 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'inputSources' was not found or empty." ); |
1791 | |
1792 | for (const auto& inputSource : inputSources) { |
1793 | RefPtr<JSON::Object> inputSourceObject; |
1794 | if (!inputSource->asObject(inputSourceObject)) |
1795 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter was invalid." ); |
1796 | |
1797 | String sourceId; |
1798 | if (!inputSourceObject->getString("sourceId"_s , sourceId)) |
1799 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter is missing a 'sourceId'." ); |
1800 | |
1801 | String sourceType; |
1802 | if (!inputSourceObject->getString("sourceType"_s , sourceType)) |
1803 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter is missing a 'sourceType'." ); |
1804 | |
1805 | auto parsedInputSourceType = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::InputSourceType>(sourceType); |
1806 | if (!parsedInputSourceType) |
1807 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "An input source in the 'inputSources' parameter has an invalid 'sourceType'." ); |
1808 | |
1809 | SimulatedInputSourceType inputSourceType = simulatedInputSourceTypeFromProtocolSourceType(*parsedInputSourceType); |
1810 | |
1811 | // Note: iOS does not support mouse input sources, and other platforms do not support touch input sources. |
1812 | // If a mismatch happens, alias to the supported input source. This works because both Mouse and Touch input sources |
1813 | // use a MouseButton to indicate the result of interacting (down/up/move), which can be interpreted for touch or mouse. |
1814 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) && ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
1815 | if (inputSourceType == SimulatedInputSourceType::Mouse) |
1816 | inputSourceType = SimulatedInputSourceType::Touch; |
1817 | #endif |
1818 | #if !ENABLE(WEBDRIVER_MOUSE_INTERACTIONS) |
1819 | if (inputSourceType == SimulatedInputSourceType::Mouse) |
1820 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Mouse input sources are not yet supported." ); |
1821 | #endif |
1822 | #if !ENABLE(WEBDRIVER_TOUCH_INTERACTIONS) |
1823 | if (inputSourceType == SimulatedInputSourceType::Touch) |
1824 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Touch input sources are not yet supported." ); |
1825 | #endif |
1826 | #if !ENABLE(WEBDRIVER_KEYBOARD_INTERACTIONS) |
1827 | if (inputSourceType == SimulatedInputSourceType::Keyboard) |
1828 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(NotImplemented, "Keyboard input sources are not yet supported." ); |
1829 | #endif |
1830 | if (typeToSourceIdMap.contains(inputSourceType)) |
1831 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Two input sources with the same type were specified." ); |
1832 | if (sourceIdToInputSourceMap.contains(sourceId)) |
1833 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Two input sources with the same sourceId were specified." ); |
1834 | |
1835 | typeToSourceIdMap.add(inputSourceType, sourceId); |
1836 | sourceIdToInputSourceMap.add(sourceId, *inputSourceForType(inputSourceType)); |
1837 | } |
1838 | |
1839 | Vector<SimulatedInputKeyFrame> keyFrames; |
1840 | |
1841 | if (!steps.length()) |
1842 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "The parameter 'steps' was not found or empty." ); |
1843 | |
1844 | for (const auto& step : steps) { |
1845 | RefPtr<JSON::Object> stepObject; |
1846 | if (!step->asObject(stepObject)) |
1847 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A step in the 'steps' parameter was not an object." ); |
1848 | |
1849 | RefPtr<JSON::Array> stepStates; |
1850 | if (!stepObject->getArray("states"_s , stepStates)) |
1851 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "A step is missing the 'states' property." ); |
1852 | |
1853 | Vector<SimulatedInputKeyFrame::StateEntry> entries; |
1854 | entries.reserveCapacity(stepStates->length()); |
1855 | |
1856 | for (const auto& state : *stepStates) { |
1857 | RefPtr<JSON::Object> stateObject; |
1858 | if (!state->asObject(stateObject)) |
1859 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered a non-object step state." ); |
1860 | |
1861 | String sourceId; |
1862 | if (!stateObject->getString("sourceId"_s , sourceId)) |
1863 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Step state lacks required 'sourceId' property." ); |
1864 | |
1865 | if (!sourceIdToInputSourceMap.contains(sourceId)) |
1866 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Unknown 'sourceId' specified." ); |
1867 | |
1868 | SimulatedInputSource& inputSource = *sourceIdToInputSourceMap.get(sourceId); |
1869 | SimulatedInputSourceState sourceState { }; |
1870 | |
1871 | String pressedCharKeyString; |
1872 | if (stateObject->getString("pressedCharKey"_s , pressedCharKeyString)) |
1873 | sourceState.pressedCharKey = pressedCharKeyString.characterAt(0); |
1874 | |
1875 | RefPtr<JSON::Array> pressedVirtualKeysArray; |
1876 | if (stateObject->getArray("pressedVirtualKeys"_s , pressedVirtualKeysArray)) { |
1877 | VirtualKeySet pressedVirtualKeys { }; |
1878 | |
1879 | for (auto it = pressedVirtualKeysArray->begin(); it != pressedVirtualKeysArray->end(); ++it) { |
1880 | String pressedVirtualKeyString; |
1881 | if (!(*it)->asString(pressedVirtualKeyString)) |
1882 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered a non-string virtual key value." ); |
1883 | |
1884 | Optional<VirtualKey> parsedVirtualKey = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::VirtualKey>(pressedVirtualKeyString); |
1885 | if (!parsedVirtualKey) |
1886 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Encountered an unknown virtual key value." ); |
1887 | else |
1888 | pressedVirtualKeys.add(parsedVirtualKey.value()); |
1889 | } |
1890 | |
1891 | sourceState.pressedVirtualKeys = pressedVirtualKeys; |
1892 | } |
1893 | |
1894 | String pressedButtonString; |
1895 | if (stateObject->getString("pressedButton"_s , pressedButtonString)) { |
1896 | auto protocolButton = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseButton>(pressedButtonString); |
1897 | sourceState.pressedMouseButton = protocolMouseButtonToWebMouseEventButton(protocolButton.valueOr(Inspector::Protocol::Automation::MouseButton::None)); |
1898 | } |
1899 | |
1900 | String originString; |
1901 | if (stateObject->getString("origin"_s , originString)) |
1902 | sourceState.origin = Inspector::Protocol::AutomationHelpers::parseEnumValueFromString<Inspector::Protocol::Automation::MouseMoveOrigin>(originString); |
1903 | |
1904 | if (sourceState.origin && sourceState.origin.value() == Inspector::Protocol::Automation::MouseMoveOrigin::Element) { |
1905 | String nodeHandleString; |
1906 | if (!stateObject->getString("nodeHandle"_s , nodeHandleString)) |
1907 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InvalidParameter, "Node handle not provided for 'Element' origin" ); |
1908 | sourceState.nodeHandle = nodeHandleString; |
1909 | } |
1910 | |
1911 | RefPtr<JSON::Object> locationObject; |
1912 | if (stateObject->getObject("location"_s , locationObject)) { |
1913 | int x, y; |
1914 | if (locationObject->getInteger("x"_s , x) && locationObject->getInteger("y"_s , y)) |
1915 | sourceState.location = WebCore::IntPoint(x, y); |
1916 | } |
1917 | |
1918 | int parsedDuration; |
1919 | if (stateObject->getInteger("duration"_s , parsedDuration)) |
1920 | sourceState.duration = Seconds::fromMilliseconds(parsedDuration); |
1921 | |
1922 | entries.uncheckedAppend(std::pair<SimulatedInputSource&, SimulatedInputSourceState> { inputSource, sourceState }); |
1923 | } |
1924 | |
1925 | keyFrames.append(SimulatedInputKeyFrame(WTFMove(entries))); |
1926 | } |
1927 | |
1928 | SimulatedInputDispatcher& inputDispatcher = inputDispatcherForPage(*page); |
1929 | if (inputDispatcher.isActive()) { |
1930 | ASSERT_NOT_REACHED(); |
1931 | ASYNC_FAIL_WITH_PREDEFINED_ERROR_AND_DETAILS(InternalError, "A previous interaction is still underway." ); |
1932 | } |
1933 | |
1934 | // Delegate the rest of §17.4 Dispatching Actions to the dispatcher. |
1935 | inputDispatcher.run(frameID.value(), WTFMove(keyFrames), m_inputSources, [protectedThis = makeRef(*this), callback = WTFMove(callback)](Optional<AutomationCommandError> error) { |
1936 | if (error) |
1937 | callback->sendFailure(error.value().toProtocolString()); |
1938 | else |
1939 | callback->sendSuccess(); |
1940 | }); |
1941 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
1942 | } |
1943 | |
1944 | void WebAutomationSession::cancelInteractionSequence(const String& handle, const String* optionalFrameHandle, Ref<CancelInteractionSequenceCallback>&& callback) |
1945 | { |
1946 | // This command implements WebKit support for §17.6 Release Actions. |
1947 | |
1948 | #if !ENABLE(WEBDRIVER_ACTIONS_API) |
1949 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(NotImplemented); |
1950 | #else |
1951 | WebPageProxy* page = webPageProxyForHandle(handle); |
1952 | if (!page) |
1953 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1954 | |
1955 | auto frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
1956 | if (!frameID) |
1957 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1958 | |
1959 | Vector<SimulatedInputKeyFrame> keyFrames({ SimulatedInputKeyFrame::keyFrameToResetInputSources(m_inputSources) }); |
1960 | SimulatedInputDispatcher& inputDispatcher = inputDispatcherForPage(*page); |
1961 | inputDispatcher.cancel(); |
1962 | |
1963 | inputDispatcher.run(frameID.value(), WTFMove(keyFrames), m_inputSources, [protectedThis = makeRef(*this), callback = WTFMove(callback)](Optional<AutomationCommandError> error) { |
1964 | if (error) |
1965 | callback->sendFailure(error.value().toProtocolString()); |
1966 | else |
1967 | callback->sendSuccess(); |
1968 | }); |
1969 | #endif // ENABLE(WEBDRIVER_ACTIONS_API) |
1970 | } |
1971 | |
1972 | void WebAutomationSession::takeScreenshot(const String& handle, const String* optionalFrameHandle, const String* optionalNodeHandle, const bool* optionalScrollIntoViewIfNeeded, const bool* optionalClipToViewport, Ref<TakeScreenshotCallback>&& callback) |
1973 | { |
1974 | WebPageProxy* page = webPageProxyForHandle(handle); |
1975 | if (!page) |
1976 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(WindowNotFound); |
1977 | |
1978 | Optional<uint64_t> frameID = webFrameIDForHandle(optionalFrameHandle ? *optionalFrameHandle : emptyString()); |
1979 | if (!frameID) |
1980 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(FrameNotFound); |
1981 | |
1982 | bool scrollIntoViewIfNeeded = optionalScrollIntoViewIfNeeded ? *optionalScrollIntoViewIfNeeded : false; |
1983 | String nodeHandle = optionalNodeHandle ? *optionalNodeHandle : emptyString(); |
1984 | bool clipToViewport = optionalClipToViewport ? *optionalClipToViewport : false; |
1985 | |
1986 | uint64_t callbackID = m_nextScreenshotCallbackID++; |
1987 | m_screenshotCallbacks.set(callbackID, WTFMove(callback)); |
1988 | |
1989 | page->process().send(Messages::WebAutomationSessionProxy::TakeScreenshot(page->pageID(), frameID.value(), nodeHandle, scrollIntoViewIfNeeded, clipToViewport, callbackID), 0); |
1990 | } |
1991 | |
1992 | void WebAutomationSession::didTakeScreenshot(uint64_t callbackID, const ShareableBitmap::Handle& imageDataHandle, const String& errorType) |
1993 | { |
1994 | auto callback = m_screenshotCallbacks.take(callbackID); |
1995 | if (!callback) |
1996 | return; |
1997 | |
1998 | if (!errorType.isEmpty()) { |
1999 | callback->sendFailure(STRING_FOR_PREDEFINED_ERROR_MESSAGE(errorType)); |
2000 | return; |
2001 | } |
2002 | |
2003 | Optional<String> base64EncodedData = platformGetBase64EncodedPNGData(imageDataHandle); |
2004 | if (!base64EncodedData) |
2005 | ASYNC_FAIL_WITH_PREDEFINED_ERROR(InternalError); |
2006 | |
2007 | callback->sendSuccess(base64EncodedData.value()); |
2008 | } |
2009 | |
2010 | #if !PLATFORM(COCOA) && !USE(CAIRO) |
2011 | Optional<String> WebAutomationSession::platformGetBase64EncodedPNGData(const ShareableBitmap::Handle&) |
2012 | { |
2013 | return WTF::nullopt; |
2014 | } |
2015 | #endif // !PLATFORM(COCOA) && !USE(CAIRO) |
2016 | |
2017 | #if !PLATFORM(COCOA) |
2018 | Optional<String> WebAutomationSession::platformGenerateLocalFilePathForRemoteFile(const String&, const String&) |
2019 | { |
2020 | return WTF::nullopt; |
2021 | } |
2022 | #endif // !PLATFORM(COCOA) |
2023 | |
2024 | } // namespace WebKit |
2025 | |