// 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 "chrome/browser/extensions/extension_disabled_ui.h"

#include <bitset>
#include <string>

#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/thread_task_runner_handle.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_install_prompt.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_uninstall_dialog.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.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/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
#include "content/public/browser/notification_source.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/image_loader.h"
#include "extensions/browser/notification_types.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_icon_set.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/permissions/permission_message.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"

using extensions::Extension;
using extensions::PermissionMessage;
using extensions::PermissionMessages;

namespace {

static const int kIconSize = extension_misc::EXTENSION_ICON_SMALL;

static base::LazyInstance<
    std::bitset<IDC_EXTENSION_DISABLED_LAST -
                IDC_EXTENSION_DISABLED_FIRST + 1> >
    menu_command_ids = LAZY_INSTANCE_INITIALIZER;

// Get an available menu ID.
int GetMenuCommandID() {
  int id;
  for (id = IDC_EXTENSION_DISABLED_FIRST;
       id <= IDC_EXTENSION_DISABLED_LAST; ++id) {
    if (!menu_command_ids.Get()[id - IDC_EXTENSION_DISABLED_FIRST]) {
      menu_command_ids.Get().set(id - IDC_EXTENSION_DISABLED_FIRST);
      return id;
    }
  }
  // This should not happen.
  DCHECK(id <= IDC_EXTENSION_DISABLED_LAST) <<
      "No available menu command IDs for ExtensionDisabledGlobalError";
  return IDC_EXTENSION_DISABLED_LAST;
}

// Make a menu ID available when it is no longer used.
void ReleaseMenuCommandID(int id) {
  menu_command_ids.Get().reset(id - IDC_EXTENSION_DISABLED_FIRST);
}

}  // namespace

// ExtensionDisabledDialogDelegate --------------------------------------------

class ExtensionDisabledDialogDelegate
    : public ExtensionInstallPrompt::Delegate,
      public base::RefCountedThreadSafe<ExtensionDisabledDialogDelegate> {
 public:
  ExtensionDisabledDialogDelegate(ExtensionService* service,
                                  scoped_ptr<ExtensionInstallPrompt> install_ui,
                                  const Extension* extension);

 private:
  friend class base::RefCountedThreadSafe<ExtensionDisabledDialogDelegate>;

  ~ExtensionDisabledDialogDelegate() override;

  // ExtensionInstallPrompt::Delegate:
  void InstallUIProceed() override;
  void InstallUIAbort(bool user_initiated) override;

  // The UI for showing the install dialog when enabling.
  scoped_ptr<ExtensionInstallPrompt> install_ui_;

  ExtensionService* service_;
  const Extension* extension_;
};

ExtensionDisabledDialogDelegate::ExtensionDisabledDialogDelegate(
    ExtensionService* service,
    scoped_ptr<ExtensionInstallPrompt> install_ui,
    const Extension* extension)
    : install_ui_(install_ui.Pass()),
      service_(service),
      extension_(extension) {
  AddRef();  // Balanced in Proceed or Abort.
  install_ui_->ConfirmReEnable(this, extension_);
}

ExtensionDisabledDialogDelegate::~ExtensionDisabledDialogDelegate() {
}

void ExtensionDisabledDialogDelegate::InstallUIProceed() {
  service_->GrantPermissionsAndEnableExtension(extension_);
  Release();
}

void ExtensionDisabledDialogDelegate::InstallUIAbort(bool user_initiated) {
  std::string histogram_name = user_initiated ? "ReEnableCancel"
                                              : "ReEnableAbort";
  ExtensionService::RecordPermissionMessagesHistogram(
      extension_, histogram_name.c_str());

  // Do nothing. The extension will remain disabled.
  Release();
}

// ExtensionDisabledGlobalError -----------------------------------------------

class ExtensionDisabledGlobalError
    : public GlobalErrorWithStandardBubble,
      public content::NotificationObserver,
      public extensions::ExtensionUninstallDialog::Delegate {
 public:
  ExtensionDisabledGlobalError(ExtensionService* service,
                               const Extension* extension,
                               bool is_remote_install,
                               const gfx::Image& icon);
  ~ExtensionDisabledGlobalError() override;

  // GlobalError implementation.
  Severity GetSeverity() override;
  bool HasMenuItem() override;
  int MenuItemCommandID() override;
  base::string16 MenuItemLabel() override;
  void ExecuteMenuItem(Browser* browser) override;
  gfx::Image GetBubbleViewIcon() override;
  base::string16 GetBubbleViewTitle() override;
  std::vector<base::string16> GetBubbleViewMessages() override;
  base::string16 GetBubbleViewAcceptButtonLabel() override;
  base::string16 GetBubbleViewCancelButtonLabel() override;
  void OnBubbleViewDidClose(Browser* browser) override;
  void BubbleViewAcceptButtonPressed(Browser* browser) override;
  void BubbleViewCancelButtonPressed(Browser* browser) override;
  bool ShouldCloseOnDeactivate() const override;

  // ExtensionUninstallDialog::Delegate implementation.
  void OnExtensionUninstallDialogClosed(bool did_start_uninstall,
                                        const base::string16& error) override;

  // content::NotificationObserver implementation.
  void Observe(int type,
               const content::NotificationSource& source,
               const content::NotificationDetails& details) override;

 private:
  ExtensionService* service_;
  const Extension* extension_;
  bool is_remote_install_;
  gfx::Image icon_;

  // How the user responded to the error; used for metrics.
  enum UserResponse {
    IGNORED,
    REENABLE,
    UNINSTALL,
    EXTENSION_DISABLED_UI_BUCKET_BOUNDARY
  };
  UserResponse user_response_;

  scoped_ptr<extensions::ExtensionUninstallDialog> uninstall_dialog_;

  // Menu command ID assigned for this extension's error.
  int menu_command_id_;

  content::NotificationRegistrar registrar_;
};

// TODO(yoz): create error at startup for disabled extensions.
ExtensionDisabledGlobalError::ExtensionDisabledGlobalError(
    ExtensionService* service,
    const Extension* extension,
    bool is_remote_install,
    const gfx::Image& icon)
    : service_(service),
      extension_(extension),
      is_remote_install_(is_remote_install),
      icon_(icon),
      user_response_(IGNORED),
      menu_command_id_(GetMenuCommandID()) {
  if (icon_.IsEmpty()) {
    icon_ = gfx::Image(
        gfx::ImageSkiaOperations::CreateResizedImage(
            extension_->is_app() ?
                extensions::util::GetDefaultAppIcon() :
                extensions::util::GetDefaultExtensionIcon(),
            skia::ImageOperations::RESIZE_BEST,
            gfx::Size(kIconSize, kIconSize)));
  }
  registrar_.Add(this,
                 extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
                 content::Source<Profile>(service->profile()));
  registrar_.Add(this,
                 extensions::NOTIFICATION_EXTENSION_REMOVED,
                 content::Source<Profile>(service->profile()));
}

ExtensionDisabledGlobalError::~ExtensionDisabledGlobalError() {
  ReleaseMenuCommandID(menu_command_id_);
  if (is_remote_install_) {
    UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponseRemoteInstall",
                              user_response_,
                              EXTENSION_DISABLED_UI_BUCKET_BOUNDARY);
  } else {
    UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponse",
                              user_response_,
                              EXTENSION_DISABLED_UI_BUCKET_BOUNDARY);
  }
}

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

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

int ExtensionDisabledGlobalError::MenuItemCommandID() {
  return menu_command_id_;
}

base::string16 ExtensionDisabledGlobalError::MenuItemLabel() {
  std::string extension_name = extension_->name();
  // Ampersands need to be escaped to avoid being treated like
  // mnemonics in the menu.
  base::ReplaceChars(extension_name, "&", "&&", &extension_name);

  if (is_remote_install_) {
    return l10n_util::GetStringFUTF16(
        IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE,
        base::UTF8ToUTF16(extension_name));
  } else {
    return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE,
                                      base::UTF8ToUTF16(extension_name));
  }
}

void ExtensionDisabledGlobalError::ExecuteMenuItem(Browser* browser) {
  ShowBubbleView(browser);
}

gfx::Image ExtensionDisabledGlobalError::GetBubbleViewIcon() {
  return icon_;
}

base::string16 ExtensionDisabledGlobalError::GetBubbleViewTitle() {
  if (is_remote_install_) {
    return l10n_util::GetStringFUTF16(
        IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE,
        base::UTF8ToUTF16(extension_->name()));
  } else {
    return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE,
                                      base::UTF8ToUTF16(extension_->name()));
  }
}

std::vector<base::string16>
ExtensionDisabledGlobalError::GetBubbleViewMessages() {
  std::vector<base::string16> messages;
  PermissionMessages permission_warnings =
      extension_->permissions_data()->GetPermissionMessages();
  if (is_remote_install_) {
    messages.push_back(l10n_util::GetStringFUTF16(
        extension_->is_app()
            ? IDS_APP_DISABLED_REMOTE_INSTALL_ERROR_LABEL
            : IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_LABEL,
        base::UTF8ToUTF16(extension_->name())));
    if (!permission_warnings.empty())
      messages.push_back(
          l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO));
  } else {
    // TODO(treib): If NeedCustodianApprovalForPermissionIncrease, add an extra
    // message for supervised users. crbug.com/461261
    messages.push_back(l10n_util::GetStringFUTF16(
        extension_->is_app() ? IDS_APP_DISABLED_ERROR_LABEL
                             : IDS_EXTENSION_DISABLED_ERROR_LABEL,
        base::UTF8ToUTF16(extension_->name())));
    messages.push_back(l10n_util::GetStringUTF16(
        IDS_EXTENSION_PROMPT_WILL_NOW_HAVE_ACCESS_TO));
  }
  for (const PermissionMessage& msg : permission_warnings) {
    messages.push_back(l10n_util::GetStringFUTF16(IDS_EXTENSION_PERMISSION_LINE,
                                                  msg.message()));
  }
  return messages;
}

base::string16 ExtensionDisabledGlobalError::GetBubbleViewAcceptButtonLabel() {
  if (extensions::util::IsExtensionSupervised(extension_,
                                              service_->profile()) &&
      extensions::util::NeedCustodianApprovalForPermissionIncrease()) {
    // TODO(treib): Probably use a new string here once we get UX design.
    // For now, just use "OK". crbug.com/461261
    return l10n_util::GetStringUTF16(IDS_OK);
  }
  if (is_remote_install_) {
    return l10n_util::GetStringUTF16(
        extension_->is_app()
            ? IDS_EXTENSION_PROMPT_REMOTE_INSTALL_BUTTON_APP
            : IDS_EXTENSION_PROMPT_REMOTE_INSTALL_BUTTON_EXTENSION);
  }
  return l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_RE_ENABLE_BUTTON);
}

base::string16 ExtensionDisabledGlobalError::GetBubbleViewCancelButtonLabel() {
  if (extensions::util::IsExtensionSupervised(extension_,
                                              service_->profile())) {
    if (extensions::util::NeedCustodianApprovalForPermissionIncrease()) {
      // If the supervised user can't approve the update, then there is no
      // "cancel" button.
      return base::string16();
    } else {
      // Supervised users can not remove extensions, so use "cancel" here
      // instead of "uninstall".
      return l10n_util::GetStringUTF16(IDS_CANCEL);
    }
  }
  return l10n_util::GetStringUTF16(IDS_EXTENSIONS_UNINSTALL);
}

void ExtensionDisabledGlobalError::OnBubbleViewDidClose(Browser* browser) {
}

void ExtensionDisabledGlobalError::BubbleViewAcceptButtonPressed(
    Browser* browser) {
  if (extensions::util::IsExtensionSupervised(extension_,
                                              service_->profile()) &&
      extensions::util::NeedCustodianApprovalForPermissionIncrease()) {
    return;
  }
  // Delay extension reenabling so this bubble closes properly.
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::Bind(&ExtensionService::GrantPermissionsAndEnableExtension,
                 service_->AsWeakPtr(), extension_));
}

void ExtensionDisabledGlobalError::BubbleViewCancelButtonPressed(
    Browser* browser) {
  if (extensions::util::IsExtensionSupervised(extension_,
                                              service_->profile())) {
    // For custodian-installed extensions, this button should only exist if the
    // supervised user can approve the update. Otherwise there is only an "OK"
    // button.
    DCHECK(!extensions::util::NeedCustodianApprovalForPermissionIncrease());
    // Supervised users may never remove custodian-installed extensions.
    return;
  }

  uninstall_dialog_.reset(extensions::ExtensionUninstallDialog::Create(
      service_->profile(), browser->window()->GetNativeWindow(), this));
  // Delay showing the uninstall dialog, so that this function returns
  // immediately, to close the bubble properly. See crbug.com/121544.
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::Bind(&extensions::ExtensionUninstallDialog::ConfirmUninstall,
                 uninstall_dialog_->AsWeakPtr(), extension_,
                 extensions::UNINSTALL_REASON_EXTENSION_DISABLED,
                 extensions::UNINSTALL_SOURCE_PERMISSIONS_INCREASE));
}

bool ExtensionDisabledGlobalError::ShouldCloseOnDeactivate() const {
  // Since this indicates that an extension was disabled, we should definitely
  // have the user acknowledge it, rather than having the bubble disappear when
  // a new window pops up.
  return false;
}

void ExtensionDisabledGlobalError::OnExtensionUninstallDialogClosed(
    bool did_start_uninstall,
    const base::string16& error) {
  // No need to do anything.
}

void ExtensionDisabledGlobalError::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  // The error is invalidated if the extension has been loaded or removed.
  DCHECK(type == extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED ||
         type == extensions::NOTIFICATION_EXTENSION_REMOVED);
  const Extension* extension = content::Details<const Extension>(details).ptr();
  if (extension != extension_)
    return;
  GlobalErrorServiceFactory::GetForProfile(service_->profile())->
      RemoveGlobalError(this);

  // Make sure we don't call RemoveGlobalError again.
  registrar_.RemoveAll();

  if (type == extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED)
    user_response_ = REENABLE;
  else if (type == extensions::NOTIFICATION_EXTENSION_REMOVED)
    user_response_ = UNINSTALL;

  // Delete this object after any running tasks, so that the extension dialog
  // still has it as a delegate to finish the current tasks.
  base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
}

// Globals --------------------------------------------------------------------

namespace extensions {

void AddExtensionDisabledErrorWithIcon(base::WeakPtr<ExtensionService> service,
                                       const std::string& extension_id,
                                       bool is_remote_install,
                                       const gfx::Image& icon) {
  if (!service.get())
    return;
  const Extension* extension = service->GetInstalledExtension(extension_id);
  if (extension) {
    GlobalErrorServiceFactory::GetForProfile(service->profile())
        ->AddGlobalError(new ExtensionDisabledGlobalError(
            service.get(), extension, is_remote_install, icon));
  }
}

void AddExtensionDisabledError(ExtensionService* service,
                               const Extension* extension,
                               bool is_remote_install) {
  // Do not display notifications for ephemeral apps that have been disabled.
  // Instead, a prompt will be shown the next time the app is launched.
  if (util::IsEphemeralApp(extension->id(), service->profile()))
    return;

  extensions::ExtensionResource image = extensions::IconsInfo::GetIconResource(
      extension, kIconSize, ExtensionIconSet::MATCH_BIGGER);
  gfx::Size size(kIconSize, kIconSize);
  ImageLoader::Get(service->profile())
      ->LoadImageAsync(extension,
                       image,
                       size,
                       base::Bind(&AddExtensionDisabledErrorWithIcon,
                                  service->AsWeakPtr(),
                                  extension->id(),
                                  is_remote_install));
}

void ShowExtensionDisabledDialog(ExtensionService* service,
                                 content::WebContents* web_contents,
                                 const Extension* extension) {
  scoped_ptr<ExtensionInstallPrompt> install_ui(
      new ExtensionInstallPrompt(web_contents));
  // This object manages its own lifetime.
  new ExtensionDisabledDialogDelegate(service, install_ui.Pass(), extension);
}

}  // namespace extensions