| // 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.h" |
| |
| #include <vector> |
| |
| #include "base/lazy_instance.h" |
| #include "base/metrics/histogram.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/renderer/render_view.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_messages.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/renderer/dom_activity_logger.h" |
| #include "extensions/renderer/extension_groups.h" |
| #include "extensions/renderer/extension_helper.h" |
| #include "extensions/renderer/script_context.h" |
| #include "extensions/renderer/user_script_slave.h" |
| #include "grit/extensions_renderer_resources.h" |
| #include "third_party/WebKit/public/web/WebDocument.h" |
| #include "third_party/WebKit/public/web/WebFrame.h" |
| #include "third_party/WebKit/public/web/WebScriptSource.h" |
| #include "third_party/WebKit/public/web/WebView.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // The id of the next pending injection. |
| int64 g_next_pending_id = 0; |
| |
| // The number of an invalid request, which is used if the feature to delay |
| // script injection is not enabled. |
| const int64 kInvalidRequestId = -1; |
| |
| // These two strings are injected before and after the Greasemonkey API and |
| // user script to wrap it in an anonymous scope. |
| const char kUserScriptHead[] = "(function (unsafeWindow) {\n"; |
| const char kUserScriptTail[] = "\n})(window);"; |
| |
| // Greasemonkey API source that is injected with the scripts. |
| struct GreasemonkeyApiJsString { |
| GreasemonkeyApiJsString(); |
| blink::WebScriptSource source; |
| }; |
| |
| // The below constructor, monstrous as it is, just makes a WebScriptSource from |
| // the GreasemonkeyApiJs resource. |
| GreasemonkeyApiJsString::GreasemonkeyApiJsString() |
| : source(blink::WebScriptSource(blink::WebString::fromUTF8( |
| ResourceBundle::GetSharedInstance().GetRawDataResource( |
| IDR_GREASEMONKEY_API_JS).as_string()))) { |
| } |
| |
| base::LazyInstance<GreasemonkeyApiJsString> g_greasemonkey_api = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| } // namespace |
| |
| ScriptInjection::ScriptsRunInfo::ScriptsRunInfo() : num_css(0u), num_js(0u) { |
| } |
| |
| ScriptInjection::ScriptsRunInfo::~ScriptsRunInfo() { |
| } |
| |
| struct ScriptInjection::PendingInjection { |
| PendingInjection(blink::WebFrame* web_frame, |
| UserScript::RunLocation run_location, |
| int page_id); |
| ~PendingInjection(); |
| |
| // The globally-unique id of this request. |
| int64 id; |
| |
| // The pointer to the web frame into which the script should be injected. |
| // This is weak, but safe because we remove pending requests when a frame is |
| // terminated. |
| blink::WebFrame* web_frame; |
| |
| // The run location to inject at. |
| // Note: This could be a lie - we might inject well after this run location |
| // has come and gone. But we need to know it to know which scripts to inject. |
| UserScript::RunLocation run_location; |
| |
| // The corresponding page id, to protect against races. |
| int page_id; |
| }; |
| |
| ScriptInjection::PendingInjection::PendingInjection( |
| blink::WebFrame* web_frame, |
| UserScript::RunLocation run_location, |
| int page_id) |
| : id(g_next_pending_id++), |
| web_frame(web_frame), |
| run_location(run_location), |
| page_id(page_id) { |
| } |
| |
| ScriptInjection::PendingInjection::~PendingInjection() { |
| } |
| |
| // static |
| GURL ScriptInjection::GetDocumentUrlForFrame(blink::WebFrame* frame) { |
| GURL data_source_url = ScriptContext::GetDataSourceURLForFrame(frame); |
| if (!data_source_url.is_empty() && frame->isViewSourceModeEnabled()) { |
| data_source_url = GURL(content::kViewSourceScheme + std::string(":") + |
| data_source_url.spec()); |
| } |
| |
| return data_source_url; |
| } |
| |
| ScriptInjection::ScriptInjection( |
| scoped_ptr<UserScript> script, |
| UserScriptSlave* user_script_slave) |
| : script_(script.Pass()), |
| extension_id_(script_->extension_id()), |
| user_script_slave_(user_script_slave), |
| is_standalone_or_emulate_greasemonkey_( |
| script_->is_standalone() || script_->emulate_greasemonkey()) { |
| } |
| |
| ScriptInjection::~ScriptInjection() { |
| } |
| |
| void ScriptInjection::InjectIfAllowed(blink::WebFrame* frame, |
| UserScript::RunLocation run_location, |
| const GURL& document_url, |
| ScriptsRunInfo* scripts_run_info) { |
| if (!WantsToRun(frame, run_location, document_url)) |
| return; |
| |
| const Extension* extension = user_script_slave_->GetExtension(extension_id_); |
| DCHECK(extension); // WantsToRun() should be false if there's no extension. |
| |
| // We use the top render view here (instead of the render view for the |
| // frame), because script injection on any frame requires permission for |
| // the top frame. Additionally, if we have to show any UI for permissions, |
| // it should only be done on the top frame. |
| content::RenderView* top_render_view = |
| content::RenderView::FromWebView(frame->top()->view()); |
| |
| int tab_id = ExtensionHelper::Get(top_render_view)->tab_id(); |
| |
| // By default, we allow injection. |
| bool should_inject = true; |
| |
| // Check if the extension requires user consent for injection *and* we have a |
| // valid tab id (if we don't have a tab id, we have no UI surface to ask for |
| // user consent). |
| if (tab_id != -1 && |
| extension->permissions_data()->RequiresActionForScriptExecution( |
| extension, tab_id, frame->top()->document().url())) { |
| int64 request_id = kInvalidRequestId; |
| int page_id = top_render_view->GetPageId(); |
| |
| // We only delay the injection if the feature is enabled. |
| // Otherwise, we simply treat this as a notification by passing an invalid |
| // id. |
| if (FeatureSwitch::scripts_require_action()->IsEnabled()) { |
| should_inject = false; |
| ScopedVector<PendingInjection>::iterator pending_injection = |
| pending_injections_.insert( |
| pending_injections_.end(), |
| new PendingInjection(frame, run_location, page_id)); |
| request_id = (*pending_injection)->id; |
| } |
| |
| top_render_view->Send( |
| new ExtensionHostMsg_RequestContentScriptPermission( |
| top_render_view->GetRoutingID(), |
| extension->id(), |
| page_id, |
| request_id)); |
| } |
| |
| if (should_inject) |
| Inject(frame, run_location, scripts_run_info); |
| } |
| |
| bool ScriptInjection::NotifyScriptPermitted( |
| int64 request_id, |
| content::RenderView* render_view, |
| ScriptsRunInfo* scripts_run_info, |
| blink::WebFrame** frame_out) { |
| ScopedVector<PendingInjection>::iterator iter = pending_injections_.begin(); |
| while (iter != pending_injections_.end() && (*iter)->id != request_id) |
| ++iter; |
| |
| // No matching request. |
| if (iter == pending_injections_.end()) |
| return false; |
| |
| // We found the request, so pull it out of the pending list. |
| scoped_ptr<PendingInjection> pending_injection(*iter); |
| pending_injections_.weak_erase(iter); |
| |
| // Ensure the Page ID and Extension are still valid. Otherwise, don't inject. |
| if (render_view->GetPageId() != pending_injection->page_id) |
| return false; |
| |
| const Extension* extension = user_script_slave_->GetExtension(extension_id_); |
| if (!extension) |
| return false; |
| |
| // Everything matches! Inject the script. |
| if (frame_out) |
| *frame_out = pending_injection->web_frame; |
| Inject(pending_injection->web_frame, |
| pending_injection->run_location, |
| scripts_run_info); |
| return true; |
| } |
| |
| void ScriptInjection::FrameDetached(blink::WebFrame* frame) { |
| // Any pending injections associated with the given frame will never run. |
| // Remove them. |
| for (ScopedVector<PendingInjection>::iterator iter = |
| pending_injections_.begin(); |
| iter != pending_injections_.end();) { |
| if ((*iter)->web_frame == frame) |
| iter = pending_injections_.erase(iter); |
| else |
| ++iter; |
| } |
| } |
| |
| void ScriptInjection::SetScript(scoped_ptr<UserScript> script) { |
| script_.reset(script.release()); |
| } |
| |
| bool ScriptInjection::WantsToRun(blink::WebFrame* frame, |
| UserScript::RunLocation run_location, |
| const GURL& document_url) const { |
| if (frame->parent() && !script_->match_all_frames()) |
| return false; // Only match subframes if the script declared it wanted to. |
| |
| const Extension* extension = user_script_slave_->GetExtension(extension_id_); |
| // Since extension info is sent separately from user script info, they can |
| // be out of sync. We just ignore this situation. |
| if (!extension) |
| return false; |
| |
| // Content scripts are not tab-specific. |
| static const int kNoTabId = -1; |
| // We don't have a process id in this context. |
| static const int kNoProcessId = -1; |
| |
| GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL( |
| frame, document_url, script_->match_about_blank()); |
| |
| if (!script_->MatchesURL(effective_document_url)) |
| return false; |
| |
| if (!extension->permissions_data()->CanRunContentScriptOnPage( |
| extension, |
| effective_document_url, |
| frame->top()->document().url(), |
| kNoTabId, |
| kNoProcessId, |
| NULL /* ignore error */)) { |
| return false; |
| } |
| |
| return ShouldInjectCSS(run_location) || ShouldInjectJS(run_location); |
| } |
| |
| void ScriptInjection::Inject(blink::WebFrame* frame, |
| UserScript::RunLocation run_location, |
| ScriptsRunInfo* scripts_run_info) const { |
| DCHECK(frame); |
| DCHECK(scripts_run_info); |
| DCHECK(WantsToRun(frame, run_location, GetDocumentUrlForFrame(frame))); |
| DCHECK(user_script_slave_->GetExtension(extension_id_)); |
| |
| if (ShouldInjectCSS(run_location)) |
| InjectCSS(frame, scripts_run_info); |
| if (ShouldInjectJS(run_location)) |
| InjectJS(frame, scripts_run_info); |
| } |
| |
| bool ScriptInjection::ShouldInjectJS(UserScript::RunLocation run_location) |
| const { |
| return !script_->js_scripts().empty() && |
| script_->run_location() == run_location; |
| } |
| |
| bool ScriptInjection::ShouldInjectCSS(UserScript::RunLocation run_location) |
| const { |
| return !script_->css_scripts().empty() && |
| run_location == UserScript::DOCUMENT_START; |
| } |
| |
| void ScriptInjection::InjectJS(blink::WebFrame* frame, |
| ScriptsRunInfo* scripts_run_info) const { |
| const UserScript::FileList& js_scripts = script_->js_scripts(); |
| std::vector<blink::WebScriptSource> sources; |
| scripts_run_info->num_js += js_scripts.size(); |
| for (UserScript::FileList::const_iterator iter = js_scripts.begin(); |
| iter != js_scripts.end(); |
| ++iter) { |
| std::string content = iter->GetContent().as_string(); |
| |
| // We add this dumb function wrapper for standalone user script to |
| // emulate what Greasemonkey does. |
| // TODO(aa): I think that maybe "is_standalone" scripts don't exist |
| // anymore. Investigate. |
| if (is_standalone_or_emulate_greasemonkey_) { |
| content.insert(0, kUserScriptHead); |
| content += kUserScriptTail; |
| } |
| sources.push_back(blink::WebScriptSource( |
| blink::WebString::fromUTF8(content), iter->url())); |
| } |
| |
| // Emulate Greasemonkey API for scripts that were converted to extensions |
| // and "standalone" user scripts. |
| if (is_standalone_or_emulate_greasemonkey_) |
| sources.insert(sources.begin(), g_greasemonkey_api.Get().source); |
| |
| int isolated_world_id = |
| user_script_slave_->GetIsolatedWorldIdForExtension( |
| user_script_slave_->GetExtension(extension_id_), frame); |
| base::ElapsedTimer exec_timer; |
| DOMActivityLogger::AttachToWorld(isolated_world_id, extension_id_); |
| frame->executeScriptInIsolatedWorld(isolated_world_id, |
| &sources.front(), |
| sources.size(), |
| EXTENSION_GROUP_CONTENT_SCRIPTS); |
| UMA_HISTOGRAM_TIMES("Extensions.InjectScriptTime", exec_timer.Elapsed()); |
| |
| for (std::vector<blink::WebScriptSource>::const_iterator iter = |
| sources.begin(); |
| iter != sources.end(); |
| ++iter) { |
| scripts_run_info->executing_scripts[extension_id_].insert( |
| GURL(iter->url).path()); |
| } |
| } |
| |
| void ScriptInjection::InjectCSS(blink::WebFrame* frame, |
| ScriptsRunInfo* scripts_run_info) const { |
| const UserScript::FileList& css_scripts = script_->css_scripts(); |
| scripts_run_info->num_css += css_scripts.size(); |
| for (UserScript::FileList::const_iterator iter = css_scripts.begin(); |
| iter != css_scripts.end(); |
| ++iter) { |
| frame->document().insertStyleSheet( |
| blink::WebString::fromUTF8(iter->GetContent().as_string())); |
| } |
| } |
| |
| } // namespace extensions |