Add new disable reason BLOCKED_MATURE for mature extensions

Child users should not be able to install extensions and themes with
mature content from Chrome Web Store (CWS). Add a new disable reason
BLOCKED_MATURE to block any extensions or themes marked mature in
CWS for child users.

Bug: 1037965,1040614
Change-Id: I37cec6efb51e0e88c1714e57be0ef279b1b32ed9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2017646
Commit-Queue: Toby Huang <[email protected]>
Reviewed-by: Aga Wronska <[email protected]>
Reviewed-by: Devlin <[email protected]>
Reviewed-by: Brian White <[email protected]>
Cr-Commit-Position: refs/heads/master@{#738456}
diff --git a/chrome/browser/extensions/api/developer_private/extension_info_generator.cc b/chrome/browser/extensions/api/developer_private/extension_info_generator.cc
index ae5cfba..11988e0 100644
--- a/chrome/browser/extensions/api/developer_private/extension_info_generator.cc
+++ b/chrome/browser/extensions/api/developer_private/extension_info_generator.cc
@@ -560,6 +560,8 @@
   info->disable_reasons.custodian_approval_required =
       (disable_reasons & disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED) !=
       0;
+  info->disable_reasons.blocked_mature =
+      (disable_reasons & disable_reason::DISABLE_BLOCKED_MATURE) != 0;
 
   // Error collection.
   bool error_console_enabled =
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index 9131c06..e99ce68 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -780,6 +780,8 @@
 void ExtensionService::DisableExtension(const std::string& extension_id,
                                         int disable_reasons) {
   CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+  DCHECK(disable_reasons != disable_reason::DISABLE_BLOCKED_MATURE ||
+         profile()->IsChild());
   extension_registrar_.DisableExtension(extension_id, disable_reasons);
 }
 
@@ -1026,6 +1028,7 @@
     // related disable reasons.
     if (!profile()->IsChild()) {
       disable_reasons &= (~disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED);
+      disable_reasons &= (~disable_reason::DISABLE_BLOCKED_MATURE);
     }
 
     extension_prefs_->ReplaceDisableReasons(extension->id(), disable_reasons);
diff --git a/chrome/browser/extensions/extension_sync_service.cc b/chrome/browser/extensions/extension_sync_service.cc
index 60151f0e..11000520 100644
--- a/chrome/browser/extensions/extension_sync_service.cc
+++ b/chrome/browser/extensions/extension_sync_service.cc
@@ -77,7 +77,7 @@
   return result;
 }
 
-static_assert(extensions::disable_reason::DISABLE_REASON_LAST == (1LL << 17),
+static_assert(extensions::disable_reason::DISABLE_REASON_LAST == (1LL << 18),
               "Please consider whether your new disable reason should be"
               " syncable, and if so update this bitmask accordingly!");
 const int kKnownSyncableDisableReasons =
diff --git a/chrome/browser/metrics/extensions_metrics_provider.cc b/chrome/browser/metrics/extensions_metrics_provider.cc
index 360ad26..201d7cd0 100644
--- a/chrome/browser/metrics/extensions_metrics_provider.cc
+++ b/chrome/browser/metrics/extensions_metrics_provider.cc
@@ -211,7 +211,7 @@
   return ExtensionInstallProto::NO_BACKGROUND_SCRIPT;
 }
 
-static_assert(extensions::disable_reason::DISABLE_REASON_LAST == (1LL << 17),
+static_assert(extensions::disable_reason::DISABLE_REASON_LAST == (1LL << 18),
               "Adding a new disable reason? Be sure to include the new reason "
               "below, update the test to exercise it, and then adjust this "
               "value for DISABLE_REASON_LAST");
@@ -250,6 +250,8 @@
        ExtensionInstallProto::CUSTODIAN_APPROVAL_REQUIRED},
       {extensions::disable_reason::DISABLE_BLOCKED_BY_POLICY,
        ExtensionInstallProto::BLOCKED_BY_POLICY},
+      {extensions::disable_reason::DISABLE_BLOCKED_MATURE,
+       ExtensionInstallProto::BLOCKED_MATURE},
   };
 
   int disable_reasons = prefs->GetDisableReasons(id);
diff --git a/chrome/browser/resources/extensions/detail_view.js b/chrome/browser/resources/extensions/detail_view.js
index f2658559..155381f 100644
--- a/chrome/browser/resources/extensions/detail_view.js
+++ b/chrome/browser/resources/extensions/detail_view.js
@@ -167,10 +167,8 @@
   hasWarnings_() {
     return this.data.disableReasons.corruptInstall ||
         this.data.disableReasons.suspiciousInstall ||
-        this.data.disableReasons.updateRequired ||
-        this.data.disableReasons.blockedByPolicy ||
-        this.data.disableReasons.custodianApprovalRequired ||
-        !!this.data.blacklistText || this.data.runtimeWarnings.length > 0;
+        this.data.disableReasons.updateRequired || !!this.data.blacklistText ||
+        this.data.runtimeWarnings.length > 0;
   },
 
   /**
diff --git a/chrome/browser/resources/extensions/item_util.js b/chrome/browser/resources/extensions/item_util.js
index d587ec0e..029ca120 100644
--- a/chrome/browser/resources/extensions/item_util.js
+++ b/chrome/browser/resources/extensions/item_util.js
@@ -68,7 +68,8 @@
       item.disableReasons.suspiciousInstall ||
       item.disableReasons.updateRequired ||
       item.disableReasons.blockedByPolicy ||
-      item.disableReasons.custodianApprovalRequired) {
+      item.disableReasons.custodianApprovalRequired ||
+      item.disableReasons.blockedMature) {
     return false;
   }
   // An item with dependent extensions can't be disabled (it would bork the
diff --git a/chrome/browser/supervised_user/supervised_user_extension_browsertest.cc b/chrome/browser/supervised_user/supervised_user_extension_browsertest.cc
index 262c29f..c793353 100644
--- a/chrome/browser/supervised_user/supervised_user_extension_browsertest.cc
+++ b/chrome/browser/supervised_user/supervised_user_extension_browsertest.cc
@@ -5,6 +5,7 @@
 #include "base/files/file_path.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/supervised_user/logged_in_user_mixin.h"
 #include "chrome/browser/supervised_user/supervised_user_features.h"
@@ -107,6 +108,12 @@
         extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED);
   }
 
+  bool IsDisabledForBlockedMature(const std::string& extension_id) {
+    ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile());
+    return extension_prefs->HasDisableReason(
+        extension_id, extensions::disable_reason::DISABLE_BLOCKED_MATURE);
+  }
+
  private:
   InProcessBrowserTestMixinHost mixin_host_;
 
@@ -140,6 +147,7 @@
   // disabled.
   EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(kGoodCrxId));
   EXPECT_TRUE(IsDisabledForCustodianApproval(kGoodCrxId));
+  EXPECT_FALSE(IsDisabledForBlockedMature(kGoodCrxId));
 }
 
 IN_PROC_BROWSER_TEST_F(SupervisedUserExtensionTest,
@@ -153,6 +161,47 @@
   // The extension should be enabled now after removing supervision.
   EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(kGoodCrxId));
   EXPECT_FALSE(IsDisabledForCustodianApproval(kGoodCrxId));
+  EXPECT_FALSE(IsDisabledForBlockedMature(kGoodCrxId));
+}
+
+// Removing supervision should also remove associated disable reasons, such as
+// DISABLE_BLOCKED_MATURE. Extensions should become enabled again after removing
+// supervision. Prevents a regression to crbug/1045625.
+IN_PROC_BROWSER_TEST_F(SupervisedUserExtensionTest,
+                       PRE_RemovingSupervisionBlockedMature) {
+  SetSupervisedUserExtensionsMayRequestPermissionsPref(true);
+
+  EXPECT_TRUE(profile()->IsChild());
+
+  base::FilePath path = test_data_dir_.AppendASCII("good.crx");
+  EXPECT_FALSE(LoadExtensionWithFlags(path, kFlagNone));
+  const Extension* extension =
+      extension_registry()->GetInstalledExtension(kGoodCrxId);
+  EXPECT_TRUE(extension);
+
+  // Let's pretend this extension is mature.
+  extension_service()->DisableExtension(kGoodCrxId,
+                                        disable_reason::DISABLE_BLOCKED_MATURE);
+
+  // This extension is a supervised user initiated install and should remain
+  // disabled.
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(kGoodCrxId));
+  EXPECT_TRUE(IsDisabledForCustodianApproval(kGoodCrxId));
+  EXPECT_TRUE(IsDisabledForBlockedMature(kGoodCrxId));
+}
+
+IN_PROC_BROWSER_TEST_F(SupervisedUserExtensionTest,
+                       RemovingSupervisionBlockedMature) {
+  EXPECT_FALSE(profile()->IsChild());
+  // The extension should still be installed since we are sharing the same data
+  // directory as the PRE test.
+  const Extension* extension =
+      extension_registry()->GetInstalledExtension(kGoodCrxId);
+  EXPECT_TRUE(extension);
+  // The extension should be enabled now after removing supervision.
+  EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(kGoodCrxId));
+  EXPECT_FALSE(IsDisabledForCustodianApproval(kGoodCrxId));
+  EXPECT_FALSE(IsDisabledForBlockedMature(kGoodCrxId));
 }
 
 }  // namespace extensions
diff --git a/chrome/common/extensions/api/developer_private.idl b/chrome/common/extensions/api/developer_private.idl
index f49c474..dcdb611 100644
--- a/chrome/common/extensions/api/developer_private.idl
+++ b/chrome/common/extensions/api/developer_private.idl
@@ -138,6 +138,7 @@
     boolean updateRequired;
     boolean blockedByPolicy;
     boolean custodianApprovalRequired;
+    boolean blockedMature;
   };
 
   dictionary OptionsPage {
diff --git a/chrome/test/data/extensions/api_test/developer/generated_output/behllobkkfkfnphdnhnkndlbkcpglgmj.json b/chrome/test/data/extensions/api_test/developer/generated_output/behllobkkfkfnphdnhnkndlbkcpglgmj.json
index de0f3f99..b15256a 100644
--- a/chrome/test/data/extensions/api_test/developer/generated_output/behllobkkfkfnphdnhnkndlbkcpglgmj.json
+++ b/chrome/test/data/extensions/api_test/developer/generated_output/behllobkkfkfnphdnhnkndlbkcpglgmj.json
@@ -3,6 +3,7 @@
    "description": "The first extension that I made.",
    "disableReasons": {
       "blockedByPolicy": false,
+      "blockedMature": false,
       "corruptInstall": false,
       "custodianApprovalRequired": false,
       "suspiciousInstall": false,
diff --git a/chrome/test/data/extensions/api_test/developer/generated_output/bjafgdebaacbbbecmhlhpofkepfkgcpa.json b/chrome/test/data/extensions/api_test/developer/generated_output/bjafgdebaacbbbecmhlhpofkepfkgcpa.json
index 72544aa..0dc7d98 100644
--- a/chrome/test/data/extensions/api_test/developer/generated_output/bjafgdebaacbbbecmhlhpofkepfkgcpa.json
+++ b/chrome/test/data/extensions/api_test/developer/generated_output/bjafgdebaacbbbecmhlhpofkepfkgcpa.json
@@ -3,6 +3,7 @@
    "description": "",
    "disableReasons": {
       "blockedByPolicy": false,
+      "blockedMature": false,
       "corruptInstall": false,
       "custodianApprovalRequired": false,
       "suspiciousInstall": false,
diff --git a/chrome/test/data/extensions/api_test/developer/generated_output/hpiknbiabeeppbpihjehijgoemciehgk.json b/chrome/test/data/extensions/api_test/developer/generated_output/hpiknbiabeeppbpihjehijgoemciehgk.json
index 560fc5b..d599b716 100644
--- a/chrome/test/data/extensions/api_test/developer/generated_output/hpiknbiabeeppbpihjehijgoemciehgk.json
+++ b/chrome/test/data/extensions/api_test/developer/generated_output/hpiknbiabeeppbpihjehijgoemciehgk.json
@@ -3,6 +3,7 @@
    "description": "",
    "disableReasons": {
       "blockedByPolicy": false,
+      "blockedMature": false,
       "corruptInstall": false,
       "custodianApprovalRequired": false,
       "suspiciousInstall": false,
diff --git a/chrome/test/data/webui/extensions/detail_view_test.js b/chrome/test/data/webui/extensions/detail_view_test.js
index 6eb2c310..ca2d374 100644
--- a/chrome/test/data/webui/extensions/detail_view_test.js
+++ b/chrome/test/data/webui/extensions/detail_view_test.js
@@ -184,6 +184,13 @@
     item.set('data.disableReasons.blockedByPolicy', false);
     flush();
 
+    item.set('data.disableReasons.blockedMature', true);
+    flush();
+    expectTrue(testIsVisible('#enableToggle'));
+    expectTrue(item.$$('#enableToggle').disabled);
+    item.set('data.disableReasons.blockedMature', false);
+    flush();
+
     item.set('data.disableReasons.custodianApprovalRequired', true);
     flush();
     expectFalse(testIsVisible('#enableToggle'));
diff --git a/chrome/test/data/webui/extensions/item_test.js b/chrome/test/data/webui/extensions/item_test.js
index c1f2d0bd..0d1d3b0 100644
--- a/chrome/test/data/webui/extensions/item_test.js
+++ b/chrome/test/data/webui/extensions/item_test.js
@@ -355,6 +355,13 @@
         item.set('data.disableReasons.blockedByPolicy', false);
         flush();
 
+        item.set('data.disableReasons.blockedMature', true);
+        flush();
+        testVisible(item, '#enableToggle', true);
+        expectTrue(item.$['enableToggle'].disabled);
+        item.set('data.disableReasons.blockedMature', false);
+        flush();
+
         item.set('data.disableReasons.custodianApprovalRequired', true);
         flush();
         testVisible(item, '#enableToggle', false);
diff --git a/chrome/test/data/webui/extensions/test_util.js b/chrome/test/data/webui/extensions/test_util.js
index 713ff49..055511f 100644
--- a/chrome/test/data/webui/extensions/test_util.js
+++ b/chrome/test/data/webui/extensions/test_util.js
@@ -181,6 +181,7 @@
           updateRequired: false,
           blockedByPolicy: false,
           custodianApprovalRequired: false,
+          blockedMature: false,
         },
         homePage: {specified: false, url: ''},
         iconUrl: 'chrome://extension-icon/' + id + '/24/0',