// 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/bundle_installer.h"

#include <algorithm>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/i18n/rtl.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/permissions_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "extensions/common/extension.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_skia.h"

namespace extensions {

namespace {

enum AutoApproveForTest {
  DO_NOT_SKIP = 0,
  PROCEED,
  ABORT
};

AutoApproveForTest g_auto_approve_for_test = DO_NOT_SKIP;

scoped_refptr<Extension> CreateDummyExtension(
    const BundleInstaller::Item& item,
    const base::DictionaryValue& manifest,
    content::BrowserContext* browser_context) {
  // We require localized names so we can have nice error messages when we can't
  // parse an extension manifest.
  CHECK(!item.localized_name.empty());

  std::string error;
  scoped_refptr<Extension> extension = Extension::Create(base::FilePath(),
                                                         Manifest::INTERNAL,
                                                         manifest,
                                                         Extension::NO_FLAGS,
                                                         item.id,
                                                         &error);
  // Initialize permissions so that withheld permissions are displayed properly
  // in the install prompt.
  PermissionsUpdater(browser_context, PermissionsUpdater::INIT_FLAG_TRANSIENT)
      .InitializePermissions(extension.get());
  return extension;
}

const int kHeadingIdsInstallPrompt[] = {
  IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_EXTENSIONS,
  IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_APPS,
  IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_EXTENSION_APPS
};

const int kHeadingIdsDelegatedInstallPrompt[] = {
  IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_EXTENSIONS,
  IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_APPS,
  IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_EXTENSION_APPS
};

const int kHeadingIdsInstalledBubble[] = {
  IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSIONS,
  IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_APPS,
  IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSION_APPS
};

}  // namespace

// static
void BundleInstaller::SetAutoApproveForTesting(bool auto_approve) {
  CHECK(base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType));
  g_auto_approve_for_test = auto_approve ? PROCEED : ABORT;
}

BundleInstaller::Item::Item() : state(STATE_PENDING) {}

BundleInstaller::Item::Item(const Item& other) = default;

BundleInstaller::Item::~Item() {}

base::string16 BundleInstaller::Item::GetNameForDisplay() const {
  base::string16 name = base::UTF8ToUTF16(localized_name);
  base::i18n::AdjustStringForLocaleDirection(&name);
  return name;
}

BundleInstaller::BundleInstaller(Browser* browser,
                                 const std::string& name,
                                 const SkBitmap& icon,
                                 const std::string& authuser,
                                 const std::string& delegated_username,
                                 const BundleInstaller::ItemList& items)
    : approved_(false),
      browser_(browser),
      name_(name),
      icon_(icon),
      authuser_(authuser),
      delegated_username_(delegated_username),
      profile_(browser->profile()),
      weak_factory_(this) {
  BrowserList::AddObserver(this);
  for (size_t i = 0; i < items.size(); ++i) {
    items_[items[i].id] = items[i];
    items_[items[i].id].state = Item::STATE_PENDING;
  }
}

BundleInstaller::~BundleInstaller() {
  BrowserList::RemoveObserver(this);
}

BundleInstaller::ItemList BundleInstaller::GetItemsWithState(
    Item::State state) const {
  ItemList list;

  for (const std::pair<std::string, Item>& entry : items_) {
    if (entry.second.state == state)
      list.push_back(entry.second);
  }

  return list;
}

bool BundleInstaller::HasItemWithState(Item::State state) const {
  return CountItemsWithState(state) > 0;
}

size_t BundleInstaller::CountItemsWithState(Item::State state) const {
  return std::count_if(items_.begin(), items_.end(),
                       [state] (const std::pair<std::string, Item>& entry) {
                         return entry.second.state == state;
                       });
}

void BundleInstaller::PromptForApproval(const ApprovalCallback& callback) {
  approval_callback_ = callback;

  ParseManifests();
}

void BundleInstaller::CompleteInstall(content::WebContents* web_contents,
                                      const base::Closure& callback) {
  DCHECK(web_contents);
  CHECK(approved_);

  install_callback_ = callback;

  if (!HasItemWithState(Item::STATE_PENDING)) {
    install_callback_.Run();
    return;
  }

  // Start each WebstoreInstaller.
  for (const std::pair<std::string, Item>& entry : items_) {
    if (entry.second.state != Item::STATE_PENDING)
      continue;

    // Since we've already confirmed the permissions, create an approval that
    // lets CrxInstaller bypass the prompt.
    scoped_ptr<WebstoreInstaller::Approval> approval(
        WebstoreInstaller::Approval::CreateWithNoInstallPrompt(
            profile_,
            entry.first,
            make_scoped_ptr(parsed_manifests_[entry.first]->DeepCopy()),
            true));
    approval->use_app_installed_bubble = false;
    approval->skip_post_install_ui = true;
    approval->authuser = authuser_;
    approval->installing_icon =
        gfx::ImageSkia::CreateFrom1xBitmap(entry.second.icon);

    scoped_refptr<WebstoreInstaller> installer = new WebstoreInstaller(
        profile_, this, web_contents, entry.first, std::move(approval),
        WebstoreInstaller::INSTALL_SOURCE_OTHER);
    installer->Start();
  }
}

base::string16 BundleInstaller::GetHeadingTextFor(Item::State state) const {
  // For STATE_FAILED, we can't tell if the items were apps or extensions
  // so we always show the same message.
  if (state == Item::STATE_FAILED) {
    if (HasItemWithState(state))
      return l10n_util::GetStringUTF16(IDS_EXTENSION_BUNDLE_ERROR_HEADING);
    return base::string16();
  }

  size_t total = CountItemsWithState(state);
  if (total == 0)
    return base::string16();

  size_t apps = std::count_if(
      dummy_extensions_.begin(), dummy_extensions_.end(),
      [] (const scoped_refptr<const Extension>& ext) { return ext->is_app(); });

  bool has_apps = apps > 0;
  bool has_extensions = apps < total;
  size_t index = (has_extensions << 0) + (has_apps << 1) - 1;

  if (state == Item::STATE_PENDING) {
    if (!delegated_username_.empty()) {
      return l10n_util::GetStringFUTF16(
          kHeadingIdsDelegatedInstallPrompt[index], base::UTF8ToUTF16(name_),
          base::UTF8ToUTF16(delegated_username_));
    } else {
      return l10n_util::GetStringFUTF16(kHeadingIdsInstallPrompt[index],
                                        base::UTF8ToUTF16(name_));
    }
  } else {
    return l10n_util::GetStringUTF16(kHeadingIdsInstalledBubble[index]);
  }
}

void BundleInstaller::ParseManifests() {
  if (items_.empty()) {
    approval_callback_.Run(APPROVAL_ERROR);
    return;
  }

  net::URLRequestContextGetter* context_getter =
      browser_ ? browser_->profile()->GetRequestContext() : nullptr;

  for (const std::pair<std::string, Item>& entry : items_) {
    scoped_refptr<WebstoreInstallHelper> helper = new WebstoreInstallHelper(
        this, entry.first, entry.second.manifest, entry.second.icon_url,
        context_getter);
    helper->Start();
  }
}

void BundleInstaller::ShowPromptIfDoneParsing() {
  // We don't prompt until all the manifests have been parsed.
  if (CountItemsWithState(Item::STATE_PENDING) != dummy_extensions_.size())
    return;

  ShowPrompt();
}

void BundleInstaller::ShowPrompt() {
  // Abort if we couldn't create any Extensions out of the manifests.
  if (dummy_extensions_.empty()) {
    approval_callback_.Run(APPROVAL_ERROR);
    return;
  }

  scoped_ptr<const PermissionSet> permissions;
  PermissionSet empty;
  for (size_t i = 0; i < dummy_extensions_.size(); ++i) {
    // Using "permissions ? *permissions : PermissionSet()" tries to do a copy,
    // and doesn't compile. Use a more verbose, but compilable, workaround.
    permissions = PermissionSet::CreateUnion(
        permissions ? *permissions : empty,
        dummy_extensions_[i]->permissions_data()->active_permissions());
  }

  if (g_auto_approve_for_test == PROCEED) {
    OnInstallPromptDone(ExtensionInstallPrompt::Result::ACCEPTED);
  } else if (g_auto_approve_for_test == ABORT) {
    OnInstallPromptDone(ExtensionInstallPrompt::Result::USER_CANCELED);
  } else {
    Browser* browser = browser_;
    if (!browser) {
      // The browser that we got initially could have gone away during our
      // thread hopping.
      browser = chrome::FindLastActiveWithProfile(profile_);
    }
    content::WebContents* web_contents = NULL;
    if (browser)
      web_contents = browser->tab_strip_model()->GetActiveWebContents();
    install_ui_.reset(new ExtensionInstallPrompt(web_contents));
    scoped_ptr<ExtensionInstallPrompt::Prompt> prompt;
    if (delegated_username_.empty()) {
      prompt.reset(new ExtensionInstallPrompt::Prompt(
          ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT));
    } else {
      prompt.reset(new ExtensionInstallPrompt::Prompt(
          ExtensionInstallPrompt::DELEGATED_BUNDLE_PERMISSIONS_PROMPT));
      prompt->set_delegated_username(delegated_username_);
    }
    prompt->set_bundle(this);
    install_ui_->ShowDialog(
        base::Bind(&BundleInstaller::OnInstallPromptDone,
                   weak_factory_.GetWeakPtr()),
        nullptr, &icon_, std::move(prompt), std::move(permissions),
        ExtensionInstallPrompt::GetDefaultShowDialogCallback());
  }
}

void BundleInstaller::ShowInstalledBubbleIfDone() {
  // We're ready to show the installed bubble when no items are pending.
  if (HasItemWithState(Item::STATE_PENDING))
    return;

  if (browser_)
    ShowInstalledBubble(this, browser_);

  install_callback_.Run();
}

void BundleInstaller::OnWebstoreParseSuccess(
    const std::string& id,
    const SkBitmap& icon,
    base::DictionaryValue* manifest) {
  items_[id].icon = icon;
  dummy_extensions_.push_back(
      CreateDummyExtension(items_[id], *manifest, profile_));
  parsed_manifests_[id] = linked_ptr<base::DictionaryValue>(manifest);

  ShowPromptIfDoneParsing();
}

void BundleInstaller::OnWebstoreParseFailure(
    const std::string& id,
    WebstoreInstallHelper::Delegate::InstallHelperResultCode result_code,
    const std::string& error_message) {
  items_[id].state = Item::STATE_FAILED;

  ShowPromptIfDoneParsing();
}

void BundleInstaller::OnInstallPromptDone(
    ExtensionInstallPrompt::Result result) {
  ApprovalState state = APPROVED;
  if (result == ExtensionInstallPrompt::Result::ACCEPTED) {
    approved_ = true;
    state = APPROVED;
  } else {
    for (std::pair<const std::string, Item>& entry : items_)
      entry.second.state = Item::STATE_FAILED;

    state = result == ExtensionInstallPrompt::Result::USER_CANCELED
                ? USER_CANCELED
                : APPROVAL_ERROR;
  }
  approval_callback_.Run(state);
}

void BundleInstaller::OnExtensionInstallSuccess(const std::string& id) {
  items_[id].state = Item::STATE_INSTALLED;

  ShowInstalledBubbleIfDone();
}

void BundleInstaller::OnExtensionInstallFailure(
    const std::string& id,
    const std::string& error,
    WebstoreInstaller::FailureReason reason) {
  items_[id].state = Item::STATE_FAILED;

  ExtensionList::iterator i = std::find_if(
      dummy_extensions_.begin(), dummy_extensions_.end(),
      [&id] (const scoped_refptr<const Extension>& ext) {
        return ext->id() == id;
      });
  CHECK(dummy_extensions_.end() != i);
  dummy_extensions_.erase(i);

  ShowInstalledBubbleIfDone();
}

void BundleInstaller::OnBrowserRemoved(Browser* browser) {
  if (browser_ == browser)
    browser_ = nullptr;
}

}  // namespace extensions