blob: 35e09843eb2ebda7371adc0f80777f232e53284c [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/accessibility/embedded_a11y_extension_loader.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/file_util.h"
namespace {
std::optional<base::Value::Dict> LoadManifestOnFileThread(
const base::FilePath& path,
const base::FilePath::CharType* manifest_filename,
bool localize) {
CHECK(extensions::GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence());
std::string error;
auto manifest =
extensions::file_util::LoadManifest(path, manifest_filename, &error);
if (!manifest) {
LOG(ERROR) << "Can't load " << path.Append(manifest_filename).AsUTF8Unsafe()
<< ": " << error;
return std::nullopt;
}
if (localize) {
// This is only called for Lacros component extensions which are loaded
// from a read-only rootfs partition, so it is safe to set
// |gzip_permission| to kAllowForTrustedSource.
bool localized = extension_l10n_util::LocalizeExtension(
path, &manifest.value(),
extension_l10n_util::GzippedMessagesPermission::kAllowForTrustedSource,
&error);
CHECK(localized) << error;
}
return manifest;
}
extensions::ComponentLoader* GetComponentLoader(Profile* profile) {
auto* extension_system = extensions::ExtensionSystem::Get(profile);
if (!extension_system) {
// May be missing on the Lacros login profile.
return nullptr;
}
auto* extension_service = extension_system->extension_service();
if (!extension_service) {
return nullptr;
}
return extension_service->component_loader();
}
} // namespace
EmbeddedA11yExtensionLoader::ExtensionInfo::ExtensionInfo(
const std::string& extension_id,
const std::string& extension_path,
const base::FilePath::CharType* extension_manifest_file,
bool should_localize)
: extension_id(extension_id),
extension_path(extension_path),
extension_manifest_file(extension_manifest_file),
should_localize(should_localize) {}
EmbeddedA11yExtensionLoader::ExtensionInfo::ExtensionInfo(
const ExtensionInfo& other) = default;
EmbeddedA11yExtensionLoader::ExtensionInfo::ExtensionInfo(ExtensionInfo&&) =
default;
EmbeddedA11yExtensionLoader::ExtensionInfo::~ExtensionInfo() = default;
// static
EmbeddedA11yExtensionLoader* EmbeddedA11yExtensionLoader::GetInstance() {
return base::Singleton<
EmbeddedA11yExtensionLoader,
base::LeakySingletonTraits<EmbeddedA11yExtensionLoader>>::get();
}
EmbeddedA11yExtensionLoader::EmbeddedA11yExtensionLoader() = default;
EmbeddedA11yExtensionLoader::~EmbeddedA11yExtensionLoader() = default;
void EmbeddedA11yExtensionLoader::Init() {
if (initialized_) {
return;
}
ProfileManager* profile_manager = g_browser_process->profile_manager();
profile_manager_observation_.Observe(profile_manager);
// Observe all existing profiles.
std::vector<Profile*> profiles =
g_browser_process->profile_manager()->GetLoadedProfiles();
for (auto* profile : profiles) {
observed_profiles_.AddObservation(profile);
}
for (const auto& extension : extension_map_) {
UpdateAllProfiles(extension.first);
}
initialized_ = true;
}
void EmbeddedA11yExtensionLoader::InstallA11yHelperExtensionForReadingMode() {
// TODO(crbug.com/324143642): Install a11y helper extension for all platforms.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
InstallExtensionWithId(extension_misc::kEmbeddedA11yHelperExtensionId,
extension_misc::kEmbeddedA11yHelperExtensionPath,
extension_misc::kEmbeddedA11yHelperManifestFilename,
/*should_localize=*/true);
#endif
}
void EmbeddedA11yExtensionLoader::RemoveA11yHelperExtensionForReadingMode() {
// TODO(crbug.com/324143642): Remove a11y helper extension for all platforms.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
RemoveExtensionWithId(extension_misc::kEmbeddedA11yHelperExtensionId);
#endif
}
void EmbeddedA11yExtensionLoader::InstallExtensionWithId(
const std::string& extension_id,
const std::string& extension_path,
const base::FilePath::CharType* manifest_name,
bool should_localize) {
if (extension_map_.contains(extension_id)) {
return;
}
ExtensionInfo new_extension = {extension_id, extension_path, manifest_name,
should_localize};
extension_map_.insert({extension_id, new_extension});
UpdateAllProfiles(extension_id);
}
void EmbeddedA11yExtensionLoader::RemoveExtensionWithId(
const std::string& extension_id) {
if (!extension_map_.contains(extension_id)) {
return;
}
extension_map_.erase(extension_id);
UpdateAllProfiles(extension_id);
}
void EmbeddedA11yExtensionLoader::AddExtensionChangedCallbackForTest(
base::RepeatingClosure callback) {
extension_installation_changed_callback_for_test_ = std::move(callback);
}
void EmbeddedA11yExtensionLoader::OnProfileWillBeDestroyed(Profile* profile) {
observed_profiles_.RemoveObservation(profile);
}
void EmbeddedA11yExtensionLoader::OnOffTheRecordProfileCreated(
Profile* off_the_record) {
observed_profiles_.AddObservation(off_the_record);
for (const auto& extension : extension_map_) {
UpdateProfile(off_the_record, extension.first);
}
}
void EmbeddedA11yExtensionLoader::OnProfileAdded(Profile* profile) {
observed_profiles_.AddObservation(profile);
for (const auto& extension : extension_map_) {
UpdateProfile(profile, extension.first);
}
}
void EmbeddedA11yExtensionLoader::OnProfileManagerDestroying() {
profile_manager_observation_.Reset();
}
void EmbeddedA11yExtensionLoader::UpdateAllProfiles(
const std::string& extension_id) {
std::vector<Profile*> profiles =
g_browser_process->profile_manager()->GetLoadedProfiles();
for (auto* profile : profiles) {
UpdateProfile(profile, extension_id);
if (profile->HasAnyOffTheRecordProfile()) {
const auto& otr_profiles = profile->GetAllOffTheRecordProfiles();
for (auto* otr_profile : otr_profiles) {
UpdateProfile(otr_profile, extension_id);
}
}
}
}
void EmbeddedA11yExtensionLoader::UpdateProfile(
Profile* profile,
const std::string& extension_id) {
if (extension_map_.contains(extension_id)) {
ExtensionInfo& extension = extension_map_.at(extension_id);
MaybeInstallExtension(profile, extension_id, extension.extension_path,
extension.extension_manifest_file,
extension.should_localize);
} else {
MaybeRemoveExtension(profile, extension_id);
}
}
void EmbeddedA11yExtensionLoader::MaybeRemoveExtension(
Profile* profile,
const std::string& extension_id) {
auto* component_loader = GetComponentLoader(profile);
if (!component_loader || !component_loader->Exists(extension_id)) {
return;
}
component_loader->Remove(extension_id);
if (extension_installation_changed_callback_for_test_) {
extension_installation_changed_callback_for_test_.Run();
}
}
void EmbeddedA11yExtensionLoader::MaybeInstallExtension(
Profile* profile,
const std::string& extension_id,
const std::string& extension_path,
const base::FilePath::CharType* manifest_name,
bool should_localize) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* component_loader = GetComponentLoader(profile);
if (!component_loader || component_loader->Exists(extension_id)) {
return;
}
base::FilePath resources_path;
if (!base::PathService::Get(chrome::DIR_RESOURCES, &resources_path)) {
NOTREACHED();
}
base::FilePath::StringType common_path;
#if BUILDFLAG(IS_WIN)
common_path = base::UTF8ToWide(extension_path);
#else
common_path = extension_path;
#endif
auto path = resources_path.Append(common_path);
extensions::GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&LoadManifestOnFileThread, path, manifest_name,
/*localize=*/should_localize),
base::BindOnce(&EmbeddedA11yExtensionLoader::InstallExtension,
weak_ptr_factory_.GetWeakPtr(), component_loader, path,
extension_id));
}
void EmbeddedA11yExtensionLoader::InstallExtension(
extensions::ComponentLoader* component_loader,
const base::FilePath& path,
const std::string& extension_id,
std::optional<base::Value::Dict> manifest) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (component_loader->Exists(extension_id)) {
// Because this is async and called from another thread, it's possible we
// already installed the extension. Don't try and reinstall in that case.
// This may happen on init, for example, when ash a11y feature state and
// new profiles are loaded all at the same time.
return;
}
CHECK(manifest) << "Unable to load extension manifest for extension "
<< extension_id;
std::string actual_id =
component_loader->Add(std::move(manifest.value()), path);
CHECK_EQ(actual_id, extension_id);
if (extension_installation_changed_callback_for_test_) {
extension_installation_changed_callback_for_test_.Run();
}
}