| // Copyright 2020 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/file_manager/app_service_file_tasks.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/web_app_id_constants.h" |
| #include "ash/webui/file_manager/url_constants.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/apps/app_service/app_icon_source.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_factory.h" |
| #include "chrome/browser/apps/app_service/launch_result_type.h" |
| #include "chrome/browser/apps/app_service/launch_utils.h" |
| #include "chrome/browser/apps/app_service/policy_util.h" |
| #include "chrome/browser/ash/crostini/crostini_features.h" |
| #include "chrome/browser/ash/file_manager/file_tasks.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/filesystem_api_util.h" |
| #include "chrome/browser/ash/file_manager/office_file_tasks.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/file_manager/virtual_file_tasks.h" |
| #include "chrome/browser/ash/fusebox/fusebox_server.h" |
| #include "chrome/browser/chromeos/upload_office_to_cloud/upload_office_to_cloud.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/webui/ash/cloud_upload/hats_office_trigger.h" |
| #include "chrome/browser/web_applications/os_integration/os_integration_manager.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/extensions/api/file_manager_private.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/ash/components/file_manager/app_id.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/services/app_service/public/cpp/app_launch_util.h" |
| #include "components/services/app_service/public/cpp/app_types.h" |
| #include "components/services/app_service/public/cpp/intent.h" |
| #include "components/services/app_service/public/cpp/intent_util.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/entry_info.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_util.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "url/gurl.h" |
| |
| namespace file_manager::file_tasks { |
| |
| extensions::api::file_manager_private::TaskResult |
| ConvertLaunchResultToTaskResult(const apps::LaunchResult& result, |
| TaskType task_type) { |
| // TODO(benwells): return the correct code here, depending |
| // on how the app will be opened in multiprofile. |
| namespace fmp = extensions::api::file_manager_private; |
| switch (result.state) { |
| case apps::State::kSuccess: |
| if (task_type == TASK_TYPE_WEB_APP) { |
| return fmp::TaskResult::kOpened; |
| } else { |
| return fmp::TaskResult::kMessageSent; |
| } |
| case apps::State::kFailedDirectoryNotShared: |
| DCHECK(task_type == TASK_TYPE_PLUGIN_VM_APP); |
| return fmp::TaskResult::kFailedPluginVmDirectoryNotShared; |
| case apps::State::kFailed: |
| return fmp::TaskResult::kFailed; |
| } |
| } |
| |
| namespace { |
| |
| TaskType GetTaskType(apps::AppType app_type) { |
| switch (app_type) { |
| case apps::AppType::kArc: |
| return TASK_TYPE_ARC_APP; |
| case apps::AppType::kWeb: |
| case apps::AppType::kSystemWeb: |
| return TASK_TYPE_WEB_APP; |
| case apps::AppType::kChromeApp: |
| case apps::AppType::kExtension: |
| // Chrome apps and Extensions both get called file_handler, even though |
| // extensions really have file_browser_handler. It doesn't matter anymore |
| // because both are executed through App Service, which can tell the |
| // difference itself. |
| return TASK_TYPE_FILE_HANDLER; |
| case apps::AppType::kBruschetta: |
| return TASK_TYPE_BRUSCHETTA_APP; |
| case apps::AppType::kCrostini: |
| return TASK_TYPE_CROSTINI_APP; |
| case apps::AppType::kPluginVm: |
| return TASK_TYPE_PLUGIN_VM_APP; |
| case apps::AppType::kUnknown: |
| case apps::AppType::kRemote: |
| case apps::AppType::kBorealis: |
| return TASK_TYPE_UNKNOWN; |
| } |
| } |
| |
| const char kImportCrostiniImageHandlerId[] = |
| "chrome://file-manager/?import-crostini-image"; |
| const char kInstallLinuxPackageHandlerId[] = |
| "chrome://file-manager/?install-linux-package"; |
| |
| } // namespace |
| |
| bool FileHandlerIsEnabled(Profile* profile, |
| const std::string& app_id, |
| const std::string& file_handler_id) { |
| if (app_id != kFileManagerSwaAppId) { |
| return true; |
| } |
| // Crostini deb files and backup files can be disabled by policy. |
| if (file_handler_id == kInstallLinuxPackageHandlerId) { |
| return crostini::CrostiniFeatures::Get()->IsRootAccessAllowed(profile); |
| } |
| if (file_handler_id == kImportCrostiniImageHandlerId) { |
| return crostini::CrostiniFeatures::Get()->IsExportImportUIAllowed(profile); |
| } |
| return true; |
| } |
| |
| // Check if the file URLs can be mapped to a path inside VMs for |
| // GuestOS apps to access. |
| bool FilesCanBeSharedToVm(Profile* profile, std::vector<GURL> file_urls) { |
| storage::FileSystemContext* file_system_context = |
| util::GetFileManagerFileSystemContext(profile); |
| base::FilePath placeholder_vm_mount("/"); |
| base::FilePath not_used; |
| for (const GURL& file_url : file_urls) { |
| if (!file_manager::util::ConvertFileSystemURLToPathInsideVM( |
| profile, file_system_context->CrackURLInFirstPartyContext(file_url), |
| placeholder_vm_mount, |
| /*map_crostini_home=*/false, ¬_used)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| Profile* GetProfileWithAppService(Profile* profile) { |
| if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) { |
| return profile; |
| } else { |
| if (profile->IsOffTheRecord()) { |
| return profile->GetOriginalProfile(); |
| } else { |
| LOG(WARNING) << "Unexpected profile type"; |
| return nullptr; |
| } |
| } |
| } |
| |
| GURL GetRealOrFuseboxGURL(Profile* profile, |
| const storage::FileSystemURL& file_system_url, |
| bool use_fusebox_for_non_real_file_paths) { |
| fusebox::Server* server = fusebox::Server::GetInstance(); |
| if (!use_fusebox_for_non_real_file_paths || !server || |
| file_system_url.TypeImpliesPathIsReal()) { |
| return file_system_url.ToGURL(); |
| } |
| |
| base::FilePath path = server->InverseResolveFSURL(file_system_url); |
| if (path.empty()) { |
| return GURL(); |
| } |
| |
| GURL url; |
| if (!util::ConvertAbsoluteFilePathToFileSystemUrl( |
| profile, path, util::GetFileManagerURL(), &url)) { |
| return GURL(); |
| } |
| return url; |
| } |
| |
| // True if |app_id| and |action_id| represent a task which opens the file by |
| // getting the URL for a file rather than by opening the local contents of the |
| // file. |
| bool IsFilesAppUrlOpener(const std::string& app_id, |
| const std::string& action_id) { |
| if (app_id != kFileManagerSwaAppId) { |
| return false; |
| } |
| return action_id == ToSwaActionId(kActionIdOpenInOffice) || |
| action_id == ToSwaActionId(kActionIdWebDriveOfficeWord) || |
| action_id == ToSwaActionId(kActionIdWebDriveOfficeExcel) || |
| action_id == ToSwaActionId(kActionIdWebDriveOfficePowerPoint); |
| } |
| |
| bool IsSystemAppIdWithFileHandlers(std::string_view id) { |
| return id == ash::kMediaAppId; |
| } |
| |
| void FindAppServiceTasks(Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| const std::vector<GURL>& file_urls, |
| const std::vector<std::string>& dlp_source_urls, |
| std::vector<FullTaskDescriptor>* result_list) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_EQ(entries.size(), file_urls.size()); |
| // App Service uses the file extension in the URL for file_handlers for Web |
| // Apps. |
| #if DCHECK_IS_ON() |
| for (const GURL& url : file_urls) { |
| DCHECK(url.is_valid()); |
| } |
| #endif // DCHECK_IS_ON() |
| |
| // App Service doesn't exist in Incognito mode but we still want to find |
| // handlers to open a download from its notification from Incognito mode. Use |
| // the base profile in these cases (see crbug.com/1111695). |
| Profile* profile_with_app_service = GetProfileWithAppService(profile); |
| if (!profile_with_app_service) { |
| LOG(WARNING) << "Unexpected profile type"; |
| return; |
| } |
| |
| apps::AppServiceProxy* proxy = |
| apps::AppServiceProxyFactory::GetForProfile(profile_with_app_service); |
| |
| bool files_shareable_to_vm = |
| FilesCanBeSharedToVm(profile_with_app_service, file_urls); |
| |
| std::vector<apps::IntentFilePtr> intent_files; |
| intent_files.reserve(entries.size()); |
| for (size_t i = 0; i < entries.size(); i++) { |
| auto file = std::make_unique<apps::IntentFile>(file_urls.at(i)); |
| file->mime_type = entries[i].mime_type; |
| file->is_directory = entries[i].is_directory; |
| file->dlp_source_url = dlp_source_urls[i]; |
| intent_files.push_back(std::move(file)); |
| } |
| std::vector<apps::IntentLaunchInfo> intent_launch_info = |
| proxy->GetAppsForFiles(std::move(intent_files)); |
| |
| std::vector<apps::AppType> supported_app_types = { |
| apps::AppType::kArc, |
| apps::AppType::kWeb, |
| apps::AppType::kSystemWeb, |
| apps::AppType::kChromeApp, |
| apps::AppType::kExtension, |
| apps::AppType::kBruschetta, |
| apps::AppType::kCrostini, |
| apps::AppType::kPluginVm, |
| }; |
| for (auto& launch_entry : intent_launch_info) { |
| auto app_type = proxy->AppRegistryCache().GetAppType(launch_entry.app_id); |
| if (!base::Contains(supported_app_types, app_type)) { |
| continue; |
| } |
| |
| if (app_type == apps::AppType::kWeb || |
| app_type == apps::AppType::kSystemWeb) { |
| // Check the origin trial and feature flag for file handling in web apps. |
| // TODO(crbug.com/255838199): Remove when this feature is fully launched. |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetDeprecated(profile_with_app_service); |
| web_app::OsIntegrationManager& os_integration_manager = |
| provider->os_integration_manager(); |
| if (!os_integration_manager.IsFileHandlingAPIAvailable( |
| launch_entry.app_id)) { |
| continue; |
| } |
| } |
| |
| if (app_type == apps::AppType::kChromeApp || |
| app_type == apps::AppType::kExtension) { |
| if (profile->IsOffTheRecord() && |
| !extensions::util::IsIncognitoEnabled(launch_entry.app_id, profile)) { |
| continue; |
| } |
| } |
| |
| if ((app_type == apps::AppType::kBruschetta || |
| app_type == apps::AppType::kCrostini || |
| app_type == apps::AppType::kPluginVm) && |
| !files_shareable_to_vm) { |
| continue; |
| } |
| |
| if (!FileHandlerIsEnabled(profile_with_app_service, launch_entry.app_id, |
| launch_entry.activity_name)) { |
| continue; |
| } |
| |
| constexpr int kIconSize = 32; |
| GURL icon_url = |
| apps::AppIconSource::GetIconURL(launch_entry.app_id, kIconSize); |
| result_list->push_back(FullTaskDescriptor( |
| TaskDescriptor(launch_entry.app_id, GetTaskType(app_type), |
| launch_entry.activity_name), |
| launch_entry.activity_label, icon_url, |
| /* is_default=*/false, |
| // TODO(petermarshall): Handle the rest of the logic from FindWebTasks() |
| // e.g. prioritise non-generic handlers. |
| /* is_generic_file_handler=*/launch_entry.is_generic_file_handler, |
| /* is_file_extension_match=*/launch_entry.is_file_extension_match, |
| /* is_dlp_blocked=*/launch_entry.is_dlp_blocked)); |
| } |
| } |
| |
| void ExecuteAppServiceTask( |
| Profile* profile, |
| const TaskDescriptor& task, |
| const std::vector<storage::FileSystemURL>& file_system_urls, |
| const std::vector<std::string>& mime_types, |
| FileTaskFinishedCallback done) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_EQ(file_system_urls.size(), mime_types.size()); |
| |
| // App Service doesn't exist in Incognito mode but apps can be |
| // launched (ie. default handler to open a download from its |
| // notification) from Incognito mode. Use the base profile in these |
| // cases (see crbug.com/1111695). |
| Profile* profile_with_app_service = GetProfileWithAppService(profile); |
| if (!profile_with_app_service) { |
| std::move(done).Run( |
| extensions::api::file_manager_private::TaskResult::kFailed, |
| "Unexpected profile type"); |
| return; |
| } |
| |
| apps::AppServiceProxy* app_service_proxy = |
| apps::AppServiceProxyFactory::GetForProfile(profile_with_app_service); |
| auto app_type = app_service_proxy->AppRegistryCache().GetAppType(task.app_id); |
| |
| // In general, WebApps only have full support for files backed by inodes, so |
| // substitute Fusebox files for any "non-real" file paths. However, the Media |
| // app and other SWAs can handle "non-real" files, as can special tasks which |
| // only access the file via URL. See https://crbug.com/1079065 |
| bool use_fusebox_for_non_real_file_paths = false; |
| if (app_type == apps::AppType::kWeb || |
| app_type == apps::AppType::kSystemWeb) { |
| use_fusebox_for_non_real_file_paths = |
| !(IsSystemAppIdWithFileHandlers(task.app_id) || |
| IsFilesAppUrlOpener(task.app_id, task.action_id)); |
| } |
| |
| std::vector<apps::IntentFilePtr> intent_files; |
| intent_files.reserve(file_system_urls.size()); |
| for (size_t i = 0; i < file_system_urls.size(); i++) { |
| GURL file_url = |
| GetRealOrFuseboxGURL(profile_with_app_service, file_system_urls[i], |
| use_fusebox_for_non_real_file_paths); |
| if (!file_url.is_valid()) { |
| continue; |
| } |
| auto file = std::make_unique<apps::IntentFile>(file_url); |
| file->mime_type = mime_types.at(i); |
| intent_files.push_back(std::move(file)); |
| } |
| |
| DCHECK(task.task_type == TASK_TYPE_WEB_APP || |
| task.task_type == TASK_TYPE_FILE_HANDLER || |
| task.task_type == TASK_TYPE_BRUSCHETTA_APP || |
| task.task_type == TASK_TYPE_CROSTINI_APP || |
| task.task_type == TASK_TYPE_PLUGIN_VM_APP || |
| task.task_type == TASK_TYPE_ARC_APP); |
| |
| apps::IntentPtr intent = std::make_unique<apps::Intent>( |
| apps_util::kIntentActionView, std::move(intent_files)); |
| intent->activity_name = task.action_id; |
| |
| if (base::FeatureList::IsEnabled(::features::kHappinessTrackingOffice) && |
| task.app_id == extension_misc::kQuickOfficeComponentExtensionId && |
| task.action_id == kActionIdQuickOffice) { |
| auto survey_launching_app = |
| chromeos::IsEligibleAndEnabledUploadOfficeToCloud(profile) |
| ? ash::cloud_upload::HatsOfficeLaunchingApp::kQuickOffice |
| : ash::cloud_upload::HatsOfficeLaunchingApp::kQuickOfficeClippyOff; |
| ash::cloud_upload::HatsOfficeTrigger::Get().ShowSurveyAfterDelay( |
| survey_launching_app); |
| } |
| // `window_info` as nullptr sets `display_id` to `display::kInvalidDisplayId` |
| // later, which is the default value. `display::kDefaultDisplayId` is not. The |
| // default value allows a window on any display to be reused, i.e. a wildcard. |
| app_service_proxy->LaunchAppWithIntent( |
| task.app_id, ui::EF_NONE, std::move(intent), |
| apps::LaunchSource::kFromFileManager, |
| /*window_info=*/nullptr, |
| base::BindOnce( |
| [](FileTaskFinishedCallback done, TaskType task_type, |
| apps::LaunchResult&& result) { |
| std::move(done).Run( |
| ConvertLaunchResultToTaskResult(result, task_type), ""); |
| }, |
| std::move(done), task.task_type)); |
| } |
| |
| bool ChooseAndSetDefaultTaskFromPolicyPrefs( |
| Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| ResultingTasks* resulting_tasks) { |
| const auto& policy_default_handlers = |
| profile->GetPrefs()->GetDict(prefs::kDefaultHandlersForFileExtensions); |
| |
| // Check that there are no conflicting assignments for the given set of |
| // entries. |
| base::flat_set<std::string> default_handlers_for_entries; |
| for (const auto& entry : entries) { |
| if (auto* policy_default_handler = |
| policy_default_handlers.FindString(entry.path.Extension())) { |
| default_handlers_for_entries.insert(*policy_default_handler); |
| } |
| } |
| |
| // If there are no policy-set handlers, we fallback to the regular flow. |
| if (default_handlers_for_entries.empty()) { |
| return false; |
| } |
| |
| // Conflicting assignment! No default should be set. |
| if (default_handlers_for_entries.size() > 1) { |
| resulting_tasks->policy_default_handler_status = |
| PolicyDefaultHandlerStatus::kIncorrectAssignment; |
| return true; |
| } |
| |
| DCHECK_EQ(default_handlers_for_entries.size(), 1U); |
| const auto& policy_id = *default_handlers_for_entries.begin(); |
| |
| std::vector<FullTaskDescriptor*> filtered_tasks; |
| // `app_id` matching is not necessary if the policy points to a virtual task. |
| if (std::optional<std::string_view> virtual_task_id = |
| apps_util::GetVirtualTaskIdFromPolicyId(policy_id)) { |
| std::string full_virtual_task_id = ToSwaActionId(*virtual_task_id); |
| for (auto& task : resulting_tasks->tasks) { |
| if (IsVirtualTask(task.task_descriptor) && |
| task.task_descriptor.action_id == full_virtual_task_id) { |
| filtered_tasks.push_back(&task); |
| } |
| } |
| } else { |
| std::vector<std::string> app_ids = |
| apps_util::GetAppIdsFromPolicyId(profile, policy_id); |
| for (auto& task : resulting_tasks->tasks) { |
| const auto& td = task.task_descriptor; |
| if (base::Contains(app_ids, td.app_id)) { |
| filtered_tasks.push_back(&task); |
| } |
| } |
| } |
| |
| // If there are no tasks found, we resort to the standard flow to not ruin the |
| // user journey. |
| if (filtered_tasks.size() == 1) { |
| filtered_tasks.front()->is_default = true; |
| resulting_tasks->policy_default_handler_status = |
| PolicyDefaultHandlerStatus::kDefaultHandlerAssignedByPolicy; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace file_manager::file_tasks |