diff options
author | jyasskin@chromium.org <jyasskin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-26 06:33:10 +0000 |
---|---|---|
committer | jyasskin@chromium.org <jyasskin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-26 06:33:10 +0000 |
commit | a714e469bd98777467ab428e00bdcccf4753e6bb (patch) | |
tree | 9b509ec8316d537c1409a97a416cb6d2938e74c9 /chrome/browser/extensions | |
parent | 5805a2fe3998a43e13b3de9721f2cb89d9629d61 (diff) | |
download | chromium_src-a714e469bd98777467ab428e00bdcccf4753e6bb.zip chromium_src-a714e469bd98777467ab428e00bdcccf4753e6bb.tar.gz chromium_src-a714e469bd98777467ab428e00bdcccf4753e6bb.tar.bz2 |
Implement the basic declarativeContent API.
This CL includes the top_url and css conditions and the ShowPageAction
action. CSS matching is implemented inefficiently using a
MutationObserver, but I plan to push that into WebKit before releasing
this to stable.
BUG=172011
Review URL: https://chromiumcodereview.appspot.com/11547033
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@179051 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/extensions')
19 files changed, 1398 insertions, 10 deletions
diff --git a/chrome/browser/extensions/api/declarative/rules_registry_service.cc b/chrome/browser/extensions/api/declarative/rules_registry_service.cc index 24e7f32..58a849d 100644 --- a/chrome/browser/extensions/api/declarative/rules_registry_service.cc +++ b/chrome/browser/extensions/api/declarative/rules_registry_service.cc @@ -8,6 +8,8 @@ #include "base/logging.h" #include "chrome/browser/extensions/api/declarative/initializing_rules_registry.h" #include "chrome/browser/extensions/api/declarative/rules_registry_storage_delegate.h" +#include "chrome/browser/extensions/api/declarative_content/content_constants.h" +#include "chrome/browser/extensions/api/declarative_content/content_rules_registry.h" #include "chrome/browser/extensions/api/declarative_webrequest/webrequest_constants.h" #include "chrome/browser/extensions/api/declarative_webrequest/webrequest_rules_registry.h" #include "chrome/browser/extensions/api/web_request/web_request_api.h" @@ -69,6 +71,21 @@ void RulesRegistryService::RegisterDefaultRulesRegistries() { content::BrowserThread::IO, FROM_HERE, base::Bind(&RegisterToExtensionWebRequestEventRouterOnIO, profile_, web_request_rules_registry)); + +#if defined(ENABLE_EXTENSIONS) + delegate = new RulesRegistryStorageDelegate(); + scoped_refptr<ContentRulesRegistry> content_rules_registry( + new ContentRulesRegistry(profile_, delegate)); + delegate->InitOnUIThread(profile_, content_rules_registry, + GetDeclarativeRuleStorageKey( + declarative_content_constants::kOnPageChanged, + profile_->IsOffTheRecord())); + delegates_.push_back(delegate); + + RegisterRulesRegistry(declarative_content_constants::kOnPageChanged, + content_rules_registry); + content_rules_registry_ = content_rules_registry.get(); +#endif // defined(ENABLE_EXTENSIONS) } void RulesRegistryService::Shutdown() { diff --git a/chrome/browser/extensions/api/declarative/rules_registry_service.h b/chrome/browser/extensions/api/declarative/rules_registry_service.h index db0485b..1910d16 100644 --- a/chrome/browser/extensions/api/declarative/rules_registry_service.h +++ b/chrome/browser/extensions/api/declarative/rules_registry_service.h @@ -21,6 +21,7 @@ class NotificationSource; } namespace extensions { +class ContentRulesRegistry; class RulesRegistry; class RulesRegistryStorageDelegate; } @@ -50,6 +51,11 @@ class RulesRegistryService : public content::NotificationObserver { scoped_refptr<RulesRegistry> GetRulesRegistry( const std::string& event_name) const; + // Accessors for each type of rules registry. + ContentRulesRegistry* content_rules_registry() const { + return content_rules_registry_; + } + // For testing. void SimulateExtensionUnloaded(const std::string& extension_id); private: @@ -72,6 +78,10 @@ class RulesRegistryService : public content::NotificationObserver { // keep track of them so we can tell them to do cleanup on shutdown. std::vector<RulesRegistryStorageDelegate*> delegates_; + // Weak pointer into rule_registries_ to make it easier to handle content rule + // conditions. + ContentRulesRegistry* content_rules_registry_; + content::NotificationRegistrar registrar_; Profile* profile_; diff --git a/chrome/browser/extensions/api/declarative/rules_registry_storage_delegate.cc b/chrome/browser/extensions/api/declarative/rules_registry_storage_delegate.cc index 2cb62d3..ce9fb2c 100644 --- a/chrome/browser/extensions/api/declarative/rules_registry_storage_delegate.cc +++ b/chrome/browser/extensions/api/declarative/rules_registry_storage_delegate.cc @@ -194,7 +194,8 @@ void RulesRegistryStorageDelegate::Inner::InitForOTRProfile() { const ExtensionSet* extensions = extension_service->extensions(); for (ExtensionSet::const_iterator i = extensions->begin(); i != extensions->end(); ++i) { - if ((*i)->HasAPIPermission(APIPermission::kDeclarativeWebRequest) && + if (((*i)->HasAPIPermission(APIPermission::kDeclarativeContent) || + (*i)->HasAPIPermission(APIPermission::kDeclarativeWebRequest)) && extension_service->IsIncognitoEnabled((*i)->id())) ReadFromStorage((*i)->id()); } @@ -211,8 +212,8 @@ void RulesRegistryStorageDelegate::Inner::Observe( content::Details<const extensions::Extension>(details).ptr(); // TODO(mpcomplete): This API check should generalize to any use of // declarative rules, not just webRequest. - if (extension->HasAPIPermission( - APIPermission::kDeclarativeWebRequest)) { + if (extension->HasAPIPermission(APIPermission::kDeclarativeContent) || + extension->HasAPIPermission(APIPermission::kDeclarativeWebRequest)) { ExtensionInfoMap* extension_info_map = ExtensionSystem::Get(profile_)->info_map(); if (profile_->IsOffTheRecord() && diff --git a/chrome/browser/extensions/api/declarative_content/OWNERS b/chrome/browser/extensions/api/declarative_content/OWNERS new file mode 100644 index 0000000..5ee8175 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/OWNERS @@ -0,0 +1,2 @@ +battre@chromium.org +jyasskin@chromium.org diff --git a/chrome/browser/extensions/api/declarative_content/content_action.cc b/chrome/browser/extensions/api/declarative_content/content_action.cc new file mode 100644 index 0000000..24661dd --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_action.cc @@ -0,0 +1,148 @@ +// Copyright (c) 2012 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/api/declarative_content/content_action.h" + +#include <map> + +#include "base/lazy_instance.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "chrome/browser/extensions/api/declarative_content/content_constants.h" +#include "chrome/browser/extensions/extension_action.h" +#include "chrome/browser/extensions/extension_action_manager.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/extension_tab_util.h" +#include "chrome/common/extensions/extension.h" +#include "content/public/browser/invalidate_type.h" +#include "content/public/browser/web_contents.h" + +namespace extensions { + +namespace keys = declarative_content_constants; + +namespace { +// Error messages. +const char kInvalidInstanceTypeError[] = + "An action has an invalid instanceType: %s"; + +#define INPUT_FORMAT_VALIDATE(test) do { \ + if (!(test)) { \ + *bad_message = true; \ + return scoped_ptr<ContentAction>(NULL); \ + } \ + } while (0) + +// +// The following are concrete actions. +// + +// Action that instructs to show an extension's page action. +class ShowPageAction : public ContentAction { + public: + ShowPageAction() {} + virtual ~ShowPageAction() {} + + // Implementation of ContentAction: + virtual Type GetType() const OVERRIDE { return ACTION_SHOW_PAGE_ACTION; } + virtual void Apply(const std::string& extension_id, + const base::Time& extension_install_time, + ApplyInfo* apply_info) const OVERRIDE { + GetPageAction(apply_info->profile, extension_id)->DeclarativeShow( + ExtensionTabUtil::GetTabId(apply_info->tab)); + apply_info->tab->NotifyNavigationStateChanged( + content::INVALIDATE_TYPE_PAGE_ACTIONS); + } + virtual void Revert(const std::string& extension_id, + const base::Time& extension_install_time, + ApplyInfo* apply_info) const OVERRIDE { + GetPageAction(apply_info->profile, extension_id)-> + UndoDeclarativeShow(ExtensionTabUtil::GetTabId(apply_info->tab)); + apply_info->tab->NotifyNavigationStateChanged( + content::INVALIDATE_TYPE_PAGE_ACTIONS); + } + + private: + static ExtensionAction* GetPageAction(Profile* profile, + const std::string& extension_id) { + ExtensionService* service = + ExtensionSystem::Get(profile)->extension_service(); + const Extension* extension = service->GetInstalledExtension(extension_id); + DCHECK(extension); + return ExtensionActionManager::Get(profile)->GetPageAction(*extension); + } + + DISALLOW_COPY_AND_ASSIGN(ShowPageAction); +}; + +// Helper function for ContentActions that can be instantiated by just +// calling the constructor. +template <class T> +scoped_ptr<ContentAction> CallConstructorFactoryMethod( + const base::DictionaryValue* dict, + std::string* error, + bool* bad_message) { + return scoped_ptr<ContentAction>(new T); +} + +struct ContentActionFactory { + // Factory methods for ContentAction instances. |dict| contains the json + // dictionary that describes the action. |error| is used to return error + // messages in case the extension passed an action that was syntactically + // correct but semantically incorrect. |bad_message| is set to true in case + // |dict| does not confirm to the validated JSON specification. + typedef scoped_ptr<ContentAction> + (* FactoryMethod)(const base::DictionaryValue* /* dict */, + std::string* /* error */, + bool* /* bad_message */); + // Maps the name of a declarativeContent action type to the factory + // function creating it. + std::map<std::string, FactoryMethod> factory_methods; + + ContentActionFactory() { + factory_methods[keys::kShowPageAction] = + &CallConstructorFactoryMethod<ShowPageAction>; + } +}; + +base::LazyInstance<ContentActionFactory>::Leaky + g_content_action_factory = LAZY_INSTANCE_INITIALIZER; + +} // namespace + +// +// ContentAction +// + +ContentAction::ContentAction() {} + +ContentAction::~ContentAction() {} + +// static +scoped_ptr<ContentAction> ContentAction::Create( + const base::Value& json_action, + std::string* error, + bool* bad_message) { + *error = ""; + *bad_message = false; + + const base::DictionaryValue* action_dict = NULL; + INPUT_FORMAT_VALIDATE(json_action.GetAsDictionary(&action_dict)); + + std::string instance_type; + INPUT_FORMAT_VALIDATE( + action_dict->GetString(keys::kInstanceType, &instance_type)); + + ContentActionFactory& factory = g_content_action_factory.Get(); + std::map<std::string, ContentActionFactory::FactoryMethod>::iterator + factory_method_iter = factory.factory_methods.find(instance_type); + if (factory_method_iter != factory.factory_methods.end()) + return (*factory_method_iter->second)(action_dict, error, bad_message); + + *error = base::StringPrintf(kInvalidInstanceTypeError, instance_type.c_str()); + return scoped_ptr<ContentAction>(); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_action.h b/chrome/browser/extensions/api/declarative_content/content_action.h new file mode 100644 index 0000000..4750ac9 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_action.h @@ -0,0 +1,70 @@ +// Copyright (c) 2012 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. + +#ifndef CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_ACTION_H_ +#define CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_ACTION_H_ + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "chrome/browser/extensions/api/declarative/declarative_rule.h" + +class Profile; + +namespace base { +class Time; +class Value; +} + +namespace content { +class WebContents; +} + +namespace extensions { + +// Base class for all ContentActions of the declarative content API. +class ContentAction { + public: + // Type identifiers for concrete ContentActions. + enum Type { + ACTION_SHOW_PAGE_ACTION, + }; + + struct ApplyInfo { + Profile* profile; + content::WebContents* tab; + }; + + ContentAction(); + virtual ~ContentAction(); + + virtual Type GetType() const = 0; + + // Applies or reverts this ContentAction on a particular tab for a particular + // extension. Revert exists to keep the actions up to date as the page + // changes. + virtual void Apply(const std::string& extension_id, + const base::Time& extension_install_time, + ApplyInfo* apply_info) const = 0; + virtual void Revert(const std::string& extension_id, + const base::Time& extension_install_time, + ApplyInfo* apply_info) const = 0; + + // Factory method that instantiates a concrete ContentAction + // implementation according to |json_action|, the representation of the + // ContentAction as received from the extension API. + // Sets |error| and returns NULL in case of a semantic error that cannot + // be caught by schema validation. Sets |bad_message| and returns NULL + // in case the input is syntactically unexpected. + static scoped_ptr<ContentAction> Create(const base::Value& json_action, + std::string* error, + bool* bad_message); +}; + +typedef DeclarativeActionSet<ContentAction> ContentActionSet; + +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_ACTION_H_ diff --git a/chrome/browser/extensions/api/declarative_content/content_action_unittest.cc b/chrome/browser/extensions/api/declarative_content/content_action_unittest.cc new file mode 100644 index 0000000..099bebf --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_action_unittest.cc @@ -0,0 +1,155 @@ +// Copyright (c) 2012 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/api/declarative_content/content_action.h" + +#include "base/command_line.h" +#include "base/message_loop.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_action.h" +#include "chrome/browser/extensions/extension_action_manager.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_tab_util.h" +#include "chrome/browser/extensions/test_extension_system.h" +#include "chrome/browser/sessions/session_tab_helper.h" +#include "chrome/common/extensions/extension_builder.h" +#include "chrome/common/extensions/value_builder.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread.h" +#include "content/public/test/web_contents_tester.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_WIN) +#include "ui/base/win/scoped_ole_initializer.h" +#endif + +namespace extensions { +namespace { + +using base::test::ParseJson; +using testing::HasSubstr; + +class TestExtensionEnvironment { + public: + TestExtensionEnvironment() + : ui_thread_(BrowserThread::UI, &loop_), + extension_service_(NULL) {} + + TestingProfile* profile() { return &profile_; } + + ExtensionService* GetExtensionService() { + if (extension_service_ == NULL) { + TestExtensionSystem* extension_system = + static_cast<TestExtensionSystem*>(ExtensionSystem::Get(&profile_)); + extension_service_ = extension_system->CreateExtensionService( + CommandLine::ForCurrentProcess(), FilePath(), false); + } + return extension_service_; + } + + scoped_refptr<Extension> MakeExtension( + const DictionaryValue& manifest_extra) { + scoped_ptr<base::DictionaryValue> manifest = DictionaryBuilder() + .Set("name", "Extension") + .Set("version", "1.0") + .Set("manifest_version", 2) + .Build(); + manifest->MergeDictionary(&manifest_extra); + + scoped_refptr<Extension> result = + ExtensionBuilder().SetManifest(manifest.Pass()).Build(); + GetExtensionService()->AddExtension(result); + return result; + } + + scoped_ptr<content::WebContents> MakeTab() { + scoped_ptr<content::WebContents> contents( + content::WebContentsTester::CreateTestWebContents(&profile_, NULL)); + // Create a tab id. + SessionTabHelper::CreateForWebContents(contents.get()); + return contents.Pass(); + } + + private: + MessageLoopForUI loop_; + content::TestBrowserThread ui_thread_; +#if defined(OS_WIN) + ui::ScopedOleInitializer ole_initializer_; +#endif + TestingProfile profile_; + ExtensionService* extension_service_; +}; + +TEST(DeclarativeContentActionTest, InvalidCreation) { + TestExtensionEnvironment env; + std::string error; + bool bad_message = false; + scoped_ptr<ContentAction> result; + + // Test wrong data type passed. + error.clear(); + result = ContentAction::Create(*ParseJson("[]"), &error, &bad_message); + EXPECT_TRUE(bad_message); + EXPECT_EQ("", error); + EXPECT_FALSE(result); + + // Test missing instanceType element. + error.clear(); + result = ContentAction::Create(*ParseJson("{}"), &error, &bad_message); + EXPECT_TRUE(bad_message); + EXPECT_EQ("", error); + EXPECT_FALSE(result); + + // Test wrong instanceType element. + error.clear(); + result = ContentAction::Create(*ParseJson( + "{\n" + " \"instanceType\": \"declarativeContent.UnknownType\",\n" + "}"), + &error, &bad_message); + EXPECT_THAT(error, HasSubstr("invalid instanceType")); + EXPECT_FALSE(result); +} + +TEST(DeclarativeContentActionTest, ShowPageAction) { + TestExtensionEnvironment env; + + std::string error; + bool bad_message = false; + scoped_ptr<ContentAction> result = ContentAction::Create( + *ParseJson("{\n" + " \"instanceType\": \"declarativeContent.ShowPageAction\",\n" + "}"), + &error, &bad_message); + EXPECT_EQ("", error); + EXPECT_FALSE(bad_message); + ASSERT_TRUE(result); + EXPECT_EQ(ContentAction::ACTION_SHOW_PAGE_ACTION, result->GetType()); + + scoped_refptr<Extension> extension = env.MakeExtension( + *DictionaryBuilder().Set("page_action", DictionaryBuilder() + .Set("default_title", "Extension")) + .Build()); + ExtensionAction* page_action = + ExtensionActionManager::Get(env.profile())->GetPageAction(*extension); + scoped_ptr<content::WebContents> contents = env.MakeTab(); + const int tab_id = ExtensionTabUtil::GetTabId(contents.get()); + EXPECT_FALSE(page_action->GetIsVisible(tab_id)); + ContentAction::ApplyInfo apply_info = { + env.profile(), contents.get() + }; + result->Apply(extension->id(), base::Time(), &apply_info); + EXPECT_TRUE(page_action->GetIsVisible(tab_id)); + result->Apply(extension->id(), base::Time(), &apply_info); + EXPECT_TRUE(page_action->GetIsVisible(tab_id)); + result->Revert(extension->id(), base::Time(), &apply_info); + EXPECT_TRUE(page_action->GetIsVisible(tab_id)); + result->Revert(extension->id(), base::Time(), &apply_info); + EXPECT_FALSE(page_action->GetIsVisible(tab_id)); +} + +} // namespace +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_condition.cc b/chrome/browser/extensions/api/declarative_content/content_condition.cc new file mode 100644 index 0000000..8ae49e5 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_condition.cc @@ -0,0 +1,140 @@ +// Copyright (c) 2012 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/api/declarative_content/content_condition.h" + +#include "base/stringprintf.h" +#include "base/values.h" +#include "chrome/browser/extensions/api/declarative_content/content_constants.h" +#include "chrome/common/extensions/matcher/url_matcher.h" +#include "chrome/common/extensions/matcher/url_matcher_factory.h" + +namespace keys = extensions::declarative_content_constants; + +namespace { +static extensions::URLMatcherConditionSet::ID g_next_id = 0; + +// TODO(jyasskin): improve error messaging to give more meaningful messages +// to the extension developer. +// Error messages: +const char kExpectedDictionary[] = "A condition has to be a dictionary."; +const char kConditionWithoutInstanceType[] = "A condition had no instanceType"; +const char kExpectedOtherConditionType[] = "Expected a condition of type " + "declarativeContent.PageStateMatcher"; +const char kUnknownConditionAttribute[] = "Unknown condition attribute '%s'"; +const char kInvalidTypeOfParamter[] = "Attribute '%s' has an invalid type"; +} // namespace + +namespace extensions { + +namespace keys = declarative_content_constants; + +RendererContentMatchData::RendererContentMatchData() {} +RendererContentMatchData::~RendererContentMatchData() {} + +// +// ContentCondition +// + +ContentCondition::ContentCondition( + scoped_refptr<URLMatcherConditionSet> url_matcher_conditions, + const std::vector<std::string>& css_selectors) + : url_matcher_conditions_(url_matcher_conditions), + css_selectors_(css_selectors) { + CHECK(url_matcher_conditions.get()); +} + +ContentCondition::~ContentCondition() {} + +bool ContentCondition::IsFulfilled( + const RendererContentMatchData& renderer_data) const { + if (!ContainsKey(renderer_data.page_url_matches, + url_matcher_conditions_->id())) + return false; + + // All attributes must be fulfilled for a fulfilled condition. + for (std::vector<std::string>::const_iterator i = + css_selectors_.begin(); i != css_selectors_.end(); ++i) { + if (!ContainsKey(renderer_data.css_selectors, *i)) + return false; + } + return true; +} + +// static +scoped_ptr<ContentCondition> ContentCondition::Create( + URLMatcherConditionFactory* url_matcher_condition_factory, + const base::Value& condition, + std::string* error) { + const base::DictionaryValue* condition_dict = NULL; + if (!condition.GetAsDictionary(&condition_dict)) { + *error = kExpectedDictionary; + return scoped_ptr<ContentCondition>(NULL); + } + + // Verify that we are dealing with a Condition whose type we understand. + std::string instance_type; + if (!condition_dict->GetString(keys::kInstanceType, &instance_type)) { + *error = kConditionWithoutInstanceType; + return scoped_ptr<ContentCondition>(NULL); + } + if (instance_type != keys::kPageStateMatcherType) { + *error = kExpectedOtherConditionType; + return scoped_ptr<ContentCondition>(NULL); + } + + scoped_refptr<URLMatcherConditionSet> url_matcher_condition_set; + std::vector<std::string> css_rules; + + for (base::DictionaryValue::Iterator iter(*condition_dict); + iter.HasNext(); iter.Advance()) { + const std::string& condition_attribute_name = iter.key(); + const Value& condition_attribute_value = iter.value(); + if (condition_attribute_name == keys::kInstanceType) { + // Skip this. + } else if (condition_attribute_name == keys::kPageUrl) { + const base::DictionaryValue* dict = NULL; + if (!condition_attribute_value.GetAsDictionary(&dict)) { + *error = base::StringPrintf(kInvalidTypeOfParamter, + condition_attribute_name.c_str()); + } else { + url_matcher_condition_set = + URLMatcherFactory::CreateFromURLFilterDictionary( + url_matcher_condition_factory, dict, ++g_next_id, error); + } + } else if (condition_attribute_name == keys::kCss) { + const base::ListValue* css_rules_value = NULL; + if (!condition_attribute_value.GetAsList(&css_rules_value)) { + *error = base::StringPrintf(kInvalidTypeOfParamter, + condition_attribute_name.c_str()); + } + for (size_t i = 0; i < css_rules_value->GetSize(); ++i) { + std::string css_rule; + if (!css_rules_value->GetString(i, &css_rule)) { + *error = base::StringPrintf(kInvalidTypeOfParamter, + condition_attribute_name.c_str()); + break; + } + css_rules.push_back(css_rule); + } + } else { + *error = base::StringPrintf(kUnknownConditionAttribute, + condition_attribute_name.c_str()); + } + if (!error->empty()) + return scoped_ptr<ContentCondition>(NULL); + } + + if (!url_matcher_condition_set) { + URLMatcherConditionSet::Conditions url_matcher_conditions; + url_matcher_conditions.insert( + url_matcher_condition_factory->CreateHostPrefixCondition("")); + url_matcher_condition_set = + new URLMatcherConditionSet(++g_next_id, url_matcher_conditions); + } + return scoped_ptr<ContentCondition>( + new ContentCondition(url_matcher_condition_set, css_rules)); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_condition.h b/chrome/browser/extensions/api/declarative_content/content_condition.h new file mode 100644 index 0000000..c07afdb --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_condition.h @@ -0,0 +1,110 @@ +// Copyright (c) 2012 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. + +#ifndef CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONDITION_H_ +#define CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONDITION_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/hash_tables.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "chrome/browser/extensions/api/declarative/declarative_rule.h" +#include "chrome/common/extensions/matcher/url_matcher.h" +#include "googleurl/src/gurl.h" + +namespace extensions { + +struct RendererContentMatchData { + RendererContentMatchData(); + ~RendererContentMatchData(); + // Match IDs for the URL of the top-level page the renderer is + // returning data for. + std::set<URLMatcherConditionSet::ID> page_url_matches; + // All watched CSS selectors that match on frames with the same + // origin as the page's main frame. + base::hash_set<std::string> css_selectors; +}; + +// Representation of a condition in the Declarative Content API. A condition +// consists of an optional URL and a list of CSS selectors. Each of these +// attributes needs to be fulfilled in order for the condition to be fulfilled. +// +// We distinguish between two types of attributes: +// - URL Matcher attributes are attributes that test the URL of a page. +// These are treated separately because we use a URLMatcher to efficiently +// test many of these attributes in parallel by using some advanced +// data structures. The URLMatcher tells us if all URL Matcher attributes +// are fulfilled for a ContentCondition. +// - All other conditions are represented individually as fields in +// ContentCondition. These conditions are probed linearly (only if the URL +// Matcher found a hit). +// +// TODO(battre) Consider making the URLMatcher an owner of the +// URLMatcherConditionSet and only pass a pointer to URLMatcherConditionSet +// in url_matcher_condition_set(). This saves some copying in +// ContentConditionSet::GetURLMatcherConditionSets. +class ContentCondition { + public: + typedef RendererContentMatchData MatchData; + + ContentCondition( + scoped_refptr<URLMatcherConditionSet> url_matcher_conditions, + const std::vector<std::string>& css_selectors); + ~ContentCondition(); + + // Factory method that instantiates a ContentCondition according to the + // description |condition| passed by the extension API. |condition| should be + // an instance of declarativeContent.PageStateMatcher. + static scoped_ptr<ContentCondition> Create( + URLMatcherConditionFactory* url_matcher_condition_factory, + const base::Value& condition, + std::string* error); + + // Returns whether the request is a match, given that the URLMatcher found + // a match for |url_matcher_conditions_|. + bool IsFulfilled(const RendererContentMatchData& renderer_data) const; + + // Returns a URLMatcherConditionSet::ID which is the canonical representation + // for all URL patterns that need to be matched by this ContentCondition. + // This ID is registered in a URLMatcher that can inform us in case of a + // match. + URLMatcherConditionSet::ID url_matcher_condition_set_id() const { + return url_matcher_conditions_->id(); + } + + // If this Condition has a url filter, appends it to |condition_sets|. + void GetURLMatcherConditionSets( + URLMatcherConditionSet::Vector* condition_sets) const { + if (url_matcher_conditions_) + condition_sets->push_back(url_matcher_conditions_); + } + + // True if GetURLMatcherConditionSets would append anything to its + // argument. + bool has_url_matcher_condition_set() const { + return url_matcher_conditions_ != NULL; + } + + // Returns the CSS selectors required to match by this condition. + const std::vector<std::string>& css_selectors() const { + return css_selectors_; + } + + private: + scoped_refptr<URLMatcherConditionSet> url_matcher_conditions_; + std::vector<std::string> css_selectors_; + + DISALLOW_COPY_AND_ASSIGN(ContentCondition); +}; + +typedef DeclarativeConditionSet<ContentCondition> ContentConditionSet; + +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONDITION_H_ diff --git a/chrome/browser/extensions/api/declarative_content/content_condition_unittest.cc b/chrome/browser/extensions/api/declarative_content/content_condition_unittest.cc new file mode 100644 index 0000000..a26bf4c --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_condition_unittest.cc @@ -0,0 +1,97 @@ +// Copyright (c) 2012 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/api/declarative_content/content_condition.h" + +#include <set> + +#include "base/message_loop.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "chrome/browser/extensions/api/declarative_content/content_constants.h" +#include "chrome/common/extensions/matcher/url_matcher.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace extensions { +namespace { + +using testing::ElementsAre; +using testing::HasSubstr; + +TEST(DeclarativeContentConditionTest, ErroneousCondition) { + URLMatcher matcher; + + std::string error; + scoped_ptr<ContentCondition> result; + + // Test unknown condition name passed. + error.clear(); + result = ContentCondition::Create( + matcher.condition_factory(), + *base::test::ParseJson( + "{\n" + " \"invalid\": \"foobar\",\n" + " \"instanceType\": \"declarativeContent.PageStateMatcher\",\n" + "}"), + &error); + EXPECT_THAT(error, HasSubstr("Unknown condition attribute")); + EXPECT_FALSE(result); + + // Test wrong datatype in pageUrl. + error.clear(); + result = ContentCondition::Create( + matcher.condition_factory(), + *base::test::ParseJson( + "{\n" + " \"pageUrl\": [],\n" + " \"instanceType\": \"declarativeContent.PageStateMatcher\",\n" + "}"), + &error); + EXPECT_THAT(error, HasSubstr("invalid type")); + EXPECT_FALSE(result); + + EXPECT_TRUE(matcher.IsEmpty()) << "Errors shouldn't add URL conditions"; +} + +TEST(DeclarativeContentConditionTest, ConditionWithUrlAndCss) { + URLMatcher matcher; + + std::string error; + scoped_ptr<ContentCondition> result = ContentCondition::Create( + matcher.condition_factory(), + *base::test::ParseJson( + "{\n" + " \"instanceType\": \"declarativeContent.PageStateMatcher\",\n" + " \"pageUrl\": {\"hostSuffix\": \"example.com\"},\n" + " \"css\": [\"input\"],\n" + "}"), + &error); + EXPECT_EQ("", error); + ASSERT_TRUE(result); + + URLMatcherConditionSet::Vector all_new_condition_sets; + result->GetURLMatcherConditionSets(&all_new_condition_sets); + matcher.AddConditionSets(all_new_condition_sets); + EXPECT_FALSE(matcher.IsEmpty()); + + RendererContentMatchData match_data; + match_data.css_selectors.insert("input"); + + EXPECT_THAT(matcher.MatchURL(GURL("http://google.com/")), + ElementsAre(/*empty*/)); + match_data.page_url_matches = matcher.MatchURL( + GURL("http://www.example.com/foobar")); + EXPECT_THAT(match_data.page_url_matches, + ElementsAre(result->url_matcher_condition_set_id())); + + EXPECT_TRUE(result->IsFulfilled(match_data)); + + match_data.css_selectors.clear(); + match_data.css_selectors.insert("body"); + EXPECT_FALSE(result->IsFulfilled(match_data)); +} + +} // namespace +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_constants.cc b/chrome/browser/extensions/api/declarative_content/content_constants.cc new file mode 100644 index 0000000..dda4f93 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_constants.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2012 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/api/declarative_content/content_constants.h" + +namespace extensions { +namespace declarative_content_constants { + +// Signals to which ContentRulesRegistries are registered. +const char kOnPageChanged[] = "declarativeContent.onPageChanged"; + +// Keys of dictionaries. +const char kCss[] = "css"; +const char kInstanceType[] = "instanceType"; +const char kPageUrl[] = "pageUrl"; + +// Values of dictionaries, in particular instance types +const char kPageStateMatcherType[] = "declarativeContent.PageStateMatcher"; +const char kShowPageAction[] = "declarativeContent.ShowPageAction"; + +} // namespace declarative_content_constants +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_constants.h b/chrome/browser/extensions/api/declarative_content/content_constants.h new file mode 100644 index 0000000..cae250b --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_constants.h @@ -0,0 +1,28 @@ +// Copyright (c) 2012 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. + +// Constants used for the declarativeContent API. + +#ifndef CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONSTANTS_H_ +#define CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONSTANTS_H_ + +namespace extensions { +namespace declarative_content_constants { + +// Signals to which ContentRulesRegistries are registered. +extern const char kOnPageChanged[]; + +// Keys of dictionaries. +extern const char kCss[]; +extern const char kInstanceType[]; +extern const char kPageUrl[]; + +// Values of dictionaries, in particular instance types +extern const char kPageStateMatcherType[]; +extern const char kShowPageAction[]; + +} // namespace declarative_content_constants +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_CONSTANTS_H_ diff --git a/chrome/browser/extensions/api/declarative_content/content_rules_registry.cc b/chrome/browser/extensions/api/declarative_content/content_rules_registry.cc new file mode 100644 index 0000000..53bae93 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_rules_registry.cc @@ -0,0 +1,276 @@ +// Copyright (c) 2012 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/api/declarative_content/content_rules_registry.h" + +#include "chrome/browser/extensions/api/declarative_content/content_action.h" +#include "chrome/browser/extensions/api/declarative_content/content_condition.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/extension_tab_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/extensions/extension_messages.h" +#include "content/public/browser/navigation_details.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" + +namespace extensions { + +ContentRulesRegistry::ContentRulesRegistry(Profile* profile, Delegate* delegate) + : RulesRegistryWithCache(delegate), + profile_(profile) { + extension_info_map_ = ExtensionSystem::Get(profile)->info_map(); + + registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED, + content::NotificationService::AllBrowserContextsAndSources()); +} + +void ContentRulesRegistry::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + switch (type) { + case content::NOTIFICATION_RENDERER_PROCESS_CREATED: + content::RenderProcessHost* process = + content::Source<content::RenderProcessHost>(source).ptr(); + if (process->GetBrowserContext() == profile_) + InstructRenderProcess(process); + break; + } +} + +void ContentRulesRegistry::Apply( + content::WebContents* contents, + const std::vector<std::string>& matching_css_selectors) { + const int tab_id = ExtensionTabUtil::GetTabId(contents); + RendererContentMatchData renderer_data; + renderer_data.page_url_matches = url_matcher_.MatchURL(contents->GetURL()); + renderer_data.css_selectors.insert(matching_css_selectors.begin(), + matching_css_selectors.end()); + std::set<ContentRule*> matching_rules = GetMatches(renderer_data); + std::set<ContentRule*>& prev_matching_rules = active_rules_[tab_id]; + ContentAction::ApplyInfo apply_info = { + profile_, contents + }; + for (std::set<ContentRule*>::const_iterator it = matching_rules.begin(); + it != matching_rules.end(); ++it) { + if (!ContainsKey(prev_matching_rules, *it)) + (*it)->actions().Apply((*it)->extension_id(), base::Time(), &apply_info); + } + for (std::set<ContentRule*>::const_iterator it = prev_matching_rules.begin(); + it != prev_matching_rules.end(); ++it) { + if (!ContainsKey(matching_rules, *it)) + (*it)->actions().Revert((*it)->extension_id(), base::Time(), &apply_info); + } + swap(matching_rules, prev_matching_rules); +} + +void ContentRulesRegistry::DidNavigateMainFrame( + content::WebContents* contents, + const content::LoadCommittedDetails& details, + const content::FrameNavigateParams& params) { + if (details.is_in_page) { + // Within-page navigations don't change the set of elements that + // exist, and we only support filtering on the top-level URL, so + // this can't change which rules match. + return; + } + + // Top-level navigation produces a new document. Initially, the + // document's empty, so no CSS rules match. The renderer will send + // an ExtensionHostMsg_OnWatchedPageChange later if any CSS rules + // match. + std::vector<std::string> no_css_selectors; + Apply(contents, no_css_selectors); +} + +std::set<ContentRule*> +ContentRulesRegistry::GetMatches( + const RendererContentMatchData& renderer_data) const { + std::set<ContentRule*> result; + + // Then we need to check for each of these, whether the other + // attributes are also fulfilled. + for (std::set<URLMatcherConditionSet::ID>::iterator + url_match = renderer_data.page_url_matches.begin(); + url_match != renderer_data.page_url_matches.end(); ++url_match) { + URLMatcherIdToRule::const_iterator rule_iter = + match_id_to_rule_.find(*url_match); + CHECK(rule_iter != match_id_to_rule_.end()); + + ContentRule* rule = rule_iter->second; + if (rule->conditions().IsFulfilled(*url_match, renderer_data)) + result.insert(rule); + } + return result; +} + +std::string ContentRulesRegistry::AddRulesImpl( + const std::string& extension_id, + const std::vector<linked_ptr<RulesRegistry::Rule> >& rules) { + base::Time extension_installation_time = + GetExtensionInstallationTime(extension_id); + + std::string error; + RulesMap new_content_rules; + + for (std::vector<linked_ptr<RulesRegistry::Rule> >::const_iterator rule = + rules.begin(); rule != rules.end(); ++rule) { + ContentRule::GlobalRuleId rule_id(extension_id, *(*rule)->id); + DCHECK(content_rules_.find(rule_id) == content_rules_.end()); + + scoped_ptr<ContentRule> content_rule( + ContentRule::Create(url_matcher_.condition_factory(), extension_id, + extension_installation_time, *rule, NULL, &error)); + if (!error.empty()) { + // Clean up temporary condition sets created during rule creation. + url_matcher_.ClearUnusedConditionSets(); + return error; + } + DCHECK(content_rule); + + new_content_rules[rule_id] = make_linked_ptr(content_rule.release()); + } + + // Wohoo, everything worked fine. + content_rules_.insert(new_content_rules.begin(), new_content_rules.end()); + + // Create the triggers. + for (RulesMap::iterator i = new_content_rules.begin(); + i != new_content_rules.end(); ++i) { + URLMatcherConditionSet::Vector url_condition_sets; + const ContentConditionSet& conditions = i->second->conditions(); + conditions.GetURLMatcherConditionSets(&url_condition_sets); + for (URLMatcherConditionSet::Vector::iterator j = + url_condition_sets.begin(); j != url_condition_sets.end(); ++j) { + match_id_to_rule_[(*j)->id()] = i->second.get(); + } + } + + // Register url patterns in url_matcher_. + URLMatcherConditionSet::Vector all_new_condition_sets; + for (RulesMap::iterator i = new_content_rules.begin(); + i != new_content_rules.end(); ++i) { + i->second->conditions().GetURLMatcherConditionSets(&all_new_condition_sets); + } + url_matcher_.AddConditionSets(all_new_condition_sets); + + UpdateConditionCache(); + + return ""; +} + +std::string ContentRulesRegistry::RemoveRulesImpl( + const std::string& extension_id, + const std::vector<std::string>& rule_identifiers) { + // URLMatcherConditionSet IDs that can be removed from URLMatcher. + std::vector<URLMatcherConditionSet::ID> remove_from_url_matcher; + + for (std::vector<std::string>::const_iterator i = rule_identifiers.begin(); + i != rule_identifiers.end(); ++i) { + ContentRule::GlobalRuleId rule_id(extension_id, *i); + + // Skip unknown rules. + RulesMap::iterator content_rules_entry = content_rules_.find(rule_id); + if (content_rules_entry == content_rules_.end()) + continue; + + // Remove all triggers but collect their IDs. + URLMatcherConditionSet::Vector condition_sets; + ContentRule* rule = content_rules_entry->second.get(); + rule->conditions().GetURLMatcherConditionSets(&condition_sets); + for (URLMatcherConditionSet::Vector::iterator j = condition_sets.begin(); + j != condition_sets.end(); ++j) { + remove_from_url_matcher.push_back((*j)->id()); + match_id_to_rule_.erase((*j)->id()); + } + + // Remove the ContentRule from active_rules_. + for (std::map<int, std::set<ContentRule*> >::iterator + it = active_rules_.begin(); + it != active_rules_.end(); ++it) { + // Has no effect if the rule wasn't present. + it->second.erase(rule); + } + + // Remove reference to actual rule. + content_rules_.erase(content_rules_entry); + } + + // Clear URLMatcher based on condition_set_ids that are not needed any more. + url_matcher_.RemoveConditionSets(remove_from_url_matcher); + + UpdateConditionCache(); + + return ""; +} + +std::string ContentRulesRegistry::RemoveAllRulesImpl( + const std::string& extension_id) { + // Search all identifiers of rules that belong to extension |extension_id|. + std::vector<std::string> rule_identifiers; + for (RulesMap::iterator i = content_rules_.begin(); + i != content_rules_.end(); ++i) { + const ContentRule::GlobalRuleId& global_rule_id = i->first; + if (global_rule_id.first == extension_id) + rule_identifiers.push_back(global_rule_id.second); + } + + return RemoveRulesImpl(extension_id, rule_identifiers); +} + +void ContentRulesRegistry::UpdateConditionCache() { + std::set<std::string> css_selectors; // We rely on this being sorted. + for (RulesMap::const_iterator i = content_rules_.begin(); + i != content_rules_.end(); ++i) { + ContentRule& rule = *i->second; + for (ContentConditionSet::const_iterator + condition = rule.conditions().begin(); + condition != rule.conditions().end(); ++condition) { + const std::vector<std::string>& condition_css_selectors = + (*condition)->css_selectors(); + css_selectors.insert(condition_css_selectors.begin(), + condition_css_selectors.end()); + } + } + + if (css_selectors.size() != watched_css_selectors_.size() || + !std::equal(css_selectors.begin(), css_selectors.end(), + watched_css_selectors_.begin())) { + watched_css_selectors_.assign(css_selectors.begin(), css_selectors.end()); + + for (content::RenderProcessHost::iterator it( + content::RenderProcessHost::AllHostsIterator()); + !it.IsAtEnd(); it.Advance()) { + content::RenderProcessHost* process = it.GetCurrentValue(); + if (process->GetBrowserContext() == profile_) + InstructRenderProcess(process); + } + } +} + +void ContentRulesRegistry::InstructRenderProcess( + content::RenderProcessHost* process) { + process->Send(new ExtensionMsg_WatchPages(watched_css_selectors_)); +} + +content::BrowserThread::ID ContentRulesRegistry::GetOwnerThread() const { + return content::BrowserThread::UI; +} + +bool ContentRulesRegistry::IsEmpty() const { + return match_id_to_rule_.empty() && content_rules_.empty() && + url_matcher_.IsEmpty(); +} + +ContentRulesRegistry::~ContentRulesRegistry() {} + +base::Time ContentRulesRegistry::GetExtensionInstallationTime( + const std::string& extension_id) const { + if (!extension_info_map_.get()) // May be NULL during testing. + return base::Time(); + + return extension_info_map_->GetInstallTime(extension_id); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/declarative_content/content_rules_registry.h b/chrome/browser/extensions/api/declarative_content/content_rules_registry.h new file mode 100644 index 0000000..16e06f7 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/content_rules_registry.h @@ -0,0 +1,145 @@ +// Copyright (c) 2012 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. + +#ifndef CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_RULES_REGISTRY_H_ +#define CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_RULES_REGISTRY_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/memory/linked_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/time.h" +#include "chrome/browser/extensions/api/declarative/declarative_rule.h" +#include "chrome/browser/extensions/api/declarative/rules_registry_with_cache.h" +#include "chrome/browser/extensions/api/declarative_content/content_action.h" +#include "chrome/browser/extensions/api/declarative_content/content_condition.h" +#include "chrome/browser/extensions/extension_info_map.h" +#include "chrome/common/extensions/matcher/url_matcher.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +class Profile; +class ContentPermissions; + +namespace content { +class RenderProcessHost; +class WebContents; +struct FrameNavigateParams; +struct LoadCommittedDetails; +} + +namespace extension_web_request_api_helpers { +struct EventResponseDelta; +} + +namespace net { +class URLRequest; +} + +namespace extensions { + +class RulesRegistryService; + +typedef DeclarativeRule<ContentCondition, ContentAction> ContentRule; + +// The ContentRulesRegistry is responsible for managing +// the internal representation of rules for the Declarative Content API. +// +// Here is the high level overview of this functionality: +// +// RulesRegistry::Rule consists of Conditions and Actions, these are +// represented as a ContentRule with ContentConditions and +// ContentRuleActions. +// +// The evaluation of URL related condition attributes (host_suffix, path_prefix) +// is delegated to a URLMatcher, because this is capable of evaluating many +// of such URL related condition attributes in parallel. +class ContentRulesRegistry : public RulesRegistryWithCache, + public content::NotificationObserver { + public: + ContentRulesRegistry(Profile* profile, Delegate* delegate); + + // Applies all content rules given an update (CSS match change or + // page navigation, for now) from the renderer. + void Apply(content::WebContents* contents, + const std::vector<std::string>& matching_css_selectors); + + // Applies all content rules given that a tab was just navigated. + void DidNavigateMainFrame(content::WebContents* tab, + const content::LoadCommittedDetails& details, + const content::FrameNavigateParams& params); + + // Implementation of RulesRegistryWithCache: + virtual std::string AddRulesImpl( + const std::string& extension_id, + const std::vector<linked_ptr<RulesRegistry::Rule> >& rules) OVERRIDE; + virtual std::string RemoveRulesImpl( + const std::string& extension_id, + const std::vector<std::string>& rule_identifiers) OVERRIDE; + virtual std::string RemoveAllRulesImpl( + const std::string& extension_id) OVERRIDE; + virtual content::BrowserThread::ID GetOwnerThread() const OVERRIDE; + + // content::NotificationObserver implementation. + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // Returns true if this object retains no allocated data. Only for debugging. + bool IsEmpty() const; + + protected: + virtual ~ContentRulesRegistry(); + + // Virtual for testing: + virtual base::Time GetExtensionInstallationTime( + const std::string& extension_id) const; + + private: + std::set<ContentRule*> + GetMatches(const RendererContentMatchData& renderer_data) const; + + // Scans the rules for the set of conditions they're watching. If the set has + // changed, calls InstructRenderProcess() for each RenderProcessHost in the + // current profile. + void UpdateConditionCache(); + + // Tells a renderer what page attributes to watch for using an + // ExtensionMsg_WatchPages. + void InstructRenderProcess(content::RenderProcessHost* process); + + typedef std::map<URLMatcherConditionSet::ID, ContentRule*> URLMatcherIdToRule; + typedef std::map<ContentRule::GlobalRuleId, linked_ptr<ContentRule> > + RulesMap; + + Profile* const profile_; + + // Map that tells us which ContentRules may match under the condition that + // the URLMatcherConditionSet::ID was returned by the |url_matcher_|. + URLMatcherIdToRule match_id_to_rule_; + + RulesMap content_rules_; + + // Maps tab_id to the set of rules that match on that tab. This + // lets us call Revert as appropriate. + std::map<int, std::set<ContentRule*> > active_rules_; + + // Matches URLs for the page_url condition. + URLMatcher url_matcher_; + + // All CSS selectors any rule's conditions watch for. + std::vector<std::string> watched_css_selectors_; + + // Manages our notification registrations. + content::NotificationRegistrar registrar_; + + scoped_refptr<ExtensionInfoMap> extension_info_map_; +}; + +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_DECLARATIVE_CONTENT_CONTENT_RULES_REGISTRY_H_ diff --git a/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc b/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc new file mode 100644 index 0000000..813b6b9 --- /dev/null +++ b/chrome/browser/extensions/api/declarative_content/declarative_content_apitest.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2012 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/extension_action.h" +#include "chrome/browser/extensions/extension_action_manager.h" +#include "chrome/browser/extensions/extension_apitest.h" +#include "chrome/browser/extensions/extension_tab_util.h" +#include "chrome/browser/extensions/extension_test_message_listener.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/extensions/features/feature.h" +#include "content/public/test/browser_test_utils.h" + +namespace extensions { +namespace { + +class DeclarativeContentApiTest : public ExtensionApiTest { + public: + DeclarativeContentApiTest() + // Set the channel to "trunk" since declarativeContent is restricted + // to trunk. + : current_channel_(chrome::VersionInfo::CHANNEL_UNKNOWN) { + } + virtual ~DeclarativeContentApiTest() {} + + extensions::Feature::ScopedCurrentChannel current_channel_; +}; + +IN_PROC_BROWSER_TEST_F(DeclarativeContentApiTest, Overview) { + ExtensionTestMessageListener ready("ready", true); + const Extension* extension = + LoadExtension(test_data_dir_.AppendASCII("declarative_content/overview")); + ASSERT_TRUE(extension); + const ExtensionAction* page_action = + ExtensionActionManager::Get(browser()->profile())-> + GetPageAction(*extension); + ASSERT_TRUE(page_action); + + ASSERT_TRUE(ready.WaitUntilSatisfied()); + content::WebContents* const tab = + browser()->tab_strip_model()->GetWebContentsAt(0); + const int tab_id = ExtensionTabUtil::GetTabId(tab); + + NavigateInRenderer(tab, GURL("http://test1/")); + + // The declarative API should show the page action instantly, rather + // than waiting for the extension to run. + EXPECT_TRUE(page_action->GetIsVisible(tab_id)); + + // Make sure leaving a matching page unshows the page action. + NavigateInRenderer(tab, GURL("http://not_checked/")); + EXPECT_FALSE(page_action->GetIsVisible(tab_id)); + + // Insert a password field to make sure that's noticed. + ASSERT_TRUE(content::ExecuteScript( + tab, "document.body.innerHTML = '<input type=\"password\">';")); + // Give the mutation observer a chance to run and send back the + // matching-selector update. + ASSERT_TRUE(content::ExecuteScript(tab, "")); + EXPECT_TRUE(page_action->GetIsVisible(tab_id)); + + // Remove it again to make sure that reverts the action. + ASSERT_TRUE(content::ExecuteScript( + tab, "document.body.innerHTML = 'Hello world';")); + // Give the mutation observer a chance to run and send back the + // matching-selector update. + ASSERT_TRUE(content::ExecuteScript(tab, "")); + EXPECT_FALSE(page_action->GetIsVisible(tab_id)); +} + +} // namespace +} // namespace extensions diff --git a/chrome/browser/extensions/extension_action.cc b/chrome/browser/extensions/extension_action.cc index c193f4c..1d7648a 100644 --- a/chrome/browser/extensions/extension_action.cc +++ b/chrome/browser/extensions/extension_action.cc @@ -272,6 +272,18 @@ bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) { return true; } +void ExtensionAction::DeclarativeShow(int tab_id) { + DCHECK_NE(tab_id, kDefaultTabId); + ++declarative_show_count_[tab_id]; // Use default initialization to 0. +} + +void ExtensionAction::UndoDeclarativeShow(int tab_id) { + int& show_count = declarative_show_count_[tab_id]; + DCHECK_GT(show_count, 0); + if (--show_count == 0) + declarative_show_count_.erase(tab_id); +} + void ExtensionAction::ClearAllValuesForTab(int tab_id) { popup_url_.erase(tab_id); title_.erase(tab_id); @@ -280,6 +292,10 @@ void ExtensionAction::ClearAllValuesForTab(int tab_id) { badge_text_color_.erase(tab_id); badge_background_color_.erase(tab_id); appearance_.erase(tab_id); + // TODO(jyasskin): Erase the element from declarative_show_count_ + // when the tab's closed. There's a race between the + // PageActionController and the ContentRulesRegistry on navigation, + // which prevents me from cleaning everything up now. icon_animation_.erase(tab_id); } diff --git a/chrome/browser/extensions/extension_action.h b/chrome/browser/extensions/extension_action.h index dacd961..943a3eb 100644 --- a/chrome/browser/extensions/extension_action.h +++ b/chrome/browser/extensions/extension_action.h @@ -220,15 +220,20 @@ class ExtensionAction { // care of any appropriate transition animations. Returns true if // the appearance has changed. bool SetAppearance(int tab_id, Appearance value); + // The declarative appearance overrides a default appearance but is overridden + // by an appearance set directly on the tab. + void DeclarativeShow(int tab_id); + void UndoDeclarativeShow(int tab_id); + // Get the badge visibility for a tab, or the default badge visibility // if none was set. bool GetIsVisible(int tab_id) const { - return GetValue(&appearance_, tab_id) != INVISIBLE; + return GetAppearance(tab_id) != INVISIBLE; } // True if the tab's action wants the user's attention. bool WantsAttention(int tab_id) const { - return GetValue(&appearance_, tab_id) == WANTS_ATTENTION; + return GetAppearance(tab_id) == WANTS_ATTENTION; } // Remove all tab-specific state. @@ -272,17 +277,47 @@ class ExtensionAction { (*map)[tab_id] = val; } + template<class Map> + static const typename Map::mapped_type* FindOrNull( + const Map* map, + const typename Map::key_type& key) { + typename Map::const_iterator iter = map->find(key); + if (iter == map->end()) + return NULL; + return &iter->second; + } + template<class T> T GetValue(const std::map<int, T>* map, int tab_id) const { - typename std::map<int, T>::const_iterator iter = map->find(tab_id); - if (iter != map->end()) { - return iter->second; + if (const T* tab_value = FindOrNull(map, tab_id)) { + return *tab_value; + } else if (const T* default_value = FindOrNull(map, kDefaultTabId)) { + return *default_value; } else { - iter = map->find(kDefaultTabId); - return iter != map->end() ? iter->second : ValueTraits<T>::CreateEmpty(); + return ValueTraits<T>::CreateEmpty(); } } + // Gets the appearance of |tab_id|. Returns the first of: a specific + // appearance set on the tab; a declarative appearance set on the tab; the + // default appearance set for all tabs; or INVISIBLE. Don't return this + // result to an extension's background page because the declarative state can + // leak information about hosts the extension doesn't have permission to + // access. + Appearance GetAppearance(int tab_id) const { + if (const Appearance* tab_appearance = FindOrNull(&appearance_, tab_id)) + return *tab_appearance; + + if (ContainsKey(declarative_show_count_, tab_id)) + return ACTIVE; + + if (const Appearance* default_appearance = + FindOrNull(&appearance_, kDefaultTabId)) + return *default_appearance; + + return INVISIBLE; + } + // The id for the extension this action belongs to (as defined in the // extension manifest). const std::string extension_id_; @@ -299,6 +334,17 @@ class ExtensionAction { std::map<int, SkColor> badge_text_color_; std::map<int, Appearance> appearance_; + // Declarative state exists for two reasons: First, we need to hide it from + // the extension's background/event page to avoid leaking data from hosts the + // extension doesn't have permission to access. Second, the action's state + // gets both reset and given its declarative values in response to a + // WebContentsObserver::DidNavigateMainFrame event, and there's no way to set + // those up to be called in the right order. + + // Maps tab_id to the number of active (applied-but-not-reverted) + // declarativeContent.ShowPageAction actions. + std::map<int, int> declarative_show_count_; + // IconAnimations are destroyed by a delayed task on the UI message loop so // that even if the Extension and ExtensionAction are destroyed on a non-UI // thread, the animation will still only be touched from the UI thread. This diff --git a/chrome/browser/extensions/tab_helper.cc b/chrome/browser/extensions/tab_helper.cc index 8fe31dd..7763f1a 100644 --- a/chrome/browser/extensions/tab_helper.cc +++ b/chrome/browser/extensions/tab_helper.cc @@ -5,11 +5,14 @@ #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/extensions/activity_log.h" +#include "chrome/browser/extensions/api/declarative/rules_registry_service.h" +#include "chrome/browser/extensions/api/declarative_content/content_rules_registry.h" #include "chrome/browser/extensions/app_notify_channel_ui.h" #include "chrome/browser/extensions/crx_installer.h" #include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action_manager.h" #include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/extensions/image_loader.h" #include "chrome/browser/extensions/page_action_controller.h" @@ -84,6 +87,10 @@ TabHelper::TabHelper(content::WebContents* web_contents) pending_web_app_action_(NONE), script_executor_(new ScriptExecutor(web_contents, &script_execution_observers_)), + rules_registry_service_( + ExtensionSystem::Get( + Profile::FromBrowserContext(web_contents->GetBrowserContext()))-> + rules_registry_service()), ALLOW_THIS_IN_INITIALIZER_LIST(image_loader_ptr_factory_(this)) { // The ActiveTabPermissionManager requires a session ID; ensure this // WebContents has one. @@ -188,6 +195,13 @@ void TabHelper::RenderViewCreated(RenderViewHost* render_view_host) { void TabHelper::DidNavigateMainFrame( const content::LoadCommittedDetails& details, const content::FrameNavigateParams& params) { +#if defined(ENABLE_EXTENSIONS) + if (rules_registry_service_) { + rules_registry_service_->content_rules_registry()->DidNavigateMainFrame( + web_contents(), details, params); + } +#endif // defined(ENABLE_EXTENSIONS) + if (details.is_in_page) return; @@ -229,6 +243,8 @@ bool TabHelper::OnMessageReceived(const IPC::Message& message) { IPC_MESSAGE_HANDLER(ExtensionHostMsg_Request, OnRequest) IPC_MESSAGE_HANDLER(ExtensionHostMsg_ContentScriptsExecuting, OnContentScriptsExecuting) + IPC_MESSAGE_HANDLER(ExtensionHostMsg_OnWatchedPageChange, + OnWatchedPageChange) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; @@ -417,6 +433,16 @@ void TabHelper::OnContentScriptsExecuting( on_url)); } +void TabHelper::OnWatchedPageChange( + const std::vector<std::string>& css_selectors) { +#if defined(ENABLE_EXTENSIONS) + if (rules_registry_service_) { + rules_registry_service_->content_rules_registry()->Apply( + web_contents(), css_selectors); + } +#endif // defined(ENABLE_EXTENSIONS) +} + const Extension* TabHelper::GetExtension(const std::string& extension_app_id) { if (extension_app_id.empty()) return NULL; diff --git a/chrome/browser/extensions/tab_helper.h b/chrome/browser/extensions/tab_helper.h index 8e3e00e..539871f 100644 --- a/chrome/browser/extensions/tab_helper.h +++ b/chrome/browser/extensions/tab_helper.h @@ -7,6 +7,8 @@ #include <map> #include <string> +#include <vector> + #include "base/memory/ref_counted.h" #include "base/memory/weak_ptr.h" #include "base/observer_list.h" @@ -30,6 +32,7 @@ struct LoadCommittedDetails; namespace extensions { class Extension; class LocationBarController; +class RulesRegistryService; class ScriptBadgeController; class ScriptBubbleController; class ScriptExecutor; @@ -192,6 +195,7 @@ class TabHelper : public content::WebContentsObserver, const ScriptExecutionObserver::ExecutingScriptsMap& extension_ids, int32 page_id, const GURL& on_url); + void OnWatchedPageChange(const std::vector<std::string>& css_selectors); // App extensions related methods: @@ -259,6 +263,8 @@ class TabHelper : public content::WebContentsObserver, scoped_ptr<ScriptBubbleController> script_bubble_controller_; + RulesRegistryService* rules_registry_service_; + Profile* profile_; // Vend weak pointers that can be invalidated to stop in-progress loads. |