| // Copyright (c) 2012 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 "chrome/browser/ash/file_manager/file_tasks.h" |
| |
| #include <stddef.h> |
| |
| #include <map> |
| #include <string> |
| #include <utility> |
| |
| #include "apps/launcher.h" |
| #include "ash/constants/ash_features.h" |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/apps/app_service/app_icon_source.h" |
| #include "chrome/browser/apps/app_service/app_launch_params.h" |
| #include "chrome/browser/apps/app_service/app_platform_metrics.h" |
| #include "chrome/browser/apps/app_service/app_service_metrics.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/ash/crostini/crostini_features.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/file_manager/app_id.h" |
| #include "chrome/browser/ash/file_manager/app_service_file_tasks.h" |
| #include "chrome/browser/ash/file_manager/arc_file_tasks.h" |
| #include "chrome/browser/ash/file_manager/file_browser_handlers.h" |
| #include "chrome/browser/ash/file_manager/file_tasks_notifier.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/guest_os_file_tasks.h" |
| #include "chrome/browser/ash/file_manager/open_util.h" |
| #include "chrome/browser/ash/file_manager/open_with_browser.h" |
| #include "chrome/browser/ash/file_manager/web_file_tasks.h" |
| #include "chrome/browser/chromeos/fileapi/file_system_backend.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/launch_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/extensions/application_launch.h" |
| #include "chrome/browser/ui/webui/extensions/extension_icon_source.h" |
| #include "chrome/browser/web_applications/components/web_app_id_constants.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/extensions/api/file_browser_handlers/file_browser_handler.h" |
| #include "chrome/common/extensions/api/file_manager_private.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/drive/drive_api_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/services/app_service/public/cpp/file_handler.h" |
| #include "components/services/app_service/public/cpp/file_handler_info.h" |
| #include "components/services/app_service/public/mojom/types.mojom.h" |
| #include "extensions/browser/api/file_handlers/mime_util.h" |
| #include "extensions/browser/entry_info.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/common/constants.h" |
| #include "extensions/common/extension_set.h" |
| #include "net/base/mime_util.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| using extensions::Extension; |
| using extensions::api::file_manager_private::Verb; |
| using extensions::app_file_handler_util::FindFileHandlerMatchesForEntries; |
| using storage::FileSystemURL; |
| |
| namespace file_manager { |
| namespace file_tasks { |
| |
| const char kActionIdView[] = "view"; |
| const char kActionIdSend[] = "send"; |
| const char kActionIdSendMultiple[] = "send_multiple"; |
| |
| namespace { |
| |
| // The values "file" and "app" are confusing, but cannot be changed easily as |
| // these are used in default task IDs stored in preferences. |
| const char kFileBrowserHandlerTaskType[] = "file"; |
| const char kFileHandlerTaskType[] = "app"; |
| const char kArcAppTaskType[] = "arc"; |
| const char kCrostiniAppTaskType[] = "crostini"; |
| const char kPluginVmAppTaskType[] = "pluginvm"; |
| const char kWebAppTaskType[] = "web"; |
| const char kImportCrostiniImageHandlerId[] = "import-crostini-image"; |
| const char kInstallLinuxPackageHandlerId[] = "install-linux-package"; |
| |
| // Returns true if path_mime_set contains a Google document. |
| bool ContainsGoogleDocument(const std::vector<extensions::EntryInfo>& entries) { |
| for (const auto& it : entries) { |
| if (drive::util::HasHostedDocumentExtension(it.path)) |
| return true; |
| } |
| return false; |
| } |
| |
| // Removes all tasks except tasks handled by file manager. |
| void KeepOnlyFileManagerInternalTasks(std::vector<FullTaskDescriptor>* tasks) { |
| std::vector<FullTaskDescriptor> filtered; |
| for (FullTaskDescriptor& task : *tasks) { |
| if (task.task_descriptor.app_id == kFileManagerAppId) |
| filtered.push_back(task); |
| } |
| tasks->swap(filtered); |
| } |
| |
| // Removes task |actions| handled by file manager. |
| void RemoveFileManagerInternalActions(const std::set<std::string>& actions, |
| std::vector<FullTaskDescriptor>* tasks) { |
| std::vector<FullTaskDescriptor> filtered; |
| for (FullTaskDescriptor& task : *tasks) { |
| const auto& action = task.task_descriptor.action_id; |
| if (task.task_descriptor.app_id != kFileManagerAppId) { |
| filtered.push_back(task); |
| } else if (actions.find(action) == actions.end()) { |
| filtered.push_back(task); |
| } |
| } |
| |
| tasks->swap(filtered); |
| } |
| |
| // Adjusts |tasks| to reflect the product decision that chrome://media-app |
| // should behave more like a user-installed app than a fallback handler. |
| // Specifically, only apps set as the default in user prefs should be preferred |
| // over chrome://media-app. |
| void AdjustTasksForMediaApp(const std::vector<extensions::EntryInfo>& entries, |
| std::vector<FullTaskDescriptor>* tasks) { |
| const auto task_for_app = [&](const std::string& app_id) { |
| return std::find_if(tasks->begin(), tasks->end(), [&](const auto& task) { |
| return task.task_descriptor.app_id == app_id; |
| }); |
| }; |
| |
| const auto media_app_task = task_for_app(web_app::kMediaAppId); |
| if (media_app_task == tasks->end()) |
| return; |
| |
| // Video Player app was replaced by media app in m91, deprecated in m93 and |
| // will be deleted m94. |
| DCHECK(task_for_app(kVideoPlayerAppId) == tasks->end()); |
| |
| // TOOD(crbug/1071289): For a while is_file_extension_match would always be |
| // false for System Web App manifests, even when specifying extension matches. |
| // So this line can be removed once the media app manifest is updated with a |
| // full complement of image file extensions. |
| media_app_task->is_file_extension_match = true; |
| |
| // The logic in ChooseAndSetDefaultTask() also requires the following to hold. |
| // This should only fail if the media app is configured for "*" (e.g. like |
| // Zip Archiver). "image/*" does not count as "generic". |
| DCHECK(!media_app_task->is_generic_file_handler); |
| |
| // Otherwise, build a new list with Media App at the front. |
| if (media_app_task == tasks->begin()) |
| return; |
| |
| std::vector<FullTaskDescriptor> new_tasks; |
| new_tasks.push_back(*media_app_task); |
| for (auto it = tasks->begin(); it != tasks->end(); ++it) { |
| if (it != media_app_task) |
| new_tasks.push_back(std::move(*it)); |
| } |
| std::swap(*tasks, new_tasks); |
| } |
| |
| // Returns true if the given task is a handler by built-in apps like the Files |
| // app itself or QuickOffice etc. They are used as the initial default app. |
| bool IsFallbackFileHandler(const FullTaskDescriptor& task) { |
| if ((task.task_descriptor.task_type != |
| file_tasks::TASK_TYPE_FILE_BROWSER_HANDLER && |
| task.task_descriptor.task_type != file_tasks::TASK_TYPE_FILE_HANDLER) || |
| task.is_generic_file_handler) { |
| return false; |
| } |
| |
| // Note that web_app::kMediaAppId does not appear in the |
| // list of built-in apps below. Doing so would mean the presence of any other |
| // handler of image files (e.g. Keep, Photos) would take precedence. But we |
| // want that only to occur if the user has explicitly set the preference for |
| // an app other than kMediaAppId to be the default (b/153387960). |
| constexpr const char* kBuiltInApps[] = { |
| kFileManagerAppId, |
| kTextEditorAppId, |
| kAudioPlayerAppId, |
| extension_misc::kQuickOfficeComponentExtensionId, |
| extension_misc::kQuickOfficeInternalExtensionId, |
| extension_misc::kQuickOfficeExtensionId}; |
| |
| return base::Contains(kBuiltInApps, task.task_descriptor.app_id); |
| } |
| |
| // Gets the profile in which a file task owned by |extension| should be |
| // launched - for example, it makes sure that a file task is not handled in OTR |
| // profile for platform apps (outside a guest session). |
| Profile* GetProfileForExtensionTask(Profile* profile, |
| const extensions::Extension& extension) { |
| // In guest profile, all available task handlers are in OTR profile. |
| if (profile->IsGuestSession()) { |
| DCHECK(profile->IsOffTheRecord()); |
| return profile; |
| } |
| |
| // Outside guest sessions, if the task is handled by a platform app, launch |
| // the handler in the original profile. |
| if (extension.is_platform_app()) |
| return profile->GetOriginalProfile(); |
| return profile; |
| } |
| |
| GURL GetIconURL(Profile* profile, const Extension& extension) { |
| if (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon) && |
| apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile) && |
| apps::AppServiceProxyFactory::GetForProfile(profile) |
| ->AppRegistryCache() |
| .GetAppType(extension.id()) != apps::mojom::AppType::kUnknown) { |
| return apps::AppIconSource::GetIconURL( |
| extension.id(), extension_misc::EXTENSION_ICON_SMALL); |
| } |
| return extensions::ExtensionIconSource::GetIconURL( |
| &extension, extension_misc::EXTENSION_ICON_SMALL, |
| ExtensionIconSet::MATCH_BIGGER, |
| false); // grayscale |
| } |
| |
| void ExecuteByArcAfterMimeTypesCollected( |
| Profile* profile, |
| const TaskDescriptor& task, |
| const std::vector<FileSystemURL>& file_urls, |
| FileTaskFinishedCallback done, |
| extensions::app_file_handler_util::MimeTypeCollector* mime_collector, |
| std::unique_ptr<std::vector<std::string>> mime_types) { |
| if (base::FeatureList::IsEnabled(features::kIntentHandlingSharing) && |
| (task.action_id == kActionIdSend || |
| task.action_id == kActionIdSendMultiple)) { |
| ExecuteAppServiceTask(profile, task, file_urls, *mime_types, |
| std::move(done)); |
| return; |
| } |
| |
| apps::RecordAppLaunchMetrics( |
| profile, apps::mojom::AppType::kArc, task.app_id, |
| apps::mojom::LaunchSource::kFromFileManager, |
| apps::mojom::LaunchContainer::kLaunchContainerWindow); |
| ExecuteArcTask(profile, task, file_urls, *mime_types, std::move(done)); |
| } |
| |
| void PostProcessFoundTasks( |
| Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| FindTasksCallback callback, |
| std::unique_ptr<std::vector<FullTaskDescriptor>> result_list) { |
| // Google documents can only be handled by internal handlers. |
| if (ContainsGoogleDocument(entries)) |
| KeepOnlyFileManagerInternalTasks(result_list.get()); |
| |
| std::set<std::string> disabled_actions; |
| |
| // kFilesArchivemount is whether we allow "mount-archive" for every filename |
| // extension listed in ui/file_manager/file_manager/manifest.json (when the |
| // feature flag is true) or only for ".rar" (when the feature flag is false). |
| // False corresponds to the status quo as of milestone M92. This feature flag |
| // will be introduced in M93 (https://crrev.com/c/3017636), false by default. |
| // "True by default" is scheduled for M94. |
| // |
| // TODO(nigeltao): some time after M94, remove the kFilesArchivemount feature |
| // flag (scheduled to expire in M100) by hard-coding it to true, so that this |
| // if-block is never taken and can be deleted. |
| if (!base::FeatureList::IsEnabled(ash::features::kFilesArchivemount)) { |
| for (const auto& entry : entries) { |
| if (!entry.path.MatchesExtension(".rar")) { |
| disabled_actions.emplace("mount-archive"); |
| break; |
| } |
| } |
| } |
| |
| // Remove file manager internal view-pdf and view-swf actions if needed. |
| if (!util::ShouldBeOpenedWithPlugin(profile, FILE_PATH_LITERAL(".pdf"), "")) |
| disabled_actions.emplace("view-pdf"); |
| if (!util::ShouldBeOpenedWithPlugin(profile, FILE_PATH_LITERAL(".swf"), "")) |
| disabled_actions.emplace("view-swf"); |
| if (!disabled_actions.empty()) |
| RemoveFileManagerInternalActions(disabled_actions, result_list.get()); |
| |
| AdjustTasksForMediaApp(entries, result_list.get()); |
| |
| ChooseAndSetDefaultTask(*profile->GetPrefs(), entries, result_list.get()); |
| std::move(callback).Run(std::move(result_list)); |
| } |
| |
| // Returns true if |extension_id| and |action_id| indicate that the file |
| // currently being handled should be opened with the browser. This function |
| // is used to handle certain action IDs of the file manager. |
| bool ShouldBeOpenedWithBrowser(const std::string& extension_id, |
| const std::string& action_id) { |
| return extension_id == kFileManagerAppId && |
| (action_id == "view-pdf" || action_id == "view-swf" || |
| action_id == "view-in-browser" || |
| action_id == "open-hosted-generic" || |
| action_id == "open-hosted-gdoc" || |
| action_id == "open-hosted-gsheet" || |
| action_id == "open-hosted-gslides"); |
| } |
| |
| // Opens the files specified by |file_urls| with the browser for |profile|. |
| // Returns true on success. It's a failure if no files are opened. |
| bool OpenFilesWithBrowser(Profile* profile, |
| const std::vector<FileSystemURL>& file_urls, |
| const std::string& action_id) { |
| int num_opened = 0; |
| for (const FileSystemURL& file_url : file_urls) { |
| if (chromeos::FileSystemBackend::CanHandleURL(file_url)) { |
| num_opened += |
| util::OpenFileWithBrowser(profile, file_url, action_id) ? 1 : 0; |
| } |
| } |
| return num_opened > 0; |
| } |
| |
| } // namespace |
| |
| // Converts a string to a TaskType. Returns TASK_TYPE_UNKNOWN on error. |
| TaskType StringToTaskType(const std::string& str) { |
| if (str == kFileBrowserHandlerTaskType) |
| return TASK_TYPE_FILE_BROWSER_HANDLER; |
| if (str == kFileHandlerTaskType) |
| return TASK_TYPE_FILE_HANDLER; |
| if (str == kArcAppTaskType) |
| return TASK_TYPE_ARC_APP; |
| if (str == kCrostiniAppTaskType) |
| return TASK_TYPE_CROSTINI_APP; |
| if (str == kWebAppTaskType) |
| return TASK_TYPE_WEB_APP; |
| if (str == kPluginVmAppTaskType) |
| return TASK_TYPE_PLUGIN_VM_APP; |
| return TASK_TYPE_UNKNOWN; |
| } |
| |
| // Converts a TaskType to a string. |
| std::string TaskTypeToString(TaskType task_type) { |
| switch (task_type) { |
| case TASK_TYPE_FILE_BROWSER_HANDLER: |
| return kFileBrowserHandlerTaskType; |
| case TASK_TYPE_FILE_HANDLER: |
| return kFileHandlerTaskType; |
| case TASK_TYPE_ARC_APP: |
| return kArcAppTaskType; |
| case TASK_TYPE_CROSTINI_APP: |
| return kCrostiniAppTaskType; |
| case TASK_TYPE_WEB_APP: |
| return kWebAppTaskType; |
| case TASK_TYPE_PLUGIN_VM_APP: |
| return kPluginVmAppTaskType; |
| case TASK_TYPE_UNKNOWN: |
| case DEPRECATED_TASK_TYPE_DRIVE_APP: |
| case NUM_TASK_TYPE: |
| break; |
| } |
| NOTREACHED(); |
| return ""; |
| } |
| |
| FullTaskDescriptor::FullTaskDescriptor(const TaskDescriptor& in_task_descriptor, |
| const std::string& in_task_title, |
| const Verb in_task_verb, |
| const GURL& in_icon_url, |
| bool in_is_default, |
| bool in_is_generic_file_handler, |
| bool in_is_file_extension_match) |
| : task_descriptor(in_task_descriptor), |
| task_title(in_task_title), |
| task_verb(in_task_verb), |
| icon_url(in_icon_url), |
| is_default(in_is_default), |
| is_generic_file_handler(in_is_generic_file_handler), |
| is_file_extension_match(in_is_file_extension_match) {} |
| |
| FullTaskDescriptor::FullTaskDescriptor(const FullTaskDescriptor& other) = |
| default; |
| |
| FullTaskDescriptor& FullTaskDescriptor::operator=( |
| const FullTaskDescriptor& other) = default; |
| |
| void UpdateDefaultTask(PrefService* pref_service, |
| const TaskDescriptor& task_descriptor, |
| const std::set<std::string>& suffixes, |
| const std::set<std::string>& mime_types) { |
| if (!pref_service) |
| return; |
| |
| std::string task_id = TaskDescriptorToId(task_descriptor); |
| if (!mime_types.empty()) { |
| DictionaryPrefUpdate mime_type_pref(pref_service, |
| prefs::kDefaultTasksByMimeType); |
| for (const std::string& mime_type : mime_types) { |
| mime_type_pref->SetKey(mime_type, base::Value(task_id)); |
| } |
| } |
| |
| if (!suffixes.empty()) { |
| DictionaryPrefUpdate mime_type_pref(pref_service, |
| prefs::kDefaultTasksBySuffix); |
| for (const std::string& suffix : suffixes) { |
| // Suffixes are case insensitive. |
| std::string lower_suffix = base::ToLowerASCII(suffix); |
| mime_type_pref->SetKey(lower_suffix, base::Value(task_id)); |
| } |
| } |
| } |
| |
| bool GetDefaultTaskFromPrefs(const PrefService& pref_service, |
| const std::string& mime_type, |
| const std::string& suffix, |
| TaskDescriptor* task_out) { |
| VLOG(1) << "Looking for default for MIME type: " << mime_type |
| << " and suffix: " << suffix; |
| if (!mime_type.empty()) { |
| const base::DictionaryValue* mime_task_prefs = |
| pref_service.GetDictionary(prefs::kDefaultTasksByMimeType); |
| DCHECK(mime_task_prefs); |
| LOG_IF(ERROR, !mime_task_prefs) << "Unable to open MIME type prefs"; |
| if (mime_task_prefs) { |
| const std::string* task_id = mime_task_prefs->FindStringKey(mime_type); |
| if (task_id) { |
| VLOG(1) << "Found MIME default handler: " << *task_id; |
| return ParseTaskID(*task_id, task_out); |
| } |
| } |
| } |
| |
| const base::DictionaryValue* suffix_task_prefs = |
| pref_service.GetDictionary(prefs::kDefaultTasksBySuffix); |
| DCHECK(suffix_task_prefs); |
| LOG_IF(ERROR, !suffix_task_prefs) << "Unable to open suffix prefs"; |
| std::string lower_suffix = base::ToLowerASCII(suffix); |
| if (!suffix_task_prefs) |
| return false; |
| |
| const std::string* task_id = suffix_task_prefs->FindStringKey(lower_suffix); |
| |
| if (!task_id || task_id->empty()) |
| return false; |
| |
| VLOG(1) << "Found suffix default handler: " << *task_id; |
| return ParseTaskID(*task_id, task_out); |
| } |
| |
| std::string MakeTaskID(const std::string& app_id, |
| TaskType task_type, |
| const std::string& action_id) { |
| return base::StringPrintf("%s|%s|%s", |
| app_id.c_str(), |
| TaskTypeToString(task_type).c_str(), |
| action_id.c_str()); |
| } |
| |
| std::string TaskDescriptorToId(const TaskDescriptor& task_descriptor) { |
| return MakeTaskID(task_descriptor.app_id, |
| task_descriptor.task_type, |
| task_descriptor.action_id); |
| } |
| |
| bool ParseTaskID(const std::string& task_id, TaskDescriptor* task) { |
| DCHECK(task); |
| |
| std::vector<std::string> result = base::SplitString( |
| task_id, "|", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // Parse a legacy task ID that only contain two parts. The legacy task IDs |
| // can be stored in preferences. |
| if (result.size() == 2) { |
| task->task_type = TASK_TYPE_FILE_BROWSER_HANDLER; |
| task->app_id = result[0]; |
| task->action_id = result[1]; |
| |
| return true; |
| } |
| |
| if (result.size() != 3) |
| return false; |
| |
| TaskType task_type = StringToTaskType(result[1]); |
| if (task_type == TASK_TYPE_UNKNOWN) |
| return false; |
| |
| task->app_id = result[0]; |
| task->task_type = task_type; |
| task->action_id = result[2]; |
| |
| return true; |
| } |
| |
| bool ExecuteFileTask(Profile* profile, |
| const GURL& source_url, |
| const TaskDescriptor& task, |
| const std::vector<FileSystemURL>& file_urls, |
| FileTaskFinishedCallback done) { |
| UMA_HISTOGRAM_ENUMERATION("FileBrowser.ViewingTaskType", task.task_type, |
| NUM_TASK_TYPE); |
| if (drive::util::GetDriveConnectionStatus(profile) == |
| drive::util::DRIVE_DISCONNECTED_NONETWORK) { |
| UMA_HISTOGRAM_ENUMERATION("FileBrowser.ViewingTaskType.Offline", |
| task.task_type, NUM_TASK_TYPE); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION("FileBrowser.ViewingTaskType.Online", |
| task.task_type, NUM_TASK_TYPE); |
| } |
| |
| // TODO(crbug.com/1005640): Move recording this metric to the App Service when |
| // file handling is supported there. |
| apps::RecordAppLaunch(task.app_id, |
| apps::mojom::LaunchSource::kFromFileManager); |
| |
| if (auto* notifier = FileTasksNotifier::GetForProfile(profile)) { |
| notifier->NotifyFileTasks(file_urls); |
| } |
| |
| // ARC apps needs mime types for launching. Retrieve them first. |
| if (task.task_type == TASK_TYPE_ARC_APP) { |
| extensions::app_file_handler_util::MimeTypeCollector* mime_collector = |
| new extensions::app_file_handler_util::MimeTypeCollector(profile); |
| mime_collector->CollectForURLs( |
| file_urls, base::BindOnce(&ExecuteByArcAfterMimeTypesCollected, profile, |
| task, file_urls, std::move(done), |
| base::Owned(mime_collector))); |
| return true; |
| } |
| |
| if (task.task_type == TASK_TYPE_CROSTINI_APP || |
| task.task_type == TASK_TYPE_PLUGIN_VM_APP) { |
| DCHECK_EQ(kGuestOsAppActionID, task.action_id); |
| ExecuteGuestOsTask(profile, task, file_urls, std::move(done)); |
| return true; |
| } |
| |
| if (task.task_type == TASK_TYPE_WEB_APP) { |
| ExecuteWebTask(profile, task, file_urls, std::move(done)); |
| return true; |
| } |
| |
| // Some action IDs of the file manager's file browser handlers require the |
| // files to be directly opened with the browser. In a multiprofile session |
| // this will always open on the current desktop, regardless of which profile |
| // owns the files, so return TASK_RESULT_OPENED. |
| if (ShouldBeOpenedWithBrowser(task.app_id, task.action_id)) { |
| const bool result = |
| OpenFilesWithBrowser(profile, file_urls, task.action_id); |
| if (result && done) { |
| std::move(done).Run( |
| extensions::api::file_manager_private::TASK_RESULT_OPENED, ""); |
| } |
| return result; |
| } |
| |
| // Get the extension. |
| const Extension* extension = extensions::ExtensionRegistry::Get( |
| profile)->enabled_extensions().GetByID(task.app_id); |
| if (!extension) |
| return false; |
| |
| Profile* extension_task_profile = |
| GetProfileForExtensionTask(profile, *extension); |
| |
| // Execute the task. |
| if (task.task_type == TASK_TYPE_FILE_BROWSER_HANDLER) { |
| return file_browser_handlers::ExecuteFileBrowserHandler( |
| extension_task_profile, extension, task.action_id, file_urls, |
| std::move(done)); |
| } else if (task.task_type == TASK_TYPE_FILE_HANDLER) { |
| std::vector<base::FilePath> paths; |
| for (const FileSystemURL& file_url : file_urls) |
| paths.push_back(file_url.path()); |
| |
| DCHECK(!extension->from_bookmark()); |
| apps::LaunchPlatformAppWithFileHandler(extension_task_profile, extension, |
| task.action_id, paths); |
| // In a multiprofile session, platform apps will open on the desktop |
| // corresponding to the profile that owns the files, so return |
| // TASK_RESULT_MESSAGE_SENT. |
| if (!done.is_null()) |
| std::move(done).Run( |
| extensions::api::file_manager_private::TASK_RESULT_MESSAGE_SENT, ""); |
| return true; |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| bool IsFileHandlerEnabled(Profile* profile, |
| const apps::FileHandlerInfo& file_handler_info) { |
| // Crostini deb files and backup files can be disabled by policy. |
| if (file_handler_info.id == kInstallLinuxPackageHandlerId) { |
| return crostini::CrostiniFeatures::Get()->IsRootAccessAllowed(profile); |
| } |
| if (file_handler_info.id == kImportCrostiniImageHandlerId) { |
| return crostini::CrostiniFeatures::Get()->IsExportImportUIAllowed(profile); |
| } |
| return true; |
| } |
| |
| bool IsGoodMatchFileHandler(const apps::FileHandlerInfo& file_handler_info, |
| const std::vector<extensions::EntryInfo>& entries) { |
| if (file_handler_info.extensions.count("*") > 0 || |
| file_handler_info.types.count("*") > 0 || |
| file_handler_info.types.count("*/*") > 0) |
| return false; |
| |
| // If text/* file handler matches with unsupported text mime type, we don't |
| // regard it as good match. |
| if (file_handler_info.types.count("text/*")) { |
| for (const auto& entry : entries) { |
| if (blink::IsUnsupportedTextMimeType(entry.mime_type)) |
| return false; |
| } |
| } |
| |
| // We consider it a good match if no directories are selected. |
| for (const auto& entry : entries) { |
| if (entry.is_directory) |
| return false; |
| } |
| return true; |
| } |
| |
| bool IsGoodMatchAppsFileHandler( |
| const apps::FileHandler& file_handler, |
| const std::vector<extensions::EntryInfo>& entries) { |
| // TODO(crbug.com/938103): Duplicates functionality from |
| // FileHandlerManager::GetMimeTypesFromFileHandlers and |
| // ::GetFileExtensionsFromFileHandlers. |
| std::set<std::string> mime_types; |
| std::set<std::string> file_extensions; |
| for (const auto& accept_entry : file_handler.accept) { |
| mime_types.insert(accept_entry.mime_type); |
| file_extensions.insert(accept_entry.file_extensions.begin(), |
| accept_entry.file_extensions.end()); |
| } |
| |
| if (mime_types.count("*") || mime_types.count("*/*") || |
| file_extensions.count("*")) |
| return false; |
| |
| // If a "text/*" file handler matches with an unsupported text MIME type, we |
| // don't regard it as a good match. |
| if (mime_types.count("text/*")) { |
| for (const auto& entry : entries) { |
| if (blink::IsUnsupportedTextMimeType(entry.mime_type)) |
| return false; |
| } |
| } |
| |
| // We consider it a good match if no directories are selected. |
| for (const auto& entry : entries) { |
| if (entry.is_directory) |
| return false; |
| } |
| return true; |
| } |
| |
| void FindFileHandlerTasks(Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| std::vector<FullTaskDescriptor>* result_list) { |
| DCHECK(!entries.empty()); |
| DCHECK(result_list); |
| |
| const extensions::ExtensionSet& enabled_extensions = |
| extensions::ExtensionRegistry::Get(profile)->enabled_extensions(); |
| |
| for (const scoped_refptr<const extensions::Extension> extension : |
| enabled_extensions) { |
| // Check that the extension can be launched with files. This includes all |
| // platform apps and allowlisted extensions. |
| if (!CanLaunchViaEvent(extension.get())) { |
| continue; |
| } |
| |
| if (profile->IsOffTheRecord() && |
| !extensions::util::IsIncognitoEnabled(extension->id(), profile)) |
| continue; |
| |
| // Video player should no longer install itself or its file handlers |
| // starting in m93. |
| DCHECK_NE(kVideoPlayerAppId, extension->id()); |
| |
| typedef std::vector<extensions::FileHandlerMatch> FileHandlerMatchList; |
| FileHandlerMatchList file_handlers = |
| FindFileHandlerMatchesForEntries(*extension, entries); |
| if (file_handlers.empty()) |
| continue; |
| |
| // A map which has as key a handler verb, and as value a pair of the |
| // handler with which to open the given entries and a boolean marking |
| // if the handler is a good match. |
| std::map<std::string, std::pair<const extensions::FileHandlerMatch*, bool>> |
| handlers_for_entries; |
| // Show the first good matching handler of each verb supporting the given |
| // entries that corresponds to the app. If there doesn't exist such handler, |
| // show the first matching handler of the verb. |
| for (const auto& handler_match : file_handlers) { |
| const apps::FileHandlerInfo* handler = handler_match.handler; |
| if (!IsFileHandlerEnabled(profile, *handler)) { |
| continue; |
| } |
| bool good_match = IsGoodMatchFileHandler(*handler, entries); |
| auto it = handlers_for_entries.find(handler->verb); |
| if (it == handlers_for_entries.end() || |
| (!it->second.second /* existing handler not a good match */ && |
| good_match)) { |
| handlers_for_entries[handler->verb] = |
| std::make_pair(&handler_match, good_match); |
| } |
| } |
| |
| for (const auto& entry : handlers_for_entries) { |
| const extensions::FileHandlerMatch* match = entry.second.first; |
| const apps::FileHandlerInfo* handler = match->handler; |
| std::string task_id = file_tasks::MakeTaskID( |
| extension->id(), file_tasks::TASK_TYPE_FILE_HANDLER, handler->id); |
| |
| const GURL best_icon = GetIconURL(profile, *extension); |
| |
| // If file handler doesn't match as good match, regards it as generic file |
| // handler. |
| const bool is_generic_file_handler = |
| !IsGoodMatchFileHandler(*handler, entries); |
| Verb verb; |
| if (handler->verb == apps::file_handler_verbs::kAddTo) { |
| verb = Verb::VERB_ADD_TO; |
| } else if (handler->verb == apps::file_handler_verbs::kPackWith) { |
| verb = Verb::VERB_PACK_WITH; |
| } else if (handler->verb == apps::file_handler_verbs::kShareWith) { |
| verb = Verb::VERB_SHARE_WITH; |
| } else { |
| // Only kOpenWith is a valid remaining verb. Invalid verbs should fall |
| // back to it. |
| DCHECK(handler->verb == apps::file_handler_verbs::kOpenWith); |
| verb = Verb::VERB_OPEN_WITH; |
| } |
| // If the handler was matched purely on the file name extension then |
| // the manifest declared its 'file_handler' to match. Used for fallback |
| // selection of the handler when we don't have a default handler set |
| const bool is_file_extension_match = match->matched_file_extension; |
| |
| result_list->push_back(FullTaskDescriptor( |
| TaskDescriptor(extension->id(), file_tasks::TASK_TYPE_FILE_HANDLER, |
| handler->id), |
| extension->name(), verb, best_icon, false /* is_default */, |
| is_generic_file_handler, is_file_extension_match)); |
| } |
| } |
| } |
| |
| void FindFileBrowserHandlerTasks( |
| Profile* profile, |
| const std::vector<GURL>& file_urls, |
| std::vector<FullTaskDescriptor>* result_list) { |
| DCHECK(!file_urls.empty()); |
| DCHECK(result_list); |
| |
| file_browser_handlers::FileBrowserHandlerList common_tasks = |
| file_browser_handlers::FindFileBrowserHandlers(profile, file_urls); |
| if (common_tasks.empty()) |
| return; |
| |
| const extensions::ExtensionSet& enabled_extensions = |
| extensions::ExtensionRegistry::Get(profile)->enabled_extensions(); |
| for (const FileBrowserHandler* handler : common_tasks) { |
| const std::string extension_id = handler->extension_id(); |
| const Extension* extension = enabled_extensions.GetByID(extension_id); |
| DCHECK(extension); |
| |
| // TODO(zelidrag): Figure out how to expose icon URL that task defined in |
| // manifest instead of the default extension icon. |
| const GURL icon_url = GetIconURL(profile, *extension); |
| |
| result_list->push_back(FullTaskDescriptor( |
| TaskDescriptor(extension_id, file_tasks::TASK_TYPE_FILE_BROWSER_HANDLER, |
| handler->id()), |
| handler->title(), Verb::VERB_NONE /* no verb for FileBrowserHandler */, |
| icon_url, false /* is_default */, false /* is_generic_file_handler */, |
| false /* is_file_extension_match */)); |
| } |
| } |
| |
| void FindExtensionAndAppTasks( |
| Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| const std::vector<GURL>& file_urls, |
| FindTasksCallback callback, |
| std::unique_ptr<std::vector<FullTaskDescriptor>> result_list) { |
| std::vector<FullTaskDescriptor>* result_list_ptr = result_list.get(); |
| |
| // 2. Find and append Web tasks. |
| FindWebTasks(profile, entries, result_list_ptr); |
| |
| // 3. Continues from FindAllTypesOfTasks. Find and append file handler tasks. |
| FindFileHandlerTasks(profile, entries, result_list_ptr); |
| |
| // 4. Find and append file browser handler tasks. We know there aren't |
| // duplicates because "file_browser_handlers" and "file_handlers" shouldn't |
| // be used in the same manifest.json. |
| FindFileBrowserHandlerTasks(profile, file_urls, result_list_ptr); |
| |
| // TODO(crbug/1092784): Link app service task finder here to test the intent |
| // handling backend. This is not fully completed and only support sharing for |
| // now. When the unified sharesheet UI is completed, this might be called from |
| // a different place. |
| if (base::FeatureList::IsEnabled(features::kIntentHandlingSharing)) { |
| FindAppServiceTasks(profile, entries, file_urls, result_list_ptr); |
| } |
| |
| // 5. Find and append Guest OS tasks. |
| FindGuestOsTasks(profile, entries, file_urls, result_list_ptr, |
| // Done. Apply post-filtering and callback. |
| base::BindOnce(PostProcessFoundTasks, profile, entries, |
| std::move(callback), std::move(result_list))); |
| } |
| |
| void FindAllTypesOfTasks(Profile* profile, |
| const std::vector<extensions::EntryInfo>& entries, |
| const std::vector<GURL>& file_urls, |
| FindTasksCallback callback) { |
| DCHECK(profile); |
| std::unique_ptr<std::vector<FullTaskDescriptor>> result_list( |
| new std::vector<FullTaskDescriptor>); |
| |
| // 1. Find and append ARC handler tasks. |
| FindArcTasks(profile, entries, file_urls, std::move(result_list), |
| base::BindOnce(&FindExtensionAndAppTasks, profile, entries, |
| file_urls, std::move(callback))); |
| } |
| |
| void ChooseAndSetDefaultTask(const PrefService& pref_service, |
| const std::vector<extensions::EntryInfo>& entries, |
| std::vector<FullTaskDescriptor>* tasks) { |
| // Collect the default tasks from the preferences into a set. |
| std::set<TaskDescriptor> default_tasks; |
| for (const extensions::EntryInfo& entry : entries) { |
| const base::FilePath& file_path = entry.path; |
| const std::string& mime_type = entry.mime_type; |
| TaskDescriptor default_task; |
| if (file_tasks::GetDefaultTaskFromPrefs( |
| pref_service, mime_type, file_path.Extension(), &default_task)) { |
| default_tasks.insert(default_task); |
| } |
| } |
| |
| // Go through all the tasks from the beginning and see if there is any |
| // default task. If found, pick and set it as default and return. |
| for (FullTaskDescriptor& task : *tasks) { |
| DCHECK(!task.is_default); |
| if (base::Contains(default_tasks, task.task_descriptor)) { |
| task.is_default = true; |
| return; |
| } |
| } |
| |
| // No default task, check for an explicit file extension match (without |
| // MIME match) in the extension manifest and pick that over the fallback |
| // handlers below (see crbug.com/803930) |
| for (FullTaskDescriptor& task : *tasks) { |
| if (task.is_file_extension_match && !task.is_generic_file_handler && |
| !IsFallbackFileHandler(task)) { |
| task.is_default = true; |
| return; |
| } |
| } |
| |
| // Prefer a fallback app over viewing in the browser (crbug.com/1111399). |
| // Unless it's HTML which should open in the browser (crbug.com/1121396). |
| for (FullTaskDescriptor& task : *tasks) { |
| if (IsFallbackFileHandler(task) && |
| task.task_descriptor.action_id != "view-in-browser") { |
| const extensions::EntryInfo entry = entries[0]; |
| const base::FilePath& file_path = entry.path; |
| |
| if (IsHtmlFile(file_path)) { |
| break; |
| } |
| task.is_default = true; |
| return; |
| } |
| } |
| |
| // No default tasks found. If there is any fallback file browser handler, |
| // make it as default task, so it's selected by default. |
| for (FullTaskDescriptor& task : *tasks) { |
| DCHECK(!task.is_default); |
| if (IsFallbackFileHandler(task)) { |
| task.is_default = true; |
| return; |
| } |
| } |
| } |
| |
| bool IsHtmlFile(const base::FilePath& path) { |
| constexpr const char* kHtmlExtensions[] = {".htm", ".html", ".mhtml", |
| ".xht", ".xhtm", ".xhtml"}; |
| for (const char* extension : kHtmlExtensions) { |
| if (path.MatchesExtension(extension)) |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace file_tasks |
| } // namespace file_manager |