| // Copyright 2014 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/browser/devtools/devtools_ui_bindings.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string_view> |
| #include <utility> |
| #include <variant> |
| |
| #include "aida_client.h" |
| #include "base/base64.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/string_escape.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/no_destructor.h" |
| #include "base/notimplemented.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/unguessable_token.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "base/version_info/channel.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/devtools/aida_service_handler.h" |
| #include "chrome/browser/devtools/devtools_file_watcher.h" |
| #include "chrome/browser/devtools/devtools_http_service_registry.h" |
| #include "chrome/browser/devtools/devtools_select_file_dialog.h" |
| #include "chrome/browser/devtools/features.h" |
| #include "chrome/browser/devtools/url_constants.h" |
| #include "chrome/browser/policy/profile_policy_connector.h" |
| #include "chrome/browser/privacy_sandbox/tracking_protection_settings_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_avatar_icon_util.h" |
| #include "chrome/browser/search/search.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/ui/browser_dialogs.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/hats/hats_service.h" |
| #include "chrome/browser/ui/hats/hats_service_factory.h" |
| #include "chrome/common/channel_info.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/content_settings/core/common/features.h" |
| #include "components/infobars/content/content_infobar_manager.h" |
| #include "components/metrics/structured/structured_events.h" |
| #include "components/metrics/structured/structured_metrics_client.h" |
| #include "components/policy/core/common/policy_map.h" |
| #include "components/policy/core/common/policy_service.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/privacy_sandbox/tracking_protection_settings.h" |
| #include "components/search_engines/util.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/sync/service/sync_service.h" |
| #include "components/sync_preferences/pref_service_syncable.h" |
| #include "components/zoom/page_zoom.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/child_process_security_policy.h" |
| #include "content/public/browser/devtools_external_agent_proxy.h" |
| #include "content/public/browser/devtools_external_agent_proxy_delegate.h" |
| #include "content/public/browser/file_url_loader.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/reload_type.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/shared_cors_origin_access_list.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/browser/web_ui_url_loader_factory.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "google_apis/google_api_keys.h" |
| #include "ipc/constants.mojom.h" |
| #include "net/base/features.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/url_util.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/cpp/simple_url_loader_stream_consumer.h" |
| #include "services/network/public/cpp/wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/public_buildflags.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/dialog_model.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/shell_dialogs/select_file_dialog.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/user_education/browser_user_education_interface.h" |
| #include "chrome/browser/user_education/user_education_service.h" |
| #include "chrome/browser/user_education/user_education_service_factory.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/manifest_handlers/devtools_page_handler.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #endif |
| |
| using content::BrowserThread; |
| |
| namespace content { |
| struct LoadCommittedDetails; |
| struct FrameNavigateParams; |
| } // namespace content |
| |
| namespace { |
| |
| const char kFrontendHostId[] = "id"; |
| const char kFrontendHostMethod[] = "method"; |
| const char kFrontendHostParams[] = "params"; |
| const char kTitleFormat[] = "DevTools - %s"; |
| |
| const char kConfigDiscoverUsbDevices[] = "discoverUsbDevices"; |
| const char kConfigPortForwardingEnabled[] = "portForwardingEnabled"; |
| const char kConfigPortForwardingConfig[] = "portForwardingConfig"; |
| const char kConfigNetworkDiscoveryEnabled[] = "networkDiscoveryEnabled"; |
| const char kConfigNetworkDiscoveryConfig[] = "networkDiscoveryConfig"; |
| |
| // This constant should be in sync with |
| // the constant |
| // kShellMaxMessageChunkSize in content/shell/browser/shell_devtools_bindings.cc |
| // and |
| // kLayoutTestMaxMessageChunkSize in |
| // content/shell/browser/layout_test/devtools_protocol_test_bindings.cc. |
| const size_t kMaxMessageChunkSize = IPC::mojom::kChannelMaximumMessageSize / 4; |
| |
| base::Value::Dict CreateFileSystemValue( |
| DevToolsFileHelper::FileSystem file_system) { |
| base::Value::Dict file_system_value; |
| file_system_value.Set("type", file_system.type); |
| file_system_value.Set("fileSystemName", file_system.file_system_name); |
| file_system_value.Set("rootURL", file_system.root_url); |
| file_system_value.Set("fileSystemPath", file_system.file_system_path); |
| return file_system_value; |
| } |
| |
| // DevToolsUIDefaultDelegate -------------------------------------------------- |
| |
| class DefaultBindingsDelegate : public DevToolsUIBindings::Delegate { |
| public: |
| explicit DefaultBindingsDelegate(content::WebContents* web_contents) |
| : web_contents_(web_contents) {} |
| |
| DefaultBindingsDelegate(const DefaultBindingsDelegate&) = delete; |
| DefaultBindingsDelegate& operator=(const DefaultBindingsDelegate&) = delete; |
| |
| private: |
| ~DefaultBindingsDelegate() override = default; |
| |
| content::WebContents* GetInspectedWebContents() override { return nullptr; } |
| void ActivateWindow() override; |
| void CloseWindow() override {} |
| void Inspect(scoped_refptr<content::DevToolsAgentHost> host) override {} |
| void SetInspectedPageBounds(const gfx::Rect& rect) override {} |
| void InspectElementCompleted() override {} |
| void SetIsDocked(bool is_docked) override {} |
| void OpenInNewTab(const std::string& url) override; |
| void OpenSearchResultsInNewTab(const std::string& query) override; |
| void SetWhitelistedShortcuts(const std::string& message) override {} |
| void SetEyeDropperActive(bool active) override {} |
| void OpenNodeFrontend() override {} |
| using DispatchCallback = |
| DevToolsEmbedderMessageDispatcher::Delegate::DispatchCallback; |
| |
| void InspectedContentsClosing() override; |
| void OnLoadCompleted() override {} |
| void ReadyForTest() override {} |
| void ConnectionReady() override {} |
| void SetOpenNewWindowForPopups(bool value) override {} |
| infobars::ContentInfoBarManager* GetInfoBarManager() override; |
| void RenderProcessGone(bool crashed) override {} |
| void ShowCertificateViewer(const std::string& cert_chain) override {} |
| |
| int GetDockStateForLogging() override { return 0; } |
| int GetOpenedByForLogging() override { return 0; } |
| int GetClosedByForLogging() override { return 0; } |
| raw_ptr<content::WebContents> web_contents_; |
| }; |
| |
| void DefaultBindingsDelegate::ActivateWindow() { |
| web_contents_->GetDelegate()->ActivateContents(web_contents_); |
| web_contents_->Focus(); |
| } |
| |
| void DefaultBindingsDelegate::OpenInNewTab(const std::string& url) { |
| content::OpenURLParams params(GURL(url), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| #if BUILDFLAG(IS_ANDROID) |
| NOTIMPLEMENTED(); |
| #else |
| Browser* browser = chrome::FindBrowserWithTab(web_contents_); |
| // Check if the browser is still alive, as it might have been closed in the |
| // meantime. |
| // TODO(https://crbug.com/403946437): We should definitely understand why this |
| // happens. |
| if (browser) { |
| browser->OpenURL(params, /*navigation_handle_callback=*/{}); |
| } |
| #endif |
| } |
| |
| void DefaultBindingsDelegate::OpenSearchResultsInNewTab( |
| const std::string& query) { |
| #if BUILDFLAG(IS_ANDROID) |
| NOTIMPLEMENTED(); |
| #else |
| Browser* browser = chrome::FindBrowserWithTab(web_contents_); |
| TemplateURLService* url_service = |
| TemplateURLServiceFactory::GetForProfile(browser->profile()); |
| DCHECK(url_service); |
| GURL url = |
| GetDefaultSearchURLForSearchTerms(url_service, base::UTF8ToUTF16(query)); |
| content::OpenURLParams params(GURL(url), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| browser->OpenURL(params, /*navigation_handle_callback=*/{}); |
| #endif |
| } |
| |
| void DefaultBindingsDelegate::InspectedContentsClosing() { |
| web_contents_->ClosePage(); |
| } |
| |
| infobars::ContentInfoBarManager* DefaultBindingsDelegate::GetInfoBarManager() { |
| return infobars::ContentInfoBarManager::FromWebContents(web_contents_); |
| } |
| |
| base::Value::Dict BuildObjectForResponse(const net::HttpResponseHeaders* rh, |
| bool success, |
| int net_error) { |
| base::Value::Dict response; |
| int responseCode = 200; |
| if (rh) { |
| responseCode = rh->response_code(); |
| } else if (!success) { |
| // In case of no headers, assume file:// URL and failed to load |
| responseCode = 404; |
| } |
| response.Set("statusCode", responseCode); |
| response.Set("netError", net_error); |
| response.Set("netErrorName", net::ErrorToString(net_error)); |
| |
| base::Value::Dict headers; |
| size_t iterator = 0; |
| std::string name; |
| std::string value; |
| // TODO(caseq): this probably needs to handle duplicate header names |
| // correctly by folding them. |
| while (rh && rh->EnumerateHeaderLines(&iterator, &name, &value)) { |
| headers.Set(name, value); |
| } |
| |
| response.Set("headers", std::move(headers)); |
| return response; |
| } |
| |
| GURL SanitizeFrontendURL(const GURL& url, |
| const std::string& scheme, |
| const std::string& host, |
| const std::string& path, |
| bool allow_query_and_fragment); |
| |
| std::string SanitizeRevision(const std::string& revision) { |
| for (size_t i = 0; i < revision.length(); i++) { |
| if (!(revision[i] == '@' && i == 0) && |
| !(revision[i] >= '0' && revision[i] <= '9') && |
| !(revision[i] >= 'a' && revision[i] <= 'z') && |
| !(revision[i] >= 'A' && revision[i] <= 'Z')) { |
| return std::string(); |
| } |
| } |
| return revision; |
| } |
| |
| std::string SanitizeRemoteVersion(const std::string& remoteVersion) { |
| for (size_t i = 0; i < remoteVersion.length(); i++) { |
| if (remoteVersion[i] != '.' && |
| !(remoteVersion[i] >= '0' && remoteVersion[i] <= '9')) { |
| return std::string(); |
| } |
| } |
| return remoteVersion; |
| } |
| |
| std::string SanitizeFrontendPath(const std::string& path) { |
| for (size_t i = 0; i < path.length(); i++) { |
| if (path[i] != '/' && path[i] != '-' && path[i] != '_' && path[i] != '.' && |
| path[i] != '@' && !(path[i] >= '0' && path[i] <= '9') && |
| !(path[i] >= 'a' && path[i] <= 'z') && |
| !(path[i] >= 'A' && path[i] <= 'Z')) { |
| return std::string(); |
| } |
| } |
| return path; |
| } |
| |
| std::string SanitizeEndpoint(const std::string& value) { |
| if (value.find('&') != std::string::npos || |
| value.find('?') != std::string::npos) { |
| return std::string(); |
| } |
| return value; |
| } |
| |
| std::string SanitizeRemoteBase(const std::string& value) { |
| GURL url(value); |
| std::string path = url.path(); |
| std::vector<std::string> parts = |
| base::SplitString(path, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| std::string revision = parts.size() > 2 ? parts[2] : ""; |
| revision = SanitizeRevision(revision); |
| path = base::StringPrintf("/%s/%s/", kRemoteFrontendPath, revision.c_str()); |
| return SanitizeFrontendURL(url, url::kHttpsScheme, kRemoteFrontendDomain, |
| path, false) |
| .spec(); |
| } |
| |
| std::string SanitizeRemoteFrontendURL(const std::string& value) { |
| GURL url(base::UnescapeBinaryURLComponent( |
| value, base::UnescapeRule::REPLACE_PLUS_WITH_SPACE)); |
| std::string path = url.path(); |
| std::vector<std::string> parts = |
| base::SplitString(path, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| std::string revision = parts.size() > 2 ? parts[2] : ""; |
| revision = SanitizeRevision(revision); |
| std::string filename = !parts.empty() ? parts[parts.size() - 1] : ""; |
| if (filename != "devtools.html") { |
| filename = "inspector.html"; |
| } |
| path = base::StringPrintf("/serve_rev/%s/%s", revision.c_str(), |
| filename.c_str()); |
| std::string sanitized = SanitizeFrontendURL(url, url::kHttpsScheme, |
| kRemoteFrontendDomain, path, true) |
| .spec(); |
| return base::EscapeQueryParamValue(sanitized, false); |
| } |
| |
| std::string SanitizeEnabledExperiments(const std::string& value) { |
| const auto is_legal = [](char ch) { |
| return base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch) || ch == ';' || |
| ch == '_'; |
| }; |
| return std::ranges::all_of(value, is_legal) ? value : std::string(); |
| } |
| |
| std::string SanitizeFrontendQueryParam(const std::string& key, |
| const std::string& value) { |
| // Convert boolean flags to true. |
| if (key == "can_dock" || key == "debugFrontend" || key == "isSharedWorker" || |
| key == "v8only" || key == "remoteFrontend" || key == "nodeFrontend" || |
| key == "hasOtherClients" || key == "uiDevTools" || |
| key == "browserConnection") { |
| return "true"; |
| } |
| |
| // Pass connection endpoints as is. |
| if (key == "ws" || key == "service-backend") { |
| return SanitizeEndpoint(value); |
| } |
| |
| if (key == "panel" && |
| (value == "elements" || value == "console" || value == "sources")) { |
| return value; |
| } |
| |
| if (key == "remoteBase") { |
| return SanitizeRemoteBase(value); |
| } |
| |
| if (key == "remoteFrontendUrl") { |
| return SanitizeRemoteFrontendURL(value); |
| } |
| |
| if (key == "remoteVersion") { |
| return SanitizeRemoteVersion(value); |
| } |
| |
| if (key == "enabledExperiments") { |
| return SanitizeEnabledExperiments(value); |
| } |
| |
| if (key == "targetType" && value == "tab") { |
| return value; |
| } |
| |
| if (key == "noJavaScriptCompletion" && value == "true") { |
| return value; |
| } |
| |
| if (key == "veLogging" && value == "true") { |
| return value; |
| } |
| |
| if (key == "isChromeForTesting" && value == "true") { |
| return value; |
| } |
| |
| if (key == "disableSelfXssWarnings" && value == "true") { |
| return value; |
| } |
| |
| return std::string(); |
| } |
| |
| GURL SanitizeFrontendURL(const GURL& url, |
| const std::string& scheme, |
| const std::string& host, |
| const std::string& path, |
| bool allow_query_and_fragment) { |
| std::vector<std::string> query_parts; |
| std::string fragment; |
| if (allow_query_and_fragment) { |
| for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) { |
| const std::string key = std::string(it.GetKey()); |
| std::string value = |
| SanitizeFrontendQueryParam(key, std::string(it.GetValue())); |
| if (!value.empty()) { |
| query_parts.push_back( |
| base::StringPrintf("%s=%s", key.c_str(), value.c_str())); |
| } |
| } |
| if (url.has_ref() && url.ref_piece().find('\'') == std::string_view::npos) { |
| fragment = '#' + url.ref(); |
| } |
| } |
| std::string query = |
| query_parts.empty() ? "" : "?" + base::JoinString(query_parts, "&"); |
| std::string constructed = |
| base::StringPrintf("%s://%s%s%s%s", scheme.c_str(), host.c_str(), |
| path.c_str(), query.c_str(), fragment.c_str()); |
| GURL result = GURL(constructed); |
| if (!result.is_valid()) { |
| return GURL(); |
| } |
| return result; |
| } |
| |
| constexpr base::TimeDelta kInitialBackoffDelay = base::Milliseconds(250); |
| constexpr base::TimeDelta kMaxBackoffDelay = base::Seconds(10); |
| |
| } // namespace |
| |
| class DevToolsUIBindings::NetworkResourceLoader |
| : public network::SimpleURLLoaderStreamConsumer { |
| public: |
| class URLLoaderFactoryHolder { |
| public: |
| network::mojom::URLLoaderFactory* get() { |
| return ptr_.get() ? ptr_.get() : refptr_.get(); |
| } |
| void operator=(std::unique_ptr<network::mojom::URLLoaderFactory>&& ptr) { |
| ptr_ = std::move(ptr); |
| } |
| void operator=(scoped_refptr<network::SharedURLLoaderFactory>&& refptr) { |
| refptr_ = std::move(refptr); |
| } |
| |
| private: |
| std::unique_ptr<network::mojom::URLLoaderFactory> ptr_; |
| scoped_refptr<network::SharedURLLoaderFactory> refptr_; |
| }; |
| |
| static void Create(int stream_id, |
| DevToolsUIBindings* bindings, |
| const network::ResourceRequest& resource_request, |
| const net::NetworkTrafficAnnotationTag& traffic_annotation, |
| URLLoaderFactoryHolder url_loader_factory, |
| DevToolsUIBindings::DispatchCallback callback, |
| base::TimeDelta retry_delay = base::TimeDelta(), |
| std::string post_body = "", |
| std::optional<base::TimeDelta> timeout = std::nullopt) { |
| auto resource_loader = |
| std::make_unique<DevToolsUIBindings::NetworkResourceLoader>( |
| stream_id, bindings, resource_request, traffic_annotation, |
| std::move(url_loader_factory), std::move(callback), retry_delay, |
| post_body, timeout); |
| bindings->loaders_.insert(std::move(resource_loader)); |
| } |
| |
| NetworkResourceLoader( |
| int stream_id, |
| DevToolsUIBindings* bindings, |
| const network::ResourceRequest& resource_request, |
| const net::NetworkTrafficAnnotationTag& traffic_annotation, |
| URLLoaderFactoryHolder url_loader_factory, |
| DispatchCallback callback, |
| base::TimeDelta delay, |
| std::string post_body, |
| std::optional<base::TimeDelta> timeout) |
| : stream_id_(stream_id), |
| bindings_(bindings), |
| resource_request_(resource_request), |
| traffic_annotation_(traffic_annotation), |
| loader_(network::SimpleURLLoader::Create( |
| std::make_unique<network::ResourceRequest>(resource_request), |
| traffic_annotation)), |
| url_loader_factory_(std::move(url_loader_factory)), |
| callback_(std::move(callback)), |
| retry_delay_(delay) { |
| if (timeout.has_value()) { |
| loader_->SetTimeoutDuration(timeout.value()); |
| } |
| if (!post_body.empty()) { |
| loader_->AttachStringForUpload(std::move(post_body)); |
| } |
| loader_->SetOnResponseStartedCallback(base::BindOnce( |
| &NetworkResourceLoader::OnResponseStarted, base::Unretained(this))); |
| timer_.Start(FROM_HERE, delay, |
| base::BindOnce(&NetworkResourceLoader::DownloadAsStream, |
| base::Unretained(this))); |
| } |
| |
| NetworkResourceLoader(const NetworkResourceLoader&) = delete; |
| NetworkResourceLoader& operator=(const NetworkResourceLoader&) = delete; |
| |
| static base::TimeDelta GetNextExponentialBackoffDelay( |
| const base::TimeDelta& delta) { |
| if (delta.is_zero()) { |
| return kInitialBackoffDelay; |
| } else { |
| return delta * 1.3; |
| } |
| } |
| |
| private: |
| void DownloadAsStream() { |
| loader_->DownloadAsStream(url_loader_factory_.get(), this); |
| } |
| |
| void OnResponseStarted(const GURL& final_url, |
| const network::mojom::URLResponseHead& response_head) { |
| response_headers_ = response_head.headers; |
| } |
| |
| void OnDataReceived(std::string_view chunk, |
| base::OnceClosure resume) override { |
| base::Value chunkValue; |
| |
| bool encoded = !base::IsStringUTF8AllowingNoncharacters(chunk); |
| if (encoded) { |
| chunkValue = base::Value(base::Base64Encode(chunk)); |
| } else { |
| chunkValue = base::Value(chunk); |
| } |
| |
| bindings_->CallClientMethod("DevToolsAPI", "streamWrite", |
| base::Value(stream_id_), std::move(chunkValue), |
| base::Value(encoded)); |
| std::move(resume).Run(); |
| } |
| |
| void OnComplete(bool success) override { |
| if (!success && loader_->NetError() == net::ERR_INSUFFICIENT_RESOURCES && |
| retry_delay_ < kMaxBackoffDelay) { |
| const base::TimeDelta delay = |
| GetNextExponentialBackoffDelay(retry_delay_); |
| LOG(WARNING) << "DevToolsUIBindings::NetworkResourceLoader id = " |
| << stream_id_ |
| << " failed with insufficient resources, retrying in " |
| << delay << "." << std::endl; |
| NetworkResourceLoader::Create( |
| stream_id_, bindings_, resource_request_, traffic_annotation_, |
| std::move(url_loader_factory_), std::move(callback_), delay); |
| } else { |
| auto response = base::Value(BuildObjectForResponse( |
| response_headers_.get(), success, loader_->NetError())); |
| std::move(callback_).Run(&response); |
| } |
| bindings_->loaders_.erase(bindings_->loaders_.find(this)); |
| } |
| |
| void OnRetry(base::OnceClosure start_retry) override { NOTREACHED(); } |
| |
| const int stream_id_; |
| const raw_ptr<DevToolsUIBindings> bindings_; |
| const network::ResourceRequest resource_request_; |
| const net::NetworkTrafficAnnotationTag traffic_annotation_; |
| std::unique_ptr<network::SimpleURLLoader> loader_; |
| URLLoaderFactoryHolder url_loader_factory_; |
| DispatchCallback callback_; |
| scoped_refptr<net::HttpResponseHeaders> response_headers_; |
| base::OneShotTimer timer_; |
| base::TimeDelta retry_delay_; |
| }; |
| |
| // DevToolsUIBindings::FrontendWebContentsObserver ---------------------------- |
| |
| class DevToolsUIBindings::FrontendWebContentsObserver |
| : public content::WebContentsObserver { |
| public: |
| explicit FrontendWebContentsObserver(DevToolsUIBindings* ui_bindings); |
| |
| FrontendWebContentsObserver(const FrontendWebContentsObserver&) = delete; |
| FrontendWebContentsObserver& operator=(const FrontendWebContentsObserver&) = |
| delete; |
| |
| ~FrontendWebContentsObserver() override; |
| |
| private: |
| // contents::WebContentsObserver: |
| void PrimaryMainFrameRenderProcessGone( |
| base::TerminationStatus status) override; |
| void ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) override; |
| void DocumentOnLoadCompletedInPrimaryMainFrame() override; |
| void PrimaryPageChanged(content::Page& page) override; |
| |
| raw_ptr<DevToolsUIBindings> devtools_bindings_; |
| }; |
| |
| DevToolsUIBindings::FrontendWebContentsObserver::FrontendWebContentsObserver( |
| DevToolsUIBindings* devtools_ui_bindings) |
| : WebContentsObserver(devtools_ui_bindings->web_contents()), |
| devtools_bindings_(devtools_ui_bindings) {} |
| |
| DevToolsUIBindings::FrontendWebContentsObserver:: |
| ~FrontendWebContentsObserver() = default; |
| |
| // static |
| GURL DevToolsUIBindings::SanitizeFrontendURL(const GURL& url) { |
| return ::SanitizeFrontendURL(url, content::kChromeDevToolsScheme, |
| chrome::kChromeUIDevToolsHost, |
| SanitizeFrontendPath(url.path()), true); |
| } |
| |
| // static |
| bool DevToolsUIBindings::IsValidFrontendURL(const GURL& url) { |
| if (url.SchemeIs(content::kChromeUIScheme) && |
| url.host() == content::kChromeUITracingHost && !url.has_query() && |
| !url.has_ref()) { |
| return true; |
| } |
| |
| return SanitizeFrontendURL(url).spec() == url.spec(); |
| } |
| |
| bool DevToolsUIBindings::IsValidRemoteFrontendURL(const GURL& url) { |
| return ::SanitizeFrontendURL(url, url::kHttpsScheme, kRemoteFrontendDomain, |
| url.path(), true) |
| .spec() == url.spec(); |
| } |
| |
| void DevToolsUIBindings::FrontendWebContentsObserver:: |
| PrimaryMainFrameRenderProcessGone(base::TerminationStatus status) { |
| bool crashed = true; |
| switch (status) { |
| case base::TERMINATION_STATUS_ABNORMAL_TERMINATION: |
| case base::TERMINATION_STATUS_PROCESS_WAS_KILLED: |
| #if BUILDFLAG(IS_CHROMEOS) |
| case base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM: |
| #endif |
| case base::TERMINATION_STATUS_PROCESS_CRASHED: |
| case base::TERMINATION_STATUS_LAUNCH_FAILED: |
| case base::TERMINATION_STATUS_OOM: |
| #if BUILDFLAG(IS_WIN) |
| case base::TERMINATION_STATUS_INTEGRITY_FAILURE: |
| #endif |
| if (devtools_bindings_->agent_host_.get()) { |
| devtools_bindings_->Detach(); |
| } |
| break; |
| case base::TERMINATION_STATUS_NORMAL_TERMINATION: |
| case base::TERMINATION_STATUS_STILL_RUNNING: |
| #if BUILDFLAG(IS_ANDROID) |
| case base::TERMINATION_STATUS_OOM_PROTECTED: |
| #endif |
| case base::TERMINATION_STATUS_MAX_ENUM: |
| crashed = false; |
| break; |
| } |
| devtools_bindings_->delegate_->RenderProcessGone(crashed); |
| } |
| |
| void DevToolsUIBindings::FrontendWebContentsObserver::ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) { |
| devtools_bindings_->ReadyToCommitNavigation(navigation_handle); |
| } |
| |
| void DevToolsUIBindings::FrontendWebContentsObserver:: |
| DocumentOnLoadCompletedInPrimaryMainFrame() { |
| devtools_bindings_->DocumentOnLoadCompletedInPrimaryMainFrame(); |
| } |
| |
| void DevToolsUIBindings::FrontendWebContentsObserver::PrimaryPageChanged( |
| content::Page&) { |
| devtools_bindings_->PrimaryPageChanged(); |
| } |
| |
| // DevToolsUIBindings --------------------------------------------------------- |
| |
| DevToolsUIBindings* DevToolsUIBindings::ForWebContents( |
| content::WebContents* web_contents) { |
| DevToolsUIBindingsList& instances = |
| DevToolsUIBindings::GetDevToolsUIBindings(); |
| auto binding = std::find_if( |
| instances.rbegin(), instances.rend(), |
| [&](auto binding) { return binding->web_contents() == web_contents; }); |
| return binding == instances.rend() ? nullptr : *binding; |
| } |
| |
| std::string DevToolsUIBindings::GetTypeForMetrics() { |
| return "DevTools"; |
| } |
| |
| namespace { |
| bool IsAnyAidaPoweredFeatureEnabled() { |
| return base::FeatureList::IsEnabled(::features::kDevToolsConsoleInsights) || |
| base::FeatureList::IsEnabled(::features::kDevToolsFreestyler) || |
| base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceFileAgent) || |
| base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceNetworkAgent) || |
| base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistancePerformanceAgent) || |
| base::FeatureList::IsEnabled(::features::kDevToolsAiCodeCompletion); |
| } |
| } // namespace |
| |
| DevToolsUIBindings::DevToolsUIBindings(content::WebContents* web_contents) |
| : profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), |
| web_contents_(web_contents), |
| delegate_(new DefaultBindingsDelegate(web_contents_)), |
| file_storage_(web_contents), |
| file_helper_(profile_, this, &file_storage_), |
| devices_updates_enabled_(false), |
| frontend_loaded_(false), |
| settings_(profile_), |
| http_service_registry_(std::make_unique<DevToolsHttpServiceRegistry>()) { |
| DevToolsUIBindings::GetDevToolsUIBindings().push_back(this); |
| frontend_contents_observer_ = |
| std::make_unique<FrontendWebContentsObserver>(this); |
| |
| file_system_indexer_ = new DevToolsFileSystemIndexer(); |
| |
| // Register on-load actions. |
| embedder_message_dispatcher_ = |
| DevToolsEmbedderMessageDispatcher::CreateForDevToolsFrontend(this); |
| #if !BUILDFLAG(IS_ANDROID) |
| ThemeServiceFactory::GetForProfile(profile_->GetOriginalProfile()) |
| ->AddObserver(this); |
| #endif |
| can_access_aida_ = IsAnyAidaPoweredFeatureEnabled(); |
| |
| MaybeStartLogging(); |
| } |
| |
| DevToolsUIBindings::~DevToolsUIBindings() { |
| if (!session_id_for_logging_.is_empty()) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::SessionEnd() |
| .SetTrigger(delegate_->GetClosedByForLogging()) |
| .SetTimeSinceSessionStart( |
| GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| #if !BUILDFLAG(IS_ANDROID) |
| ThemeServiceFactory::GetForProfile(profile_->GetOriginalProfile()) |
| ->RemoveObserver(this); |
| #endif |
| |
| if (agent_host_.get()) { |
| agent_host_->DetachClient(this); |
| } |
| |
| for (IndexingJobsMap::const_iterator jobs_it(indexing_jobs_.begin()); |
| jobs_it != indexing_jobs_.end(); ++jobs_it) { |
| jobs_it->second->Stop(); |
| } |
| indexing_jobs_.clear(); |
| SetDevicesUpdatesEnabled(false); |
| |
| // Remove self from global list. |
| DevToolsUIBindingsList& instances = |
| DevToolsUIBindings::GetDevToolsUIBindings(); |
| auto it = std::ranges::find(instances, this); |
| CHECK(it != instances.end()); |
| instances.erase(it); |
| } |
| |
| // content::DevToolsFrontendHost::Delegate implementation --------------------- |
| void DevToolsUIBindings::HandleMessageFromDevToolsFrontend( |
| base::Value::Dict message) { |
| if (!frontend_host_) { |
| return; |
| } |
| const std::string* method = message.FindString(kFrontendHostMethod); |
| base::Value* params = message.Find(kFrontendHostParams); |
| if (!method || (params && !params->is_list())) { |
| LOG(ERROR) << "Invalid message was sent to embedder: " << message; |
| return; |
| } |
| int id = message.FindInt(kFrontendHostId).value_or(0); |
| base::Value::List params_list; |
| if (params) { |
| params_list = std::move(*params).TakeList(); |
| } |
| embedder_message_dispatcher_->Dispatch( |
| base::BindOnce(&DevToolsUIBindings::SendMessageAck, |
| weak_factory_.GetWeakPtr(), id), |
| *method, params_list); |
| } |
| |
| // content::DevToolsAgentHostClient implementation -------------------------- |
| // There is a sibling implementation of DevToolsAgentHostClient in |
| // content/shell/browser/shell_devtools_bindings.cc |
| // that is used in layout tests, which only use content_shell. |
| // The two implementations needs to be kept in sync wrt. the interface they |
| // provide to the DevTools front-end. |
| |
| void DevToolsUIBindings::DispatchProtocolMessage( |
| content::DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) { |
| DCHECK(agent_host == agent_host_.get()); |
| if (!frontend_host_) { |
| return; |
| } |
| |
| std::string_view message_sp(reinterpret_cast<const char*>(message.data()), |
| message.size()); |
| if (message_sp.length() < kMaxMessageChunkSize) { |
| CallClientMethod("DevToolsAPI", "dispatchMessage", base::Value(message_sp)); |
| return; |
| } |
| |
| for (size_t pos = 0; pos < message_sp.length(); pos += kMaxMessageChunkSize) { |
| base::Value message_value(message_sp.substr(pos, kMaxMessageChunkSize)); |
| if (pos == 0) { |
| CallClientMethod("DevToolsAPI", "dispatchMessageChunk", |
| std::move(message_value), |
| base::Value(static_cast<int>(message_sp.length()))); |
| |
| } else { |
| CallClientMethod("DevToolsAPI", "dispatchMessageChunk", |
| std::move(message_value)); |
| } |
| } |
| } |
| |
| void DevToolsUIBindings::AgentHostClosed( |
| content::DevToolsAgentHost* agent_host) { |
| DCHECK(agent_host == agent_host_.get()); |
| agent_host_.reset(); |
| delegate_->InspectedContentsClosing(); |
| } |
| |
| bool DevToolsUIBindings::MayWriteLocalFiles() { |
| // Do not allow local file system access via the front-end on Chrome OS. |
| #if BUILDFLAG(IS_CHROMEOS) |
| return false; |
| #else |
| return true; |
| #endif |
| } |
| |
| void DevToolsUIBindings::SendMessageAck(int request_id, |
| const base::Value* arg) { |
| if (arg) { |
| CallClientMethod("DevToolsAPI", "embedderMessageAck", |
| base::Value(request_id), arg->Clone()); |
| } else { |
| CallClientMethod("DevToolsAPI", "embedderMessageAck", |
| base::Value(request_id)); |
| } |
| } |
| |
| void DevToolsUIBindings::InnerAttach() { |
| DCHECK(agent_host_.get()); |
| // TODO(dgozman): handle return value of AttachClient. |
| agent_host_->AttachClient(this); |
| } |
| |
| // DevToolsEmbedderMessageDispatcher::Delegate implementation ----------------- |
| |
| void DevToolsUIBindings::ActivateWindow() { |
| delegate_->ActivateWindow(); |
| } |
| |
| void DevToolsUIBindings::CloseWindow() { |
| delegate_->CloseWindow(); |
| } |
| |
| void DevToolsUIBindings::LoadCompleted() { |
| FrontendLoaded(); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android we don't support showing menus with custom menu info provided |
| // by blink::ContextMenuProvider. Use the soft menu to work around it. |
| CallClientMethod("DevToolsAPI", "setUseSoftMenu", base::Value(true)); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| void DevToolsUIBindings::SetInspectedPageBounds(const gfx::Rect& rect) { |
| delegate_->SetInspectedPageBounds(rect); |
| } |
| |
| void DevToolsUIBindings::SetIsDocked(DispatchCallback callback, |
| bool dock_requested) { |
| delegate_->SetIsDocked(dock_requested); |
| std::move(callback).Run(nullptr); |
| } |
| |
| void DevToolsUIBindings::HandleAidaRequestError( |
| DispatchCallback callback, |
| std::variant<network::ResourceRequest, std::string> |
| resource_request_or_error) { |
| base::Value::Dict response_dict; |
| response_dict.Set("response", |
| std::get<std::string>(resource_request_or_error)); |
| auto response_value = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response_value); |
| } |
| |
| void DevToolsUIBindings::OnAidaConversationRequest( |
| DispatchCallback callback, |
| int stream_id, |
| const std::string& request, |
| base::TimeDelta delay, |
| std::variant<network::ResourceRequest, std::string> |
| resource_request_or_error) { |
| if (std::holds_alternative<std::string>(resource_request_or_error)) { |
| HandleAidaRequestError(std::move(callback), |
| std::move(resource_request_or_error)); |
| return; |
| } |
| DevToolsUIBindings::NetworkResourceLoader::URLLoaderFactoryHolder |
| url_loader_factory; |
| url_loader_factory = profile_->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| auto resource_request = |
| std::get<network::ResourceRequest>(resource_request_or_error); |
| resource_request.url = GURL(AidaClient::kDoConversationUrl); |
| // Set a maximum timeout value, individual features may send a shorter timeout |
| // in the DevTools repo. |
| base::TimeDelta timeout = base::Seconds(120); |
| auto response_handler_callback = |
| base::BindOnce(&DevToolsUIBindings::OnAidaConversationResponse, |
| base::Unretained(this), std::move(callback), stream_id, |
| request, delay, resource_request, base::TimeTicks::Now()); |
| NetworkResourceLoader::Create( |
| stream_id, this, resource_request, |
| AidaServiceHandler::TrafficAnnotation(), std::move(url_loader_factory), |
| std::move(response_handler_callback), delay, std::move(request), timeout); |
| } |
| |
| void DevToolsUIBindings::OnAidaRequest( |
| const GURL& url, |
| const std::string& response_histogram_name, |
| DispatchCallback callback, |
| const std::string& request, |
| std::variant<network::ResourceRequest, std::string> |
| resource_request_or_error) { |
| if (std::holds_alternative<std::string>(resource_request_or_error)) { |
| HandleAidaRequestError(std::move(callback), |
| std::move(resource_request_or_error)); |
| return; |
| } |
| auto url_loader_factory = profile_->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| auto resource_request = std::make_unique<network::ResourceRequest>( |
| std::get<network::ResourceRequest>(resource_request_or_error)); |
| resource_request->url = url; |
| auto simple_url_loader = network::SimpleURLLoader::Create( |
| std::move(resource_request), AidaServiceHandler::TrafficAnnotation()); |
| simple_url_loader->AttachStringForUpload(request); |
| |
| network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get(); |
| simple_url_loader_ptr->DownloadToString( |
| url_loader_factory.get(), |
| base::BindOnce(&DevToolsUIBindings::OnAidaResponse, |
| base::Unretained(this), response_histogram_name, |
| std::move(callback), std::move(simple_url_loader), |
| base::TimeTicks::Now()), |
| network::SimpleURLLoader::kMaxBoundedStringDownloadSize); |
| } |
| |
| void DevToolsUIBindings::OnAidaConversationResponse( |
| DispatchCallback callback, |
| int stream_id, |
| const std::string request, |
| base::TimeDelta delay, |
| std::variant<network::ResourceRequest, std::string> |
| resource_request_or_error, |
| base::TimeTicks start_time, |
| const base::Value* response) { |
| int response_code = |
| response->is_dict() |
| ? response->GetDict().FindInt("statusCode").value_or(0) |
| : 0; |
| |
| if (response_code >= 500 || response_code == net::HTTP_TOO_MANY_REQUESTS) { |
| OnAidaConversationRequest( |
| std::move(callback), stream_id, request, |
| NetworkResourceLoader::GetNextExponentialBackoffDelay(delay), |
| resource_request_or_error); |
| } else if (response_code == net::HTTP_UNAUTHORIZED) { |
| aida_client_->RemoveAccessToken(); |
| aida_client_->PrepareRequestOrFail(base::BindOnce( |
| &DevToolsUIBindings::OnAidaConversationRequest, base::Unretained(this), |
| std::move(callback), stream_id, request, |
| NetworkResourceLoader::GetNextExponentialBackoffDelay(delay))); |
| } else { |
| base::UmaHistogramTimes("DevTools.AidaResponseTime", |
| base::TimeTicks::Now() - start_time); |
| std::move(callback).Run(response); |
| } |
| } |
| |
| void DevToolsUIBindings::OnAidaResponse( |
| const std::string& histogram_name, |
| DispatchCallback callback, |
| std::unique_ptr<network::SimpleURLLoader> simple_url_loader, |
| base::TimeTicks start_time, |
| std::optional<std::string> response_body) { |
| int response_code = -1; |
| if (simple_url_loader->ResponseInfo() && |
| simple_url_loader->ResponseInfo()->headers) { |
| response_code = simple_url_loader->ResponseInfo()->headers->response_code(); |
| } |
| if (response_code != net::HTTP_OK) { |
| base::Value::Dict error_dict; |
| error_dict.Set("error", "Got error response from AIDA"); |
| error_dict.Set("detail", response_body.value_or("")); |
| auto error = base::Value(std::move(error_dict)); |
| std::move(callback).Run(&error); |
| return; |
| } |
| |
| base::Value::Dict response_dict; |
| response_dict.Set("response", response_body.value_or("")); |
| auto response = base::Value(std::move(response_dict)); |
| base::UmaHistogramTimes(histogram_name, base::TimeTicks::Now() - start_time); |
| std::move(callback).Run(&response); |
| } |
| |
| void DevToolsUIBindings::DispatchHttpRequest( |
| DispatchCallback callback, |
| const DevToolsDispatchHttpRequestParams& params) { |
| http_service_registry_->Request( |
| profile_, params, |
| base::BindOnce(&DevToolsUIBindings::OnHttpRequestPerformed, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void DevToolsUIBindings::OnHttpRequestPerformed( |
| DispatchCallback callback, |
| std::unique_ptr<DevToolsHttpServiceHandler::Result> result) { |
| base::Value::Dict response_dict; |
| using Error = DevToolsHttpServiceHandler::Result::Error; |
| switch (result->error) { |
| case Error::kNone: |
| response_dict.Set("response", result->response_body.value_or("")); |
| response_dict.Set("statusCode", result->http_status); |
| break; |
| case Error::kServiceNotFound: |
| response_dict.Set("error", "Service not found"); |
| break; |
| case Error::kAccessDenied: |
| response_dict.Set("error", "Disallowed path or method"); |
| break; |
| case Error::kValidationFailed: |
| response_dict.Set("error", "Request validation failed"); |
| break; |
| case Error::kTokenFetchFailed: |
| response_dict.Set("error", "Token fetch error"); |
| response_dict.Set("detail", result->error_detail); |
| break; |
| case Error::kNetworkError: |
| case Error::kHttpError: |
| response_dict.Set("error", "Request failed"); |
| response_dict.Set("detail", result->response_body.value_or("")); |
| response_dict.Set("netError", result->net_error); |
| response_dict.Set("netErrorName", net::ErrorToString(result->net_error)); |
| response_dict.Set("statusCode", result->http_status); |
| break; |
| } |
| auto response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| } |
| |
| void DevToolsUIBindings::InspectElementCompleted() { |
| delegate_->InspectElementCompleted(); |
| } |
| |
| void DevToolsUIBindings::InspectedURLChanged(const std::string& url) { |
| content::NavigationController& controller = web_contents()->GetController(); |
| content::NavigationEntry* entry = controller.GetActiveEntry(); |
| |
| const std::string kHttpPrefix = "http://"; |
| const std::string kHttpsPrefix = "https://"; |
| const std::string simplified_url = |
| base::StartsWith(url, kHttpsPrefix, base::CompareCase::SENSITIVE) |
| ? url.substr(kHttpsPrefix.length()) |
| : base::StartsWith(url, kHttpPrefix, base::CompareCase::SENSITIVE) |
| ? url.substr(kHttpPrefix.length()) |
| : url; |
| // DevTools UI is not localized. |
| web_contents()->UpdateTitleForEntry( |
| entry, base::UTF8ToUTF16( |
| base::StringPrintf(kTitleFormat, simplified_url.c_str()))); |
| } |
| |
| void DevToolsUIBindings::LoadNetworkResource(DispatchCallback callback, |
| const std::string& url, |
| const std::string& headers, |
| int stream_id) { |
| GURL gurl(url); |
| if (!gurl.is_valid()) { |
| base::Value::Dict response_dict; |
| response_dict.Set("statusCode", 404); |
| response_dict.Set("urlValid", false); |
| auto response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| return; |
| } |
| // Create traffic annotation tag. |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("devtools_network_resource", R"( |
| semantics { |
| sender: "Developer Tools" |
| description: |
| "When user opens Developer Tools, the browser may fetch additional " |
| "resources from the network to enrich the debugging experience " |
| "(e.g. source map resources)." |
| trigger: "User opens Developer Tools to debug a web page." |
| data: "Any resources requested by Developer Tools." |
| destination: WEBSITE |
| internal { |
| contacts { |
| email: "chrome-devtools@google.com" |
| } |
| } |
| user_data { |
| type: WEB_CONTENT |
| } |
| last_reviewed: "2024-02-09" |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "It's not possible to disable this feature from settings." |
| chrome_policy { |
| DeveloperToolsAvailability { |
| policy_options {mode: MANDATORY} |
| DeveloperToolsAvailability: 2 |
| } |
| } |
| })"); |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = gurl; |
| // TODO(caseq): this preserves behavior of URLFetcher-based implementation. |
| // We really need to pass proper first party origin from the front-end. |
| resource_request.site_for_cookies = net::SiteForCookies::FromUrl(gurl); |
| resource_request.headers.AddHeadersFromString(headers); |
| |
| NetworkResourceLoader::URLLoaderFactoryHolder url_loader_factory; |
| if (gurl.SchemeIsFile()) { |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote = |
| content::CreateFileURLLoaderFactory( |
| base::FilePath() /* profile_path */, |
| nullptr /* shared_cors_origin_access_list */); |
| url_loader_factory = network::SharedURLLoaderFactory::Create( |
| std::make_unique<network::WrapperPendingSharedURLLoaderFactory>( |
| std::move(pending_remote))); |
| } else if (content::HasWebUIScheme(gurl)) { |
| content::WebContents* target_tab = delegate_->GetInspectedWebContents(); |
| #if defined(NDEBUG) |
| // In release builds, allow files from the chrome://, devtools:// and |
| // chrome-untrusted:// schemes if a custom devtools front-end was specified. |
| const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); |
| const bool allow_web_ui_scheme = |
| cmd_line->HasSwitch(switches::kCustomDevtoolsFrontend); |
| #else |
| // In debug builds, always allow retrieving files from the chrome://, |
| // devtools:// and chrome-untrusted:// schemes. |
| const bool allow_web_ui_scheme = true; |
| #endif |
| // Only allow retrieval if the scheme of the file is the same as the |
| // top-level frame of the inspected page. |
| // TODO(sigurds): Track which frame triggered the load, match schemes to the |
| // committed URL of that frame, and use the loader associated with that |
| // frame to allow nested frames with different schemes to load files. |
| if (allow_web_ui_scheme && target_tab && |
| target_tab->GetLastCommittedURL().scheme() == gurl.scheme()) { |
| std::vector<std::string> allowed_webui_hosts; |
| content::RenderFrameHost* frame_host = |
| web_contents()->GetPrimaryMainFrame(); |
| |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote = |
| content::CreateWebUIURLLoaderFactory( |
| frame_host, target_tab->GetLastCommittedURL().scheme(), |
| std::move(allowed_webui_hosts)); |
| url_loader_factory = network::SharedURLLoaderFactory::Create( |
| std::make_unique<network::WrapperPendingSharedURLLoaderFactory>( |
| std::move(pending_remote))); |
| } else { |
| base::Value::Dict response_dict; |
| response_dict.Set("schemeSupported", false); |
| response_dict.Set("statusCode", 403); |
| auto response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| return; |
| } |
| } else { |
| content::WebContents* target_tab = delegate_->GetInspectedWebContents(); |
| if (target_tab) { |
| auto* partition = |
| target_tab->GetPrimaryMainFrame()->GetStoragePartition(); |
| url_loader_factory = partition->GetURLLoaderFactoryForBrowserProcess(); |
| } else { |
| base::Value::Dict response_dict; |
| response_dict.Set("statusCode", 409); |
| auto response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| return; |
| } |
| } |
| |
| NetworkResourceLoader::Create( |
| stream_id, this, resource_request, traffic_annotation, |
| std::move(url_loader_factory), std::move(callback)); |
| } |
| |
| void DevToolsUIBindings::OpenInNewTab(const std::string& url) { |
| delegate_->OpenInNewTab(url); |
| } |
| |
| void DevToolsUIBindings::OpenSearchResultsInNewTab(const std::string& query) { |
| delegate_->OpenSearchResultsInNewTab(query); |
| } |
| |
| void DevToolsUIBindings::ShowItemInFolder(const std::string& file_system_path) { |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| file_helper_.ShowItemInFolder(file_system_path); |
| } |
| |
| void DevToolsUIBindings::SaveToFile(const std::string& url, |
| const std::string& content, |
| bool save_as, |
| bool is_base64) { |
| file_helper_.Save( |
| url, content, save_as, is_base64, |
| base::BindOnce(&DevToolsSelectFileDialog::SelectFile, web_contents_, |
| ui::SelectFileDialog::SELECT_SAVEAS_FILE), |
| base::BindOnce(&DevToolsUIBindings::FileSavedAs, |
| weak_factory_.GetWeakPtr(), url), |
| base::BindOnce(&DevToolsUIBindings::CanceledFileSaveAs, |
| weak_factory_.GetWeakPtr(), url)); |
| } |
| |
| void DevToolsUIBindings::AppendToFile(const std::string& url, |
| const std::string& content) { |
| file_helper_.Append(url, content, |
| base::BindOnce(&DevToolsUIBindings::AppendedTo, |
| weak_factory_.GetWeakPtr(), url)); |
| } |
| |
| void DevToolsUIBindings::RequestFileSystems() { |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| base::Value::List file_systems_value; |
| for (auto const& file_system : file_helper_.GetFileSystems()) { |
| file_systems_value.Append(CreateFileSystemValue(file_system)); |
| } |
| CallClientMethod("DevToolsAPI", "fileSystemsLoaded", |
| base::Value(std::move(file_systems_value))); |
| } |
| |
| void DevToolsUIBindings::AddFileSystem(const std::string& type) { |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| file_helper_.AddFileSystem( |
| type, |
| base::BindOnce(&DevToolsSelectFileDialog::SelectFile, web_contents_, |
| ui::SelectFileDialog::SELECT_FOLDER), |
| base::BindRepeating(&DevToolsUIBindings::HandleDirectoryPermissions, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void DevToolsUIBindings::RemoveFileSystem(const std::string& file_system_path) { |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| file_helper_.RemoveFileSystem(file_system_path); |
| } |
| |
| void DevToolsUIBindings::UpgradeDraggedFileSystemPermissions( |
| const std::string& file_system_url) { |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| file_helper_.UpgradeDraggedFileSystemPermissions( |
| file_system_url, |
| base::BindRepeating(&DevToolsUIBindings::HandleDirectoryPermissions, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void DevToolsUIBindings::ConnectAutomaticFileSystem( |
| DispatchCallback callback, |
| const std::string& file_system_path, |
| const std::string& file_system_uuid, |
| bool add_if_missing) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| // Ensure that the |file_system_uuid| is indeed a valid UUID. |
| base::Uuid uuid = base::Uuid::ParseCaseInsensitive(file_system_uuid); |
| if (!uuid.is_valid()) { |
| LOG(ERROR) << "Rejecting automatic file system " << file_system_path |
| << " with invalid UUID " << file_system_uuid << "."; |
| ConnectAutomaticFileSystemDone(std::move(callback), false); |
| return; |
| } |
| |
| file_helper_.ConnectAutomaticFileSystem( |
| file_system_path, uuid, add_if_missing, |
| BindRepeating(&DevToolsUIBindings::HandleDirectoryPermissions, |
| weak_factory_.GetWeakPtr()), |
| BindOnce(&DevToolsUIBindings::ConnectAutomaticFileSystemDone, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void DevToolsUIBindings::ConnectAutomaticFileSystemDone( |
| DispatchCallback callback, |
| bool success) { |
| base::Value::Dict result_dict; |
| result_dict.Set("success", success); |
| base::Value result(std::move(result_dict)); |
| std::move(callback).Run(&result); |
| } |
| |
| void DevToolsUIBindings::DisconnectAutomaticFileSystem( |
| const std::string& file_system_path) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| file_helper_.DisconnectAutomaticFileSystem(file_system_path); |
| } |
| |
| void DevToolsUIBindings::IndexPath( |
| int index_request_id, |
| const std::string& file_system_path, |
| const std::string& excluded_folders_message) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| if (!file_helper_.IsFileSystemAdded(file_system_path)) { |
| IndexingDone(index_request_id, file_system_path); |
| return; |
| } |
| if (indexing_jobs_.count(index_request_id) != 0) { |
| return; |
| } |
| std::vector<std::string> excluded_folders; |
| std::optional<base::Value> parsed_excluded_folders = |
| base::JSONReader::Read(excluded_folders_message); |
| if (parsed_excluded_folders && parsed_excluded_folders->is_list()) { |
| for (const base::Value& folder_path : parsed_excluded_folders->GetList()) { |
| if (folder_path.is_string()) { |
| excluded_folders.push_back(folder_path.GetString()); |
| } |
| } |
| } |
| |
| indexing_jobs_[index_request_id] = |
| scoped_refptr<DevToolsFileSystemIndexer::FileSystemIndexingJob>( |
| file_system_indexer_->IndexPath( |
| file_system_path, excluded_folders, |
| BindOnce(&DevToolsUIBindings::IndexingTotalWorkCalculated, |
| weak_factory_.GetWeakPtr(), index_request_id, |
| file_system_path), |
| BindRepeating(&DevToolsUIBindings::IndexingWorked, |
| weak_factory_.GetWeakPtr(), index_request_id, |
| file_system_path), |
| BindOnce(&DevToolsUIBindings::IndexingDone, |
| weak_factory_.GetWeakPtr(), index_request_id, |
| file_system_path))); |
| } |
| |
| void DevToolsUIBindings::StopIndexing(int index_request_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto it = indexing_jobs_.find(index_request_id); |
| if (it == indexing_jobs_.end()) { |
| return; |
| } |
| it->second->Stop(); |
| indexing_jobs_.erase(it); |
| } |
| |
| void DevToolsUIBindings::SearchInPath(int search_request_id, |
| const std::string& file_system_path, |
| const std::string& query) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && |
| frontend_host_); |
| if (!file_helper_.IsFileSystemAdded(file_system_path)) { |
| SearchCompleted(search_request_id, file_system_path, |
| std::vector<std::string>()); |
| return; |
| } |
| file_system_indexer_->SearchInPath( |
| file_system_path, query, |
| BindOnce(&DevToolsUIBindings::SearchCompleted, weak_factory_.GetWeakPtr(), |
| search_request_id, file_system_path)); |
| } |
| |
| void DevToolsUIBindings::SetWhitelistedShortcuts(const std::string& message) { |
| delegate_->SetWhitelistedShortcuts(message); |
| } |
| |
| void DevToolsUIBindings::SetEyeDropperActive(bool active) { |
| delegate_->SetEyeDropperActive(active); |
| } |
| |
| void DevToolsUIBindings::ShowCertificateViewer(const std::string& cert_chain) { |
| delegate_->ShowCertificateViewer(cert_chain); |
| } |
| |
| void DevToolsUIBindings::ZoomIn() { |
| zoom::PageZoom::Zoom(web_contents(), content::PAGE_ZOOM_IN); |
| } |
| |
| void DevToolsUIBindings::ZoomOut() { |
| zoom::PageZoom::Zoom(web_contents(), content::PAGE_ZOOM_OUT); |
| } |
| |
| void DevToolsUIBindings::ResetZoom() { |
| zoom::PageZoom::Zoom(web_contents(), content::PAGE_ZOOM_RESET); |
| } |
| |
| void DevToolsUIBindings::SetDevicesDiscoveryConfig( |
| bool discover_usb_devices, |
| bool port_forwarding_enabled, |
| const std::string& port_forwarding_config, |
| bool network_discovery_enabled, |
| const std::string& network_discovery_config) { |
| std::optional<base::Value::Dict> parsed_port_forwarding = |
| base::JSONReader::ReadDict(port_forwarding_config); |
| if (!parsed_port_forwarding) { |
| return; |
| } |
| std::optional<base::Value> parsed_network = |
| base::JSONReader::Read(network_discovery_config); |
| if (!parsed_network || !parsed_network->is_list()) { |
| return; |
| } |
| profile_->GetPrefs()->SetBoolean(prefs::kDevToolsDiscoverUsbDevicesEnabled, |
| discover_usb_devices); |
| profile_->GetPrefs()->SetBoolean(prefs::kDevToolsPortForwardingEnabled, |
| port_forwarding_enabled); |
| profile_->GetPrefs()->Set(prefs::kDevToolsPortForwardingConfig, |
| base::Value(std::move(*parsed_port_forwarding))); |
| profile_->GetPrefs()->SetBoolean(prefs::kDevToolsDiscoverTCPTargetsEnabled, |
| network_discovery_enabled); |
| profile_->GetPrefs()->Set(prefs::kDevToolsTCPDiscoveryConfig, |
| *parsed_network); |
| } |
| |
| void DevToolsUIBindings::DevicesDiscoveryConfigUpdated() { |
| base::Value::Dict config; |
| config.Set(kConfigDiscoverUsbDevices, |
| profile_->GetPrefs() |
| ->FindPreference(prefs::kDevToolsDiscoverUsbDevicesEnabled) |
| ->GetValue() |
| ->Clone()); |
| config.Set(kConfigPortForwardingEnabled, |
| profile_->GetPrefs() |
| ->FindPreference(prefs::kDevToolsPortForwardingEnabled) |
| ->GetValue() |
| ->Clone()); |
| config.Set(kConfigPortForwardingConfig, |
| profile_->GetPrefs() |
| ->FindPreference(prefs::kDevToolsPortForwardingConfig) |
| ->GetValue() |
| ->Clone()); |
| config.Set(kConfigNetworkDiscoveryEnabled, |
| profile_->GetPrefs() |
| ->FindPreference(prefs::kDevToolsDiscoverTCPTargetsEnabled) |
| ->GetValue() |
| ->Clone()); |
| config.Set(kConfigNetworkDiscoveryConfig, |
| profile_->GetPrefs() |
| ->FindPreference(prefs::kDevToolsTCPDiscoveryConfig) |
| ->GetValue() |
| ->Clone()); |
| CallClientMethod("DevToolsAPI", "devicesDiscoveryConfigChanged", |
| base::Value(std::move(config))); |
| } |
| |
| void DevToolsUIBindings::SendPortForwardingStatus(base::Value status) { |
| CallClientMethod("DevToolsAPI", "devicesPortForwardingStatusChanged", |
| std::move(status)); |
| } |
| |
| void DevToolsUIBindings::SetDevicesUpdatesEnabled(bool enabled) { |
| #if BUILDFLAG(IS_ANDROID) |
| NOTIMPLEMENTED(); |
| #else |
| if (devices_updates_enabled_ == enabled) { |
| return; |
| } |
| devices_updates_enabled_ = enabled; |
| if (enabled) { |
| remote_targets_handler_ = DevToolsTargetsUIHandler::CreateForAdb( |
| base::BindRepeating(&DevToolsUIBindings::DevicesUpdated, |
| base::Unretained(this)), |
| profile_); |
| pref_change_registrar_.Init(profile_->GetPrefs()); |
| pref_change_registrar_.Add( |
| prefs::kDevToolsDiscoverUsbDevicesEnabled, |
| base::BindRepeating(&DevToolsUIBindings::DevicesDiscoveryConfigUpdated, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kDevToolsPortForwardingEnabled, |
| base::BindRepeating(&DevToolsUIBindings::DevicesDiscoveryConfigUpdated, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kDevToolsPortForwardingConfig, |
| base::BindRepeating(&DevToolsUIBindings::DevicesDiscoveryConfigUpdated, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kDevToolsDiscoverTCPTargetsEnabled, |
| base::BindRepeating(&DevToolsUIBindings::DevicesDiscoveryConfigUpdated, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kDevToolsTCPDiscoveryConfig, |
| base::BindRepeating(&DevToolsUIBindings::DevicesDiscoveryConfigUpdated, |
| base::Unretained(this))); |
| port_status_serializer_ = std::make_unique<PortForwardingStatusSerializer>( |
| base::BindRepeating(&DevToolsUIBindings::SendPortForwardingStatus, |
| base::Unretained(this)), |
| profile_); |
| DevicesDiscoveryConfigUpdated(); |
| } else { |
| remote_targets_handler_.reset(); |
| port_status_serializer_.reset(); |
| pref_change_registrar_.RemoveAll(); |
| SendPortForwardingStatus(base::Value()); |
| } |
| #endif |
| } |
| |
| void DevToolsUIBindings::OpenRemotePage(const std::string& browser_id, |
| const std::string& url) { |
| if (!remote_targets_handler_) { |
| return; |
| } |
| remote_targets_handler_->Open(browser_id, url); |
| } |
| |
| void DevToolsUIBindings::OpenNodeFrontend() { |
| delegate_->OpenNodeFrontend(); |
| } |
| |
| void DevToolsUIBindings::RegisterPreference(const std::string& name, |
| const RegisterOptions& options) { |
| settings_.Register(name, options); |
| } |
| |
| void DevToolsUIBindings::GetPreferences(DispatchCallback callback) { |
| base::Value settings = base::Value(settings_.Get()); |
| std::move(callback).Run(&settings); |
| } |
| |
| void DevToolsUIBindings::GetPreference(DispatchCallback callback, |
| const std::string& name) { |
| base::Value pref = settings_.Get(name).value_or(base::Value()); |
| std::move(callback).Run(&pref); |
| } |
| |
| void DevToolsUIBindings::SetPreference(const std::string& name, |
| const std::string& value) { |
| settings_.Set(name, value); |
| } |
| |
| void DevToolsUIBindings::RemovePreference(const std::string& name) { |
| settings_.Remove(name); |
| } |
| |
| void DevToolsUIBindings::ClearPreferences() { |
| settings_.Clear(); |
| } |
| |
| void DevToolsUIBindings::GetSyncInformation(DispatchCallback callback) { |
| auto result = |
| base::Value(DevToolsUIBindings::GetSyncInformationForProfile(profile_)); |
| std::move(callback).Run(&result); |
| } |
| |
| base::Value::Dict DevToolsUIBindings::GetSyncInformationForProfile( |
| Profile* profile) { |
| base::Value::Dict result; |
| syncer::SyncService* sync_service = |
| SyncServiceFactory::GetForProfile(profile); |
| if (!sync_service) { |
| result.Set("isSyncActive", false); |
| return result; |
| } |
| |
| result.Set("isSyncActive", sync_service->IsSyncFeatureActive()); |
| result.Set("arePreferencesSynced", sync_service->GetActiveDataTypes().Has( |
| syncer::DataType::PREFERENCES)); |
| result.Set("isSyncPaused", sync_service->GetTransportState() == |
| syncer::SyncService::TransportState::PAUSED); |
| |
| CoreAccountInfo account_info = sync_service->GetAccountInfo(); |
| if (account_info.IsEmpty()) { |
| return result; |
| } |
| |
| result.Set("accountEmail", account_info.email); |
| |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile); |
| AccountInfo extended_info = |
| identity_manager->FindExtendedAccountInfo(account_info); |
| gfx::Image account_image; |
| if (extended_info.IsEmpty() || extended_info.account_image.IsEmpty()) { |
| account_image = ui::ResourceBundle::GetSharedInstance().GetImageNamed( |
| profiles::GetPlaceholderAvatarIconResourceID()); |
| } else { |
| account_image = extended_info.account_image; |
| } |
| scoped_refptr<base::RefCountedMemory> png_bytes = |
| account_image.As1xPNGBytes(); |
| if (png_bytes->size() > 0) { |
| result.Set("accountImage", base::Base64Encode(*png_bytes)); |
| } |
| |
| if (!extended_info.IsEmpty()) { |
| result.Set("accountFullName", extended_info.full_name); |
| } |
| |
| return result; |
| } |
| |
| void DevToolsUIBindings::GetHostConfig(DispatchCallback callback) { |
| base::Value::Dict response_dict; |
| |
| AidaClient::Availability availability = AidaClient::CanUseAida(profile_); |
| |
| base::Value::Dict aida_availability; |
| aida_availability.Set("enabled", availability.available); |
| aida_availability.Set("blockedByAge", availability.blocked_by_age); |
| aida_availability.Set("blockedByEnterprisePolicy", |
| availability.blocked_by_enterprise_policy); |
| aida_availability.Set("blockedByGeo", availability.blocked_by_geo); |
| aida_availability.Set("disallowLogging", availability.disallow_logging); |
| aida_availability.Set("enterprisePolicyValue", |
| static_cast<int>(availability.enterprise_policy_value)); |
| response_dict.Set("aidaAvailability", std::move(aida_availability)); |
| |
| version_info::Channel channel = chrome::GetChannel(); |
| if (channel != version_info::Channel::UNKNOWN) { |
| response_dict.Set("channel", version_info::GetChannelString(channel)); |
| } |
| |
| base::Value::Dict console_insights_dict; |
| console_insights_dict.Set( |
| "enabled", |
| base::FeatureList::IsEnabled(::features::kDevToolsConsoleInsights)); |
| console_insights_dict.Set("modelId", |
| features::kDevToolsConsoleInsightsModelId.Get()); |
| console_insights_dict.Set( |
| "temperature", features::kDevToolsConsoleInsightsTemperature.Get()); |
| response_dict.Set("devToolsConsoleInsights", |
| std::move(console_insights_dict)); |
| |
| if (base::FeatureList::IsEnabled(::features::kDevToolsFreestyler)) { |
| base::Value::Dict freestyler_dict; |
| freestyler_dict.Set("enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsFreestyler)); |
| freestyler_dict.Set("featureName", ::features::kDevToolsFreestyler.name); |
| freestyler_dict.Set("modelId", features::kDevToolsFreestylerModelId.Get()); |
| freestyler_dict.Set("temperature", |
| features::kDevToolsFreestylerTemperature.Get()); |
| freestyler_dict.Set("userTier", |
| features::kDevToolsFreestylerUserTier.GetName( |
| features::kDevToolsFreestylerUserTier.Get())); |
| freestyler_dict.Set("executionMode", |
| features::kDevToolsFreestylerExecutionMode.GetName( |
| features::kDevToolsFreestylerExecutionMode.Get())); |
| freestyler_dict.Set("patching", |
| features::kDevToolsFreestylerPatching.Get()); |
| freestyler_dict.Set("multimodal", |
| features::kDevToolsFreestylerMultimodal.Get()); |
| freestyler_dict.Set( |
| "multimodalUploadInput", |
| features::kDevToolsFreestylerMultimodalUploadInput.Get()); |
| freestyler_dict.Set("functionCalling", |
| features::kDevToolsFreestylerFunctionCalling.Get()); |
| response_dict.Set("devToolsFreestyler", std::move(freestyler_dict)); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceNetworkAgent)) { |
| base::Value::Dict network_agent_dict; |
| network_agent_dict.Set("enabled", |
| base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceNetworkAgent)); |
| network_agent_dict.Set("featureName", |
| ::features::kDevToolsAiAssistanceNetworkAgent.name); |
| network_agent_dict.Set( |
| "modelId", features::kDevToolsAiAssistanceNetworkAgentModelId.Get()); |
| network_agent_dict.Set( |
| "temperature", |
| features::kDevToolsAiAssistanceNetworkAgentTemperature.Get()); |
| network_agent_dict.Set( |
| "userTier", |
| features::kDevToolsAiAssistanceNetworkAgentUserTier.GetName( |
| features::kDevToolsAiAssistanceNetworkAgentUserTier.Get())); |
| response_dict.Set("devToolsAiAssistanceNetworkAgent", |
| std::move(network_agent_dict)); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistancePerformanceAgent)) { |
| base::Value::Dict ai_assistance_performance_agent_dict; |
| ai_assistance_performance_agent_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistancePerformanceAgent)); |
| ai_assistance_performance_agent_dict.Set( |
| "featureName", ::features::kDevToolsAiAssistancePerformanceAgent.name); |
| ai_assistance_performance_agent_dict.Set( |
| "modelId", |
| features::kDevToolsAiAssistancePerformanceAgentModelId.Get()); |
| ai_assistance_performance_agent_dict.Set( |
| "temperature", |
| features::kDevToolsAiAssistancePerformanceAgentTemperature.Get()); |
| ai_assistance_performance_agent_dict.Set( |
| "userTier", |
| features::kDevToolsAiAssistancePerformanceAgentUserTier.GetName( |
| features::kDevToolsAiAssistancePerformanceAgentUserTier.Get())); |
| ai_assistance_performance_agent_dict.Set( |
| "insightsEnabled", |
| features::kDevToolsAiAssistancePerformanceAgentInsightsEnabled.Get()); |
| response_dict.Set("devToolsAiAssistancePerformanceAgent", |
| std::move(ai_assistance_performance_agent_dict)); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceFileAgent)) { |
| base::Value::Dict ai_assistance_file_agent_dict; |
| ai_assistance_file_agent_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiAssistanceFileAgent)); |
| ai_assistance_file_agent_dict.Set("featureName", |
| ::features::kDevToolsAiAssistanceFileAgent.name); |
| ai_assistance_file_agent_dict.Set( |
| "modelId", features::kDevToolsAiAssistanceFileAgentModelId.Get()); |
| ai_assistance_file_agent_dict.Set( |
| "temperature", |
| features::kDevToolsAiAssistanceFileAgentTemperature.Get()); |
| ai_assistance_file_agent_dict.Set( |
| "userTier", |
| features::kDevToolsAiAssistanceFileAgentUserTier.GetName( |
| features::kDevToolsAiAssistanceFileAgentUserTier.Get())); |
| response_dict.Set("devToolsAiAssistanceFileAgent", |
| std::move(ai_assistance_file_agent_dict)); |
| } |
| |
| if (base::FeatureList::IsEnabled(::features::kDevToolsAiCodeCompletion)) { |
| base::Value::Dict ai_code_completion_dict; |
| ai_code_completion_dict.Set( |
| "enabled", |
| base::FeatureList::IsEnabled(::features::kDevToolsAiCodeCompletion)); |
| ai_code_completion_dict.Set( |
| "modelId", features::kDevToolsAiCodeCompletionModelId.Get()); |
| ai_code_completion_dict.Set( |
| "temperature", features::kDevToolsAiCodeCompletionTemperature.Get()); |
| ai_code_completion_dict.Set( |
| "userTier", |
| features::kDevToolsAiCodeCompletionUserTier.GetName( |
| features::kDevToolsAiCodeCompletionUserTier.Get())); |
| response_dict.Set("devToolsAiCodeCompletion", |
| std::move(ai_code_completion_dict)); |
| } |
| |
| base::Value::Dict devtools_well_known_dict; |
| devtools_well_known_dict.Set( |
| "enabled", base::FeatureList::IsEnabled(::features::kDevToolsWellKnown)); |
| response_dict.Set("devToolsWellKnown", std::move(devtools_well_known_dict)); |
| |
| base::Value::Dict ve_logging_dict; |
| ve_logging_dict.Set("enabled", true); |
| ve_logging_dict.Set("testing", false); |
| response_dict.Set("devToolsVeLogging", std::move(ve_logging_dict)); |
| |
| response_dict.Set("isOffTheRecord", profile_->IsOffTheRecord()); |
| |
| base::Value::Dict devtools_privacy_ui_dict; |
| devtools_privacy_ui_dict.Set( |
| "enabled", base::FeatureList::IsEnabled(::features::kDevToolsPrivacyUI)); |
| response_dict.Set("devToolsPrivacyUI", std::move(devtools_privacy_ui_dict)); |
| |
| if (base::FeatureList::IsEnabled(features::kDevToolsPrivacyUI)) { |
| base::Value::Dict third_party_cookie_controls_dict; |
| third_party_cookie_controls_dict.Set( |
| "thirdPartyCookieRestrictionEnabled", |
| TrackingProtectionSettingsFactory::GetForProfile(profile()) |
| ->IsTrackingProtection3pcdEnabled()); |
| |
| third_party_cookie_controls_dict.Set( |
| "thirdPartyCookieMetadataEnabled", |
| base::FeatureList::IsEnabled(net::features::kTpcdMetadataGrants)); |
| third_party_cookie_controls_dict.Set( |
| "thirdPartyCookieHeuristicsEnabled", |
| base::FeatureList::IsEnabled( |
| content_settings::features::kTpcdHeuristicsGrants)); |
| |
| policy::PolicyService* policy_service = |
| profile()->GetProfilePolicyConnector()->policy_service(); |
| CHECK(policy_service); |
| const policy::PolicyMap& policies = policy_service->GetPolicies( |
| policy::PolicyNamespace(policy::POLICY_DOMAIN_CHROME, std::string())); |
| const base::Value* block_third_party_cookies = policies.GetValue( |
| policy::key::kBlockThirdPartyCookies, base::Value::Type::BOOLEAN); |
| if (block_third_party_cookies && block_third_party_cookies->is_bool()) { |
| third_party_cookie_controls_dict.Set( |
| "managedBlockThirdPartyCookies", |
| block_third_party_cookies->GetBool()); |
| } else { |
| third_party_cookie_controls_dict.Set("managedBlockThirdPartyCookies", |
| "Unset"); |
| } |
| // TODO: Add enterprise policy CookiesAllowedForUrls. |
| |
| response_dict.Set("thirdPartyCookieControls", |
| std::move(third_party_cookie_controls_dict)); |
| } |
| base::Value::Dict origin_bound_cookies_dict; |
| origin_bound_cookies_dict.Set( |
| "portBindingEnabled", |
| base::FeatureList::IsEnabled(net::features::kEnablePortBoundCookies)); |
| origin_bound_cookies_dict.Set( |
| "schemeBindingEnabled", |
| base::FeatureList::IsEnabled(net::features::kEnableSchemeBoundCookies)); |
| response_dict.Set("devToolsEnableOriginBoundCookies", |
| std::move(origin_bound_cookies_dict)); |
| |
| if (base::FeatureList::IsEnabled( |
| ::features::kDevToolsAnimationStylesInStylesTab)) { |
| base::Value::Dict devtools_animation_styles_in_styles_tab_dict; |
| devtools_animation_styles_in_styles_tab_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAnimationStylesInStylesTab)); |
| response_dict.Set("devToolsAnimationStylesInStylesTab", |
| std::move(devtools_animation_styles_in_styles_tab_dict)); |
| } |
| |
| if (net::features::kIpPrivacyEnableIppInDevTools.Get()) { |
| response_dict.Set("devToolsIpProtectionInDevTools", |
| base::Value::Dict().Set("enabled", true)); |
| } |
| |
| if (net::features::kIpPrivacyEnableIppPanelInDevTools.Get()) { |
| response_dict.Set("devToolsIpProtectionPanelInDevTools", |
| base::Value::Dict().Set("enabled", true)); |
| } |
| |
| base::Value::Dict deep_links_via_extensibility_api_dict; |
| deep_links_via_extensibility_api_dict.Set( |
| "enabled", |
| base::FeatureList::IsEnabled( |
| ::blink::features::kEnableDevtoolsDeepLinkViaExtensibilityApi)); |
| response_dict.Set("devToolsDeepLinksViaExtensibilityApi", |
| std::move(deep_links_via_extensibility_api_dict)); |
| |
| base::Value::Dict ai_generated_timeline_labels_dict; |
| ai_generated_timeline_labels_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiGeneratedTimelineLabels)); |
| response_dict.Set("devToolsAiGeneratedTimelineLabels", |
| std::move(ai_generated_timeline_labels_dict)); |
| |
| base::Value::Dict devtools_force_popover_dict; |
| devtools_force_popover_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| blink::features::kDevToolsAllowPopoverForcing)); |
| response_dict.Set("devToolsAllowPopoverForcing", |
| std::move(devtools_force_popover_dict)); |
| |
| base::Value::Dict flexible_layout_dict; |
| flexible_layout_dict.Set( |
| "verticalDrawerEnabled", |
| base::FeatureList::IsEnabled(::features::kDevToolsVerticalDrawer)); |
| response_dict.Set("devToolsFlexibleLayout", std::move(flexible_layout_dict)); |
| |
| base::Value::Dict ai_submenu_prompts_dict; |
| ai_submenu_prompts_dict.Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiSubmenuPrompts)); |
| ai_submenu_prompts_dict.Set("featureName", |
| ::features::kDevToolsAiSubmenuPrompts.name); |
| response_dict.Set("devToolsAiSubmenuPrompts", |
| std::move(ai_submenu_prompts_dict)); |
| |
| base::Value::Dict ai_debug_with_ai_dict; |
| ai_debug_with_ai_dict.Set("enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsAiDebugWithAi)); |
| ai_debug_with_ai_dict.Set("featureName", |
| ::features::kDevToolsAiDebugWithAi.name); |
| response_dict.Set("devToolsAiDebugWithAi", std::move(ai_debug_with_ai_dict)); |
| |
| if (base::FeatureList::IsEnabled(::features::kDevToolsGlobalAiButton)) { |
| base::Value::Dict global_ai_button_dict; |
| global_ai_button_dict.Set( |
| "enabled", |
| base::FeatureList::IsEnabled(::features::kDevToolsGlobalAiButton)); |
| global_ai_button_dict.Set( |
| "promotionEnabled", |
| features::kDevToolsGlobalAiButtonPromotionEnabled.Get()); |
| response_dict.Set("devToolsGlobalAiButton", |
| std::move(global_ai_button_dict)); |
| } |
| |
| // Once the feature is fully launched and the base::Features are enabled by |
| // default, this dict can be removed. |
| if (base::FeatureList::IsEnabled(::features::kDevToolsGdpProfiles)) { |
| base::Value::Dict gdp_profiles_dict; |
| gdp_profiles_dict.Set("enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsGdpProfiles)); |
| gdp_profiles_dict.Set( |
| "starterBadgeEnabled", |
| features::kDevToolsGdpProfilesStarterBadgeEnabled.Get()); |
| response_dict.Set("devToolsGdpProfiles", std::move(gdp_profiles_dict)); |
| } |
| |
| base::Value::Dict gdp_profiles_availability_dict; |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| gdp_profiles_availability_dict.Set("enabled", true); |
| #else |
| gdp_profiles_availability_dict.Set("enabled", false); |
| #endif |
| gdp_profiles_availability_dict.Set( |
| "enterprisePolicyValue", |
| profile_->GetPrefs()->GetInteger( |
| prefs::kDevToolsGoogleDeveloperProgramProfileAvailability)); |
| response_dict.Set("devToolsGdpProfilesAvailability", |
| std::move(gdp_profiles_availability_dict)); |
| |
| response_dict.Set( |
| "devToolsLiveEdit", |
| base::Value::Dict().Set("enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsLiveEdit))); |
| |
| response_dict.Set( |
| "devToolsIndividualRequestThrottling", |
| base::Value::Dict().Set( |
| "enabled", base::FeatureList::IsEnabled( |
| ::features::kDevToolsIndividualRequestThrottling))); |
| |
| base::Value response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| } |
| |
| void DevToolsUIBindings::Reattach(DispatchCallback callback) { |
| if (agent_host_.get()) { |
| agent_host_->DetachClient(this); |
| InnerAttach(); |
| } |
| std::move(callback).Run(nullptr); |
| } |
| |
| void DevToolsUIBindings::ReadyForTest() { |
| delegate_->ReadyForTest(); |
| } |
| |
| void DevToolsUIBindings::ConnectionReady() { |
| delegate_->ConnectionReady(); |
| } |
| |
| void DevToolsUIBindings::SetOpenNewWindowForPopups(bool value) { |
| delegate_->SetOpenNewWindowForPopups(value); |
| } |
| |
| void DevToolsUIBindings::DispatchProtocolMessageFromDevToolsFrontend( |
| const std::string& message) { |
| if (!agent_host_) { |
| return; |
| } |
| agent_host_->DispatchProtocolMessage(this, base::as_byte_span(message)); |
| } |
| |
| void DevToolsUIBindings::SetHttpServiceRegistryForTesting( |
| std::unique_ptr<DevToolsHttpServiceRegistry> service_registry) { |
| http_service_registry_ = std::move(service_registry); |
| } |
| |
| void DevToolsUIBindings::RecordCountHistogram(const std::string& name, |
| int sample, |
| int min, |
| int exclusive_max, |
| int buckets) { |
| if (!frontend_host_) { |
| return; |
| } |
| |
| // DevTools previously would crash if histogram counts didn't make sense. |
| // We've changed this to a DCHECK and instead clamp the value for counts, |
| // because it doesn't really make sense to crash if the histogram is out |
| // of range. |
| DCHECK_GE(sample, min); |
| DCHECK_LT(sample, exclusive_max); |
| |
| if (sample < min) { |
| sample = 0; |
| } else if (sample >= exclusive_max) { |
| sample = exclusive_max - 1; |
| } |
| |
| base::UmaHistogramCustomCounts(name, sample, min, exclusive_max, buckets); |
| } |
| |
| void DevToolsUIBindings::RecordEnumeratedHistogram(const std::string& name, |
| int sample, |
| int boundary_value) { |
| if (!frontend_host_) { |
| return; |
| } |
| |
| DCHECK_GE(boundary_value, 0); |
| DCHECK_LT(boundary_value, 1000); |
| DCHECK_GE(sample, 0); |
| DCHECK_LT(sample, boundary_value); |
| if (!(boundary_value >= 0 && boundary_value <= 1000 && sample >= 0 && |
| sample < boundary_value)) { |
| // We should have DCHECK'd in debug builds; for release builds, if we're |
| // out of range, just omit the histogram |
| return; |
| } |
| |
| const std::string kDevToolsHistogramPrefix = "DevTools."; |
| DCHECK_EQ(name.compare(0, kDevToolsHistogramPrefix.size(), |
| kDevToolsHistogramPrefix), |
| 0); |
| base::UmaHistogramExactLinear(name, sample, boundary_value); |
| } |
| |
| void DevToolsUIBindings::RecordPerformanceHistogram(const std::string& name, |
| double duration) { |
| if (!frontend_host_) { |
| return; |
| } |
| if (duration < 0) { |
| return; |
| } |
| // Use histogram_functions.h instead of macros as the name comes from the |
| // DevTools frontend javascript and so will always have the same call site. |
| base::TimeDelta delta = base::Milliseconds(duration); |
| base::UmaHistogramTimes(name, delta); |
| } |
| |
| void DevToolsUIBindings::RecordPerformanceHistogramMedium( |
| const std::string& name, |
| double duration) { |
| if (!frontend_host_) { |
| return; |
| } |
| if (duration < 0) { |
| return; |
| } |
| // Use histogram_functions.h instead of macros as the name comes from the |
| // DevTools frontend javascript and so will always have the same call site. |
| base::TimeDelta delta = base::Milliseconds(duration); |
| base::UmaHistogramMediumTimes(name, delta); |
| } |
| |
| void DevToolsUIBindings::RecordUserMetricsAction(const std::string& name) { |
| if (!frontend_host_) { |
| return; |
| } |
| // Use RecordComputedAction instead of RecordAction as the name comes from |
| // DevTools frontend javascript and so will always have the same call site. |
| base::RecordComputedAction(name); |
| } |
| |
| void DevToolsUIBindings::RecordNewBadgeUsage(const std::string& feature_name) { |
| #if BUILDFLAG(IS_ANDROID) |
| NOTIMPLEMENTED(); |
| #else |
| |
| auto* user_education_service = |
| UserEducationServiceFactory::GetForBrowserContext(profile_); |
| if (!user_education_service || |
| !user_education_service->new_badge_registry()) { |
| return; |
| } |
| |
| const base::Feature* feature_to_register = nullptr; |
| for (const auto& [feature, spec] : |
| user_education_service->new_badge_registry()->feature_data()) { |
| if (feature_name == feature->name) { |
| feature_to_register = feature; |
| break; |
| } |
| } |
| |
| if (feature_to_register) { |
| UserEducationService::MaybeNotifyNewBadgeFeatureUsed( |
| web_contents()->GetBrowserContext(), *feature_to_register); |
| } |
| #endif |
| } |
| |
| void DevToolsUIBindings::MaybeStartLogging() { |
| if (session_id_for_logging_.is_empty()) { |
| session_id_for_logging_ = base::UnguessableToken::Create(); |
| session_start_time_ = base::TimeTicks::Now(); |
| base::Value::Dict sync_info = GetSyncInformationForProfile(profile_); |
| int64_t session_tags = 0; |
| bool is_signed_in = sync_info.FindBool("isSyncActive").value_or(false) && |
| !sync_info.FindBool("isSyncPaused").value_or(false); |
| if (is_signed_in) { |
| session_tags |= SessionTags::kUserSignedIn; |
| } |
| int gen_ai_settings = |
| profile_->GetPrefs()->GetInteger(prefs::kDevToolsGenAiSettings); |
| if (gen_ai_settings == |
| static_cast<int>(DevToolsGenAiEnterprisePolicyValue::kDisable)) { |
| session_tags |= SessionTags::kDevToolsGetAiEnterprisePolicyDisabled; |
| } |
| if (gen_ai_settings == |
| static_cast<int>( |
| DevToolsGenAiEnterprisePolicyValue::kAllowWithoutLogging)) { |
| session_tags |= |
| SessionTags::kDevToolsGetAiEnterprisePolicyAllowWithoutLogging; |
| } |
| bool remote_debugging_enabled = |
| g_browser_process->local_state()->GetBoolean( |
| prefs::kDevToolsRemoteDebuggingAllowed); |
| if (!remote_debugging_enabled) { |
| session_tags |= SessionTags::kDevToolsRemoteDebuggingDisabled; |
| } |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::SessionStart() |
| .SetTags(session_tags) |
| .SetTrigger(delegate_->GetOpenedByForLogging()) |
| .SetDockSide(delegate_->GetDockStateForLogging()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization()) |
| .SetIsSignedIn(is_signed_in)); |
| } |
| } |
| |
| base::TimeDelta DevToolsUIBindings::GetTimeSinceSessionStart() { |
| return base::TimeTicks::Now() - session_start_time_; |
| } |
| |
| void DevToolsUIBindings::RecordImpression(const ImpressionEvent& event) { |
| for (const auto& ve : event.impressions) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Impression() |
| .SetVeId(ve.id) |
| .SetVeType(ve.type) |
| .SetVeParent(ve.parent) |
| .SetVeContext(ve.context) |
| .SetWidth(ve.width) |
| .SetHeight(ve.height) |
| .SetTimeSinceSessionStart( |
| GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| } |
| |
| void DevToolsUIBindings::RecordResize(const ResizeEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Resize() |
| .SetVeId(event.veid) |
| .SetWidth(event.width) |
| .SetHeight(event.height) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordClick(const ClickEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Click() |
| .SetVeId(event.veid) |
| .SetMouseButton(event.mouse_button) |
| .SetDoubleClick(event.double_click) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordHover(const HoverEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Hover() |
| .SetVeId(event.veid) |
| .SetTime(event.time) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordDrag(const DragEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Drag() |
| .SetVeId(event.veid) |
| .SetDistance(event.distance) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordChange(const ChangeEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::Change() |
| .SetVeId(event.veid) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordKeyDown(const KeyDownEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::KeyDown() |
| .SetVeId(event.veid) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordSettingAccess(const SettingAccessEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::SettingAccess() |
| .SetName(event.name) |
| .SetNumericValue(event.numeric_value) |
| .SetStringValue(event.string_value) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::RecordFunctionCall(const FunctionCallEvent& event) { |
| metrics::structured::StructuredMetricsClient::Record( |
| metrics::structured::events::v2::dev_tools::FunctionCall() |
| .SetName(event.name) |
| .SetContext(event.context) |
| .SetTimeSinceSessionStart(GetTimeSinceSessionStart().InMilliseconds()) |
| .SetSessionId(session_id_for_logging_.GetLowForSerialization())); |
| } |
| |
| void DevToolsUIBindings::DeviceCountChanged(int count) { |
| CallClientMethod("DevToolsAPI", "deviceCountUpdated", base::Value(count)); |
| } |
| |
| void DevToolsUIBindings::DevicesUpdated(const std::string& source, |
| const base::Value& targets) { |
| CallClientMethod("DevToolsAPI", "devicesUpdated", targets.Clone()); |
| } |
| |
| void DevToolsUIBindings::FileSavedAs(const std::string& url, |
| const std::string& file_system_path) { |
| CallClientMethod("DevToolsAPI", "savedURL", base::Value(url), |
| base::Value(file_system_path)); |
| } |
| |
| void DevToolsUIBindings::CanceledFileSaveAs(const std::string& url) { |
| CallClientMethod("DevToolsAPI", "canceledSaveURL", base::Value(url)); |
| } |
| |
| void DevToolsUIBindings::AppendedTo(const std::string& url) { |
| CallClientMethod("DevToolsAPI", "appendedToURL", base::Value(url)); |
| } |
| |
| void DevToolsUIBindings::FileSystemAdded( |
| const std::string& error, |
| const DevToolsFileHelper::FileSystem* file_system) { |
| if (file_system) { |
| CallClientMethod("DevToolsAPI", "fileSystemAdded", base::Value(error), |
| base::Value(CreateFileSystemValue(*file_system))); |
| } else { |
| CallClientMethod("DevToolsAPI", "fileSystemAdded", base::Value(error)); |
| } |
| } |
| |
| void DevToolsUIBindings::FileSystemRemoved( |
| const std::string& file_system_path) { |
| CallClientMethod("DevToolsAPI", "fileSystemRemoved", |
| base::Value(file_system_path)); |
| } |
| |
| void DevToolsUIBindings::FilePathsChanged( |
| const std::vector<std::string>& changed_paths, |
| const std::vector<std::string>& added_paths, |
| const std::vector<std::string>& removed_paths) { |
| const int kMaxPathsPerMessage = 1000; |
| size_t changed_index = 0; |
| size_t added_index = 0; |
| size_t removed_index = 0; |
| // Dispatch limited amount of file paths in a time to avoid |
| // IPC max message size limit. See https://crbug.com/797817. |
| while (changed_index < changed_paths.size() || |
| added_index < added_paths.size() || |
| removed_index < removed_paths.size()) { |
| int budget = kMaxPathsPerMessage; |
| base::Value::List changed, added, removed; |
| while (budget > 0 && changed_index < changed_paths.size()) { |
| changed.Append(changed_paths[changed_index++]); |
| --budget; |
| } |
| while (budget > 0 && added_index < added_paths.size()) { |
| added.Append(added_paths[added_index++]); |
| --budget; |
| } |
| while (budget > 0 && removed_index < removed_paths.size()) { |
| removed.Append(removed_paths[removed_index++]); |
| --budget; |
| } |
| CallClientMethod("DevToolsAPI", "fileSystemFilesChangedAddedRemoved", |
| base::Value(std::move(changed)), |
| base::Value(std::move(added)), |
| base::Value(std::move(removed))); |
| } |
| } |
| |
| void DevToolsUIBindings::IndexingTotalWorkCalculated( |
| int request_id, |
| const std::string& file_system_path, |
| int total_work) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallClientMethod("DevToolsAPI", "indexingTotalWorkCalculated", |
| base::Value(request_id), base::Value(file_system_path), |
| base::Value(total_work)); |
| } |
| |
| void DevToolsUIBindings::IndexingWorked(int request_id, |
| const std::string& file_system_path, |
| int worked) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallClientMethod("DevToolsAPI", "indexingWorked", base::Value(request_id), |
| base::Value(file_system_path), base::Value(worked)); |
| } |
| |
| void DevToolsUIBindings::IndexingDone(int request_id, |
| const std::string& file_system_path) { |
| indexing_jobs_.erase(request_id); |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallClientMethod("DevToolsAPI", "indexingDone", base::Value(request_id), |
| base::Value(file_system_path)); |
| } |
| |
| void DevToolsUIBindings::SearchCompleted( |
| int request_id, |
| const std::string& file_system_path, |
| const std::vector<std::string>& file_paths) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::Value::List file_paths_value; |
| for (auto const& file_path : file_paths) { |
| file_paths_value.Append(file_path); |
| } |
| CallClientMethod("DevToolsAPI", "searchCompleted", base::Value(request_id), |
| base::Value(file_system_path), |
| base::Value(std::move(file_paths_value))); |
| } |
| |
| void DevToolsUIBindings::HandleDirectoryPermissions( |
| const std::string& directory_path, |
| const std::u16string& message, |
| DevToolsInfoBarDelegate::Callback callback) { |
| if (base::FeatureList::IsEnabled(::features::kDevToolsNewPermissionDialog)) { |
| ShowDirectoryPermissionDialog(directory_path, std::move(callback)); |
| } else { |
| ShowDevToolsInfoBar(message, std::move(callback)); |
| } |
| } |
| |
| void DevToolsUIBindings::ShowDevToolsInfoBar( |
| const std::u16string& message, |
| DevToolsInfoBarDelegate::Callback callback) { |
| #if BUILDFLAG(IS_ANDROID) |
| NOTIMPLEMENTED(); |
| #else |
| if (!delegate_->GetInfoBarManager()) { |
| std::move(callback).Run(false); |
| return; |
| } |
| DevToolsInfoBarDelegate::Create(message, std::move(callback)); |
| #endif |
| } |
| |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kCancelButtonId); |
| |
| void DevToolsUIBindings::ShowDirectoryPermissionDialog( |
| const std::string& directory_path, |
| DevToolsInfoBarDelegate::Callback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto split_callback = base::SplitOnceCallback(std::move(callback)); |
| auto accept_callback = base::BindOnce(std::move(split_callback.first), true); |
| auto cancel_callbacks = base::SplitOnceCallback( |
| base::BindOnce(std::move(split_callback.second), false)); |
| std::u16string origin_identity_name = u"DevTools"; |
| chrome::ShowTabModal( |
| ui::DialogModel::Builder() |
| .SetTitle(l10n_util::GetStringUTF16( |
| IDS_DEV_TOOLS_EDIT_DIRECTORY_PERMISSION_TITLE)) |
| .AddParagraph(ui::DialogModelLabel::CreateWithReplacements( |
| IDS_FILE_SYSTEM_ACCESS_WRITE_PERMISSION_DIRECTORY_TEXT, |
| {ui::DialogModelLabel::CreateEmphasizedText(origin_identity_name), |
| ui::DialogModelLabel::CreateEmphasizedText( |
| base::FilePath::FromUTF8Unsafe(directory_path) |
| .LossyDisplayName())})) |
| .AddOkButton( |
| std::move(accept_callback), |
| ui::DialogModel::Button::Params().SetLabel(l10n_util::GetStringUTF16( |
| IDS_FILE_SYSTEM_ACCESS_EDIT_DIRECTORY_PERMISSION_ALLOW_TEXT))) |
| .AddCancelButton( |
| std::move(cancel_callbacks.first), |
| ui::DialogModel::Button::Params().SetId(kCancelButtonId)) |
| .SetCloseActionCallback(std::move(cancel_callbacks.second)) |
| .SetInitiallyFocusedField(kCancelButtonId) |
| .Build(), |
| web_contents_); |
| } |
| |
| void DevToolsUIBindings::OnPermissionDialogResult( |
| DevToolsInfoBarDelegate::Callback callback, |
| permissions::PermissionAction result) { |
| std::move(callback).Run(result == permissions::PermissionAction::GRANTED); |
| } |
| |
| void DevToolsUIBindings::AddDevToolsExtensionsToClient() { |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| const extensions::ExtensionRegistry* registry = |
| extensions::ExtensionRegistry::Get(profile_->GetOriginalProfile()); |
| if (!registry) { |
| return; |
| } |
| |
| base::Value::List results; |
| base::Value::List forbidden_origins; |
| bool have_user_installed_devtools_extensions = false; |
| extensions::ExtensionManagement* management = |
| extensions::ExtensionManagementFactory::GetForBrowserContext( |
| web_contents_->GetBrowserContext()); |
| forbidden_origins.Append( |
| url::Origin::Create(search::GetNewTabPageURL(profile_)).Serialize()); |
| for (const scoped_refptr<const extensions::Extension>& extension : |
| registry->enabled_extensions()) { |
| if (extensions::Manifest::IsComponentLocation(extension->location())) { |
| forbidden_origins.Append(extension->origin().Serialize()); |
| } |
| if (extensions::chrome_manifest_urls::GetDevToolsPage(extension.get()) |
| .is_empty()) { |
| continue; |
| } |
| GURL url = |
| extensions::chrome_manifest_urls::GetDevToolsPage(extension.get()); |
| const bool is_extension_url = url.SchemeIs(extensions::kExtensionScheme) && |
| url.host_piece() == extension->id(); |
| CHECK(is_extension_url || url.SchemeIsHTTPOrHTTPS()); |
| |
| // Each devtools extension will need to be able to run in the devtools |
| // process. Grant the devtools process the ability to request URLs from the |
| // extension. |
| content::ChildProcessSecurityPolicy::GetInstance()->GrantRequestOrigin( |
| web_contents_->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), |
| url::Origin::Create(extension->url())); |
| |
| base::Value::List runtime_allowed_hosts; |
| std::vector<std::string> allowed_hosts = |
| management->GetPolicyAllowedHosts(extension.get()).ToStringVector(); |
| for (auto& host : allowed_hosts) { |
| runtime_allowed_hosts.Append(std::move(host)); |
| } |
| base::Value::List runtime_blocked_hosts; |
| std::vector<std::string> blocked_hosts = |
| management->GetPolicyBlockedHosts(extension.get()).ToStringVector(); |
| for (auto& host : blocked_hosts) { |
| runtime_blocked_hosts.Append(std::move(host)); |
| } |
| |
| base::Value::Dict extension_info; |
| extension_info.Set("startPage", url.spec()); |
| extension_info.Set("name", extension->name()); |
| extension_info.Set("exposeExperimentalAPIs", |
| extension->permissions_data()->HasAPIPermission( |
| extensions::mojom::APIPermissionID::kExperimental)); |
| extension_info.Set("allowFileAccess", extensions::util::AllowFileAccess( |
| extension->id(), profile_)); |
| extension_info.Set( |
| "hostsPolicy", |
| base::Value::Dict() |
| .Set("runtimeAllowedHosts", std::move(runtime_allowed_hosts)) |
| .Set("runtimeBlockedHosts", std::move(runtime_blocked_hosts))); |
| results.Append(std::move(extension_info)); |
| |
| if (!(extensions::Manifest::IsPolicyLocation(extension->location()) || |
| extensions::Manifest::IsComponentLocation(extension->location()))) { |
| have_user_installed_devtools_extensions = true; |
| } |
| } |
| |
| if (have_user_installed_devtools_extensions) { |
| bool is_developer_mode = |
| profile_->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode); |
| base::UmaHistogramBoolean("Extensions.DevTools.UserIsInDeveloperMode", |
| is_developer_mode); |
| } |
| |
| CallClientMethod("DevToolsAPI", "setOriginsForbiddenForExtensions", |
| base::Value(std::move(forbidden_origins))); |
| CallClientMethod("DevToolsAPI", "addExtensions", |
| base::Value(std::move(results))); |
| #endif |
| } |
| |
| void DevToolsUIBindings::RegisterExtensionsAPI(const std::string& origin, |
| const std::string& script) { |
| extensions_api_[origin + "/"] = script; |
| } |
| |
| namespace { |
| |
| void ShowSurveyCallback(DevToolsUIBindings::DispatchCallback callback, |
| bool survey_shown) { |
| base::Value::Dict response_dict; |
| response_dict.Set("surveyShown", survey_shown); |
| base::Value response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| } |
| |
| } // namespace |
| |
| void DevToolsUIBindings::ShowSurvey(DispatchCallback callback, |
| const std::string& trigger) { |
| HatsService* hats_service = |
| HatsServiceFactory::GetForProfile(profile_->GetOriginalProfile(), true); |
| if (!hats_service) { |
| ShowSurveyCallback(std::move(callback), false); |
| return; |
| } |
| auto split_callback = base::SplitOnceCallback(std::move(callback)); |
| hats_service->LaunchSurvey( |
| trigger, |
| base::BindOnce(ShowSurveyCallback, std::move(split_callback.first), true), |
| base::BindOnce(ShowSurveyCallback, std::move(split_callback.second), |
| false)); |
| } |
| |
| void DevToolsUIBindings::CanShowSurvey(DispatchCallback callback, |
| const std::string& trigger) { |
| HatsService* hats_service = |
| HatsServiceFactory::GetForProfile(profile_->GetOriginalProfile(), true); |
| bool can_show = hats_service ? hats_service->CanShowSurvey(trigger) : false; |
| base::Value::Dict response_dict; |
| response_dict.Set("canShowSurvey", can_show); |
| base::Value response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| } |
| |
| bool DevToolsUIBindings::EnsureAidaClientAvailable() { |
| if (!can_access_aida_ || AidaClient::CanUseAida(profile_).blocked) { |
| return false; |
| } |
| if (!aida_client_) { |
| aida_client_ = std::make_unique<AidaClient>(profile_); |
| } |
| return true; |
| } |
| |
| void DevToolsUIBindings::HandleAidaClientUnavailable( |
| DispatchCallback callback) { |
| base::Value::Dict response_dict; |
| response_dict.Set("error", "AIDA request was blocked"); |
| base::Value response = base::Value(std::move(response_dict)); |
| std::move(callback).Run(&response); |
| } |
| |
| void DevToolsUIBindings::DoAidaConversation(DispatchCallback callback, |
| const std::string& request, |
| int stream_id) { |
| if (!EnsureAidaClientAvailable()) { |
| HandleAidaClientUnavailable(std::move(callback)); |
| return; |
| } |
| aida_client_->PrepareRequestOrFail(base::BindOnce( |
| &DevToolsUIBindings::OnAidaConversationRequest, base::Unretained(this), |
| std::move(callback), stream_id, request, base::TimeDelta())); |
| } |
| |
| void DevToolsUIBindings::AidaCodeComplete(DispatchCallback callback, |
| const std::string& request) { |
| if (!EnsureAidaClientAvailable()) { |
| HandleAidaClientUnavailable(std::move(callback)); |
| return; |
| } |
| aida_client_->PrepareRequestOrFail(base::BindOnce( |
| &DevToolsUIBindings::OnAidaRequest, base::Unretained(this), |
| GURL(AidaClient::kCompleteCodeUrl), |
| "DevTools.AidaCodeCompleteResponseTime", std::move(callback), request)); |
| } |
| |
| void DevToolsUIBindings::RegisterAidaClientEvent(DispatchCallback callback, |
| const std::string& request) { |
| if (!EnsureAidaClientAvailable()) { |
| HandleAidaClientUnavailable(std::move(callback)); |
| return; |
| } |
| aida_client_->PrepareRequestOrFail( |
| base::BindOnce(&DevToolsUIBindings::OnAidaRequest, base::Unretained(this), |
| GURL(AidaClient::kRegisterClientEventUrl), |
| "DevTools.RegisterAidaClientEventResponseTime", |
| std::move(callback), request)); |
| } |
| |
| void DevToolsUIBindings::SetDelegate(Delegate* delegate) { |
| delegate_.reset(delegate); |
| } |
| |
| void DevToolsUIBindings::TransferDelegate(DevToolsUIBindings& other) { |
| std::swap(delegate_, other.delegate_); |
| if (auto agent_host = agent_host_) { |
| Detach(); |
| other.AttachTo(agent_host); |
| } |
| } |
| |
| void DevToolsUIBindings::AttachTo( |
| const scoped_refptr<content::DevToolsAgentHost>& agent_host) { |
| if (agent_host_.get()) { |
| Detach(); |
| } |
| agent_host_ = agent_host; |
| InnerAttach(); |
| } |
| |
| void DevToolsUIBindings::AttachViaBrowserTarget( |
| const scoped_refptr<content::DevToolsAgentHost>& agent_host) { |
| DCHECK(!agent_host_ || |
| agent_host->GetType() == content::DevToolsAgentHost::kTypeBrowser); |
| if (!agent_host_) { |
| agent_host_ = content::DevToolsAgentHost::CreateForBrowser( |
| nullptr /* tethering_task_runner */, |
| content::DevToolsAgentHost::CreateServerSocketCallback()); |
| } |
| initial_target_id_ = agent_host->GetId(); |
| InnerAttach(); |
| } |
| |
| void DevToolsUIBindings::Detach() { |
| if (agent_host_.get()) { |
| agent_host_->DetachClient(this); |
| } |
| agent_host_.reset(); |
| } |
| |
| bool DevToolsUIBindings::IsAttachedTo(content::DevToolsAgentHost* agent_host) { |
| // TODO(caseq): find better way to track attached targets. |
| return initial_target_id_.empty() ? agent_host_.get() == agent_host |
| : initial_target_id_ == agent_host->GetId(); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| void DevToolsUIBindings::OnThemeChanged() { |
| CallClientMethod("DevToolsAPI", "colorThemeChanged"); |
| } |
| #endif |
| |
| void DevToolsUIBindings::CallClientMethod( |
| const std::string& object_name, |
| const std::string& method_name, |
| base::Value arg1, |
| base::Value arg2, |
| base::Value arg3, |
| base::OnceCallback<void(base::Value)> completion_callback) { |
| // If we're not exposing bindings, we shouldn't call functions either. |
| if (!frontend_host_) { |
| return; |
| } |
| // If the client renderer is gone (e.g., the window was closed with both the |
| // inspector and client being destroyed), the message can not be sent. |
| if (!web_contents_->GetPrimaryMainFrame()->IsRenderFrameLive()) { |
| return; |
| } |
| base::Value::List arguments; |
| if (!arg1.is_none()) { |
| arguments.Append(std::move(arg1)); |
| if (!arg2.is_none()) { |
| arguments.Append(std::move(arg2)); |
| if (!arg3.is_none()) { |
| arguments.Append(std::move(arg3)); |
| } |
| } |
| } |
| web_contents_->GetPrimaryMainFrame()->ExecuteJavaScriptMethod( |
| base::ASCIIToUTF16(object_name), base::ASCIIToUTF16(method_name), |
| std::move(arguments), std::move(completion_callback)); |
| } |
| |
| void DevToolsUIBindings::ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle->IsInPrimaryMainFrame()) { |
| if (frontend_loaded_ && agent_host_.get()) { |
| agent_host_->DetachClient(this); |
| InnerAttach(); |
| } |
| if (!IsValidFrontendURL(navigation_handle->GetURL())) { |
| LOG(ERROR) << "Attempt to navigate to an invalid DevTools front-end URL: " |
| << navigation_handle->GetURL().spec(); |
| frontend_host_.reset(); |
| return; |
| } |
| if (frontend_host_) { |
| return; |
| } |
| if (content::RenderFrameHost* opener = web_contents_->GetOpener()) { |
| content::WebContents* opener_wc = |
| content::WebContents::FromRenderFrameHost(opener); |
| DevToolsUIBindings* opener_bindings = |
| opener_wc ? DevToolsUIBindings::ForWebContents(opener_wc) : nullptr; |
| if (!opener_bindings || !opener_bindings->frontend_host_) { |
| return; |
| } |
| } |
| frontend_host_ = content::DevToolsFrontendHost::Create( |
| navigation_handle->GetRenderFrameHost(), |
| base::BindRepeating( |
| &DevToolsUIBindings::HandleMessageFromDevToolsFrontend, |
| base::Unretained(this))); |
| return; |
| } |
| |
| content::RenderFrameHost* frame = navigation_handle->GetRenderFrameHost(); |
| std::string origin = |
| navigation_handle->GetURL().DeprecatedGetOriginAsURL().spec(); |
| auto it = extensions_api_.find(origin); |
| if (it == extensions_api_.end()) { |
| return; |
| } |
| std::string script = base::StringPrintf( |
| "%s(\"%s\")", it->second.c_str(), |
| base::Uuid::GenerateRandomV4().AsLowercaseString().c_str()); |
| content::DevToolsFrontendHost::SetupExtensionsAPI(frame, script); |
| } |
| |
| void DevToolsUIBindings::DocumentOnLoadCompletedInPrimaryMainFrame() { |
| FrontendLoaded(); |
| } |
| |
| void DevToolsUIBindings::PrimaryPageChanged() { |
| frontend_loaded_ = false; |
| } |
| |
| void DevToolsUIBindings::FrontendLoaded() { |
| if (frontend_loaded_) { |
| return; |
| } |
| frontend_loaded_ = true; |
| |
| // Call delegate first - it seeds importants bit of information. |
| delegate_->OnLoadCompleted(); |
| |
| if (!initial_target_id_.empty()) { |
| CallClientMethod("DevToolsAPI", "setInitialTargetId", |
| base::Value(initial_target_id_)); |
| } |
| AddDevToolsExtensionsToClient(); |
| } |
| |
| DevToolsUIBindings::DevToolsUIBindingsList& |
| DevToolsUIBindings::GetDevToolsUIBindings() { |
| static base::NoDestructor<DevToolsUIBindings::DevToolsUIBindingsList> |
| bindings; |
| return *bindings; |
| } |