| // Copyright 2023 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/ash/shimless_rma/diagnostics_app_profile_helper.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/webui/shimless_rma/backend/shimless_rma_delegate.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/files/file_path.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "chrome/browser/ash/shimless_rma/chrome_shimless_rma_delegate.h" |
| #include "chrome/browser/ash/shimless_rma/diagnostics_app_profile_helper_constants.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/extensions/crx_installer.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_command_scheduler.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/common/chromeos/extensions/chromeos_system_extension_info.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chromeos/ash/components/browser_context_helper/browser_context_helper.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/web_package/signed_web_bundles/signed_web_bundle_id.h" |
| #include "components/webapps/common/web_app_id.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "extensions/browser/crx_file_info.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/common/manifest_handlers/background_info.h" |
| #include "extensions/common/permissions/permission_message.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/verifier_formats.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/origin.h" |
| |
| namespace context { |
| class BrowserContext; |
| } |
| |
| namespace ash::shimless_rma { |
| |
| namespace { |
| |
| // Polling interval and the timeout to wait for the extension being ready. |
| constexpr base::TimeDelta kExtensionReadyPollingInterval = |
| base::Milliseconds(50); |
| constexpr base::TimeDelta kExtensionReadyPollingTimeout = base::Seconds(3); |
| |
| // The set of allowlisted permission policy for diagnostics IWA. |
| constexpr auto kAllowlistedPermissionPolicyStringMap = |
| base::MakeFixedFlatMap<blink::mojom::PermissionsPolicyFeature, int>( |
| {{blink::mojom::PermissionsPolicyFeature::kCamera, |
| IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_CAMERA}, |
| {blink::mojom::PermissionsPolicyFeature::kMicrophone, |
| IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_MICROPHONE}, |
| {blink::mojom::PermissionsPolicyFeature::kFullscreen, |
| IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_FULLSCREEN}, |
| {blink::mojom::PermissionsPolicyFeature::kHid, |
| IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION_HID_DEVICES}}); |
| |
| std::optional<url::Origin>& GetInstalledDiagnosticsAppOriginInternal() { |
| static base::NoDestructor<std::optional<url::Origin>> g_origin; |
| return *g_origin; |
| } |
| |
| extensions::ExtensionService* GetExtensionService( |
| content::BrowserContext* context) { |
| CHECK(context); |
| auto* system = extensions::ExtensionSystem::Get(context); |
| CHECK(system); |
| auto* service = system->extension_service(); |
| CHECK(service); |
| return service; |
| } |
| |
| void DisableAllExtensions(content::BrowserContext* context) { |
| auto* registry = extensions::ExtensionRegistry::Get(context); |
| CHECK(registry); |
| auto* service = GetExtensionService(context); |
| |
| std::vector<std::string> ids; |
| for (const auto& extension : registry->enabled_extensions()) { |
| ids.push_back(extension->id()); |
| } |
| for (const auto& extension : registry->terminated_extensions()) { |
| ids.push_back(extension->id()); |
| } |
| |
| for (const auto& id : ids) { |
| service->DisableExtension(id, |
| extensions::disable_reason::DISABLE_USER_ACTION); |
| } |
| } |
| |
| struct PrepareDiagnosticsAppProfileState { |
| PrepareDiagnosticsAppProfileState(); |
| PrepareDiagnosticsAppProfileState(PrepareDiagnosticsAppProfileState&) = |
| delete; |
| PrepareDiagnosticsAppProfileState& operator=( |
| PrepareDiagnosticsAppProfileState&) = delete; |
| ~PrepareDiagnosticsAppProfileState(); |
| |
| // Arguments |
| raw_ptr<DiagnosticsAppProfileHelperDelegate> delegate; |
| base::FilePath crx_path; |
| base::FilePath swbn_path; |
| ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextCallback callback; |
| // Reference to the crx_installer. |
| scoped_refptr<extensions::CrxInstaller> crx_installer = nullptr; |
| // Result. |
| raw_ptr<content::BrowserContext> context; |
| std::optional<std::string> extension_id; |
| std::optional<web_package::SignedWebBundleId> iwa_id; |
| std::optional<std::string> name; |
| std::optional<std::string> permission_message; |
| std::optional<GURL> iwa_start_url; |
| }; |
| |
| PrepareDiagnosticsAppProfileState::PrepareDiagnosticsAppProfileState() = |
| default; |
| |
| PrepareDiagnosticsAppProfileState::~PrepareDiagnosticsAppProfileState() = |
| default; |
| |
| void ReportError(std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| const std::string& message) { |
| std::move(state->callback).Run(base::unexpected(message)); |
| } |
| |
| void ReportSuccess(std::unique_ptr<PrepareDiagnosticsAppProfileState> state) { |
| CHECK(state->context); |
| CHECK(state->extension_id); |
| CHECK(state->iwa_id); |
| CHECK(state->iwa_start_url); |
| |
| GetInstalledDiagnosticsAppOriginInternal() = |
| url::Origin::Create(state->iwa_start_url.value()); |
| |
| std::move(state->callback) |
| .Run(base::ok( |
| ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextResult( |
| state->context, state->extension_id.value(), |
| state->iwa_id.value(), state->name.value(), |
| state->permission_message))); |
| } |
| |
| void OnIsolatedWebAppInstalled( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| base::expected<web_app::InstallIsolatedWebAppCommandSuccess, |
| web_app::InstallIsolatedWebAppCommandError> result) { |
| CHECK(state->context); |
| CHECK(state->extension_id); |
| CHECK(state->iwa_id); |
| |
| if (!result.has_value()) { |
| ReportError(std::move(state), "Failed to install Isolated web app: " + |
| result.error().message); |
| return; |
| } |
| |
| const web_app::WebApp* web_app = state->delegate->GetWebAppById( |
| web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId( |
| *state->iwa_id) |
| .app_id(), |
| state->context); |
| // TODO(b/294815884): Check this when installing the IWA after we can add |
| // custom checker. For now, we just install the IWA. Because we won't return |
| // the profile and won't launch the IWA it should be fine. |
| if (!web_app->permissions_policy().empty()) { |
| if (!ash::features:: |
| IsShimlessRMA3pDiagnosticsAllowPermissionPolicyEnabled()) { |
| ReportError(std::move(state), k3pDiagErrorIWACannotHasPermissionPolicy); |
| return; |
| } |
| |
| std::u16string permission_message; |
| for (const auto& permission_policy : web_app->permissions_policy()) { |
| if (!kAllowlistedPermissionPolicyStringMap.contains( |
| permission_policy.feature)) { |
| ReportError(std::move(state), k3pDiagErrorIWACannotHasPermissionPolicy); |
| return; |
| } |
| base::StrAppend( |
| &permission_message, |
| {u"- ", |
| l10n_util::GetStringUTF16(kAllowlistedPermissionPolicyStringMap.at( |
| permission_policy.feature)), |
| u"\n"}); |
| } |
| state->permission_message = state->permission_message.value_or(""); |
| base::StrAppend(&*state->permission_message, |
| {base::UTF16ToUTF8(l10n_util::GetStringUTF16( |
| IDS_ASH_SHIMLESS_RMA_APP_ACCESS_PERMISSION)), |
| "\n", base::UTF16ToUTF8(permission_message)}); |
| } |
| |
| state->name = web_app->untranslated_name(); |
| state->iwa_start_url = web_app->start_url(); |
| |
| ReportSuccess(std::move(state)); |
| } |
| |
| void InstallIsolatedWebApp( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state) { |
| CHECK(state->context); |
| CHECK(state->extension_id); |
| |
| auto info = |
| chromeos::GetChromeOSExtensionInfoById(state->extension_id.value()); |
| if (!info.iwa_id) { |
| ReportError(std::move(state), "Extension " + state->extension_id.value() + |
| " doesn't have a connected IWA."); |
| return; |
| } |
| state->iwa_id = info.iwa_id; |
| |
| auto url_info = web_app::IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId( |
| state->iwa_id.value()); |
| auto install_source = web_app::IsolatedWebAppInstallSource::FromShimlessRma( |
| web_app::IwaSourceBundleProdModeWithFileOp( |
| state->swbn_path, web_app::IwaSourceBundleProdFileOp::kCopy)); |
| state->delegate->GetWebAppCommandScheduler(state->context) |
| ->InstallIsolatedWebApp( |
| url_info, install_source, |
| /*expected_version=*/std::nullopt, /*optional_keep_alive=*/nullptr, |
| /*optional_profile_keep_alive=*/nullptr, |
| base::BindOnce(&OnIsolatedWebAppInstalled, std::move(state))); |
| } |
| |
| void CheckExtensionIsReady( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| GURL script_url, |
| blink::StorageKey storage_key, |
| base::Time start_time); |
| |
| void OnCheckExtensionIsReadyResponse( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| GURL script_url, |
| blink::StorageKey storage_key, |
| base::Time start_time, |
| content::ServiceWorkerCapability capability) { |
| if (capability == content::ServiceWorkerCapability::NO_SERVICE_WORKER) { |
| // The service worker could still be registering, or the extension is failed |
| // to be activated. |
| if (base::Time::Now() - start_time <= kExtensionReadyPollingTimeout) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&CheckExtensionIsReady, std::move(state), script_url, |
| storage_key, start_time), |
| kExtensionReadyPollingInterval); |
| return; |
| } |
| ReportError(std::move(state), k3pDiagErrorCannotActivateExtension); |
| return; |
| } |
| |
| InstallIsolatedWebApp(std::move(state)); |
| } |
| |
| void CheckExtensionIsReady( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| GURL script_url, |
| blink::StorageKey storage_key, |
| base::Time start_time) { |
| content::ServiceWorkerContext* service_worker_context = |
| state->delegate->GetServiceWorkerContextForExtensionId( |
| state->extension_id.value(), state->context); |
| // Extensions register a service worker. Diagnostics app IWAs need to be |
| // started after the service worker is up to communicate with the extensions. |
| service_worker_context->CheckHasServiceWorker( |
| script_url, storage_key, |
| base::BindOnce(&OnCheckExtensionIsReadyResponse, std::move(state), |
| script_url, storage_key, start_time)); |
| } |
| |
| void OnExtensionInstalled( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| const std::optional<extensions::CrxInstallError>& error) { |
| CHECK(state->context); |
| CHECK(state->crx_installer); |
| |
| if (error) { |
| ReportError(std::move(state), |
| "Failed to install 3p diagnostics extension: " + |
| base::UTF16ToUTF8(error->message())); |
| return; |
| } |
| const extensions::Extension* extension = state->crx_installer->extension(); |
| CHECK(extension); |
| state->extension_id = extension->id(); |
| state->crx_installer.reset(); |
| |
| if (!chromeos::IsChromeOSSystemExtension(extension->id())) { |
| ReportError(std::move(state), |
| base::StringPrintf(k3pDiagErrorNotChromeOSSystemExtension, |
| extension->id().c_str())); |
| return; |
| } |
| |
| if (!extension->install_warnings().empty()) { |
| LOG(ERROR) |
| << "Extension " << extension->id() |
| << " may not work as expected because of these install warnings:"; |
| for (const auto& warning : extension->install_warnings()) { |
| LOG(ERROR) << warning.message; |
| } |
| } |
| |
| extensions::PermissionMessages permission_messages = |
| extension->permissions_data()->GetPermissionMessages(); |
| if (permission_messages.empty()) { |
| state->permission_message = std::nullopt; |
| } else { |
| std::u16string message; |
| for (const auto& permission_message : permission_messages) { |
| base::StrAppend(&message, {permission_message.message(), u"\n"}); |
| for (const auto& submessage : permission_message.submessages()) { |
| base::StrAppend(&message, {u"- ", submessage, u"\n"}); |
| } |
| } |
| state->permission_message = base::UTF16ToUTF8(message); |
| } |
| |
| GetExtensionService(state->context)->EnableExtension(extension->id()); |
| // Reload the extension to make sure old service worker are cleaned. This is |
| // important when the extension has already been installed to the profile. |
| GetExtensionService(state->context)->ReloadExtension(extension->id()); |
| |
| GURL script_url = extension->GetResourceURL( |
| extensions::BackgroundInfo::GetBackgroundServiceWorkerScript(extension)); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&CheckExtensionIsReady, std::move(state), script_url, |
| blink::StorageKey::CreateFirstParty( |
| url::Origin::Create(extension->url())), |
| base::Time::Now()), |
| kExtensionReadyPollingInterval); |
| } |
| |
| void InstallExtension( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state) { |
| CHECK(state->context); |
| |
| auto crx_installer = extensions::CrxInstaller::CreateSilent( |
| GetExtensionService(state->context)); |
| state->crx_installer = crx_installer; |
| const base::FilePath& crx_path = state->crx_path; |
| crx_installer->AddInstallerCallback( |
| base::BindOnce(&OnExtensionInstalled, std::move(state))); |
| const crx_file::VerifierFormat verifier_format = |
| ash::features::IsShimlessRMA3pDiagnosticsDevModeEnabled() |
| ? extensions::GetTestVerifierFormat() |
| : extensions::GetWebstoreVerifierFormat( |
| /*test_publisher_enabled=*/false); |
| crx_installer->InstallCrxFile( |
| extensions::CRXFileInfo{crx_path, verifier_format}); |
| } |
| |
| void OnExtensionSystemReady( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state) { |
| CHECK(state->context); |
| |
| DisableAllExtensions(state->context); |
| InstallExtension(std::move(state)); |
| } |
| |
| void OnProfileLoaded(std::unique_ptr<PrepareDiagnosticsAppProfileState> state, |
| Profile* profile) { |
| if (!profile) { |
| ReportError(std::move(state), |
| "Failed to load shimless diagnostics app profile."); |
| return; |
| } |
| // Extensions and IWAs should be installed to the original profile. |
| if (profile->IsOffTheRecord()) { |
| profile = profile->GetOriginalProfile(); |
| } |
| profile->GetPrefs()->SetBoolean(prefs::kForceEphemeralProfiles, true); |
| |
| state->context = profile; |
| auto* system = extensions::ExtensionSystem::Get(state->context); |
| CHECK(system); |
| system->ready().Post( |
| FROM_HERE, base::BindOnce(&OnExtensionSystemReady, std::move(state))); |
| } |
| |
| void PrepareDiagnosticsAppProfileImpl( |
| std::unique_ptr<PrepareDiagnosticsAppProfileState> state) { |
| CHECK(g_browser_process); |
| CHECK(g_browser_process->profile_manager()); |
| CHECK(BrowserContextHelper::Get()); |
| // TODO(b/292227137): Use ScopedProfileKeepAlive before migrate this to |
| // LaCrOS. |
| g_browser_process->profile_manager()->CreateProfileAsync( |
| BrowserContextHelper::Get()->GetShimlessRmaAppBrowserContextPath(), |
| base::BindOnce(&OnProfileLoaded, std::move(state))); |
| } |
| |
| } // namespace |
| |
| DiagnosticsAppProfileHelperDelegate::DiagnosticsAppProfileHelperDelegate() = |
| default; |
| |
| DiagnosticsAppProfileHelperDelegate::~DiagnosticsAppProfileHelperDelegate() = |
| default; |
| |
| content::ServiceWorkerContext* |
| DiagnosticsAppProfileHelperDelegate::GetServiceWorkerContextForExtensionId( |
| const extensions::ExtensionId& extension_id, |
| content::BrowserContext* browser_context) { |
| return extensions::util::GetServiceWorkerContextForExtensionId( |
| extension_id, browser_context); |
| } |
| |
| web_app::WebAppCommandScheduler* |
| DiagnosticsAppProfileHelperDelegate::GetWebAppCommandScheduler( |
| content::BrowserContext* browser_context) { |
| auto* web_app_provider = web_app::WebAppProvider::GetForWebApps( |
| Profile::FromBrowserContext(browser_context)); |
| CHECK(web_app_provider); |
| return &web_app_provider->scheduler(); |
| } |
| |
| const web_app::WebApp* DiagnosticsAppProfileHelperDelegate::GetWebAppById( |
| const webapps::AppId& app_id, |
| content::BrowserContext* browser_context) { |
| auto* web_app_provider = web_app::WebAppProvider::GetForWebApps( |
| Profile::FromBrowserContext(browser_context)); |
| const web_app::WebAppRegistrar& registrar = |
| web_app_provider->registrar_unsafe(); |
| return registrar.GetAppById(app_id); |
| } |
| |
| const std::optional<url::Origin>& |
| DiagnosticsAppProfileHelperDelegate::GetInstalledDiagnosticsAppOrigin() { |
| return GetInstalledDiagnosticsAppOriginInternal(); |
| } |
| |
| void PrepareDiagnosticsAppProfile( |
| DiagnosticsAppProfileHelperDelegate* delegate, |
| const base::FilePath& crx_path, |
| const base::FilePath& swbn_path, |
| ShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextCallback callback) { |
| CHECK(::ash::features::IsShimlessRMA3pDiagnosticsEnabled()); |
| GetInstalledDiagnosticsAppOriginInternal() = std::nullopt; |
| auto state = std::make_unique<PrepareDiagnosticsAppProfileState>(); |
| state->delegate = delegate; |
| state->crx_path = crx_path; |
| state->swbn_path = swbn_path; |
| state->callback = std::move(callback); |
| PrepareDiagnosticsAppProfileImpl(std::move(state)); |
| } |
| |
| } // namespace ash::shimless_rma |