// Copyright (c) 2011 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/crx_installer.h"

#include <map>
#include <set>

#include "base/file_util.h"
#include "base/lazy_instance.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/metrics/histogram.h"
#include "base/path_service.h"
#include "base/stl_util-inl.h"
#include "base/stringprintf.h"
#include "base/task.h"
#include "base/threading/thread_restrictions.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/convert_user_script.h"
#include "chrome/browser/extensions/convert_web_app.h"
#include "chrome/browser/extensions/extension_error_reporter.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/extension_file_util.h"
#include "content/browser/browser_thread.h"
#include "content/common/notification_service.h"
#include "content/common/notification_type.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"

namespace {

struct WhitelistedInstallData {
  WhitelistedInstallData() {}
  std::set<std::string> ids;
  std::map<std::string, linked_ptr<DictionaryValue> > manifests;
};

static base::LazyInstance<WhitelistedInstallData>
    g_whitelisted_install_data(base::LINKER_INITIALIZED);

}  // namespace

// static
void CrxInstaller::SetWhitelistedInstallId(const std::string& id) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  g_whitelisted_install_data.Get().ids.insert(id);
}

// static
void CrxInstaller::SetWhitelistedManifest(const std::string& id,
                                          DictionaryValue* parsed_manifest) {
  CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  WhitelistedInstallData& data = g_whitelisted_install_data.Get();
  data.manifests[id] = linked_ptr<DictionaryValue>(parsed_manifest);
}

// static
const DictionaryValue* CrxInstaller::GetWhitelistedManifest(
    const std::string& id) {
  WhitelistedInstallData& data = g_whitelisted_install_data.Get();
  if (ContainsKey(data.manifests, id))
    return data.manifests[id].get();
  else
    return NULL;
}

// static
DictionaryValue* CrxInstaller::RemoveWhitelistedManifest(
    const std::string& id) {
  WhitelistedInstallData& data = g_whitelisted_install_data.Get();
  if (ContainsKey(data.manifests, id)) {
    DictionaryValue* manifest = data.manifests[id].release();
    data.manifests.erase(id);
    return manifest;
  }
  return NULL;
}

// static
bool CrxInstaller::IsIdWhitelisted(const std::string& id) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  std::set<std::string>& ids = g_whitelisted_install_data.Get().ids;
  return ContainsKey(ids, id);
}

// static
bool CrxInstaller::ClearWhitelistedInstallId(const std::string& id) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  std::set<std::string>& ids = g_whitelisted_install_data.Get().ids;
  if (ContainsKey(ids, id)) {
    ids.erase(id);
    return true;
  }
  return false;
}

CrxInstaller::CrxInstaller(ExtensionService* frontend,
                           ExtensionInstallUI* client)
    : install_directory_(frontend->install_directory()),
      install_source_(Extension::INTERNAL),
      extensions_enabled_(frontend->extensions_enabled()),
      delete_source_(false),
      is_gallery_install_(false),
      create_app_shortcut_(false),
      frontend_(frontend),
      client_(client),
      apps_require_extension_mime_type_(false),
      allow_silent_install_(false) {
}

CrxInstaller::~CrxInstaller() {
  // Delete the temp directory and crx file as necessary. Note that the
  // destructor might be called on any thread, so we post a task to the file
  // thread to make sure the delete happens there.
  if (!temp_dir_.value().empty()) {
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableFunction(
            &extension_file_util::DeleteFile, temp_dir_, true));
  }

  if (delete_source_) {
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableFunction(
            &extension_file_util::DeleteFile, source_file_, false));
  }

  // Make sure the UI is deleted on the ui thread.
  BrowserThread::DeleteSoon(BrowserThread::UI, FROM_HERE, client_);
  client_ = NULL;
}

void CrxInstaller::InstallCrx(const FilePath& source_file) {
  source_file_ = source_file;

  scoped_refptr<SandboxedExtensionUnpacker> unpacker(
      new SandboxedExtensionUnpacker(
          source_file,
          g_browser_process->resource_dispatcher_host(),
          this));

  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableMethod(
          unpacker.get(), &SandboxedExtensionUnpacker::Start));
}

void CrxInstaller::InstallUserScript(const FilePath& source_file,
                                     const GURL& original_url) {
  DCHECK(!original_url.is_empty());

  source_file_ = source_file;
  original_url_ = original_url;

  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableMethod(this, &CrxInstaller::ConvertUserScriptOnFileThread));
}

void CrxInstaller::ConvertUserScriptOnFileThread() {
  std::string error;
  scoped_refptr<Extension> extension =
      ConvertUserScriptToExtension(source_file_, original_url_, &error);
  if (!extension) {
    ReportFailureFromFileThread(error);
    return;
  }

  OnUnpackSuccess(extension->path(), extension->path(), extension);
}

void CrxInstaller::InstallWebApp(const WebApplicationInfo& web_app) {
  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableMethod(this, &CrxInstaller::ConvertWebAppOnFileThread,
                        web_app));
}

void CrxInstaller::ConvertWebAppOnFileThread(
    const WebApplicationInfo& web_app) {
  std::string error;
  scoped_refptr<Extension> extension(
      ConvertWebAppToExtension(web_app, base::Time::Now()));
  if (!extension) {
    // Validation should have stopped any potential errors before getting here.
    NOTREACHED() << "Could not convert web app to extension.";
    return;
  }

  // TODO(aa): conversion data gets lost here :(

  OnUnpackSuccess(extension->path(), extension->path(), extension);
}

bool CrxInstaller::AllowInstall(const Extension* extension,
                                std::string* error) {
  DCHECK(error);

  // Make sure the expected id matches.
  if (!expected_id_.empty() && expected_id_ != extension->id()) {
    *error = base::StringPrintf(
        "ID in new CRX manifest (%s) does not match expected id (%s)",
        extension->id().c_str(),
        expected_id_.c_str());
    return false;
  }

  if (expected_version_.get() &&
      !expected_version_->Equals(*extension->version())) {
    *error = base::StringPrintf(
        "Version in new CRX %s manifest (%s) does not match expected "
        "version (%s)",
        extension->id().c_str(),
        expected_version_->GetString().c_str(),
        extension->version()->GetString().c_str());
    return false;
  }

  // The checks below are skipped for themes and external installs.
  if (extension->is_theme() || Extension::IsExternalLocation(install_source_))
    return true;

  if (!extensions_enabled_) {
    *error = "Extensions are not enabled.";
    return false;
  }

  if (extension_->is_app()) {
    // If the app was downloaded, apps_require_extension_mime_type_
    // will be set.  In this case, check that it was served with the
    // right mime type.  Make an exception for file URLs, which come
    // from the users computer and have no headers.
    if (!original_url_.SchemeIsFile() &&
        apps_require_extension_mime_type_ &&
        original_mime_type_ != Extension::kMimeType) {
      *error = base::StringPrintf(
          "Apps must be served with content type %s.",
          Extension::kMimeType);
      return false;
    }

    // If the client_ is NULL, then the app is either being installed via
    // an internal mechanism like sync, external_extensions, or default apps.
    // In that case, we don't want to enforce things like the install origin.
    if (!is_gallery_install_ && client_) {
      // For apps with a gallery update URL, require that they be installed
      // from the gallery.
      // TODO(erikkay) Apply this rule for paid extensions and themes as well.
      if (extension->UpdatesFromGallery()) {
        *error = l10n_util::GetStringFUTF8(
            IDS_EXTENSION_DISALLOW_NON_DOWNLOADED_GALLERY_INSTALLS,
            l10n_util::GetStringUTF16(IDS_EXTENSION_WEB_STORE_TITLE));
        return false;
      }

      // For self-hosted apps, verify that the entire extent is on the same
      // host (or a subdomain of the host) the download happened from.  There's
      // no way for us to verify that the app controls any other hosts.
      URLPattern pattern(UserScript::kValidUserScriptSchemes);
      pattern.set_host(original_url_.host());
      pattern.set_match_subdomains(true);

      ExtensionExtent::PatternList patterns =
          extension_->web_extent().patterns();
      for (size_t i = 0; i < patterns.size(); ++i) {
        if (!pattern.MatchesHost(patterns[i].host())) {
          *error = base::StringPrintf(
              "Apps must be served from the host that they affect.");
          return false;
        }
      }
    }
  }

  return true;
}

void CrxInstaller::OnUnpackFailure(const std::string& error_message) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  ReportFailureFromFileThread(error_message);
}

void CrxInstaller::OnUnpackSuccess(const FilePath& temp_dir,
                                   const FilePath& extension_dir,
                                   const Extension* extension) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));

  // Note: We take ownership of |extension| and |temp_dir|.
  extension_ = extension;
  temp_dir_ = temp_dir;

  // We don't have to delete the unpack dir explicity since it is a child of
  // the temp dir.
  unpacked_extension_root_ = extension_dir;

  std::string error;
  if (!AllowInstall(extension, &error)) {
    ReportFailureFromFileThread(error);
    return;
  }

  if (client_) {
    Extension::DecodeIcon(extension_.get(), Extension::EXTENSION_ICON_LARGE,
                          &install_icon_);
  }

  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &CrxInstaller::ConfirmInstall));
}

// Helper method to let us compare a whitelisted manifest with the actual
// downloaded extension's manifest, but ignoring the kPublicKey since the
// whitelisted manifest doesn't have that value.
static bool EqualsIgnoringPublicKey(
    const DictionaryValue& extension_manifest,
    const DictionaryValue& whitelisted_manifest) {
  scoped_ptr<DictionaryValue> manifest_copy(extension_manifest.DeepCopy());
  manifest_copy->Remove(extension_manifest_keys::kPublicKey, NULL);
  return manifest_copy->Equals(&whitelisted_manifest);
}

void CrxInstaller::ConfirmInstall() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (frontend_->extension_prefs()->IsExtensionBlacklisted(extension_->id())) {
    VLOG(1) << "This extension: " << extension_->id()
            << " is blacklisted. Install failed.";
    ReportFailureFromUIThread("This extension is blacklisted.");
    return;
  }

  if (!frontend_->extension_prefs()->IsExtensionAllowedByPolicy(
      extension_->id())) {
    ReportFailureFromUIThread("This extension is blacklisted by admin policy.");
    return;
  }

  GURL overlapping_url;
  const Extension* overlapping_extension =
      frontend_->GetExtensionByOverlappingWebExtent(extension_->web_extent());
  if (overlapping_extension &&
      overlapping_extension->id() != extension_->id()) {
    ReportFailureFromUIThread(l10n_util::GetStringFUTF8(
        IDS_EXTENSION_OVERLAPPING_WEB_EXTENT,
        UTF8ToUTF16(overlapping_extension->name())));
    return;
  }

  current_version_ =
      frontend_->extension_prefs()->GetVersionString(extension_->id());

  // First see if it's whitelisted by id (the old mechanism).
  bool whitelisted = ClearWhitelistedInstallId(extension_->id()) &&
      extension_->plugins().empty() && is_gallery_install_;

  // Now check if it's whitelisted by manifest.
  scoped_ptr<DictionaryValue> whitelisted_manifest(
      RemoveWhitelistedManifest(extension_->id()));
  if (is_gallery_install_ && whitelisted_manifest.get()) {
    if (!EqualsIgnoringPublicKey(*extension_->manifest_value(),
                                 *whitelisted_manifest)) {
      ReportFailureFromUIThread(
          l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_INVALID));
      return;
    }
    whitelisted = true;
  }

  if (client_ &&
      (!allow_silent_install_ || !whitelisted)) {
    AddRef();  // Balanced in Proceed() and Abort().
    client_->ConfirmInstall(this, extension_.get());
  } else {
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableMethod(this, &CrxInstaller::CompleteInstall));
  }
  return;
}

void CrxInstaller::InstallUIProceed() {
  BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableMethod(this, &CrxInstaller::CompleteInstall));

  Release();  // balanced in ConfirmInstall().
}

void CrxInstaller::InstallUIAbort() {
  // Technically, this can be called for other reasons than the user hitting
  // cancel, but they're rare.
  ExtensionService::RecordPermissionMessagesHistogram(
      extension_, "Extensions.Permissions_InstallCancel");

  // Kill the theme loading bubble.
  NotificationService* service = NotificationService::current();
  service->Notify(NotificationType::NO_THEME_DETECTED,
                  Source<CrxInstaller>(this),
                  NotificationService::NoDetails());
  Release();  // balanced in ConfirmInstall().

  // We're done. Since we don't post any more tasks to ourself, our ref count
  // should go to zero and we die. The destructor will clean up the temp dir.
}

void CrxInstaller::CompleteInstall() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));

  if (!current_version_.empty()) {
    scoped_ptr<Version> current_version(
        Version::GetVersionFromString(current_version_));
    if (current_version->CompareTo(*(extension_->version())) > 0) {
      ReportFailureFromFileThread("Attempted to downgrade extension.");
      return;
    }
  }

  // See how long extension install paths are.  This is important on
  // windows, because file operations may fail if the path to a file
  // exceeds a small constant.  See crbug.com/69693 .
  UMA_HISTOGRAM_CUSTOM_COUNTS(
    "Extensions.CrxInstallDirPathLength",
        install_directory_.value().length(), 0, 500, 100);

  FilePath version_dir = extension_file_util::InstallExtension(
      unpacked_extension_root_,
      extension_->id(),
      extension_->VersionString(),
      install_directory_);
  if (version_dir.empty()) {
    ReportFailureFromFileThread(
        l10n_util::GetStringUTF8(
            IDS_EXTENSION_MOVE_DIRECTORY_TO_PROFILE_FAILED));
    return;
  }

  // This is lame, but we must reload the extension because absolute paths
  // inside the content scripts are established inside InitFromValue() and we
  // just moved the extension.
  // TODO(aa): All paths to resources inside extensions should be created
  // lazily and based on the Extension's root path at that moment.
  std::string error;
  extension_ = extension_file_util::LoadExtension(
      version_dir,
      install_source_,
      Extension::REQUIRE_KEY,
      &error);
  CHECK(error.empty()) << error;

  ReportSuccessFromFileThread();
}

void CrxInstaller::ReportFailureFromFileThread(const std::string& error) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &CrxInstaller::ReportFailureFromUIThread, error));
}

void CrxInstaller::ReportFailureFromUIThread(const std::string& error) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  NotificationService* service = NotificationService::current();
  service->Notify(NotificationType::EXTENSION_INSTALL_ERROR,
                  Source<CrxInstaller>(this),
                  Details<const std::string>(&error));

  // This isn't really necessary, it is only used because unit tests expect to
  // see errors get reported via this interface.
  //
  // TODO(aa): Need to go through unit tests and clean them up too, probably get
  // rid of this line.
  ExtensionErrorReporter::GetInstance()->ReportError(error, false);  // quiet

  if (client_)
    client_->OnInstallFailure(error);
}

void CrxInstaller::ReportSuccessFromFileThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &CrxInstaller::ReportSuccessFromUIThread));
}

void CrxInstaller::ReportSuccessFromUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  // If there is a client, tell the client about installation.
  if (client_)
    client_->OnInstallSuccess(extension_.get(), install_icon_.get());

  // Tell the frontend about the installation and hand off ownership of
  // extension_ to it.
  frontend_->OnExtensionInstalled(extension_);
  extension_ = NULL;

  // We're done. We don't post any more tasks to ourselves so we are deleted
  // soon.
}