chromium/chrome/browser/offline_pages/offline_page_request_handler.cc

// Copyright 2018 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_request_handler.h"

#include <utility>

#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/offline_page_tab_helper.h"
#include "chrome/browser/offline_pages/offline_page_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.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_feature.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_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/file_stream.h"
#include "net/base/filename_util.h"
#include "net/base/io_buffer.h"
#include "net/base/network_change_notifier.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request_redirect_job.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"

namespace offline_pages {

namespace {

// This enum is used to tell all possible outcomes of handling network requests
// that might serve offline contents.
enum class RequestResult {
  // Offline page was shown for current URL.
  OFFLINE_PAGE_SERVED,
  // Redirected from original URL to final URL in preparation to show the
  // offline page under final URL. OFFLINE_PAGE_SERVED is most likely to be
  // reported next if no other error is encountered.
  REDIRECTED,
  // Tab was gone.
  NO_TAB_ID,
  // Web contents was gone.
  NO_WEB_CONTENTS,
  // The offline page found was not fresh enough, i.e. not created in the past
  // day. This only applies in prohibitively slow network.
  PAGE_NOT_FRESH,
  // Offline page was not found, by searching with either final URL or original
  // URL.
  OFFLINE_PAGE_NOT_FOUND,
  // Digest for the archive file does not match with the one saved in the
  // metadata database.
  DIGEST_MISMATCH,
  // The archive file does not exist.
  FILE_NOT_FOUND,
};

// Consistent with the buffer size used in url request data reading.
const size_t kMaxBufferSizeForValidation = 4096;

void GetFileSize(const base::FilePath& file_path, int64_t* file_size) {
  bool succeeded = base::GetFileSize(file_path, file_size);
  if (!succeeded) {
    // Use -1 to indicate that file is not found.
    *file_size = -1;
  }
}

void UpdateDigest(
    const scoped_refptr<OfflinePageRequestHandler::ThreadSafeArchiveValidator>&
        validator,
    scoped_refptr<net::IOBuffer> buffer,
    size_t len) {
  validator->Update(buffer->data(), len);
}

OfflinePageModel* GetOfflinePageModel(
    content::WebContents::Getter web_contents_getter) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  content::WebContents* web_contents = web_contents_getter.Run();
  return web_contents ? OfflinePageModelFactory::GetForBrowserContext(
                            web_contents->GetBrowserContext())
                      : nullptr;
}

// Notifies OfflinePageRequestHandler about all the matched offline pages.
void NotifyAvailableOfflinePagesOnUI(
    base::WeakPtr<OfflinePageRequestHandler> job,
    const std::vector<OfflinePageRequestHandler::Candidate>& candidates) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (job)
    job->OnOfflinePagesAvailable(candidates);
}

// Failed to find an offline page.
void FailedToFindOfflinePage(
    RequestResult request_error_result,
    OfflinePageRequestHandler::NetworkState network_state,
    base::WeakPtr<OfflinePageRequestHandler> job) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK_NE(RequestResult::OFFLINE_PAGE_SERVED, request_error_result);

  NotifyAvailableOfflinePagesOnUI(
      job, std::vector<OfflinePageRequestHandler::Candidate>());
}

// Handles the result of finding matched offline pages.
void SelectPagesForURLDone(
    const GURL& url,
    const OfflinePageHeader& offline_header,
    OfflinePageRequestHandler::NetworkState network_state,
    base::WeakPtr<OfflinePageRequestHandler> job,
    content::WebContents::Getter web_contents_getter,
    const std::vector<OfflinePageItem>& offline_pages) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Bail out if no page is found.
  if (offline_pages.empty()) {
    FailedToFindOfflinePage(RequestResult::OFFLINE_PAGE_NOT_FOUND,
                            network_state, job);
    return;
  }

  // Bail out if web_contents is gone.
  OfflinePageModel* offline_page_model =
      GetOfflinePageModel(web_contents_getter);
  if (!offline_page_model) {
    FailedToFindOfflinePage(RequestResult::NO_WEB_CONTENTS, network_state, job);
    return;
  }

  std::vector<OfflinePageRequestHandler::Candidate> candidates;
  for (const auto& offline_page : offline_pages) {
    OfflinePageRequestHandler::Candidate candidate;
    candidate.offline_page = offline_page;
    candidate.archive_is_in_internal_dir =
        offline_page_model->IsArchiveInInternalDir(offline_page.file_path);
    candidates.push_back(candidate);
  }

  NotifyAvailableOfflinePagesOnUI(job, candidates);
}

// Handles the result of finding an offline page with the requested offline ID.
void GetPageByOfflineIdDone(
    const GURL& url,
    const OfflinePageHeader& offline_header,
    OfflinePageRequestHandler::NetworkState network_state,
    content::WebContents::Getter web_contents_getter,
    base::WeakPtr<OfflinePageRequestHandler> job,
    const OfflinePageItem* offline_page) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // If the found offline page does not match the request URL, fail.
  if (!offline_page || offline_page->url != url) {
    FailedToFindOfflinePage(RequestResult::OFFLINE_PAGE_NOT_FOUND,
                            network_state, job);
    return;
  }

  std::vector<OfflinePageItem> offline_pages;
  offline_pages.push_back(*offline_page);
  SelectPagesForURLDone(url, offline_header, network_state, job,
                        web_contents_getter, offline_pages);
}

// Tries to find all the offline pages to serve for |url|.
void GetPagesToServeURL(
    const GURL& url,
    const OfflinePageHeader& offline_header,
    OfflinePageRequestHandler::NetworkState network_state,
    content::WebContents::Getter web_contents_getter,
    OfflinePageRequestHandler::Delegate::TabIdGetter tab_id_getter,
    base::WeakPtr<OfflinePageRequestHandler> job) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  content::WebContents* web_contents = web_contents_getter.Run();
  if (!web_contents) {
    FailedToFindOfflinePage(RequestResult::NO_WEB_CONTENTS, network_state, job);
    return;
  }
  int tab_id;
  if (!tab_id_getter.Run(web_contents, &tab_id)) {
    FailedToFindOfflinePage(RequestResult::NO_TAB_ID, network_state, job);
    return;
  }

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  SimpleFactoryKey* key = profile->GetProfileKey();

  // If an int64 offline ID is present in the offline header, try to load that
  // particular version.
  if (!offline_header.id.empty()) {
    int64_t offline_id;
    if (base::StringToInt64(offline_header.id, &offline_id)) {
      OfflinePageModel* offline_page_model =
          OfflinePageModelFactory::GetForKey(key);
      if (!offline_page_model) {
        FailedToFindOfflinePage(RequestResult::OFFLINE_PAGE_NOT_FOUND,
                                network_state, job);
        return;
      }
      offline_page_model->GetPageByOfflineId(
          offline_id,
          base::BindOnce(&GetPageByOfflineIdDone, url, offline_header,
                         network_state, web_contents_getter, job));
      return;
    }
  }

  OfflinePageUtils::SelectPagesForURL(
      key, url, tab_id,
      base::BindOnce(&SelectPagesForURLDone, url, offline_header, network_state,
                     job, web_contents_getter));
}

// Do all the things needed to be done on UI thread after a trusted offline
// page has been visited.
void VisitTrustedOfflinePageOnUI(
    const OfflinePageHeader& offline_header,
    OfflinePageRequestHandler::NetworkState network_state,
    content::WebContents::Getter web_contents_getter,
    const OfflinePageItem& offline_page,
    bool archive_is_in_internal_dir) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // |web_contents_getter| is passed from IO thread. We need to check if
  // web contents is still valid.
  content::WebContents* web_contents = web_contents_getter.Run();
  if (!web_contents)
    return;

  OfflinePageModel* offline_page_model =
      OfflinePageModelFactory::GetForBrowserContext(
          web_contents->GetBrowserContext());
  if (!offline_page_model)
    return;

  // Mark the offline page as being accessed.
  offline_page_model->MarkPageAccessed(offline_page.offline_id);

  // Save an cached copy of OfflinePageItem such that Tab code can get
  // the loaded offline page immediately.
  OfflinePageTabHelper* tab_helper =
      OfflinePageTabHelper::FromWebContents(web_contents);
  DCHECK(tab_helper);
  tab_helper->SetOfflinePage(
      offline_page, offline_header,
      archive_is_in_internal_dir
          ? OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR
          : OfflinePageTrustedState::TRUSTED_AS_UNMODIFIED_AND_IN_PUBLIC_DIR,
      network_state ==
          OfflinePageRequestHandler::NetworkState::PROHIBITIVELY_SLOW_NETWORK);
}

void ClearOfflinePageData(content::WebContents::Getter web_contents_getter) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // |web_contents_getter| is passed from IO thread. We need to check if
  // web contents is still valid.
  content::WebContents* web_contents = web_contents_getter.Run();
  if (!web_contents)
    return;

  // Save an cached copy of OfflinePageItem such that Tab code can get
  // the loaded offline page immediately.
  OfflinePageTabHelper* tab_helper =
      OfflinePageTabHelper::FromWebContents(web_contents);
  DCHECK(tab_helper);
  tab_helper->ClearOfflinePage();
}

}  // namespace

OfflinePageRequestHandler::OfflinePageRequestHandler(
    const GURL& url,
    const net::HttpRequestHeaders& extra_request_headers,
    Delegate* delegate)
    : url_(url),
      delegate_(delegate),
      network_state_(NetworkState::CONNECTED_NETWORK),
      candidate_index_(0) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  std::string offline_header_value =
      extra_request_headers.GetHeader(kOfflinePageHeader)
          .value_or(std::string());
  // Note that |offline_header| will be empty if parsing from the header value
  // fails.
  offline_header_ = OfflinePageHeader(offline_header_value);
}

OfflinePageRequestHandler::~OfflinePageRequestHandler() {}

OfflinePageRequestHandler::NetworkState
OfflinePageRequestHandler::GetNetworkState() const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (offline_header_.reason == OfflinePageHeader::Reason::NET_ERROR)
    return OfflinePageRequestHandler::NetworkState::FLAKY_NETWORK;

  bool is_offline = net::NetworkChangeNotifier::IsOffline();
  if (offline_pages::IsOfflinePagesNetworkStateLikelyUnknown()) {
    is_offline = false;
  }

  if (is_offline)
    return OfflinePageRequestHandler::NetworkState::DISCONNECTED_NETWORK;

  // If RELOAD is present in the offline header, load the live page.
  if (offline_header_.reason == OfflinePageHeader::Reason::RELOAD)
    return OfflinePageRequestHandler::NetworkState::CONNECTED_NETWORK;

  // If other reason is present in the offline header, force loading the offline
  // page even when the network is connected.
  if (offline_header_.reason != OfflinePageHeader::Reason::NONE) {
    return OfflinePageRequestHandler::NetworkState::
        FORCE_OFFLINE_ON_CONNECTED_NETWORK;
  }

  // Otherwise, the network state is a good network.
  return OfflinePageRequestHandler::NetworkState::CONNECTED_NETWORK;
}

void OfflinePageRequestHandler::Start() {
  network_state_ = GetNetworkState();
  if (network_state_ == NetworkState::CONNECTED_NETWORK) {
    delegate_->FallbackToDefault();
    return;
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&OfflinePageRequestHandler::StartAsync,
                                weak_ptr_factory_.GetWeakPtr()));
}

void OfflinePageRequestHandler::StartAsync() {
  if (content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
    GetPagesToServeURL(url_, offline_header_, network_state_,
                       delegate_->GetWebContentsGetter(),
                       delegate_->GetTabIdGetter(),
                       weak_ptr_factory_.GetWeakPtr());
  } else {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&GetPagesToServeURL, url_, offline_header_,
                       network_state_, delegate_->GetWebContentsGetter(),
                       delegate_->GetTabIdGetter(),
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void OfflinePageRequestHandler::Kill() {
  stream_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
}

bool OfflinePageRequestHandler::IsServingOfflinePage() const {
  return candidates_.size() > 0 && candidate_index_ < candidates_.size();
}

scoped_refptr<net::HttpResponseHeaders>
OfflinePageRequestHandler::GetRedirectHeaders() {
  return fake_headers_for_redirect_;
}

int OfflinePageRequestHandler::ReadRawData(net::IOBuffer* dest, int dest_size) {
  DCHECK_NE(dest_size, 0);

  return stream_->Read(
      dest, dest_size,
      base::BindOnce(&OfflinePageRequestHandler::DidReadForServing,
                     weak_ptr_factory_.GetWeakPtr(),
                     base::WrapRefCounted(dest)));
}

void OfflinePageRequestHandler::OnOfflinePagesAvailable(
    const std::vector<Candidate>& candidates) {
  // If no offline page is found, restart the job to fall back to the default
  // handling.
  if (candidates.empty()) {
    delegate_->FallbackToDefault();
    return;
  }

  file_task_runner_ = base::ThreadPool::CreateTaskRunner(
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});

  // Start the file validation from the 1st offline page.
  candidates_ = candidates;
  candidate_index_ = 0;
  ValidateFile();
}

void OfflinePageRequestHandler::OnTrustedOfflinePageFound() {
  // If the match is for original URL, trigger the redirect.
  // Note: If the offline page has same orginal URL and final URL, don't trigger
  // the redirect. Some websites might route the redirect finally back to itself
  // after intermediate redirects for authentication. Previously this case was
  // not handled and some pages might be saved with same URLs. Though we fixed
  // the problem, we still need to support those pages already saved with this
  if (url_ == GetCurrentOfflinePage().original_url_if_different &&
      url_ != GetCurrentOfflinePage().url) {
    Redirect(GetCurrentOfflinePage().url);
    return;
  }

  // No need to open the file if it has already been opened for the validation.
  if (stream_) {
    DidOpenForServing(net::OK);
    return;
  }

  // If a file:// or content:// intent is being processed, open the file:// or
  // content:// denoted in the intent instead. Otherwise, open the archive file
  // associated with the offline page.
  base::FilePath file_path;
  if (IsProcessingFileUrlIntent()) {
    bool valid = net::FileURLToFilePath(offline_header_.intent_url, &file_path);
    DCHECK(valid);
#if BUILDFLAG(IS_ANDROID)
  } else if (IsProcessingContentUrlIntent()) {
    file_path = base::FilePath(offline_header_.intent_url.spec());
    DCHECK(file_path.IsContentUri());
#endif  // BUILDFLAG(IS_ANDROID)
  } else {
    file_path = GetCurrentOfflinePage().file_path;
  }
  OpenFile(file_path,
           base::BindRepeating(&OfflinePageRequestHandler::DidOpenForServing,
                               weak_ptr_factory_.GetWeakPtr()));
}

void OfflinePageRequestHandler::VisitTrustedOfflinePage() {
  delegate_->SetOfflinePageNavigationUIData(true /*is_offline_page*/);

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&VisitTrustedOfflinePageOnUI, offline_header_,
                     network_state_, delegate_->GetWebContentsGetter(),
                     GetCurrentOfflinePage(),
                     candidates_[candidate_index_].archive_is_in_internal_dir));
}

void OfflinePageRequestHandler::Redirect(const GURL& redirected_url) {
  std::string header_string = base::StringPrintf(
      "HTTP/1.1 %i Internal Redirect\n"
      // Clear referrer to avoid leak when going online.
      "Referrer-Policy: no-referrer\n"
      "Location: %s\n"
      "Non-Authoritative-Reason: offline redirects",
      // 302 is used to remove response bodies in order to
      // avoid leak when going online.
      static_cast<int>(net::RedirectUtil::ResponseCode::REDIRECT_302_FOUND),
      redirected_url.spec().c_str());

  fake_headers_for_redirect_ = base::MakeRefCounted<net::HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(header_string));
  DCHECK(fake_headers_for_redirect_->IsRedirect(nullptr));

  delegate_->NotifyHeadersComplete(0);
}

const OfflinePageItem& OfflinePageRequestHandler::GetCurrentOfflinePage()
    const {
  return candidates_[candidate_index_].offline_page;
}

bool OfflinePageRequestHandler::IsProcessingFileUrlIntent() const {
  return offline_header_.reason == OfflinePageHeader::Reason::FILE_URL_INTENT;
}

bool OfflinePageRequestHandler::IsProcessingContentUrlIntent() const {
  return offline_header_.reason ==
         OfflinePageHeader::Reason::CONTENT_URL_INTENT;
}

bool OfflinePageRequestHandler::IsProcessingFileOrContentUrlIntent() const {
  return IsProcessingFileUrlIntent() || IsProcessingContentUrlIntent();
}

void OfflinePageRequestHandler::OpenFile(
    const base::FilePath& file_path,
    const base::RepeatingCallback<void(int)>& callback) {
  if (!stream_)
    stream_ = std::make_unique<net::FileStream>(file_task_runner_);

  int flags =
      base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_ASYNC;
#if BUILDFLAG(IS_WIN)
  flags |= base::File::FLAG_WIN_EXCLUSIVE_READ;
#endif  // BUILDFLAG(IS_WIN)
  int result = stream_->Open(file_path, flags, callback);
  if (result != net::ERR_IO_PENDING)
    callback.Run(result);
}

void OfflinePageRequestHandler::UpdateDigestOnBackground(
    scoped_refptr<net::IOBuffer> buffer,
    size_t len,
    base::OnceCallback<void(void)> digest_updated_callback) {
  DCHECK_GT(len, 0u);

  if (!archive_validator_)
    archive_validator_ = new ThreadSafeArchiveValidator();

  // Delegate to background task runner to update the hash since it is time
  // consuming. Once it is done, |digest_updated_callback| will be called to
  // continue the reading.
  file_task_runner_->PostTaskAndReply(
      FROM_HERE, base::BindOnce(&UpdateDigest, archive_validator_, buffer, len),
      std::move(digest_updated_callback));
}

void OfflinePageRequestHandler::FinalizeDigestOnBackground(
    base::OnceCallback<void(const std::string&)> digest_finalized_callback) {
  if (!archive_validator_)
    archive_validator_ = new ThreadSafeArchiveValidator();

  // Delegate to background task runner to finalize the hash to get the digest
  // since it is time consuming. Once it is done, |digest_finalized_callback|
  // will be called with the digest.
  file_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&ThreadSafeArchiveValidator::Finish, archive_validator_),
      std::move(digest_finalized_callback));
}

void OfflinePageRequestHandler::ValidateFile() {
  // If the archive file is in internal directory, the offline page can be
  // deemed as trusted without going through valication.
  if (candidates_[candidate_index_].archive_is_in_internal_dir) {
    OnTrustedOfflinePageFound();
    return;
  }

  // Otherwise, the archive file is in public directory. If the digest is empty,
  // the validation can fail immediately.
  if (GetCurrentOfflinePage().digest.empty()) {
    OnFileValidationDone(FileValidationResult::FILE_VALIDATION_FAILED);
    return;
  }

  // If a file:// or content:// URL intent is being viewed, skip the validation.
  // The digest for the file:// or content:// denoted in the intent was computed
  // and used to find the offline page. However, we will not validate and read
  // from the archive archive file assoicated with the offline page since it may
  // not exist or even got modified. We will read from the file:// or content://
  // denoted in the intent  and compute the digest of the read data to make sure
  // it does not get changed.
  if (IsProcessingFileOrContentUrlIntent()) {
    OnFileValidationDone(FileValidationResult::FILE_VALIDATION_SUCCEEDED);
    return;
  }

  GetFileSizeForValidation();
}

void OfflinePageRequestHandler::GetFileSizeForValidation() {
  int64_t* file_size = new int64_t(0);
  file_task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&GetFileSize, GetCurrentOfflinePage().file_path,
                     base::Unretained(file_size)),
      base::BindOnce(&OfflinePageRequestHandler::DidGetFileSizeForValidation,
                     weak_ptr_factory_.GetWeakPtr(), base::Owned(file_size)));
}

void OfflinePageRequestHandler::DidGetFileSizeForValidation(
    const int64_t* actual_file_size) {
  if (*actual_file_size == -1) {
    OnFileValidationDone(FileValidationResult::FILE_NOT_FOUND);
    return;
  }

  if (*actual_file_size != GetCurrentOfflinePage().file_size) {
    OnFileValidationDone(FileValidationResult::FILE_VALIDATION_FAILED);
    return;
  }

  // Open file to compute the digest.
  OpenFile(GetCurrentOfflinePage().file_path,
           base::BindRepeating(&OfflinePageRequestHandler::DidOpenForValidation,
                               weak_ptr_factory_.GetWeakPtr()));
}

void OfflinePageRequestHandler::DidOpenForValidation(int result) {
  if (result != net::OK) {
    OnFileValidationDone(FileValidationResult::FILE_VALIDATION_FAILED);
    return;
  }

  if (!buffer_)
    buffer_ = base::MakeRefCounted<net::IOBufferWithSize>(
        kMaxBufferSizeForValidation);

  ReadForValidation();
}

void OfflinePageRequestHandler::ReadForValidation() {
  int result = stream_->Read(
      buffer_.get(), kMaxBufferSizeForValidation,
      base::BindOnce(&OfflinePageRequestHandler::DidReadForValidation,
                     weak_ptr_factory_.GetWeakPtr()));
  if (result != net::ERR_IO_PENDING)
    DidReadForValidation(result);
}

void OfflinePageRequestHandler::DidReadForValidation(int result) {
  if (result < 0) {
    OnFileValidationDone(FileValidationResult::FILE_VALIDATION_FAILED);
    return;
  }

  if (result > 0) {
    UpdateDigestOnBackground(
        buffer_, result,
        base::BindOnce(&OfflinePageRequestHandler::ReadForValidation,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  // When |result| is 0 (net::OK), it indicates EOF. We need to finalize the
  // hash to get the actual digest.
  FinalizeDigestOnBackground(base::BindOnce(
      &OfflinePageRequestHandler::DidComputeActualDigestForValidation,
      weak_ptr_factory_.GetWeakPtr()));
}

void OfflinePageRequestHandler::DidComputeActualDigestForValidation(
    const std::string& actual_digest) {
  DCHECK(!GetCurrentOfflinePage().digest.empty());
  bool is_trusted = actual_digest == GetCurrentOfflinePage().digest;
  OnFileValidationDone(is_trusted
                           ? FileValidationResult::FILE_VALIDATION_SUCCEEDED
                           : FileValidationResult::FILE_VALIDATION_FAILED);
}

void OfflinePageRequestHandler::OnFileValidationDone(
    FileValidationResult result) {
  if (result == FileValidationResult::FILE_VALIDATION_SUCCEEDED) {
    OnTrustedOfflinePageFound();
    return;
  }

  // The stream is no longer needed.
  stream_.reset();

  // Move to next archive file if there is more.
  candidate_index_++;
  if (candidate_index_ < candidates_.size()) {
    ValidateFile();
    return;
  }

  // Otherwise, no trusted offline page can be found so we fall back to the
  // default handling.
  delegate_->FallbackToDefault();
}

void OfflinePageRequestHandler::DidOpenForServing(int result) {
  // Handle the file opening failure.
  if (result != net::OK) {
    // If the file:// or content:// intent is being processed, don't fall
    // back to the default handling. Instead, we should fail the request.
    if (IsProcessingFileOrContentUrlIntent())
      delegate_->NotifyStartError(net::ERR_FAILED);
    else
      delegate_->FallbackToDefault();
    return;
  }

  // Now we're going to read the archive file to serve the offline page. Do
  // all the necessary reporting and bookkeeping for using this offline page.
  VisitTrustedOfflinePage();

  // Note that we always seek to the beginning of the file because the file may
  // have already been read for validation purpose.
  int seek_result = stream_->Seek(
      0, base::BindOnce(&OfflinePageRequestHandler::DidSeekForServing,
                        weak_ptr_factory_.GetWeakPtr()));
  if (seek_result != net::ERR_IO_PENDING)
    DidSeekForServing(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
}

void OfflinePageRequestHandler::DidSeekForServing(int64_t result) {
  DCHECK_LE(result, 0);

  if (result < 0) {
    delegate_->NotifyStartError(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
    return;
  }

  delegate_->NotifyHeadersComplete(GetCurrentOfflinePage().file_size);
}

void OfflinePageRequestHandler::DidReadForServing(
    scoped_refptr<net::IOBuffer> buf,
    int result) {
  if (result < 0 || !IsProcessingFileOrContentUrlIntent()) {
    buf = nullptr;
    NotifyReadRawDataComplete(result);
    return;
  }

  // At this point, we have result >= 0 && IsProcessingFileOrContentUrlIntent()
  // which means the read succeeds for processing the file:// or content:// URL
  // intent. We need to compute the digest to ensure that the file:// or
  // content:// we read is not modified since the time we received the intent,
  // validated the data provided by file:// or content:// URL, and decided to
  // turn it into the corresponding http/https URL and let
  // OfflinePageRequestHandler handle it.
  if (result > 0) {
    UpdateDigestOnBackground(
        buf, result,
        base::BindOnce(&OfflinePageRequestHandler::NotifyReadRawDataComplete,
                       weak_ptr_factory_.GetWeakPtr(), result));

  } else {
    // When |result| is 0 (net::OK), it indicates EOF. We need to finalize the
    // hash to get the actual digest.
    FinalizeDigestOnBackground(base::BindOnce(
        &OfflinePageRequestHandler::DidComputeActualDigestForServing,
        weak_ptr_factory_.GetWeakPtr(), result));
  }
}

void OfflinePageRequestHandler::NotifyReadRawDataComplete(int result) {
  delegate_->NotifyReadRawDataComplete(result);
}

void OfflinePageRequestHandler::DidComputeActualDigestForServing(
    int result,
    const std::string& actual_digest) {
  // If the actual digest does not match, fail the request job.
  bool mismatch = actual_digest != GetCurrentOfflinePage().digest;
  if (mismatch) {
    // Note: Do not call delegate_->SetOfflinePageNavigationUIData to clear
    // the offline bit since SetOfflinePageNavigationUIData is supposed to
    // be called before the response is being received. Furthermore, there is
    // no need to clear the offline bit since the error code should already
    // indicate that the offline page is not loaded.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&ClearOfflinePageData,
                                  delegate_->GetWebContentsGetter()));
    result = net::ERR_FAILED;
  }

  NotifyReadRawDataComplete(result);
}

}  // namespace offline_pages