chromium/chrome/browser/offline_pages/offline_page_utils.cc

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