| // Copyright 2014 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 "extensions/renderer/script_injection_manager.h" |
| |
| #include "base/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/values.h" |
| #include "content/public/renderer/render_view.h" |
| #include "content/public/renderer/render_view_observer.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_messages.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/renderer/extension_helper.h" |
| #include "extensions/renderer/programmatic_script_injector.h" |
| #include "extensions/renderer/script_injection.h" |
| #include "extensions/renderer/scripts_run_info.h" |
| #include "ipc/ipc_message_macros.h" |
| #include "third_party/WebKit/public/web/WebFrame.h" |
| #include "third_party/WebKit/public/web/WebLocalFrame.h" |
| #include "third_party/WebKit/public/web/WebView.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // The length of time to wait after the DOM is complete to try and run user |
| // scripts. |
| const int kScriptIdleTimeoutInMs = 200; |
| |
| } // namespace |
| |
| class ScriptInjectionManager::RVOHelper : public content::RenderViewObserver { |
| public: |
| RVOHelper(content::RenderView* render_view, ScriptInjectionManager* manager); |
| virtual ~RVOHelper(); |
| |
| private: |
| // RenderViewObserver implementation. |
| virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; |
| virtual void DidCreateDocumentElement(blink::WebLocalFrame* frame) OVERRIDE; |
| virtual void DidFinishDocumentLoad(blink::WebLocalFrame* frame) OVERRIDE; |
| virtual void DidFinishLoad(blink::WebLocalFrame* frame) OVERRIDE; |
| virtual void DidStartProvisionalLoad(blink::WebLocalFrame* frame) OVERRIDE; |
| virtual void FrameDetached(blink::WebFrame* frame) OVERRIDE; |
| virtual void OnDestruct() OVERRIDE; |
| |
| virtual void OnExecuteCode(const ExtensionMsg_ExecuteCode_Params& params); |
| virtual void OnPermitScriptInjection(int64 request_id); |
| |
| // Tells the ScriptInjectionManager to run tasks associated with |
| // document_idle. |
| void RunIdle(blink::WebFrame* frame); |
| |
| // Indicate that the given |frame| is no longer valid because it is starting |
| // a new load or closing. |
| void InvalidateFrame(blink::WebFrame* frame); |
| |
| // The owning ScriptInjectionManager. |
| ScriptInjectionManager* manager_; |
| |
| // The set of frames that we are about to notify for DOCUMENT_IDLE. We keep |
| // a set of those that are valid, so we don't notify that an invalid frame |
| // became idle. |
| std::set<blink::WebFrame*> pending_idle_frames_; |
| |
| base::WeakPtrFactory<RVOHelper> weak_factory_; |
| }; |
| |
| ScriptInjectionManager::RVOHelper::RVOHelper( |
| content::RenderView* render_view, |
| ScriptInjectionManager* manager) |
| : content::RenderViewObserver(render_view), |
| manager_(manager), |
| weak_factory_(this) { |
| } |
| |
| ScriptInjectionManager::RVOHelper::~RVOHelper() { |
| } |
| |
| bool ScriptInjectionManager::RVOHelper::OnMessageReceived( |
| const IPC::Message& message) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(ScriptInjectionManager::RVOHelper, message) |
| IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteCode, OnExecuteCode) |
| IPC_MESSAGE_HANDLER(ExtensionMsg_PermitScriptInjection, |
| OnPermitScriptInjection) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void ScriptInjectionManager::RVOHelper::DidCreateDocumentElement( |
| blink::WebLocalFrame* frame) { |
| manager_->InjectScripts(frame, UserScript::DOCUMENT_START); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::DidFinishDocumentLoad( |
| blink::WebLocalFrame* frame) { |
| manager_->InjectScripts(frame, UserScript::DOCUMENT_END); |
| pending_idle_frames_.insert(frame); |
| // We try to run idle in two places: here and DidFinishLoad. |
| // DidFinishDocumentLoad() corresponds to completing the document's load, |
| // whereas DidFinishLoad corresponds to completing the document and all |
| // subresources' load. We don't want to hold up script injection for a |
| // particularly slow subresource, so we set a delayed task from here - but if |
| // we finish everything before that point (i.e., DidFinishLoad() is |
| // triggered), then there's no reason to keep waiting. |
| base::MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&ScriptInjectionManager::RVOHelper::RunIdle, |
| weak_factory_.GetWeakPtr(), |
| frame), |
| base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs)); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::DidFinishLoad( |
| blink::WebLocalFrame* frame) { |
| // Ensure that we don't block any UI progress by running scripts. |
| // We *don't* add the frame to |pending_idle_frames_| here because |
| // DidFinishDocumentLoad should strictly come before DidFinishLoad, so the |
| // first posted task to RunIdle() pops it out of the set. This ensures we |
| // don't try to run idle twice. |
| base::MessageLoop::current()->PostTask( |
| FROM_HERE, |
| base::Bind(&ScriptInjectionManager::RVOHelper::RunIdle, |
| weak_factory_.GetWeakPtr(), |
| frame)); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::DidStartProvisionalLoad( |
| blink::WebLocalFrame* frame) { |
| // We're starting a new load - invalidate. |
| InvalidateFrame(frame); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::FrameDetached(blink::WebFrame* frame) { |
| // The frame is closing - invalidate. |
| InvalidateFrame(frame); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::OnDestruct() { |
| manager_->RemoveObserver(this); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::OnExecuteCode( |
| const ExtensionMsg_ExecuteCode_Params& params) { |
| manager_->HandleExecuteCode(params, render_view()); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::OnPermitScriptInjection( |
| int64 request_id) { |
| manager_->HandlePermitScriptInjection(request_id); |
| } |
| |
| void ScriptInjectionManager::RVOHelper::RunIdle(blink::WebFrame* frame) { |
| // Only notify the manager if the frame hasn't either been removed or already |
| // had idle run since the task to RunIdle() was posted. |
| if (pending_idle_frames_.count(frame) > 0) { |
| manager_->InjectScripts(frame, UserScript::DOCUMENT_IDLE); |
| pending_idle_frames_.erase(frame); |
| } |
| } |
| |
| void ScriptInjectionManager::RVOHelper::InvalidateFrame( |
| blink::WebFrame* frame) { |
| pending_idle_frames_.erase(frame); |
| manager_->InvalidateForFrame(frame); |
| } |
| |
| ScriptInjectionManager::ScriptInjectionManager( |
| const ExtensionSet* extensions, |
| UserScriptSetManager* user_script_set_manager) |
| : extensions_(extensions), |
| user_script_set_manager_(user_script_set_manager), |
| user_script_set_manager_observer_(this) { |
| user_script_set_manager_observer_.Add(user_script_set_manager_); |
| } |
| |
| ScriptInjectionManager::~ScriptInjectionManager() { |
| } |
| |
| void ScriptInjectionManager::OnRenderViewCreated( |
| content::RenderView* render_view) { |
| rvo_helpers_.push_back(new RVOHelper(render_view, this)); |
| } |
| |
| void ScriptInjectionManager::OnUserScriptsUpdated( |
| const std::set<std::string>& changed_extensions, |
| const std::vector<UserScript*>& scripts) { |
| for (ScopedVector<ScriptInjection>::iterator iter = |
| pending_injections_.begin(); |
| iter != pending_injections_.end();) { |
| if (changed_extensions.count((*iter)->extension_id()) > 0) |
| iter = pending_injections_.erase(iter); |
| else |
| ++iter; |
| } |
| } |
| |
| void ScriptInjectionManager::RemoveObserver(RVOHelper* helper) { |
| for (ScopedVector<RVOHelper>::iterator iter = rvo_helpers_.begin(); |
| iter != rvo_helpers_.end(); |
| ++iter) { |
| if (*iter == helper) { |
| rvo_helpers_.erase(iter); |
| break; |
| } |
| } |
| } |
| |
| void ScriptInjectionManager::InvalidateForFrame(blink::WebFrame* frame) { |
| for (ScopedVector<ScriptInjection>::iterator iter = |
| pending_injections_.begin(); |
| iter != pending_injections_.end();) { |
| if ((*iter)->web_frame() == frame) |
| iter = pending_injections_.erase(iter); |
| else |
| ++iter; |
| } |
| |
| frame_statuses_.erase(frame); |
| } |
| |
| void ScriptInjectionManager::InjectScripts( |
| blink::WebFrame* frame, UserScript::RunLocation run_location) { |
| FrameStatusMap::iterator iter = frame_statuses_.find(frame); |
| // We also don't execute if we detect that the run location is somehow out of |
| // order. This can happen if: |
| // - The first run location reported for the frame isn't DOCUMENT_START, or |
| // - The run location reported doesn't immediately follow the previous |
| // reported run location. |
| // We don't want to run because extensions may have requirements that scripts |
| // running in an earlier run location have run by the time a later script |
| // runs. Better to just not run. |
| if ((iter == frame_statuses_.end() && |
| run_location != UserScript::DOCUMENT_START) || |
| (iter != frame_statuses_.end() && run_location - iter->second > 1)) { |
| // We also invalidate the frame, because the run order of pending injections |
| // may also be bad. |
| InvalidateForFrame(frame); |
| return; |
| } else if (iter != frame_statuses_.end() && iter->second > run_location) { |
| // Certain run location signals (like DidCreateDocumentElement) can happen |
| // multiple times. Ignore the subsequent signals. |
| return; |
| } |
| |
| // Otherwise, all is right in the world, and we can get on with the |
| // injections! |
| |
| frame_statuses_[frame] = run_location; |
| |
| // Inject any scripts that were waiting for the right run location. |
| ScriptsRunInfo scripts_run_info; |
| for (ScopedVector<ScriptInjection>::iterator iter = |
| pending_injections_.begin(); |
| iter != pending_injections_.end();) { |
| if ((*iter)->web_frame() == frame && |
| (*iter)->TryToInject(run_location, |
| extensions_->GetByID((*iter)->extension_id()), |
| &scripts_run_info)) { |
| iter = pending_injections_.erase(iter); |
| } else { |
| ++iter; |
| } |
| } |
| |
| // Try to inject any user scripts that should run for this location. If they |
| // don't complete their injection (for example, waiting for a permission |
| // response) then they will be added to |pending_injections_|. |
| ScopedVector<ScriptInjection> user_script_injections; |
| int tab_id = ExtensionHelper::Get(content::RenderView::FromWebView( |
| frame->top()->view()))->tab_id(); |
| user_script_set_manager_->GetAllInjections( |
| &user_script_injections, frame, tab_id, run_location); |
| for (ScopedVector<ScriptInjection>::iterator iter = |
| user_script_injections.begin(); |
| iter != user_script_injections.end();) { |
| scoped_ptr<ScriptInjection> injection(*iter); |
| iter = user_script_injections.weak_erase(iter); |
| if (!injection->TryToInject(run_location, |
| extensions_->GetByID(injection->extension_id()), |
| &scripts_run_info)) { |
| pending_injections_.push_back(injection.release()); |
| } |
| } |
| |
| scripts_run_info.LogRun(frame, run_location); |
| } |
| |
| void ScriptInjectionManager::HandleExecuteCode( |
| const ExtensionMsg_ExecuteCode_Params& params, |
| content::RenderView* render_view) { |
| blink::WebFrame* main_frame = render_view->GetWebView()->mainFrame(); |
| if (!main_frame) { |
| render_view->Send( |
| new ExtensionHostMsg_ExecuteCodeFinished(render_view->GetRoutingID(), |
| params.request_id, |
| "No main frame", |
| GURL(std::string()), |
| base::ListValue())); |
| return; |
| } |
| |
| scoped_ptr<ScriptInjection> injection(new ScriptInjection( |
| scoped_ptr<ScriptInjector>( |
| new ProgrammaticScriptInjector(params, main_frame)), |
| main_frame, |
| params.extension_id, |
| static_cast<UserScript::RunLocation>(params.run_at), |
| ExtensionHelper::Get(render_view)->tab_id())); |
| |
| ScriptsRunInfo scripts_run_info; |
| FrameStatusMap::const_iterator iter = frame_statuses_.find(main_frame); |
| if (!injection->TryToInject( |
| iter == frame_statuses_.end() ? UserScript::UNDEFINED : iter->second, |
| extensions_->GetByID(injection->extension_id()), |
| &scripts_run_info)) { |
| pending_injections_.push_back(injection.release()); |
| } |
| } |
| |
| void ScriptInjectionManager::HandlePermitScriptInjection(int64 request_id) { |
| ScopedVector<ScriptInjection>::iterator iter = |
| pending_injections_.begin(); |
| for (; iter != pending_injections_.end(); ++iter) { |
| if ((*iter)->request_id() == request_id) |
| break; |
| } |
| if (iter == pending_injections_.end()) |
| return; |
| |
| // At this point, because the request is present in pending_injections_, we |
| // know that this is the same page that issued the request (otherwise, |
| // RVOHelper's DidStartProvisionalLoad callback would have caused it to be |
| // cleared out). |
| |
| scoped_ptr<ScriptInjection> injection(*iter); |
| pending_injections_.weak_erase(iter); |
| |
| ScriptsRunInfo scripts_run_info; |
| if (injection->OnPermissionGranted(extensions_->GetByID( |
| injection->extension_id()), |
| &scripts_run_info)) { |
| scripts_run_info.LogRun(injection->web_frame(), UserScript::RUN_DEFERRED); |
| } |
| } |
| |
| } // namespace extensions |