// 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/command_line.h"
#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 "base/version_info/version_info.h"
#include "build/build_config.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/browser/webui_config.h"
#include "content/public/browser/webui_config_map.h"
#include "content/public/common/content_features.h"
#include "content/public/common/url_constants.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA)
#include "components/crash/content/browser/error_reporting/error_reporting_util.h"
#include "components/crash/content/browser/error_reporting/javascript_error_report.h"
#include "components/crash/content/browser/error_reporting/js_error_report_processor.h"
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA)

namespace content {

namespace {

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
}

// The command line flag to disable the crash on WebUI JS error in development
// builds.
const char kDisableCrashOnWebUIJsError[] = "disable-crash-on-webui-js-error";

}  // 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;
  }

  // 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;
  }

  // Ignore console messages from frames that are pending deletion to avoid
  // reporting errors during frame teardown.
  if (source_frame->GetLifecycleState() ==
      RenderFrameHost::LifecycleState::kPendingDeletion) {
    DVLOG(3) << "Message not reported, frame is being deleted";
    return;
  }

  JavaScriptErrorReport report;
  report.message = base::UTF16ToUTF8(message);
  report.line_number = line_no;
  report.url = RedactUrlForErrorReports(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 = RedactUrlForErrorReports(page_url);
  }

  if (should_crash_on_error_) {
    // LOG(FATAL) will crash the browser.
    LOG(FATAL) << "JavaScript error in development build. To disable this "
                  "crash, use --"
               << kDisableCrashOnWebUIJsError << ".\n"
               << "URL: " << report.url << "\n"
               << "Page URL: " << report.page_url.value_or("(empty)") << "\n"
               << "Message: " << report.message << "\n"
               << "Stack trace: " << report.stack_trace.value_or("(empty)");
  }

  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;
  }

  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();

  // WebUIConfigMap::GetConfig() uses the origin of the URL to look up the
  // config, so this works even if the page URL has a path, e.g.
  // chrome://omnibox-popup.top-chrome/omnibox_popup_aim.html.
  WebUIConfig* config = WebUIConfigMap::GetInstance().GetConfig(
      web_contents()->GetBrowserContext(), navigation_handle->GetURL());
  should_crash_on_error_ =
      config && config->ShouldCrashOnJavascriptErrorInDevelopmentBuild() &&
      !version_info::IsOfficialBuild();

  // Allow disabling the crash via a command line switch, for testing purposes.
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          kDisableCrashOnWebUIJsError)) {
    should_crash_on_error_ = false;
  }

  // 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_->SetUpMojoInterfaceBroker();

  GetContentClient()->browser()->LogWebUIUsage(web_ui_);
  MaybeEnableWebUIJavaScriptErrorReporting(navigation_handle);
}

void WebUIMainFrameObserver::PrimaryPageChanged(Page& page) {
  web_ui_->DisallowJavascriptOnAllHandlers();
  web_ui_->GetController()->WebUIPrimaryPageChanged(page);
}

}  // namespace content
