| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/webui/web_ui_main_frame_observer.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/feature_list.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "components/crash/content/browser/error_reporting/javascript_error_report.h" |
| #include "components/crash/content/browser/error_reporting/js_error_report_processor.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/webui/web_ui_impl.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui_controller.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_constants.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| #if !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_FUCHSIA)) |
| // Remove the pieces of the URL we don't want to send back with the error |
| // reports. In particular, do not send query or fragments as those can have |
| // privacy-sensitive information in them. |
| std::string RedactURL(const GURL& url) { |
| std::string redacted_url = url.DeprecatedGetOriginAsURL().spec(); |
| // Path will start with / and GetOrigin ends with /. Cut one / to avoid |
| // chrome://discards//graph. |
| if (!redacted_url.empty() && redacted_url.back() == '/') { |
| redacted_url.pop_back(); |
| } |
| base::StrAppend(&redacted_url, {url.path()}); |
| return redacted_url; |
| } |
| #endif // !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_FUCHSIA)) |
| |
| bool IsWebUIJavaScriptErrorReportingSupported() { |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_FUCHSIA) |
| return false; |
| #elif BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) |
| return true; |
| #else |
| return base::FeatureList::IsEnabled(features::kWebUIJSErrorReportingExtended); |
| #endif |
| } |
| |
| } // namespace |
| |
| WebUIMainFrameObserver::WebUIMainFrameObserver(WebUIImpl* web_ui, |
| WebContents* contents) |
| : WebContentsObserver(contents), web_ui_(web_ui) {} |
| |
| WebUIMainFrameObserver::~WebUIMainFrameObserver() = default; |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA) |
| void WebUIMainFrameObserver::OnDidAddMessageToConsole( |
| RenderFrameHost* source_frame, |
| blink::mojom::ConsoleMessageLevel log_level, |
| const std::u16string& message, |
| int32_t line_no, |
| const std::u16string& source_id, |
| const std::optional<std::u16string>& untrusted_stack_trace) { |
| DVLOG(3) << "OnDidAddMessageToConsole called for " << message; |
| if (untrusted_stack_trace) { |
| DVLOG(3) << "stack is " << *untrusted_stack_trace; |
| } |
| |
| if (!error_reporting_enabled_) { |
| DVLOG(3) << "Message not reported, error reporting disabled for this page " |
| "or experiment is off"; |
| return; |
| } |
| |
| if (log_level != blink::mojom::ConsoleMessageLevel::kError) { |
| DVLOG(3) << "Message not reported, not an error"; |
| return; |
| } |
| |
| // Some WebUI pages have another WebUI page in an <iframe>. Both |
| // WebUIMainFrameObservers will get a callback when either page gets an error. |
| // To avoid duplicates, only report on errors from this page's frame. |
| if (source_frame != web_ui_->GetRenderFrameHost()) { |
| DVLOG(3) << "Message not reported, different frame"; |
| return; |
| } |
| |
| scoped_refptr<JsErrorReportProcessor> processor = |
| JsErrorReportProcessor::Get(); |
| |
| if (!processor) { |
| // This usually means we are not on an official Google build, FYI. |
| DVLOG(3) << "Message not reported, no processor"; |
| return; |
| } |
| |
| // Redact query parameters & fragment. Also the username and password. |
| // TODO(crbug.com/40146362) Improve redaction. |
| GURL url(source_id); |
| if (!url.is_valid()) { |
| DVLOG(3) << "Message not reported, invalid URL"; |
| return; |
| } |
| |
| // If this is not a chrome:// page, do not report the error. In particular, |
| // some WebUIs use chrome-untrusted:// to host pages with some |
| // not-controlled-by-Chrome content. The code must not send reports for such |
| // content because Chrome cannot control what information is being included in |
| // the reports. |
| if (!url.SchemeIs(kChromeUIScheme)) { |
| DVLOG(3) << "Message not reported, not a chrome:// URL"; |
| return; |
| } |
| |
| JavaScriptErrorReport report; |
| report.message = base::UTF16ToUTF8(message); |
| report.line_number = line_no; |
| report.url = RedactURL(url); |
| report.source_system = JavaScriptErrorReport::SourceSystem::kWebUIObserver; |
| if (untrusted_stack_trace) { |
| report.stack_trace = base::UTF16ToUTF8(*untrusted_stack_trace); |
| } |
| |
| GURL page_url = source_frame->GetLastCommittedURL(); |
| if (page_url.is_valid()) { |
| report.page_url = RedactURL(page_url); |
| } |
| |
| DVLOG(3) << "Error being sent to Google"; |
| processor->SendErrorReport(std::move(report), base::DoNothing(), |
| web_contents()->GetBrowserContext()); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA) |
| |
| void WebUIMainFrameObserver::MaybeEnableWebUIJavaScriptErrorReporting( |
| NavigationHandle* navigation_handle) { |
| if (!IsWebUIJavaScriptErrorReportingSupported()) { |
| return; |
| } |
| |
| error_reporting_enabled_ = |
| web_ui_->GetController()->IsJavascriptErrorReportingEnabled(); |
| |
| // If we are collecting error reports, make sure the main frame sends us |
| // stacks along with those messages. Warning: Don't call |
| // RenderFrameHostImpl::SetWantErrorMessageStackTrace() before the remote |
| // frame is created, or it will lock up the communications channel. (See |
| // https://crbug.com/1154866). |
| if (error_reporting_enabled_) { |
| DVLOG(3) << "Enabled"; |
| static_cast<content::RenderFrameHostImpl*>(web_ui_->GetRenderFrameHost()) |
| ->SetWantErrorMessageStackTrace(); |
| } else { |
| DVLOG(3) << "Error reporting disabled for this page"; |
| } |
| } |
| |
| void WebUIMainFrameObserver::ReadyToCommitNavigation( |
| NavigationHandle* navigation_handle) { |
| // Navigation didn't occur in the frame associated with this WebUI. |
| if (navigation_handle->GetRenderFrameHost() != |
| web_ui_->GetRenderFrameHost()) { |
| return; |
| } |
| |
| web_ui_->GetController()->WebUIReadyToCommitNavigation( |
| web_ui_->GetRenderFrameHost()); |
| |
| GURL site_url = |
| web_ui_->GetRenderFrameHost()->GetSiteInstance()->GetSiteURL(); |
| GetContentClient()->browser()->LogWebUIUsage(web_ui_); |
| MaybeEnableWebUIJavaScriptErrorReporting(navigation_handle); |
| } |
| |
| void WebUIMainFrameObserver::PrimaryPageChanged(Page& page) { |
| web_ui_->DisallowJavascriptOnAllHandlers(); |
| web_ui_->GetController()->WebUIPrimaryPageChanged(page); |
| } |
| |
| } // namespace content |