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,