| // Copyright 2012 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/web_applications/os_integration/mac/web_app_shortcut_mac.h" |
| |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #import "base/apple/foundation_util.h" |
| #include "base/check_is_test.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/task_runner.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/web_applications/os_integration/mac/apps_folder_support.h" |
| #import "chrome/browser/web_applications/os_integration/mac/bundle_info_plist.h" |
| #include "chrome/browser/web_applications/os_integration/mac/web_app_auto_login_util.h" |
| #include "chrome/browser/web_applications/os_integration/mac/web_app_shortcut_creator.h" |
| #include "chrome/browser/web_applications/os_integration/os_integration_test_override.h" |
| #include "chrome/browser/web_applications/os_integration/web_app_shortcut.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/common/content_switches.h" |
| |
| namespace web_app { |
| |
| bool AppShimCreationAndLaunchDisabledForTest() { |
| // Note: The kTestType switch is only added on browser tests, but not unit |
| // tests. Unit tests need to set the test override. |
| return base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kTestType) && |
| !OsIntegrationTestOverride::Get(); |
| } |
| |
| // Removes the app shim from the list of Login Items. |
| void RemoveAppShimFromLoginItems(const std::string& app_id) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| const std::string bundle_id = GetBundleIdentifierForShim(app_id); |
| auto bundle_infos = |
| BundleInfoPlist::SearchForBundlesById(bundle_id, GetChromeAppsFolder()); |
| for (const auto& bundle_info : bundle_infos) { |
| WebAppAutoLoginUtil::GetInstance()->RemoveFromLoginItems( |
| bundle_info.bundle_path()); |
| } |
| } |
| |
| std::string GetBundleIdentifierForShim(const std::string& app_id, |
| const base::FilePath& profile_path) { |
| // Note that this matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp. |
| if (!profile_path.empty()) { |
| // Replace spaces in the profile path with hyphen. |
| std::string normalized_profile_path; |
| base::ReplaceChars(profile_path.BaseName().value(), " ", "-", |
| &normalized_profile_path); |
| return base::StrCat({base::apple::BaseBundleID(), ".app.", |
| normalized_profile_path, "-", app_id}); |
| } |
| return base::StrCat({base::apple::BaseBundleID(), ".app.", app_id}); |
| } |
| |
| bool UseAdHocSigningForWebAppShims() { |
| // A disabled feature flag takes precedence over any enterprise policy. |
| if (!base::FeatureList::IsEnabled(features::kUseAdHocSigningForWebAppShims)) { |
| return false; |
| } |
| |
| // An explicitly enabled (via command line or chrome://flags) feature flag |
| // also takes precedence over any enterprise policy, to allow testing the |
| // behavior even if the enterprise policy is set to disabled. |
| if (base::FeatureList::GetInstance()->IsFeatureOverriddenFromCommandLine( |
| features::kUseAdHocSigningForWebAppShims.name, |
| base::FeatureList::OVERRIDE_ENABLE_FEATURE)) { |
| return true; |
| } |
| |
| // The browser's local_state can be null in tests. In that case there is no |
| // enterprise policy to consider. |
| if (PrefService* local_state = g_browser_process->local_state()) { |
| // Respect an enterprise policy if one is set. |
| if (local_state->IsManagedPreference( |
| prefs::kWebAppsUseAdHocCodeSigningForAppShims)) { |
| return local_state->GetBoolean( |
| prefs::kWebAppsUseAdHocCodeSigningForAppShims); |
| } |
| } |
| |
| return true; |
| } |
| |
| bool UseNotificationAttributionForWebAppShims() { |
| return base::FeatureList::IsEnabled( |
| features::kAppShimNotificationAttribution) && |
| UseAdHocSigningForWebAppShims(); |
| } |
| |
| namespace internals { |
| |
| void CreatePlatformShortcuts_WithUseAdHocSigningForWebAppShims( |
| const base::FilePath& app_data_path, |
| const ShortcutLocations& creation_locations, |
| ShortcutCreationReason creation_reason, |
| const ShortcutInfo& shortcut_info, |
| CreateShortcutsCallback callback, |
| bool use_ad_hoc_signing_for_web_app_shims) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| bool created_shortcuts = false; |
| { |
| WebAppShortcutCreator shortcut_creator( |
| app_data_path, GetChromeAppsFolder(), &shortcut_info, |
| use_ad_hoc_signing_for_web_app_shims); |
| created_shortcuts = |
| shortcut_creator.CreateShortcuts(creation_reason, creation_locations); |
| // Make sure WebAppShortcutCreator is destroyed before invoking the |
| // callback, as invoking the callback might for example invalidate the |
| // `shortcut_info` reference that is held by `shortcut_creator`. |
| } |
| std::move(callback).Run(created_shortcuts); |
| } |
| |
| void CreatePlatformShortcuts(const base::FilePath& app_data_path, |
| const ShortcutLocations& creation_locations, |
| ShortcutCreationReason creation_reason, |
| const ShortcutInfo& shortcut_info, |
| CreateShortcutsCallback callback) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| if (AppShimCreationAndLaunchDisabledForTest()) { |
| std::move(callback).Run(true); |
| return; |
| } |
| |
| content::GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(UseAdHocSigningForWebAppShims), |
| base::BindOnce(&CreatePlatformShortcuts_WithUseAdHocSigningForWebAppShims, |
| app_data_path, creation_locations, creation_reason, |
| std::cref(shortcut_info), std::move(callback))); |
| } |
| |
| ShortcutLocations GetAppExistingShortCutLocationImpl( |
| const ShortcutInfo& shortcut_info) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| |
| bool has_shortcuts = false; |
| const std::string bundle_id = GetBundleIdentifierForShim( |
| shortcut_info.app_id, shortcut_info.is_multi_profile |
| ? base::FilePath() |
| : shortcut_info.profile_path); |
| if (!BundleInfoPlist::SearchForBundlesById(bundle_id, GetChromeAppsFolder()) |
| .empty()) { |
| has_shortcuts = true; |
| } else if (shortcut_info.is_multi_profile) { |
| // If in multi-profile mode, search using the profile-scoped bundle id, in |
| // case the user has an old shim hanging around. |
| const std::string profile_scoped_bundle_id = GetBundleIdentifierForShim( |
| shortcut_info.app_id, shortcut_info.profile_path); |
| has_shortcuts = !BundleInfoPlist::SearchForBundlesById( |
| profile_scoped_bundle_id, GetChromeAppsFolder()) |
| .empty(); |
| } |
| |
| ShortcutLocations locations; |
| if (has_shortcuts) { |
| locations.applications_menu_location = APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; |
| } |
| return locations; |
| } |
| |
| void DeletePlatformShortcuts(const base::FilePath& app_data_path, |
| const ShortcutInfo& shortcut_info, |
| scoped_refptr<base::TaskRunner> result_runner, |
| DeleteShortcutsCallback callback) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| |
| if (test_override) { |
| CHECK_IS_TEST(); |
| test_override->RegisterProtocolSchemes(shortcut_info.app_id, |
| std::vector<std::string>()); |
| } |
| const std::string bundle_id = GetBundleIdentifierForShim( |
| shortcut_info.app_id, shortcut_info.profile_path); |
| auto bundle_infos = |
| BundleInfoPlist::SearchForBundlesById(bundle_id, GetChromeAppsFolder()); |
| bool result = true; |
| for (const auto& bundle_info : bundle_infos) { |
| WebAppAutoLoginUtil::GetInstance()->RemoveFromLoginItems( |
| bundle_info.bundle_path()); |
| if (!base::DeletePathRecursively(bundle_info.bundle_path())) { |
| result = false; |
| } |
| } |
| result_runner->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), result)); |
| } |
| |
| void DeleteMultiProfileShortcutsForApp(const std::string& app_id) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| const std::string bundle_id = GetBundleIdentifierForShim(app_id); |
| auto bundle_infos = |
| BundleInfoPlist::SearchForBundlesById(bundle_id, GetChromeAppsFolder()); |
| for (const auto& bundle_info : bundle_infos) { |
| WebAppAutoLoginUtil::GetInstance()->RemoveFromLoginItems( |
| bundle_info.bundle_path()); |
| base::DeletePathRecursively(bundle_info.bundle_path()); |
| } |
| } |
| |
| void UpdatePlatformShortcuts_WithUseAdHocSigningForWebAppShims( |
| const base::FilePath& app_data_path, |
| const std::u16string& old_app_title, |
| std::optional<ShortcutLocations> user_specified_locations, |
| ResultCallback callback, |
| const ShortcutInfo& shortcut_info, |
| bool use_ad_hoc_signing_for_web_app_shims) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| WebAppShortcutCreator shortcut_creator(app_data_path, GetChromeAppsFolder(), |
| &shortcut_info, |
| use_ad_hoc_signing_for_web_app_shims); |
| std::vector<base::FilePath> updated_shim_paths; |
| Result result = (shortcut_creator.UpdateShortcuts(/*create_if_needed=*/false, |
| &updated_shim_paths) |
| ? Result::kOk |
| : Result::kError); |
| std::move(callback).Run(result); |
| } |
| |
| void UpdatePlatformShortcuts( |
| const base::FilePath& app_data_path, |
| const std::u16string& old_app_title, |
| std::optional<ShortcutLocations> user_specified_locations, |
| ResultCallback callback, |
| const ShortcutInfo& shortcut_info) { |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| if (AppShimCreationAndLaunchDisabledForTest()) { |
| std::move(callback).Run(Result::kOk); |
| return; |
| } |
| |
| content::GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&UseAdHocSigningForWebAppShims), |
| base::BindOnce(&UpdatePlatformShortcuts_WithUseAdHocSigningForWebAppShims, |
| app_data_path, old_app_title, |
| std::move(user_specified_locations), std::move(callback), |
| std::cref(shortcut_info))); |
| } |
| |
| void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| // If this is set, then keeping this as a local variable ensures it is not |
| // destroyed while we use state from it (retrieved in |
| // `GetChromeAppsFolder()`). |
| scoped_refptr<OsIntegrationTestOverride> test_override = |
| web_app::OsIntegrationTestOverride::Get(); |
| std::list<BundleInfoPlist> bundles_info = |
| BundleInfoPlist::GetAllInPath(GetChromeAppsFolder(), /*recursive=*/true); |
| for (const auto& info : bundles_info) { |
| if (!info.IsForCurrentUserDataDir()) { |
| continue; |
| } |
| if (!info.IsForProfile(profile_path)) { |
| continue; |
| } |
| WebAppAutoLoginUtil::GetInstance()->RemoveFromLoginItems( |
| info.bundle_path()); |
| base::DeletePathRecursively(info.bundle_path()); |
| } |
| } |
| |
| } // namespace internals |
| |
| } // namespace web_app |