1// Copyright 2014 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 "extensions/renderer/script_injection_manager.h"
6
7#include "base/bind.h"
8#include "base/memory/weak_ptr.h"
9#include "base/values.h"
10#include "content/public/renderer/render_view.h"
11#include "content/public/renderer/render_view_observer.h"
12#include "extensions/common/extension.h"
13#include "extensions/common/extension_messages.h"
14#include "extensions/common/extension_set.h"
15#include "extensions/renderer/extension_helper.h"
16#include "extensions/renderer/programmatic_script_injector.h"
17#include "extensions/renderer/script_injection.h"
18#include "extensions/renderer/scripts_run_info.h"
19#include "ipc/ipc_message_macros.h"
20#include "third_party/WebKit/public/web/WebDocument.h"
21#include "third_party/WebKit/public/web/WebFrame.h"
22#include "third_party/WebKit/public/web/WebLocalFrame.h"
23#include "third_party/WebKit/public/web/WebView.h"
24#include "url/gurl.h"
25
26namespace extensions {
27
28namespace {
29
30// The length of time to wait after the DOM is complete to try and run user
31// scripts.
32const int kScriptIdleTimeoutInMs = 200;
33
34}  // namespace
35
36class ScriptInjectionManager::RVOHelper : public content::RenderViewObserver {
37 public:
38  RVOHelper(content::RenderView* render_view, ScriptInjectionManager* manager);
39  virtual ~RVOHelper();
40
41 private:
42  // RenderViewObserver implementation.
43  virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
44  virtual void DidCreateDocumentElement(blink::WebLocalFrame* frame) OVERRIDE;
45  virtual void DidFinishDocumentLoad(blink::WebLocalFrame* frame) OVERRIDE;
46  virtual void DidFinishLoad(blink::WebLocalFrame* frame) OVERRIDE;
47  virtual void DidStartProvisionalLoad(blink::WebLocalFrame* frame) OVERRIDE;
48  virtual void FrameDetached(blink::WebFrame* frame) OVERRIDE;
49  virtual void OnDestruct() OVERRIDE;
50
51  virtual void OnExecuteCode(const ExtensionMsg_ExecuteCode_Params& params);
52  virtual void OnExecuteDeclarativeScript(int tab_id,
53                                          const ExtensionId& extension_id,
54                                          int script_id,
55                                          const GURL& url);
56  virtual void OnPermitScriptInjection(int64 request_id);
57
58  // Tells the ScriptInjectionManager to run tasks associated with
59  // document_idle.
60  void RunIdle(blink::WebFrame* frame);
61
62  // Indicate that the given |frame| is no longer valid because it is starting
63  // a new load or closing.
64  void InvalidateFrame(blink::WebFrame* frame);
65
66  // The owning ScriptInjectionManager.
67  ScriptInjectionManager* manager_;
68
69  // The set of frames that we are about to notify for DOCUMENT_IDLE. We keep
70  // a set of those that are valid, so we don't notify that an invalid frame
71  // became idle.
72  std::set<blink::WebFrame*> pending_idle_frames_;
73
74  base::WeakPtrFactory<RVOHelper> weak_factory_;
75};
76
77ScriptInjectionManager::RVOHelper::RVOHelper(
78    content::RenderView* render_view,
79    ScriptInjectionManager* manager)
80    : content::RenderViewObserver(render_view),
81      manager_(manager),
82      weak_factory_(this) {
83}
84
85ScriptInjectionManager::RVOHelper::~RVOHelper() {
86}
87
88bool ScriptInjectionManager::RVOHelper::OnMessageReceived(
89    const IPC::Message& message) {
90  bool handled = true;
91  IPC_BEGIN_MESSAGE_MAP(ScriptInjectionManager::RVOHelper, message)
92    IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteCode, OnExecuteCode)
93    IPC_MESSAGE_HANDLER(ExtensionMsg_PermitScriptInjection,
94                        OnPermitScriptInjection)
95    IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteDeclarativeScript,
96                        OnExecuteDeclarativeScript)
97    IPC_MESSAGE_UNHANDLED(handled = false)
98  IPC_END_MESSAGE_MAP()
99  return handled;
100}
101
102void ScriptInjectionManager::RVOHelper::DidCreateDocumentElement(
103    blink::WebLocalFrame* frame) {
104  manager_->InjectScripts(frame, UserScript::DOCUMENT_START);
105}
106
107void ScriptInjectionManager::RVOHelper::DidFinishDocumentLoad(
108    blink::WebLocalFrame* frame) {
109  manager_->InjectScripts(frame, UserScript::DOCUMENT_END);
110  pending_idle_frames_.insert(frame);
111  // We try to run idle in two places: here and DidFinishLoad.
112  // DidFinishDocumentLoad() corresponds to completing the document's load,
113  // whereas DidFinishLoad corresponds to completing the document and all
114  // subresources' load. We don't want to hold up script injection for a
115  // particularly slow subresource, so we set a delayed task from here - but if
116  // we finish everything before that point (i.e., DidFinishLoad() is
117  // triggered), then there's no reason to keep waiting.
118  base::MessageLoop::current()->PostDelayedTask(
119      FROM_HERE,
120      base::Bind(&ScriptInjectionManager::RVOHelper::RunIdle,
121                 weak_factory_.GetWeakPtr(),
122                 frame),
123      base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs));
124}
125
126void ScriptInjectionManager::RVOHelper::DidFinishLoad(
127    blink::WebLocalFrame* frame) {
128  // Ensure that we don't block any UI progress by running scripts.
129  // We *don't* add the frame to |pending_idle_frames_| here because
130  // DidFinishDocumentLoad should strictly come before DidFinishLoad, so the
131  // first posted task to RunIdle() pops it out of the set. This ensures we
132  // don't try to run idle twice.
133  base::MessageLoop::current()->PostTask(
134      FROM_HERE,
135      base::Bind(&ScriptInjectionManager::RVOHelper::RunIdle,
136                 weak_factory_.GetWeakPtr(),
137                 frame));
138}
139
140void ScriptInjectionManager::RVOHelper::DidStartProvisionalLoad(
141    blink::WebLocalFrame* frame) {
142  // We're starting a new load - invalidate.
143  InvalidateFrame(frame);
144}
145
146void ScriptInjectionManager::RVOHelper::FrameDetached(blink::WebFrame* frame) {
147  // The frame is closing - invalidate.
148  InvalidateFrame(frame);
149}
150
151void ScriptInjectionManager::RVOHelper::OnDestruct() {
152  manager_->RemoveObserver(this);
153}
154
155void ScriptInjectionManager::RVOHelper::OnExecuteCode(
156    const ExtensionMsg_ExecuteCode_Params& params) {
157  manager_->HandleExecuteCode(params, render_view());
158}
159
160void ScriptInjectionManager::RVOHelper::OnExecuteDeclarativeScript(
161    int tab_id,
162    const ExtensionId& extension_id,
163    int script_id,
164    const GURL& url) {
165  blink::WebFrame* main_frame = render_view()->GetWebView()->mainFrame();
166  CHECK(main_frame);
167
168  // TODO(markdittmer): This would be cleaner if we compared page_ids instead.
169  // Begin script injeciton workflow only if the current URL is identical to
170  // the one that matched declarative conditions in the browser.
171  if (main_frame->top()->document().url() == url) {
172    manager_->HandleExecuteDeclarativeScript(main_frame,
173                                             tab_id,
174                                             extension_id,
175                                             script_id,
176                                             url);
177  }
178}
179
180void ScriptInjectionManager::RVOHelper::OnPermitScriptInjection(
181    int64 request_id) {
182  manager_->HandlePermitScriptInjection(request_id);
183}
184
185void ScriptInjectionManager::RVOHelper::RunIdle(blink::WebFrame* frame) {
186  // Only notify the manager if the frame hasn't either been removed or already
187  // had idle run since the task to RunIdle() was posted.
188  if (pending_idle_frames_.count(frame) > 0) {
189    manager_->InjectScripts(frame, UserScript::DOCUMENT_IDLE);
190    pending_idle_frames_.erase(frame);
191  }
192}
193
194void ScriptInjectionManager::RVOHelper::InvalidateFrame(
195    blink::WebFrame* frame) {
196  pending_idle_frames_.erase(frame);
197  manager_->InvalidateForFrame(frame);
198}
199
200ScriptInjectionManager::ScriptInjectionManager(
201    const ExtensionSet* extensions,
202    UserScriptSetManager* user_script_set_manager)
203    : extensions_(extensions),
204      user_script_set_manager_(user_script_set_manager),
205      user_script_set_manager_observer_(this) {
206  user_script_set_manager_observer_.Add(user_script_set_manager_);
207}
208
209ScriptInjectionManager::~ScriptInjectionManager() {
210}
211
212void ScriptInjectionManager::OnRenderViewCreated(
213    content::RenderView* render_view) {
214  rvo_helpers_.push_back(new RVOHelper(render_view, this));
215}
216
217void ScriptInjectionManager::OnUserScriptsUpdated(
218    const std::set<std::string>& changed_extensions,
219    const std::vector<UserScript*>& scripts) {
220  for (ScopedVector<ScriptInjection>::iterator iter =
221           pending_injections_.begin();
222       iter != pending_injections_.end();) {
223    if (changed_extensions.count((*iter)->extension_id()) > 0)
224      iter = pending_injections_.erase(iter);
225    else
226      ++iter;
227  }
228}
229
230void ScriptInjectionManager::RemoveObserver(RVOHelper* helper) {
231  for (ScopedVector<RVOHelper>::iterator iter = rvo_helpers_.begin();
232       iter != rvo_helpers_.end();
233       ++iter) {
234    if (*iter == helper) {
235      rvo_helpers_.erase(iter);
236      break;
237    }
238  }
239}
240
241void ScriptInjectionManager::InvalidateForFrame(blink::WebFrame* frame) {
242  for (ScopedVector<ScriptInjection>::iterator iter =
243           pending_injections_.begin();
244       iter != pending_injections_.end();) {
245    if ((*iter)->web_frame() == frame)
246      iter = pending_injections_.erase(iter);
247    else
248      ++iter;
249  }
250
251  frame_statuses_.erase(frame);
252}
253
254void ScriptInjectionManager::InjectScripts(
255    blink::WebFrame* frame, UserScript::RunLocation run_location) {
256  FrameStatusMap::iterator iter = frame_statuses_.find(frame);
257  // We also don't execute if we detect that the run location is somehow out of
258  // order. This can happen if:
259  // - The first run location reported for the frame isn't DOCUMENT_START, or
260  // - The run location reported doesn't immediately follow the previous
261  //   reported run location.
262  // We don't want to run because extensions may have requirements that scripts
263  // running in an earlier run location have run by the time a later script
264  // runs. Better to just not run.
265  if ((iter == frame_statuses_.end() &&
266           run_location != UserScript::DOCUMENT_START) ||
267      (iter != frame_statuses_.end() && run_location - iter->second > 1)) {
268    // We also invalidate the frame, because the run order of pending injections
269    // may also be bad.
270    InvalidateForFrame(frame);
271    return;
272  } else if (iter != frame_statuses_.end() && iter->second > run_location) {
273    // Certain run location signals (like DidCreateDocumentElement) can happen
274    // multiple times. Ignore the subsequent signals.
275    return;
276  }
277
278  // Otherwise, all is right in the world, and we can get on with the
279  // injections!
280
281  frame_statuses_[frame] = run_location;
282
283  // Inject any scripts that were waiting for the right run location.
284  ScriptsRunInfo scripts_run_info;
285  for (ScopedVector<ScriptInjection>::iterator iter =
286           pending_injections_.begin();
287       iter != pending_injections_.end();) {
288    if ((*iter)->web_frame() == frame &&
289        (*iter)->TryToInject(run_location,
290                             extensions_->GetByID((*iter)->extension_id()),
291                             &scripts_run_info)) {
292      iter = pending_injections_.erase(iter);
293    } else {
294      ++iter;
295    }
296  }
297
298  // Try to inject any user scripts that should run for this location. If they
299  // don't complete their injection (for example, waiting for a permission
300  // response) then they will be added to |pending_injections_|.
301  ScopedVector<ScriptInjection> user_script_injections;
302  int tab_id = ExtensionHelper::Get(content::RenderView::FromWebView(
303                                        frame->top()->view()))->tab_id();
304  user_script_set_manager_->GetAllInjections(
305      &user_script_injections, frame, tab_id, run_location);
306  for (ScopedVector<ScriptInjection>::iterator iter =
307           user_script_injections.begin();
308       iter != user_script_injections.end();) {
309    scoped_ptr<ScriptInjection> injection(*iter);
310    iter = user_script_injections.weak_erase(iter);
311    if (!injection->TryToInject(run_location,
312                                extensions_->GetByID(injection->extension_id()),
313                                &scripts_run_info)) {
314      pending_injections_.push_back(injection.release());
315    }
316  }
317
318  scripts_run_info.LogRun(frame, run_location);
319}
320
321void ScriptInjectionManager::HandleExecuteCode(
322    const ExtensionMsg_ExecuteCode_Params& params,
323    content::RenderView* render_view) {
324  // TODO(dcheng): Not sure how this can happen today. In an OOPI world, it
325  // would indicate a logic error--the browser must direct this request to the
326  // right renderer process to begin with.
327  blink::WebLocalFrame* main_frame =
328      render_view->GetWebView()->mainFrame()->toWebLocalFrame();
329  if (!main_frame) {
330    render_view->Send(
331        new ExtensionHostMsg_ExecuteCodeFinished(render_view->GetRoutingID(),
332                                                 params.request_id,
333                                                 "No main frame",
334                                                 GURL(std::string()),
335                                                 base::ListValue()));
336    return;
337  }
338
339  scoped_ptr<ScriptInjection> injection(new ScriptInjection(
340      scoped_ptr<ScriptInjector>(
341          new ProgrammaticScriptInjector(params, main_frame)),
342      main_frame,
343      params.extension_id,
344      static_cast<UserScript::RunLocation>(params.run_at),
345      ExtensionHelper::Get(render_view)->tab_id()));
346
347  ScriptsRunInfo scripts_run_info;
348  FrameStatusMap::const_iterator iter = frame_statuses_.find(main_frame);
349  if (!injection->TryToInject(
350          iter == frame_statuses_.end() ? UserScript::UNDEFINED : iter->second,
351          extensions_->GetByID(injection->extension_id()),
352          &scripts_run_info)) {
353    pending_injections_.push_back(injection.release());
354  }
355}
356
357void ScriptInjectionManager::HandleExecuteDeclarativeScript(
358    blink::WebFrame* web_frame,
359    int tab_id,
360    const ExtensionId& extension_id,
361    int script_id,
362    const GURL& url) {
363  const Extension* extension = extensions_->GetByID(extension_id);
364  // TODO(dcheng): This function signature should really be a WebLocalFrame,
365  // rather than trying to coerce it here.
366  scoped_ptr<ScriptInjection> injection =
367      user_script_set_manager_->GetInjectionForDeclarativeScript(
368          script_id,
369          web_frame->toWebLocalFrame(),
370          tab_id,
371          url,
372          extension);
373  if (injection.get()) {
374    ScriptsRunInfo scripts_run_info;
375    // TODO(markdittmer): Use return value of TryToInject for error handling.
376    injection->TryToInject(UserScript::BROWSER_DRIVEN,
377                           extension,
378                           &scripts_run_info);
379    scripts_run_info.LogRun(web_frame, UserScript::BROWSER_DRIVEN);
380  }
381}
382
383void ScriptInjectionManager::HandlePermitScriptInjection(int64 request_id) {
384  ScopedVector<ScriptInjection>::iterator iter =
385      pending_injections_.begin();
386  for (; iter != pending_injections_.end(); ++iter) {
387    if ((*iter)->request_id() == request_id)
388      break;
389  }
390  if (iter == pending_injections_.end())
391    return;
392
393  // At this point, because the request is present in pending_injections_, we
394  // know that this is the same page that issued the request (otherwise,
395  // RVOHelper's DidStartProvisionalLoad callback would have caused it to be
396  // cleared out).
397
398  scoped_ptr<ScriptInjection> injection(*iter);
399  pending_injections_.weak_erase(iter);
400
401  ScriptsRunInfo scripts_run_info;
402  if (injection->OnPermissionGranted(extensions_->GetByID(
403                                         injection->extension_id()),
404                                     &scripts_run_info)) {
405    scripts_run_info.LogRun(injection->web_frame(), UserScript::RUN_DEFERRED);
406  }
407}
408
409}  // namespace extensions
410