| // 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. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include "chrome/browser/apps/app_service/intent_util.h" |
| |
| #include <algorithm> |
| #include <functional> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/flat_set.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/apps/app_service/file_utils.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/extensions/application_launch.h" |
| #include "components/services/app_service/public/cpp/file_handler_info.h" |
| #include "components/services/app_service/public/cpp/intent_filter_util.h" |
| #include "components/services/app_service/public/cpp/intent_util.h" |
| #include "extensions/common/api/app_runtime.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest_handlers/file_handler_info.h" |
| #include "extensions/common/manifest_handlers/web_file_handlers_info.h" |
| #include "extensions/common/url_pattern.h" |
| #include "extensions/common/url_pattern_set.h" |
| #include "mojo/public/cpp/bindings/struct_ptr.h" |
| #include "third_party/re2/src/re2/re2.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "base/files/file_path.h" |
| #include "chrome/browser/ash/app_list/arc/intent.h" |
| #include "chrome/browser/ash/fusebox/fusebox_server.h" |
| #include "chrome/common/extensions/api/file_browser_handlers/file_browser_handler.h" |
| #include "chromeos/ash/experiences/arc/intent_helper/arc_intent_helper_bridge.h" |
| #include "chromeos/ash/experiences/arc/intent_helper/intent_constants.h" |
| #include "chromeos/ash/experiences/arc/intent_helper/intent_filter.h" |
| #include "chromeos/ash/experiences/arc/mojom/intent_helper.mojom-shared.h" |
| #include "chromeos/ash/experiences/arc/mojom/intent_helper.mojom.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| namespace apps_util { |
| |
| namespace { |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| apps::IntentFilterPtr CreateFileURLFilter( |
| const std::vector<std::string>& patterns, |
| const std::string& activity_name, |
| const std::string& activity_label) { |
| DCHECK(!patterns.empty()); |
| auto intent_filter = std::make_unique<apps::IntentFilter>(); |
| |
| // kAction == View. |
| apps::ConditionValues action_condition_values; |
| action_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| kIntentActionView, apps::PatternMatchType::kLiteral)); |
| auto action_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kAction, std::move(action_condition_values)); |
| intent_filter->conditions.push_back(std::move(action_condition)); |
| |
| // URL patterns. |
| apps::ConditionValues condition_values; |
| for (const std::string& pattern : patterns) { |
| condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| pattern, apps::PatternMatchType::kGlob)); |
| } |
| auto file_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kFile, std::move(condition_values)); |
| intent_filter->conditions.push_back(std::move(file_condition)); |
| |
| intent_filter->activity_name = activity_name; |
| intent_filter->activity_label = activity_label; |
| |
| return intent_filter; |
| } |
| |
| // Takes a URL pattern that represents a path like *.pdf and returns a string |
| // representing a pattern matching a file system URL spec. If |legacy| flag is |
| // set to true the function returns a pattern that matches URLs generated by the |
| // legacy Files app (e.g., "filesystem:chrome-extension://.*/.*\.pdf"). If |
| // |legacy| flag is set to false, the pattern matches URLs generated by the |
| // Files System Web App (e.g., "filesystem:chrome://file-manager/.*\.pdf). |
| const std::string URLPatternToFileSystemPattern(const URLPattern& pattern, |
| bool legacy) { |
| const char* scheme = |
| legacy ? "chrome-extension://*" : "chrome://file-manager"; |
| std::string path = |
| base::StrCat({url::kFileSystemScheme, ":", scheme, pattern.path()}); |
| base::ReplaceChars(path, ".", R"(\.)", &path); |
| base::ReplaceChars(path, "*", ".*", &path); |
| return path; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| constexpr char kIntentExtraText[] = "S.android.intent.extra.TEXT"; |
| constexpr char kIntentExtraSubject[] = "S.android.intent.extra.SUBJECT"; |
| constexpr char kIntentExtraStartType[] = "S.org.chromium.arc.start_type"; |
| constexpr char kIntentActionPrefix[] = "android.intent.action"; |
| constexpr char kType[] = "type"; |
| |
| constexpr int kIntentPrefixLength = 2; |
| |
| const char* ConvertAppServiceToArcIntentAction(const std::string& action) { |
| if (action == kIntentActionMain) { |
| return arc::kIntentActionMain; |
| } else if (action == kIntentActionView) { |
| return arc::kIntentActionView; |
| } else if (action == kIntentActionSend) { |
| return arc::kIntentActionSend; |
| } else if (action == kIntentActionSendMultiple) { |
| return arc::kIntentActionSendMultiple; |
| } else if (action.compare(0, strlen(kIntentActionPrefix), |
| kIntentActionPrefix) == 0) { |
| return action.c_str(); |
| } else { |
| return arc::kIntentActionView; |
| } |
| } |
| |
| // Returns true if |pattern| is a Glob (as in PatternMatchType::kGlob) which |
| // behaves like a Prefix pattern. That is, the only special characters are a |
| // ".*" at the end of the string. |
| bool IsPrefixOnlyGlob(std::string_view pattern) { |
| if (!base::EndsWith(pattern, ".*")) { |
| return false; |
| } |
| |
| size_t i = 0; |
| while (i < pattern.size() - 2) { |
| if (pattern[i] == '.' || pattern[i] == '*') { |
| return false; |
| } |
| i++; |
| } |
| return true; |
| } |
| |
| apps::ConditionValuePtr ConvertArcPatternMatcherToConditionValue( |
| const arc::IntentFilter::PatternMatcher& path) { |
| apps::PatternMatchType match_type; |
| if (!arc::IsKnownPatternType(path.match_type())) { |
| LOG(ERROR) |
| << " Received an ARC intent filter with unsupported PatternType: " |
| << path.match_type() |
| << " for the filter path, need to update ARC code to support new " |
| "pattern types."; |
| base::debug::DumpWithoutCrashing(); |
| return nullptr; |
| } |
| switch (path.match_type()) { |
| case arc::PatternType::kLiteral: |
| match_type = apps::PatternMatchType::kLiteral; |
| break; |
| case arc::PatternType::kPrefix: |
| match_type = apps::PatternMatchType::kPrefix; |
| break; |
| case arc::PatternType::kSimpleGlob: |
| match_type = apps::PatternMatchType::kGlob; |
| |
| // It's common for Globs to be used to encode patterns which are actually |
| // prefixes. Detect and convert these, since prefix matching is easier & |
| // cheaper. |
| if (IsPrefixOnlyGlob(path.pattern())) { |
| DCHECK_GE(path.pattern().size(), 2u); |
| return std::make_unique<apps::ConditionValue>( |
| path.pattern().substr(0, path.pattern().size() - 2), |
| apps::PatternMatchType::kPrefix); |
| } |
| break; |
| // TODO(crbug.com/40275407): support the new pattern types. |
| case arc::PatternType::kAdvancedGlob: |
| case arc::PatternType::kSuffix: |
| case arc::PatternType::kUnknown: |
| LOG(ERROR) |
| << " Received an ARC intent filter with unsupported PatternType: " |
| << path.match_type() |
| << " for the filter path. Need to update code to support new pattern " |
| "types."; |
| return nullptr; |
| } |
| |
| return std::make_unique<apps::ConditionValue>(path.pattern(), match_type); |
| } |
| |
| std::string ExtractExtensionType(std::string path) { |
| // Look for a valid set of characters at the end of the string that directly |
| // follow the characters ".*\.", e.g. the regex should capture "tar.gz" in |
| // this string: ".*\..*\..*\..*\.tar.gz". |
| // TODO(b/270483199): Make this regex stricter to check for invalid characters |
| // at the start and middle of the path, and any invalid characters at the |
| // start of the extension. |
| re2::RE2 extension_regex_pattern("\\.\\*\\\\.([a-zA-Z0-9_\\-.]*)$"); |
| std::string extension_capture; |
| RE2::PartialMatch(path, extension_regex_pattern, &extension_capture); |
| return extension_capture; |
| } |
| |
| apps::ConditionValues ConvertPathToExtensionConditionValues( |
| apps::ConditionValues path_condition_values) { |
| base::flat_set<std::string> unique_extensions; |
| // Go through all the path values and extract any file extensions. |
| for (auto& path_condition_value : path_condition_values) { |
| if (path_condition_value->match_type != apps::PatternMatchType::kGlob) { |
| continue; |
| } |
| std::string extension_type = |
| ExtractExtensionType(path_condition_value->value); |
| if (extension_type.empty()) { |
| continue; |
| } |
| // If we found an extension type, save it in the set. |
| unique_extensions.insert(extension_type); |
| } |
| // Convert all the unique extension types into condition values. |
| apps::ConditionValues ext_condition_values; |
| for (const std::string& ext : unique_extensions) { |
| ext_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| ext, apps::PatternMatchType::kFileExtension)); |
| } |
| return ext_condition_values; |
| } |
| |
| bool IsFileExtensionFilter(const arc::IntentFilter& arc_intent_filter) { |
| // Check that we have the correct fields available. |
| if (arc_intent_filter.paths().size() == 0 || |
| arc_intent_filter.schemes().size() == 0) { |
| return false; |
| } |
| |
| // Check that the filter has a view action. |
| if (!base::Contains(arc_intent_filter.actions(), arc::kIntentActionView)) { |
| return false; |
| } |
| |
| // Check that the scheme is generic or has a value related to files. |
| bool has_generic_scheme = std::ranges::any_of( |
| arc_intent_filter.schemes(), [](const std::string& scheme) { |
| return scheme == "content" || scheme == "file" || scheme == "*"; |
| }); |
| if (!has_generic_scheme) { |
| return false; |
| } |
| |
| // Check that the host is generic or doesn't exist. |
| bool has_generic_host = std::ranges::any_of( |
| arc_intent_filter.authorities(), |
| [](const arc::IntentFilter::AuthorityEntry& authority) { |
| return authority.wild(); |
| }); |
| if (arc_intent_filter.authorities().size() != 0 && !has_generic_host) { |
| return false; |
| } |
| |
| // Check that the mime type is generic or doesn't exist. |
| bool has_generic_mime = std::ranges::any_of( |
| arc_intent_filter.mime_types(), [](const std::string& mime_type) { |
| return mime_type == "*" || mime_type == "*/*"; |
| }); |
| if (arc_intent_filter.mime_types().size() != 0 && !has_generic_mime) { |
| return false; |
| } |
| return true; |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| } // namespace |
| |
| apps::IntentFilterPtr CreateFileFilter( |
| const std::vector<std::string>& intent_actions, |
| const std::vector<std::string>& mime_types, |
| const std::vector<std::string>& file_extensions, |
| const std::string& activity_name, |
| bool include_directories) { |
| DCHECK(!mime_types.empty() || !file_extensions.empty()); |
| auto intent_filter = std::make_unique<apps::IntentFilter>(); |
| |
| // kAction == View, Share etc. |
| apps::ConditionValues action_condition_values; |
| for (auto& action : intent_actions) { |
| action_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| action, apps::PatternMatchType::kLiteral)); |
| } |
| if (!action_condition_values.empty()) { |
| intent_filter->conditions.push_back(std::make_unique<apps::Condition>( |
| apps::ConditionType::kAction, std::move(action_condition_values))); |
| } |
| |
| apps::ConditionValues file_condition_values; |
| |
| // Mime types. |
| for (auto& mime_type : mime_types) { |
| file_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| mime_type, apps::PatternMatchType::kMimeType)); |
| } |
| // And file extensions. |
| for (const std::string& extension : file_extensions) { |
| file_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| extension, apps::PatternMatchType::kFileExtension)); |
| } |
| if (include_directories) { |
| file_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| "", apps::PatternMatchType::kIsDirectory)); |
| } |
| |
| DCHECK(!file_condition_values.empty()); |
| if (!file_condition_values.empty()) { |
| intent_filter->conditions.push_back(std::make_unique<apps::Condition>( |
| apps::ConditionType::kFile, std::move(file_condition_values))); |
| } |
| |
| if (!activity_name.empty()) { |
| intent_filter->activity_name = activity_name; |
| } |
| |
| return intent_filter; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| apps::IntentFilters CreateIntentFiltersFromArcBridge( |
| const std::string& package_name, |
| arc::ArcIntentHelperBridge* intent_helper_bridge) { |
| apps::IntentFilters filters; |
| const std::vector<arc::IntentFilter>& arc_intent_filters = |
| intent_helper_bridge->GetIntentFilterForPackage(package_name); |
| for (const auto& arc_intent_filter : arc_intent_filters) { |
| apps::IntentFilterPtr filter = CreateIntentFilterForArc(arc_intent_filter); |
| if (filter) { |
| filters.push_back(std::move(filter)); |
| } |
| } |
| return filters; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| apps::IntentFilters CreateIntentFiltersForChromeApp( |
| const extensions::Extension* extension) { |
| apps::IntentFilters filters; |
| |
| // Check that the extension can be launched with files. This includes all |
| // platform apps and allowlisted extensions. |
| if (!CanLaunchViaEvent(extension)) { |
| return filters; |
| } |
| |
| const extensions::FileHandlersInfo* file_handlers = |
| extensions::FileHandlers::GetFileHandlers(extension); |
| if (!file_handlers) { |
| return filters; |
| } |
| |
| for (const apps::FileHandlerInfo& handler : *file_handlers) { |
| // "share_with", "add_to" and "pack_with" are ignored in the Files app |
| // frontend. |
| if (handler.verb != apps::file_handler_verbs::kOpenWith) { |
| continue; |
| } |
| std::vector<std::string> mime_types(handler.types.begin(), |
| handler.types.end()); |
| std::vector<std::string> file_extensions(handler.extensions.begin(), |
| handler.extensions.end()); |
| filters.push_back(CreateFileFilter({kIntentActionView}, mime_types, |
| file_extensions, handler.id, |
| handler.include_directories)); |
| filters.back()->activity_label = extension->name(); |
| } |
| |
| return filters; |
| } |
| |
| apps::IntentFilters CreateIntentFiltersForExtension( |
| const extensions::Extension* extension) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| // MV3+ manifest support for the `file_handlers` key. |
| DCHECK(extension); |
| const extensions::WebFileHandlersInfo* intent_filter_data = |
| extensions::WebFileHandlers::GetFileHandlers(*extension); |
| if (intent_filter_data && !intent_filter_data->empty()) { |
| apps::IntentFilters filters; |
| for (const auto& web_file_handler : *intent_filter_data) { |
| // Flatten mime_types and file_extensions. |
| std::vector<std::string> mime_types; |
| std::vector<std::string> file_extensions; |
| for (const auto [mime_type, file_extension_list] : |
| web_file_handler.file_handler.accept.additional_properties) { |
| mime_types.emplace_back(mime_type); |
| for (const auto& file_extension : file_extension_list.GetList()) { |
| file_extensions.emplace_back(file_extension.GetString()); |
| } |
| } |
| |
| filters.push_back(CreateFileFilter({kIntentActionView}, mime_types, |
| file_extensions, |
| web_file_handler.file_handler.action)); |
| } |
| return filters; |
| } |
| |
| FileBrowserHandler::List* handler_list = |
| FileBrowserHandler::GetHandlers(extension); |
| if (!handler_list) { |
| return {}; |
| } |
| |
| apps::IntentFilters filters; |
| for (const std::unique_ptr<FileBrowserHandler>& handler : *handler_list) { |
| std::vector<std::string> patterns; |
| for (const URLPattern& pattern : handler->file_url_patterns()) { |
| patterns.push_back(URLPatternToFileSystemPattern(pattern, true)); |
| patterns.push_back(URLPatternToFileSystemPattern(pattern, false)); |
| } |
| filters.push_back( |
| CreateFileURLFilter(patterns, handler->id(), handler->title())); |
| } |
| return filters; |
| #else |
| return {}; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| apps::IntentFilterPtr CreateNoteTakingFilter() { |
| auto intent_filter = std::make_unique<apps::IntentFilter>(); |
| intent_filter->AddSingleValueCondition(apps::ConditionType::kAction, |
| kIntentActionCreateNote, |
| apps::PatternMatchType::kLiteral); |
| return intent_filter; |
| } |
| |
| apps::IntentFilterPtr CreateLockScreenFilter() { |
| auto intent_filter = std::make_unique<apps::IntentFilter>(); |
| intent_filter->AddSingleValueCondition(apps::ConditionType::kAction, |
| kIntentActionStartOnLockScreen, |
| apps::PatternMatchType::kLiteral); |
| return intent_filter; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| apps::IntentPtr CreateShareIntentFromFiles( |
| Profile* profile, |
| const std::vector<base::FilePath>& file_paths, |
| const std::vector<std::string>& mime_types) { |
| auto file_urls = apps::GetFileSystemUrls(profile, file_paths); |
| return MakeShareIntent(file_urls, mime_types); |
| } |
| |
| apps::IntentPtr CreateShareIntentFromFiles( |
| Profile* profile, |
| const std::vector<base::FilePath>& file_paths, |
| const std::vector<std::string>& mime_types, |
| const std::string& share_text, |
| const std::string& share_title) { |
| auto file_urls = apps::GetFileSystemUrls(profile, file_paths); |
| return MakeShareIntent(file_urls, mime_types, share_text, share_title); |
| } |
| |
| base::flat_map<std::string, std::string> CreateArcIntentExtras( |
| const apps::IntentPtr& intent) { |
| auto extras = base::flat_map<std::string, std::string>(); |
| if (intent->share_text.has_value()) { |
| // Slice off the "S." prefix for the key. |
| extras.insert(std::make_pair(kIntentExtraText + kIntentPrefixLength, |
| intent->share_text.value())); |
| } |
| if (intent->share_title.has_value()) { |
| // Slice off the "S." prefix for the key. |
| extras.insert(std::make_pair(kIntentExtraSubject + kIntentPrefixLength, |
| intent->share_title.value())); |
| } |
| if (intent->start_type.has_value()) { |
| // Slice off the "S." prefix for the key. |
| extras.insert(std::make_pair(kIntentExtraStartType + kIntentPrefixLength, |
| intent->start_type.value())); |
| } |
| if (!intent->extras.empty()) { |
| extras.insert(intent->extras.begin(), intent->extras.end()); |
| } |
| return extras; |
| } |
| |
| arc::mojom::IntentInfoPtr ConvertAppServiceToArcIntent( |
| const apps::IntentPtr& intent) { |
| arc::mojom::IntentInfoPtr arc_intent; |
| if (!intent->url.has_value() && !intent->share_text.has_value() && |
| !intent->activity_name.has_value()) { |
| return arc_intent; |
| } |
| |
| arc_intent = arc::mojom::IntentInfo::New(); |
| arc_intent->action = ConvertAppServiceToArcIntentAction(intent->action); |
| if (intent->url.has_value()) { |
| arc_intent->data = intent->url->spec(); |
| } |
| if (intent->share_text.has_value() || intent->share_title.has_value() || |
| intent->start_type.has_value() || !intent->extras.empty()) { |
| arc_intent->extras = CreateArcIntentExtras(intent); |
| } |
| if (!intent->categories.empty()) { |
| arc_intent->categories = intent->categories; |
| } |
| if (intent->data.has_value()) { |
| arc_intent->data = intent->data; |
| } |
| if (intent->mime_type.has_value()) { |
| arc_intent->type = intent->mime_type; |
| } |
| if (intent->ui_bypassed.has_value()) { |
| arc_intent->ui_bypassed = intent->ui_bypassed.value(); |
| } |
| return arc_intent; |
| } |
| |
| const char* ConvertArcToAppServiceIntentAction(const std::string& arc_action) { |
| if (arc_action == arc::kIntentActionMain) { |
| return kIntentActionMain; |
| } else if (arc_action == arc::kIntentActionView) { |
| return kIntentActionView; |
| } else if (arc_action == arc::kIntentActionSend) { |
| return kIntentActionSend; |
| } else if (arc_action == arc::kIntentActionSendMultiple) { |
| return kIntentActionSendMultiple; |
| } |
| |
| return nullptr; |
| } |
| |
| std::string CreateLaunchIntent(const std::string& package_name, |
| const apps::IntentPtr& intent) { |
| // If |intent| has |ui_bypassed|, |url| or |data|, it is too complex to |
| // convert to a string, so return the empty string. |
| if (intent->ui_bypassed.has_value() || intent->url.has_value() || |
| intent->data.has_value()) { |
| return std::string(); |
| } |
| |
| std::string ret = base::StringPrintf("%s;", arc::kIntentPrefix); |
| |
| // Convert action. |
| std::string action = ConvertAppServiceToArcIntentAction(intent->action); |
| ret += base::StringPrintf("%s=%s;", arc::kAction, |
| ConvertAppServiceToArcIntentAction(intent->action)); |
| |
| // Convert categories. |
| for (const auto& category : intent->categories) { |
| ret += base::StringPrintf("%s=%s;", arc::kCategory, category.c_str()); |
| } |
| |
| // Set launch flags. |
| ret += |
| base::StringPrintf("%s=0x%x;", arc::kLaunchFlags, |
| arc::Intent::FLAG_ACTIVITY_NEW_TASK | |
| arc::Intent::FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); |
| |
| // Convert activity_name. |
| if (intent->activity_name.has_value()) { |
| // Remove the |package_name| prefix, if activity starts with it. |
| const std::string& activity = intent->activity_name.value(); |
| const char* activity_compact_name = |
| activity.find(package_name.c_str()) == 0 |
| ? activity.c_str() + package_name.length() |
| : activity.c_str(); |
| ret += base::StringPrintf("%s=%s/%s;", arc::kComponent, |
| package_name.c_str(), activity_compact_name); |
| } else { |
| ret += base::StringPrintf("%s=%s/;", arc::kComponent, package_name.c_str()); |
| } |
| |
| if (intent->mime_type.has_value()) { |
| ret += |
| base::StringPrintf("%s=%s;", kType, intent->mime_type.value().c_str()); |
| } |
| |
| if (intent->share_text.has_value()) { |
| ret += base::StringPrintf("%s=%s;", kIntentExtraText, |
| intent->share_text.value().c_str()); |
| } |
| |
| if (intent->share_title.has_value()) { |
| ret += base::StringPrintf("%s=%s;", kIntentExtraSubject, |
| intent->share_title.value().c_str()); |
| } |
| |
| if (intent->start_type.has_value()) { |
| ret += base::StringPrintf("%s=%s;", kIntentExtraStartType, |
| intent->start_type.value().c_str()); |
| } |
| |
| for (auto it : intent->extras) { |
| ret += base::StringPrintf("%s=%s;", it.first.c_str(), it.second.c_str()); |
| } |
| |
| ret += arc::kEndSuffix; |
| DCHECK(!ret.empty()); |
| return ret; |
| } |
| |
| arc::IntentFilter ConvertAppServiceToArcIntentFilter( |
| const std::string& package_name, |
| const apps::IntentFilterPtr& intent_filter) { |
| std::vector<std::string> actions; |
| std::vector<std::string> schemes; |
| std::vector<arc::IntentFilter::AuthorityEntry> authorities; |
| std::vector<arc::IntentFilter::PatternMatcher> paths; |
| std::vector<std::string> mime_types; |
| for (auto& condition : intent_filter->conditions) { |
| switch (condition->condition_type) { |
| case apps::ConditionType::kScheme: |
| for (auto& condition_value : condition->condition_values) { |
| schemes.push_back(condition_value->value); |
| } |
| break; |
| case apps::ConditionType::kAuthority: |
| for (auto& condition_value : condition->condition_values) { |
| authorities.emplace_back( |
| /*host=*/condition_value->value, /*port=*/0); |
| } |
| break; |
| case apps::ConditionType::kPath: |
| for (auto& condition_value : condition->condition_values) { |
| arc::PatternType match_type; |
| switch (condition_value->match_type) { |
| case apps::PatternMatchType::kLiteral: |
| match_type = arc::PatternType::kLiteral; |
| break; |
| case apps::PatternMatchType::kPrefix: |
| match_type = arc::PatternType::kPrefix; |
| break; |
| case apps::PatternMatchType::kGlob: |
| match_type = arc::PatternType::kSimpleGlob; |
| break; |
| case apps::PatternMatchType::kMimeType: |
| case apps::PatternMatchType::kFileExtension: |
| case apps::PatternMatchType::kIsDirectory: |
| case apps::PatternMatchType::kSuffix: |
| NOTREACHED(); |
| } |
| paths.emplace_back(condition_value->value, match_type); |
| } |
| break; |
| case apps::ConditionType::kAction: |
| for (auto& condition_value : condition->condition_values) { |
| actions.push_back( |
| ConvertAppServiceToArcIntentAction(condition_value->value)); |
| } |
| break; |
| case apps::ConditionType::kMimeType: |
| for (auto& condition_value : condition->condition_values) { |
| mime_types.push_back(condition_value->value); |
| } |
| break; |
| case apps::ConditionType::kFile: |
| NOTREACHED(); |
| } |
| } |
| return arc::IntentFilter(package_name, std::move(actions), |
| std::move(authorities), std::move(paths), |
| std::move(schemes), std::move(mime_types)); |
| } |
| |
| apps::IntentFilterPtr CreateIntentFilterForArc( |
| const arc::IntentFilter& arc_intent_filter) { |
| auto intent_filter = std::make_unique<apps::IntentFilter>(); |
| |
| bool has_view_action = false; |
| apps::ConditionValues action_condition_values; |
| for (auto& arc_action : arc_intent_filter.actions()) { |
| const char* action = ConvertArcToAppServiceIntentAction(arc_action); |
| has_view_action = has_view_action || action == kIntentActionView; |
| |
| if (!action) { |
| continue; |
| } |
| |
| action_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| action, apps::PatternMatchType::kLiteral)); |
| } |
| if (!action_condition_values.empty()) { |
| auto action_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kAction, std::move(action_condition_values)); |
| intent_filter->conditions.push_back(std::move(action_condition)); |
| } |
| |
| bool is_mime_file_filter = |
| has_view_action && arc_intent_filter.mime_types().size() > 0; |
| bool is_file_extension_filter = IsFileExtensionFilter(arc_intent_filter); |
| bool is_file_filter = is_mime_file_filter || is_file_extension_filter; |
| |
| // Don't allow scheme/ host for ARC view file filters. |
| if (!is_file_filter) { |
| apps::ConditionValues scheme_condition_values; |
| for (auto& scheme : arc_intent_filter.schemes()) { |
| scheme_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| scheme, apps::PatternMatchType::kLiteral)); |
| } |
| if (!scheme_condition_values.empty()) { |
| auto scheme_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kScheme, std::move(scheme_condition_values)); |
| intent_filter->conditions.push_back(std::move(scheme_condition)); |
| } |
| |
| apps::ConditionValues host_condition_values; |
| for (auto& authority : arc_intent_filter.authorities()) { |
| auto match_type = authority.wild() ? apps::PatternMatchType::kSuffix |
| : apps::PatternMatchType::kLiteral; |
| host_condition_values.push_back( |
| std::make_unique<apps::ConditionValue>(authority.host(), match_type)); |
| } |
| |
| if (!host_condition_values.empty()) { |
| // It's common for Android apps to include duplicate host conditions, we |
| // can de-duplicate these to reduce time/space usage down the line. |
| std::sort( |
| host_condition_values.begin(), host_condition_values.end(), |
| [](const apps::ConditionValuePtr& v1, |
| const apps::ConditionValuePtr& v2) -> bool { |
| return v1->value < v2->value || |
| (v1->value == v2->value && v1->match_type < v2->match_type); |
| }); |
| host_condition_values.erase( |
| std::unique(host_condition_values.begin(), |
| host_condition_values.end(), |
| [](const apps::ConditionValuePtr& v1, |
| const apps::ConditionValuePtr& v2) -> bool { |
| return *v1 == *v2; |
| }), |
| host_condition_values.end()); |
| |
| auto host_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kAuthority, std::move(host_condition_values)); |
| intent_filter->conditions.push_back(std::move(host_condition)); |
| } |
| } |
| |
| apps::ConditionValues path_condition_values; |
| bool has_invalid_path = false; |
| for (auto& path : arc_intent_filter.paths()) { |
| apps::ConditionValuePtr path_condition_value = |
| ConvertArcPatternMatcherToConditionValue(path); |
| if (path_condition_value) { |
| path_condition_values.push_back(std::move(path_condition_value)); |
| } else { |
| has_invalid_path = true; |
| } |
| } |
| |
| // If there is path condition set in ARC app, but we cannot get valid path, |
| // it is likely that the only path condition set in ARC is value that we |
| // cannot handle. We should not create this intent filter because empty path |
| // condition means it matches with any path, which is different from what it |
| // is expected. |
| if (path_condition_values.empty() && has_invalid_path) { |
| return nullptr; |
| } |
| |
| // For ARC apps, specifying a path is optional. For any intent filters which |
| // match every URL on a host with a "view" action, add a path which matches |
| // everything to ensure the filter is treated as a supported link. |
| if (path_condition_values.empty() && has_view_action && |
| arc_intent_filter.authorities().size() > 0 && |
| arc_intent_filter.schemes().size() > 0) { |
| path_condition_values.push_back(std::make_unique<apps::ConditionValue>( |
| "/", apps::PatternMatchType::kPrefix)); |
| } |
| |
| // For path file filters, extract the desired file extension from the path |
| // fields listed in the intent filter and add it to the new filter as a |
| // general kFile condition. |
| if (is_file_extension_filter) { |
| // Convert the path condition values into extension condition values. |
| apps::ConditionValues ext_condition_values = |
| ConvertPathToExtensionConditionValues(std::move(path_condition_values)); |
| // If this is a path file filter without any valid file extensions, then the |
| // entire intent filter is invalid. |
| if (ext_condition_values.size() == 0) { |
| return nullptr; |
| } |
| // Wrap any found extension condition values into one file condition. |
| auto file_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kFile, std::move(ext_condition_values)); |
| intent_filter->conditions.push_back(std::move(file_condition)); |
| } else if (!path_condition_values.empty()) { |
| auto path_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kPath, std::move(path_condition_values)); |
| intent_filter->conditions.push_back(std::move(path_condition)); |
| } |
| |
| if (!is_file_extension_filter) { |
| apps::ConditionValues mime_type_condition_values; |
| for (auto& mime_type : arc_intent_filter.mime_types()) { |
| mime_type_condition_values.push_back( |
| std::make_unique<apps::ConditionValue>( |
| mime_type, apps::PatternMatchType::kMimeType)); |
| } |
| if (!mime_type_condition_values.empty()) { |
| // For ARC view file intents, save the mime type conditions under kFile |
| // instead of kMimeType to maintain consistency with view file intents of |
| // other app types. |
| if (is_mime_file_filter) { |
| auto file_type_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kFile, std::move(mime_type_condition_values)); |
| intent_filter->conditions.push_back(std::move(file_type_condition)); |
| } else { |
| auto mime_type_condition = std::make_unique<apps::Condition>( |
| apps::ConditionType::kMimeType, |
| std::move(mime_type_condition_values)); |
| intent_filter->conditions.push_back(std::move(mime_type_condition)); |
| } |
| } |
| } |
| |
| if (!arc_intent_filter.activity_name().empty()) { |
| intent_filter->activity_name = arc_intent_filter.activity_name(); |
| } |
| if (!arc_intent_filter.activity_label().empty()) { |
| intent_filter->activity_label = arc_intent_filter.activity_label(); |
| } |
| |
| return intent_filter; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| } // namespace apps_util |