| // 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() = default; |
| |
| 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 |