chromium/chrome/browser/offline_pages/offline_page_tab_helper.cc

// Copyright 2016 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/offline_pages/offline_page_tab_helper.h"

#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/offline_page_request_handler.h"
#include "chrome/browser/offline_pages/offline_page_utils.h"
#include "chrome/browser/offline_pages/request_coordinator_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "components/offline_pages/core/background/request_coordinator.h"
#include "components/offline_pages/core/model/offline_page_model_utils.h"
#include "components/offline_pages/core/offline_page_client_policy.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_page_item_utils.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "components/offline_pages/core/page_criteria.h"
#include "components/offline_pages/core/request_header/offline_page_header.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/browser_thread.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/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/page_transition_types.h"

namespace offline_pages {

using blink::mojom::MHTMLLoadResult;

namespace {
bool SchemeIsForUntrustedOfflinePages(const GURL& url) {
#if BUILDFLAG(IS_ANDROID)
  if (url.SchemeIs(url::kContentScheme))
    return true;
#endif
  return url.SchemeIsFile();
}
}  // namespace

OfflinePageTabHelper::LoadedOfflinePageInfo::LoadedOfflinePageInfo()
    : trusted_state(OfflinePageTrustedState::UNTRUSTED),
      is_showing_offline_preview(false) {}

OfflinePageTabHelper::LoadedOfflinePageInfo::LoadedOfflinePageInfo(
    OfflinePageTabHelper::LoadedOfflinePageInfo&& other) = default;

OfflinePageTabHelper::LoadedOfflinePageInfo::~LoadedOfflinePageInfo() = default;

OfflinePageTabHelper::LoadedOfflinePageInfo&
OfflinePageTabHelper::LoadedOfflinePageInfo::operator=(
    OfflinePageTabHelper::LoadedOfflinePageInfo&& other) = default;

// static
void OfflinePageTabHelper::BindHtmlPageNotifier(
    mojo::PendingAssociatedReceiver<offline_pages::mojom::MhtmlPageNotifier>
        receiver,
    content::RenderFrameHost* rfh) {
  auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
  if (!web_contents)
    return;
  auto* tab_helper = OfflinePageTabHelper::FromWebContents(web_contents);
  if (!tab_helper)
    return;
  tab_helper->mhtml_page_notifier_receivers_.Bind(rfh, std::move(receiver));
}

// static
OfflinePageTabHelper::LoadedOfflinePageInfo
OfflinePageTabHelper::LoadedOfflinePageInfo::MakeUntrusted() {
  LoadedOfflinePageInfo untrusted_info;
  untrusted_info.offline_page = std::make_unique<OfflinePageItem>();
  untrusted_info.offline_page->offline_id = store_utils::GenerateOfflineId();

  return untrusted_info;
}

void OfflinePageTabHelper::LoadedOfflinePageInfo::Clear() {
  offline_page.reset();
  offline_header.Clear();
  trusted_state = OfflinePageTrustedState::UNTRUSTED;
  is_showing_offline_preview = false;
}

bool OfflinePageTabHelper::LoadedOfflinePageInfo::IsValid() const {
  return offline_page != nullptr;
}

OfflinePageTabHelper::OfflinePageTabHelper(content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      content::WebContentsUserData<OfflinePageTabHelper>(*web_contents),
      mhtml_page_notifier_receivers_(web_contents, this) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
}

OfflinePageTabHelper::~OfflinePageTabHelper() {}

void OfflinePageTabHelper::NotifyMhtmlPageLoadAttempted(
    MHTMLLoadResult load_result,
    const GURL& main_frame_url,
    base::Time date) {
  auto* current_target_frame =
      mhtml_page_notifier_receivers_.GetCurrentTargetFrame();
  if (current_target_frame != current_target_frame->GetOutermostMainFrame()) {
    // Only handle loads from outermost main frames.
    return;
  }
  if (!current_target_frame->IsInPrimaryMainFrame() &&
      !current_target_frame->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kPendingCommit)) {
    // The MHTML load notification attempt is sent in the middle of committing
    // the MHTML document in the renderer. The RenderFrameHost that hosts that
    // document can be the primary main RFH (if it's already used to host the
    // previous document), or a pending commit RFH (if it's newly created for
    // this document). Return early if the RFH is neither of those.
    return;
  }

  bool is_trusted = provisional_offline_info_.trusted_state !=
                    OfflinePageTrustedState::UNTRUSTED;

  // We shouldn't have a trusted page without valid offline info and namespace.
  DCHECK(!(!provisional_offline_info_.IsValid() && is_trusted));

  // If file is untrusted or we are missing the namespace, MHTML load result is
  // reported on the "untrusted" histogram.
  if (is_trusted) {
    // Ensure we have a non-empy namespace.
    DCHECK(
        provisional_offline_info_.offline_page &&
        !provisional_offline_info_.offline_page->client_id.name_space.empty());

    // If we're here, we have valid offline info, so since the page is trusted,
    // we should not use the renderer's information.
    return;
  }

  // Sanity checking the input URL.
  if (!main_frame_url.is_valid() || !main_frame_url.SchemeIsHTTPOrHTTPS())
    return;

  if (!provisional_offline_info_.IsValid())
    provisional_offline_info_ = LoadedOfflinePageInfo::MakeUntrusted();
  provisional_offline_info_.offline_page->url = main_frame_url;

  if (!date.is_null())
    provisional_offline_info_.offline_page->creation_time = date;
}

void OfflinePageTabHelper::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  // Skips non-main frame.
  if (!navigation_handle->IsInPrimaryMainFrame())
    return;

  // The provisional offline info can be cleared no matter how.
  provisional_offline_info_.Clear();
}

void OfflinePageTabHelper::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  // Skips non-main frame.
  if (!navigation_handle->IsInPrimaryMainFrame())
    return;

  if (!navigation_handle->HasCommitted())
    return;

  if (navigation_handle->IsSameDocument())
    return;

  if (offline_info_.IsValid()) {
    // Do not store the offline page we are navigating away from in bfcache.
    // If we managed to establish a network connection, we should reload the
    // full page on back navigation. If not, offline page is fast to load,
    // so back-forward cache is not going to be useful here.
    content::BackForwardCache::DisableForRenderFrameHost(
        navigation_handle->GetPreviousRenderFrameHostId(),
        back_forward_cache::DisabledReason(
            back_forward_cache::DisabledReasonId::kOfflinePage));
  }

  // This is a new navigation so we can invalidate any previously scheduled
  // operations.
  weak_ptr_factory_.InvalidateWeakPtrs();
  reloading_url_on_net_error_ = false;

  FinalizeOfflineInfo(navigation_handle);
  provisional_offline_info_.Clear();

  TryLoadingOfflinePageOnNetError(navigation_handle);
}

void OfflinePageTabHelper::FinalizeOfflineInfo(
    content::NavigationHandle* navigation_handle) {
  offline_info_.Clear();

  if (navigation_handle->IsErrorPage())
    return;

  GURL navigated_url = navigation_handle->GetURL();

  content::WebContents* web_contents = navigation_handle->GetWebContents();
  if (web_contents->GetContentsMimeType() != "multipart/related" &&
      web_contents->GetContentsMimeType() != "message/rfc822") {
    return;
  }

  if (SchemeIsForUntrustedOfflinePages(navigated_url)) {
    // If a MHTML archive is being loaded for file: or content: URL, and we did
    // get a message from the renderer describing the contents, the results of
    // that message will be stored in |provisional_offline_info_|.
    if (provisional_offline_info_.IsValid()) {
      offline_info_ = std::move(provisional_offline_info_);
      provisional_offline_info_.Clear();
    } else {
      // Otherwise, just use an empty untrusted page.
      offline_info_ = LoadedOfflinePageInfo::MakeUntrusted();
      offline_info_.offline_page->url = navigated_url;
    }
  } else if (navigated_url.SchemeIsHTTPOrHTTPS()) {
    // For http/https URL, commit the provisional offline info if any.
    if (provisional_offline_info_.IsValid()) {
      DCHECK(EqualsIgnoringFragment(
          navigated_url, provisional_offline_info_.offline_page->url));
      offline_info_ = std::move(provisional_offline_info_);
      provisional_offline_info_.Clear();
    }
  }
}

void OfflinePageTabHelper::TryLoadingOfflinePageOnNetError(
    content::NavigationHandle* navigation_handle) {
  // If the offline page has been loaded successfully, nothing more to do.
  net::Error error_code = navigation_handle->GetNetErrorCode();
  if (error_code == net::OK)
    return;

  // We might be reloading the URL in order to fetch the offline page.
  // * If successful, nothing to do.
  // * Otherwise, we're hitting error again. Bail out to avoid loop.
  if (reloading_url_on_net_error_)
    return;

  // When the navigation starts, the request might be intercepted to serve the
  // offline content if the network is detected to be in disconnected or poor
  // conditions. This detection might not work for some cases, i.e., connected
  // to a hotspot or proxy that does not have network, and the navigation will
  // eventually fail. To handle this, we will reload the page to force the
  // offline interception if the error code matches the following list.
  // Otherwise, the error page will be shown.
  if (error_code != net::ERR_INTERNET_DISCONNECTED &&
      error_code != net::ERR_NAME_NOT_RESOLVED &&
      error_code != net::ERR_ADDRESS_UNREACHABLE &&
      error_code != net::ERR_PROXY_CONNECTION_FAILED) {
    return;
  }

  // When there is no valid tab android there is nowhere to show the offline
  // page, so we can leave.
  int tab_id;
  if (!OfflinePageUtils::GetTabId(web_contents(), &tab_id)) {
    // No need to report NO_TAB_ID since it should have already been detected
    // and reported in offline page request handler.
    return;
  }

  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  PageCriteria criteria;
  criteria.url = navigation_handle->GetURL();
  criteria.pages_for_tab_id = tab_id;
  criteria.maximum_matches = 1;
  OfflinePageUtils::SelectPagesWithCriteria(
      profile->GetProfileKey(), criteria,
      base::BindOnce(&OfflinePageTabHelper::SelectPagesForURLDone,
                     weak_ptr_factory_.GetWeakPtr()));
}

void OfflinePageTabHelper::SelectPagesForURLDone(
    const std::vector<OfflinePageItem>& offline_pages) {
  // Bails out if no offline page is found.
  if (offline_pages.empty()) {
    return;
  }

  reloading_url_on_net_error_ = true;

  // Reloads the page with extra header set to force loading the offline page.
  content::NavigationController::LoadURLParams load_params(
      offline_pages.front().url);
  load_params.transition_type = ui::PAGE_TRANSITION_RELOAD;
  OfflinePageHeader offline_header;
  offline_header.reason = OfflinePageHeader::Reason::NET_ERROR;
  load_params.extra_headers = offline_header.GetCompleteHeaderString();
  web_contents()->GetController().LoadURLWithParams(load_params);
}

// This is a callback from network request interceptor. It happens between
// DidStartNavigation and DidFinishNavigation calls on this tab helper.
void OfflinePageTabHelper::SetOfflinePage(
    const OfflinePageItem& offline_page,
    const OfflinePageHeader& offline_header,
    OfflinePageTrustedState trusted_state,
    bool is_offline_preview) {
  provisional_offline_info_.offline_page =
      std::make_unique<OfflinePageItem>(offline_page);
  provisional_offline_info_.offline_header = offline_header;
  provisional_offline_info_.trusted_state = trusted_state;
  provisional_offline_info_.is_showing_offline_preview = is_offline_preview;
}

void OfflinePageTabHelper::ClearOfflinePage() {
  provisional_offline_info_.Clear();
  offline_info_.Clear();
}

bool OfflinePageTabHelper::IsShowingTrustedOfflinePage() const {
  return offline_info_.offline_page &&
         (offline_info_.trusted_state != OfflinePageTrustedState::UNTRUSTED);
}

bool OfflinePageTabHelper::IsLoadingOfflinePage() const {
  return provisional_offline_info_.offline_page.get() != nullptr;
}

const OfflinePageItem* OfflinePageTabHelper::GetOfflinePageForTest() const {
  return provisional_offline_info_.offline_page.get();
}

OfflinePageTrustedState OfflinePageTabHelper::GetTrustedStateForTest() const {
  return provisional_offline_info_.trusted_state;
}

void OfflinePageTabHelper::SetCurrentTargetFrameForTest(
    content::RenderFrameHost* render_frame_host) {
  mhtml_page_notifier_receivers_.SetCurrentTargetFrameForTesting(
      render_frame_host);
}

const OfflinePageItem* OfflinePageTabHelper::GetOfflinePreviewItem() const {
  if (provisional_offline_info_.is_showing_offline_preview)
    return provisional_offline_info_.offline_page.get();
  if (offline_info_.is_showing_offline_preview)
    return offline_info_.offline_page.get();
  return nullptr;
}

void OfflinePageTabHelper::ScheduleDownloadHelper(
    content::WebContents* web_contents,
    const std::string& name_space,
    const GURL& url,
    OfflinePageUtils::DownloadUIActionFlags ui_action,
    const std::string& request_origin) {
  OfflinePageUtils::CheckDuplicateDownloads(
      web_contents->GetBrowserContext(), url,
      base::BindOnce(
          &OfflinePageTabHelper::DuplicateCheckDoneForScheduleDownload,
          weak_ptr_factory_.GetWeakPtr(), web_contents, name_space, url,
          ui_action, request_origin));
}

void OfflinePageTabHelper::DuplicateCheckDoneForScheduleDownload(
    content::WebContents* web_contents,
    const std::string& name_space,
    const GURL& url,
    OfflinePageUtils::DownloadUIActionFlags ui_action,
    const std::string& request_origin,
    OfflinePageUtils::DuplicateCheckResult result) {
  if (result != OfflinePageUtils::DuplicateCheckResult::NOT_FOUND) {
    if (static_cast<int>(ui_action) &
        static_cast<int>(
            OfflinePageUtils::DownloadUIActionFlags::PROMPT_DUPLICATE)) {
      OfflinePageUtils::ShowDuplicatePrompt(
          base::BindOnce(&OfflinePageTabHelper::DoDownloadPageLater,
                         weak_ptr_factory_.GetWeakPtr(), web_contents,
                         name_space, url, ui_action, request_origin),
          url,
          result ==
              OfflinePageUtils::DuplicateCheckResult::DUPLICATE_REQUEST_FOUND,
          web_contents);
      return;
    }
  }

  DoDownloadPageLater(web_contents, name_space, url, ui_action, request_origin);
}

void OfflinePageTabHelper::DoDownloadPageLater(
    content::WebContents* web_contents,
    const std::string& name_space,
    const GURL& url,
    OfflinePageUtils::DownloadUIActionFlags ui_action,
    const std::string& request_origin) {
  offline_pages::RequestCoordinator* request_coordinator =
      offline_pages::RequestCoordinatorFactory::GetForBrowserContext(
          web_contents->GetBrowserContext());
  if (!request_coordinator)
    return;

  offline_pages::RequestCoordinator::SavePageLaterParams params;
  params.url = url;
  params.client_id = offline_pages::ClientId(
      name_space, base::Uuid::GenerateRandomV4().AsLowercaseString());
  params.request_origin = request_origin;
  request_coordinator->SavePageLater(params, base::DoNothing());

  if (static_cast<int>(ui_action) &
      static_cast<int>(OfflinePageUtils::DownloadUIActionFlags::
                           SHOW_TOAST_ON_NEW_DOWNLOAD)) {
    OfflinePageUtils::ShowDownloadingToast();
  }
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(OfflinePageTabHelper);

}  // namespace offline_pages