// Copyright 2014 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/extensions/external_install_error_desktop.h"

#include <stddef.h>

#include <memory>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/extensions/extension_install_error_menu_item_id_provider.h"
#include "chrome/browser/extensions/extension_install_prompt_show_params.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/external_install_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/global_error/global_error.h"
#include "chrome/browser/ui/global_error/global_error_service.h"
#include "chrome/browser/ui/global_error/global_error_service_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/grit/generated_resources.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pref_names.h"
#include "extensions/browser/ui_util.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/browser/webstore_data_fetcher.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/native_ui_types.h"

namespace extensions {

namespace {

// Return the menu label for a global error.
std::u16string GetMenuItemLabel(const Extension* extension) {
  if (!extension) {
    return std::u16string();
  }

  int id = -1;
  if (extension->is_app()) {
    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP;
  } else if (extension->is_theme()) {
    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME;
  } else {
    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION;
  }

  return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension->name()));
}

// A global error that spawns a dialog when the menu item is clicked.
class ExternalInstallMenuAlert : public GlobalError {
 public:
  explicit ExternalInstallMenuAlert(ExternalInstallErrorDesktop* error);

  ExternalInstallMenuAlert(const ExternalInstallMenuAlert&) = delete;
  ExternalInstallMenuAlert& operator=(const ExternalInstallMenuAlert&) = delete;

  ~ExternalInstallMenuAlert() override;

 private:
  // GlobalError implementation.
  Severity GetSeverity() override;
  bool HasMenuItem() override;
  int MenuItemCommandID() override;
  std::u16string MenuItemLabel() override;
  void ExecuteMenuItem(BrowserWindowInterface* browser) override;
  bool HasBubbleView() override;
  bool HasShownBubbleView() override;
  void ShowBubbleView(BrowserWindowInterface* browser) override;
  GlobalErrorBubbleViewBase* GetBubbleView() override;

  // The owning ExternalInstallErrorDesktop.
  raw_ptr<ExternalInstallErrorDesktop> error_;

  // Provides menu item id for GlobalError.
  ExtensionInstallErrorMenuItemIdProvider id_provider_;
};

// A global error that spawns a bubble when the menu item is clicked.
class ExternalInstallBubbleAlert final : public GlobalErrorWithStandardBubble {
 public:
  ExternalInstallBubbleAlert(ExternalInstallError* error,
                             InstallPromptData* prompt);

  ExternalInstallBubbleAlert(const ExternalInstallBubbleAlert&) = delete;
  ExternalInstallBubbleAlert& operator=(const ExternalInstallBubbleAlert&) =
      delete;

  ~ExternalInstallBubbleAlert() override;

 private:
  // GlobalError implementation.
  Severity GetSeverity() override;
  bool HasMenuItem() override;
  int MenuItemCommandID() override;
  std::u16string MenuItemLabel() override;
  void ExecuteMenuItem(BrowserWindowInterface* browser) override;

  // GlobalErrorWithStandardBubble implementation.
  std::u16string GetBubbleViewTitle() override;
  std::vector<std::u16string> GetBubbleViewMessages() override;
  std::u16string GetBubbleViewAcceptButtonLabel() override;
  std::u16string GetBubbleViewCancelButtonLabel() override;
  void OnBubbleViewDidClose(BrowserWindowInterface* browser) override;
  void BubbleViewAcceptButtonPressed(BrowserWindowInterface* browser) override;
  void BubbleViewCancelButtonPressed(BrowserWindowInterface* browser) override;
  base::WeakPtr<GlobalErrorWithStandardBubble> AsWeakPtr() override;

  // The owning ExternalInstallErrorDesktop.
  raw_ptr<ExternalInstallError> error_;
  ExtensionInstallErrorMenuItemIdProvider id_provider_;

  // The Prompt with all information, which we then use to populate the bubble.
  // Owned by |error|.
  raw_ptr<InstallPromptData> prompt_;

  base::WeakPtrFactory<ExternalInstallBubbleAlert> weak_ptr_factory_{this};
};

////////////////////////////////////////////////////////////////////////////////
// ExternalInstallMenuAlert

ExternalInstallMenuAlert::ExternalInstallMenuAlert(
    ExternalInstallErrorDesktop* error)
    : error_(error) {}

ExternalInstallMenuAlert::~ExternalInstallMenuAlert() = default;

GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() {
  return SEVERITY_LOW;
}

bool ExternalInstallMenuAlert::HasMenuItem() {
  return true;
}

int ExternalInstallMenuAlert::MenuItemCommandID() {
  return id_provider_.menu_command_id();
}

std::u16string ExternalInstallMenuAlert::MenuItemLabel() {
  return GetMenuItemLabel(error_->GetExtension());
}

void ExternalInstallMenuAlert::ExecuteMenuItem(
    BrowserWindowInterface* browser) {
  error_->ShowDialog(browser);
}

bool ExternalInstallMenuAlert::HasBubbleView() {
  return false;
}

bool ExternalInstallMenuAlert::HasShownBubbleView() {
  NOTREACHED();
}

void ExternalInstallMenuAlert::ShowBubbleView(BrowserWindowInterface* browser) {
  NOTREACHED();
}

GlobalErrorBubbleViewBase* ExternalInstallMenuAlert::GetBubbleView() {
  return nullptr;
}

////////////////////////////////////////////////////////////////////////////////
// ExternalInstallBubbleAlert

ExternalInstallBubbleAlert::ExternalInstallBubbleAlert(
    ExternalInstallError* error,
    InstallPromptData* prompt)
    : error_(error), prompt_(prompt) {
  DCHECK(error_);
  DCHECK(prompt_);
}

ExternalInstallBubbleAlert::~ExternalInstallBubbleAlert() = default;

GlobalError::Severity ExternalInstallBubbleAlert::GetSeverity() {
  return SEVERITY_LOW;
}

bool ExternalInstallBubbleAlert::HasMenuItem() {
  return true;
}

int ExternalInstallBubbleAlert::MenuItemCommandID() {
  return id_provider_.menu_command_id();
}

std::u16string ExternalInstallBubbleAlert::MenuItemLabel() {
  return GetMenuItemLabel(error_->GetExtension());
}

void ExternalInstallBubbleAlert::ExecuteMenuItem(
    BrowserWindowInterface* browser) {
  // |browser| is nullptr in unit test.
  if (browser) {
    ShowBubbleView(browser);
  }
  error_->DidOpenBubbleView();
}

std::u16string ExternalInstallBubbleAlert::GetBubbleViewTitle() {
  return l10n_util::GetStringFUTF16(
      IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_BUBBLE_TITLE,
      ui_util::GetFixupExtensionNameForUIDisplay(prompt_->extension()->name()));
}

std::vector<std::u16string>
ExternalInstallBubbleAlert::GetBubbleViewMessages() {
  std::vector<std::u16string> messages;
  int heading_id =
      IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_BUBBLE_HEADING_EXTENSION;
  if (prompt_->extension()->is_app()) {
    heading_id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_BUBBLE_HEADING_APP;
  } else if (prompt_->extension()->is_theme()) {
    heading_id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_BUBBLE_HEADING_THEME;
  }
  messages.push_back(l10n_util::GetStringUTF16(heading_id));

  if (prompt_->GetPermissionCount()) {
    messages.push_back(prompt_->GetPermissionsHeading());
    for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) {
      messages.push_back(l10n_util::GetStringFUTF16(
          IDS_EXTENSION_PERMISSION_LINE, prompt_->GetPermission(i)));
    }
  }
  // TODO(yoz): OAuth issue advice?
  return messages;
}

std::u16string ExternalInstallBubbleAlert::GetBubbleViewAcceptButtonLabel() {
  return prompt_->GetAcceptButtonLabel();
}

std::u16string ExternalInstallBubbleAlert::GetBubbleViewCancelButtonLabel() {
  return prompt_->GetAbortButtonLabel();
}

void ExternalInstallBubbleAlert::OnBubbleViewDidClose(
    BrowserWindowInterface* browser) {
  error_->DidCloseBubbleView();
}

void ExternalInstallBubbleAlert::BubbleViewAcceptButtonPressed(
    BrowserWindowInterface* browser) {
  error_->OnInstallPromptDone(ExtensionInstallPrompt::DoneCallbackPayload(
      ExtensionInstallPrompt::Result::ACCEPTED));
}

void ExternalInstallBubbleAlert::BubbleViewCancelButtonPressed(
    BrowserWindowInterface* browser) {
  error_->OnInstallPromptDone(ExtensionInstallPrompt::DoneCallbackPayload(
      ExtensionInstallPrompt::Result::USER_CANCELED));
}

base::WeakPtr<GlobalErrorWithStandardBubble>
ExternalInstallBubbleAlert::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ExternalInstallErrorDesktop

ExternalInstallErrorDesktop::ExternalInstallErrorDesktop(
    content::BrowserContext* browser_context,
    const std::string& extension_id,
    AlertType alert_type,
    ExternalInstallManager* manager)
    : browser_context_(browser_context),
      extension_id_(extension_id),
      alert_type_(alert_type),
      manager_(manager),
      error_service_(GlobalErrorServiceFactory::GetForProfile(
          Profile::FromBrowserContext(browser_context_))) {
  prompt_ = std::make_unique<InstallPromptData>(
      InstallPromptData::EXTERNAL_INSTALL_PROMPT);

  const Extension* extension = GetExtension();

  if (base::FeatureList::IsEnabled(features::kInitialExternalExtensions) &&
      extension &&
      extension->location() == mojom::ManifestLocation::kExternalPrefDownload) {
    Profile* profile = Profile::FromBrowserContext(browser_context_);

    PrefService* prefs = profile->GetPrefs();

    const base::ListValue& initial_list =
        prefs->GetList(pref_names::kInitialInstallList);

    if (initial_list.contains(extension_id_)) {
      prompt_->SetInitialExtensionsProviderName(base::UTF8ToUTF16(
          prefs->GetString(pref_names::kInitialInstallProviderName)));
    }
  }

  ExtensionManagement* extension_management =
      ExtensionManagementFactory::GetForBrowserContext(browser_context_);

  // Only make a call to fetch webstore data if the `extension` updates from the
  // webstore. Otherwise, show a prompt without webstore data.
  if (extension && extension_management->UpdatesFromWebstore(*extension)) {
    webstore_data_fetcher_ =
        std::make_unique<WebstoreDataFetcher>(this, GURL(), extension_id_);
    webstore_data_fetcher_->Start(browser_context_->GetDefaultStoragePartition()
                                      ->GetURLLoaderFactoryForBrowserProcess()
                                      .get());
  } else {
    OnFetchComplete();
  }
}

ExternalInstallErrorDesktop::~ExternalInstallErrorDesktop() {
#if DCHECK_IS_ON()
  // Errors should only be removed while the profile is valid, since removing
  // the error can trigger other subsystems listening for changes.
  BrowserContextDependencyManager::GetInstance()
      ->AssertBrowserContextWasntDestroyed(browser_context_);
#endif
  if (global_error_.get()) {
    error_service_->RemoveUnownedGlobalError(global_error_.get());
  }
}

void ExternalInstallErrorDesktop::OnInstallPromptDone(
    ExtensionInstallPrompt::DoneCallbackPayload payload) {
  const Extension* extension = GetExtension();

  // If the error isn't removed and deleted as part of handling the user's
  // response (which can happen, e.g., if an uninstall fails), be sure to remove
  // the error directly in order to ensure it's not called twice.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&ExternalInstallErrorDesktop::RemoveError,
                                weak_factory_.GetWeakPtr()));

  switch (payload.result) {
    case ExtensionInstallPrompt::Result::ACCEPTED:
      if (extension) {
        ExtensionRegistrar::Get(browser_context_)
            ->GrantPermissionsAndEnableExtension(*extension);
      }
      break;
    case ExtensionInstallPrompt::Result::ACCEPTED_WITH_WITHHELD_PERMISSIONS:
      // TODO(crbug.com/40636075): Handle `ACCEPTED_WITH_WITHHELD_PERMISSIONS`
      // when it is supported for external installs.
      NOTREACHED();
    case ExtensionInstallPrompt::Result::USER_CANCELED:
      if (extension) {
        ExtensionRegistrar::Get(browser_context_)
            ->UninstallExtension(extension_id_,
                                 extensions::UNINSTALL_REASON_INSTALL_CANCELED,
                                 nullptr);  // Ignore error.
      }
      break;
    case ExtensionInstallPrompt::Result::ABORTED:
      manager_->DidChangeInstallAlertVisibility(this, false);
      break;
  }
  // NOTE: We may be deleted here!
}

void ExternalInstallErrorDesktop::DidOpenBubbleView() {
  manager_->DidChangeInstallAlertVisibility(this, true);
}

void ExternalInstallErrorDesktop::DidCloseBubbleView() {
  manager_->DidChangeInstallAlertVisibility(this, false);
}

void ExternalInstallErrorDesktop::ShowDialog(BrowserWindowInterface* browser) {
  DCHECK(install_ui_.get());
  DCHECK(prompt_.get());
  DCHECK(browser);
  content::WebContents* web_contents = nullptr;
  web_contents = browser->GetTabStripModel()->GetActiveWebContents();
  manager_->DidChangeInstallAlertVisibility(this, true);
  ExtensionInstallPrompt::GetDefaultShowDialogCallback().Run(
      std::make_unique<ExtensionInstallPromptShowParams>(web_contents),
      base::BindOnce(&ExternalInstallErrorDesktop::OnInstallPromptDone,
                     weak_factory_.GetWeakPtr()),
      std::move(prompt_));
}

const Extension* ExternalInstallErrorDesktop::GetExtension() const {
  return ExtensionRegistry::Get(browser_context_)
      ->GetExtensionById(extension_id_, ExtensionRegistry::EVERYTHING);
}

const ExtensionId& ExternalInstallErrorDesktop::extension_id() const {
  return extension_id_;
}

ExternalInstallError::AlertType ExternalInstallErrorDesktop::alert_type()
    const {
  return alert_type_;
}

InstallPromptData* ExternalInstallErrorDesktop::GetPromptForTesting() const {
  return prompt_.get();
}

void ExternalInstallErrorDesktop::OnWebstoreRequestFailure(
    const std::string& extension_id) {
  OnFetchComplete();
}

void ExternalInstallErrorDesktop::OnFetchItemSnippetParseSuccess(
    const std::string& extension_id,
    FetchItemSnippetResponse item_snippet) {
  prompt_->SetWebstoreData(item_snippet.user_count_string(),
                           !item_snippet.user_count_string().empty(),
                           item_snippet.average_rating(),
                           base::checked_cast<int>(item_snippet.rating_count()),
                           item_snippet.rating_count_string());
  OnFetchComplete();
}

void ExternalInstallErrorDesktop::OnWebstoreResponseParseFailure(
    const std::string& extension_id,
    const std::string& error) {
  OnFetchComplete();
}

void ExternalInstallErrorDesktop::OnFetchComplete() {
  // Create a new ExtensionInstallPrompt. We pass in NULL for the UI
  // components because we display at a later point, and don't want
  // to pass ones which may be invalidated.
  install_ui_ = base::WrapUnique(new ExtensionInstallPrompt(
      Profile::FromBrowserContext(browser_context_),
      /*native_window=*/gfx::NativeWindow(), std::move(prompt_)));

  install_ui_->ShowDialog(
      base::BindOnce(&ExternalInstallErrorDesktop::OnInstallPromptDone,
                     weak_factory_.GetWeakPtr()),
      GetExtension(),
      nullptr,  // Force a fetch of the icon.
      base::BindRepeating(&ExternalInstallErrorDesktop::OnDialogReady,
                          weak_factory_.GetWeakPtr()));
}

void ExternalInstallErrorDesktop::OnDialogReady(
    std::unique_ptr<ExtensionInstallPromptShowParams> show_params,
    ExtensionInstallPrompt::DoneCallback callback,
    std::unique_ptr<InstallPromptData> prompt) {
  prompt_ = std::move(prompt);

  if (alert_type_ == BUBBLE_ALERT) {
    global_error_ =
        std::make_unique<ExternalInstallBubbleAlert>(this, prompt_.get());
    error_service_->AddUnownedGlobalError(global_error_.get());

    if (!manager_->has_currently_visible_install_alert()) {
      // |browser| is nullptr during unit tests, so call
      // DidChangeInstallAlertVisibility() regardless because we depend on this
      // in unit tests.
      manager_->DidChangeInstallAlertVisibility(this, true);
      BrowserWindowInterface* browser =
          ProfileBrowserCollection::GetForProfile(
              Profile::FromBrowserContext(browser_context_))
              ->FindTabbedBrowser(/*match_original_profiles=*/true);
      if (browser) {
        global_error_->ShowBubbleView(browser);
      }
    }
  } else {
    DCHECK(alert_type_ == MENU_ALERT);
    global_error_ = std::make_unique<ExternalInstallMenuAlert>(this);
    error_service_->AddUnownedGlobalError(global_error_.get());
  }
}

void ExternalInstallErrorDesktop::RemoveError() {
  manager_->RemoveExternalInstallError(extension_id_);
}

}  // namespace extensions
