| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/test/chromedriver/session_commands.h" |
| |
| #include <list> |
| #include <memory> |
| #include <thread> |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_reader.h" |
| #include "base/logging.h" // For CHECK macros. |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/test/chromedriver/basic_types.h" |
| #include "chrome/test/chromedriver/bidimapper/bidimapper.h" |
| #include "chrome/test/chromedriver/capabilities.h" |
| #include "chrome/test/chromedriver/chrome/bidi_tracker.h" |
| #include "chrome/test/chromedriver/chrome/browser_info.h" |
| #include "chrome/test/chromedriver/chrome/chrome.h" |
| #include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h" |
| #include "chrome/test/chromedriver/chrome/chrome_impl.h" |
| #include "chrome/test/chromedriver/chrome/device_manager.h" |
| #include "chrome/test/chromedriver/chrome/devtools_client_impl.h" |
| #include "chrome/test/chromedriver/chrome/devtools_event_listener.h" |
| #include "chrome/test/chromedriver/chrome/geoposition.h" |
| #include "chrome/test/chromedriver/chrome/javascript_dialog_manager.h" |
| #include "chrome/test/chromedriver/chrome/status.h" |
| #include "chrome/test/chromedriver/chrome/web_view.h" |
| #include "chrome/test/chromedriver/chrome_launcher.h" |
| #include "chrome/test/chromedriver/command_listener.h" |
| #include "chrome/test/chromedriver/constants/version.h" |
| #include "chrome/test/chromedriver/logging.h" |
| #include "chrome/test/chromedriver/session.h" |
| #include "chrome/test/chromedriver/util.h" |
| #include "services/network/public/mojom/url_loader_factory.mojom.h" |
| |
| namespace { |
| |
| const int kWifiMask = 0x2; |
| const int k4GMask = 0x8; |
| const int k3GMask = 0x10; |
| const int k2GMask = 0x20; |
| |
| const int kAirplaneModeLatency = 0; |
| const int kAirplaneModeThroughput = 0; |
| const int kWifiLatency = 2; |
| const int kWifiThroughput = 30720 * 1024; |
| const int k4GLatency = 20; |
| const int k4GThroughput = 4096 * 1024; |
| const int k3GLatency = 100; |
| const int k3GThroughput = 750 * 1024; |
| const int k2GLatency = 300; |
| const int k2GThroughput = 250 * 1024; |
| |
| Status EvaluateScriptAndIgnoreResult(Session* session, |
| std::string expression, |
| const bool await_promise = false) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| if (!web_view->IsServiceWorker() && |
| web_view->GetJavaScriptDialogManager()->IsDialogOpen()) { |
| std::string alert_text; |
| status = |
| web_view->GetJavaScriptDialogManager()->GetDialogMessage(&alert_text); |
| if (status.IsError()) |
| return Status(kUnexpectedAlertOpen); |
| return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}"); |
| } |
| std::string frame_id = session->GetCurrentFrameId(); |
| std::unique_ptr<base::Value> result; |
| return web_view->EvaluateScript(frame_id, expression, await_promise, &result); |
| } |
| |
| void InitSessionForWebSocketConnection(SessionConnectionMap* session_map, |
| std::string session_id) { |
| session_map->insert({session_id, std::vector<int>{}}); |
| } |
| |
| } // namespace |
| |
| InitSessionParams::InitSessionParams( |
| network::mojom::URLLoaderFactory* factory, |
| const SyncWebSocketFactory& socket_factory, |
| DeviceManager* device_manager, |
| const scoped_refptr<base::SingleThreadTaskRunner> cmd_task_runner, |
| SessionConnectionMap* session_map) |
| : url_loader_factory(factory), |
| socket_factory(socket_factory), |
| device_manager(device_manager), |
| cmd_task_runner(cmd_task_runner), |
| session_map(session_map) {} |
| |
| InitSessionParams::InitSessionParams(const InitSessionParams& other) = default; |
| |
| InitSessionParams::~InitSessionParams() = default; |
| |
| // Look for W3C mode setting in InitSession command parameters. |
| bool GetW3CSetting(const base::Value::Dict& params) { |
| const base::Value::Dict* options_dict = nullptr; |
| |
| const base::Value::Dict* caps_dict = |
| params.FindDictByDottedPath("capabilities.alwaysMatch"); |
| if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) { |
| absl::optional<bool> w3c = options_dict->FindBool("w3c"); |
| if (w3c.has_value()) |
| return *w3c; |
| } |
| |
| const base::Value::List* list = |
| params.FindListByDottedPath("capabilities.firstMatch"); |
| if (list && list->size()) { |
| const base::Value& caps_dict_ref = (*list)[0]; |
| if (caps_dict_ref.is_dict() && |
| GetChromeOptionsDictionary(caps_dict_ref.GetDict(), &options_dict)) { |
| absl::optional<bool> w3c = options_dict->FindBool("w3c"); |
| if (w3c.has_value()) |
| return *w3c; |
| } |
| } |
| |
| caps_dict = params.FindDict("desiredCapabilities"); |
| if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) { |
| absl::optional<bool> w3c = options_dict->FindBool("w3c"); |
| if (w3c.has_value()) |
| return *w3c; |
| } |
| |
| if (!params.contains("capabilities") && |
| params.contains("desiredCapabilities")) { |
| return false; |
| } |
| |
| return kW3CDefault; |
| } |
| |
| namespace { |
| |
| std::string PlatformNameToW3C(const std::string& platform_name) { |
| std::string result = base::ToLowerASCII(platform_name); |
| if (base::StartsWith(result, "mac")) { |
| result = "mac"; |
| } |
| return result; |
| } |
| |
| // Creates a JSON object (represented by base::Value::Dict) that contains |
| // the capabilities, for returning to the client app as the result of New |
| // Session command. |
| base::Value::Dict CreateCapabilities(Session* session, |
| const Capabilities& capabilities, |
| const base::Value::Dict& desired_caps) { |
| base::Value::Dict caps; |
| |
| // Capabilities defined by W3C. Some of these capabilities have different |
| // names in legacy mode. |
| caps.Set("browserName", base::ToLowerASCII(kBrowserShortName)); |
| caps.Set(session->w3c_compliant ? "browserVersion" : "version", |
| session->chrome->GetBrowserInfo()->browser_version); |
| std::string os_name = session->chrome->GetOperatingSystemName(); |
| if (os_name.find("Windows") != std::string::npos) |
| os_name = "Windows"; |
| if (session->w3c_compliant) { |
| caps.Set("platformName", PlatformNameToW3C(os_name)); |
| } else { |
| caps.Set("platform", os_name); |
| } |
| caps.Set("pageLoadStrategy", session->chrome->page_load_strategy()); |
| caps.Set("acceptInsecureCerts", capabilities.accept_insecure_certs); |
| |
| const base::Value* proxy = desired_caps.Find("proxy"); |
| if (proxy == nullptr || proxy->is_none()) |
| caps.Set("proxy", base::Value::Dict()); |
| else |
| caps.Set("proxy", proxy->Clone()); |
| |
| // add setWindowRect based on whether we are desktop/android/remote |
| if (capabilities.IsAndroid() || capabilities.IsRemoteBrowser()) { |
| caps.Set("setWindowRect", false); |
| } else { |
| caps.Set("setWindowRect", true); |
| } |
| if (session->script_timeout == base::TimeDelta::Max()) { |
| caps.SetByDottedPath("timeouts.script", base::Value()); |
| } else { |
| SetSafeInt(caps, "timeouts.script", |
| session->script_timeout.InMilliseconds()); |
| } |
| SetSafeInt(caps, "timeouts.pageLoad", |
| session->page_load_timeout.InMilliseconds()); |
| SetSafeInt(caps, "timeouts.implicit", |
| session->implicit_wait.InMilliseconds()); |
| caps.Set("strictFileInteractability", session->strict_file_interactability); |
| caps.Set(session->w3c_compliant ? "unhandledPromptBehavior" |
| : "unexpectedAlertBehaviour", |
| session->unhandled_prompt_behavior); |
| |
| // Extensions defined by the W3C. |
| // See https://w3c.github.io/webauthn/#sctn-automation-webdriver-capability |
| caps.Set("webauthn:virtualAuthenticators", !capabilities.IsAndroid()); |
| caps.Set("webauthn:extension:largeBlob", !capabilities.IsAndroid()); |
| caps.Set("webauthn:extension:minPinLength", !capabilities.IsAndroid()); |
| caps.Set("webauthn:extension:credBlob", !capabilities.IsAndroid()); |
| caps.Set("webauthn:extension:prf", !capabilities.IsAndroid()); |
| |
| // See https://github.com/fedidcg/FedCM/pull/478 |
| caps.Set("fedcm:accounts", true); |
| |
| // Chrome-specific extensions. |
| const std::string chrome_driver_version_key = base::StringPrintf( |
| "%s.%sVersion", base::ToLowerASCII(kBrowserShortName).c_str(), |
| base::ToLowerASCII(kChromeDriverProductShortName).c_str()); |
| caps.SetByDottedPath(chrome_driver_version_key, kChromeDriverVersion); |
| if (session->chrome->GetBrowserInfo()->debugger_endpoint.IsValid()) { |
| const std::string debugger_address_key = base::StringPrintf( |
| "%s.debuggerAddress", kChromeDriverOptionsKeyPrefixed); |
| caps.SetByDottedPath(debugger_address_key, session->chrome->GetBrowserInfo() |
| ->debugger_endpoint.Address() |
| .ToString()); |
| } |
| ChromeDesktopImpl* desktop = nullptr; |
| Status status = session->chrome->GetAsDesktop(&desktop); |
| if (status.IsOk()) { |
| const std::string user_data_key = base::StringPrintf( |
| "%s.userDataDir", base::ToLowerASCII(kBrowserShortName).c_str()); |
| caps.SetByDottedPath( |
| user_data_key, |
| desktop->command().GetSwitchValuePath("user-data-dir").AsUTF8Unsafe()); |
| caps.Set("networkConnectionEnabled", desktop->IsNetworkConnectionEnabled()); |
| } |
| |
| // Legacy capabilities. |
| if (!session->w3c_compliant) { |
| caps.Set("javascriptEnabled", true); |
| caps.Set("takesScreenshot", true); |
| caps.Set("takesHeapSnapshot", true); |
| caps.Set("handlesAlerts", true); |
| caps.Set("databaseEnabled", false); |
| caps.Set("locationContextEnabled", true); |
| caps.Set("mobileEmulationEnabled", |
| session->chrome->IsMobileEmulationEnabled()); |
| caps.Set("browserConnectionEnabled", false); |
| caps.Set("cssSelectorsEnabled", true); |
| caps.Set("webStorageEnabled", true); |
| caps.Set("rotatable", false); |
| caps.Set("acceptSslCerts", capabilities.accept_insecure_certs); |
| caps.Set("nativeEvents", true); |
| caps.Set("hasTouchScreen", session->chrome->HasTouchScreen()); |
| } |
| |
| if (session->webSocketUrl) { |
| caps.Set("webSocketUrl", |
| "ws://" + session->host + "/session/" + session->id); |
| } |
| |
| return caps; |
| } |
| |
| Status InitSessionHelper(const InitSessionParams& bound_params, |
| Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| if (!bound_params.device_manager) { |
| return Status{kSessionNotCreated, "device manager cannot be null"}; |
| } |
| |
| const base::Value::Dict* desired_caps; |
| base::Value::Dict merged_caps; |
| |
| Capabilities capabilities; |
| Status status = internal::ConfigureSession(session, params, desired_caps, |
| merged_caps, &capabilities); |
| if (status.IsError()) |
| return status; |
| |
| // Create Log's and DevToolsEventListener's for ones that are DevTools-based. |
| // Session will own the Log's, Chrome will own the listeners. |
| // Also create |CommandListener|s for the appropriate logs. |
| std::vector<std::unique_ptr<DevToolsEventListener>> devtools_event_listeners; |
| std::vector<std::unique_ptr<CommandListener>> command_listeners; |
| status = CreateLogs(capabilities, |
| session, |
| &session->devtools_logs, |
| &devtools_event_listeners, |
| &command_listeners); |
| if (status.IsError()) |
| return status; |
| |
| // |session| will own the |CommandListener|s. |
| session->command_listeners.swap(command_listeners); |
| |
| if (session->webSocketUrl) { |
| // Suffixes used with the client channels. |
| std::string client_suffixes[] = {Session::kChannelSuffix, |
| Session::kNoChannelSuffix, |
| Session::kBlockingChannelSuffix}; |
| for (std::string suffix : client_suffixes) { |
| BidiTracker* bidi_tracker = new BidiTracker(); |
| bidi_tracker->SetChannelSuffix(std::move(suffix)); |
| bidi_tracker->SetBidiCallback(base::BindRepeating( |
| &Session::OnBidiResponse, base::Unretained(session))); |
| devtools_event_listeners.emplace_back(bidi_tracker); |
| } |
| } |
| |
| status = |
| LaunchChrome(bound_params.url_loader_factory, bound_params.socket_factory, |
| *bound_params.device_manager, capabilities, |
| std::move(devtools_event_listeners), session->w3c_compliant, |
| session->chrome); |
| |
| if (status.IsError()) |
| return status; |
| |
| if (capabilities.accept_insecure_certs) { |
| status = session->chrome->SetAcceptInsecureCerts(); |
| if (status.IsError()) |
| return status; |
| } |
| |
| status = session->chrome->GetWebViewIdForFirstTab(&session->window, |
| session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| session->detach = capabilities.detach; |
| session->capabilities = std::make_unique<base::Value::Dict>( |
| CreateCapabilities(session, capabilities, *desired_caps)); |
| |
| status = internal::ConfigureHeadlessSession(session, capabilities); |
| if (status.IsError()) |
| return status; |
| |
| if (session->w3c_compliant) { |
| base::Value::Dict body; |
| body.Set("capabilities", session->capabilities->Clone()); |
| body.Set("sessionId", session->id); |
| *value = std::make_unique<base::Value>(body.Clone()); |
| } else { |
| *value = std::make_unique<base::Value>(session->capabilities->Clone()); |
| } |
| |
| if (session->webSocketUrl) { |
| WebView* web_view = nullptr; |
| status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| session->bidi_mapper_web_view_id = session->window; |
| |
| // Wait until the default page navigation is over to prevent the mapper |
| // from begin evicted by the navigation. |
| status = web_view->WaitForPendingNavigations( |
| session->GetCurrentFrameId(), Timeout(session->page_load_timeout), |
| true); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); |
| base::FilePath bidi_mapper_path = |
| cmd_line->GetSwitchValuePath("bidi-mapper-path"); |
| |
| std::string mapper_script = kMapperScript; |
| |
| if (!bidi_mapper_path.empty()) { |
| VLOG(0) << "Custom BiDi mapper path specified: " << bidi_mapper_path; |
| |
| if (!base::ReadFileToString(bidi_mapper_path, &mapper_script)) { |
| return Status(StatusCode::kUnknownError, |
| "Failed to read the specified BiDi mapper path: " + |
| bidi_mapper_path.AsUTF8Unsafe()); |
| } |
| } |
| |
| status = web_view->StartBidiServer(mapper_script); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| { |
| // Create a new tab because the default one is occupied by the BiDiMapper |
| std::string web_view_id; |
| status = session->chrome->NewWindow( |
| session->window, Chrome::WindowType::kTab, &web_view_id); |
| |
| if (status.IsError()) |
| return status; |
| |
| std::unique_ptr<base::Value> result; |
| base::Value::Dict body; |
| body.Set("handle", web_view_id); |
| |
| status = ExecuteSwitchToWindow(session, body, &result); |
| } |
| } |
| |
| return status; |
| } |
| |
| } // namespace |
| |
| namespace internal { |
| |
| Status ConfigureSession(Session* session, |
| const base::Value::Dict& params, |
| const base::Value::Dict*& desired_caps, |
| base::Value::Dict& merged_caps, |
| Capabilities* capabilities) { |
| session->driver_log = |
| std::make_unique<WebDriverLog>(WebDriverLog::kDriverType, Log::kAll); |
| |
| session->w3c_compliant = GetW3CSetting(params); |
| if (session->w3c_compliant) { |
| Status status = ProcessCapabilities(params, merged_caps); |
| if (status.IsError()) |
| return status; |
| desired_caps = &merged_caps; |
| } else { |
| const base::Value::Dict* caps = params.FindDict("desiredCapabilities"); |
| if (!caps) |
| return Status(kSessionNotCreated, "Missing or invalid capabilities"); |
| desired_caps = caps; |
| } |
| |
| Status status = capabilities->Parse(*desired_caps, session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| |
| if (capabilities->unhandled_prompt_behavior.length() > 0) { |
| session->unhandled_prompt_behavior = |
| capabilities->unhandled_prompt_behavior; |
| } else { |
| // W3C spec (https://www.w3.org/TR/webdriver/#dfn-handle-any-user-prompts) |
| // shows the default behavior to be dismiss and notify. For backward |
| // compatibility, in legacy mode default behavior is not handling prompt. |
| session->unhandled_prompt_behavior = |
| session->w3c_compliant ? ::prompt_behavior::kDismissAndNotify |
| : ::prompt_behavior::kIgnore; |
| } |
| |
| session->implicit_wait = capabilities->implicit_wait_timeout; |
| session->page_load_timeout = capabilities->page_load_timeout; |
| session->script_timeout = capabilities->script_timeout; |
| session->strict_file_interactability = |
| capabilities->strict_file_interactability; |
| session->webSocketUrl = capabilities->webSocketUrl; |
| Log::Level driver_level = Log::kWarning; |
| if (capabilities->logging_prefs.count(WebDriverLog::kDriverType)) |
| driver_level = capabilities->logging_prefs[WebDriverLog::kDriverType]; |
| session->driver_log->set_min_level(driver_level); |
| |
| return Status(kOk); |
| } |
| |
| Status ConfigureHeadlessSession(Session* session, |
| const Capabilities& capabilities) { |
| if (!session->chrome->GetBrowserInfo()->is_headless_shell) { |
| return Status(kOk); |
| } |
| |
| const std::string* download_directory = nullptr; |
| if (capabilities.prefs) { |
| download_directory = capabilities.prefs->FindStringByDottedPath( |
| "download.default_directory"); |
| if (!download_directory) { |
| download_directory = |
| capabilities.prefs->FindString("download.default_directory"); |
| } |
| } |
| session->headless_download_directory = std::make_unique<std::string>( |
| download_directory ? *download_directory : "."); |
| |
| WebView* first_view; |
| session->chrome->GetWebViewById(session->window, &first_view); |
| return first_view->OverrideDownloadDirectoryIfNeeded( |
| *session->headless_download_directory); |
| } |
| |
| } // namespace internal |
| |
| bool MergeCapabilities(const base::Value::Dict& always_match, |
| const base::Value::Dict& first_match, |
| base::Value::Dict& merged) { |
| merged.clear(); |
| |
| for (auto kv : first_match) { |
| if (always_match.Find(kv.first)) { |
| // `first_match` cannot have the same `keys` as `always_match`. |
| return false; |
| } |
| } |
| |
| // merge the capabilities together since guaranteed no key collisions |
| merged = always_match.Clone(); |
| merged.Merge(first_match.Clone()); |
| return true; |
| } |
| |
| // Implementation of "matching capabilities", as defined in W3C spec at |
| // https://www.w3.org/TR/webdriver/#dfn-matching-capabilities. |
| // It checks some requested capabilities and make sure they are supported. |
| // Currently, we only check "browserName", "platformName", "fedcm:accounts" |
| // and webauthn capabilities but more can be added as necessary. |
| bool MatchCapabilities(const base::Value::Dict& capabilities) { |
| const base::Value* name = capabilities.Find("browserName"); |
| if (name && !name->is_none()) { |
| if (!(name->is_string() && name->GetString() == kBrowserCapabilityName)) |
| return false; |
| } |
| |
| const base::Value::Dict* chrome_options; |
| const bool has_chrome_options = |
| GetChromeOptionsDictionary(capabilities, &chrome_options); |
| |
| bool is_android = has_chrome_options && |
| chrome_options->FindString("androidPackage") != nullptr; |
| |
| const base::Value* platform_name_value = capabilities.Find("platformName"); |
| if (platform_name_value && !platform_name_value->is_none()) { |
| if (!platform_name_value->is_string()) |
| return false; |
| |
| std::string requested_platform_name = platform_name_value->GetString(); |
| std::string requested_first_token = |
| requested_platform_name.substr(0, requested_platform_name.find(' ')); |
| |
| std::string actual_platform_name = |
| base::ToLowerASCII(base::SysInfo::OperatingSystemName()); |
| std::string actual_first_token = |
| actual_platform_name.substr(0, actual_platform_name.find(' ')); |
| |
| bool is_remote = has_chrome_options && |
| chrome_options->FindString("debuggerAddress") != nullptr; |
| if (requested_platform_name == "any" || is_remote || |
| (is_android && requested_platform_name == "android")) { |
| // "any" can be used as a wild card for platformName. |
| // if |is_remote| there is no easy way to know |
| // target platform. Android check also occurs here. |
| // If any of the above cases pass, we return true. |
| } else if (is_android && requested_platform_name != "android") { |
| return false; |
| } else if (requested_first_token == "mac" || |
| requested_first_token == "windows" || |
| requested_first_token == "linux") { |
| if (actual_first_token != requested_first_token) |
| return false; |
| } else if (requested_platform_name != actual_platform_name) { |
| return false; |
| } |
| } |
| |
| const base::Value* virtual_authenticators_value = |
| capabilities.Find("webauthn:virtualAuthenticators"); |
| if (virtual_authenticators_value) { |
| if (!virtual_authenticators_value->is_bool() || |
| (virtual_authenticators_value->GetBool() && is_android)) { |
| return false; |
| } |
| } |
| |
| const base::Value* large_blob_value = |
| capabilities.Find("webauthn:extension:largeBlob"); |
| if (large_blob_value) { |
| if (!large_blob_value->is_bool() || |
| (large_blob_value->GetBool() && is_android)) { |
| return false; |
| } |
| } |
| |
| const base::Value* fedcm_accounts_value = capabilities.Find("fedcm:accounts"); |
| if (fedcm_accounts_value) { |
| if (!fedcm_accounts_value->is_bool() || !fedcm_accounts_value->GetBool()) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // Implementation of "process capabilities", as defined in W3C spec at |
| // https://www.w3.org/TR/webdriver/#processing-capabilities. Step numbers in |
| // the comments correspond to the step numbers in the spec. |
| Status ProcessCapabilities(const base::Value::Dict& params, |
| base::Value::Dict& result_capabilities) { |
| // 1. Get the property "capabilities" from parameters. |
| const base::Value::Dict* capabilities_request = |
| params.FindDict("capabilities"); |
| if (!capabilities_request) |
| return Status(kInvalidArgument, "'capabilities' must be a JSON object"); |
| |
| // 2. Get the property "alwaysMatch" from capabilities request. |
| const base::Value::Dict empty_object; |
| const base::Value::Dict* required_capabilities; |
| const base::Value* required_capabilities_value = |
| capabilities_request->Find("alwaysMatch"); |
| if (required_capabilities_value == nullptr) { |
| required_capabilities = &empty_object; |
| } else if (required_capabilities_value->is_dict()) { |
| required_capabilities = &required_capabilities_value->GetDict(); |
| Capabilities cap; |
| Status status = cap.Parse(*required_capabilities); |
| if (status.IsError()) |
| return status; |
| } else { |
| return Status(kInvalidArgument, "'alwaysMatch' must be a JSON object"); |
| } |
| |
| // 3. Get the property "firstMatch" from capabilities request. |
| base::Value::List default_list; |
| const base::Value::List* all_first_match_capabilities; |
| const base::Value* all_first_match_capabilities_value = |
| capabilities_request->Find("firstMatch"); |
| if (all_first_match_capabilities_value == nullptr) { |
| default_list.Append(base::Value::Dict()); |
| all_first_match_capabilities = &default_list; |
| } else if (all_first_match_capabilities_value->is_list()) { |
| all_first_match_capabilities = |
| &all_first_match_capabilities_value->GetList(); |
| if (all_first_match_capabilities->size() < 1) { |
| return Status(kInvalidArgument, |
| "'firstMatch' must contain at least one entry"); |
| } |
| } else { |
| return Status(kInvalidArgument, "'firstMatch' must be a JSON list"); |
| } |
| |
| // 4. Let validated first match capabilities be an empty JSON List. |
| std::vector<const base::Value::Dict*> validated_first_match_capabilities; |
| |
| // 5. Validate all first match capabilities. |
| for (size_t i = 0; i < all_first_match_capabilities->size(); ++i) { |
| const base::Value& first_match = (*all_first_match_capabilities)[i]; |
| if (!first_match.is_dict()) { |
| return Status(kInvalidArgument, |
| base::StringPrintf( |
| "entry %zu of 'firstMatch' must be a JSON object", i)); |
| } |
| Capabilities cap; |
| Status status = cap.Parse(first_match.GetDict()); |
| if (status.IsError()) |
| return Status( |
| kInvalidArgument, |
| base::StringPrintf("entry %zu of 'firstMatch' is invalid", i), |
| status); |
| validated_first_match_capabilities.push_back(&first_match.GetDict()); |
| } |
| |
| // 6. Let merged capabilities be an empty List. |
| std::vector<base::Value::Dict> merged_capabilities; |
| |
| // 7. Merge capabilities. |
| for (size_t i = 0; i < validated_first_match_capabilities.size(); ++i) { |
| const base::Value::Dict* first_match_capabilities = |
| validated_first_match_capabilities[i]; |
| base::Value::Dict merged; |
| if (!MergeCapabilities(*required_capabilities, *first_match_capabilities, |
| merged)) { |
| return Status( |
| kInvalidArgument, |
| base::StringPrintf( |
| "unable to merge 'alwaysMatch' with entry %zu of 'firstMatch'", |
| i)); |
| } |
| merged_capabilities.emplace_back(std::move(merged)); |
| } |
| |
| // 8. Match capabilities. |
| for (auto& capabilities : merged_capabilities) { |
| if (MatchCapabilities(capabilities)) { |
| result_capabilities = std::move(capabilities); |
| return Status(kOk); |
| } |
| } |
| |
| // 9. The spec says "return success with data null", but then the caller is |
| // instructed to return error when the data is null. Since we don't have a |
| // convenient way to return data null, we will take a shortcut and return an |
| // error directly. |
| return Status(kSessionNotCreated, "No matching capabilities found"); |
| } |
| |
| Status ExecuteInitSession(const InitSessionParams& bound_params, |
| Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| Status status = InitSessionHelper(bound_params, session, params, value); |
| if (status.IsError()) { |
| session->quit = true; |
| if (session->chrome != nullptr) |
| session->chrome->Quit(); |
| } else if (session->webSocketUrl) { |
| bound_params.cmd_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(&InitSessionForWebSocketConnection, |
| bound_params.session_map, session->id)); |
| } |
| return status; |
| } |
| |
| Status ExecuteQuit(bool allow_detach, |
| Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| session->quit = true; |
| if (allow_detach && session->detach) |
| return Status(kOk); |
| return session->chrome->Quit(); |
| } |
| |
| Status ExecuteGetSessionCapabilities(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| *value = std::make_unique<base::Value>(session->capabilities->Clone()); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetCurrentWindowHandle(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| *value = std::make_unique<base::Value>(web_view->GetId()); |
| return Status(kOk); |
| } |
| |
| Status ExecuteClose(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| std::list<std::string> web_view_ids; |
| Status status = session->chrome->GetWebViewIds(&web_view_ids, |
| session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| bool is_last_web_view = web_view_ids.size() == 1u; |
| if (session->webSocketUrl) { |
| is_last_web_view = web_view_ids.size() <= 2u; |
| } |
| web_view_ids.clear(); |
| |
| WebView* web_view = nullptr; |
| status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| status = web_view->HandleReceivedEvents(); |
| if (status.IsError()) |
| return status; |
| |
| |
| JavaScriptDialogManager* dialog_manager = |
| web_view->GetJavaScriptDialogManager(); |
| if (dialog_manager->IsDialogOpen()) { |
| std::string alert_text; |
| status = dialog_manager->GetDialogMessage(&alert_text); |
| if (status.IsError()) |
| return status; |
| |
| // Close the dialog depending on the unexpectedalert behaviour set by user |
| // before returning an error, so that subsequent commands do not fail. |
| const std::string& prompt_behavior = session->unhandled_prompt_behavior; |
| |
| if (prompt_behavior == ::prompt_behavior::kAccept || |
| prompt_behavior == ::prompt_behavior::kAcceptAndNotify) { |
| status = dialog_manager->HandleDialog(true, session->prompt_text.get()); |
| } else if (prompt_behavior == ::prompt_behavior::kDismiss || |
| prompt_behavior == ::prompt_behavior::kDismissAndNotify) { |
| status = dialog_manager->HandleDialog(false, session->prompt_text.get()); |
| } |
| if (status.IsError()) |
| return status; |
| |
| // For backward compatibility, in legacy mode we always notify. |
| if (!session->w3c_compliant || |
| prompt_behavior == ::prompt_behavior::kAcceptAndNotify || |
| prompt_behavior == ::prompt_behavior::kDismissAndNotify || |
| prompt_behavior == ::prompt_behavior::kIgnore) { |
| return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}"); |
| } |
| } |
| |
| status = session->chrome->CloseWebView(web_view->GetId()); |
| if (status.IsError()) |
| return status; |
| |
| if (is_last_web_view) { |
| // If there is only one window left, call quit as well. |
| session->quit = true; |
| status = session->chrome->Quit(); |
| if (status.IsOk()) |
| *value = std::make_unique<base::Value>(base::Value::Type::LIST); |
| } else { |
| status = ExecuteGetWindowHandles(session, base::Value::Dict(), value); |
| if (status.IsError()) |
| return status; |
| } |
| |
| return status; |
| } |
| |
| Status ExecuteGetWindowHandles(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| std::list<std::string> web_view_ids; |
| Status status = session->chrome->GetWebViewIds(&web_view_ids, |
| session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| |
| if (session->webSocketUrl) { |
| auto it = |
| base::ranges::find(web_view_ids, session->bidi_mapper_web_view_id); |
| if (it != web_view_ids.end()) { |
| web_view_ids.erase(it); |
| } |
| } |
| |
| base::Value::List window_ids; |
| for (std::list<std::string>::const_iterator it = web_view_ids.begin(); |
| it != web_view_ids.end(); ++it) { |
| window_ids.Append(*it); |
| } |
| *value = std::make_unique<base::Value>(std::move(window_ids)); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSwitchToWindow(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| const std::string* name; |
| if (session->w3c_compliant) { |
| name = params.FindString("handle"); |
| if (!name) |
| return Status(kInvalidArgument, "'handle' must be a string"); |
| } else { |
| name = params.FindString("name"); |
| if (!name) |
| return Status(kInvalidArgument, "'name' must be a string"); |
| } |
| |
| std::list<std::string> web_view_ids; |
| Status status = session->chrome->GetWebViewIds(&web_view_ids, |
| session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| |
| std::string web_view_id; |
| bool found = false; |
| // Check if any web_view matches |name|. |
| for (std::list<std::string>::const_iterator it = web_view_ids.begin(); |
| it != web_view_ids.end(); ++it) { |
| if (*it == *name) { |
| web_view_id = *name; |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| // Check if any of the tab window names match |name|. |
| const char* kGetWindowNameScript = "function() { return window.name; }"; |
| base::Value::List args; |
| for (std::list<std::string>::const_iterator it = web_view_ids.begin(); |
| it != web_view_ids.end(); ++it) { |
| std::unique_ptr<base::Value> result; |
| WebView* web_view; |
| status = session->chrome->GetWebViewById(*it, &web_view); |
| if (status.IsError()) |
| return status; |
| status = web_view->CallFunction( |
| std::string(), kGetWindowNameScript, args, &result); |
| if (status.IsError()) |
| return status; |
| if (!result->is_string()) |
| return Status(kUnknownError, "failed to get window name"); |
| if (result->GetString() == *name) { |
| web_view_id = *it; |
| found = true; |
| break; |
| } |
| } |
| } |
| |
| if (!found) |
| return Status(kNoSuchWindow); |
| |
| if (session->overridden_geoposition || |
| session->overridden_network_conditions || |
| session->headless_download_directory || |
| session->chrome->IsMobileEmulationEnabled()) { |
| // Connect to new window to apply session configuration |
| WebView* web_view; |
| status = session->chrome->GetWebViewById(web_view_id, &web_view); |
| if (status.IsError()) |
| return status; |
| |
| // apply type specific configurations: |
| if (session->overridden_geoposition) { |
| status = web_view->OverrideGeolocation(*session->overridden_geoposition); |
| if (status.IsError()) |
| return status; |
| } |
| if (session->overridden_network_conditions) { |
| status = web_view->OverrideNetworkConditions( |
| *session->overridden_network_conditions); |
| if (status.IsError()) |
| return status; |
| } |
| if (session->headless_download_directory) { |
| status = web_view->OverrideDownloadDirectoryIfNeeded( |
| *session->headless_download_directory); |
| if (status.IsError()) |
| return status; |
| } |
| } |
| |
| status = session->chrome->ActivateWebView(web_view_id); |
| if (status.IsError()) |
| return status; |
| session->window = web_view_id; |
| session->SwitchToTopFrame(); |
| session->mouse_position = WebPoint(0, 0); |
| return Status(kOk); |
| } |
| |
| // Handles legacy format SetTimeout command. |
| // TODO(crbug.com/chromedriver/2596): Remove when we stop supporting legacy |
| // protocol. |
| Status ExecuteSetTimeoutLegacy(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| absl::optional<double> maybe_ms = params.FindDouble("ms"); |
| if (!maybe_ms.has_value()) |
| return Status(kInvalidArgument, "'ms' must be a double"); |
| |
| const std::string* type = params.FindString("type"); |
| if (!type) |
| return Status(kInvalidArgument, "'type' must be a string"); |
| |
| base::TimeDelta timeout = |
| base::Milliseconds(static_cast<int>(maybe_ms.value())); |
| if (*type == "implicit") { |
| session->implicit_wait = timeout; |
| } else if (*type == "script") { |
| session->script_timeout = timeout; |
| } else if (*type == "page load") { |
| session->page_load_timeout = |
| ((timeout.is_negative()) ? Session::kDefaultPageLoadTimeout : timeout); |
| } else { |
| return Status(kInvalidArgument, "unknown type of timeout:" + *type); |
| } |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetTimeoutsW3C(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| for (auto setting : params) { |
| int64_t timeout_ms_int64 = -1; |
| base::TimeDelta timeout; |
| const std::string& type = setting.first; |
| if (setting.second.is_none()) { |
| if (type != "script") |
| return Status(kInvalidArgument, "timeout can not be null"); |
| timeout = base::TimeDelta::Max(); |
| } else { |
| if (!GetOptionalSafeInt(params, setting.first, &timeout_ms_int64) || |
| timeout_ms_int64 < 0) { |
| return Status(kInvalidArgument, "value must be a non-negative integer"); |
| } |
| timeout = base::Milliseconds(timeout_ms_int64); |
| } |
| if (type == "script") { |
| session->script_timeout = timeout; |
| } else if (type == "pageLoad") { |
| session->page_load_timeout = timeout; |
| } else if (type == "implicit") { |
| session->implicit_wait = timeout; |
| } |
| } |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetTimeouts(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| // TODO(crbug.com/chromedriver/2596): Remove legacy version support when we |
| // stop supporting non-W3C protocol. At that time, we can delete the legacy |
| // function and merge the W3C function into this function. |
| if (params.contains("ms")) |
| return ExecuteSetTimeoutLegacy(session, params, value); |
| return ExecuteSetTimeoutsW3C(session, params, value); |
| } |
| |
| Status ExecuteGetTimeouts(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| base::Value::Dict timeouts; |
| if (session->script_timeout == base::TimeDelta::Max()) |
| timeouts.Set("script", base::Value()); |
| else |
| SetSafeInt(timeouts, "script", session->script_timeout.InMilliseconds()); |
| |
| SetSafeInt(timeouts, "pageLoad", session->page_load_timeout.InMilliseconds()); |
| SetSafeInt(timeouts, "implicit", session->implicit_wait.InMilliseconds()); |
| |
| *value = base::Value::ToUniquePtrValue(base::Value(std::move(timeouts))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetScriptTimeout(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| absl::optional<double> maybe_ms = params.FindDouble("ms"); |
| if (!maybe_ms.has_value() || maybe_ms.value() < 0) |
| return Status(kInvalidArgument, "'ms' must be a non-negative number"); |
| session->script_timeout = |
| base::Milliseconds(static_cast<int>(maybe_ms.value())); |
| return Status(kOk); |
| } |
| |
| Status ExecuteImplicitlyWait(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| absl::optional<double> maybe_ms = params.FindDouble("ms"); |
| if (!maybe_ms.has_value() || maybe_ms.value() < 0) |
| return Status(kInvalidArgument, "'ms' must be a non-negative number"); |
| session->implicit_wait = |
| base::Milliseconds(static_cast<int>(maybe_ms.value())); |
| return Status(kOk); |
| } |
| |
| Status ExecuteIsLoading(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| bool is_pending; |
| status = web_view->IsPendingNavigation(nullptr, &is_pending); |
| if (status.IsError()) |
| return status; |
| *value = std::make_unique<base::Value>(is_pending); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetLocation(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| if (!session->overridden_geoposition) { |
| return Status(kUnknownError, |
| "Location must be set before it can be retrieved"); |
| } |
| base::Value::Dict location; |
| location.Set("latitude", session->overridden_geoposition->latitude); |
| location.Set("longitude", session->overridden_geoposition->longitude); |
| location.Set("accuracy", session->overridden_geoposition->accuracy); |
| // Set a dummy altitude to make WebDriver clients happy. |
| // https://code.google.com/p/chromedriver/issues/detail?id=281 |
| location.Set("altitude", 0); |
| *value = base::Value::ToUniquePtrValue(base::Value(std::move(location))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetNetworkConnection(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| ChromeDesktopImpl* desktop = nullptr; |
| Status status = session->chrome->GetAsDesktop(&desktop); |
| if (status.IsError()) |
| return status; |
| if (!desktop->IsNetworkConnectionEnabled()) |
| return Status(kUnknownError, "network connection must be enabled"); |
| |
| int connection_type = 0; |
| connection_type = desktop->GetNetworkConnection(); |
| |
| *value = std::make_unique<base::Value>(connection_type); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetNetworkConditions(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| if (!session->overridden_network_conditions) { |
| return Status(kUnknownError, |
| "network conditions must be set before it can be retrieved"); |
| } |
| base::Value::Dict conditions = |
| base::Value::Dict() |
| .Set("offline", session->overridden_network_conditions->offline) |
| .Set("latency", session->overridden_network_conditions->latency) |
| .Set("download_throughput", |
| session->overridden_network_conditions->download_throughput) |
| .Set("upload_throughput", |
| session->overridden_network_conditions->upload_throughput); |
| *value = base::Value::ToUniquePtrValue(base::Value(std::move(conditions))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetNetworkConnection(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| ChromeDesktopImpl* desktop = nullptr; |
| Status status = session->chrome->GetAsDesktop(&desktop); |
| if (status.IsError()) |
| return status; |
| if (!desktop->IsNetworkConnectionEnabled()) |
| return Status(kUnknownError, "network connection must be enabled"); |
| |
| absl::optional<int> connection_type = |
| params.FindIntByDottedPath("parameters.type"); |
| if (!connection_type) |
| return Status(kInvalidArgument, "invalid connection_type"); |
| |
| desktop->SetNetworkConnection(*connection_type); |
| |
| std::unique_ptr<NetworkConditions> network_conditions( |
| new NetworkConditions()); |
| |
| if (*connection_type & kWifiMask) { |
| network_conditions->latency = kWifiLatency; |
| network_conditions->upload_throughput = kWifiThroughput; |
| network_conditions->download_throughput = kWifiThroughput; |
| network_conditions->offline = false; |
| } else if (*connection_type & k4GMask) { |
| network_conditions->latency = k4GLatency; |
| network_conditions->upload_throughput = k4GThroughput; |
| network_conditions->download_throughput = k4GThroughput; |
| network_conditions->offline = false; |
| } else if (*connection_type & k3GMask) { |
| network_conditions->latency = k3GLatency; |
| network_conditions->upload_throughput = k3GThroughput; |
| network_conditions->download_throughput = k3GThroughput; |
| network_conditions->offline = false; |
| } else if (*connection_type & k2GMask) { |
| network_conditions->latency = k2GLatency; |
| network_conditions->upload_throughput = k2GThroughput; |
| network_conditions->download_throughput = k2GThroughput; |
| network_conditions->offline = false; |
| } else { |
| network_conditions->latency = kAirplaneModeLatency; |
| network_conditions->upload_throughput = kAirplaneModeThroughput; |
| network_conditions->download_throughput = kAirplaneModeThroughput; |
| network_conditions->offline = true; |
| } |
| |
| session->overridden_network_conditions.reset( |
| network_conditions.release()); |
| |
| // Applies overridden network connection to all WebViews of the session |
| // to ensure that network emulation is applied on a per-session basis |
| // rather than the just to the current WebView. |
| std::list<std::string> web_view_ids; |
| status = session->chrome->GetWebViewIds(&web_view_ids, |
| session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| |
| for (std::string web_view_id : web_view_ids) { |
| WebView* web_view; |
| status = session->chrome->GetWebViewById(web_view_id, &web_view); |
| if (status.IsError()) |
| return status; |
| web_view->OverrideNetworkConditions( |
| *session->overridden_network_conditions); |
| } |
| |
| *value = std::make_unique<base::Value>(*connection_type); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetWindowPosition(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| Chrome::WindowRect window_rect; |
| Status status = session->chrome->GetWindowRect(session->window, &window_rect); |
| |
| if (status.IsError()) |
| return status; |
| |
| base::Value::Dict position = |
| base::Value::Dict().Set("x", window_rect.x).Set("y", window_rect.y); |
| *value = base::Value::ToUniquePtrValue(base::Value(std::move(position))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetWindowPosition(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| absl::optional<double> maybe_x = params.FindDouble("x"); |
| absl::optional<double> maybe_y = params.FindDouble("y"); |
| |
| if (!maybe_x.has_value() || !maybe_y.has_value()) |
| return Status(kInvalidArgument, "missing or invalid 'x' or 'y'"); |
| |
| base::Value::Dict rect_params; |
| rect_params.Set("x", static_cast<int>(maybe_x.value())); |
| rect_params.Set("y", static_cast<int>(maybe_y.value())); |
| return session->chrome->SetWindowRect(session->window, |
| std::move(rect_params)); |
| } |
| |
| Status ExecuteGetWindowSize(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| Chrome::WindowRect window_rect; |
| Status status = session->chrome->GetWindowRect(session->window, &window_rect); |
| |
| if (status.IsError()) |
| return status; |
| |
| base::Value::Dict size = base::Value::Dict() |
| .Set("width", window_rect.width) |
| .Set("height", window_rect.height); |
| *value = base::Value::ToUniquePtrValue(base::Value(std::move(size))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetWindowSize(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| absl::optional<double> maybe_width = params.FindDouble("width"); |
| absl::optional<double> maybe_height = params.FindDouble("height"); |
| |
| if (!maybe_width.has_value() || !maybe_height.has_value()) |
| return Status(kInvalidArgument, "missing or invalid 'width' or 'height'"); |
| |
| base::Value::Dict rect_params; |
| rect_params.Set("width", static_cast<int>(maybe_width.value())); |
| rect_params.Set("height", static_cast<int>(maybe_height.value())); |
| return session->chrome->SetWindowRect(session->window, |
| std::move(rect_params)); |
| } |
| |
| Status ExecuteGetAvailableLogTypes(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| std::unique_ptr<base::Value::List> types(new base::Value::List()); |
| std::vector<WebDriverLog*> logs = session->GetAllLogs(); |
| for (std::vector<WebDriverLog*>::const_iterator log = logs.begin(); |
| log != logs.end(); |
| ++log) { |
| types->Append((*log)->type()); |
| } |
| *value = std::make_unique<base::Value>(base::Value(std::move(*types))); |
| return Status(kOk); |
| } |
| |
| Status ExecuteGetLog(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| const std::string* log_type = params.FindString("type"); |
| if (!log_type) { |
| return Status(kInvalidArgument, "missing or invalid 'type'"); |
| } |
| |
| // Evaluate a JavaScript in the renderer process for the current tab, to flush |
| // out any pending logging-related events. |
| Status status = EvaluateScriptAndIgnoreResult(session, "1"); |
| if (status.IsError()) { |
| // Sometimes a WebDriver client fetches logs to diagnose an error that has |
| // occurred. It's possible that in the case of an error, the renderer is no |
| // be longer available, but we should return the logs anyway. So log (but |
| // don't fail on) any error that we get while evaluating the script. |
| LOG(WARNING) << "Unable to evaluate script: " << status.message(); |
| } |
| |
| std::vector<WebDriverLog*> logs = session->GetAllLogs(); |
| for (std::vector<WebDriverLog*>::const_iterator log = logs.begin(); |
| log != logs.end(); |
| ++log) { |
| if (*log_type == (*log)->type()) { |
| *value = base::Value::ToUniquePtrValue( |
| base::Value((*log)->GetAndClearEntries())); |
| return Status(kOk); |
| } |
| } |
| return Status(kInvalidArgument, "log type '" + *log_type + "' not found"); |
| } |
| |
| Status ExecuteUploadFile(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| const std::string* base64_zip_data = params.FindString("file"); |
| if (!base64_zip_data) |
| return Status(kInvalidArgument, "missing or invalid 'file'"); |
| std::string zip_data; |
| if (!Base64Decode(*base64_zip_data, &zip_data)) |
| return Status(kUnknownError, "unable to decode 'file'"); |
| |
| if (!session->temp_dir.IsValid()) { |
| if (!session->temp_dir.CreateUniqueTempDir()) |
| return Status(kUnknownError, "unable to create temp dir"); |
| } |
| base::FilePath upload_dir; |
| if (!base::CreateTemporaryDirInDir(session->temp_dir.GetPath(), |
| FILE_PATH_LITERAL("upload"), |
| &upload_dir)) { |
| return Status(kUnknownError, "unable to create temp dir"); |
| } |
| std::string error_msg; |
| base::FilePath upload; |
| Status status = UnzipSoleFile(upload_dir, zip_data, &upload); |
| if (status.IsError()) |
| return Status(kUnknownError, "unable to unzip 'file'", status); |
| |
| *value = std::make_unique<base::Value>(upload.AsUTF8Unsafe()); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetSPCTransactionMode(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| const std::string* mode = params.FindString("mode"); |
| if (!mode) |
| return Status(kInvalidArgument, "missing parameter 'mode'"); |
| |
| base::Value::Dict body; |
| body.Set("mode", *mode); |
| |
| return web_view->SendCommandAndGetResult("Page.setSPCTransactionMode", body, |
| value); |
| } |
| |
| Status ExecuteGenerateTestReport(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| const std::string* message = params.FindString("message"); |
| if (!message) |
| return Status(kInvalidArgument, "missing parameter 'message'"); |
| const std::string* group = params.FindString("group"); |
| |
| base::Value::Dict body; |
| body.Set("message", *message); |
| body.Set("group", group ? *group : "default"); |
| |
| web_view->SendCommandAndGetResult("Page.generateTestReport", body, value); |
| return Status(kOk); |
| } |
| |
| Status ExecuteSetTimeZone(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| WebView* web_view = nullptr; |
| Status status = session->GetTargetWindow(&web_view); |
| if (status.IsError()) |
| return status; |
| |
| const std::string* time_zone = params.FindString("time_zone"); |
| if (!time_zone) |
| return Status(kInvalidArgument, "missing parameter 'time_zone'"); |
| |
| base::Value::Dict body; |
| body.Set("timezoneId", *time_zone); |
| |
| web_view->SendCommandAndGetResult("Emulation.setTimezoneOverride", body, |
| value); |
| return Status(kOk); |
| } |
| |
| // Run a BiDi command |
| Status ExecuteBidiCommand(Session* session, |
| const base::Value::Dict& params, |
| std::unique_ptr<base::Value>* value) { |
| // session == nullptr is a valid case: ExecuteQuit has already been handled |
| // in the session thread but the following |
| // TerminateSessionThreadOnCommandThread has not yet been executed (the later |
| // destroys the session thread) The connection has already been accepted by |
| // the CMD thread but soon it will be closed. We don't need to do anything. |
| if (session == nullptr) { |
| return Status{kNoSuchFrame, "session not found"}; |
| } |
| const std::string* data = params.FindString("bidiCommand"); |
| if (!data) { |
| return Status{kUnknownError, "bidiCommand is missing in params"}; |
| } |
| |
| absl::optional<int> connection_id = params.FindInt("connectionId"); |
| if (!connection_id) { |
| return Status{kUnknownCommand, "connectionId is missing in params"}; |
| } |
| |
| WebView* web_view = nullptr; |
| Status status = session->chrome->GetWebViewById( |
| session->bidi_mapper_web_view_id, &web_view); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| absl::optional<base::Value> data_parsed = |
| base::JSONReader::Read(*data, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| |
| if (!data_parsed) { |
| return Status(kUnknownError, "cannot parse the BiDi command: " + *data); |
| } |
| |
| if (!data_parsed->is_dict()) { |
| return Status(kUnknownError, |
| "a JSON map is expected as a BiDi command: " + *data); |
| } |
| |
| base::Value::Dict& bidi_cmd = data_parsed->GetDict(); |
| |
| std::string* method = bidi_cmd.FindString("method"); |
| if (!method) { |
| return Status(kUnknownError, |
| "BiDi command is missing 'method' field: " + *data); |
| } |
| |
| std::string* user_channel = bidi_cmd.FindString("channel"); |
| std::string channel; |
| if (user_channel) { |
| channel = *user_channel + "/" + base::NumberToString(*connection_id) + |
| Session::kChannelSuffix; |
| } else { |
| channel = |
| "/" + base::NumberToString(*connection_id) + Session::kNoChannelSuffix; |
| } |
| |
| if (*method == "browsingContext.close") { |
| bidi_cmd.Set("channel", channel + Session::kBlockingChannelSuffix); |
| // Closing of the context is handled in a blocking way. |
| // This simplifies us closing the browser if the last tab was closed. |
| session->awaiting_bidi_response = true; |
| status = web_view->PostBidiCommand(std::move(bidi_cmd)); |
| base::RepeatingCallback<Status(bool*)> bidi_response_is_received = |
| base::BindRepeating( |
| [](Session* session, bool* condition_is_met) { |
| *condition_is_met = !session->awaiting_bidi_response; |
| return Status{kOk}; |
| }, |
| base::Unretained(session)); |
| if (status.IsError()) { |
| return status; |
| } |
| |
| // The timeout is the same as in ChromeImpl::CloseTarget |
| status = web_view->HandleEventsUntil(std::move(bidi_response_is_received), |
| Timeout(base::Seconds(20))); |
| if (status.code() == kTimeout) { |
| // It looks like something is going wrong with the BiDiMapper. |
| // Terminating the session... |
| session->quit = true; |
| status = session->chrome->Quit(); |
| return Status(kUnknownError, "failed to close window in 20 seconds"); |
| } |
| if (status.IsError()) |
| return status; |
| |
| std::list<std::string> web_view_ids; |
| status = |
| session->chrome->GetWebViewIds(&web_view_ids, session->w3c_compliant); |
| if (status.IsError()) |
| return status; |
| |
| bool is_last_web_view = web_view_ids.size() <= 1u; |
| if (is_last_web_view) { |
| session->quit = true; |
| status = session->chrome->Quit(); |
| } |
| } else { |
| bidi_cmd.Set("channel", std::move(channel)); |
| status = web_view->PostBidiCommand(std::move(bidi_cmd)); |
| } |
| |
| return status; |
| } |