Implement cssOrigin option for tabs.insertCSS

crrev.com/c/765642 makes user style sheets opt-in. In this patch we
implement the "cssOrigin" option for tabs.insertCSS, similar to Firefox.
If the value is set to "user", the CSS will be injected as a user style
sheet.

Style sheets specified in an extension's manifest are still injected as
author style sheets.

These are the code-level changes:

 1.  InjectDetails in extension_types.json now has a new "cssOrigin"
     property that takes the values "author" (default) and "user"
 2.  ScriptExecutor::ExecuteScript now takes a css_origin parameter of
     type base::Optional<CSSOrigin>
 3.  ExtensionMsg_ExecuteCode_Params now has a new member called
     css_origin of type base::Optional<CSSOrigin>, the value of which
     comes from the API call and is propagated down to
     blink::WebDocument

This API change is fully backwards compatible.

BUG=632009

Change-Id: Ia41ea4b917c7a9a4729e0a340ed7b3be43abdc11
Reviewed-on: https://chromium-review.googlesource.com/778402
Commit-Queue: Will Harris <wfh@chromium.org>
Reviewed-by: Will Harris <wfh@chromium.org>
Reviewed-by: Devlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#530686}
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index e802ba0..173a98d 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -1389,7 +1389,7 @@
             ScriptExecutor::SINGLE_FRAME, ExtensionApiFrameIdMap::kTopFrameId,
             ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
             ScriptExecutor::MAIN_WORLD, ScriptExecutor::DEFAULT_PROCESS, GURL(),
-            GURL(), user_gesture(), ScriptExecutor::NO_RESULT,
+            GURL(), user_gesture(), base::nullopt, ScriptExecutor::NO_RESULT,
             base::Bind(&TabsUpdateFunction::OnExecuteCodeFinished, this));
 
     *is_async = true;
diff --git a/chrome/browser/extensions/execute_script_apitest.cc b/chrome/browser/extensions/execute_script_apitest.cc
index ca31131..d249e0e 100644
--- a/chrome/browser/extensions/execute_script_apitest.cc
+++ b/chrome/browser/extensions/execute_script_apitest.cc
@@ -90,6 +90,10 @@
   ASSERT_TRUE(RunExtensionTest("executescript/run_at")) << message_;
 }
 
+IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptCSSOrigin) {
+  ASSERT_TRUE(RunExtensionTest("executescript/css_origin")) << message_;
+}
+
 IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptCallback) {
   ASSERT_TRUE(RunExtensionTest("executescript/callback")) << message_;
 }
diff --git a/chrome/test/data/extensions/api_test/executescript/css_origin/b.js b/chrome/test/data/extensions/api_test/executescript/css_origin/b.js
new file mode 100644
index 0000000..653fedc
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/executescript/css_origin/b.js
@@ -0,0 +1,13 @@
+// Copyright 2017 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.
+
+chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
+  var element = document.getElementById(message.id);
+  var style = getComputedStyle(element);
+  var response = {
+    color: style.getPropertyValue('color'),
+    backgroundColor: style.getPropertyValue('background-color')
+  };
+  sendResponse(response);
+});
diff --git a/chrome/test/data/extensions/api_test/executescript/css_origin/manifest.json b/chrome/test/data/extensions/api_test/executescript/css_origin/manifest.json
new file mode 100644
index 0000000..bfbdf15
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/executescript/css_origin/manifest.json
@@ -0,0 +1,16 @@
+{
+  "version": "1.0.0.0",
+  "manifest_version": 2,
+  "name": "css_origin test",
+  "description": "Test the css_origin property of insertCSS",
+  "background": {
+    "scripts": ["test.js"]
+  },
+  "permissions": ["tabs", "http://b.com/"],
+  "content_scripts": [
+    {
+      "matches": ["http://*/*"],
+      "js": ["b.js"]
+    }
+  ]
+}
diff --git a/chrome/test/data/extensions/api_test/executescript/css_origin/test.html b/chrome/test/data/extensions/api_test/executescript/css_origin/test.html
new file mode 100644
index 0000000..a806a79
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/executescript/css_origin/test.html
@@ -0,0 +1,16 @@
+<html>
+  <style>
+    #author, #user, #none {
+      color: red;
+    }
+  </style>
+  <div id="author" style="background-color: black !important">
+    Hello.
+  </div>
+  <div id="user" style="background-color: black !important">
+    Hello.
+  </div>
+  <div id="none" style="background-color: black !important">
+    Hello.
+  </div>
+</html>
diff --git a/chrome/test/data/extensions/api_test/executescript/css_origin/test.js b/chrome/test/data/extensions/api_test/executescript/css_origin/test.js
new file mode 100644
index 0000000..7804733
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/executescript/css_origin/test.js
@@ -0,0 +1,92 @@
+// Copyright 2017 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.
+
+chrome.test.getConfig(function(config) {
+  var testUrl = 'http://b.com:' + config.testServer.port +
+                '/extensions/api_test/executescript/css_origin/test.html';
+  chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo, tab) {
+    if (changeInfo.status != 'complete')
+      return;
+    chrome.tabs.onUpdated.removeListener(listener);
+    chrome.test.runTests([
+      // Until we have tabs.removeCSS we just have to target a different
+      // element on the page for each test.
+      function authorOriginShouldSucceed() {
+        var injectDetails = {};
+        injectDetails.code = '#author {' +
+                             ' color: blue !important;' +
+                             ' background-color: white !important;' +
+                             '}';
+        injectDetails.cssOrigin = 'author';
+        chrome.tabs.insertCSS(tabId, injectDetails,
+                              chrome.test.callbackPass(function() {
+          chrome.tabs.sendMessage(tabId, { id: 'author' },
+                                  chrome.test.callbackPass(function(response) {
+            chrome.test.assertEq('rgb(0, 0, 255)', response.color);
+            // !important rules in author style sheets do not override inline
+            // !important rules.
+            chrome.test.assertEq('rgb(0, 0, 0)', response.backgroundColor);
+          }));
+        }));
+      },
+      function userOriginShouldSucceed() {
+        var injectDetails = {};
+        injectDetails.code = '#user {' +
+                             ' color: blue !important;' +
+                             ' background-color: white !important;' +
+                             '}';
+        injectDetails.cssOrigin = 'user';
+        chrome.tabs.insertCSS(tabId, injectDetails,
+                              chrome.test.callbackPass(function() {
+          chrome.tabs.sendMessage(tabId, { id: 'user' },
+                                  chrome.test.callbackPass(function(response) {
+            chrome.test.assertEq('rgb(0, 0, 255)', response.color);
+            // !important rules in user style sheets do override inline
+            // !important rules.
+            chrome.test.assertEq('rgb(255, 255, 255)',
+                                 response.backgroundColor);
+          }));
+        }));
+      },
+      function noneOriginShouldSucceed() {
+        // When no CSS origin is specified, it should default to author origin.
+        var injectDetails = {};
+        injectDetails.code = '#none {' +
+                             ' color: blue !important;' +
+                             ' background-color: white !important;' +
+                             '}';
+        chrome.tabs.insertCSS(tabId, injectDetails,
+                              chrome.test.callbackPass(function() {
+          chrome.tabs.sendMessage(tabId, { id: 'none' },
+                                  chrome.test.callbackPass(function(response) {
+            chrome.test.assertEq('rgb(0, 0, 255)', response.color);
+            // !important rules in author style sheets do not override inline
+            // !important rules.
+            chrome.test.assertEq('rgb(0, 0, 0)', response.backgroundColor);
+          }));
+        }));
+      },
+      function unknownOriginShouldFail() {
+        var injectDetails = {};
+        injectDetails.code = '#unknown { color: black !important }';
+        injectDetails.cssOrigin = 'unknown';
+        try {
+          chrome.tabs.insertCSS(tabId, injectDetails);
+          chrome.test.fail('Unknown CSS origin should throw an error');
+        } catch (e) {
+          chrome.test.succeed();
+        }
+      },
+      function originInExecuteScriptShouldFail() {
+        var injectDetails = {};
+        injectDetails.code = '(function(){})();';
+        injectDetails.cssOrigin = 'author';
+        chrome.tabs.executeScript(tabId, injectDetails,
+            chrome.test.callbackFail(
+                'CSS origin should be specified only for CSS code.'));
+      }
+    ]);
+  });
+  chrome.tabs.create({ url: testUrl });
+});
diff --git a/extensions/browser/api/execute_code_function.cc b/extensions/browser/api/execute_code_function.cc
index ab9d4b5..099079901 100644
--- a/extensions/browser/api/execute_code_function.cc
+++ b/extensions/browser/api/execute_code_function.cc
@@ -31,6 +31,8 @@
 const char kBadFileEncodingError[] =
     "Could not load file '*' for content script. It isn't UTF-8 encoded.";
 const char kLoadFileError[] = "Failed to load file: \"*\". ";
+const char kCSSOriginForNonCSSError[] =
+    "CSS origin should be specified only for CSS code.";
 
 }
 
@@ -147,12 +149,18 @@
   }
   CHECK_NE(UserScript::UNDEFINED, run_at);
 
+  base::Optional<CSSOrigin> css_origin;
+  if (details_->css_origin == api::extension_types::CSS_ORIGIN_USER)
+    css_origin = CSS_ORIGIN_USER;
+  else if (details_->css_origin == api::extension_types::CSS_ORIGIN_AUTHOR)
+    css_origin = CSS_ORIGIN_AUTHOR;
+
   executor->ExecuteScript(
       host_id_, script_type, code_string, frame_scope, frame_id,
       match_about_blank, run_at, ScriptExecutor::ISOLATED_WORLD,
       IsWebView() ? ScriptExecutor::WEB_VIEW_PROCESS
                   : ScriptExecutor::DEFAULT_PROCESS,
-      GetWebViewSrc(), file_url_, user_gesture(),
+      GetWebViewSrc(), file_url_, user_gesture(), css_origin,
       has_callback() ? ScriptExecutor::JSON_SERIALIZED_RESULT
                      : ScriptExecutor::NO_RESULT,
       base::Bind(&ExecuteCodeFunction::OnExecuteCodeFinished, this));
@@ -180,6 +188,11 @@
     error_ = kMoreThanOneValuesError;
     return false;
   }
+  if (details_->css_origin != api::extension_types::CSS_ORIGIN_NONE &&
+      !ShouldInsertCSS()) {
+    error_ = kCSSOriginForNonCSSError;
+    return false;
+  }
 
   if (!CanExecuteScriptOnPage())
     return false;
diff --git a/extensions/browser/script_executor.cc b/extensions/browser/script_executor.cc
index 9adc646..c3cfa38 100644
--- a/extensions/browser/script_executor.cc
+++ b/extensions/browser/script_executor.cc
@@ -242,6 +242,7 @@
                                    const GURL& webview_src,
                                    const GURL& file_url,
                                    bool user_gesture,
+                                   base::Optional<CSSOrigin> css_origin,
                                    ScriptExecutor::ResultType result_type,
                                    const ExecuteScriptCallback& callback) {
   if (host_id.type() == HostID::EXTENSIONS) {
@@ -268,6 +269,7 @@
   params.file_url = file_url;
   params.wants_result = (result_type == JSON_SERIALIZED_RESULT);
   params.user_gesture = user_gesture;
+  params.css_origin = css_origin;
 
   // Handler handles IPCs and deletes itself on completion.
   new Handler(script_observers_, web_contents_, params, frame_scope, frame_id,
diff --git a/extensions/browser/script_executor.h b/extensions/browser/script_executor.h
index 25e6c54..46cb9bb 100644
--- a/extensions/browser/script_executor.h
+++ b/extensions/browser/script_executor.h
@@ -7,6 +7,8 @@
 
 #include "base/callback_forward.h"
 #include "base/observer_list.h"
+#include "base/optional.h"
+#include "extensions/common/constants.h"
 #include "extensions/common/user_script.h"
 
 class GURL;
@@ -101,6 +103,7 @@
                      const GURL& webview_src,
                      const GURL& file_url,
                      bool user_gesture,
+                     base::Optional<CSSOrigin> css_origin,
                      ResultType result_type,
                      const ExecuteScriptCallback& callback);
 
diff --git a/extensions/common/api/extension_types.json b/extensions/common/api/extension_types.json
index 9f27590..e2ea16dc 100644
--- a/extensions/common/api/extension_types.json
+++ b/extensions/common/api/extension_types.json
@@ -39,6 +39,12 @@
         "description": "The soonest that the JavaScript or CSS will be injected into the tab."
       },
       {
+        "id": "CSSOrigin",
+        "type": "string",
+        "enum": ["author", "user"],
+        "description": "The <a href=\"https://www.w3.org/TR/css3-cascade/#cascading-origins\">origin</a> of injected CSS."
+      },
+      {
         "id": "InjectDetails",
         "type": "object",
         "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time.",
@@ -61,6 +67,11 @@
             "$ref": "RunAt",
             "optional": true,
             "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"."
+          },
+          "cssOrigin": {
+            "$ref": "CSSOrigin",
+            "optional": true,
+            "description": "The <a href=\"https://www.w3.org/TR/css3-cascade/#cascading-origins\">origin</a> of the CSS to inject. This may only be specified for CSS, not JavaScript. Defaults to <code>\"author\"</code>."
           }
         }
       }
diff --git a/extensions/common/constants.h b/extensions/common/constants.h
index b96eb342..a55a018 100644
--- a/extensions/common/constants.h
+++ b/extensions/common/constants.h
@@ -185,6 +185,10 @@
   NUM_LAUNCH_CONTAINERS
 };
 
+// The origin of injected CSS.
+enum CSSOrigin { CSS_ORIGIN_AUTHOR, CSS_ORIGIN_USER };
+static const CSSOrigin CSS_ORIGIN_LAST = CSS_ORIGIN_USER;
+
 }  // namespace extensions
 
 namespace extension_misc {
diff --git a/extensions/common/extension_messages.h b/extensions/common/extension_messages.h
index f21c902..790420a 100644
--- a/extensions/common/extension_messages.h
+++ b/extensions/common/extension_messages.h
@@ -18,6 +18,7 @@
 #include "content/public/common/socket_permission_request.h"
 #include "extensions/common/api/messaging/message.h"
 #include "extensions/common/api/messaging/port_id.h"
+#include "extensions/common/constants.h"
 #include "extensions/common/common_param_traits.h"
 #include "extensions/common/draggable_region.h"
 #include "extensions/common/event_filtering_info.h"
@@ -38,6 +39,8 @@
 
 #define IPC_MESSAGE_START ExtensionMsgStart
 
+IPC_ENUM_TRAITS_MAX_VALUE(extensions::CSSOrigin, extensions::CSS_ORIGIN_LAST)
+
 IPC_ENUM_TRAITS_MAX_VALUE(extensions::ViewType, extensions::VIEW_TYPE_LAST)
 IPC_ENUM_TRAITS_MAX_VALUE(content::SocketPermissionRequest::OperationType,
                           content::SocketPermissionRequest::OPERATION_TYPE_LAST)
@@ -179,6 +182,9 @@
 
   // Whether the code to be executed should be associated with a user gesture.
   IPC_STRUCT_MEMBER(bool, user_gesture)
+
+  // The origin of the CSS.
+  IPC_STRUCT_MEMBER(base::Optional<extensions::CSSOrigin>, css_origin)
 IPC_STRUCT_END()
 
 // Struct containing information about the sender of connect() calls that
diff --git a/extensions/renderer/programmatic_script_injector.cc b/extensions/renderer/programmatic_script_injector.cc
index 0e0b0de..7b55c0d 100644
--- a/extensions/renderer/programmatic_script_injector.cc
+++ b/extensions/renderer/programmatic_script_injector.cc
@@ -47,6 +47,10 @@
   return params_->user_gesture;
 }
 
+base::Optional<CSSOrigin> ProgrammaticScriptInjector::GetCssOrigin() const {
+  return params_->css_origin;
+}
+
 bool ProgrammaticScriptInjector::ExpectsResults() const {
   return params_->wants_result;
 }
diff --git a/extensions/renderer/programmatic_script_injector.h b/extensions/renderer/programmatic_script_injector.h
index d48e6e9..ef61b20 100644
--- a/extensions/renderer/programmatic_script_injector.h
+++ b/extensions/renderer/programmatic_script_injector.h
@@ -32,6 +32,7 @@
   UserScript::InjectionType script_type() const override;
   bool ShouldExecuteInMainWorld() const override;
   bool IsUserGesture() const override;
+  base::Optional<CSSOrigin> GetCssOrigin() const override;
   bool ExpectsResults() const override;
   bool ShouldInjectJs(
       UserScript::RunLocation run_location,
diff --git a/extensions/renderer/script_injection.cc b/extensions/renderer/script_injection.cc
index fa7beb1..5699905 100644
--- a/extensions/renderer/script_injection.cc
+++ b/extensions/renderer/script_injection.cc
@@ -402,8 +402,14 @@
   std::vector<blink::WebString> css_sources = injector_->GetCssSources(
       run_location_, injected_stylesheets, num_injected_stylesheets);
   blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame();
+  // Default CSS origin is "author", but can be overridden to "user" by scripts.
+  base::Optional<CSSOrigin> css_origin = injector_->GetCssOrigin();
+  blink::WebDocument::CSSOrigin blink_css_origin =
+      css_origin && *css_origin == CSS_ORIGIN_USER
+          ? blink::WebDocument::kUserOrigin
+          : blink::WebDocument::kAuthorOrigin;
   for (const blink::WebString& css : css_sources)
-    web_frame->GetDocument().InsertStyleSheet(css);
+    web_frame->GetDocument().InsertStyleSheet(css, blink_css_origin);
 }
 
 }  // namespace extensions
diff --git a/extensions/renderer/script_injector.h b/extensions/renderer/script_injector.h
index cc03624..46470fd 100644
--- a/extensions/renderer/script_injector.h
+++ b/extensions/renderer/script_injector.h
@@ -9,6 +9,7 @@
 #include <vector>
 
 #include "extensions/common/permissions/permissions_data.h"
+#include "extensions/common/constants.h"
 #include "extensions/common/user_script.h"
 #include "third_party/WebKit/public/web/WebScriptSource.h"
 
@@ -44,6 +45,9 @@
   // Returns true if the script is running inside a user gesture.
   virtual bool IsUserGesture() const = 0;
 
+  // Returns the CSS origin of this injection.
+  virtual base::Optional<CSSOrigin> GetCssOrigin() const = 0;
+
   // Returns true if the script expects results.
   virtual bool ExpectsResults() const = 0;
 
diff --git a/extensions/renderer/user_script_injector.cc b/extensions/renderer/user_script_injector.cc
index 9be67bc..972d47a 100644
--- a/extensions/renderer/user_script_injector.cc
+++ b/extensions/renderer/user_script_injector.cc
@@ -145,6 +145,10 @@
   return false;
 }
 
+base::Optional<CSSOrigin> UserScriptInjector::GetCssOrigin() const {
+  return base::nullopt;
+}
+
 bool UserScriptInjector::ShouldInjectJs(
     UserScript::RunLocation run_location,
     const std::set<std::string>& executing_scripts) const {
diff --git a/extensions/renderer/user_script_injector.h b/extensions/renderer/user_script_injector.h
index c380b81..c543608 100644
--- a/extensions/renderer/user_script_injector.h
+++ b/extensions/renderer/user_script_injector.h
@@ -40,6 +40,7 @@
   UserScript::InjectionType script_type() const override;
   bool ShouldExecuteInMainWorld() const override;
   bool IsUserGesture() const override;
+  base::Optional<CSSOrigin> GetCssOrigin() const override;
   bool ExpectsResults() const override;
   bool ShouldInjectJs(
       UserScript::RunLocation run_location,