| // 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 "chrome/browser/extensions/active_script_controller.h" |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/metrics/histogram.h" |
| #include "base/stl_util.h" |
| #include "chrome/browser/extensions/active_tab_permission_granter.h" |
| #include "chrome/browser/extensions/extension_action.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/location_bar_controller.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/sessions/session_id.h" |
| #include "chrome/common/extensions/api/extension_action/action_info.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_messages.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "ipc/ipc_message_macros.h" |
| |
| namespace extensions { |
| |
| ActiveScriptController::PendingRequest::PendingRequest() : |
| page_id(-1) { |
| } |
| |
| ActiveScriptController::PendingRequest::PendingRequest( |
| const base::Closure& closure, |
| int page_id) |
| : closure(closure), |
| page_id(page_id) { |
| } |
| |
| ActiveScriptController::PendingRequest::~PendingRequest() { |
| } |
| |
| ActiveScriptController::ActiveScriptController( |
| content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents), |
| enabled_(FeatureSwitch::scripts_require_action()->IsEnabled()) { |
| CHECK(web_contents); |
| } |
| |
| ActiveScriptController::~ActiveScriptController() { |
| LogUMA(); |
| } |
| |
| // static |
| ActiveScriptController* ActiveScriptController::GetForWebContents( |
| content::WebContents* web_contents) { |
| if (!web_contents) |
| return NULL; |
| TabHelper* tab_helper = TabHelper::FromWebContents(web_contents); |
| if (!tab_helper) |
| return NULL; |
| LocationBarController* location_bar_controller = |
| tab_helper->location_bar_controller(); |
| // This should never be NULL. |
| DCHECK(location_bar_controller); |
| return location_bar_controller->active_script_controller(); |
| } |
| |
| bool ActiveScriptController::RequiresUserConsentForScriptInjection( |
| const Extension* extension) { |
| CHECK(extension); |
| if (!extension->permissions_data()->RequiresActionForScriptExecution( |
| extension, |
| SessionID::IdForTab(web_contents()), |
| web_contents()->GetVisibleURL()) || |
| util::AllowedScriptingOnAllUrls(extension->id(), |
| web_contents()->GetBrowserContext())) { |
| return false; |
| } |
| |
| // If the feature is not enabled, we automatically allow all extensions to |
| // run scripts. |
| if (!enabled_) |
| permitted_extensions_.insert(extension->id()); |
| |
| return permitted_extensions_.count(extension->id()) == 0; |
| } |
| |
| void ActiveScriptController::RequestScriptInjection( |
| const Extension* extension, |
| int page_id, |
| const base::Closure& callback) { |
| CHECK(extension); |
| PendingRequestList& list = pending_requests_[extension->id()]; |
| list.push_back(PendingRequest(callback, page_id)); |
| |
| // If this was the first entry, notify the location bar that there's a new |
| // icon. |
| if (list.size() == 1u) |
| LocationBarController::NotifyChange(web_contents()); |
| } |
| |
| void ActiveScriptController::OnActiveTabPermissionGranted( |
| const Extension* extension) { |
| RunPendingForExtension(extension); |
| } |
| |
| void ActiveScriptController::OnAdInjectionDetected( |
| const std::set<std::string>& ad_injectors) { |
| // We're only interested in data if there are ad injectors detected. |
| if (ad_injectors.empty()) |
| return; |
| |
| size_t num_preventable_ad_injectors = |
| base::STLSetIntersection<std::set<std::string> >( |
| ad_injectors, permitted_extensions_).size(); |
| |
| UMA_HISTOGRAM_COUNTS_100( |
| "Extensions.ActiveScriptController.PreventableAdInjectors", |
| num_preventable_ad_injectors); |
| UMA_HISTOGRAM_COUNTS_100( |
| "Extensions.ActiveScriptController.UnpreventableAdInjectors", |
| ad_injectors.size() - num_preventable_ad_injectors); |
| } |
| |
| ExtensionAction* ActiveScriptController::GetActionForExtension( |
| const Extension* extension) { |
| if (!enabled_ || pending_requests_.count(extension->id()) == 0) |
| return NULL; // No action for this extension. |
| |
| ActiveScriptMap::iterator existing = |
| active_script_actions_.find(extension->id()); |
| if (existing != active_script_actions_.end()) |
| return existing->second.get(); |
| |
| linked_ptr<ExtensionAction> action(new ExtensionAction( |
| extension->id(), ActionInfo::TYPE_PAGE, ActionInfo())); |
| action->SetTitle(ExtensionAction::kDefaultTabId, extension->name()); |
| action->SetIsVisible(ExtensionAction::kDefaultTabId, true); |
| |
| const ActionInfo* action_info = ActionInfo::GetPageActionInfo(extension); |
| if (!action_info) |
| action_info = ActionInfo::GetBrowserActionInfo(extension); |
| |
| if (action_info && !action_info->default_icon.empty()) { |
| action->set_default_icon( |
| make_scoped_ptr(new ExtensionIconSet(action_info->default_icon))); |
| } |
| |
| active_script_actions_[extension->id()] = action; |
| return action.get(); |
| } |
| |
| LocationBarController::Action ActiveScriptController::OnClicked( |
| const Extension* extension) { |
| DCHECK(ContainsKey(pending_requests_, extension->id())); |
| RunPendingForExtension(extension); |
| return LocationBarController::ACTION_NONE; |
| } |
| |
| void ActiveScriptController::OnNavigated() { |
| LogUMA(); |
| permitted_extensions_.clear(); |
| pending_requests_.clear(); |
| } |
| |
| void ActiveScriptController::OnExtensionUnloaded(const Extension* extension) { |
| PendingRequestMap::iterator iter = pending_requests_.find(extension->id()); |
| if (iter != pending_requests_.end()) |
| pending_requests_.erase(iter); |
| } |
| |
| void ActiveScriptController::RunPendingForExtension( |
| const Extension* extension) { |
| DCHECK(extension); |
| PendingRequestMap::iterator iter = |
| pending_requests_.find(extension->id()); |
| if (iter == pending_requests_.end()) |
| return; |
| |
| content::NavigationEntry* visible_entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| // Refuse to run if there's no visible entry, because we have no idea of |
| // determining if it's the proper page. This should rarely, if ever, happen. |
| if (!visible_entry) |
| return; |
| |
| int page_id = visible_entry->GetPageID(); |
| |
| // We add this to the list of permitted extensions and erase pending entries |
| // *before* running them to guard against the crazy case where running the |
| // callbacks adds more entries. |
| permitted_extensions_.insert(extension->id()); |
| PendingRequestList requests; |
| iter->second.swap(requests); |
| pending_requests_.erase(extension->id()); |
| |
| // Clicking to run the extension counts as granting it permission to run on |
| // the given tab. |
| // The extension may already have active tab at this point, but granting |
| // it twice is essentially a no-op. |
| TabHelper::FromWebContents(web_contents())-> |
| active_tab_permission_granter()->GrantIfRequested(extension); |
| |
| // Run all pending injections for the given extension. |
| for (PendingRequestList::iterator request = requests.begin(); |
| request != requests.end(); |
| ++request) { |
| // Only run if it's on the proper page. |
| if (request->page_id == page_id) |
| request->closure.Run(); |
| } |
| |
| // Inform the location bar that the action is now gone. |
| LocationBarController::NotifyChange(web_contents()); |
| } |
| |
| void ActiveScriptController::OnRequestContentScriptPermission( |
| const std::string& extension_id, |
| int page_id, |
| int request_id) { |
| if (!Extension::IdIsValid(extension_id)) { |
| NOTREACHED() << "'" << extension_id << "' is not a valid id."; |
| return; |
| } |
| |
| const Extension* extension = |
| ExtensionRegistry::Get(web_contents()->GetBrowserContext()) |
| ->enabled_extensions().GetByID(extension_id); |
| // We shouldn't allow extensions which are no longer enabled to run any |
| // scripts. Ignore the request. |
| if (!extension) |
| return; |
| |
| // If the request id is -1, that signals that the content script has already |
| // ran (because this feature is not enabled). Add the extension to the list of |
| // permitted extensions (for metrics), and return immediately. |
| if (request_id == -1) { |
| DCHECK(!enabled_); |
| permitted_extensions_.insert(extension->id()); |
| return; |
| } |
| |
| if (RequiresUserConsentForScriptInjection(extension)) { |
| // This base::Unretained() is safe, because the callback is only invoked by |
| // this object. |
| RequestScriptInjection( |
| extension, |
| page_id, |
| base::Bind(&ActiveScriptController::GrantContentScriptPermission, |
| base::Unretained(this), |
| request_id)); |
| } else { |
| GrantContentScriptPermission(request_id); |
| } |
| } |
| |
| void ActiveScriptController::GrantContentScriptPermission(int request_id) { |
| content::RenderViewHost* render_view_host = |
| web_contents()->GetRenderViewHost(); |
| if (render_view_host) { |
| render_view_host->Send(new ExtensionMsg_GrantContentScriptPermission( |
| render_view_host->GetRoutingID(), |
| request_id)); |
| } |
| } |
| |
| bool ActiveScriptController::OnMessageReceived(const IPC::Message& message) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(ActiveScriptController, message) |
| IPC_MESSAGE_HANDLER(ExtensionHostMsg_RequestContentScriptPermission, |
| OnRequestContentScriptPermission) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void ActiveScriptController::LogUMA() const { |
| UMA_HISTOGRAM_COUNTS_100( |
| "Extensions.ActiveScriptController.ShownActiveScriptsOnPage", |
| pending_requests_.size()); |
| |
| // We only log the permitted extensions metric if the feature is enabled, |
| // because otherwise the data will be boring (100% allowed). |
| if (enabled_) { |
| UMA_HISTOGRAM_COUNTS_100( |
| "Extensions.ActiveScriptController.PermittedExtensions", |
| permitted_extensions_.size()); |
| UMA_HISTOGRAM_COUNTS_100( |
| "Extensions.ActiveScriptController.DeniedExtensions", |
| pending_requests_.size()); |
| } |
| } |
| |
| } // namespace extensions |