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

#include "extensions/browser/webstore_installer.h"

#include <stddef.h>
#include <stdint.h>

#include <limits>
#include <memory>
#include <set>
#include <string_view>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial.h"
#include "base/path_service.h"
#include "base/rand_util.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/crx_file/id_util.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/update_client/update_query_params.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/navigation_controller.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/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/crx_installer.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/install/crx_install_error.h"
#include "extensions/browser/install_approval.h"
#include "extensions/browser/install_tracker.h"
#include "extensions/browser/install_verifier.h"
#include "extensions/browser/manifest_check_level.h"
#include "extensions/browser/shared_module_service.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/shared_module_info.h"
#include "extensions/common/switches.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "net/base/url_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using content::BrowserContext;
using content::BrowserThread;
using content::DownloadManager;
using content::NavigationController;
using download::DownloadItem;
using download::DownloadUrlParameters;

namespace extensions {

namespace {

// Key used to attach the InstallApproval to the DownloadItem.
const char kApprovalKey[] = "extensions.webstore_installer";

const char kInvalidIdError[] = "Invalid id";
const char kDownloadDirectoryError[] = "Could not create download directory";
const char kDownloadCanceledError[] = "Download canceled";
const char kDownloadInterruptedError[] = "Download interrupted";
const char kInvalidDownloadError[] =
    "Download was not a valid extension or user script";
const char kDependencyNotFoundError[] = "Dependency not found";
const char kDependencyNotSharedModuleError[] =
    "Dependency is not shared module";
const char kInlineInstallSource[] = "inline";
const char kDefaultInstallSource[] = "ondemand";
const char kAppLauncherInstallSource[] = "applauncher";

// TODO(rockot): Share this duplicated constant with the extension updater.
// See http://crbug.com/40364679.
const char kAuthUserQueryKey[] = "authuser";

constexpr base::TimeDelta kTimeRemainingThreshold = base::Seconds(1);

// Folder for downloading crx files from the webstore. This is used so that the
// crx files don't go via the usual downloads folder.
const base::FilePath::CharType kWebstoreDownloadFolder[] =
    FILE_PATH_LITERAL("Webstore Downloads");

base::FilePath* g_download_directory_for_tests = nullptr;

base::FilePath GetDownloadFilePath(const base::FilePath& download_directory,
                                   const ExtensionId& id) {
  // Ensure the download directory exists. TODO(asargent) - make this use
  // common code from the downloads system.
  if (!base::DirectoryExists(download_directory) &&
      !base::CreateDirectory(download_directory)) {
    return base::FilePath();
  }

  // This is to help avoid a race condition between when we generate this
  // filename and when the download starts writing to it (think concurrently
  // running sharded browser tests installing the same test file, for
  // instance).
  std::string random_number = base::NumberToString(
      base::RandGenerator(std::numeric_limits<uint16_t>::max()));

  base::FilePath file =
      download_directory.AppendASCII(id + "_" + random_number + ".crx");

  return base::GetUniquePath(file);
}

}  // namespace

// static
void WebstoreInstaller::MaybeAppendAuthUserParameter(
    const std::string& authuser,
    GURL& url) {
  if (authuser.empty() || net::GetValueForKeyInQuery(url, kAuthUserQueryKey,
                                                     /*out_value=*/nullptr)) {
    return;
  }

  url = net::AppendQueryParameter(url, kAuthUserQueryKey, authuser);
}

namespace {

std::string GetErrorMessageForDownloadInterrupt(
    download::DownloadInterruptReason reason) {
  switch (reason) {
    case download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED:
    case download::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN:
      return l10n_util::GetStringUTF8(IDS_WEBSTORE_DOWNLOAD_ACCESS_DENIED);
    default:
      break;
  }
  return kDownloadInterruptedError;
}

}  // namespace

// static
GURL WebstoreInstaller::GetWebstoreInstallURL(const std::string& extension_id,
                                              InstallSource source) {
  std::string install_source;
  switch (source) {
    case INSTALL_SOURCE_INLINE:
      install_source = kInlineInstallSource;
      break;
    case INSTALL_SOURCE_APP_LAUNCHER:
      install_source = kAppLauncherInstallSource;
      break;
    case INSTALL_SOURCE_OTHER:
      install_source = kDefaultInstallSource;
  }

  base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
  if (cmd_line->HasSwitch(switches::kAppsGalleryDownloadURL)) {
    std::string download_url =
        cmd_line->GetSwitchValueASCII(switches::kAppsGalleryDownloadURL);
    base::ReplaceFirstSubstringAfterOffset(&download_url, 0, "%s",
                                           extension_id);
    return GURL(download_url);
  }
  std::vector<std::string_view> params;
  std::string extension_param = "id=" + extension_id;
  std::string installsource_param = "installsource=" + install_source;
  params.push_back(extension_param);
  if (!install_source.empty()) {
    params.push_back(installsource_param);
  }
  params.push_back("uc");
  std::string url_string = extension_urls::GetWebstoreUpdateUrl().spec();

  GURL url(
      url_string + "?response=redirect&" +
      update_client::UpdateQueryParams::Get(
          update_client::UpdateQueryParams::CRX) +
      "&x=" + base::EscapeQueryParamValue(base::JoinString(params, "&"), true));
  DCHECK(url.is_valid());

  return url;
}

// static
const InstallApproval* WebstoreInstaller::GetAssociatedApproval(
    const DownloadItem& download) {
  return static_cast<const InstallApproval*>(
      download.GetUserData(kApprovalKey));
}

WebstoreInstaller::WebstoreInstaller(content::BrowserContext* browser_context,
                                     SuccessCallback success_callback,
                                     FailureCallback failure_callback,
                                     content::WebContents* web_contents,
                                     const ExtensionId& id,
                                     std::unique_ptr<InstallApproval> approval,
                                     InstallSource source)
    : web_contents_(web_contents->GetWeakPtr()),
      browser_context_(browser_context),
      success_callback_(std::move(success_callback)),
      failure_callback_(std::move(failure_callback)),
      id_(id),
      install_source_(source),
      approval_(approval.release()) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(web_contents);
  CHECK(success_callback_);
  CHECK(failure_callback_);

  extension_registry_observation_.Observe(
      ExtensionRegistry::Get(browser_context));
}

void WebstoreInstaller::Start() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  AddRef();  // Balanced in ReportSuccess and ReportFailure.

  if (!crx_file::id_util::IdIsValid(id_)) {
    ReportFailure(kInvalidIdError, FAILURE_REASON_OTHER);
    return;
  }

  if (approval_.get() && approval_->dummy_extension.get()) {
    ExtensionsBrowserClient::Get()
        ->GetSharedModuleService(browser_context_)
        ->CheckImports(approval_->dummy_extension.get(), &pending_modules_,
                       &pending_modules_);
    // Do not check the return value of CheckImports, the CRX installer
    // will report appropriate error messages and fail to install if there
    // is an import error.
  }

  // Add the extension main module into the list.
  SharedModuleInfo::ImportInfo info;
  info.extension_id = id_;
  pending_modules_.push_back(info);

  total_modules_ = pending_modules_.size();

  std::set<ExtensionId> ids;
  std::list<SharedModuleInfo::ImportInfo>::const_iterator i;
  for (i = pending_modules_.begin(); i != pending_modules_.end(); ++i) {
    ids.insert(i->extension_id);
  }
  ExtensionsBrowserClient::Get()
      ->GetInstallVerifier(browser_context_)
      ->AddProvisional(ids);

  const std::string* name =
      approval_->manifest->available_values().FindString(manifest_keys::kName);
  if (!name) {
    NOTREACHED();
  }
  InstallTracker* tracker =
      ExtensionsBrowserClient::Get()->GetInstallTracker(browser_context_);
  InstallObserver::ExtensionInstallParams params(
      id_, *name, approval_->installing_icon, approval_->manifest->is_app(),
      approval_->manifest->is_platform_app());
  tracker->OnBeginExtensionInstall(params);

  tracker->OnBeginExtensionDownload(id_);

  // TODO(crbug.com/41064141): Query manifest of dependencies before
  // downloading & installing those dependencies.
  DownloadNextPendingModule();
}

void WebstoreInstaller::OnInstallerDone(
    const std::optional<CrxInstallError>& error) {
  if (!error) {
    return;
  }

  // TODO(rdevlin.cronin): Continue removing std::string errors and
  // replacing with std::u16string. See crbug.com/71980.
  const std::string utf8_error = base::UTF16ToUTF8(error->message());
  crx_installer_ = nullptr;
  // ReportFailure releases a reference to this object so it must be the
  // last operation in this method.
  ReportFailure(utf8_error, FAILURE_REASON_OTHER);
}

void WebstoreInstaller::OnExtensionInstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    bool is_update) {
  CHECK(ExtensionsBrowserClient::Get()->IsSameContext(browser_context_,
                                                      browser_context));
  if (pending_modules_.empty()) {
    return;
  }
  SharedModuleInfo::ImportInfo info = pending_modules_.front();
  if (extension->id() != info.extension_id) {
    return;
  }
  pending_modules_.pop_front();

  // Clean up local state from the current download.
  if (download_item_) {
    download_item_->RemoveObserver(this);
    download_item_->Remove();
    download_item_ = nullptr;
  }
  crx_installer_.reset();

  if (pending_modules_.empty()) {
    CHECK_EQ(extension->id(), id_);
    ReportSuccess();
  } else {
    const base::Version version_required(info.minimum_version);
    if (version_required.IsValid() &&
        extension->version().CompareTo(version_required) < 0) {
      // It should not happen, CrxInstaller will make sure the version is
      // equal or newer than version_required.
      ReportFailure(kDependencyNotFoundError,
                    FAILURE_REASON_DEPENDENCY_NOT_FOUND);
    } else if (!SharedModuleInfo::IsSharedModule(extension)) {
      // It should not happen, CrxInstaller will make sure it is a shared
      // module.
      ReportFailure(kDependencyNotSharedModuleError,
                    FAILURE_REASON_DEPENDENCY_NOT_SHARED_MODULE);
    } else {
      DownloadNextPendingModule();
    }
  }
}

void WebstoreInstaller::SetDownloadDirectoryForTests(  // IN-TEST
    base::FilePath* directory) {
  g_download_directory_for_tests = directory;
}

WebstoreInstaller::~WebstoreInstaller() {
  if (download_item_) {
    download_item_->RemoveObserver(this);
    download_item_ = nullptr;
  }
}

void WebstoreInstaller::OnDownloadStarted(
    const ExtensionId& extension_id,
    DownloadItem* item,
    download::DownloadInterruptReason interrupt_reason) {
  if (!item || interrupt_reason != download::DOWNLOAD_INTERRUPT_REASON_NONE) {
    if (item) {
      item->Remove();
    }
    ReportFailure(download::DownloadInterruptReasonToString(interrupt_reason),
                  FAILURE_REASON_OTHER);
    return;
  }

  bool found = false;
  for (const auto& module : pending_modules_) {
    if (extension_id == module.extension_id) {
      found = true;
      break;
    }
  }
  if (!found) {
    // If this extension is not pending, it means another installer has
    // installed this extension and triggered OnExtensionInstalled(). In this
    // case, either it was the main module and success has already been
    // reported, or it was a dependency and either failed (ie. wrong version) or
    // the next download was triggered. In any case, the only thing that needs
    // to be done is to stop this download.
    item->Remove();
    return;
  }

  DCHECK_EQ(download::DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason);
  DCHECK(!pending_modules_.empty());
  download_item_ = item;
  download_item_->AddObserver(this);
  if (pending_modules_.size() > 1) {
    // We are downloading a shared module. We need create an approval for it.
    std::unique_ptr<InstallApproval> approval =
        InstallApproval::CreateForSharedModule(browser_context_);
    const SharedModuleInfo::ImportInfo& info = pending_modules_.front();
    approval->extension_id = info.extension_id;
    const base::Version version_required(info.minimum_version);

    if (version_required.IsValid()) {
      approval->minimum_version =
          std::make_unique<base::Version>(version_required);
    }
    download_item_->SetUserData(kApprovalKey, std::move(approval));
  } else {
    // It is for the main module of the extension. We should use the provided
    // |approval_|.
    if (approval_) {
      download_item_->SetUserData(kApprovalKey, std::move(approval_));
    }
  }

  if (!download_started_) {
    download_started_ = true;
  }
}

void WebstoreInstaller::OnDownloadUpdated(DownloadItem* download) {
  CHECK_EQ(download_item_, download);

  switch (download->GetState()) {
    case DownloadItem::CANCELLED:
      ReportFailure(kDownloadCanceledError, FAILURE_REASON_CANCELLED);
      break;
    case DownloadItem::INTERRUPTED:
      ReportFailure(
          GetErrorMessageForDownloadInterrupt(download->GetLastReason()),
          FAILURE_REASON_OTHER);
      break;
    case DownloadItem::COMPLETE:
      // Stop the progress timer if it's running.
      download_progress_timer_.Stop();

      // Only wait for other notifications if the download is really
      // an extension.
      if (!util::IsExtensionDownload(*download)) {
        ReportFailure(kInvalidDownloadError, FAILURE_REASON_OTHER);
        return;
      }

      if (crx_installer_.get()) {
        return;  // DownloadItemImpl calls the observer twice, ignore it.
      }

      StartCrxInstaller(*download);

      if (pending_modules_.size() == 1) {
        // The download is the last module - the extension main module.
        InstallTracker* tracker =
            ExtensionsBrowserClient::Get()->GetInstallTracker(browser_context_);
        tracker->OnDownloadProgress(id_, 100);
      }
      break;
    case DownloadItem::IN_PROGRESS: {
      UpdateDownloadProgress();
      break;
    }
    default:
      // Continue listening if the download is not in one of the above states.
      break;
  }
}

void WebstoreInstaller::OnDownloadDestroyed(DownloadItem* download) {
  CHECK_EQ(download_item_, download);
  download_item_->RemoveObserver(this);
  download_item_ = nullptr;
}

void WebstoreInstaller::DownloadNextPendingModule() {
  CHECK(!pending_modules_.empty());
  if (pending_modules_.size() == 1) {
    DCHECK_EQ(id_, pending_modules_.front().extension_id);
    DownloadCrx(id_, install_source_);
  } else {
    DownloadCrx(pending_modules_.front().extension_id, INSTALL_SOURCE_OTHER);
  }
}

void WebstoreInstaller::DownloadCrx(const ExtensionId& extension_id,
                                    InstallSource source) {
  download_url_ = GetWebstoreInstallURL(extension_id, source);
  MaybeAppendAuthUserParameter(approval_->authuser, download_url_);

  base::FilePath user_data_dir =
      ExtensionsBrowserClient::Get()->GetUserDataDir();
  base::FilePath download_path = user_data_dir.Append(kWebstoreDownloadFolder);

  base::FilePath download_directory(g_download_directory_for_tests
                                        ? *g_download_directory_for_tests
                                        : download_path);

  GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetDownloadFilePath, download_directory, extension_id),
      base::BindOnce(&WebstoreInstaller::StartDownload, this, extension_id));
}

// http://crbug.com/40957413
// http://crbug.com/40798038
// The current working theory is that one of the many pointers dereferenced in
// here is occasionally deleted before all of its referrers are nullified,
// probably in a callback race. After this comment is released, the crash
// reports should narrow down exactly which pointer it is.  Collapsing all the
// early-returns into a single branch makes it hard to see exactly which pointer
// it is.
void WebstoreInstaller::StartDownload(const ExtensionId& extension_id,
                                      const base::FilePath& file) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (file.empty()) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }

  DownloadManager* download_manager = browser_context_->GetDownloadManager();
  if (!download_manager) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }

  if (!web_contents_) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }
  if (!web_contents_->GetPrimaryMainFrame()->GetRenderViewHost()) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }
  if (!web_contents_->GetPrimaryMainFrame()
           ->GetRenderViewHost()
           ->GetProcess()) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }

  content::NavigationController& controller = web_contents_->GetController();
  if (!controller.GetBrowserContext()) {
    ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER);
    return;
  }

  // The download url for the given extension is contained in |download_url_|.
  // We will navigate the current tab to this url to start the download. The
  // download system will then pass the crx to the CrxInstaller.
  content::RenderFrameHost* render_frame_host =
      web_contents_->GetPrimaryMainFrame();
  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("webstore_installer", R"(
        semantics {
          sender: "Webstore Installer"
          description: "Downloads an extension for installation."
          trigger:
            "User initiates a webstore extension installation flow, including "
            "installing from the webstore, inline installation from a site, "
            "re-installing a corrupted extension, and others."
          data:
            "The id of the extension to be installed and information about the "
            "user's installation, including version, language, distribution "
            "(Chrome vs Chromium), NaCl architecture, installation source (as "
            "an enum), and accepted crx formats."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              owners: "//extensions/OWNERS"
            }
          }
          user_data {
            type: NONE
          }
          last_reviewed: "2026-03-20"
        }
        policy {
          cookies_allowed: YES
          cookies_store: "user"
          setting:
            "This feature cannot be disabled. It is only activated if the user "
            "triggers an extension installation."
          chrome_policy {
            ExtensionInstallBlocklist {
              ExtensionInstallBlocklist: {
                entries: '*'
              }
            }
          }
        })");
  std::unique_ptr<DownloadUrlParameters> params =
      render_frame_host->CreateDownloadUrlParameters(download_url_,
                                                     traffic_annotation);
  params->set_file_path(file);
  if (controller.GetVisibleEntry()) {
    content::Referrer referrer = content::Referrer::SanitizeForRequest(
        download_url_,
        content::Referrer(controller.GetVisibleEntry()->GetURL(),
                          network::mojom::ReferrerPolicy::kDefault));
    params->set_referrer(referrer.url);
    params->set_referrer_policy(
        content::Referrer::ReferrerPolicyForUrlRequest(referrer.policy));
  }
  params->set_callback(base::BindOnce(&WebstoreInstaller::OnDownloadStarted,
                                      this, extension_id));
  params->set_download_source(download::DownloadSource::EXTENSION_INSTALLER);

  // This is set to `true` so that the download stack can correctly apply
  // download restriction policies and understand that a user gesture started
  // the extension download.
  params->set_has_user_gesture(true);

  download_manager->DownloadUrl(std::move(params));
}

void WebstoreInstaller::UpdateDownloadProgress() {
  // If the download has gone away, or isn't in progress (in which case we can't
  // give a good progress estimate), stop any running timers and return.
  if (!download_item_ ||
      download_item_->GetState() != DownloadItem::IN_PROGRESS) {
    download_progress_timer_.Stop();
    return;
  }

  int percent = download_item_->PercentComplete();
  // Only report progress if percent is more than 0 or we have finished
  // downloading at least one of the pending modules.
  int finished_modules = total_modules_ - pending_modules_.size();
  if (finished_modules > 0 && percent < 0) {
    percent = 0;
  }
  if (percent >= 0) {
    percent = (percent + (finished_modules * 100)) / total_modules_;
    InstallTracker* tracker =
        ExtensionsBrowserClient::Get()->GetInstallTracker(browser_context_);
    tracker->OnDownloadProgress(id_, percent);
  }

  // If there's enough time remaining on the download to warrant an update,
  // set the timer (overwriting any current timers). Otherwise, stop the
  // timer.
  base::TimeDelta time_remaining;
  if (download_item_->TimeRemaining(&time_remaining) &&
      time_remaining > kTimeRemainingThreshold) {
    download_progress_timer_.Start(FROM_HERE, kTimeRemainingThreshold, this,
                                   &WebstoreInstaller::UpdateDownloadProgress);
  } else {
    download_progress_timer_.Stop();
  }
}

void WebstoreInstaller::StartCrxInstaller(const DownloadItem& download) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!crx_installer_.get());

  const InstallApproval* approval = GetAssociatedApproval(download);
  DCHECK(approval);

  crx_installer_ =
      ExtensionsBrowserClient::Get()->CreateCrxInstallerFromDownloadItem(
          browser_context_, download);

  crx_installer_->set_expected_id(approval->extension_id);
  crx_installer_->set_is_gallery_install(true);
  crx_installer_->set_allow_silent_install(true);
  crx_installer_->AddInstallerCallback(base::BindOnce(
      &WebstoreInstaller::OnInstallerDone, weak_ptr_factory_.GetWeakPtr()));
  if (approval->withhold_permissions) {
    crx_installer_->set_withhold_permissions();
  }

  crx_installer_->InstallCrx(download.GetFullPath());
}

void WebstoreInstaller::ReportFailure(const std::string& error,
                                      FailureReason reason) {
  CHECK(failure_callback_);
  std::move(failure_callback_).Run(id_, error, reason);
  success_callback_ = base::NullCallback();
  extension_registry_observation_.Reset();

  InstallTracker* tracker =
      ExtensionsBrowserClient::Get()->GetInstallTracker(browser_context_);
  tracker->OnInstallFailure(id_);

  Release();  // Balanced in Start().
}

void WebstoreInstaller::ReportSuccess() {
  CHECK(success_callback_);
  std::move(success_callback_).Run(id_);
  failure_callback_ = base::NullCallback();
  extension_registry_observation_.Reset();

  Release();  // Balanced in Start().
}

}  // namespace extensions
