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.
4
5#include "chrome/browser/extensions/api/declarative_content/chrome_content_rules_registry.h"
6
7#include "chrome/browser/chrome_notification_types.h"
8#include "chrome/browser/extensions/api/declarative_content/content_action.h"
9#include "chrome/browser/extensions/api/declarative_content/content_condition.h"
10#include "chrome/browser/extensions/api/declarative_content/content_constants.h"
11#include "chrome/browser/extensions/extension_tab_util.h"
12#include "content/public/browser/navigation_details.h"
13#include "content/public/browser/notification_service.h"
14#include "content/public/browser/notification_source.h"
15#include "content/public/browser/render_process_host.h"
16#include "content/public/browser/web_contents.h"
17#include "extensions/browser/extension_registry.h"
18#include "extensions/browser/extension_system.h"
19#include "extensions/common/extension_messages.h"
20
21using url_matcher::URLMatcherConditionSet;
22
23namespace extensions {
24
25ChromeContentRulesRegistry::ChromeContentRulesRegistry(
26    content::BrowserContext* browser_context,
27    RulesCacheDelegate* cache_delegate)
28    : ContentRulesRegistry(browser_context,
29                           declarative_content_constants::kOnPageChanged,
30                           content::BrowserThread::UI,
31                           cache_delegate,
32                           WebViewKey(0, 0)) {
33  extension_info_map_ = ExtensionSystem::Get(browser_context)->info_map();
34
35  registrar_.Add(this,
36                 content::NOTIFICATION_RENDERER_PROCESS_CREATED,
37                 content::NotificationService::AllBrowserContextsAndSources());
38  registrar_.Add(this,
39                 content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
40                 content::NotificationService::AllBrowserContextsAndSources());
41}
42
43void ChromeContentRulesRegistry::Observe(
44    int type,
45    const content::NotificationSource& source,
46    const content::NotificationDetails& details) {
47  switch (type) {
48    case content::NOTIFICATION_RENDERER_PROCESS_CREATED: {
49      content::RenderProcessHost* process =
50          content::Source<content::RenderProcessHost>(source).ptr();
51      if (process->GetBrowserContext() == browser_context())
52        InstructRenderProcess(process);
53      break;
54    }
55    case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: {
56      content::WebContents* tab =
57          content::Source<content::WebContents>(source).ptr();
58      // GetTabId() returns -1 for non-tab WebContents, which won't be
59      // in the map.  Similarly, tabs from other browser_contexts won't be in
60      // the map.
61      active_rules_.erase(ExtensionTabUtil::GetTabId(tab));
62      break;
63    }
64  }
65}
66
67void ChromeContentRulesRegistry::Apply(
68    content::WebContents* contents,
69    const std::vector<std::string>& matching_css_selectors) {
70  const int tab_id = ExtensionTabUtil::GetTabId(contents);
71  RendererContentMatchData renderer_data;
72  renderer_data.page_url_matches = url_matcher_.MatchURL(contents->GetURL());
73  renderer_data.css_selectors.insert(matching_css_selectors.begin(),
74                                     matching_css_selectors.end());
75  std::set<ContentRule*> matching_rules = GetMatches(renderer_data);
76  if (matching_rules.empty() && !ContainsKey(active_rules_, tab_id))
77    return;
78
79  std::set<ContentRule*>& prev_matching_rules = active_rules_[tab_id];
80  ContentAction::ApplyInfo apply_info = {browser_context(), contents};
81  for (std::set<ContentRule*>::const_iterator it = matching_rules.begin();
82       it != matching_rules.end();
83       ++it) {
84    apply_info.priority = (*it)->priority();
85    if (!ContainsKey(prev_matching_rules, *it)) {
86      (*it)->actions().Apply((*it)->extension_id(), base::Time(), &apply_info);
87    } else {
88      (*it)->actions().Reapply(
89          (*it)->extension_id(), base::Time(), &apply_info);
90    }
91  }
92  for (std::set<ContentRule*>::const_iterator it = prev_matching_rules.begin();
93       it != prev_matching_rules.end();
94       ++it) {
95    if (!ContainsKey(matching_rules, *it)) {
96      apply_info.priority = (*it)->priority();
97      (*it)->actions().Revert((*it)->extension_id(), base::Time(), &apply_info);
98    }
99  }
100
101  if (matching_rules.empty())
102    active_rules_.erase(tab_id);
103  else
104    swap(matching_rules, prev_matching_rules);
105}
106
107void ChromeContentRulesRegistry::DidNavigateMainFrame(
108    content::WebContents* contents,
109    const content::LoadCommittedDetails& details,
110    const content::FrameNavigateParams& params) {
111  if (details.is_in_page) {
112    // Within-page navigations don't change the set of elements that
113    // exist, and we only support filtering on the top-level URL, so
114    // this can't change which rules match.
115    return;
116  }
117
118  // Top-level navigation produces a new document. Initially, the
119  // document's empty, so no CSS rules match.  The renderer will send
120  // an ExtensionHostMsg_OnWatchedPageChange later if any CSS rules
121  // match.
122  std::vector<std::string> no_css_selectors;
123  Apply(contents, no_css_selectors);
124}
125
126std::set<ContentRule*> ChromeContentRulesRegistry::GetMatches(
127    const RendererContentMatchData& renderer_data) const {
128  std::set<ContentRule*> result;
129
130  // Then we need to check for each of these, whether the other
131  // attributes are also fulfilled.
132  for (std::set<URLMatcherConditionSet::ID>::iterator url_match =
133           renderer_data.page_url_matches.begin();
134       url_match != renderer_data.page_url_matches.end();
135       ++url_match) {
136    URLMatcherIdToRule::const_iterator rule_iter =
137        match_id_to_rule_.find(*url_match);
138    CHECK(rule_iter != match_id_to_rule_.end());
139
140    ContentRule* rule = rule_iter->second;
141    if (rule->conditions().IsFulfilled(*url_match, renderer_data))
142      result.insert(rule);
143  }
144  return result;
145}
146
147std::string ChromeContentRulesRegistry::AddRulesImpl(
148    const std::string& extension_id,
149    const std::vector<linked_ptr<RulesRegistry::Rule> >& rules) {
150  const Extension* extension =
151      ExtensionRegistry::Get(browser_context())
152          ->GetExtensionById(extension_id, ExtensionRegistry::EVERYTHING);
153  DCHECK(extension) << "Must have extension with id " << extension_id;
154
155  base::Time extension_installation_time =
156      GetExtensionInstallationTime(extension_id);
157
158  std::string error;
159  RulesMap new_content_rules;
160
161  for (std::vector<linked_ptr<RulesRegistry::Rule> >::const_iterator rule =
162           rules.begin();
163       rule != rules.end();
164       ++rule) {
165    ContentRule::GlobalRuleId rule_id(extension_id, *(*rule)->id);
166    DCHECK(content_rules_.find(rule_id) == content_rules_.end());
167
168    scoped_ptr<ContentRule> content_rule(
169        ContentRule::Create(url_matcher_.condition_factory(),
170                            browser_context(),
171                            extension,
172                            extension_installation_time,
173                            *rule,
174                            ContentRule::ConsistencyChecker(),
175                            &error));
176    if (!error.empty()) {
177      // Clean up temporary condition sets created during rule creation.
178      url_matcher_.ClearUnusedConditionSets();
179      return error;
180    }
181    DCHECK(content_rule);
182
183    new_content_rules[rule_id] = make_linked_ptr(content_rule.release());
184  }
185
186  // Wohoo, everything worked fine.
187  content_rules_.insert(new_content_rules.begin(), new_content_rules.end());
188
189  // Create the triggers.
190  for (RulesMap::iterator i = new_content_rules.begin();
191       i != new_content_rules.end();
192       ++i) {
193    URLMatcherConditionSet::Vector url_condition_sets;
194    const ContentConditionSet& conditions = i->second->conditions();
195    conditions.GetURLMatcherConditionSets(&url_condition_sets);
196    for (URLMatcherConditionSet::Vector::iterator j =
197             url_condition_sets.begin();
198         j != url_condition_sets.end();
199         ++j) {
200      match_id_to_rule_[(*j)->id()] = i->second.get();
201    }
202  }
203
204  // Register url patterns in url_matcher_.
205  URLMatcherConditionSet::Vector all_new_condition_sets;
206  for (RulesMap::iterator i = new_content_rules.begin();
207       i != new_content_rules.end();
208       ++i) {
209    i->second->conditions().GetURLMatcherConditionSets(&all_new_condition_sets);
210  }
211  url_matcher_.AddConditionSets(all_new_condition_sets);
212
213  UpdateConditionCache();
214
215  return std::string();
216}
217
218std::string ChromeContentRulesRegistry::RemoveRulesImpl(
219    const std::string& extension_id,
220    const std::vector<std::string>& rule_identifiers) {
221  // URLMatcherConditionSet IDs that can be removed from URLMatcher.
222  std::vector<URLMatcherConditionSet::ID> remove_from_url_matcher;
223
224  for (std::vector<std::string>::const_iterator i = rule_identifiers.begin();
225       i != rule_identifiers.end();
226       ++i) {
227    ContentRule::GlobalRuleId rule_id(extension_id, *i);
228
229    // Skip unknown rules.
230    RulesMap::iterator content_rules_entry = content_rules_.find(rule_id);
231    if (content_rules_entry == content_rules_.end())
232      continue;
233
234    // Remove all triggers but collect their IDs.
235    URLMatcherConditionSet::Vector condition_sets;
236    ContentRule* rule = content_rules_entry->second.get();
237    rule->conditions().GetURLMatcherConditionSets(&condition_sets);
238    for (URLMatcherConditionSet::Vector::iterator j = condition_sets.begin();
239         j != condition_sets.end();
240         ++j) {
241      remove_from_url_matcher.push_back((*j)->id());
242      match_id_to_rule_.erase((*j)->id());
243    }
244
245    // Remove the ContentRule from active_rules_.
246    for (std::map<int, std::set<ContentRule*> >::iterator it =
247             active_rules_.begin();
248         it != active_rules_.end();
249         ++it) {
250      if (ContainsKey(it->second, rule)) {
251        content::WebContents* tab;
252        if (!ExtensionTabUtil::GetTabById(
253                it->first, browser_context(), true, NULL, NULL, &tab, NULL)) {
254          LOG(DFATAL) << "Tab id " << it->first
255                      << " still in active_rules_, but tab has been destroyed";
256          continue;
257        }
258        ContentAction::ApplyInfo apply_info = {browser_context(), tab};
259        rule->actions().Revert(rule->extension_id(), base::Time(), &apply_info);
260        it->second.erase(rule);
261      }
262    }
263
264    // Remove reference to actual rule.
265    content_rules_.erase(content_rules_entry);
266  }
267
268  // Clear URLMatcher based on condition_set_ids that are not needed any more.
269  url_matcher_.RemoveConditionSets(remove_from_url_matcher);
270
271  UpdateConditionCache();
272
273  return std::string();
274}
275
276std::string ChromeContentRulesRegistry::RemoveAllRulesImpl(
277    const std::string& extension_id) {
278  // Search all identifiers of rules that belong to extension |extension_id|.
279  std::vector<std::string> rule_identifiers;
280  for (RulesMap::iterator i = content_rules_.begin(); i != content_rules_.end();
281       ++i) {
282    const ContentRule::GlobalRuleId& global_rule_id = i->first;
283    if (global_rule_id.first == extension_id)
284      rule_identifiers.push_back(global_rule_id.second);
285  }
286
287  return RemoveRulesImpl(extension_id, rule_identifiers);
288}
289
290void ChromeContentRulesRegistry::UpdateConditionCache() {
291  std::set<std::string> css_selectors;  // We rely on this being sorted.
292  for (RulesMap::const_iterator i = content_rules_.begin();
293       i != content_rules_.end();
294       ++i) {
295    ContentRule& rule = *i->second;
296    for (ContentConditionSet::const_iterator condition =
297             rule.conditions().begin();
298         condition != rule.conditions().end();
299         ++condition) {
300      const std::vector<std::string>& condition_css_selectors =
301          (*condition)->css_selectors();
302      css_selectors.insert(condition_css_selectors.begin(),
303                           condition_css_selectors.end());
304    }
305  }
306
307  if (css_selectors.size() != watched_css_selectors_.size() ||
308      !std::equal(css_selectors.begin(),
309                  css_selectors.end(),
310                  watched_css_selectors_.begin())) {
311    watched_css_selectors_.assign(css_selectors.begin(), css_selectors.end());
312
313    for (content::RenderProcessHost::iterator it(
314             content::RenderProcessHost::AllHostsIterator());
315         !it.IsAtEnd();
316         it.Advance()) {
317      content::RenderProcessHost* process = it.GetCurrentValue();
318      if (process->GetBrowserContext() == browser_context())
319        InstructRenderProcess(process);
320    }
321  }
322}
323
324void ChromeContentRulesRegistry::InstructRenderProcess(
325    content::RenderProcessHost* process) {
326  process->Send(new ExtensionMsg_WatchPages(watched_css_selectors_));
327}
328
329bool ChromeContentRulesRegistry::IsEmpty() const {
330  return match_id_to_rule_.empty() && content_rules_.empty() &&
331         url_matcher_.IsEmpty();
332}
333
334ChromeContentRulesRegistry::~ChromeContentRulesRegistry() {
335}
336
337base::Time ChromeContentRulesRegistry::GetExtensionInstallationTime(
338    const std::string& extension_id) const {
339  if (!extension_info_map_.get())  // May be NULL during testing.
340    return base::Time();
341
342  return extension_info_map_->GetInstallTime(extension_id);
343}
344
345}  // namespace extensions
346