[Extensions] Update external extension warning to allow new platforms

Certain platforms (currently only Windows) prompt for externally-
installed, or sideloaded, extensions. In the future, we may want to
spread this to other platforms, but we'll want to grandfather in any
extensions that were already installed.

Update the external installation disablement and warning code to only
affect new installation of extensions. Add additional unit tests for:
- The general case of installing and disabling an external extension (I
  couldn't find any for this already in place, though some probably test
  it transitively?).
- Extensions installed before the feature is turned on not being
  disabled.
- Not warning about extensions installed before the feature is turned
  on.

BUG=669277

Review-Url: https://codereview.chromium.org/2607673002
Cr-Commit-Position: refs/heads/master@{#441000}
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index cce0d63..f63d065 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -2224,7 +2224,15 @@
 }
 
 int ExtensionService::GetDisableReasonsOnInstalled(const Extension* extension) {
-  Extension::DisableReason disable_reason;
+  bool is_update_from_same_type = false;
+  {
+    const Extension* existing_extension =
+        GetInstalledExtension(extension->id());
+    is_update_from_same_type =
+        existing_extension &&
+        existing_extension->manifest()->type() == extension->manifest()->type();
+  }
+  Extension::DisableReason disable_reason = Extension::DISABLE_NONE;
   // Extensions disabled by management policy should always be disabled, even
   // if it's force-installed.
   if (system_->management_policy()->MustRemainDisabled(
@@ -2253,10 +2261,15 @@
   if (FeatureSwitch::prompt_for_external_extensions()->IsEnabled()) {
     // External extensions are initially disabled. We prompt the user before
     // enabling them. Hosted apps are excepted because they are not dangerous
-    // (they need to be launched by the user anyway).
+    // (they need to be launched by the user anyway). We also don't prompt for
+    // extensions updating; this is because the extension will be disabled from
+    // the initial install if it is supposed to be, and this allows us to turn
+    // this on for other platforms without disabling already-installed
+    // extensions.
     if (extension->GetType() != Manifest::TYPE_HOSTED_APP &&
         Manifest::IsExternalLocation(extension->location()) &&
-        !extension_prefs_->IsExternalExtensionAcknowledged(extension->id())) {
+        !extension_prefs_->IsExternalExtensionAcknowledged(extension->id()) &&
+        !is_update_from_same_type) {
       return Extension::DISABLE_EXTERNAL_EXTENSION;
     }
   }
diff --git a/chrome/browser/extensions/extension_service_unittest.cc b/chrome/browser/extensions/extension_service_unittest.cc
index 663e4c5..0433e03a8 100644
--- a/chrome/browser/extensions/extension_service_unittest.cc
+++ b/chrome/browser/extensions/extension_service_unittest.cc
@@ -594,6 +594,15 @@
     service()->AddProviderForTesting(base::WrapUnique(provider));
   }
 
+  // Checks for external extensions and waits for one to complete installing.
+  void WaitForExternalExtensionInstalled() {
+    content::WindowedNotificationObserver observer(
+        extensions::NOTIFICATION_CRX_INSTALLER_DONE,
+        content::NotificationService::AllSources());
+    service()->CheckForExternalUpdates();
+    observer.Wait();
+  }
+
  protected:
   // Paths to some of the fake extensions.
   base::FilePath good1_path() {
@@ -4199,6 +4208,91 @@
   ASSERT_TRUE(prefs->IsExternalExtensionAcknowledged(page_action));
 }
 
+// Tests that an extension added through an external source is initially
+// disabled with the "prompt for external extensions" feature.
+TEST_F(ExtensionServiceTest, ExternalExtensionDisabledOnInstallation) {
+  FeatureSwitch::ScopedOverride external_prompt_override(
+      FeatureSwitch::prompt_for_external_extensions(), true);
+  InitializeEmptyExtensionService();
+
+  // Register and install an external extension.
+  MockExtensionProvider* provider =
+      new MockExtensionProvider(service(), Manifest::EXTERNAL_PREF);
+  AddMockExternalProvider(provider);  // Takes ownership.
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.0",
+                                 data_dir().AppendASCII("good.crx"));
+
+  WaitForExternalExtensionInstalled();
+
+  EXPECT_TRUE(registry()->disabled_extensions().Contains(good_crx));
+  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
+  EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(good_crx));
+  EXPECT_EQ(Extension::DISABLE_EXTERNAL_EXTENSION,
+            prefs->GetDisableReasons(good_crx));
+
+  // Updating the extension shouldn't cause it to be enabled.
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.1",
+                                 data_dir().AppendASCII("good2.crx"));
+  WaitForExternalExtensionInstalled();
+
+  EXPECT_TRUE(registry()->disabled_extensions().Contains(good_crx));
+  EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(good_crx));
+  EXPECT_EQ(Extension::DISABLE_EXTERNAL_EXTENSION,
+            prefs->GetDisableReasons(good_crx));
+  const Extension* extension =
+      registry()->disabled_extensions().GetByID(good_crx);
+  ASSERT_TRUE(extension);
+  // Double check that we did, in fact, update the extension.
+  EXPECT_EQ("1.0.0.1", extension->version()->GetString());
+}
+
+// Test that if an extension is installed before the "prompt for external
+// extensions" feature is enabled, but is updated when the feature is
+// enabled, the extension is not disabled.
+TEST_F(ExtensionServiceTest, ExternalExtensionIsNotDisabledOnUpdate) {
+  auto external_prompt_override =
+      base::MakeUnique<FeatureSwitch::ScopedOverride>(
+          FeatureSwitch::prompt_for_external_extensions(), false);
+  InitializeEmptyExtensionService();
+
+  // Register and install an external extension.
+  MockExtensionProvider* provider =
+      new MockExtensionProvider(service(), Manifest::EXTERNAL_PREF);
+  AddMockExternalProvider(provider);
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.0",
+                                 data_dir().AppendASCII("good.crx"));
+
+  WaitForExternalExtensionInstalled();
+
+  EXPECT_TRUE(registry()->enabled_extensions().Contains(good_crx));
+  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
+  EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(good_crx));
+  EXPECT_EQ(Extension::DISABLE_NONE, prefs->GetDisableReasons(good_crx));
+
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.1",
+                                 data_dir().AppendASCII("good2.crx"));
+
+  // We explicitly reset the override first. ScopedOverrides reset the value
+  // to the original value on destruction, but if we reset by passing a new
+  // object, the new object is constructed (overriding the current value)
+  // before the old is destructed (which will immediately reset to the
+  // original).
+  external_prompt_override.reset();
+  external_prompt_override = base::MakeUnique<FeatureSwitch::ScopedOverride>(
+      FeatureSwitch::prompt_for_external_extensions(), true);
+  WaitForExternalExtensionInstalled();
+
+  EXPECT_TRUE(registry()->enabled_extensions().Contains(good_crx));
+  {
+    const Extension* extension =
+        registry()->enabled_extensions().GetByID(good_crx);
+    ASSERT_TRUE(extension);
+    EXPECT_EQ("1.0.0.1", extension->version()->GetString());
+  }
+  EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(good_crx));
+  EXPECT_EQ(Extension::DISABLE_NONE, prefs->GetDisableReasons(good_crx));
+}
+
 #if !defined(OS_CHROMEOS)
 // This tests if default apps are installed correctly.
 TEST_F(ExtensionServiceTest, DefaultAppsInstall) {
@@ -6804,6 +6898,55 @@
   EXPECT_FALSE(HasExternalInstallErrors(service_));
 }
 
+// Test that the external install bubble only takes disabled extensions into
+// account - enabled extensions, even those that weren't acknowledged, should
+// not be warned about. This lets us grandfather extensions in.
+TEST_F(ExtensionServiceTest,
+       ExternalInstallBubbleDoesntShowForEnabledExtensions) {
+  auto external_prompt_override =
+      base::MakeUnique<FeatureSwitch::ScopedOverride>(
+          FeatureSwitch::prompt_for_external_extensions(), false);
+  InitializeEmptyExtensionService();
+
+  // Register and install an external extension.
+  MockExtensionProvider* provider =
+      new MockExtensionProvider(service(), Manifest::EXTERNAL_PREF);
+  AddMockExternalProvider(provider);
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.0",
+                                 data_dir().AppendASCII("good.crx"));
+
+  WaitForExternalExtensionInstalled();
+
+  EXPECT_TRUE(registry()->enabled_extensions().Contains(good_crx));
+  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
+  EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(good_crx));
+  EXPECT_EQ(Extension::DISABLE_NONE, prefs->GetDisableReasons(good_crx));
+
+  // We explicitly reset the override first. ScopedOverrides reset the value
+  // to the original value on destruction, but if we reset by passing a new
+  // object, the new object is constructed (overriding the current value)
+  // before the old is destructed (which will immediately reset to the
+  // original).
+  external_prompt_override.reset();
+  external_prompt_override = base::MakeUnique<FeatureSwitch::ScopedOverride>(
+      FeatureSwitch::prompt_for_external_extensions(), true);
+
+  extensions::ExternalInstallManager* external_manager =
+      service()->external_install_manager();
+  external_manager->UpdateExternalExtensionAlert();
+  EXPECT_FALSE(external_manager->has_currently_visible_install_alert());
+  EXPECT_TRUE(external_manager->GetErrorsForTesting().empty());
+
+  provider->UpdateOrAddExtension(good_crx, "1.0.0.1",
+                                 data_dir().AppendASCII("good2.crx"));
+
+  WaitForExternalExtensionInstalled();
+
+  external_manager->UpdateExternalExtensionAlert();
+  EXPECT_FALSE(external_manager->has_currently_visible_install_alert());
+  EXPECT_TRUE(external_manager->GetErrorsForTesting().empty());
+}
+
 TEST_F(ExtensionServiceTest, InstallBlacklistedExtension) {
   InitializeEmptyExtensionService();
 
diff --git a/chrome/browser/extensions/external_install_manager.cc b/chrome/browser/extensions/external_install_manager.cc
index 774247c..802627a 100644
--- a/chrome/browser/extensions/external_install_manager.cc
+++ b/chrome/browser/extensions/external_install_manager.cc
@@ -70,6 +70,15 @@
       this,
       extensions::NOTIFICATION_EXTENSION_REMOVED,
       content::Source<Profile>(Profile::FromBrowserContext(browser_context_)));
+  // Populate the set of unacknowledged external extensions now. We can't just
+  // rely on IsUnacknowledgedExternalExtension() for cases like
+  // OnExtensionLoaded(), since we need to examine the disable reasons, which
+  // can be removed throughout the session.
+  for (const auto& extension :
+       ExtensionRegistry::Get(browser_context)->disabled_extensions()) {
+    if (IsUnacknowledgedExternalExtension(*extension))
+      unacknowledged_ids_.insert(extension->id());
+  }
 }
 
 ExternalInstallManager::~ExternalInstallManager() {
@@ -95,12 +104,12 @@
 
 void ExternalInstallManager::RemoveExternalInstallError(
     const std::string& extension_id) {
-  std::map<std::string, std::unique_ptr<ExternalInstallError>>::iterator iter =
-      errors_.find(extension_id);
+  auto iter = errors_.find(extension_id);
   if (iter != errors_.end()) {
     if (iter->second.get() == currently_visible_install_alert_)
       currently_visible_install_alert_ = nullptr;
     errors_.erase(iter);
+    unacknowledged_ids_.erase(extension_id);
     UpdateExternalExtensionAlert();
   }
 }
@@ -114,31 +123,29 @@
   // external extensions.
   const ExtensionSet& disabled_extensions =
       ExtensionRegistry::Get(browser_context_)->disabled_extensions();
-  for (const scoped_refptr<const Extension>& extension : disabled_extensions) {
-    if (base::ContainsKey(errors_, extension->id()) ||
-        shown_ids_.count(extension->id()) > 0)
+  for (const auto& id : unacknowledged_ids_) {
+    if (base::ContainsKey(errors_, id) || shown_ids_.count(id) > 0)
       continue;
 
-    if (!IsUnacknowledgedExternalExtension(extension.get()))
-      continue;
+    const Extension* extension = disabled_extensions.GetByID(id);
+    CHECK(extension);
 
     // Warn the user about the suspicious extension.
-    if (extension_prefs_->IncrementAcknowledgePromptCount(extension->id()) >
+    if (extension_prefs_->IncrementAcknowledgePromptCount(id) >
         kMaxExtensionAcknowledgePromptCount) {
       // Stop prompting for this extension and record metrics.
-      extension_prefs_->AcknowledgeExternalExtension(extension->id());
-      LogExternalExtensionEvent(extension.get(), EXTERNAL_EXTENSION_IGNORED);
+      extension_prefs_->AcknowledgeExternalExtension(id);
+      LogExternalExtensionEvent(extension, EXTERNAL_EXTENSION_IGNORED);
       continue;
     }
 
     if (is_first_run_)
-      extension_prefs_->SetExternalInstallFirstRun(extension->id());
+      extension_prefs_->SetExternalInstallFirstRun(id);
 
     // |first_run| is true if the extension was installed during a first run
     // (even if it's post-first run now).
-    AddExternalInstallError(
-        extension.get(),
-        extension_prefs_->IsExternalInstallFirstRun(extension->id()));
+    AddExternalInstallError(extension,
+                            extension_prefs_->IsExternalInstallFirstRun(id));
   }
 }
 
@@ -170,7 +177,7 @@
 void ExternalInstallManager::OnExtensionLoaded(
     content::BrowserContext* browser_context,
     const Extension* extension) {
-  if (!IsUnacknowledgedExternalExtension(extension))
+  if (!unacknowledged_ids_.count(extension->id()))
     return;
 
   // We treat loading as acknowledgement (since the user consciously chose to
@@ -194,11 +201,11 @@
     return;
   }
 
-  if (!IsUnacknowledgedExternalExtension(extension))
+  if (!IsUnacknowledgedExternalExtension(*extension))
     return;
 
+  unacknowledged_ids_.insert(extension->id());
   LogExternalExtensionEvent(extension, EXTERNAL_EXTENSION_INSTALLED);
-
   UpdateExternalExtensionAlert();
 }
 
@@ -206,19 +213,26 @@
     content::BrowserContext* browser_context,
     const Extension* extension,
     extensions::UninstallReason reason) {
-  if (IsUnacknowledgedExternalExtension(extension))
+  if (unacknowledged_ids_.erase(extension->id()))
     LogExternalExtensionEvent(extension, EXTERNAL_EXTENSION_UNINSTALLED);
 }
 
 bool ExternalInstallManager::IsUnacknowledgedExternalExtension(
-    const Extension* extension) const {
+    const Extension& extension) const {
   if (!FeatureSwitch::prompt_for_external_extensions()->IsEnabled())
     return false;
 
-  return (Manifest::IsExternalLocation(extension->location()) &&
-          !extension_prefs_->IsExternalExtensionAcknowledged(extension->id()) &&
-          !(extension_prefs_->GetDisableReasons(extension->id()) &
-            Extension::DISABLE_SIDELOAD_WIPEOUT));
+  int disable_reasons = extension_prefs_->GetDisableReasons(extension.id());
+  bool is_from_sideload_wipeout =
+      (disable_reasons & Extension::DISABLE_SIDELOAD_WIPEOUT) != 0;
+  // We don't consider extensions that weren't disabled for being external so
+  // that we grandfather in extensions. External extensions are only disabled on
+  // install with the "prompt for external extensions" feature enabled.
+  bool is_disabled_external =
+      (disable_reasons & Extension::DISABLE_EXTERNAL_EXTENSION) != 0;
+  return is_disabled_external && !is_from_sideload_wipeout &&
+         Manifest::IsExternalLocation(extension.location()) &&
+         !extension_prefs_->IsExternalExtensionAcknowledged(extension.id());
 }
 
 void ExternalInstallManager::Observe(
diff --git a/chrome/browser/extensions/external_install_manager.h b/chrome/browser/extensions/external_install_manager.h
index 51f3a14..99c60cb4 100644
--- a/chrome/browser/extensions/external_install_manager.h
+++ b/chrome/browser/extensions/external_install_manager.h
@@ -13,6 +13,7 @@
 #include "content/public/browser/notification_observer.h"
 #include "content/public/browser/notification_registrar.h"
 #include "extensions/browser/extension_registry_observer.h"
+#include "extensions/common/extension_id.h"
 
 namespace content {
 class BrowserContext;
@@ -83,7 +84,7 @@
 
   // Returns true if this extension is an external one that has yet to be
   // marked as acknowledged.
-  bool IsUnacknowledgedExternalExtension(const Extension* extension) const;
+  bool IsUnacknowledgedExternalExtension(const Extension& extension) const;
 
   // The associated BrowserContext.
   content::BrowserContext* browser_context_;
@@ -97,7 +98,13 @@
   // The collection of ExternalInstallErrors.
   std::map<std::string, std::unique_ptr<ExternalInstallError>> errors_;
 
-  std::set<std::string> shown_ids_;
+  // The set of ids of unacknowledged external extensions. Populated at
+  // initialization, and then updated as extensions are added, removed,
+  // acknowledged, etc.
+  std::set<ExtensionId> unacknowledged_ids_;
+
+  // The set of ids of extensions that we have warned about in this session.
+  std::set<ExtensionId> shown_ids_;
 
   // The error that is currently showing an alert dialog/bubble.
   ExternalInstallError* currently_visible_install_alert_;