// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/renderer/net/net_error_helper.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/json/json_writer.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_resource_request_blocked_reason.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/renderer/process_state.h"
#include "components/error_page/common/error.h"
#include "components/error_page/common/localized_error.h"
#include "components/error_page/common/net_error_info.h"
#include "components/grit/components_resources.h"
#include "components/offline_pages/buildflags/buildflags.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/security_interstitials/content/renderer/security_interstitial_page_controller.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/renderer/content_renderer_client.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "net/base/net_errors.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "skia/ext/skia_utils_base.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-shared.h"
#include "third_party/blink/public/platform/web_data.h"
#include "third_party/blink/public/platform/web_security_origin.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/public/platform/web_url_response.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_frame.h"
#include "third_party/blink/public/web/web_history_item.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/webui/web_ui_util.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/common/offline_page_auto_fetcher.mojom.h"
#endif

using content::RenderFrame;
using content::RenderFrameObserver;
using content::RenderThread;
using error_page::DnsProbeStatus;
using error_page::DnsProbeStatusToString;
using error_page::LocalizedError;

namespace {

NetErrorHelperCore::FrameType GetFrameType(RenderFrame* render_frame) {
  if (render_frame->IsMainFrame())
    return NetErrorHelperCore::MAIN_FRAME;
  return NetErrorHelperCore::SUB_FRAME;
}

bool IsExtensionExtendedErrorCode(int extended_error_code) {
  return extended_error_code ==
         static_cast<int>(ChromeResourceRequestBlockedReason::kExtension);
}

bool IsAutoFetchFeatureEnabled() {
#if BUILDFLAG(IS_ANDROID)
  return base::FeatureList::IsEnabled(features::kOfflineAutoFetch);
#else   // BUILDFLAG(IS_ANDROID)
  return false;
#endif  // BUILDFLAG(IS_ANDROID)
}

bool IsRunningInForcedAppMode() {
  return base::CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kForceAppMode);
}

}  // namespace

NetErrorHelper::NetErrorHelper(RenderFrame* render_frame)
    : RenderFrameObserver(render_frame),
      content::RenderFrameObserverTracker<NetErrorHelper>(render_frame) {
  // TODO(mmenke): Consider only creating a NetErrorHelperCore for main frames.
  // subframes don't need any of the NetErrorHelperCore's extra logic.
  core_ = std::make_unique<NetErrorHelperCore>(this);

  render_frame->GetAssociatedInterfaceRegistry()
      ->AddInterface<chrome::mojom::NetworkDiagnosticsClient>(
          base::BindRepeating(
              &NetErrorHelper::OnNetworkDiagnosticsClientRequest,
              base::Unretained(this)));
}

NetErrorHelper::~NetErrorHelper() = default;

void NetErrorHelper::ButtonPressed(NetErrorHelperCore::Button button) {
  core_->ExecuteButtonPress(button);
}

void NetErrorHelper::SavePageForLater() {
  core_->SavePageForLater();
}

void NetErrorHelper::CancelSavePage() {
  core_->CancelSavePage();
}

content::RenderFrame* NetErrorHelper::GetRenderFrame() {
  return render_frame();
}

void NetErrorHelper::DidCommitProvisionalLoad(ui::PageTransition transition) {
  // Invalidate weak pointers from the old error page controller. If loading a
  // new error page, the controller has not yet been attached, so this won't
  // affect it.
  weak_controller_delegate_factory_.InvalidateWeakPtrs();

  core_->OnCommitLoad(GetFrameType(render_frame()),
                      render_frame()->GetWebFrame()->GetDocument().Url());
}

void NetErrorHelper::DidFinishLoad() {
  core_->OnFinishLoad(GetFrameType(render_frame()));
}

void NetErrorHelper::OnDestruct() {
  delete this;
}

void NetErrorHelper::PrepareErrorPage(
    const error_page::Error& error,
    bool is_failed_post,
    content::mojom::AlternativeErrorPageOverrideInfoPtr
        alternative_error_page_info,
    std::string* error_html) {
  core_->PrepareErrorPage(GetFrameType(render_frame()), error, is_failed_post,
                          std::move(alternative_error_page_info), error_html);
}

chrome::mojom::NetworkDiagnostics*
NetErrorHelper::GetRemoteNetworkDiagnostics() {
  if (!remote_network_diagnostics_) {
    render_frame()->GetRemoteAssociatedInterfaces()
        ->GetInterface(&remote_network_diagnostics_);
  }
  return remote_network_diagnostics_.get();
}

chrome::mojom::NetworkEasterEgg* NetErrorHelper::GetRemoteNetworkEasterEgg() {
  if (!remote_network_easter_egg_) {
    render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(
        &remote_network_easter_egg_);
  }
  return remote_network_easter_egg_.get();
}

chrome::mojom::NetErrorPageSupport*
NetErrorHelper::GetRemoteNetErrorPageSupport() {
  if (!remote_net_error_page_support_) {
    render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(
        &remote_net_error_page_support_);
  }
  return remote_net_error_page_support_.get();
}

LocalizedError::PageState NetErrorHelper::GenerateLocalizedErrorPage(
    const error_page::Error& error,
    bool is_failed_post,
    bool can_show_network_diagnostics_dialog,
    content::mojom::AlternativeErrorPageOverrideInfoPtr
        alternative_error_page_info,
    std::string* error_html) {
  error_html->clear();
  int resource_id = IDR_NET_ERROR_HTML;
  LocalizedError::PageState page_state;
  // If the user is viewing an offline web app then a default page is shown
  // rather than the dino.

  if (alternative_error_page_info &&
      alternative_error_page_info->alternative_error_page_params
          .FindBool(error_page::kOverrideErrorPage)
          .value_or(false)) {
    base::UmaHistogramSparse("Net.ErrorPageCounts.WebAppAlternativeErrorPage",
                             -error.reason());
    resource_id = alternative_error_page_info->resource_id;
    page_state = LocalizedError::GetPageStateForOverriddenErrorPage(
        std::move(alternative_error_page_info->alternative_error_page_params),
        error.reason(), error.domain(), error.url(),
        RenderThread::Get()->GetLocale());
  } else {
    if (alternative_error_page_info) {
      error_page_params_ =
          alternative_error_page_info->alternative_error_page_params.Clone();
    } else {
      error_page_params_.clear();
    }
    page_state = LocalizedError::GetPageState(
        error.reason(), error.domain(), error.url(), is_failed_post,
        error.resolve_error_info().is_secure_network_error,
        error.stale_copy_in_cache(), can_show_network_diagnostics_dialog,
        process_state::IsIncognitoProcess(), IsAutoFetchFeatureEnabled(),
        IsRunningInForcedAppMode(), RenderThread::Get()->GetLocale(),
        IsExtensionExtendedErrorCode(error.extended_reason()),
        &error_page_params_);
  }
  std::string template_html =
      ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
          resource_id);
  DCHECK(!template_html.empty()) << "unable to load template.";
  *error_html = webui::GetLocalizedHtml(template_html, page_state.strings);
  return page_state;
}

void NetErrorHelper::EnablePageHelperFunctions() {
  NetErrorPageController::Install(
      render_frame(), weak_controller_delegate_factory_.GetWeakPtr());
}

LocalizedError::PageState NetErrorHelper::UpdateErrorPage(
    const error_page::Error& error,
    bool is_failed_post,
    bool can_show_network_diagnostics_dialog) {
  LocalizedError::PageState page_state = LocalizedError::GetPageState(
      error.reason(), error.domain(), error.url(), is_failed_post,
      error.resolve_error_info().is_secure_network_error,
      error.stale_copy_in_cache(), can_show_network_diagnostics_dialog,
      process_state::IsIncognitoProcess(), IsAutoFetchFeatureEnabled(),
      IsRunningInForcedAppMode(), RenderThread::Get()->GetLocale(),
      IsExtensionExtendedErrorCode(error.extended_reason()),
      &error_page_params_);

  std::string json = base::WriteJson(page_state.strings).value_or("");

  std::string js = "if (window.updateForDnsProbe) "
                   "updateForDnsProbe(" + json + ");";
  std::u16string js16;
  if (base::UTF8ToUTF16(js.c_str(), js.length(), &js16)) {
    render_frame()->ExecuteJavaScript(js16);
  } else {
    NOTREACHED();
  }
  return page_state;
}

void NetErrorHelper::InitializeErrorPageEasterEggHighScore(int high_score) {
  std::string js = base::StringPrintf(
      "if (window.initializeEasterEggHighScore) "
      "initializeEasterEggHighScore(%i);",
      high_score);
  std::u16string js16;
  if (!base::UTF8ToUTF16(js.c_str(), js.length(), &js16)) {
    NOTREACHED();
  }

  render_frame()->ExecuteJavaScript(js16);
}

void NetErrorHelper::RequestEasterEggHighScore() {
  GetRemoteNetworkEasterEgg()->GetHighScore(base::BindOnce(
      [](NetErrorHelper* helper, uint32_t high_score) {
        helper->core_->OnEasterEggHighScoreReceived(high_score);
      },
      base::Unretained(this)));
}

void NetErrorHelper::UpdateEasterEggHighScore(int high_score) {
  GetRemoteNetworkEasterEgg()->UpdateHighScore(high_score);
}

void NetErrorHelper::ResetEasterEggHighScore() {
  GetRemoteNetworkEasterEgg()->ResetHighScore();
}

void NetErrorHelper::ReloadFrame() {
  render_frame()->GetWebFrame()->StartReload(blink::WebFrameLoadType::kReload);
}

void NetErrorHelper::DiagnoseError(const GURL& page_url) {
  GetRemoteNetworkDiagnostics()->RunNetworkDiagnostics(page_url);
}

void NetErrorHelper::PortalSignin() {
#if BUILDFLAG(IS_CHROMEOS)
  GetRemoteNetErrorPageSupport()->ShowPortalSignin();
#endif
}

void NetErrorHelper::DownloadPageLater() {
#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
  GetRemoteNetErrorPageSupport()->DownloadPageLater();
#endif  // BUILDFLAG(ENABLE_OFFLINE_PAGES)
}

void NetErrorHelper::SetIsShowingDownloadButton(bool show) {
#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
  GetRemoteNetErrorPageSupport()->SetIsShowingDownloadButtonInErrorPage(show);
#endif  // BUILDFLAG(ENABLE_OFFLINE_PAGES)
}

#if BUILDFLAG(IS_ANDROID)
void NetErrorHelper::SetAutoFetchState(
    chrome::mojom::OfflinePageAutoFetcherScheduleResult result) {
  const char* scheduled = "false";
  const char* can_schedule = "false";
  switch (result) {
    case chrome::mojom::OfflinePageAutoFetcherScheduleResult::kAlreadyScheduled:
    case chrome::mojom::OfflinePageAutoFetcherScheduleResult::kScheduled:
      scheduled = "true";
      can_schedule = "true";
      break;
    case chrome::mojom::OfflinePageAutoFetcherScheduleResult::kOtherError:
      break;
    case chrome::mojom::OfflinePageAutoFetcherScheduleResult::kNotEnoughQuota:
      can_schedule = "true";
      break;
  }
  render_frame()->ExecuteJavaScript(base::UTF8ToUTF16(base::StrCat(
      {"setAutoFetchState(", scheduled, ", ", can_schedule, ");"})));
}
#endif  // BUILDFLAG(IS_ANDROID)

void NetErrorHelper::DNSProbeStatus(int32_t status_num) {
  DCHECK(status_num >= 0 && status_num < error_page::DNS_PROBE_MAX);

  DVLOG(1) << "Received status " << DnsProbeStatusToString(status_num);

  core_->OnNetErrorInfo(static_cast<DnsProbeStatus>(status_num));
}

void NetErrorHelper::OnNetworkDiagnosticsClientRequest(
    mojo::PendingAssociatedReceiver<chrome::mojom::NetworkDiagnosticsClient>
        receiver) {
  network_diagnostics_client_receivers_.Add(this, std::move(receiver));
}

void NetErrorHelper::SetCanShowNetworkDiagnosticsDialog(bool can_show) {
  core_->OnSetCanShowNetworkDiagnosticsDialog(can_show);
}
