// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/download/save_package.h"

#include <algorithm>
#include <utility>

#include "base/bind.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/file_util_icu.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/rand_util.h"
#include "base/stl_util.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task_runner_util.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "components/download/public/common/download_item_impl.h"
#include "components/download/public/common/download_stats.h"
#include "components/download/public/common/download_task_runner.h"
#include "components/download/public/common/download_ukm_helper.h"
#include "components/download/public/common/download_utils.h"
#include "components/filename_generation/filename_generation.h"
#include "components/url_formatter/url_formatter.h"
#include "content/browser/bad_message.h"
#include "content/browser/download/download_manager_impl.h"
#include "content/browser/download/save_file.h"
#include "content/browser/download/save_file_manager.h"
#include "content/browser/download/save_item.h"
#include "content/browser/download/save_package_serialization_handler.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_delegate.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame_messages.h"
#include "content/common/view_messages.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/resource_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/mhtml_generation_params.h"
#include "content/public/common/referrer_type_converters.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/filename_util.h"
#include "net/base/mime_util.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "url/url_constants.h"

namespace content {
namespace {

// Generates unique ids for SavePackage::unique_id_ field.
SavePackageId GetNextSavePackageId() {
  static SavePackageId::Generator g_save_package_id_generator;
  return g_save_package_id_generator.GenerateNextId();
}

// Default name which will be used when we can not get proper name from
// resource URL.
const char kDefaultSaveName[] = "saved_resource";

// Maximum number of file ordinal number. I think it's big enough for resolving
// name-conflict files which has same base file name.
const int32_t kMaxFileOrdinalNumber = 9999;

// Maximum length for file path. Since Windows have MAX_PATH limitation for
// file path, we need to make sure length of file path of every saved file
// is less than MAX_PATH
#if defined(OS_WIN)
const uint32_t kMaxFilePathLength = MAX_PATH - 1;
#elif defined(OS_POSIX) || defined(OS_FUCHSIA)
const uint32_t kMaxFilePathLength = PATH_MAX - 1;
#endif

// Maximum length for file ordinal number part. Since we only support the
// maximum 9999 for ordinal number, which means maximum file ordinal number part
// should be "(9998)", so the value is 6.
const uint32_t kMaxFileOrdinalNumberPartLength = 6;

// Strip current ordinal number, if any. Should only be used on pure
// file names, i.e. those stripped of their extensions.
// TODO(estade): improve this to not choke on alternate encodings.
base::FilePath::StringType StripOrdinalNumber(
    const base::FilePath::StringType& base_name) {
  base::FilePath::StringType::size_type r_paren_index =
      base_name.rfind(FILE_PATH_LITERAL(')'));
  base::FilePath::StringType::size_type l_paren_index =
      base_name.rfind(FILE_PATH_LITERAL('('));
  if (l_paren_index >= r_paren_index)
    return base_name;

  for (base::FilePath::StringType::size_type i = l_paren_index + 1;
       i != r_paren_index; ++i) {
    if (!base::IsAsciiDigit(base_name[i]))
      return base_name;
  }

  return base_name.substr(0, l_paren_index);
}

// Check whether we can save page as complete-HTML for the contents which
// have specified a MIME type. Now only contents which have the MIME type
// "text/html" can be saved as complete-HTML.
bool CanSaveAsComplete(const std::string& contents_mime_type) {
  return contents_mime_type == "text/html" ||
         contents_mime_type == "application/xhtml+xml";
}

void CancelSavePackage(base::WeakPtr<SavePackage> save_package,
                       bool user_cancel) {
  if (save_package.get() && !save_package->canceled())
    save_package->Cancel(user_cancel, false);
}

const std::string GetMimeTypeForSaveType(SavePageType save_type) {
  switch (save_type) {
    case SAVE_PAGE_TYPE_AS_ONLY_HTML:
    case SAVE_PAGE_TYPE_AS_COMPLETE_HTML:
      return "text/html";
    case SAVE_PAGE_TYPE_AS_MHTML:
      return "multipart/related";
    case SAVE_PAGE_TYPE_AS_WEB_BUNDLE:
      return "application/webbundle";
    case SAVE_PAGE_TYPE_UNKNOWN:
    case SAVE_PAGE_TYPE_MAX:
      NOTREACHED();
      return "";
  }
  NOTREACHED();
}

}  // namespace

const base::FilePath::CharType SavePackage::kDefaultHtmlExtension[] =
    FILE_PATH_LITERAL("html");

SavePackage::SavePackage(WebContents* web_contents)
    : WebContentsObserver(web_contents),
      page_url_(GetUrlToBeSaved(web_contents)),
      title_(web_contents->GetTitle()),
      start_tick_(base::TimeTicks::Now()),
      file_name_set_(&base::FilePath::CompareLessIgnoreCase),
      unique_id_(GetNextSavePackageId()) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  InternalInit();
}

// Used for tests.
SavePackage::SavePackage(WebContents* web_contents,
                         SavePageType save_type,
                         const base::FilePath& file_full_path,
                         const base::FilePath& directory_full_path)
    : WebContentsObserver(web_contents),
      page_url_(GetUrlToBeSaved(web_contents)),
      saved_main_file_path_(file_full_path),
      saved_main_directory_path_(directory_full_path),
      title_(web_contents->GetTitle()),
      start_tick_(base::TimeTicks::Now()),
      save_type_(save_type),
      file_name_set_(&base::FilePath::CompareLessIgnoreCase),
      unique_id_(GetNextSavePackageId()) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK((save_type_ == SAVE_PAGE_TYPE_AS_ONLY_HTML) ||
         (save_type_ == SAVE_PAGE_TYPE_AS_MHTML) ||
         (save_type_ == SAVE_PAGE_TYPE_AS_COMPLETE_HTML) ||
         (save_type_ == SAVE_PAGE_TYPE_AS_WEB_BUNDLE))
      << save_type_;
  DCHECK(!saved_main_file_path_.empty() &&
         saved_main_file_path_.value().length() <= kMaxFilePathLength);
  DCHECK(!saved_main_directory_path_.empty() &&
         saved_main_directory_path_.value().length() < kMaxFilePathLength);
  InternalInit();
}

SavePackage::~SavePackage() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Stop receiving saving job's updates
  if (!finished_ && !canceled()) {
    // Unexpected quit.
    Cancel(true);
  }

  // We should no longer be observing the download::DownloadItem at this point.
  CHECK(!download_);

  DCHECK_EQ(all_save_items_count_,
            waiting_item_queue_.size() + completed_count() + in_process_count())
      << "waiting: " << waiting_item_queue_.size()
      << " completed: " << completed_count()
      << " in_progress: " << in_process_count();

  // Free all SaveItems.
  waiting_item_queue_.clear();
  in_progress_items_.clear();
  saved_success_items_.clear();
  saved_failed_items_.clear();
  // Clear containers that contain (now dangling/invalid) pointers to the
  // save items freed above.  This is not strictly required (as the containers
  // will be destructed soon by ~SavePackage), but seems like good code hygiene.
  frame_tree_node_id_to_contained_save_items_.clear();
  frame_tree_node_id_to_save_item_.clear();
  url_to_save_item_.clear();

  file_manager_ = nullptr;
}

// static
GURL SavePackage::GetUrlToBeSaved(WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Instead of using web_contents->GetURL here, we call GetURL() (which is the
  // "real" url of the page) from the NavigationEntry because it reflects its
  // origin rather than the displayed one (returned by GetVirtualURL) which may
  // be different (like having "view-source:" on the front).
  NavigationEntry* visible_entry =
      web_contents->GetController().GetVisibleEntry();
  return visible_entry ? visible_entry->GetURL() : GURL::EmptyGURL();
}

void SavePackage::Cancel(bool user_action, bool cancel_download_item) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!canceled()) {
    if (user_action)
      user_canceled_ = true;
    else
      disk_error_occurred_ = true;
    Stop(cancel_download_item);
  }
  download::RecordSavePackageEvent(download::SAVE_PACKAGE_CANCELLED);
}

// Init() can be called directly, or indirectly via GetSaveInfo(). In both
// cases, we need file_manager_ to be initialized, so we do this first.
void SavePackage::InternalInit() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  file_manager_ = SaveFileManager::Get();
  DCHECK(file_manager_);

  download_manager_ = static_cast<DownloadManagerImpl*>(
      BrowserContext::GetDownloadManager(
          web_contents()->GetBrowserContext()));
  DCHECK(download_manager_);

  download::RecordSavePackageEvent(download::SAVE_PACKAGE_STARTED);

  // TODO(crbug.com/1061899): The code here should take an explicit reference
  // to the corresponding frame instead of using the current main frame.
  ukm_source_id_ = static_cast<WebContentsImpl*>(web_contents())
                       ->GetMainFrame()
                       ->GetPageUkmSourceId();
  ukm_download_id_ = download::GetUniqueDownloadId();
  download::DownloadUkmHelper::RecordDownloadStarted(
      ukm_download_id_, ukm_source_id_, download::DownloadContent::TEXT,
      download::DownloadSource::UNKNOWN,
      download::CheckDownloadConnectionSecurity(
          web_contents()->GetLastCommittedURL(),
          std::vector<GURL>{web_contents()->GetLastCommittedURL()}),
      true /* is_same_host_download */);
}

bool SavePackage::Init(
    SavePackageDownloadCreatedCallback download_created_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(page_url_.is_valid());
  // Set proper running state.
  if (wait_state_ != INITIALIZE)
    return false;

  wait_state_ = START_PROCESS;

  // Initialize the request context and resource dispatcher.
  BrowserContext* browser_context = web_contents()->GetBrowserContext();
  if (!browser_context) {
    NOTREACHED();
    return false;
  }

  RenderFrameHost* frame_host = web_contents()->GetMainFrame();
  download_manager_->CreateSavePackageDownloadItem(
      saved_main_file_path_, page_url_, GetMimeTypeForSaveType(save_type_),
      frame_host->GetProcess()->GetID(), frame_host->GetRoutingID(),
      base::BindOnce(&CancelSavePackage, AsWeakPtr()),
      base::BindOnce(&SavePackage::InitWithDownloadItem, AsWeakPtr(),
                     std::move(download_created_callback)));
  return true;
}

void SavePackage::InitWithDownloadItem(
    SavePackageDownloadCreatedCallback download_created_callback,
    download::DownloadItemImpl* item) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(item);
  download_ = item;
  // Confirm above didn't delete the tab out from under us.
  if (!download_created_callback.is_null())
    std::move(download_created_callback).Run(download_);

  // Check save type and process the save page job.
  if (save_type_ == SAVE_PAGE_TYPE_AS_COMPLETE_HTML) {
    // Get directory
    DCHECK(!saved_main_directory_path_.empty());
    GetSavableResourceLinks();
  } else if (save_type_ == SAVE_PAGE_TYPE_AS_MHTML) {
    MHTMLGenerationParams mhtml_generation_params(saved_main_file_path_);
    web_contents()->GenerateMHTML(
        mhtml_generation_params,
        base::BindOnce(&SavePackage::OnMHTMLOrWebBundleGenerated, this));
  } else if (save_type_ == SAVE_PAGE_TYPE_AS_WEB_BUNDLE) {
    web_contents()->GenerateWebBundle(
        saved_main_file_path_,
        base::BindOnce(&SavePackage::OnWebBundleGenerated, this));
  } else {
    DCHECK_EQ(SAVE_PAGE_TYPE_AS_ONLY_HTML, save_type_);
    wait_state_ = NET_FILES;
    // Add this item to waiting list.
    waiting_item_queue_.push_back(base::WrapUnique(new SaveItem(
        page_url_, Referrer(), this, SaveFileCreateInfo::SAVE_FILE_FROM_NET,
        FrameTreeNode::kFrameTreeNodeInvalidId,
        web_contents()->GetMainFrame()->GetFrameTreeNodeId())));
    all_save_items_count_ = 1;
    download_->SetTotalBytes(1);

    DoSavingProcess();
  }
}

void SavePackage::OnMHTMLOrWebBundleGenerated(int64_t size) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!download_)
    return;

  CHECK_EQ(download_->GetState(), download::DownloadItem::IN_PROGRESS);
  if (size <= 0) {
    Cancel(false);
    return;
  }
  wrote_to_completed_file_ = true;

  download_->OnAllDataSaved(size, std::unique_ptr<crypto::SecureHash>());

  auto* delegate = download_manager_->GetDelegate();
  if (!delegate || delegate->ShouldCompleteDownload(
                       download_, base::BindOnce(&SavePackage::Finish, this))) {
    Finish();
  }
}

void SavePackage::OnWebBundleGenerated(
    uint64_t size,
    data_decoder::mojom::WebBundlerError error) {
  if (error == data_decoder::mojom::WebBundlerError::kOK)
    DCHECK_GT(size, 0ULL);
  else
    DCHECK_EQ(size, 0ULL);
  OnMHTMLOrWebBundleGenerated(size);
}

// On POSIX, the length of |base_name| + |file_name_ext| is further
// restricted by NAME_MAX. The maximum allowed path looks like:
// '/path/to/save_dir' + '/' + NAME_MAX.
uint32_t SavePackage::GetMaxPathLengthForDirectory(
    const base::FilePath& base_dir) {
#if defined(OS_WIN)
  return kMaxFilePathLength;
#elif defined(OS_POSIX) || defined(OS_FUCHSIA)
  return std::min(
      kMaxFilePathLength,
      static_cast<uint32_t>(base_dir.value().length()) + NAME_MAX + 1);
#endif
}

// static
bool SavePackage::TruncateBaseNameToFitPathConstraints(
    const base::FilePath& dir_path,
    const base::FilePath::StringType& file_name_ext,
    uint32_t max_file_path_len,
    base::FilePath::StringType* base_name) {
  DCHECK(!base_name->empty());
  int available_length = static_cast<int>(max_file_path_len -
                                          dir_path.value().length() -
                                          file_name_ext.length());
  // Need an extra space for the separator.
  if (!dir_path.EndsWithSeparator())
    --available_length;

  // Plenty of room.
  if (static_cast<int>(base_name->length()) <= available_length)
    return true;

  // Limited room. Truncate |base_name| to fit.
  if (available_length > 0) {
    *base_name = base_name->substr(0, available_length);
    return true;
  }

  // Not enough room to even use a shortened |base_name|.
  base_name->clear();
  return false;
}

// Generate name for saving resource.
bool SavePackage::GenerateFileName(const std::string& disposition,
                                   const GURL& url,
                                   bool need_html_ext,
                                   base::FilePath::StringType* generated_name) {
  // TODO(jungshik): Figure out the referrer charset when having one
  // makes sense and pass it to GenerateFileName.
  base::FilePath file_path = net::GenerateFileName(url,
                                                   disposition,
                                                   std::string(),
                                                   std::string(),
                                                   std::string(),
                                                   kDefaultSaveName);

  DCHECK(!file_path.empty());
  if (need_html_ext)
    file_path = file_path.ReplaceExtension(kDefaultHtmlExtension);

  DownloadManagerDelegate* delegate = download_manager_->GetDelegate();
  if (delegate)
    delegate->SanitizeSavePackageResourceName(&file_path);

  DCHECK_EQ(file_path.value(), file_path.BaseName().value())
      << "SanitizeSavePackageResourceName should only return a basename.";

  base::FilePath::StringType base_name =
      file_path.RemoveExtension().BaseName().value();
  base::FilePath::StringType file_name_ext = file_path.Extension();

  // Need to make sure the suggested file name is not too long.
  uint32_t max_path = GetMaxPathLengthForDirectory(saved_main_directory_path_);

  // Get safe pure file name.
  if (!TruncateBaseNameToFitPathConstraints(
          saved_main_directory_path_, file_name_ext, max_path, &base_name))
    return false;

  base::FilePath::StringType file_name = base_name + file_name_ext;

  // Check whether we already have same name in a case insensitive manner.
  auto iter = file_name_set_.find(file_name);
  if (iter == file_name_set_.end()) {
    DCHECK(!file_name.empty());
    file_name_set_.insert(file_name);
    generated_name->assign(file_name);
    return true;
  }

  // Found same name, increase the ordinal number for the file name.
  base_name = base::FilePath(*iter).RemoveExtension().BaseName().value();
  base::FilePath::StringType base_file_name = StripOrdinalNumber(base_name);

  // We need to make sure the length of base file name plus maximum ordinal
  // number path will be less than or equal to kMaxFilePathLength.
  if (!TruncateBaseNameToFitPathConstraints(
          saved_main_directory_path_, file_name_ext,
          max_path - kMaxFileOrdinalNumberPartLength, &base_file_name)) {
    return false;
  }

  // Prepare the new ordinal number.
  uint32_t ordinal_number;
  auto it = file_name_count_map_.find(base_file_name);
  if (it == file_name_count_map_.end()) {
    // First base-name-conflict resolving, use 1 as initial ordinal number.
    file_name_count_map_[base_file_name] = 1;
    ordinal_number = 1;
  } else {
    // We have met same base-name conflict, use latest ordinal number.
    ordinal_number = it->second;
  }

  if (ordinal_number > kMaxFileOrdinalNumber - 1) {
    // Use a random file from temporary file.
    base::FilePath temp_file;
    base::CreateTemporaryFile(&temp_file);
    file_name = temp_file.RemoveExtension().BaseName().value();
    // Get safe pure file name.
    if (!TruncateBaseNameToFitPathConstraints(saved_main_directory_path_,
                                              base::FilePath::StringType(),
                                              max_path, &file_name)) {
      return false;
    }
  } else {
    for (int i = ordinal_number; i < kMaxFileOrdinalNumber; ++i) {
      base::FilePath::StringType new_name =
          base_file_name + base::StringPrintf(FILE_PATH_LITERAL("(%d)"), i) +
          file_name_ext;
      if (!base::Contains(file_name_set_, new_name)) {
        // Resolved name conflict.
        file_name = new_name;
        file_name_count_map_[base_file_name] = ++i;
        break;
      }
    }
  }

  DCHECK(!file_name.empty());
  file_name_set_.insert(file_name);
  generated_name->assign(file_name);

  return true;
}

// We have received a message from SaveFileManager about a new saving job. We
// find a SaveItem and store it in our in_progress list.
void SavePackage::StartSave(const SaveFileCreateInfo* info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(info);

  auto it = in_progress_items_.find(info->save_item_id);
  if (it == in_progress_items_.end()) {
    // If not found, we must have cancel action.
    DCHECK(canceled());
    return;
  }
  SaveItem* save_item = it->second.get();

  DCHECK(!saved_main_file_path_.empty());

  // Determine the proper path for a saving job, by choosing either the default
  // save directory, or prompting the user.
  DCHECK(!save_item->has_final_name());
  if (info->url != page_url_) {
    base::FilePath::StringType generated_name;
    // For HTML resource file, make sure it will have .htm as extension name,
    // otherwise, when you open the saved page in Chrome again, download
    // file manager will treat it as downloadable resource, and download it
    // instead of opening it as HTML.
    bool need_html_ext =
        info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM;
    if (!GenerateFileName(info->content_disposition,
                          GURL(info->url),
                          need_html_ext,
                          &generated_name)) {
      // We can not generate file name for this SaveItem, so we cancel the
      // saving page job if the save source is from serialized DOM data.
      // Otherwise, it means this SaveItem is sub-resource type, we treat it
      // as an error happened on saving. We can ignore this type error for
      // sub-resource links which will be resolved as absolute links instead
      // of local links in final saved contents.
      if (info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM)
        Cancel(true);
      else
        SaveFinished(save_item->id(), 0, false);
      return;
    }

    // When saving page as only-HTML, we only have a SaveItem whose url
    // must be page_url_.
    DCHECK_EQ(SAVE_PAGE_TYPE_AS_COMPLETE_HTML, save_type_);
    DCHECK(!saved_main_directory_path_.empty());

    // Now we get final name retrieved from GenerateFileName, we will use it
    // rename the SaveItem.
    base::FilePath final_name =
        saved_main_directory_path_.Append(generated_name);
    save_item->SetTargetPath(final_name);
  } else {
    // It is the main HTML file, use the name chosen by the user.
    save_item->SetTargetPath(saved_main_file_path_);
  }

  // Check whether we begin to require serialized HTML data.
  if (save_type_ == SAVE_PAGE_TYPE_AS_COMPLETE_HTML &&
      wait_state_ == HTML_DATA) {
    // Inform backend to serialize the all frames' DOM and send serialized
    // HTML data back.
    GetSerializedHtmlWithLocalLinks();
  }
}

SaveItem* SavePackage::LookupInProgressSaveItem(SaveItemId save_item_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  auto it = in_progress_items_.find(save_item_id);
  if (it != in_progress_items_.end()) {
    SaveItem* save_item = it->second.get();
    DCHECK_EQ(SaveItem::IN_PROGRESS, save_item->state());
    return save_item;
  }
  return nullptr;
}

void SavePackage::PutInProgressItemToSavedMap(SaveItem* save_item) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  auto it = in_progress_items_.find(save_item->id());
  DCHECK(it != in_progress_items_.end());
  DCHECK_EQ(save_item, it->second.get());
  std::unique_ptr<SaveItem> owned_item = std::move(it->second);
  in_progress_items_.erase(it);

  SaveItemIdMap& map = save_item->success() ?
      saved_success_items_ : saved_failed_items_;
  DCHECK(!base::Contains(map, save_item->id()));
  map[save_item->id()] = std::move(owned_item);
}

// Called for updating saving state.
bool SavePackage::UpdateSaveProgress(SaveItemId save_item_id,
                                     int64_t size,
                                     bool write_success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Because we might have canceled this saving job before,
  // so we might not find corresponding SaveItem.
  SaveItem* save_item = LookupInProgressSaveItem(save_item_id);
  if (!save_item)
    return false;

  save_item->Update(size);

  // If we got disk error, cancel whole save page job.
  if (!write_success) {
    // Cancel job with reason of disk error.
    Cancel(false);
  }
  return true;
}

// Stop all page saving jobs that are in progress and instruct the download
// sequence to delete all saved files.
void SavePackage::Stop(bool cancel_download_item) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // If we haven't moved out of the initial state, there's nothing to cancel and
  // there won't be valid pointers for |file_manager_| or |download_|.
  if (wait_state_ == INITIALIZE)
    return;

  // When stopping, if it still has some items in in_progress, cancel them.
  DCHECK(canceled());
  if (in_process_count()) {
    for (const auto& it : in_progress_items_) {
      SaveItem* save_item = it.second.get();
      DCHECK_EQ(SaveItem::IN_PROGRESS, save_item->state());
      save_item->Cancel();
    }
    // Remove all in progress item to saved map. For failed items, they will
    // be put into saved_failed_items_, for successful item, they will be put
    // into saved_success_items_.
    while (in_process_count())
      PutInProgressItemToSavedMap(in_progress_items_.begin()->second.get());
  }

  // This vector contains the save ids of the save files which SaveFileManager
  // needs to remove from its save_file_map_.
  std::vector<SaveItemId> save_item_ids;
  for (const auto& it : saved_success_items_)
    save_item_ids.push_back(it.first);
  for (const auto& it : saved_failed_items_)
    save_item_ids.push_back(it.first);

  download::GetDownloadTaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&SaveFileManager::RemoveSavedFileFromFileMap,
                                file_manager_, save_item_ids));

  finished_ = true;
  wait_state_ = FAILED;

  // Inform the download::DownloadItem we have canceled whole save page job.
  if (download_) {
    if (cancel_download_item)
      download_->Cancel(false);
    FinalizeDownloadEntry();
  }
}

void SavePackage::CheckFinish() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (in_process_count() || finished_)
    return;

  base::FilePath dir = (save_type_ == SAVE_PAGE_TYPE_AS_COMPLETE_HTML &&
                        saved_success_items_.size() > 1) ?
                        saved_main_directory_path_ : base::FilePath();

  FinalNamesMap final_names;
  for (const auto& it : saved_success_items_)
    final_names.insert(std::make_pair(it.first, it.second->full_path()));

  download::GetDownloadTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(&SaveFileManager::RenameAllFiles, file_manager_,
                     final_names, dir,
                     web_contents()->GetMainFrame()->GetProcess()->GetID(),
                     web_contents()->GetMainFrame()->GetRoutingID(), id()));
}

// Successfully finished all items of this SavePackage.
void SavePackage::Finish() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // User may cancel the job when we're moving files to the final directory.
  if (canceled())
    return;

  wait_state_ = SUCCESSFUL;
  finished_ = true;

  // Record finish.
  download::RecordSavePackageEvent(download::SAVE_PACKAGE_FINISHED);

  // TODO(qinmin): report the actual file size and duration for the download.
  download::DownloadUkmHelper::RecordDownloadCompleted(ukm_download_id_, 1,
                                                       base::TimeDelta(), 0);

  // Record any errors that occurred.
  if (wrote_to_completed_file_)
    download::RecordSavePackageEvent(download::SAVE_PACKAGE_WRITE_TO_COMPLETED);

  if (wrote_to_failed_file_)
    download::RecordSavePackageEvent(download::SAVE_PACKAGE_WRITE_TO_FAILED);

  // This vector contains the save ids of the save files which SaveFileManager
  // needs to remove from its |save_file_map_|.
  std::vector<SaveItemId> list_of_failed_save_item_ids;
  for (const auto& it : saved_failed_items_) {
    const SaveItem* save_item = it.second.get();
    DCHECK_EQ(it.first, save_item->id());
    list_of_failed_save_item_ids.push_back(save_item->id());
  }

  download::GetDownloadTaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&SaveFileManager::RemoveSavedFileFromFileMap,
                                file_manager_, list_of_failed_save_item_ids));

  if (download_) {
    if (save_type_ != SAVE_PAGE_TYPE_AS_MHTML &&
        save_type_ != SAVE_PAGE_TYPE_AS_WEB_BUNDLE) {
      CHECK_EQ(download_->GetState(), download::DownloadItem::IN_PROGRESS);
      download_->DestinationUpdate(
          all_save_items_count_, CurrentSpeed(),
          std::vector<download::DownloadItem::ReceivedSlice>());
      download_->OnAllDataSaved(all_save_items_count_,
                                std::unique_ptr<crypto::SecureHash>());
    }
    download_->MarkAsComplete();
    FinalizeDownloadEntry();
  }
}

void SavePackage::SaveFinished(SaveItemId save_item_id,
                               int64_t size,
                               bool is_success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Because we might have canceled this saving job before,
  // so we might not find corresponding SaveItem. Just ignore it.
  SaveItem* save_item = LookupInProgressSaveItem(save_item_id);
  if (!save_item)
    return;

  // Let SaveItem set end state.
  save_item->Finish(size, is_success);
  // Remove the associated save id and SavePackage.
  file_manager_->RemoveSaveFile(save_item->id(), this);

  PutInProgressItemToSavedMap(save_item);

  // Inform the download::DownloadItem to update UI.
  // We use the received bytes as number of saved files.
  if (download_) {
    CHECK_EQ(download_->GetState(), download::DownloadItem::IN_PROGRESS);
    download_->DestinationUpdate(
        completed_count(), CurrentSpeed(),
        std::vector<download::DownloadItem::ReceivedSlice>());
  }

  if (save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM &&
      save_item->url() == page_url_ && !save_item->received_bytes()) {
    // If size of main HTML page is 0, treat it as disk error.
    Cancel(false);
    return;
  }

  if (canceled()) {
    DCHECK(finished_);
    return;
  }

  // Continue processing the save page job.
  DoSavingProcess();

  // Check whether we can successfully finish whole job.
  CheckFinish();
}

void SavePackage::SaveCanceled(const SaveItem* save_item) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  file_manager_->RemoveSaveFile(save_item->id(), this);
  download::GetDownloadTaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&SaveFileManager::CancelSave, file_manager_,
                                save_item->id()));
}

void SavePackage::SaveNextFile(bool process_all_remaining_items) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(web_contents());
  DCHECK(!waiting_item_queue_.empty());

  do {
    // Pop SaveItem from waiting list.
    std::unique_ptr<SaveItem> save_item =
        std::move(waiting_item_queue_.front());
    SaveItem* save_item_ptr = save_item.get();
    waiting_item_queue_.pop_front();

    // Add the item to |in_progress_items_|.
    DCHECK(!base::Contains(in_progress_items_, save_item->id()));
    in_progress_items_[save_item_ptr->id()] = std::move(save_item);
    save_item_ptr->Start();

    // Find the frame responsible for making the network request below - it will
    // be used in security checks made later.
    int requester_frame_tree_node_id =
        save_item_ptr->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_NET
            ? save_item_ptr->container_frame_tree_node_id()
            : save_item_ptr->frame_tree_node_id();
    DCHECK_NE(FrameTreeNode::kFrameTreeNodeInvalidId,
              requester_frame_tree_node_id);
    FrameTreeNode* requester_frame_tree_node =
        FrameTreeNode::GloballyFindByID(requester_frame_tree_node_id);
    if (!requester_frame_tree_node) {
      save_item_ptr->Finish(0, /* is_success = */ false);
      continue;
    }
    RenderFrameHostImpl* requester_frame =
        requester_frame_tree_node->current_frame_host();

    mojo::PendingRemote<quarantine::mojom::Quarantine> quarantine;
    auto quarantine_callback =
        download_manager_->GetQuarantineConnectionCallback();
    if (quarantine_callback)
      quarantine_callback.Run(quarantine.InitWithNewPipeAndPassReceiver());

    file_manager_->SaveURL(
        save_item_ptr->id(), save_item_ptr->url(), save_item_ptr->referrer(),
        requester_frame->GetProcess()->GetID(),
        requester_frame->render_view_host()->GetRoutingID(),
        requester_frame->routing_id(), save_item_ptr->save_source(),
        save_item_ptr->full_path(), web_contents()->GetBrowserContext(),
        web_contents()
            ->GetRenderViewHost()
            ->GetProcess()
            ->GetStoragePartition(),
        this, download_manager_->GetApplicationClientIdForFileScanning(),
        std::move(quarantine));
  } while (process_all_remaining_items && !waiting_item_queue_.empty());
}

int SavePackage::PercentComplete() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!all_save_items_count_)
    return 0;
  if (!in_process_count())
    return 100;
  return completed_count() / all_save_items_count_;
}

int64_t SavePackage::CurrentSpeed() const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::TimeDelta diff = base::TimeTicks::Now() - start_tick_;
  int64_t diff_ms = diff.InMilliseconds();
  return diff_ms == 0 ? 0 : completed_count() * 1000 / diff_ms;
}

void SavePackage::DoSavingProcess() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (save_type_ != SAVE_PAGE_TYPE_AS_COMPLETE_HTML) {
    // Save as HTML only or MHTML, or Web Bundle.
    DCHECK_EQ(NET_FILES, wait_state_);
    DCHECK((save_type_ == SAVE_PAGE_TYPE_AS_ONLY_HTML) ||
           (save_type_ == SAVE_PAGE_TYPE_AS_MHTML) ||
           (save_type_ == SAVE_PAGE_TYPE_AS_WEB_BUNDLE))
        << save_type_;
    if (waiting_item_queue_.size()) {
      DCHECK_EQ(all_save_items_count_, waiting_item_queue_.size());
      SaveNextFile(false);
    }
    return;
  }

  // We guarantee that images and JavaScripts must be downloaded first.
  // So when finishing all those sub-resources, we will know which
  // sub-resource's link can be replaced with local file path, which
  // sub-resource's link need to be replaced with absolute URL which
  // point to its internet address because it got error when saving its data.

  // Start a new SaveItem job if we still have job in waiting queue.
  if (!waiting_item_queue_.empty()) {
    DCHECK_EQ(NET_FILES, wait_state_);
    const SaveItem* save_item = waiting_item_queue_.front().get();
    if (save_item->save_source() != SaveFileCreateInfo::SAVE_FILE_FROM_DOM) {
      SaveNextFile(false);
    } else if (!in_process_count()) {
      // If there is no in-process SaveItem, it means all sub-resources
      // have been processed. Now we need to start serializing HTML DOM
      // for the current page to get the generated HTML data.
      wait_state_ = HTML_DATA;
      // All non-HTML resources have been finished, start all remaining
      // HTML files.
      SaveNextFile(true);
    }
  } else if (in_process_count()) {
    // Continue asking for HTML data.
    DCHECK_EQ(HTML_DATA, wait_state_);
  }
}

// After finishing all SaveItems which need to get data from net.
// We collect all URLs which have local storage and send the
// map:(originalURL:currentLocalPath) to render process (backend).
// Then render process will serialize DOM and send data to us.
void SavePackage::GetSerializedHtmlWithLocalLinks() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (wait_state_ != HTML_DATA)
    return;

  // First collect the status of all the resource files and check whether they
  // have created local files (although they have not been completely saved).
  int successful_started_items_count = 0;
  for (const auto& item : in_progress_items_) {
    DCHECK_EQ(SaveFileCreateInfo::SAVE_FILE_FROM_DOM,
              item.second->save_source());
    if (item.second->has_final_name())
      successful_started_items_count++;
  }
  // If not all file of HTML resource have been started, then wait.
  if (successful_started_items_count != in_process_count())
    return;

  // Try to serialize all the frames gathered during GetSavableResourceLinks.
  DCHECK_EQ(0, number_of_frames_pending_response_);
  FrameTree* frame_tree =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetMainFrame())
          ->frame_tree();
  for (const auto& item : frame_tree_node_id_to_save_item_) {
    int frame_tree_node_id = item.first;
    const SaveItem* save_item = item.second;

    FrameTreeNode* frame_tree_node = frame_tree->FindByID(frame_tree_node_id);
    if (frame_tree_node &&
        frame_tree_node->current_frame_host()->IsRenderFrameLive()) {
      // Ask the frame for HTML to be written to the associated SaveItem.
      GetSerializedHtmlWithLocalLinksForFrame(frame_tree_node);
      number_of_frames_pending_response_++;
    } else {
      // Notify SaveFileManager about the failure to save this SaveItem.
      download::GetDownloadTaskRunner()->PostTask(
          FROM_HERE,
          base::BindOnce(&SaveFileManager::SaveFinished, file_manager_,
                         save_item->id(), id(), false));
    }
  }
  if (number_of_frames_pending_response_ == 0) {
    // All frames disappeared since gathering of savable resources?
    // Treat this as cancellation.
    Cancel(false);
  }
}

void SavePackage::GetSerializedHtmlWithLocalLinksForFrame(
    FrameTreeNode* target_tree_node) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(target_tree_node);
  int target_frame_tree_node_id = target_tree_node->frame_tree_node_id();
  RenderFrameHostImpl* target = target_tree_node->current_frame_host();

  // Collect all saved success items.
  // SECURITY NOTE: We don't send *all* urls / local paths, but only
  // those that the given frame had access to already (because it contained
  // the savable resources / subframes associated with save items).
  base::flat_map<GURL, base::FilePath> url_to_local_path;
  base::flat_map<base::UnguessableToken, base::FilePath>
      frame_token_to_local_path;

  auto it = frame_tree_node_id_to_contained_save_items_.find(
      target_frame_tree_node_id);
  if (it != frame_tree_node_id_to_contained_save_items_.end()) {
    for (const SaveItem* save_item : it->second) {
      // Skip items that failed to save.
      if (!save_item->has_final_name()) {
        DCHECK_EQ(SaveItem::SaveState::COMPLETE, save_item->state());
        DCHECK(!save_item->success());
        continue;
      }

      // Calculate the relative path for referring to the |save_item|.
      base::FilePath local_path(base::FilePath::kCurrentDirectory);
      if (target_tree_node->IsMainFrame()) {
        local_path = local_path.Append(saved_main_directory_path_.BaseName());
      }
      local_path = local_path.Append(save_item->full_path().BaseName());

      // Insert the link into |url_to_local_path| or
      // |frame_token_to_local_path|.
      if (save_item->save_source() != SaveFileCreateInfo::SAVE_FILE_FROM_DOM) {
        DCHECK_EQ(FrameTreeNode::kFrameTreeNodeInvalidId,
                  save_item->frame_tree_node_id());
        url_to_local_path[save_item->url()] = local_path;
      } else {
        FrameTreeNode* save_item_frame_tree_node =
            target_tree_node->frame_tree()->FindByID(
                save_item->frame_tree_node_id());
        if (!save_item_frame_tree_node) {
          // crbug.com/541354: Raciness when saving a dynamically changing page.
          continue;
        }

        base::Optional<base::UnguessableToken> frame_token =
            save_item_frame_tree_node->render_manager()
                ->GetFrameTokenForSiteInstance(target->GetSiteInstance());

        DCHECK(frame_token.has_value());

        frame_token_to_local_path[frame_token.value()] = local_path;
      }
    }
  }

  // Create a SavePackageSerializationHandler for the target RenderFrameHost
  // plus the required callbacks to report progress, and make it owned by a mojo
  // receiver that will be alive for the time that the serialization process is
  // in progress. It's expected that the Done() callback will be called right
  // after the last time the DidReceiveData() callback gets invoked, at which
  // point the remote end of the pipe will be closed, disposing the receiver.
  mojo::PendingRemote<mojom::FrameHTMLSerializerHandler> serializer_handler;
  mojo::MakeSelfOwnedReceiver(
      std::make_unique<SavePackageSerializationHandler>(
          base::BindRepeating(&SavePackage::OnDidReceiveSerializedHtmlData,
                              AsWeakPtr(), target->GetWeakPtr()),
          base::BindOnce(&SavePackage::OnDidFinishedSerializingHtmlData,
                         AsWeakPtr(), target->GetWeakPtr())),
      serializer_handler.InitWithNewPipeAndPassReceiver());

  // Ask target frame to serialize itself.
  target->GetSerializedHtmlWithLocalLinks(
      url_to_local_path, frame_token_to_local_path,
      web_contents()->GetBrowserContext()->IsOffTheRecord(),
      std::move(serializer_handler));
}

void SavePackage::OnDidReceiveSerializedHtmlData(
    base::WeakPtr<RenderFrameHostImpl> sender,
    const std::string& data) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Check current state.
  if (!sender || wait_state_ != HTML_DATA)
    return;

  // This method can only get called as a response to the serialization request
  // previously sent from the browser to the renderer for a given FrameTreeNode.
  const SaveItem* save_item = LookupSaveItemForSender(sender);
  DCHECK(save_item);

  if (save_item->state() != SaveItem::IN_PROGRESS) {
    for (const auto& saved_it : saved_success_items_) {
      if (saved_it.second->url() == save_item->url()) {
        wrote_to_completed_file_ = true;
        break;
      }
    }

    if (base::Contains(saved_failed_items_, save_item->id()))
      wrote_to_failed_file_ = true;

    return;
  }

  if (!data.empty()) {
    // Call write file functionality in download sequence.
    download::GetDownloadTaskRunner()->PostTask(
        FROM_HERE, base::BindOnce(&SaveFileManager::UpdateSaveProgress,
                                  file_manager_, save_item->id(), data));
  }
}

void SavePackage::OnDidFinishedSerializingHtmlData(
    base::WeakPtr<RenderFrameHostImpl> sender) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Check current state.
  if (!sender || wait_state_ != HTML_DATA)
    return;

  // This method can only get called as a response to the serialization request
  // previously sent from the browser to the renderer for a given FrameTreeNode.
  const SaveItem* save_item = LookupSaveItemForSender(sender);
  DCHECK(save_item);

  // Current frame is completed saving, call finish in download sequence.
  DVLOG(20) << __func__ << "() save_item_id = " << save_item->id()
            << " url = \"" << save_item->url().spec() << "\"";
  download::GetDownloadTaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&SaveFileManager::SaveFinished, file_manager_,
                                save_item->id(), id(), true));
  number_of_frames_pending_response_--;
  DCHECK_LE(0, number_of_frames_pending_response_);
}

const SaveItem* SavePackage::LookupSaveItemForSender(
    base::WeakPtr<RenderFrameHostImpl> sender) {
  if (!sender)
    return nullptr;

  int frame_tree_node_id = sender->frame_tree_node()->frame_tree_node_id();
  auto it = frame_tree_node_id_to_save_item_.find(frame_tree_node_id);
  if (it == frame_tree_node_id_to_save_item_.end())
    return nullptr;

  const SaveItem* save_item = it->second;
  DCHECK_EQ(SaveFileCreateInfo::SAVE_FILE_FROM_DOM, save_item->save_source());

  return save_item;
}

// Ask for all savable resource links from backend, include main frame and
// sub-frame.
void SavePackage::GetSavableResourceLinks() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (wait_state_ != START_PROCESS)
    return;

  wait_state_ = RESOURCES_LIST;

  DCHECK_EQ(0, number_of_frames_pending_response_);
  for (RenderFrameHost* rfh : web_contents()->GetAllFrames()) {
    if (!rfh->IsRenderFrameLive())
      continue;
    ++number_of_frames_pending_response_;
    static_cast<RenderFrameHostImpl*>(rfh)
        ->GetSavableResourceLinksFromRenderer();
  }
  DCHECK_LT(0, number_of_frames_pending_response_);

  // Enqueue the main frame separately (because this frame won't show up in any
  // of GetsSavableResourceLinks callbacks).
  FrameTreeNode* main_frame_tree_node =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetMainFrame())
          ->frame_tree_node();
  EnqueueFrame(FrameTreeNode::kFrameTreeNodeInvalidId,  // No container.
               main_frame_tree_node->frame_tree_node_id(),
               main_frame_tree_node->current_url());
  all_save_items_count_ = 1;
}

void SavePackage::SavableResourceLinksResponse(
    RenderFrameHostImpl* sender,
    const std::vector<GURL>& resources_list,
    blink::mojom::ReferrerPtr referrer,
    const std::vector<blink::mojom::SavableSubframePtr>& subframes) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (wait_state_ != RESOURCES_LIST)
    return;

  // Add all sub-resources to wait list.
  int container_frame_tree_node_id =
      sender->frame_tree_node()->frame_tree_node_id();
  for (const GURL& u : resources_list) {
    EnqueueSavableResource(container_frame_tree_node_id, u,
                           referrer.To<content::Referrer>());
  }
  for (auto& subframe : subframes) {
    RenderFrameHostImpl* rfh_subframe = sender->FindAndVerifyChild(
        subframe->subframe_token,
        bad_message::DWNLD_INVALID_SAVABLE_RESOURCE_LINKS_RESPONSE);

    if (!rfh_subframe) {
      // crbug.com/541354 - Raciness when saving a dynamically changing page.
      continue;
    }

    EnqueueFrame(container_frame_tree_node_id,
                 rfh_subframe->frame_tree_node()->frame_tree_node_id(),
                 subframe->original_url);
  }

  CompleteSavableResourceLinksResponse();
}

SaveItem* SavePackage::CreatePendingSaveItem(
    int container_frame_tree_node_id,
    int save_item_frame_tree_node_id,
    const GURL& url,
    const Referrer& referrer,
    SaveFileCreateInfo::SaveFileSource save_source) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  Referrer sanitized_referrer = Referrer::SanitizeForRequest(url, referrer);
  SaveItem* save_item =
      new SaveItem(url, sanitized_referrer, this, save_source,
                   save_item_frame_tree_node_id, container_frame_tree_node_id);
  waiting_item_queue_.push_back(base::WrapUnique(save_item));

  frame_tree_node_id_to_contained_save_items_[container_frame_tree_node_id]
      .push_back(save_item);
  return save_item;
}

void SavePackage::CreatePendingSaveItemDeduplicatingByUrl(
    int container_frame_tree_node_id,
    int save_item_frame_tree_node_id,
    const GURL& url,
    const Referrer& referrer,
    SaveFileCreateInfo::SaveFileSource save_source) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(url.is_valid());  // |url| should be validated by the callers.

  // Frames should not be deduplicated by URL.
  DCHECK_NE(SaveFileCreateInfo::SAVE_FILE_FROM_DOM, save_source);

  auto it = url_to_save_item_.find(url);
  if (it != url_to_save_item_.end()) {
    frame_tree_node_id_to_contained_save_items_[container_frame_tree_node_id]
        .push_back(it->second);
  } else {
    url_to_save_item_[url] = CreatePendingSaveItem(container_frame_tree_node_id,
                                                   save_item_frame_tree_node_id,
                                                   url, referrer, save_source);
  }
}

void SavePackage::EnqueueSavableResource(int container_frame_tree_node_id,
                                         const GURL& url,
                                         const Referrer& referrer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!url.is_valid())
    return;

  CreatePendingSaveItemDeduplicatingByUrl(
      container_frame_tree_node_id, FrameTreeNode::kFrameTreeNodeInvalidId, url,
      referrer, SaveFileCreateInfo::SAVE_FILE_FROM_NET);
}

void SavePackage::EnqueueFrame(int container_frame_tree_node_id,
                               int frame_tree_node_id,
                               const GURL& frame_original_url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  SaveItem* save_item = CreatePendingSaveItem(
      container_frame_tree_node_id, frame_tree_node_id, frame_original_url,
      Referrer(), SaveFileCreateInfo::SAVE_FILE_FROM_DOM);
  DCHECK(save_item);
  frame_tree_node_id_to_save_item_[frame_tree_node_id] = save_item;
}

void SavePackage::SavableResourceLinksError(RenderFrameHostImpl* sender) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CompleteSavableResourceLinksResponse();
}

void SavePackage::CompleteSavableResourceLinksResponse() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  --number_of_frames_pending_response_;
  DCHECK_LE(0, number_of_frames_pending_response_);
  if (number_of_frames_pending_response_ != 0)
    return;  // Need to wait for more responses from RenderFrames.

  // Sort |waiting_item_queue_| so that frames go last (frames are identified by
  // SAVE_FILE_FROM_DOM in the comparison function below).
  std::stable_sort(
      waiting_item_queue_.begin(), waiting_item_queue_.end(),
      [](const std::unique_ptr<SaveItem>& x,
         const std::unique_ptr<SaveItem>& y) {
        DCHECK(x);
        DCHECK(y);
        return (x->save_source() != SaveFileCreateInfo::SAVE_FILE_FROM_DOM) &&
               (y->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM);
      });

  all_save_items_count_ = static_cast<int>(waiting_item_queue_.size());

  // We use total bytes as the total number of files we want to save.
  if (download_)
    download_->SetTotalBytes(all_save_items_count_);

  if (all_save_items_count_) {
    wait_state_ = NET_FILES;

    // Give backend the lists which contain all resource links that have local
    // storage, after which, render process will serialize DOM for generating
    // HTML data.
    DoSavingProcess();
  } else {
    // No savable frames and/or resources - treat it as user cancel.
    Cancel(true);
  }
}

void SavePackage::GetSaveInfo() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Can't use |web_contents_| in the download sequence, so get the data that we
  // need before calling to it.
  base::FilePath website_save_dir;
  base::FilePath download_save_dir;
  auto* delegate = download_manager_->GetDelegate();
  if (delegate) {
    delegate->GetSaveDir(web_contents()->GetBrowserContext(), &website_save_dir,
                         &download_save_dir);
  }
  std::string mime_type = web_contents()->GetContentsMimeType();
  bool can_save_as_complete = CanSaveAsComplete(mime_type);
  base::PostTaskAndReplyWithResult(
      download::GetDownloadTaskRunner().get(), FROM_HERE,
      base::BindOnce(&SavePackage::CreateDirectoryOnFileThread, title_,
                     page_url_, can_save_as_complete, mime_type,
                     website_save_dir, download_save_dir),
      base::BindOnce(&SavePackage::ContinueGetSaveInfo, this,
                     can_save_as_complete));
}

// static
base::FilePath SavePackage::CreateDirectoryOnFileThread(
    const base::string16& title,
    const GURL& page_url,
    bool can_save_as_complete,
    const std::string& mime_type,
    const base::FilePath& website_save_dir,
    const base::FilePath& download_save_dir) {
  DCHECK(download::GetDownloadTaskRunner()->RunsTasksInCurrentSequence());

  base::FilePath suggested_filename = filename_generation::GenerateFilename(
      title, page_url, can_save_as_complete, mime_type);

  base::FilePath save_dir;
  // If the default html/websites save folder doesn't exist...
  if (!base::DirectoryExists(website_save_dir)) {
    // If the default download dir doesn't exist, create it.
    if (!base::DirectoryExists(download_save_dir)) {
      bool res = base::CreateDirectory(download_save_dir);
      DCHECK(res);
    }
    save_dir = download_save_dir;
  } else {
    // If it does exist, use the default save dir param.
    save_dir = website_save_dir;
  }

  base::FilePath::StringType base_name =
      suggested_filename.RemoveExtension().BaseName().value();
  base::FilePath::StringType file_name_ext = suggested_filename.Extension();

  // Need to make sure the suggested file name is not too long.
  uint32_t max_path = GetMaxPathLengthForDirectory(save_dir);

  if (TruncateBaseNameToFitPathConstraints(save_dir, file_name_ext, max_path,
                                           &base_name)) {
    return save_dir.Append(base_name + file_name_ext);
  }
  // Cannot create a shorter filename. This will cause the save as operation
  // to fail unless the user pick a shorter name. Continuing even though it
  // will fail because returning means no save as popup for the user, which
  // is even more confusing. This case should be rare though.
  return save_dir.Append(suggested_filename);
}

void SavePackage::ContinueGetSaveInfo(bool can_save_as_complete,
                                      const base::FilePath& suggested_path) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // The WebContents which owns this SavePackage may have disappeared during
  // the UI->download sequence->UI thread hop of
  // GetSaveInfo->CreateDirectoryOnFileThread->ContinueGetSaveInfo.
  if (!web_contents() || !download_manager_->GetDelegate())
    return;

  base::FilePath::StringType default_extension;
  if (can_save_as_complete)
    default_extension = kDefaultHtmlExtension;

  download_manager_->GetDelegate()->ChooseSavePath(
      web_contents(), suggested_path, default_extension, can_save_as_complete,
      base::BindOnce(&SavePackage::OnPathPicked, AsWeakPtr()));
}

void SavePackage::OnPathPicked(
    const base::FilePath& final_name,
    SavePageType type,
    SavePackageDownloadCreatedCallback download_created_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK((type == SAVE_PAGE_TYPE_AS_ONLY_HTML) ||
         (type == SAVE_PAGE_TYPE_AS_MHTML) ||
         (type == SAVE_PAGE_TYPE_AS_COMPLETE_HTML) ||
         (type == SAVE_PAGE_TYPE_AS_WEB_BUNDLE))
      << type;
  // Ensure the filename is safe.
  saved_main_file_path_ = final_name;
  // TODO(asanka): This call may block on IO and shouldn't be made
  // from the UI thread.  See http://crbug.com/61827.
  net::GenerateSafeFileName(web_contents()->GetContentsMimeType(), false,
                            &saved_main_file_path_);

  saved_main_directory_path_ = saved_main_file_path_.DirName();
  save_type_ = type;
  if (save_type_ == SAVE_PAGE_TYPE_AS_COMPLETE_HTML) {
    // Make new directory for saving complete file.
    saved_main_directory_path_ = saved_main_directory_path_.Append(
        saved_main_file_path_.RemoveExtension().BaseName().value() +
        FILE_PATH_LITERAL("_files"));
  }

  Init(std::move(download_created_callback));
}

void SavePackage::FinalizeDownloadEntry() {
  DCHECK(download_);
  DCHECK(download_manager_);
  download_ = nullptr;
  download_manager_ = nullptr;
}

}  // namespace content
