// 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 <memory>
#include <optional>
#include <string>
#include <vector>
#include "ash/constants/ash_features.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/escape.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_base.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/app_service_test.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/crostini/crostini_test_helper.h"
#include "chrome/browser/ash/crostini/fake_crostini_features.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/file_manager_test_util.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager_factory.h"
#include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_rules_manager.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_test_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user_type.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/entry_info.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
namespace {
const char kAppIdText[] = "abcdefg";
const char kAppIdImage[] = "gfedcba";
const char kAppIdAny[] = "hijklmn";
const char kChromeAppId[] = "chromeappid";
const char kChromeAppWithVerbsId[] = "chromeappwithverbsid";
const char kExtensionId[] = "extensionid";
const char kAppIdTextWild[] = "zxcvbn";
const char kMimeTypeText[] = "text/plain";
const char kMimeTypeImage[] = "image/jpeg";
const char kMimeTypeHtml[] = "text/html";
const char kMimeTypeAny[] = "*/*";
const char kMimeTypeTextWild[] = "text/*";
const char kFileExtensionText[] = "txt";
const char kFileExtensionImage[] = "jpeg";
const char kFileExtensionAny[] = "fake";
const char kActivityLabelText[] = "some_text_activity";
const char kActivityLabelImage[] = "some_image_activity";
const char kActivityLabelAny[] = "some_any_file";
const char kActivityLabelTextWild[] = "some_text_wild_file";
} // namespace
namespace file_manager {
namespace file_tasks {
using test::AddFakeAppWithIntentFilters;
using test::AddFakeWebApp;
class AppServiceFileTasksTest : public testing::Test {
protected:
AppServiceFileTasksTest() = default;
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
app_service_test_.SetUp(profile_.get());
app_service_proxy_ =
apps::AppServiceProxyFactory::GetForProfile(profile_.get());
ASSERT_TRUE(app_service_proxy_);
storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
util::GetDownloadsMountPointName(profile_.get()),
storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
util::GetMyFilesFolderForProfile(profile_.get()));
}
Profile* profile() { return profile_.get(); }
struct FakeFile {
std::string file_name;
std::string mime_type;
bool is_directory = false;
GURL file_url;
};
GURL test_url(const std::string& file_name) {
GURL url =
GURL("filesystem:chrome-extension://id/external/" +
base::EscapeUrlEncodedData(
util::GetDownloadsMountPointName(profile()) + "/" + file_name,
/*use_plus=*/false));
EXPECT_TRUE(url.is_valid());
return url;
}
std::vector<FullTaskDescriptor> FindAppServiceTasks(
const std::vector<FakeFile>& files) {
auto resulting_tasks = FindAppServiceTasksImpl(files);
return resulting_tasks->tasks;
}
std::unique_ptr<ResultingTasks> FindAppServiceTasksImpl(
const std::vector<FakeFile>& files) {
std::vector<extensions::EntryInfo> entries;
std::vector<GURL> file_urls;
std::vector<std::string> dlp_source_urls;
for (const FakeFile& fake_file : files) {
entries.emplace_back(
util::GetMyFilesFolderForProfile(profile()).AppendASCII(
fake_file.file_name),
fake_file.mime_type, fake_file.is_directory);
if (fake_file.file_url.is_empty()) {
file_urls.push_back(test_url(fake_file.file_name));
} else {
file_urls.push_back(fake_file.file_url);
}
dlp_source_urls.push_back("");
}
auto resulting_tasks = std::make_unique<ResultingTasks>();
file_tasks::FindAppServiceTasks(profile(), entries, file_urls,
dlp_source_urls, &resulting_tasks->tasks);
// Sort by app ID so we don't rely on ordering.
base::ranges::sort(
resulting_tasks->tasks, base::ranges::less(),
[](const auto& task) { return task.task_descriptor.app_id; });
return resulting_tasks;
}
std::unique_ptr<ResultingTasks> FindAppServiceTasksWithPolicy(
const std::vector<FakeFile>& files) {
auto resulting_tasks = FindAppServiceTasksImpl(files);
ChooseAndSetDefaultTaskFromPolicyPrefs(
profile(), ConvertFakeFilesToEntryInfos(files), resulting_tasks.get());
return resulting_tasks;
}
void AddTextApp() {
AddFakeWebApp(kAppIdText, kMimeTypeText, kFileExtensionText,
kActivityLabelText, true, app_service_proxy_);
}
void AddImageApp() {
AddFakeWebApp(kAppIdImage, kMimeTypeImage, kFileExtensionImage,
kActivityLabelImage, true, app_service_proxy_);
}
void AddTextWildApp() {
AddFakeWebApp(kAppIdTextWild, kMimeTypeTextWild, kFileExtensionAny,
kActivityLabelTextWild, true, app_service_proxy_);
}
void AddAnyApp() {
AddFakeWebApp(kAppIdAny, kMimeTypeAny, kFileExtensionAny, kActivityLabelAny,
true, app_service_proxy_);
}
// Provides file handlers for all extensions and images.
void AddChromeApp() {
extensions::ExtensionBuilder baz_app;
baz_app.SetManifest(
base::Value::Dict()
.Set("name", "Baz")
.Set("version", "1.0.0")
.Set("manifest_version", 2)
.Set("app", base::Value::Dict().Set(
"background",
base::Value::Dict().Set(
"scripts",
base::Value::List().Append("background.js"))))
.Set("file_handlers",
base::Value::Dict()
.Set("any",
base::Value::Dict().Set(
"extensions",
base::Value::List().Append("*").Append("bar")))
.Set("image", base::Value::Dict().Set(
"types", base::Value::List().Append(
"image/*")))));
baz_app.SetID(kChromeAppId);
auto filters =
apps_util::CreateIntentFiltersForChromeApp(baz_app.Build().get());
AddFakeAppWithIntentFilters(kChromeAppId, std::move(filters),
apps::AppType::kChromeApp, true,
app_service_proxy_);
}
void AddChromeAppWithVerbs() {
extensions::ExtensionBuilder foo_app;
foo_app.SetManifest(
base::Value::Dict()
.Set("name", "Foo")
.Set("version", "1.0.0")
.Set("manifest_version", 2)
.Set("app", base::Value::Dict().Set(
"background",
base::Value::Dict().Set(
"scripts",
base::Value::List().Append("background.js"))))
.Set("file_handlers",
base::Value::Dict()
.Set("any_with_directories",
base::Value::Dict()
.Set("include_directories", true)
.Set("types", base::Value::List().Append("*"))
.Set("verb", "open_with"))
.Set("html_handler",
base::Value::Dict()
.Set("title", "Html")
.Set("types",
base::Value::List().Append("text/html"))
.Set("verb", "open_with"))
.Set("plain_text",
base::Value::Dict()
.Set("title", "Plain")
.Set("types",
base::Value::List().Append("text/plain")))
.Set("share_plain_text",
base::Value::Dict()
.Set("title", "Share Plain")
.Set("types",
base::Value::List().Append("text/plain"))
.Set("verb", "share_with"))
.Set("any_pack",
base::Value::Dict()
.Set("types", base::Value::List().Append("*"))
.Set("verb", "pack_with"))
.Set("plain_text_add_to",
base::Value::Dict()
.Set("title", "Plain")
.Set("types",
base::Value::List().Append("text/plain"))
.Set("verb", "add_to"))));
foo_app.SetID(kChromeAppWithVerbsId);
auto filters =
apps_util::CreateIntentFiltersForChromeApp(foo_app.Build().get());
AddFakeAppWithIntentFilters(kChromeAppWithVerbsId, std::move(filters),
apps::AppType::kChromeApp, true,
app_service_proxy_);
}
// Adds file_browser_handler to handle .txt files.
void AddExtension() {
extensions::ExtensionBuilder fbh_app;
fbh_app.SetManifest(
base::Value::Dict()
.Set("name", "Fbh")
.Set("version", "1.0.0")
.Set("manifest_version", 2)
.Set("permissions",
base::Value::List().Append("fileBrowserHandler"))
.Set("file_browser_handlers",
base::Value::List().Append(
base::Value::Dict()
.Set("id", "open")
.Set("default_title", "open title")
.Set("file_filters", base::Value::List().Append(
"filesystem:*.txt")))));
fbh_app.SetID(kExtensionId);
auto filters =
apps_util::CreateIntentFiltersForExtension(fbh_app.Build().get());
AddFakeAppWithIntentFilters(kExtensionId, std::move(filters),
apps::AppType::kChromeApp, true,
app_service_proxy_);
}
// Load an extension from the supplied manifest, then add intent filters.
void LoadExtension(const std::string manifest) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("file handlers").AddJSON(manifest).Build();
auto filters = apps_util::CreateIntentFiltersForExtension(extension.get());
AddFakeAppWithIntentFilters(kExtensionId, std::move(filters),
apps::AppType::kExtension,
/*handles_intents=*/true, app_service_proxy_);
}
apps::IntentFilterPtr CreateMimeTypeFileIntentFilter(std::string action,
std::string mime_type) {
auto intent_filter = std::make_unique<apps::IntentFilter>();
intent_filter->AddSingleValueCondition(apps::ConditionType::kAction, action,
apps::PatternMatchType::kLiteral);
intent_filter->AddSingleValueCondition(apps::ConditionType::kFile,
mime_type,
apps::PatternMatchType::kMimeType);
return intent_filter;
}
apps::IntentFilterPtr CreateExtensionTypeFileIntentFilter(
std::string action,
std::string extension_type) {
auto intent_filter = std::make_unique<apps::IntentFilter>();
intent_filter->AddSingleValueCondition(apps::ConditionType::kAction, action,
apps::PatternMatchType::kLiteral);
intent_filter->AddSingleValueCondition(
apps::ConditionType::kFile, extension_type,
apps::PatternMatchType::kFileExtension);
return intent_filter;
}
std::string AddArcAppWithIntentFilter(const std::string& package,
const std::string& activity,
apps::IntentFilterPtr intent_filter) {
std::string app_id = ArcAppListPrefs::GetAppId(package, activity);
std::vector<apps::IntentFilterPtr> filters;
filters.push_back(std::move(intent_filter));
AddFakeAppWithIntentFilters(app_id, std::move(filters), apps::AppType::kArc,
true, app_service_proxy_);
return app_id;
}
void AddGuestOsAppWithIntentFilter(std::string app_id,
apps::AppType app_type,
apps::IntentFilterPtr intent_filter) {
std::vector<apps::IntentFilterPtr> filters;
filters.push_back(std::move(intent_filter));
AddFakeAppWithIntentFilters(app_id, std::move(filters), app_type, true,
app_service_proxy_);
}
std::vector<extensions::EntryInfo> ConvertFakeFilesToEntryInfos(
const std::vector<FakeFile>& files) {
std::vector<extensions::EntryInfo> entries;
for (const FakeFile& fake_file : files) {
entries.emplace_back(
util::GetMyFilesFolderForProfile(profile()).AppendASCII(
fake_file.file_name),
fake_file.mime_type, fake_file.is_directory);
}
return entries;
}
base::test::ScopedFeatureList feature_list_;
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
raw_ptr<apps::AppServiceProxy> app_service_proxy_ = nullptr;
apps::AppServiceTest app_service_test_;
};
// An app which does not handle intents should not be found even if the filters
// match.
TEST_F(AppServiceFileTasksTest, FindAppServiceFileTasksHandlesIntent) {
AddFakeWebApp(kAppIdImage, kMimeTypeImage, kFileExtensionImage,
kActivityLabelImage, false, app_service_proxy_);
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.jpeg", kMimeTypeImage}});
ASSERT_EQ(0U, tasks.size());
}
// Test that between an image app and text app, the text app can be
// found for an text file entry.
TEST_F(AppServiceFileTasksTest, FindAppServiceFileTasksText) {
AddTextApp();
AddImageApp();
// Find apps for a "text/plain" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kAppIdText, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasks[0].task_title);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
}
// Test that between an image app and text app, the image app can be
// found for an image file entry.
TEST_F(AppServiceFileTasksTest, FindAppServiceFileTasksImage) {
AddTextApp();
AddImageApp();
// Find apps for a "image/jpeg" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"bar.jpeg", kMimeTypeImage}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kAppIdImage, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelImage, tasks[0].task_title);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
}
// Test that between an image app, text app and an app that can handle every
// file, the app that can handle every file can be found for an image file entry
// and text file entry.
TEST_F(AppServiceFileTasksTest, FindAppServiceFileTasksMultiple) {
AddTextApp();
AddImageApp();
AddAnyApp();
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks(
{{"foo.txt", kMimeTypeText}, {"bar.jpeg", kMimeTypeImage}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kAppIdAny, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelAny, tasks[0].task_title);
EXPECT_TRUE(tasks[0].is_generic_file_handler);
}
// Don't register any apps and check that we get no matches.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksNoTasks) {
// Find web apps for a "text/plain" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(0U, tasks.size());
}
// Register a text handler and check we get no matches with an image.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksNoMatchingTask) {
AddTextApp();
// Find apps for a "image/jpeg" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"bar.jpeg", kMimeTypeImage}});
ASSERT_EQ(0U, tasks.size());
}
// Check we get a match for a text file + web app.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksText) {
AddTextApp();
// Find web apps for a "text/plain" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kAppIdText, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasks[0].task_title);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
}
// Check that a web app that only handles text does not match when we have both
// a text file and an image.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksTwoFilesNoMatch) {
AddTextApp();
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks(
{{"foo.txt", kMimeTypeText}, {"bar.jpeg", kMimeTypeImage}});
ASSERT_EQ(0U, tasks.size());
}
// Check we get a match for a text file + text wildcard filter.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksTextWild) {
AddTextWildApp();
AddTextApp();
// Find web apps for a "text/plain" file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(2U, tasks.size());
EXPECT_EQ(kAppIdText, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasks[0].task_title);
EXPECT_EQ(kAppIdTextWild, tasks[1].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelTextWild, tasks[1].task_title);
}
// Check we get a match for a text file and HTML file + text wildcard filter.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksTextWildMultiple) {
AddTextWildApp();
AddTextApp(); // Should not be matched.
AddImageApp(); // Should not be matched.
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks(
{{"foo.txt", kMimeTypeText}, {"bar.html", kMimeTypeHtml}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kAppIdTextWild, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelTextWild, tasks[0].task_title);
}
// An edge case where we have one file that matches the mime type but not the
// file extension, and another file that matches the file extension but not the
// mime type. This should still match the handler.
TEST_F(AppServiceFileTasksTest, FindAppServiceWebFileTasksAllFilesMatchEither) {
AddTextApp();
// First check that each file alone matches the text app.
std::vector<FullTaskDescriptor> tasksFoo =
FindAppServiceTasks({{"foo.txt", "text/plane"}});
ASSERT_EQ(1U, tasksFoo.size());
std::vector<FullTaskDescriptor> tasksBar =
FindAppServiceTasks({{"bar.text", kMimeTypeText}});
ASSERT_EQ(1U, tasksFoo.size());
// Now check that both together match.
std::vector<FullTaskDescriptor> tasksBoth = FindAppServiceTasks(
{{"foo.txt", "text/plane"}, {"bar.text", kMimeTypeText}});
ASSERT_EQ(1U, tasksBoth.size());
EXPECT_EQ(kAppIdText, tasksBoth[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasksBoth[0].task_title);
}
// Check that Baz's ".*" handler, which is generic, is matched.
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppText) {
AddChromeApp();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kChromeAppId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("any", tasks[0].task_descriptor.action_id);
EXPECT_EQ(TASK_TYPE_FILE_HANDLER, tasks[0].task_descriptor.task_type);
EXPECT_EQ("Baz", tasks[0].task_title);
EXPECT_TRUE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
}
// File extension matches with bar, but there is a generic * type as well,
// so the overall match should still be generic.
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppBar) {
AddChromeApp();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.bar", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kChromeAppId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("any", tasks[0].task_descriptor.action_id);
EXPECT_EQ(TASK_TYPE_FILE_HANDLER, tasks[0].task_descriptor.task_type);
EXPECT_EQ("Baz", tasks[0].task_title);
EXPECT_TRUE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
}
// Check that we can get web apps and Chrome apps in the same call.
TEST_F(AppServiceFileTasksTest, FindAppServiceMultiAppType) {
AddTextApp();
AddChromeApp();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(2U, tasks.size());
EXPECT_EQ(kAppIdText, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasks[0].task_title);
EXPECT_EQ(TASK_TYPE_WEB_APP, tasks[0].task_descriptor.task_type);
EXPECT_EQ(kChromeAppId, tasks[1].task_descriptor.app_id);
EXPECT_EQ("Baz", tasks[1].task_title);
EXPECT_EQ(TASK_TYPE_FILE_HANDLER, tasks[1].task_descriptor.task_type);
}
// Check that Baz's "image/*" handler is picked because it is not generic,
// because it matches the mime type directly, even though there is an earlier
// generic handler.
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppImage) {
AddChromeApp();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"bar.jpeg", kMimeTypeImage}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kChromeAppId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("image", tasks[0].task_descriptor.action_id);
EXPECT_EQ(TASK_TYPE_FILE_HANDLER, tasks[0].task_descriptor.task_type);
EXPECT_EQ("Baz", tasks[0].task_title);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppWithVerbs) {
AddChromeAppWithVerbs();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
// We expect that all non-"open_with" handlers are ignored, and that we
// only get one open_with handler.
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kChromeAppWithVerbsId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("Foo", tasks[0].task_title);
EXPECT_EQ("plain_text", tasks[0].task_descriptor.action_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppWithVerbs_Html) {
AddChromeAppWithVerbs();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.html", kMimeTypeHtml}});
// Check that we get the non-generic handler which appears later in the
// manifest.
EXPECT_EQ(kChromeAppWithVerbsId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("Foo", tasks[0].task_title);
EXPECT_EQ("html_handler", tasks[0].task_descriptor.action_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceChromeAppWithVerbs_Directory) {
AddChromeAppWithVerbs();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"dir", "", true}});
// Only one handler handles directories.
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kChromeAppWithVerbsId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("Foo", tasks[0].task_title);
EXPECT_EQ("any_with_directories", tasks[0].task_descriptor.action_id);
EXPECT_TRUE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceExtension) {
AddExtension();
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kExtensionId, tasks[0].task_descriptor.app_id);
EXPECT_EQ("open title", tasks[0].task_title);
EXPECT_EQ("open", tasks[0].task_descriptor.action_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceArcAppWithExtensionMatching) {
// Create an app with a text file filter.
std::string package_name = "com.example.xyzViewer";
std::string activity = "xyzViewerActivity";
std::string app_id = AddArcAppWithIntentFilter(
package_name, activity,
CreateExtensionTypeFileIntentFilter(apps_util::kIntentActionView, "xyz"));
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks({{"foo.xyz"}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(app_id, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
}
// Enable MV3 File Handlers.
class AppServiceFileHandlersTest : public AppServiceFileTasksTest {
public:
AppServiceFileHandlersTest() {
feature_list_.InitAndEnableFeature(
extensions_features::kExtensionWebFileHandlers);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Verify App Service tasks for extensions with MV3 File Handlers.
TEST_F(AppServiceFileHandlersTest, FindAppServiceExtension) {
static constexpr char kAction[] = "/open.html";
const std::string manifest = base::StringPrintf(R"(
"version": "0.0.1",
"manifest_version": 3,
"file_handlers": [
{
"name": "Text file",
"action": "%s",
"accept": {"text/plain": ".txt"}
}
]
)",
kAction);
LoadExtension(manifest);
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"test.txt", kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(kExtensionId, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kAction, tasks[0].task_descriptor.action_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceArcApp) {
std::string text_mime_type = "text/plain";
std::string image_mime_type = "image/jpeg";
// Create an app with a text file filter.
std::string text_package_name = "com.example.textViewer";
std::string text_activity = "TextViewerActivity";
std::string text_app_id = AddArcAppWithIntentFilter(
text_package_name, text_activity,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
text_mime_type));
// Create an app with an image file filter.
std::string image_package_name = "com.example.imageViewer";
std::string image_activity = "ImageViewerActivity";
std::string image_app_id = AddArcAppWithIntentFilter(
image_package_name, image_activity,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
image_mime_type));
// Check if only the text ARC app appears as a result.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", text_mime_type}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(text_app_id, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindAppServiceCrostiniApp) {
std::string file_name = "foo.txt";
std::string text_app_id = "Text app";
AddGuestOsAppWithIntentFilter(
text_app_id, apps::AppType::kCrostini,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
kMimeTypeText));
// Check if the text Crostini app is returned.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{file_name, kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(text_app_id, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
}
// Checks that we can detect when the file paths can/ can't be shared for
// Crostini and PluginVm.
TEST_F(AppServiceFileTasksTest, CheckPathsCanBeShared) {
std::string file_name = "foo.txt";
std::string text_app_id = "Text app";
AddGuestOsAppWithIntentFilter(
text_app_id, apps::AppType::kCrostini,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
kMimeTypeText));
// Possible to share path.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{file_name, kMimeTypeText}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(text_app_id, tasks[0].task_descriptor.app_id);
// Should not be possible to share path.
GURL invalid_url = GURL("broken:url");
tasks =
FindAppServiceTasks({{file_name, kMimeTypeText, /*is_directory=*/false,
/*file_url=*/invalid_url}});
ASSERT_EQ(0U, tasks.size());
}
TEST_F(AppServiceFileTasksTest, FindMultipleAppServiceCrostiniApps) {
std::string file_name = "foo.txt";
std::string app_id_1 = "Text app 1";
std::string app_id_2 = "Text app 2";
AddGuestOsAppWithIntentFilter(
app_id_1, apps::AppType::kCrostini,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
kMimeTypeText));
AddGuestOsAppWithIntentFilter(
app_id_2, apps::AppType::kCrostini,
CreateMimeTypeFileIntentFilter(apps_util::kIntentActionView,
kMimeTypeText));
// Check if both Crostini apps are returned.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{file_name, kMimeTypeText}});
ASSERT_EQ(2U, tasks.size());
EXPECT_EQ(app_id_1, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_FALSE(tasks[0].is_file_extension_match);
EXPECT_EQ(app_id_2, tasks[1].task_descriptor.app_id);
EXPECT_FALSE(tasks[1].is_generic_file_handler);
EXPECT_FALSE(tasks[1].is_file_extension_match);
}
// When we encounter a file with an unknown mime-type (i.e.
// application/octet-stream), we rely on matching with the extension type. Check
// whether extension matching works for Crostini.
TEST_F(AppServiceFileTasksTest, FindAppServiceCrostiniAppWithExtension) {
std::string extension = "randomExtension";
std::string mime_type = "test/randomMimeType";
std::string file_name = "foo." + extension;
std::string app_id = "App";
auto intent_filter =
apps_util::CreateFileFilter({apps_util::kIntentActionView}, {mime_type},
{extension}, "open-with", false);
AddGuestOsAppWithIntentFilter(app_id, apps::AppType::kCrostini,
std::move(intent_filter));
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{file_name, "application/octet-stream"}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(app_id, tasks[0].task_descriptor.app_id);
}
TEST_F(AppServiceFileTasksTest, FindAppServicePluginVmApp) {
std::string file_ext = "txt";
std::string file_name = "foo." + file_ext;
std::string text_app_id = "Text app";
AddGuestOsAppWithIntentFilter(text_app_id, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(
apps_util::kIntentActionView, file_ext));
// Check if the text PluginVm app is returned.
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(text_app_id, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, FindMultipleAppServicePluginVmApps) {
std::string file_ext = "txt";
std::string file_name = "foo." + file_ext;
std::string app_id_1 = "Text app 1";
std::string app_id_2 = "Text app 2";
AddGuestOsAppWithIntentFilter(app_id_1, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(
apps_util::kIntentActionView, file_ext));
AddGuestOsAppWithIntentFilter(app_id_2, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(
apps_util::kIntentActionView, file_ext));
// Check if both PluginVm apps are returned.
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(2U, tasks.size());
EXPECT_EQ(app_id_1, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
EXPECT_EQ(app_id_2, tasks[1].task_descriptor.app_id);
EXPECT_FALSE(tasks[1].is_generic_file_handler);
EXPECT_TRUE(tasks[1].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest,
FindAppServicePluginVmApp_IgnoringExtensionCase) {
std::string file_ext = "Txt";
std::string file_name = "foo.txT";
std::string text_app_id = "Text app";
AddGuestOsAppWithIntentFilter(text_app_id, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(
apps_util::kIntentActionView, file_ext));
// Check if the text PluginVm app is returned.
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(text_app_id, tasks[0].task_descriptor.app_id);
EXPECT_FALSE(tasks[0].is_generic_file_handler);
EXPECT_TRUE(tasks[0].is_file_extension_match);
}
TEST_F(AppServiceFileTasksTest, NoPluginVmAppsForFileSelection) {
std::string image_file_name = "foo.jpeg";
std::string image_app_id = "Image app";
std::string text_file_name = "foo.txt";
std::string text_app_id = "Text app";
// Add a text-only app and an image-only app.
AddGuestOsAppWithIntentFilter(
text_app_id, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(apps_util::kIntentActionView, "txt"));
AddGuestOsAppWithIntentFilter(image_app_id, apps::AppType::kPluginVm,
CreateExtensionTypeFileIntentFilter(
apps_util::kIntentActionView, "jpeg"));
// Find an app that can open both the text and image file.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{text_file_name}, {image_file_name}});
// There shouldn't be any apps available.
ASSERT_EQ(0U, tasks.size());
}
TEST_F(AppServiceFileTasksTest, CrositiniTasksControlledByPolicy) {
std::string tini_task_name = "chrome://file-manager/?import-crostini-image";
std::string deb_task_name = "chrome://file-manager/?install-linux-package";
std::vector<apps::IntentFilterPtr> filters;
filters.push_back(
apps_util::MakeFileFilterForView("tini", "tini", "import-tini"));
filters[0]->activity_name = tini_task_name;
filters.push_back(
apps_util::MakeFileFilterForView("deb", "deb", "import-deb"));
filters[1]->activity_name = deb_task_name;
AddFakeAppWithIntentFilters(file_manager::kFileManagerSwaAppId,
std::move(filters), apps::AppType::kWeb, true,
app_service_proxy_);
std::string file_name = "test.tini";
crostini::FakeCrostiniFeatures crostini_features;
crostini_features.set_export_import_ui_allowed(true);
std::vector<FullTaskDescriptor> tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(file_manager::kFileManagerSwaAppId,
tasks[0].task_descriptor.app_id);
EXPECT_EQ(tini_task_name, tasks[0].task_descriptor.action_id);
crostini_features.set_export_import_ui_allowed(false);
tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(0U, tasks.size());
file_name = "test.deb";
crostini_features.set_root_access_allowed(true);
tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(1U, tasks.size());
EXPECT_EQ(file_manager::kFileManagerSwaAppId,
tasks[0].task_descriptor.app_id);
EXPECT_EQ(deb_task_name, tasks[0].task_descriptor.action_id);
crostini_features.set_root_access_allowed(false);
tasks = FindAppServiceTasks({{file_name}});
ASSERT_EQ(0U, tasks.size());
}
// Tests applying policies when listing tasks.
class AppServiceFileTasksPolicyTest : public AppServiceFileTasksTest {
protected:
class MockFilesController : public policy::DlpFilesControllerAsh {
public:
explicit MockFilesController(const policy::DlpRulesManager& rules_manager,
Profile* profile)
: DlpFilesControllerAsh(rules_manager, profile) {}
~MockFilesController() override = default;
MOCK_METHOD(bool,
IsLaunchBlocked,
(const apps::AppUpdate&, const apps::IntentPtr&),
(override));
};
AppServiceFileTasksPolicyTest() = default;
std::unique_ptr<KeyedService> SetDlpRulesManager(
content::BrowserContext* context) {
auto dlp_rules_manager =
std::make_unique<testing::NiceMock<policy::MockDlpRulesManager>>(
Profile::FromBrowserContext(context));
rules_manager_ = dlp_rules_manager.get();
return dlp_rules_manager;
}
void SetUp() override {
AppServiceFileTasksTest::SetUp();
AccountId account_id =
AccountId::FromUserEmailGaiaId("[email protected]", "12345");
profile_->SetIsNewProfile(true);
user_manager::User* user =
fake_user_manager_->AddUserWithAffiliationAndTypeAndProfile(
account_id, /*is_affiliated=*/false,
user_manager::UserType::kRegular, profile_.get());
fake_user_manager_->UserLoggedIn(account_id, user->username_hash(),
/*browser_restart=*/false,
/*is_child=*/false);
fake_user_manager_->SimulateUserProfileLoad(account_id);
policy::DlpRulesManagerFactory::GetInstance()->SetTestingFactory(
profile_.get(),
base::BindRepeating(&AppServiceFileTasksPolicyTest::SetDlpRulesManager,
base::Unretained(this)));
ASSERT_TRUE(policy::DlpRulesManagerFactory::GetForPrimaryProfile());
ON_CALL(*rules_manager_, IsFilesPolicyEnabled)
.WillByDefault(testing::Return(true));
mock_files_controller_ =
std::make_unique<MockFilesController>(*rules_manager_, profile_.get());
ON_CALL(*rules_manager_, GetDlpFilesController)
.WillByDefault(testing::Return(mock_files_controller_.get()));
}
void TearDown() override { fake_user_manager_.Reset(); }
raw_ptr<policy::MockDlpRulesManager> rules_manager_ = nullptr;
std::unique_ptr<MockFilesController> mock_files_controller_ = nullptr;
user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
fake_user_manager_{std::make_unique<ash::FakeChromeUserManager>()};
};
// Test that out of two apps, one can be blocked by DLP and the other allowed.
TEST_F(AppServiceFileTasksPolicyTest, FindAppServiceFileTasksText_DlpChecked) {
EXPECT_CALL(*mock_files_controller_.get(), IsLaunchBlocked)
.WillOnce(testing::Return(false))
.WillOnce(testing::Return(true));
AddTextApp();
AddAnyApp();
// Find apps for a "text/plain" file. First app shouldn't be blocked, but the
// second one yes.
std::vector<FullTaskDescriptor> tasks =
FindAppServiceTasks({{"foo.txt", kMimeTypeText}});
ASSERT_EQ(2U, tasks.size());
EXPECT_EQ(kAppIdText, tasks[0].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelText, tasks[0].task_title);
EXPECT_FALSE(tasks[0].is_dlp_blocked);
EXPECT_EQ(kAppIdAny, tasks[1].task_descriptor.app_id);
EXPECT_EQ(kActivityLabelAny, tasks[1].task_title);
EXPECT_TRUE(tasks[1].is_dlp_blocked);
}
} // namespace file_tasks
} // namespace file_manager.