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/user_script_listener.h"
6
7#include "base/bind.h"
8#include "chrome/browser/chrome_notification_types.h"
9#include "chrome/browser/profiles/profile.h"
10#include "chrome/common/extensions/manifest_handlers/content_scripts_handler.h"
11#include "content/public/browser/browser_thread.h"
12#include "content/public/browser/notification_service.h"
13#include "content/public/browser/resource_controller.h"
14#include "content/public/browser/resource_throttle.h"
15#include "extensions/browser/extension_registry.h"
16#include "extensions/common/extension.h"
17#include "extensions/common/url_pattern.h"
18#include "net/url_request/url_request.h"
19
20using content::BrowserThread;
21using content::ResourceThrottle;
22using content::ResourceType;
23
24namespace extensions {
25
26class UserScriptListener::Throttle
27    : public ResourceThrottle,
28      public base::SupportsWeakPtr<UserScriptListener::Throttle> {
29 public:
30  Throttle() : should_defer_(true), did_defer_(false) {
31  }
32
33  void Resume() {
34    DCHECK(should_defer_);
35    should_defer_ = false;
36    // Only resume the request if |this| has deferred it.
37    if (did_defer_)
38      controller()->Resume();
39  }
40
41  // ResourceThrottle implementation:
42  virtual void WillStartRequest(bool* defer) OVERRIDE {
43    // Only defer requests if Resume has not yet been called.
44    if (should_defer_) {
45      *defer = true;
46      did_defer_ = true;
47    }
48  }
49
50  virtual const char* GetNameForLogging() const OVERRIDE {
51    return "UserScriptListener::Throttle";
52  }
53
54 private:
55  bool should_defer_;
56  bool did_defer_;
57};
58
59struct UserScriptListener::ProfileData {
60  // True if the user scripts contained in |url_patterns| are ready for
61  // injection.
62  bool user_scripts_ready;
63
64  // A list of URL patterns that have will have user scripts applied to them.
65  URLPatterns url_patterns;
66
67  ProfileData() : user_scripts_ready(false) {}
68};
69
70UserScriptListener::UserScriptListener()
71    : user_scripts_ready_(false) {
72  DCHECK_CURRENTLY_ON(BrowserThread::UI);
73
74  registrar_.Add(this,
75                 extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
76                 content::NotificationService::AllSources());
77  registrar_.Add(this,
78                 extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED,
79                 content::NotificationService::AllSources());
80  registrar_.Add(this,
81                 extensions::NOTIFICATION_USER_SCRIPTS_UPDATED,
82                 content::NotificationService::AllSources());
83  registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
84                 content::NotificationService::AllSources());
85}
86
87ResourceThrottle* UserScriptListener::CreateResourceThrottle(
88    const GURL& url,
89    ResourceType resource_type) {
90  if (!ShouldDelayRequest(url, resource_type))
91    return NULL;
92
93  Throttle* throttle = new Throttle();
94  throttles_.push_back(throttle->AsWeakPtr());
95  return throttle;
96}
97
98UserScriptListener::~UserScriptListener() {
99}
100
101bool UserScriptListener::ShouldDelayRequest(const GURL& url,
102                                            ResourceType resource_type) {
103  DCHECK_CURRENTLY_ON(BrowserThread::IO);
104
105  // If it's a frame load, then we need to check the URL against the list of
106  // user scripts to see if we need to wait.
107  if (resource_type != content::RESOURCE_TYPE_MAIN_FRAME &&
108      resource_type != content::RESOURCE_TYPE_SUB_FRAME)
109    return false;
110
111  // Note: we could delay only requests made by the profile who is causing the
112  // delay, but it's a little more complicated to associate requests with the
113  // right profile. Since this is a rare case, we'll just take the easy way
114  // out.
115  if (user_scripts_ready_)
116    return false;
117
118  for (ProfileDataMap::const_iterator pt = profile_data_.begin();
119       pt != profile_data_.end(); ++pt) {
120    for (URLPatterns::const_iterator it = pt->second.url_patterns.begin();
121         it != pt->second.url_patterns.end(); ++it) {
122      if ((*it).MatchesURL(url)) {
123        // One of the user scripts wants to inject into this request, but the
124        // script isn't ready yet. Delay the request.
125        return true;
126      }
127    }
128  }
129
130  return false;
131}
132
133void UserScriptListener::StartDelayedRequests() {
134  WeakThrottleList::const_iterator it;
135  for (it = throttles_.begin(); it != throttles_.end(); ++it) {
136    if (it->get())
137      (*it)->Resume();
138  }
139  throttles_.clear();
140}
141
142void UserScriptListener::CheckIfAllUserScriptsReady() {
143  DCHECK_CURRENTLY_ON(BrowserThread::IO);
144  bool was_ready = user_scripts_ready_;
145
146  user_scripts_ready_ = true;
147  for (ProfileDataMap::const_iterator it = profile_data_.begin();
148       it != profile_data_.end(); ++it) {
149    if (!it->second.user_scripts_ready)
150      user_scripts_ready_ = false;
151  }
152
153  if (user_scripts_ready_ && !was_ready)
154    StartDelayedRequests();
155}
156
157void UserScriptListener::UserScriptsReady(void* profile_id) {
158  DCHECK_CURRENTLY_ON(BrowserThread::IO);
159
160  profile_data_[profile_id].user_scripts_ready = true;
161  CheckIfAllUserScriptsReady();
162}
163
164void UserScriptListener::ProfileDestroyed(void* profile_id) {
165  DCHECK_CURRENTLY_ON(BrowserThread::IO);
166  profile_data_.erase(profile_id);
167
168  // We may have deleted the only profile we were waiting on.
169  CheckIfAllUserScriptsReady();
170}
171
172void UserScriptListener::AppendNewURLPatterns(void* profile_id,
173                                              const URLPatterns& new_patterns) {
174  DCHECK_CURRENTLY_ON(BrowserThread::IO);
175
176  user_scripts_ready_ = false;
177
178  ProfileData& data = profile_data_[profile_id];
179  data.user_scripts_ready = false;
180
181  data.url_patterns.insert(data.url_patterns.end(),
182                           new_patterns.begin(), new_patterns.end());
183}
184
185void UserScriptListener::ReplaceURLPatterns(void* profile_id,
186                                            const URLPatterns& patterns) {
187  DCHECK_CURRENTLY_ON(BrowserThread::IO);
188
189  ProfileData& data = profile_data_[profile_id];
190  data.url_patterns = patterns;
191}
192
193void UserScriptListener::CollectURLPatterns(const Extension* extension,
194                                            URLPatterns* patterns) {
195  DCHECK_CURRENTLY_ON(BrowserThread::UI);
196
197  const UserScriptList& scripts =
198      ContentScriptsInfo::GetContentScripts(extension);
199  for (UserScriptList::const_iterator iter = scripts.begin();
200       iter != scripts.end(); ++iter) {
201    patterns->insert(patterns->end(),
202                     (*iter).url_patterns().begin(),
203                     (*iter).url_patterns().end());
204  }
205}
206
207void UserScriptListener::Observe(int type,
208                                 const content::NotificationSource& source,
209                                 const content::NotificationDetails& details) {
210  DCHECK_CURRENTLY_ON(BrowserThread::UI);
211
212  switch (type) {
213    case extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED: {
214      Profile* profile = content::Source<Profile>(source).ptr();
215      const Extension* extension =
216          content::Details<const Extension>(details).ptr();
217      if (ContentScriptsInfo::GetContentScripts(extension).empty())
218        return;  // no new patterns from this extension.
219
220      URLPatterns new_patterns;
221      CollectURLPatterns(extension, &new_patterns);
222      if (!new_patterns.empty()) {
223        BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(
224            &UserScriptListener::AppendNewURLPatterns, this,
225            profile, new_patterns));
226      }
227      break;
228    }
229
230    case extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED: {
231      Profile* profile = content::Source<Profile>(source).ptr();
232      const Extension* unloaded_extension =
233          content::Details<UnloadedExtensionInfo>(details)->extension;
234      if (ContentScriptsInfo::GetContentScripts(unloaded_extension).empty())
235        return;  // no patterns to delete for this extension.
236
237      // Clear all our patterns and reregister all the still-loaded extensions.
238      const ExtensionSet& extensions =
239          ExtensionRegistry::Get(profile)->enabled_extensions();
240      URLPatterns new_patterns;
241      for (ExtensionSet::const_iterator it = extensions.begin();
242           it != extensions.end(); ++it) {
243        if (it->get() != unloaded_extension)
244          CollectURLPatterns(it->get(), &new_patterns);
245      }
246      BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(
247          &UserScriptListener::ReplaceURLPatterns, this,
248          profile, new_patterns));
249      break;
250    }
251
252    case extensions::NOTIFICATION_USER_SCRIPTS_UPDATED: {
253      Profile* profile = content::Source<Profile>(source).ptr();
254      BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(
255          &UserScriptListener::UserScriptsReady, this, profile));
256      break;
257    }
258
259    case chrome::NOTIFICATION_PROFILE_DESTROYED: {
260      Profile* profile = content::Source<Profile>(source).ptr();
261      BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(
262          &UserScriptListener::ProfileDestroyed, this, profile));
263      break;
264    }
265
266    default:
267      NOTREACHED();
268  }
269}
270
271}  // namespace extensions
272