blob: 87560d4383085399dad0bc2b1b2f24ce7ffe2853 [file] [log] [blame]
// Copyright 2017 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/browser/extension_registrar.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "build/chromeos_buildflags.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/app_sorting.h"
#include "extensions/browser/blocklist_extension_prefs.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/lazy_context_id.h"
#include "extensions/browser/lazy_context_task_queue.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/renderer_startup_helper.h"
#include "extensions/browser/service_worker_task_queue.h"
#include "extensions/browser/task_queue_util.h"
#include "extensions/common/manifest_handlers/background_info.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/browser_util.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
using content::DevToolsAgentHost;
namespace extensions {
ExtensionRegistrar::ExtensionRegistrar(content::BrowserContext* browser_context,
Delegate* delegate)
: browser_context_(browser_context),
delegate_(delegate),
extension_system_(ExtensionSystem::Get(browser_context)),
extension_prefs_(ExtensionPrefs::Get(browser_context)),
registry_(ExtensionRegistry::Get(browser_context)),
renderer_helper_(
RendererStartupHelperFactory::GetForBrowserContext(browser_context)) {
// ExtensionRegistrar is created by ExtensionSystem via ExtensionService, and
// ExtensionSystemFactory depends on ProcessManager, so this should be safe.
auto* process_manager = ProcessManager::Get(browser_context_);
DCHECK(process_manager);
process_manager_observation_.Observe(process_manager);
}
ExtensionRegistrar::~ExtensionRegistrar() = default;
void ExtensionRegistrar::AddExtension(
scoped_refptr<const Extension> extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool is_extension_loaded = false;
const Extension* old = registry_->GetInstalledExtension(extension->id());
if (old) {
is_extension_loaded = true;
int version_compare_result = extension->version().CompareTo(old->version());
// Other than for unpacked extensions, we should not be downgrading.
if (!Manifest::IsUnpackedLocation(extension->location()) &&
version_compare_result < 0) {
UMA_HISTOGRAM_ENUMERATION(
"Extensions.AttemptedToDowngradeVersionLocation",
extension->location());
// TODO(https://crbug.com/810799): It would be awfully nice to CHECK this,
// but that's caused problems. There are apparently times when this
// happens that we aren't accounting for. We should track those down and
// fix them, but it can be tricky.
NOTREACHED() << "Attempted to downgrade extension."
<< "\nID: " << extension->id()
<< "\nOld Version: " << old->version()
<< "\nNew Version: " << extension->version()
<< "\nLocation: " << extension->location();
return;
}
}
// If the extension was disabled for a reload, we will enable it.
bool was_reloading = reloading_extensions_.erase(extension->id()) > 0;
// The extension is now loaded; remove its data from unloaded extension map.
unloaded_extension_paths_.erase(extension->id());
// If a terminated extension is loaded, remove it from the terminated list.
UntrackTerminatedExtension(extension->id());
// Notify the delegate we will add the extension.
delegate_->PreAddExtension(extension.get(), old);
if (was_reloading) {
failed_to_reload_unpacked_extensions_.erase(extension->path());
ReplaceReloadedExtension(extension);
} else {
if (is_extension_loaded) {
// To upgrade an extension in place, remove the old one and then activate
// the new one. ReloadExtension disables the extension, which is
// sufficient.
RemoveExtension(extension->id(), UnloadedExtensionReason::UPDATE);
}
AddNewExtension(extension);
}
}
void ExtensionRegistrar::AddNewExtension(
scoped_refptr<const Extension> extension) {
if (blocklist_prefs::IsExtensionBlocklisted(extension->id(),
extension_prefs_)) {
DCHECK(!Manifest::IsComponentLocation(extension->location()));
// Only prefs is checked for the blocklist. We rely on callers to check the
// blocklist before calling into here, e.g. CrxInstaller checks before
// installation then threads through the install and pending install flow
// of this class, and ExtensionService checks when loading installed
// extensions.
registry_->AddBlocklisted(extension);
} else if (delegate_->ShouldBlockExtension(extension.get())) {
DCHECK(!Manifest::IsComponentLocation(extension->location()));
registry_->AddBlocked(extension);
} else if (extension_prefs_->IsExtensionDisabled(extension->id())) {
registry_->AddDisabled(extension);
} else { // Extension should be enabled.
// All apps that are displayed in the launcher are ordered by their ordinals
// so we must ensure they have valid ordinals.
if (extension->RequiresSortOrdinal()) {
AppSorting* app_sorting = extension_system_->app_sorting();
app_sorting->SetExtensionVisible(extension->id(),
extension->ShouldDisplayInNewTabPage());
app_sorting->EnsureValidOrdinals(extension->id(),
syncer::StringOrdinal());
}
registry_->AddEnabled(extension);
ActivateExtension(extension.get(), true);
}
}
void ExtensionRegistrar::RemoveExtension(const ExtensionId& extension_id,
UnloadedExtensionReason reason) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
int include_mask = ExtensionRegistry::ENABLED | ExtensionRegistry::DISABLED |
ExtensionRegistry::TERMINATED;
scoped_refptr<const Extension> extension(
registry_->GetExtensionById(extension_id, include_mask));
// If the extension is blocked/blocklisted, no need to notify again.
if (!extension)
return;
if (registry_->terminated_extensions().Contains(extension_id)) {
// The extension was already deactivated from the call to
// TerminateExtension(), which also should have added it to
// unloaded_extension_paths_ if necessary.
registry_->RemoveTerminated(extension->id());
return;
}
// Keep information about the extension so that we can reload it later
// even if it's not permanently installed.
unloaded_extension_paths_[extension->id()] = extension->path();
// Stop tracking whether the extension was meant to be enabled after a reload.
reloading_extensions_.erase(extension->id());
if (registry_->enabled_extensions().Contains(extension_id)) {
registry_->RemoveEnabled(extension_id);
DeactivateExtension(extension.get(), reason);
} else {
// The extension was already deactivated from the call to
// DisableExtension().
bool removed = registry_->RemoveDisabled(extension->id());
DCHECK(removed);
}
}
void ExtensionRegistrar::EnableExtension(const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// If the extension is currently reloading, it will be enabled once the reload
// is complete.
if (reloading_extensions_.count(extension_id) > 0)
return;
// First, check that the extension can be enabled.
if (IsExtensionEnabled(extension_id) ||
blocklist_prefs::IsExtensionBlocklisted(extension_id, extension_prefs_) ||
registry_->blocked_extensions().Contains(extension_id)) {
return;
}
const Extension* extension =
registry_->disabled_extensions().GetByID(extension_id);
if (extension && !delegate_->CanEnableExtension(extension))
return;
// Now that we know the extension can be enabled, update the prefs.
extension_prefs_->SetExtensionEnabled(extension_id);
// This can happen if sync enables an extension that is not installed yet.
if (!extension)
return;
// Actually enable the extension.
registry_->AddEnabled(extension);
registry_->RemoveDisabled(extension->id());
ActivateExtension(extension, false);
}
void ExtensionRegistrar::DisableExtension(const ExtensionId& extension_id,
int disable_reasons) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_NE(disable_reason::DISABLE_NONE, disable_reasons);
scoped_refptr<const Extension> extension =
registry_->GetExtensionById(extension_id, ExtensionRegistry::EVERYTHING);
bool is_controlled_extension =
!delegate_->CanDisableExtension(extension.get());
if (is_controlled_extension) {
// Remove disallowed disable reasons.
// Certain disable reasons are always allowed, since they are more internal
// to the browser (rather than the user choosing to disable the extension).
int internal_disable_reason_mask =
extensions::disable_reason::DISABLE_RELOAD |
extensions::disable_reason::DISABLE_CORRUPTED |
extensions::disable_reason::DISABLE_UPDATE_REQUIRED_BY_POLICY |
extensions::disable_reason::DISABLE_BLOCKED_BY_POLICY |
extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED |
extensions::disable_reason::DISABLE_REINSTALL;
#if BUILDFLAG(IS_CHROMEOS_ASH)
// For controlled extensions, only allow disabling not ash-keeplisted
// extensions if Lacros is the only browser.
if (!crosapi::browser_util::IsAshWebBrowserEnabled()) {
internal_disable_reason_mask |=
extensions::disable_reason::DISABLE_NOT_ASH_KEEPLISTED;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
disable_reasons &= internal_disable_reason_mask;
if (disable_reasons == disable_reason::DISABLE_NONE)
return;
}
// The extension may have been disabled already. Just add the disable reasons.
if (!IsExtensionEnabled(extension_id)) {
extension_prefs_->AddDisableReasons(extension_id, disable_reasons);
return;
}
extension_prefs_->SetExtensionDisabled(extension_id, disable_reasons);
int include_mask =
ExtensionRegistry::EVERYTHING & ~ExtensionRegistry::DISABLED;
extension = registry_->GetExtensionById(extension_id, include_mask);
if (!extension)
return;
// The extension is either enabled or terminated.
DCHECK(registry_->enabled_extensions().Contains(extension->id()) ||
registry_->terminated_extensions().Contains(extension->id()));
// Move the extension to the disabled list.
registry_->AddDisabled(extension);
if (registry_->enabled_extensions().Contains(extension->id())) {
registry_->RemoveEnabled(extension->id());
DeactivateExtension(extension.get(), UnloadedExtensionReason::DISABLE);
} else {
// The extension must have been terminated. Don't send additional
// notifications for it being disabled.
bool removed = registry_->RemoveTerminated(extension->id());
DCHECK(removed);
}
}
namespace {
std::vector<scoped_refptr<DevToolsAgentHost>> GetDevToolsAgentHostsFor(
ProcessManager* process_manager,
const Extension* extension) {
std::vector<scoped_refptr<DevToolsAgentHost>> result;
if (!BackgroundInfo::IsServiceWorkerBased(extension)) {
ExtensionHost* host =
process_manager->GetBackgroundHostForExtension(extension->id());
if (host && content::DevToolsAgentHost::HasFor(host->host_contents())) {
result.push_back(
content::DevToolsAgentHost::GetOrCreateFor(host->host_contents()));
}
} else {
content::ServiceWorkerContext* context =
util::GetStoragePartitionForExtensionId(
extension->id(), process_manager->browser_context())
->GetServiceWorkerContext();
std::vector<WorkerId> service_worker_ids =
process_manager->GetServiceWorkersForExtension(extension->id());
for (const auto& worker_id : service_worker_ids) {
auto devtools_host =
DevToolsAgentHost::GetForServiceWorker(context, worker_id.version_id);
if (devtools_host)
result.push_back(std::move(devtools_host));
}
}
return result;
}
} // namespace
void ExtensionRegistrar::ReloadExtension(
const ExtensionId extension_id, // Passed by value because reloading can
// invalidate a reference to the ID.
LoadErrorBehavior load_error_behavior) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::FilePath path;
const Extension* disabled_extension =
registry_->disabled_extensions().GetByID(extension_id);
if (disabled_extension) {
path = disabled_extension->path();
}
// If the extension is already reloading, don't reload again.
if (extension_prefs_->HasDisableReason(extension_id,
disable_reason::DISABLE_RELOAD)) {
DCHECK(disabled_extension);
// If an unpacked extension previously failed to reload, it will still be
// marked as disabled, but we can try to reload it again - the developer
// may have fixed the issue.
if (failed_to_reload_unpacked_extensions_.count(path) == 0)
return;
failed_to_reload_unpacked_extensions_.erase(path);
}
// Ignore attempts to reload a blocklisted or blocked extension. Sometimes
// this can happen in a convoluted reload sequence triggered by the
// termination of a blocklisted or blocked extension and a naive attempt to
// reload it. For an example see http://crbug.com/373842.
if (registry_->blocklisted_extensions().Contains(extension_id) ||
registry_->blocked_extensions().Contains(extension_id)) {
return;
}
const Extension* enabled_extension =
registry_->enabled_extensions().GetByID(extension_id);
// Disable the extension if it's loaded. It might not be loaded if it crashed.
if (enabled_extension) {
// If the extension has an inspector open for its background page, detach
// the inspector and hang onto a cookie for it, so that we can reattach
// later.
// TODO(yoz): this is not incognito-safe!
ProcessManager* manager = ProcessManager::Get(browser_context_);
auto agent_hosts = GetDevToolsAgentHostsFor(manager, enabled_extension);
if (!agent_hosts.empty()) {
for (auto& host : agent_hosts) {
// Let DevTools know we'll be back once extension is reloaded.
// TODO(caseq): this should rather be called Disconnect().
host->DisconnectWebContents();
}
// Retain DevToolsAgentHosts for the extension being reloaded to prevent
// client disconnecting. We will re-attach later, when the extension is
// loaded.
// TODO(crbug.com/1246530): clean up upon failure to reload.
orphaned_dev_tools_[extension_id] = std::move(agent_hosts);
}
path = enabled_extension->path();
DisableExtension(extension_id, disable_reason::DISABLE_RELOAD);
DCHECK(registry_->disabled_extensions().Contains(extension_id));
reloading_extensions_.insert(extension_id);
} else if (!disabled_extension) {
std::map<ExtensionId, base::FilePath>::const_iterator iter =
unloaded_extension_paths_.find(extension_id);
if (iter == unloaded_extension_paths_.end()) {
return;
}
path = unloaded_extension_paths_[extension_id];
}
delegate_->LoadExtensionForReload(extension_id, path, load_error_behavior);
}
void ExtensionRegistrar::OnUnpackedExtensionReloadFailed(
const base::FilePath& path) {
failed_to_reload_unpacked_extensions_.insert(path);
}
void ExtensionRegistrar::TerminateExtension(const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
scoped_refptr<const Extension> extension =
registry_->enabled_extensions().GetByID(extension_id);
if (!extension)
return;
// Keep information about the extension so that we can reload it later
// even if it's not permanently installed.
unloaded_extension_paths_[extension->id()] = extension->path();
DCHECK(!base::Contains(reloading_extensions_, extension->id()))
<< "Enabled extension shouldn't be marked for reloading";
registry_->AddTerminated(extension);
registry_->RemoveEnabled(extension_id);
DeactivateExtension(extension.get(), UnloadedExtensionReason::TERMINATE);
}
void ExtensionRegistrar::UntrackTerminatedExtension(
const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
scoped_refptr<const Extension> extension =
registry_->terminated_extensions().GetByID(extension_id);
if (!extension)
return;
registry_->RemoveTerminated(extension_id);
}
bool ExtensionRegistrar::IsExtensionEnabled(
const ExtensionId& extension_id) const {
if (registry_->enabled_extensions().Contains(extension_id) ||
registry_->terminated_extensions().Contains(extension_id)) {
return true;
}
if (registry_->disabled_extensions().Contains(extension_id) ||
registry_->blocklisted_extensions().Contains(extension_id) ||
registry_->blocked_extensions().Contains(extension_id)) {
return false;
}
if (delegate_->ShouldBlockExtension(nullptr))
return false;
// If the extension hasn't been loaded yet, check the prefs for it. Assume
// enabled unless otherwise noted.
return !extension_prefs_->IsExtensionDisabled(extension_id) &&
!blocklist_prefs::IsExtensionBlocklisted(extension_id,
extension_prefs_) &&
!extension_prefs_->IsExternalExtensionUninstalled(extension_id);
}
void ExtensionRegistrar::DidCreateMainFrameForBackgroundPage(
ExtensionHost* host) {
auto iter = orphaned_dev_tools_.find(host->extension_id());
if (iter == orphaned_dev_tools_.end())
return;
// Keepalive count is reset on extension reload. This re-establishes the
// keepalive that was added when the DevTools agent was initially attached.
ProcessManager::Get(browser_context_)
->IncrementLazyKeepaliveCount(host->extension(), Activity::DEV_TOOLS,
std::string());
DCHECK_GE(1u, iter->second.size());
// TODO(caseq): do we need to handle the case when the extension changed
// from SW-based to WC-based during reload?
iter->second[0]->ConnectWebContents(host->host_contents());
orphaned_dev_tools_.erase(iter);
}
void ExtensionRegistrar::ActivateExtension(const Extension* extension,
bool is_newly_added) {
// The URLRequestContexts need to be first to know that the extension
// was loaded. Otherwise a race can arise where a renderer that is created
// for the extension may try to load an extension URL with an extension id
// that the request context doesn't yet know about. The BrowserContext should
// ensure its URLRequestContexts appropriately discover the loaded extension.
extension_system_->RegisterExtensionWithRequestContexts(
extension,
base::BindOnce(
&ExtensionRegistrar::OnExtensionRegisteredWithRequestContexts,
weak_factory_.GetWeakPtr(), WrapRefCounted(extension)));
// Activate the extension before calling
// RendererStartupHelper::OnExtensionLoaded() below, so that we have
// activation information ready while we send ExtensionMsg_Load IPC.
//
// TODO(lazyboy): We should move all logic that is required to start up an
// extension to a separate class, instead of calling adhoc methods like
// service worker ones below.
ActivateTaskQueueForExtension(browser_context_, extension);
renderer_helper_->OnExtensionLoaded(*extension);
// Tell subsystems that use the ExtensionRegistryObserver::OnExtensionLoaded
// about the new extension.
//
// NOTE: It is important that this happen after notifying the renderers about
// the new extensions so that if we navigate to an extension URL in
// ExtensionRegistryObserver::OnExtensionLoaded the renderer is guaranteed to
// know about it.
registry_->TriggerOnLoaded(extension);
delegate_->PostActivateExtension(extension);
// When an existing extension is re-enabled, it may be necessary to spin up
// its lazy background page.
if (!is_newly_added)
MaybeSpinUpLazyBackgroundPage(extension);
}
void ExtensionRegistrar::DeactivateExtension(const Extension* extension,
UnloadedExtensionReason reason) {
registry_->TriggerOnUnloaded(extension, reason);
renderer_helper_->OnExtensionUnloaded(*extension);
extension_system_->UnregisterExtensionWithRequestContexts(extension->id());
DeactivateTaskQueueForExtension(browser_context_, extension);
delegate_->PostDeactivateExtension(extension);
}
bool ExtensionRegistrar::ReplaceReloadedExtension(
scoped_refptr<const Extension> extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// The extension must already be disabled, and the original extension has
// been unloaded.
CHECK(registry_->disabled_extensions().Contains(extension->id()));
if (!delegate_->CanEnableExtension(extension.get()))
return false;
// TODO(michaelpg): Other disable reasons might have been added after the
// reload started. We may want to keep the extension disabled and just remove
// the DISABLE_RELOAD reason in that case.
extension_prefs_->SetExtensionEnabled(extension->id());
// Move it over to the enabled list.
CHECK(registry_->RemoveDisabled(extension->id()));
CHECK(registry_->AddEnabled(extension));
ActivateExtension(extension.get(), false);
return true;
}
void ExtensionRegistrar::OnExtensionRegisteredWithRequestContexts(
scoped_refptr<const Extension> extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
registry_->AddReady(extension);
if (registry_->enabled_extensions().Contains(extension->id()))
registry_->TriggerOnReady(extension.get());
}
void ExtensionRegistrar::MaybeSpinUpLazyBackgroundPage(
const Extension* extension) {
if (!BackgroundInfo::HasLazyBackgroundPage(extension))
return;
// For orphaned devtools, we will reconnect devtools to it later in
// DidCreateMainFrameForBackgroundPage().
bool has_orphaned_dev_tools =
base::Contains(orphaned_dev_tools_, extension->id());
// Reloading component extension does not trigger install, so RuntimeAPI won't
// be able to detect its loading. Therefore, we need to spin up its lazy
// background page.
bool is_component_extension =
Manifest::IsComponentLocation(extension->location());
if (!has_orphaned_dev_tools && !is_component_extension)
return;
// Wake up the event page by posting a dummy task.
const LazyContextId context_id(browser_context_, extension->id());
context_id.GetTaskQueue()->AddPendingTask(context_id, base::DoNothing());
}
void ExtensionRegistrar::OnServiceWorkerRegistered(const WorkerId& worker_id) {
// Just release the host. We only get here when the new worker has been
// attached and resumed by the DevTools, and all we need in case of service
// worker-based extensions is to keep the host around for the target
// auto-attacher to do its job.
orphaned_dev_tools_.erase(worker_id.extension_id);
}
} // namespace extensions