// Copyright 2015 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_utils.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/net/net_error_tab_helper.h"
#include "chrome/browser/offline_pages/offline_page_mhtml_archiver.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/offline_page_origin_utils.h"
#include "chrome/browser/offline_pages/offline_page_tab_helper.h"
#include "chrome/browser/offline_pages/request_coordinator_factory.h"
#include "components/offline_pages/core/background/request_coordinator.h"
#include "components/offline_pages/core/background/save_page_request.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/offline_clock.h"
#include "components/offline_pages/core/offline_page_client_policy.h"
#include "components/offline_pages/core/offline_page_feature.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/request_header/offline_page_header.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/mime_util.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/download/android/download_controller_base.h"
#endif // BUILDFLAG(IS_ANDROID)
namespace offline_pages {
namespace {
class OfflinePageComparer {
public:
OfflinePageComparer() = default;
bool operator()(const OfflinePageItem& a, const OfflinePageItem& b) {
return a.creation_time > b.creation_time;
}
};
bool IsSupportedByDownload(content::BrowserContext* browser_context,
const std::string& name_space) {
return GetPolicy(name_space).is_supported_by_download;
}
void CheckDuplicateOngoingDownloads(
content::BrowserContext* browser_context,
const GURL& url,
OfflinePageUtils::DuplicateCheckCallback callback) {
RequestCoordinator* request_coordinator =
RequestCoordinatorFactory::GetForBrowserContext(browser_context);
if (!request_coordinator)
return;
auto request_coordinator_continuation =
[](content::BrowserContext* browser_context, const GURL& url,
OfflinePageUtils::DuplicateCheckCallback callback,
std::vector<std::unique_ptr<SavePageRequest>> requests) {
base::Time latest_request_time;
for (auto& request : requests) {
if (IsSupportedByDownload(browser_context,
request->client_id().name_space) &&
request->url() == url &&
latest_request_time < request->creation_time()) {
latest_request_time = request->creation_time();
}
}
if (latest_request_time.is_null()) {
std::move(callback).Run(
OfflinePageUtils::DuplicateCheckResult::NOT_FOUND);
} else {
std::move(callback).Run(
OfflinePageUtils::DuplicateCheckResult::DUPLICATE_REQUEST_FOUND);
}
};
request_coordinator->GetAllRequests(
base::BindOnce(request_coordinator_continuation, browser_context, url,
std::move(callback)));
}
void DoCalculateSizeBetween(
offline_pages::SizeInBytesCallback callback,
const base::Time& begin_time,
const base::Time& end_time,
const offline_pages::MultipleOfflinePageItemResult& result) {
int64_t total_size = 0;
for (auto& page : result) {
if (begin_time <= page.creation_time && page.creation_time < end_time)
total_size += page.file_size;
}
std::move(callback).Run(total_size);
}
content::WebContents* GetWebContentsByFrameID(int render_process_id,
int render_frame_id) {
content::RenderFrameHost* render_frame_host =
content::RenderFrameHost::FromID(render_process_id, render_frame_id);
if (!render_frame_host)
return NULL;
return content::WebContents::FromRenderFrameHost(render_frame_host);
}
content::WebContents::Getter GetWebContentsGetter(
content::WebContents* web_contents) {
// The FrameTreeNode ID should be used to access the WebContents.
int frame_tree_node_id =
web_contents->GetPrimaryMainFrame()->GetFrameTreeNodeId();
if (frame_tree_node_id != content::RenderFrameHost::kNoFrameTreeNodeId) {
return base::BindRepeating(content::WebContents::FromFrameTreeNodeId,
frame_tree_node_id);
}
// In other cases, use the RenderProcessHost ID + RenderFrameHost ID to get
// the WebContents.
return base::BindRepeating(
&GetWebContentsByFrameID,
web_contents->GetPrimaryMainFrame()->GetProcess()->GetID(),
web_contents->GetPrimaryMainFrame()->GetRoutingID());
}
void AcquireFileAccessPermissionDoneForScheduleDownload(
const content::WebContents::Getter& wc_getter,
const std::string& name_space,
const GURL& url,
OfflinePageUtils::DownloadUIActionFlags ui_action,
const std::string& request_origin,
bool granted) {
if (!granted)
return;
content::WebContents* web_contents = wc_getter.Run();
if (!web_contents) {
return;
}
OfflinePageTabHelper* tab_helper =
OfflinePageTabHelper::FromWebContents(web_contents);
if (!tab_helper)
return;
tab_helper->ScheduleDownloadHelper(web_contents, name_space, url, ui_action,
request_origin);
}
} // namespace
// static
const base::FilePath::CharType OfflinePageUtils::kMHTMLExtension[] =
FILE_PATH_LITERAL("mhtml");
// static
void OfflinePageUtils::SelectPagesForURL(
SimpleFactoryKey* key,
const GURL& url,
int tab_id,
base::OnceCallback<void(const std::vector<OfflinePageItem>&)> callback) {
PageCriteria criteria;
criteria.url = url;
criteria.pages_for_tab_id = tab_id;
SelectPagesWithCriteria(key, criteria, std::move(callback));
}
// static
void OfflinePageUtils::SelectPagesWithCriteria(
SimpleFactoryKey* key,
const PageCriteria& criteria,
base::OnceCallback<void(const std::vector<OfflinePageItem>&)> callback) {
OfflinePageModel* offline_page_model =
OfflinePageModelFactory::GetForKey(key);
if (!offline_page_model) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), std::vector<OfflinePageItem>()));
return;
}
offline_page_model->GetPagesWithCriteria(criteria, std::move(callback));
}
const OfflinePageItem* OfflinePageUtils::GetOfflinePageFromWebContents(
content::WebContents* web_contents) {
OfflinePageTabHelper* tab_helper =
OfflinePageTabHelper::FromWebContents(web_contents);
if (!tab_helper)
return nullptr;
const OfflinePageItem* offline_page = tab_helper->offline_page();
if (!offline_page)
return nullptr;
// If a pending navigation that hasn't committed yet, don't return the cached
// offline page that was set at the last commit time. This is to prevent
// from returning the wrong offline page if DidStartNavigation is never called
// to clear it up.
if (!EqualsIgnoringFragment(web_contents->GetVisibleURL(),
web_contents->GetLastCommittedURL())) {
return nullptr;
}
return offline_page;
}
// static
const OfflinePageHeader* OfflinePageUtils::GetOfflineHeaderFromWebContents(
content::WebContents* web_contents) {
OfflinePageTabHelper* tab_helper =
OfflinePageTabHelper::FromWebContents(web_contents);
return tab_helper ? &(tab_helper->offline_header()) : nullptr;
}
// static
bool OfflinePageUtils::IsShowingOfflinePreview(
content::WebContents* web_contents) {
OfflinePageTabHelper* tab_helper =
OfflinePageTabHelper::FromWebContents(web_contents);
return tab_helper && tab_helper->GetOfflinePreviewItem();
}
// static
bool OfflinePageUtils::IsShowingDownloadButtonInErrorPage(
content::WebContents* web_contents) {
chrome_browser_net::NetErrorTabHelper* tab_helper =
chrome_browser_net::NetErrorTabHelper::FromWebContents(web_contents);
return tab_helper && tab_helper->is_showing_download_button_in_error_page();
}
// static
GURL OfflinePageUtils::GetOriginalURLFromWebContents(
content::WebContents* web_contents) {
content::NavigationEntry* entry =
web_contents->GetController().GetLastCommittedEntry();
if (!entry || entry->GetRedirectChain().size() <= 1)
return GURL();
return entry->GetRedirectChain().front();
}
// static
void OfflinePageUtils::CheckDuplicateDownloads(
content::BrowserContext* browser_context,
const GURL& url,
DuplicateCheckCallback callback) {
// First check for finished downloads, that is, saved pages.
OfflinePageModel* offline_page_model =
OfflinePageModelFactory::GetForBrowserContext(browser_context);
if (!offline_page_model)
return;
auto continuation = [](content::BrowserContext* browser_context,
const GURL& url, DuplicateCheckCallback callback,
const std::vector<OfflinePageItem>& pages) {
base::Time latest_saved_time;
for (const auto& offline_page_item : pages) {
if (IsSupportedByDownload(browser_context,
offline_page_item.client_id.name_space) &&
latest_saved_time < offline_page_item.creation_time) {
latest_saved_time = offline_page_item.creation_time;
}
}
if (latest_saved_time.is_null()) {
// Then check for ongoing downloads, that is, requests.
CheckDuplicateOngoingDownloads(browser_context, url, std::move(callback));
} else {
std::move(callback).Run(DuplicateCheckResult::DUPLICATE_PAGE_FOUND);
}
};
PageCriteria criteria;
criteria.url = url;
offline_page_model->GetPagesWithCriteria(
criteria,
base::BindOnce(continuation, browser_context, url, std::move(callback)));
}
// static
void OfflinePageUtils::ScheduleDownload(content::WebContents* web_contents,
const std::string& name_space,
const GURL& url,
DownloadUIActionFlags ui_action,
const std::string& request_origin) {
if (!web_contents)
return;
// Ensure that the storage permission is granted since the archive file is
// going to be placed in the public directory.
AcquireFileAccessPermission(
web_contents,
base::BindOnce(&AcquireFileAccessPermissionDoneForScheduleDownload,
GetWebContentsGetter(web_contents), name_space, url,
ui_action, request_origin));
}
// static
void OfflinePageUtils::ScheduleDownload(content::WebContents* web_contents,
const std::string& name_space,
const GURL& url,
DownloadUIActionFlags ui_action) {
std::string origin =
OfflinePageOriginUtils::GetEncodedOriginAppFor(web_contents);
ScheduleDownload(web_contents, name_space, url, ui_action, origin);
}
// static
bool OfflinePageUtils::CanDownloadAsOfflinePage(
const GURL& url,
const std::string& contents_mime_type) {
return OfflinePageModel::CanSaveURL(url) &&
(net::MatchesMimeType(contents_mime_type, "text/html") ||
net::MatchesMimeType(contents_mime_type, "application/xhtml+xml"));
}
// static
bool OfflinePageUtils::GetCachedOfflinePageSizeBetween(
content::BrowserContext* browser_context,
SizeInBytesCallback callback,
const base::Time& begin_time,
const base::Time& end_time) {
OfflinePageModel* offline_page_model =
OfflinePageModelFactory::GetForBrowserContext(browser_context);
if (!offline_page_model || begin_time > end_time)
return false;
PageCriteria criteria;
criteria.lifetime_type = LifetimeType::TEMPORARY;
offline_page_model->GetPagesWithCriteria(
criteria, base::BindOnce(&DoCalculateSizeBetween, std::move(callback),
begin_time, end_time));
return true;
}
// static
std::string OfflinePageUtils::ExtractOfflineHeaderValueFromNavigationEntry(
content::NavigationEntry* entry) {
std::string extra_headers = entry->GetExtraHeaders();
if (extra_headers.empty())
return std::string();
// The offline header will be the only extra header if it is present.
std::string offline_header_key(offline_pages::kOfflinePageHeader);
offline_header_key += ": ";
if (!base::StartsWith(extra_headers, offline_header_key,
base::CompareCase::INSENSITIVE_ASCII)) {
return std::string();
}
std::string header_value = extra_headers.substr(offline_header_key.length());
if (header_value.find("\n") != std::string::npos)
return std::string();
return header_value;
}
// static
bool OfflinePageUtils::IsShowingTrustedOfflinePage(
content::WebContents* web_contents) {
OfflinePageTabHelper* tab_helper =
OfflinePageTabHelper::FromWebContents(web_contents);
return tab_helper && tab_helper->IsShowingTrustedOfflinePage();
}
// static
void OfflinePageUtils::AcquireFileAccessPermission(
content::WebContents* web_contents,
base::OnceCallback<void(bool)> callback) {
#if BUILDFLAG(IS_ANDROID)
content::WebContents::Getter web_contents_getter =
GetWebContentsGetter(web_contents);
DownloadControllerBase::Get()->AcquireFileAccessPermission(
web_contents_getter, std::move(callback));
#else
// Not needed in other platforms.
std::move(callback).Run(true /*granted*/);
#endif // BUILDFLAG(IS_ANDROID)
}
} // namespace offline_pages