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