Decouple URL matching from CSS and bookmark state matching in ChromeContentRulesRegistry
[chromium-blink-merge.git] / chrome / browser / extensions / api / declarative_content / chrome_content_rules_registry.cc
blob253e1715de62813dfd73eeb71d5fedcf8990d9c0
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/extensions/api/declarative_content/chrome_content_rules_registry.h"
7 #include <utility>
9 #include "chrome/browser/chrome_notification_types.h"
10 #include "chrome/browser/extensions/api/declarative_content/content_action.h"
11 #include "chrome/browser/extensions/api/declarative_content/content_condition.h"
12 #include "chrome/browser/extensions/api/declarative_content/content_constants.h"
13 #include "chrome/browser/extensions/extension_util.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_iterator.h"
17 #include "chrome/browser/ui/tabs/tab_strip_model.h"
18 #include "content/public/browser/navigation_details.h"
19 #include "content/public/browser/notification_service.h"
20 #include "content/public/browser/notification_source.h"
21 #include "content/public/browser/web_contents.h"
22 #include "extensions/browser/api/declarative/rules_registry_service.h"
23 #include "extensions/browser/extension_registry.h"
24 #include "extensions/browser/extension_system.h"
26 using url_matcher::URLMatcherConditionSet;
28 namespace extensions {
30 namespace {
33 } // namespace
36 // EvaluationScope
39 // Used to coalesce multiple requests for evaluation into a zero or one actual
40 // evaluations (depending on the EvaluationDisposition). This is required for
41 // correctness when multiple trackers respond to the same event. Otherwise,
42 // executing the request from the first tracker will be done before the tracked
43 // state has been updated for the other trackers.
44 class ChromeContentRulesRegistry::EvaluationScope {
45 public:
46 // Default disposition is PERFORM_EVALUATION.
47 explicit EvaluationScope(ChromeContentRulesRegistry* registry);
48 EvaluationScope(ChromeContentRulesRegistry* registry,
49 EvaluationDisposition disposition);
50 ~EvaluationScope();
52 private:
53 ChromeContentRulesRegistry* const registry_;
54 const EvaluationDisposition previous_disposition_;
56 DISALLOW_COPY_AND_ASSIGN(EvaluationScope);
59 ChromeContentRulesRegistry::EvaluationScope::EvaluationScope(
60 ChromeContentRulesRegistry* registry)
61 : EvaluationScope(registry, DEFER_REQUESTS) {}
63 ChromeContentRulesRegistry::EvaluationScope::EvaluationScope(
64 ChromeContentRulesRegistry* registry,
65 EvaluationDisposition disposition)
66 : registry_(registry),
67 previous_disposition_(registry_->evaluation_disposition_) {
68 DCHECK_NE(EVALUATE_REQUESTS, disposition);
69 registry_->evaluation_disposition_ = disposition;
72 ChromeContentRulesRegistry::EvaluationScope::~EvaluationScope() {
73 registry_->evaluation_disposition_ = previous_disposition_;
74 if (registry_->evaluation_disposition_ == EVALUATE_REQUESTS) {
75 for (content::WebContents* tab : registry_->evaluation_pending_)
76 registry_->EvaluateConditionsForTab(tab);
77 registry_->evaluation_pending_.clear();
82 // ChromeContentRulesRegistry
85 ChromeContentRulesRegistry::ChromeContentRulesRegistry(
86 content::BrowserContext* browser_context,
87 RulesCacheDelegate* cache_delegate)
88 : ContentRulesRegistry(browser_context,
89 declarative_content_constants::kOnPageChanged,
90 content::BrowserThread::UI,
91 cache_delegate,
92 RulesRegistryService::kDefaultRulesRegistryID),
93 page_url_condition_tracker_(browser_context, this),
94 css_condition_tracker_(browser_context, this),
95 is_bookmarked_condition_tracker_(browser_context, this),
96 evaluation_disposition_(EVALUATE_REQUESTS) {
97 registrar_.Add(this,
98 content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
99 content::NotificationService::AllBrowserContextsAndSources());
102 void ChromeContentRulesRegistry::Observe(
103 int type,
104 const content::NotificationSource& source,
105 const content::NotificationDetails& details) {
106 switch (type) {
107 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: {
108 content::WebContents* tab =
109 content::Source<content::WebContents>(source).ptr();
110 // Note that neither non-tab WebContents nor tabs from other browser
111 // contexts will be in the map.
112 active_rules_.erase(tab);
113 break;
118 void ChromeContentRulesRegistry::RequestEvaluation(
119 content::WebContents* contents) {
120 switch (evaluation_disposition_) {
121 case EVALUATE_REQUESTS:
122 EvaluateConditionsForTab(contents);
123 break;
124 case DEFER_REQUESTS:
125 evaluation_pending_.insert(contents);
126 break;
127 case IGNORE_REQUESTS:
128 break;
132 bool ChromeContentRulesRegistry::ShouldManageConditionsForBrowserContext(
133 content::BrowserContext* context) {
134 return ManagingRulesForBrowserContext(context);
137 void ChromeContentRulesRegistry::MonitorWebContentsForRuleEvaluation(
138 content::WebContents* contents) {
139 // We rely on active_rules_ to have a key-value pair for |contents| to know
140 // which WebContents we are working with.
141 active_rules_[contents] = std::set<const ContentRule*>();
143 EvaluationScope evaluation_scope(this);
144 page_url_condition_tracker_.TrackForWebContents(contents);
145 css_condition_tracker_.TrackForWebContents(contents);
146 is_bookmarked_condition_tracker_.TrackForWebContents(contents);
149 void ChromeContentRulesRegistry::DidNavigateMainFrame(
150 content::WebContents* contents,
151 const content::LoadCommittedDetails& details,
152 const content::FrameNavigateParams& params) {
153 if (ContainsKey(active_rules_, contents)) {
154 EvaluationScope evaluation_scope(this);
155 page_url_condition_tracker_.OnWebContentsNavigation(contents, details,
156 params);
157 css_condition_tracker_.OnWebContentsNavigation(contents, details, params);
158 is_bookmarked_condition_tracker_.OnWebContentsNavigation(contents, details,
159 params);
163 ChromeContentRulesRegistry::ContentRule::ContentRule(
164 const Extension* extension,
165 ScopedVector<const ContentCondition> conditions,
166 ScopedVector<const ContentAction> actions,
167 int priority)
168 : extension(extension),
169 conditions(conditions.Pass()),
170 actions(actions.Pass()),
171 priority(priority) {
174 ChromeContentRulesRegistry::ContentRule::~ContentRule() {}
176 scoped_ptr<const ChromeContentRulesRegistry::ContentRule>
177 ChromeContentRulesRegistry::CreateRule(const Extension* extension,
178 const api::events::Rule& api_rule,
179 std::string* error) {
180 ScopedVector<const ContentCondition> conditions;
181 for (const linked_ptr<base::Value>& value : api_rule.conditions) {
182 conditions.push_back(
183 ContentCondition::Create(
184 extension,
185 page_url_condition_tracker_.condition_factory(),
186 *value,
187 error));
188 if (!error->empty())
189 return scoped_ptr<ContentRule>();
192 ScopedVector<const ContentAction> actions;
193 for (const linked_ptr<base::Value>& value : api_rule.actions) {
194 actions.push_back(ContentAction::Create(browser_context(), extension,
195 *value, error));
196 if (!error->empty())
197 return scoped_ptr<ContentRule>();
200 // Note: |api_rule| may contain tags, but these are ignored.
202 return make_scoped_ptr(
203 new ContentRule(extension, conditions.Pass(), actions.Pass(),
204 *api_rule.priority));
207 bool ChromeContentRulesRegistry::ManagingRulesForBrowserContext(
208 content::BrowserContext* context) {
209 // Manage both the normal context and incognito contexts associated with it.
210 return Profile::FromBrowserContext(context)->GetOriginalProfile() ==
211 Profile::FromBrowserContext(browser_context());
214 std::set<const ChromeContentRulesRegistry::ContentRule*>
215 ChromeContentRulesRegistry::GetMatches(
216 const RendererContentMatchData& renderer_data,
217 bool is_incognito_renderer) const {
218 // Create the set of conditions that have matching page URL predicates
219 // according to |renderer_data.page_url_matches|.
220 std::set<const ContentCondition*> conditions_with_page_url_match;
221 for (URLMatcherConditionSet::ID url_match : renderer_data.page_url_matches) {
222 RuleAndConditionForURLMatcherId::const_iterator rule_condition_iter =
223 rule_and_conditions_for_match_id_.find(url_match);
224 CHECK(rule_condition_iter != rule_and_conditions_for_match_id_.end());
226 const ContentCondition* condition = rule_condition_iter->second.second;
227 conditions_with_page_url_match.insert(condition);
230 std::set<const ContentRule*> matching_rules;
231 for (const RulesMap::value_type& rule_id_rule_pair : content_rules_) {
232 const ContentRule* rule = rule_id_rule_pair.second.get();
233 if (is_incognito_renderer &&
234 !ShouldEvaluateExtensionRulesForIncognitoRenderer(rule->extension))
235 continue;
237 for (const ContentCondition* condition : rule->conditions) {
238 // If we already know the URL matcher predicate within |condition| doesn't
239 // match, we don't need to check whether the other predicates are
240 // fulfilled.
241 if (ContainsKey(url_matcher_conditions_, condition) &&
242 !ContainsKey(conditions_with_page_url_match, condition))
243 continue;
245 if (!condition->IsFulfilled(renderer_data))
246 continue;
248 matching_rules.insert(rule);
251 return matching_rules;
254 std::string ChromeContentRulesRegistry::AddRulesImpl(
255 const std::string& extension_id,
256 const std::vector<linked_ptr<api::events::Rule>>& rules) {
257 EvaluationScope evaluation_scope(this);
258 const Extension* extension = ExtensionRegistry::Get(browser_context())
259 ->GetInstalledExtension(extension_id);
260 DCHECK(extension) << "Must have extension with id " << extension_id;
262 std::string error;
263 RulesMap new_content_rules;
265 for (const linked_ptr<api::events::Rule>& rule : rules) {
266 ExtensionRuleIdPair rule_id(extension, *rule->id);
267 DCHECK(content_rules_.find(rule_id) == content_rules_.end());
269 scoped_ptr<const ContentRule> content_rule(CreateRule(
270 extension,
271 *rule,
272 &error));
273 if (!error.empty()) {
274 // Clean up temporary condition sets created during rule creation.
275 page_url_condition_tracker_.ClearUnusedConditionSets();
276 return error;
278 DCHECK(content_rule);
280 new_content_rules[rule_id] = make_linked_ptr(content_rule.release());
283 // Wohoo, everything worked fine.
284 content_rules_.insert(new_content_rules.begin(), new_content_rules.end());
286 // Record the URL matcher condition set to rule condition pair mappings, and
287 // register URL patterns in the URL matcher.
288 URLMatcherConditionSet::Vector all_new_condition_sets;
289 for (const RulesMap::value_type& rule_id_rule_pair : new_content_rules) {
290 const linked_ptr<const ContentRule>& rule = rule_id_rule_pair.second;
291 for (const ContentCondition* condition : rule->conditions) {
292 if (condition->url_matcher_condition_set()) {
293 URLMatcherConditionSet::ID condition_set_id =
294 condition->url_matcher_condition_set()->id();
295 rule_and_conditions_for_match_id_[condition_set_id] =
296 std::make_pair(rule.get(), condition);
297 all_new_condition_sets.push_back(
298 condition->url_matcher_condition_set());
299 url_matcher_conditions_.insert(condition);
303 page_url_condition_tracker_.AddConditionSets(
304 all_new_condition_sets);
306 UpdateCssSelectorsFromRules();
308 // Request evaluation for all WebContents, under the assumption that a
309 // non-empty condition has been added.
310 for (auto web_contents_rules_pair : active_rules_)
311 EvaluateConditionsForTab(web_contents_rules_pair.first);
313 return std::string();
316 std::string ChromeContentRulesRegistry::RemoveRulesImpl(
317 const std::string& extension_id,
318 const std::vector<std::string>& rule_identifiers) {
319 // Ignore evaluation requests in this function because it reverts actions on
320 // any active rules itself. Otherwise, we run the risk of reverting the same
321 // rule multiple times.
322 EvaluationScope evaluation_scope(this, IGNORE_REQUESTS);
323 // URLMatcherConditionSet IDs that can be removed from URLMatcher.
324 std::vector<URLMatcherConditionSet::ID> condition_set_ids_to_remove;
326 const Extension* extension = ExtensionRegistry::Get(browser_context())
327 ->GetInstalledExtension(extension_id);
328 for (const std::string& id : rule_identifiers) {
329 // Skip unknown rules.
330 RulesMap::iterator content_rules_entry =
331 content_rules_.find(std::make_pair(extension, id));
332 if (content_rules_entry == content_rules_.end())
333 continue;
335 // Remove state associated with URL matcher conditions, and collect the
336 // URLMatcherConditionSet::IDs to remove later.
337 URLMatcherConditionSet::Vector condition_sets;
338 const ContentRule* rule = content_rules_entry->second.get();
339 for (const ContentCondition* condition : rule->conditions) {
340 if (condition->url_matcher_condition_set()) {
341 URLMatcherConditionSet::ID condition_set_id =
342 condition->url_matcher_condition_set()->id();
343 condition_set_ids_to_remove.push_back(condition_set_id);
344 rule_and_conditions_for_match_id_.erase(condition_set_id);
345 url_matcher_conditions_.erase(condition);
349 // Remove the ContentRule from active_rules_.
350 for (auto& tab_rules_pair : active_rules_) {
351 if (ContainsKey(tab_rules_pair.second, rule)) {
352 ContentAction::ApplyInfo apply_info =
353 {rule->extension, browser_context(), tab_rules_pair.first,
354 rule->priority};
355 for (const ContentAction* action : rule->actions)
356 action->Revert(apply_info);
357 tab_rules_pair.second.erase(rule);
361 // Remove reference to actual rule.
362 content_rules_.erase(content_rules_entry);
365 // Clear URLMatcher of condition sets that are not needed any more.
366 page_url_condition_tracker_.RemoveConditionSets(
367 condition_set_ids_to_remove);
369 UpdateCssSelectorsFromRules();
371 // Request evaluation for all WebContents, under the assumption that a
372 // non-empty condition has been removed.
373 for (auto web_contents_rules_pair : active_rules_)
374 EvaluateConditionsForTab(web_contents_rules_pair.first);
376 return std::string();
379 std::string ChromeContentRulesRegistry::RemoveAllRulesImpl(
380 const std::string& extension_id) {
381 // Search all identifiers of rules that belong to extension |extension_id|.
382 std::vector<std::string> rule_identifiers;
383 for (const RulesMap::value_type& id_rule_pair : content_rules_) {
384 const ExtensionRuleIdPair& extension_rule_id_pair = id_rule_pair.first;
385 if (extension_rule_id_pair.first->id() == extension_id)
386 rule_identifiers.push_back(extension_rule_id_pair.second);
389 return RemoveRulesImpl(extension_id, rule_identifiers);
392 void ChromeContentRulesRegistry::UpdateCssSelectorsFromRules() {
393 std::set<std::string> css_selectors; // We rely on this being sorted.
394 for (const RulesMap::value_type& id_rule_pair : content_rules_) {
395 const ContentRule* rule = id_rule_pair.second.get();
396 for (const ContentCondition* condition : rule->conditions) {
397 const std::vector<std::string>& condition_css_selectors =
398 condition->css_selectors();
399 css_selectors.insert(condition_css_selectors.begin(),
400 condition_css_selectors.end());
404 css_condition_tracker_.SetWatchedCssSelectors(css_selectors);
407 void ChromeContentRulesRegistry::EvaluateConditionsForTab(
408 content::WebContents* tab) {
409 extensions::RendererContentMatchData renderer_data;
410 page_url_condition_tracker_.GetMatches(tab, &renderer_data.page_url_matches);
411 css_condition_tracker_.GetMatchingCssSelectors(tab,
412 &renderer_data.css_selectors);
413 renderer_data.is_bookmarked =
414 is_bookmarked_condition_tracker_.IsUrlBookmarked(tab);
415 std::set<const ContentRule*> matching_rules =
416 GetMatches(renderer_data, tab->GetBrowserContext()->IsOffTheRecord());
417 if (matching_rules.empty() && !ContainsKey(active_rules_, tab))
418 return;
420 std::set<const ContentRule*>& prev_matching_rules = active_rules_[tab];
421 for (const ContentRule* rule : matching_rules) {
422 ContentAction::ApplyInfo apply_info =
423 {rule->extension, browser_context(), tab, rule->priority};
424 if (!ContainsKey(prev_matching_rules, rule)) {
425 for (const ContentAction* action : rule->actions)
426 action->Apply(apply_info);
427 } else {
428 for (const ContentAction* action : rule->actions)
429 action->Reapply(apply_info);
432 for (const ContentRule* rule : prev_matching_rules) {
433 if (!ContainsKey(matching_rules, rule)) {
434 ContentAction::ApplyInfo apply_info =
435 {rule->extension, browser_context(), tab, rule->priority};
436 for (const ContentAction* action : rule->actions)
437 action->Revert(apply_info);
441 if (matching_rules.empty())
442 active_rules_[tab].clear();
443 else
444 swap(matching_rules, prev_matching_rules);
447 bool
448 ChromeContentRulesRegistry::ShouldEvaluateExtensionRulesForIncognitoRenderer(
449 const Extension* extension) const {
450 if (!util::IsIncognitoEnabled(extension->id(), browser_context()))
451 return false;
453 // Split-mode incognito extensions register their rules with separate
454 // RulesRegistries per Original/OffTheRecord browser contexts, whereas
455 // spanning-mode extensions share the Original browser context.
456 if (util::CanCrossIncognito(extension, browser_context())) {
457 // The extension uses spanning mode incognito. No rules should have been
458 // registered for the extension in the OffTheRecord registry so
459 // execution for that registry should never reach this point.
460 CHECK(!browser_context()->IsOffTheRecord());
461 } else {
462 // The extension uses split mode incognito. Both the Original and
463 // OffTheRecord registries may have (separate) rules for this extension.
464 // Since we're looking at an incognito renderer, so only the OffTheRecord
465 // registry should process its rules.
466 if (!browser_context()->IsOffTheRecord())
467 return false;
470 return true;
473 bool ChromeContentRulesRegistry::IsEmpty() const {
474 return rule_and_conditions_for_match_id_.empty() && content_rules_.empty() &&
475 page_url_condition_tracker_.IsEmpty();
478 void ChromeContentRulesRegistry::UpdateMatchingCssSelectorsForTesting(
479 content::WebContents* contents,
480 const std::vector<std::string>& matching_css_selectors) {
481 css_condition_tracker_.UpdateMatchingCssSelectorsForTesting(
482 contents,
483 matching_css_selectors);
486 size_t ChromeContentRulesRegistry::GetActiveRulesCountForTesting() {
487 size_t count = 0;
488 for (auto web_contents_rules_pair : active_rules_)
489 count += web_contents_rules_pair.second.size();
490 return count;
493 ChromeContentRulesRegistry::~ChromeContentRulesRegistry() {
496 } // namespace extensions