| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/services/app_service/public/cpp/intent_util.h" |
| |
| #include <algorithm> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/compiler_specific.h" |
| #include "base/containers/flat_map.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/to_string.h" |
| #include "base/values.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| |
| namespace { |
| |
| const char kWildCardAny[] = "*"; |
| const char kMimeTypeSeparator[] = "/"; |
| constexpr size_t kMimeTypeComponentSize = 2; |
| const char kAuthorityHostPortSeparator[] = ":"; |
| |
| const char kActionKey[] = "action"; |
| const char kUrlKey[] = "url"; |
| const char kMimeTypeKey[] = "mime_type"; |
| const char kFileUrlsKey[] = "file_urls"; |
| const char kActivityNameKey[] = "activity_name"; |
| const char kDriveShareUrlKey[] = "drive_share_url"; |
| const char kShareTextKey[] = "share_text"; |
| const char kShareTitleKey[] = "share_title"; |
| const char kStartTypeKey[] = "start_type"; |
| const char kCategoriesKey[] = "categories"; |
| const char kDataKey[] = "data"; |
| const char kUiBypassedKey[] = "ui_bypassed"; |
| const char kExtrasKey[] = "extras"; |
| const char kMimeTypeInodeDirectory[] = "inode/directory"; |
| |
| bool ComponentMatched(const std::string& intent_component, |
| const std::string& filter_component) { |
| return filter_component == kWildCardAny || |
| intent_component == filter_component; |
| } |
| |
| } // namespace |
| |
| namespace apps_util { |
| |
| const char kIntentActionMain[] = "main"; |
| const char kIntentActionView[] = "view"; |
| const char kIntentActionSend[] = "send"; |
| const char kIntentActionSendMultiple[] = "send_multiple"; |
| const char kIntentActionCreateNote[] = "create_note"; |
| const char kIntentActionStartOnLockScreen[] = "start_on_lock_screen"; |
| const char kIntentActionEdit[] = "edit"; |
| const char kIntentActionPotentialFileHandler[] = "potential_file_handler"; |
| |
| const char kUseBrowserForLink[] = "use_browser"; |
| const char kGuestOsActivityName[] = "open-with"; |
| |
| apps::IntentPtr MakeShareIntent(const std::vector<GURL>& filesystem_urls, |
| const std::vector<std::string>& mime_types) { |
| auto intent = std::make_unique<apps::Intent>(filesystem_urls.size() == 1 |
| ? kIntentActionSend |
| : kIntentActionSendMultiple); |
| intent->mime_type = CalculateCommonMimeType(mime_types); |
| |
| DCHECK_EQ(filesystem_urls.size(), mime_types.size()); |
| for (size_t i = 0; i < filesystem_urls.size(); i++) { |
| auto file = std::make_unique<apps::IntentFile>(filesystem_urls[i]); |
| file->mime_type = mime_types.at(i); |
| intent->files.push_back(std::move(file)); |
| } |
| |
| return intent; |
| } |
| |
| apps::IntentPtr MakeShareIntent(const std::vector<GURL>& filesystem_urls, |
| const std::vector<std::string>& mime_types, |
| const std::string& text, |
| const std::string& title) { |
| auto intent = MakeShareIntent(filesystem_urls, mime_types); |
| if (!text.empty()) { |
| intent->share_text = text; |
| } |
| if (!title.empty()) { |
| intent->share_title = title; |
| } |
| return intent; |
| } |
| |
| apps::IntentPtr MakeShareIntent(const GURL& filesystem_url, |
| const std::string& mime_type, |
| const GURL& drive_share_url, |
| bool is_directory) { |
| auto intent = std::make_unique<apps::Intent>(kIntentActionSend); |
| if (!is_directory) { |
| intent->mime_type = mime_type; |
| intent->files = std::vector<apps::IntentFilePtr>{}; |
| auto file = std::make_unique<apps::IntentFile>(filesystem_url); |
| file->mime_type = mime_type; |
| intent->files.push_back(std::move(file)); |
| } |
| if (!drive_share_url.is_empty()) { |
| intent->drive_share_url = drive_share_url; |
| } |
| return intent; |
| } |
| |
| apps::IntentPtr MakeShareIntent(const std::string& text, |
| const std::string& title) { |
| auto intent = std::make_unique<apps::Intent>(kIntentActionSend); |
| intent->mime_type = "text/plain"; |
| intent->share_text = text; |
| if (!title.empty()) { |
| intent->share_title = title; |
| } |
| return intent; |
| } |
| |
| apps::IntentPtr MakeShareIntent( |
| const std::vector<GURL>& filesystem_urls, |
| const std::vector<std::string>& mime_types, |
| const std::vector<std::string>& dlp_source_urls) { |
| auto intent = MakeShareIntent(filesystem_urls, mime_types); |
| |
| DCHECK_EQ(filesystem_urls.size(), dlp_source_urls.size()); |
| for (size_t i = 0; i < filesystem_urls.size(); i++) { |
| intent->files[i]->dlp_source_url = dlp_source_urls[i]; |
| } |
| return intent; |
| } |
| |
| apps::IntentPtr MakeEditIntent(const GURL& filesystem_url, |
| const std::string& mime_type) { |
| auto intent = std::make_unique<apps::Intent>(kIntentActionEdit); |
| intent->mime_type = mime_type; |
| |
| auto file = std::make_unique<apps::IntentFile>(filesystem_url); |
| file->mime_type = mime_type; |
| intent->files.push_back(std::move(file)); |
| return intent; |
| } |
| |
| apps::IntentPtr MakeIntentForActivity(const std::string& activity, |
| const std::string& start_type, |
| const std::string& category) { |
| auto intent = std::make_unique<apps::Intent>(kIntentActionMain); |
| intent->activity_name = activity; |
| intent->start_type = start_type; |
| intent->categories = std::vector<std::string>{category}; |
| return intent; |
| } |
| |
| apps::IntentPtr CreateCreateNoteIntent() { |
| return std::make_unique<apps::Intent>(kIntentActionCreateNote); |
| } |
| |
| apps::IntentPtr CreateStartOnLockScreenIntent() { |
| return std::make_unique<apps::Intent>(kIntentActionStartOnLockScreen); |
| } |
| |
| bool ConditionValueMatches(std::string_view value, |
| const apps::ConditionValuePtr& condition_value) { |
| return PatternMatchValue(value, condition_value->match_type, |
| condition_value->value); |
| } |
| |
| bool PatternMatchValue(std::string_view test_value, |
| apps::PatternMatchType match_type, |
| std::string_view match_value) { |
| switch (match_type) { |
| case apps::PatternMatchType::kLiteral: |
| return test_value == match_value; |
| case apps::PatternMatchType::kPrefix: |
| return base::StartsWith(test_value, match_value, |
| base::CompareCase::INSENSITIVE_ASCII); |
| case apps::PatternMatchType::kSuffix: |
| return base::EndsWith(test_value, match_value, |
| base::CompareCase::INSENSITIVE_ASCII); |
| case apps::PatternMatchType::kGlob: |
| return MatchGlob(test_value, match_value); |
| case apps::PatternMatchType::kMimeType: |
| // kMimeType as a match for kFile is handled in FileMatchesConditionValue. |
| return MimeTypeMatched(test_value, match_value); |
| case apps::PatternMatchType::kFileExtension: |
| case apps::PatternMatchType::kIsDirectory: { |
| // Handled in FileMatchesConditionValue. |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| bool IsGenericFileHandler(const apps::IntentPtr& intent, |
| const apps::IntentFilterPtr& filter) { |
| if (!intent || !filter || intent->files.empty()) { |
| return false; |
| } |
| |
| std::set<std::string> mime_types; |
| std::set<std::string> file_extensions; |
| filter->GetMimeTypesAndExtensions(mime_types, file_extensions); |
| if (file_extensions.count("*") > 0 || mime_types.count("*") > 0 || |
| mime_types.count("*/*") > 0) { |
| return true; |
| } |
| |
| // If a text/* file handler matches with an unsupported text mime type, we |
| // regard it as a generic match. |
| if (mime_types.count("text/*")) { |
| for (const auto& file : intent->files) { |
| DCHECK(file); |
| if (file->mime_type.has_value() && |
| blink::IsUnsupportedTextMimeType(file->mime_type.value())) { |
| return true; |
| } |
| } |
| } |
| |
| // If directory is selected, it is generic unless mime_types included |
| // 'inode/directory'. |
| for (const auto& file : intent->files) { |
| DCHECK(file); |
| if (file->is_directory.value_or(false)) { |
| return mime_types.count(kMimeTypeInodeDirectory) == 0; |
| } |
| } |
| return false; |
| } |
| |
| bool MatchGlob(std::string_view value, std::string_view pattern) { |
| static constexpr auto get_char = [](std::string_view s, size_t i) { |
| if (i >= s.length()) [[unlikely]] { |
| return '\0'; |
| } |
| return s[i]; |
| }; |
| |
| const size_t NP = pattern.length(); |
| const size_t NS = value.length(); |
| if (NP == 0) { |
| return NS == 0; |
| } |
| size_t ip = 0, is = 0; |
| char nextChar = get_char(pattern, 0); |
| while (ip < NP && is < NS) { |
| char c = nextChar; |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| const bool escaped = (c == '\\'); |
| if (escaped) { |
| c = nextChar; |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| } |
| if (nextChar == '*') { |
| if (!escaped && c == '.') { |
| if (ip >= (NP - 1)) { |
| // At the end with a pattern match |
| return true; |
| } |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| // Consume everything until the next char in the pattern is found. |
| if (nextChar == '\\') { |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| } |
| do { |
| if (get_char(value, is) == nextChar) { |
| break; |
| } |
| ++is; |
| } while (is < NS); |
| if (is == NS) { |
| // Next char in the pattern didn't exist in the match. |
| return false; |
| } |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| ++is; |
| } else { |
| // Consume only characters matching the one before '*'. |
| do { |
| if (get_char(value, is) != c) { |
| break; |
| } |
| ++is; |
| } while (is < NS); |
| ++ip; |
| nextChar = get_char(pattern, ip); |
| } |
| } else { |
| if (c != '.' && get_char(value, is) != c) { |
| return false; |
| } |
| ++is; |
| } |
| } |
| |
| if (ip >= NP && is >= NS) { |
| // Reached the end of both strings |
| return true; |
| } |
| |
| // One last check: we may have finished the match string, but still have a |
| // '.*' at the end of the pattern, which is still a match. |
| if (ip == NP - 2 && get_char(pattern, ip) == '.' && |
| get_char(pattern, ip + 1) == '*') { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool MimeTypeMatched(std::string_view intent_mime_type, |
| std::string_view filter_mime_type) { |
| std::vector<std::string> intent_components = |
| base::SplitString(intent_mime_type, kMimeTypeSeparator, |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| std::vector<std::string> filter_components = |
| base::SplitString(filter_mime_type, kMimeTypeSeparator, |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| if (intent_components.size() > kMimeTypeComponentSize || |
| filter_components.size() > kMimeTypeComponentSize || |
| intent_components.size() == 0 || filter_components.size() == 0) { |
| return false; |
| } |
| |
| // If the filter component only contain main mime type, check if main type |
| // matches. |
| if (filter_components.size() == 1) { |
| return ComponentMatched(intent_components[0], filter_components[0]); |
| } |
| |
| // If the intent component only contain main mime type, complete the |
| // mime type. |
| if (intent_components.size() == 1) { |
| intent_components.push_back(kWildCardAny); |
| } |
| |
| // Both intent and intent filter can use wildcard for mime type. |
| for (size_t i = 0; i < kMimeTypeComponentSize; i++) { |
| if (!ComponentMatched(intent_components[i], filter_components[i])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool ExtensionMatched(const std::string& file_name, |
| const std::string& filter_extension) { |
| if (filter_extension == kWildCardAny) |
| return true; |
| |
| // Normalise to have a preceding ".". |
| std::string normalised_extension = filter_extension; |
| if (filter_extension.length() > 0 && filter_extension[0] != '.') { |
| normalised_extension = '.' + normalised_extension; |
| } |
| base::FilePath::StringType handler_extension = |
| base::FilePath::FromUTF8Unsafe(normalised_extension).Extension(); |
| |
| base::FilePath file_path = base::FilePath::FromUTF8Unsafe(file_name); |
| |
| // Accept files whose extension or combined extension (e.g. ".tar.gz") |
| // match the filter extension. |
| return base::FilePath::CompareEqualIgnoreCase(handler_extension, |
| file_path.Extension()) || |
| base::FilePath::CompareEqualIgnoreCase(handler_extension, |
| file_path.FinalExtension()); |
| } |
| |
| base::Value ConvertIntentToValue(const apps::IntentPtr& intent) { |
| base::Value::Dict intent_value; |
| intent_value.Set(kActionKey, intent->action); |
| |
| if (intent->url.has_value()) { |
| DCHECK(intent->url.value().is_valid()); |
| intent_value.Set(kUrlKey, intent->url.value().spec()); |
| } |
| |
| if (intent->mime_type.has_value() && !intent->mime_type.value().empty()) |
| intent_value.Set(kMimeTypeKey, intent->mime_type.value()); |
| |
| if (!intent->files.empty()) { |
| base::Value::List file_urls_list; |
| for (const auto& file : intent->files) { |
| DCHECK(file->url.is_valid()); |
| file_urls_list.Append(base::Value(file->url.spec())); |
| } |
| intent_value.Set(kFileUrlsKey, std::move(file_urls_list)); |
| } |
| |
| if (intent->activity_name.has_value() && |
| !intent->activity_name.value().empty()) { |
| intent_value.Set(kActivityNameKey, intent->activity_name.value()); |
| } |
| |
| if (intent->drive_share_url.has_value()) { |
| DCHECK(intent->drive_share_url.value().is_valid()); |
| intent_value.Set(kDriveShareUrlKey, intent->drive_share_url.value().spec()); |
| } |
| |
| if (intent->share_text.has_value() && !intent->share_text.value().empty()) |
| intent_value.Set(kShareTextKey, intent->share_text.value()); |
| |
| if (intent->share_title.has_value() && !intent->share_title.value().empty()) |
| intent_value.Set(kShareTitleKey, intent->share_title.value()); |
| |
| if (intent->start_type.has_value() && !intent->start_type.value().empty()) |
| intent_value.Set(kStartTypeKey, intent->start_type.value()); |
| |
| if (!intent->categories.empty()) { |
| base::Value::List categories; |
| for (const auto& category : intent->categories) { |
| categories.Append(category); |
| } |
| intent_value.Set(kCategoriesKey, std::move(categories)); |
| } |
| |
| if (intent->data.has_value() && !intent->data.value().empty()) |
| intent_value.Set(kDataKey, intent->data.value()); |
| |
| if (intent->ui_bypassed.has_value()) { |
| intent_value.Set(kUiBypassedKey, intent->ui_bypassed.value()); |
| } |
| |
| if (!intent->extras.empty()) { |
| base::Value::Dict extras; |
| for (const auto& extra : intent->extras) { |
| extras.Set(extra.first, extra.second); |
| } |
| intent_value.Set(kExtrasKey, std::move(extras)); |
| } |
| |
| return base::Value(std::move(intent_value)); |
| } |
| |
| std::optional<std::string> GetStringValueFromDict(const base::Value::Dict& dict, |
| const std::string& key_name) { |
| const base::Value* value = dict.Find(key_name); |
| if (!value) |
| return std::nullopt; |
| |
| const std::string* string_value = value->GetIfString(); |
| if (!string_value || string_value->empty()) |
| return std::nullopt; |
| |
| return *string_value; |
| } |
| |
| std::optional<bool> GetBoolValueFromDict(const base::Value::Dict& dict, |
| const std::string& key_name) { |
| return dict.FindBool(key_name); |
| } |
| |
| std::optional<GURL> GetGurlValueFromDict(const base::Value::Dict& dict, |
| const std::string& key_name) { |
| const std::string* url_spec = dict.FindString(key_name); |
| if (!url_spec) |
| return std::nullopt; |
| |
| GURL url(*url_spec); |
| if (!url.is_valid()) |
| return std::nullopt; |
| |
| return url; |
| } |
| |
| std::vector<apps::IntentFilePtr> GetFilesFromDict(const base::Value::Dict& dict, |
| const std::string& key_name) { |
| const base::Value::List* value = dict.FindList(key_name); |
| if (!value || value->empty()) |
| return std::vector<apps::IntentFilePtr>(); |
| |
| std::vector<apps::IntentFilePtr> files; |
| for (const auto& item : *value) { |
| GURL url(item.GetString()); |
| if (url.is_valid()) { |
| files.push_back(std::make_unique<apps::IntentFile>(url)); |
| } |
| } |
| return files; |
| } |
| |
| std::vector<std::string> GetCategoriesFromDict(const base::Value::Dict& dict, |
| const std::string& key_name) { |
| const base::Value::List* value = dict.FindList(key_name); |
| if (!value || value->empty()) |
| return std::vector<std::string>(); |
| |
| std::vector<std::string> categories; |
| for (const auto& item : *value) |
| categories.push_back(item.GetString()); |
| |
| return categories; |
| } |
| |
| base::flat_map<std::string, std::string> GetExtrasFromDict( |
| const base::Value::Dict& dict, |
| const std::string& key_name) { |
| const base::Value::Dict* value = dict.FindDict(key_name); |
| if (!value) |
| return base::flat_map<std::string, std::string>(); |
| |
| base::flat_map<std::string, std::string> extras; |
| for (auto pair : *value) { |
| if (pair.second.is_string()) |
| extras[pair.first] = pair.second.GetString(); |
| } |
| |
| return extras; |
| } |
| |
| apps::IntentPtr ConvertValueToIntent(base::Value&& value) { |
| base::Value::Dict* dict = value.GetIfDict(); |
| if (!dict) |
| return nullptr; |
| |
| return ConvertDictToIntent(*dict); |
| } |
| |
| apps::IntentPtr ConvertDictToIntent(const base::Value::Dict& dict) { |
| auto action = GetStringValueFromDict(dict, kActionKey); |
| if (!action.has_value()) |
| return nullptr; |
| auto intent = std::make_unique<apps::Intent>(action.value()); |
| intent->url = GetGurlValueFromDict(dict, kUrlKey); |
| intent->mime_type = GetStringValueFromDict(dict, kMimeTypeKey); |
| intent->files = GetFilesFromDict(dict, kFileUrlsKey); |
| intent->activity_name = GetStringValueFromDict(dict, kActivityNameKey); |
| intent->drive_share_url = GetGurlValueFromDict(dict, kDriveShareUrlKey); |
| intent->share_text = GetStringValueFromDict(dict, kShareTextKey); |
| intent->share_title = GetStringValueFromDict(dict, kShareTitleKey); |
| intent->start_type = GetStringValueFromDict(dict, kStartTypeKey); |
| intent->categories = GetCategoriesFromDict(dict, kCategoriesKey); |
| intent->data = GetStringValueFromDict(dict, kDataKey); |
| intent->ui_bypassed = GetBoolValueFromDict(dict, kUiBypassedKey); |
| intent->extras = GetExtrasFromDict(dict, kExtrasKey); |
| |
| return intent; |
| } |
| |
| std::string CalculateCommonMimeType( |
| const std::vector<std::string>& mime_types) { |
| const std::string any_mime_type = std::string(kWildCardAny) + |
| std::string(kMimeTypeSeparator) + |
| std::string(kWildCardAny); |
| if (mime_types.size() == 0) { |
| return any_mime_type; |
| } |
| |
| std::vector<std::string> common_type = |
| base::SplitString(mime_types[0], kMimeTypeSeparator, |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| if (common_type.size() != 2) { |
| return any_mime_type; |
| } |
| |
| for (auto& mime_type : mime_types) { |
| std::vector<std::string> type = |
| base::SplitString(mime_type, kMimeTypeSeparator, base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (type.size() != kMimeTypeComponentSize) { |
| return any_mime_type; |
| } |
| if (common_type[0] != type[0]) { |
| return any_mime_type; |
| } |
| if (common_type[1] != type[1]) { |
| common_type[1] = kWildCardAny; |
| } |
| } |
| return common_type[0] + kMimeTypeSeparator + common_type[1]; |
| } |
| |
| SharedText ExtractSharedText(const std::string& share_text) { |
| SharedText shared_text; |
| std::string extracted_text = share_text; |
| GURL extracted_url; |
| size_t separator_pos = extracted_text.find_last_of(' '); |
| size_t newline_pos = extracted_text.find_last_of('\n'); |
| if (newline_pos != std::string::npos && |
| (separator_pos == std::string::npos || separator_pos < newline_pos)) { |
| separator_pos = newline_pos; |
| } |
| |
| if (separator_pos == std::string::npos) { |
| extracted_url = GURL(extracted_text); |
| if (extracted_url.is_valid()) |
| extracted_text.clear(); |
| } else { |
| extracted_url = GURL(extracted_text.substr(separator_pos + 1)); |
| if (extracted_url.is_valid()) |
| extracted_text.erase(separator_pos); |
| } |
| |
| if (!extracted_text.empty()) |
| shared_text.text = extracted_text; |
| |
| if (extracted_url.is_valid()) |
| shared_text.url = extracted_url; |
| |
| return shared_text; |
| } |
| |
| // static |
| std::optional<std::string> AuthorityView::PortToString(const GURL& url) { |
| int port_number = url.EffectiveIntPort(); |
| if (port_number == url::PORT_UNSPECIFIED) { |
| return std::nullopt; |
| } |
| return base::ToString(port_number); |
| } |
| |
| // static |
| std::optional<std::string> AuthorityView::PortToString( |
| const url::Origin& origin) { |
| if (origin.port() == 0) { |
| return std::nullopt; |
| } |
| return base::ToString(origin.port()); |
| } |
| |
| // static |
| AuthorityView AuthorityView::Decode(std::string_view encoded_string) { |
| size_t i = encoded_string.find_last_of(kAuthorityHostPortSeparator); |
| if (i == std::string_view::npos) { |
| return {.host = encoded_string}; |
| } |
| return {.host = encoded_string.substr(0, i), |
| .port = encoded_string.substr(i + 1)}; |
| } |
| |
| // static |
| std::string AuthorityView::Encode(const GURL& url) { |
| CHECK(url.is_valid()); |
| return AuthorityView{.host = url.host(), .port = PortToString(url)}.Encode(); |
| } |
| |
| // static |
| std::string AuthorityView::Encode(const url::Origin& origin) { |
| CHECK(!origin.opaque()); |
| return AuthorityView{.host = origin.host(), .port = PortToString(origin)} |
| .Encode(); |
| } |
| |
| std::string AuthorityView::Encode() { |
| if (!port) { |
| return std::string(host); |
| } |
| return base::StrCat({host, kAuthorityHostPortSeparator, *port}); |
| } |
| |
| } // namespace apps_util |