| // 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_action_manager.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/location_bar_controller.h" |
| #include "chrome/browser/extensions/permissions_updater.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/profiles/profile.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/manifest.h" |
| #include "extensions/common/permissions/permission_set.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "ipc/ipc_message_macros.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Returns true if the extension should be regarded as a "permitted" extension |
| // for the case of metrics. We need this because we only actually withhold |
| // permissions if the switch is enabled, but want to record metrics in all |
| // cases. |
| // "ExtensionWouldHaveHadHostPermissionsWithheldIfSwitchWasOn()" would be |
| // more accurate, but too long. |
| bool ShouldRecordExtension(const Extension* extension) { |
| return extension->ShouldDisplayInExtensionSettings() && |
| !Manifest::IsPolicyLocation(extension->location()) && |
| !Manifest::IsComponentLocation(extension->location()) && |
| !PermissionsData::CanExecuteScriptEverywhere(extension) && |
| extension->permissions_data() |
| ->active_permissions() |
| ->ShouldWarnAllHosts(); |
| } |
| |
| } // namespace |
| |
| 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(); |
| } |
| |
| 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); |
| } |
| |
| void ActiveScriptController::AlwaysRunOnVisibleOrigin( |
| const Extension* extension) { |
| const GURL& url = web_contents()->GetVisibleURL(); |
| URLPatternSet new_explicit_hosts; |
| URLPatternSet new_scriptable_hosts; |
| |
| scoped_refptr<const PermissionSet> withheld_permissions = |
| extension->permissions_data()->withheld_permissions(); |
| if (withheld_permissions->explicit_hosts().MatchesURL(url)) { |
| new_explicit_hosts.AddOrigin(UserScript::ValidUserScriptSchemes(), |
| url.GetOrigin()); |
| } |
| if (withheld_permissions->scriptable_hosts().MatchesURL(url)) { |
| new_scriptable_hosts.AddOrigin(UserScript::ValidUserScriptSchemes(), |
| url.GetOrigin()); |
| } |
| |
| scoped_refptr<PermissionSet> new_permissions = |
| new PermissionSet(APIPermissionSet(), |
| ManifestPermissionSet(), |
| new_explicit_hosts, |
| new_scriptable_hosts); |
| |
| // Update permissions for the session. This adds |new_permissions| to active |
| // permissions and granted permissions. |
| // TODO(devlin): Make sure that the permission is removed from |
| // withheld_permissions if appropriate. |
| PermissionsUpdater(web_contents()->GetBrowserContext()) |
| .AddPermissions(extension, new_permissions.get()); |
| |
| // Allow current tab to run injection. |
| OnClicked(extension); |
| } |
| |
| bool ActiveScriptController::HasActiveScriptAction(const Extension* extension) { |
| return enabled_ && active_script_actions_.count(extension->id()) > 0; |
| } |
| |
| 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(ExtensionActionManager::Get( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())) |
| ->GetBestFitAction(*extension, ActionInfo::TYPE_PAGE).release()); |
| action->SetIsVisible(ExtensionAction::kDefaultTabId, true); |
| |
| active_script_actions_[extension->id()] = action; |
| return action.get(); |
| } |
| |
| ExtensionAction::ShowAction ActiveScriptController::OnClicked( |
| const Extension* extension) { |
| DCHECK(ContainsKey(pending_requests_, extension->id())); |
| RunPendingForExtension(extension); |
| return ExtensionAction::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); |
| } |
| |
| PermissionsData::AccessType |
| ActiveScriptController::RequiresUserConsentForScriptInjection( |
| const Extension* extension, |
| UserScript::InjectionType type) { |
| CHECK(extension); |
| |
| // If the feature is not enabled, we automatically allow all extensions to |
| // run scripts. |
| if (!enabled_) |
| permitted_extensions_.insert(extension->id()); |
| |
| // Allow the extension if it's been explicitly granted permission. |
| if (permitted_extensions_.count(extension->id()) > 0) |
| return PermissionsData::ACCESS_ALLOWED; |
| |
| GURL url = web_contents()->GetVisibleURL(); |
| int tab_id = SessionID::IdForTab(web_contents()); |
| switch (type) { |
| case UserScript::CONTENT_SCRIPT: |
| return extension->permissions_data()->GetContentScriptAccess( |
| extension, url, url, tab_id, -1, NULL); |
| case UserScript::PROGRAMMATIC_SCRIPT: |
| return extension->permissions_data()->GetPageAccess( |
| extension, url, url, tab_id, -1, NULL); |
| } |
| |
| NOTREACHED(); |
| return PermissionsData::ACCESS_DENIED; |
| } |
| |
| void ActiveScriptController::RequestScriptInjection( |
| const Extension* extension, |
| const base::Closure& callback) { |
| CHECK(extension); |
| PendingRequestList& list = pending_requests_[extension->id()]; |
| list.push_back(callback); |
| |
| // 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::RunPendingForExtension( |
| const Extension* extension) { |
| DCHECK(extension); |
| |
| 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; |
| |
| // 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()); |
| |
| PendingRequestMap::iterator iter = pending_requests_.find(extension->id()); |
| if (iter == pending_requests_.end()) |
| return; |
| |
| 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) { |
| request->Run(); |
| } |
| |
| // Inform the location bar that the action is now gone. |
| LocationBarController::NotifyChange(web_contents()); |
| } |
| |
| void ActiveScriptController::OnRequestScriptInjectionPermission( |
| const std::string& extension_id, |
| UserScript::InjectionType script_type, |
| int64 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) { |
| if (ShouldRecordExtension(extension)) { |
| DCHECK(!enabled_); |
| permitted_extensions_.insert(extension->id()); |
| } |
| return; |
| } |
| |
| switch (RequiresUserConsentForScriptInjection(extension, script_type)) { |
| case PermissionsData::ACCESS_ALLOWED: |
| PermitScriptInjection(request_id); |
| break; |
| case PermissionsData::ACCESS_WITHHELD: |
| // This base::Unretained() is safe, because the callback is only invoked |
| // by this object. |
| RequestScriptInjection( |
| extension, |
| base::Bind(&ActiveScriptController::PermitScriptInjection, |
| base::Unretained(this), |
| request_id)); |
| break; |
| case PermissionsData::ACCESS_DENIED: |
| // We should usually only get a "deny access" if the page changed (as the |
| // renderer wouldn't have requested permission if the answer was always |
| // "no"). Just let the request fizzle and die. |
| break; |
| } |
| } |
| |
| void ActiveScriptController::PermitScriptInjection(int64 request_id) { |
| // This only sends the response to the renderer - the process of adding the |
| // extension to the list of |permitted_extensions_| is done elsewhere. |
| content::RenderViewHost* render_view_host = |
| web_contents()->GetRenderViewHost(); |
| if (render_view_host) { |
| render_view_host->Send(new ExtensionMsg_PermitScriptInjection( |
| 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_RequestScriptInjectionPermission, |
| OnRequestScriptInjectionPermission) |
| 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 |