blob: 5f91daab27b4961a10650f6caa1ea40e56085721 [file] [log] [blame]
// Copyright 2018 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 "ash/constants/ash_features.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/apps/app_service/app_launch_params.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/fileapi_util.h"
#include "chrome/browser/ash/file_manager/filesystem_api_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/web_app_launch_manager.h"
#include "chrome/browser/web_applications/test/profile_test_helper.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/file_handlers/mime_util.h"
#include "extensions/browser/entry_info.h"
#include "extensions/common/constants.h"
#include "net/base/mime_util.h"
#include "storage/browser/file_system/file_system_url.h"
#include "third_party/blink/public/common/features.h"
using web_app::kMediaAppId;
namespace file_manager {
namespace file_tasks {
namespace {
// A list of file extensions (`/` delimited) representing a selection of files
// and the app expected to be the default to open these files.
// A null app_id indicates there is no preferred default.
// A mime_type can be set to a result normally given by sniffing when
// net::GetMimeTypeFromFile() would not provide a result.
struct Expectation {
const char* file_extensions;
const char* app_id;
const char* mime_type = nullptr;
};
// Verifies that a single default task expectation (i.e. the expected
// default app to open a given set of file extensions) matches the default
// task in a vector of task descriptors. Decrements the provided |remaining|
// integer to provide additional verification that this function is invoked
// an expected number of times (i.e. even if the callback could be invoked
// asynchronously).
void VerifyTasks(int* remaining,
Expectation expectation,
std::unique_ptr<std::vector<FullTaskDescriptor>> result) {
ASSERT_TRUE(result) << expectation.file_extensions;
--*remaining;
auto default_task =
std::find_if(result->begin(), result->end(),
[](const auto& task) { return task.is_default; });
// Early exit for the uncommon situation where no default should be set.
if (!expectation.app_id) {
EXPECT_TRUE(default_task == result->end()) << expectation.file_extensions;
return;
}
ASSERT_TRUE(default_task != result->end()) << expectation.file_extensions;
EXPECT_EQ(expectation.app_id, default_task->task_descriptor.app_id)
<< " for extension: " << expectation.file_extensions;
// Verify no other task is set as default.
EXPECT_EQ(1, std::count_if(result->begin(), result->end(),
[](const auto& task) { return task.is_default; }))
<< expectation.file_extensions;
}
// Helper to quit a run loop after invoking VerifyTasks().
void VerifyAsyncTask(int* remaining,
Expectation expectation,
base::OnceClosure quit_closure,
std::unique_ptr<std::vector<FullTaskDescriptor>> result) {
VerifyTasks(remaining, expectation, std::move(result));
std::move(quit_closure).Run();
}
// Installs a chrome app that handles .tiff.
scoped_refptr<const extensions::Extension> InstallTiffHandlerChromeApp(
Profile* profile) {
return test::InstallTestingChromeApp(
profile, "extensions/api_test/file_browser/app_file_handler");
}
class FileTasksBrowserTest : public TestProfileTypeMixin<InProcessBrowserTest> {
public:
void SetUpOnMainThread() override {
test::AddDefaultComponentExtensionsOnMainThread(browser()->profile());
ash::SystemWebAppManager::GetForTest(browser()->profile())
->InstallSystemAppsForTesting();
}
// Tests that each of the passed expectations open by default in the expected
// app.
void TestExpectationsAgainstDefaultTasks(
const std::vector<Expectation>& expectations) {
int remaining = expectations.size();
const base::FilePath prefix = base::FilePath().AppendASCII("file");
for (const Expectation& test : expectations) {
std::vector<extensions::EntryInfo> entries;
std::vector<GURL> file_urls;
std::vector<base::StringPiece> all_extensions =
base::SplitStringPiece(test.file_extensions, "/",
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
for (base::StringPiece extension : all_extensions) {
base::FilePath path = prefix.AddExtension(extension);
std::string mime_type;
net::GetMimeTypeFromFile(path, &mime_type);
if (test.mime_type != nullptr) {
// Sniffing isn't used when GetMimeTypeFromFile() succeeds, so there
// shouldn't be a hard-coded mime type configured.
EXPECT_TRUE(mime_type.empty())
<< "Did not expect mime match " << mime_type << " for " << path;
mime_type = test.mime_type;
} else {
EXPECT_FALSE(mime_type.empty()) << "No mime type for " << path;
}
entries.push_back({path, mime_type, false});
GURL url = GURL(base::JoinString(
{"filesystem:https://site.com/isolated/foo.", extension}, ""));
ASSERT_TRUE(url.is_valid());
file_urls.push_back(url);
}
// task_verifier callback is invoked synchronously from
// FindAllTypesOfTasks.
FindAllTypesOfTasks(browser()->profile(), entries, file_urls,
base::BindOnce(&VerifyTasks, &remaining, test));
}
EXPECT_EQ(0, remaining);
}
private:
base::test::ScopedFeatureList feature_list_{
blink::features::kFileHandlingAPI};
};
} // namespace
// Changes to the following tests may have implications for file handling
// declarations in built-in app manifests, because logic in
// ChooseAndSetDefaultTask() treats handlers for extensions with a higher
// priority than handlers for mime types. Provide MIME types here for extensions
// known to be missing mime types from net::GetMimeTypeFromFile() (see
// ExtensionToMimeMapping test). In practice, these MIME types are populated via
// file sniffing, but tests in this file do not operate on real files. We hard
// code MIME types that file sniffing obtained experimentally from sample files.
// Test file extensions correspond to mime types where expected.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExtensionToMimeMapping) {
constexpr struct {
const char* file_extension;
bool has_mime = true;
} kExpectations[] = {
// Images.
{"bmp"},
{"gif"},
{"ico"},
{"jpg"},
{"jpeg"},
{"png"},
{"webp"},
// Raw.
{"arw"},
{"cr2"},
{"dng"},
{"nef"},
{"nrw"},
{"orf"},
{"raf"},
{"rw2"},
// Video.
{"3gp"},
{"avi"},
{"m4v"},
{"mkv"},
{"mov"},
{"mp4"},
{"mpeg"},
{"mpeg4", false},
{"mpg"},
{"mpg4", false},
{"ogm"},
{"ogv"},
{"ogx"},
{"webm"},
// Audio.
{"amr"},
{"flac"},
{"m4a"},
{"mp3"},
{"oga"},
{"ogg"},
{"wav"},
};
const base::FilePath prefix = base::FilePath().AppendASCII("file");
std::string mime_type;
for (const auto& test : kExpectations) {
base::FilePath path = prefix.AddExtension(test.file_extension);
EXPECT_EQ(test.has_mime, net::GetMimeTypeFromFile(path, &mime_type))
<< test.file_extension;
}
}
// Tests the default handlers for various file types in ChromeOS. This test
// exists to ensure the default app that launches when you open a file in the
// ChromeOS file manager does not change unexpectedly. Multiple default apps are
// allowed to register a handler for the same file type. Without that, it is not
// possible for an app to open that type even when given explicit direction via
// the chrome.fileManagerPrivate.executeTask API. The current conflict
// resolution mechanism is "sort by extension ID", which has the desired result.
// If desires change, we'll need to update ChooseAndSetDefaultTask() with some
// additional logic.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ImageHandlerChangeDetector) {
std::vector<Expectation> expectations = {
// Images.
{"bmp", kMediaAppId},
{"gif", kMediaAppId},
{"ico", kMediaAppId},
{"jpg", kMediaAppId},
{"jpeg", kMediaAppId},
{"png", kMediaAppId},
{"webp", kMediaAppId},
// Raw (handled by MediaApp).
{"arw", kMediaAppId},
{"cr2", kMediaAppId},
{"dng", kMediaAppId},
{"nef", kMediaAppId},
{"nrw", kMediaAppId},
{"orf", kMediaAppId},
{"raf", kMediaAppId},
{"rw2", kMediaAppId},
{"NRW", kMediaAppId}, // Uppercase extension.
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, VideoHandlerChangeDetector) {
std::vector<Expectation> expectations = {
{"3gp", kMediaAppId}, {"avi", kMediaAppId},
{"m4v", kMediaAppId}, {"mkv", kMediaAppId},
{"mov", kMediaAppId}, {"mp4", kMediaAppId},
{"mpeg", kMediaAppId}, {"mpeg4", kMediaAppId, "video/mpeg"},
{"mpg", kMediaAppId}, {"mpg4", kMediaAppId, "video/mpeg"},
{"ogm", kMediaAppId}, {"ogv", kMediaAppId},
{"ogx", kMediaAppId}, {"webm", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, AudioHandlerChangeDetector) {
std::vector<Expectation> expectations = {
{"flac", kMediaAppId}, {"m4a", kMediaAppId}, {"mp3", kMediaAppId},
{"oga", kMediaAppId}, {"ogg", kMediaAppId}, {"wav", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, PdfHandlerChangeDetector) {
std::vector<Expectation> expectations = {{"pdf", kMediaAppId},
{"PDF", kMediaAppId}};
TestExpectationsAgainstDefaultTasks(expectations);
}
// Spot test the default handlers for selections that include multiple different
// file types. Only tests combinations of interest to the Media App.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, MultiSelectDefaultHandler) {
std::vector<Expectation> expectations = {
{"jpg/gif", kMediaAppId},
{"jpg/mp4", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Check that QuickOffice has a handler installed for common Office doc types.
// This test only runs with the is_chrome_branded GN flag set because otherwise
// QuickOffice is not installed.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, QuickOffice) {
std::vector<Expectation> expectations = {
{"doc", extension_misc::kQuickOfficeComponentExtensionId},
{"docx", extension_misc::kQuickOfficeComponentExtensionId},
{"ppt", extension_misc::kQuickOfficeComponentExtensionId},
{"pptx", extension_misc::kQuickOfficeComponentExtensionId},
{"xls", extension_misc::kQuickOfficeComponentExtensionId},
{"xlsx", extension_misc::kQuickOfficeComponentExtensionId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// The Media App will be preferred over a chrome app with a specific extension,
// unless that app is set default via prefs.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, MediaAppPreferredOverChromeApps) {
if (profile_type() == TestProfileType::kGuest) {
// The provided file system can't install in guest mode. Just check that
// MediaApp handles tiff.
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
return;
}
Profile* profile = browser()->profile();
auto extension = InstallTiffHandlerChromeApp(profile);
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
UpdateDefaultTask(
profile,
TaskDescriptor(extension->id(), StringToTaskType("app"), "tiffAction"),
{"tiff"}, {"image/tiff"});
if (profile_type() == TestProfileType::kIncognito) {
// In incognito, the installed app is not enabled and we filter it out.
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
} else {
TestExpectationsAgainstDefaultTasks({{"tiff", extension->id().c_str()}});
}
}
// Test expectations for files coming from provided file systems.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ProvidedFileSystemFileSource) {
if (profile_type() == TestProfileType::kGuest) {
// Provided file systems don't exist in guest. This test seems to work OK in
// incognito mode though.
return;
}
// The current test expectation: a GIF file in the provided file system called
// "readwrite.gif" should open with the MediaApp.
const char kTestFile[] = "readwrite.gif";
Expectation test = {"gif", kMediaAppId};
int remaining_expectations = 1;
Profile* profile = browser()->profile();
base::WeakPtr<Volume> volume =
test::InstallFileSystemProviderChromeApp(profile);
GURL url;
ASSERT_TRUE(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, volume->mount_path().AppendASCII(kTestFile),
util::GetFileManagerURL(), &url));
// Note |url| differs slightly to the result of ToGURL() below. The colons
// either side of `:test-image-provider-fs:` become escaped as `%3A`.
storage::FileSystemURL filesystem_url =
util::GetFileManagerFileSystemContext(profile)
->CrackURLInFirstPartyContext(url);
std::vector<GURL> urls = {filesystem_url.ToGURL()};
std::vector<extensions::EntryInfo> entries;
// We could add the mime type here, but since a "real" file is provided, we
// can get additional coverage of the mime determination. For non-native files
// this uses metadata only (not sniffing).
entries.emplace_back(filesystem_url.path(), "", false);
base::RunLoop run_loop;
auto verifier = base::BindOnce(&VerifyAsyncTask, &remaining_expectations,
test, run_loop.QuitClosure());
extensions::app_file_handler_util::GetMimeTypeForLocalPath(
profile, entries[0].path,
base::BindLambdaForTesting([&](const std::string& mime_type) {
entries[0].mime_type = mime_type;
EXPECT_EQ(entries[0].mime_type, "image/gif");
FindAllTypesOfTasks(profile, entries, urls, std::move(verifier));
}));
run_loop.Run();
EXPECT_EQ(remaining_expectations, 0);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExecuteWebApp) {
auto web_app_info = std::make_unique<WebAppInstallInfo>();
web_app_info->start_url = GURL("https://www.example.com/");
web_app_info->scope = GURL("https://www.example.com/");
apps::FileHandler handler;
handler.action = GURL("https://www.example.com/handle_file");
handler.display_name = u"activity name";
apps::FileHandler::AcceptEntry accept_entry1;
accept_entry1.mime_type = "image/jpeg";
accept_entry1.file_extensions.insert(".jpeg");
handler.accept.push_back(accept_entry1);
apps::FileHandler::AcceptEntry accept_entry2;
accept_entry2.mime_type = "image/png";
accept_entry2.file_extensions.insert(".png");
handler.accept.push_back(accept_entry2);
web_app_info->file_handlers.push_back(std::move(handler));
Profile* const profile = browser()->profile();
TaskDescriptor task_descriptor;
if (GetParam().crosapi_state == TestProfileParam::CrosapiParam::kDisabled) {
// Install a PWA in ash.
web_app::AppId app_id =
web_app::test::InstallWebApp(profile, std::move(web_app_info));
task_descriptor = TaskDescriptor(app_id, TaskType::TASK_TYPE_WEB_APP,
"https://www.example.com/handle_file");
// Skip past the permission dialog.
web_app::WebAppProvider::GetForTest(profile)
->sync_bridge()
.SetAppFileHandlerApprovalState(app_id,
web_app::ApiApprovalState::kAllowed);
} else {
// Use an existing SWA in ash - Media app.
task_descriptor = TaskDescriptor(kMediaAppId, TaskType::TASK_TYPE_WEB_APP,
"chrome://media-app/open");
// TODO(petermarshall): Install the web app in Lacros once installing and
// launching apps from ash -> lacros is possible.
}
base::RunLoop run_loop;
web_app::WebAppLaunchManager::SetOpenApplicationCallbackForTesting(
base::BindLambdaForTesting(
[&run_loop](apps::AppLaunchParams&& params) -> content::WebContents* {
if (GetParam().crosapi_state ==
TestProfileParam::CrosapiParam::kDisabled) {
EXPECT_EQ(params.override_url,
"https://www.example.com/handle_file");
} else {
EXPECT_EQ(params.override_url, "chrome://media-app/open");
}
EXPECT_EQ(params.launch_files.size(), 2U);
EXPECT_TRUE(base::EndsWith(params.launch_files.at(0).MaybeAsASCII(),
"foo.jpeg"));
EXPECT_TRUE(base::EndsWith(params.launch_files.at(1).MaybeAsASCII(),
"bar.png"));
run_loop.Quit();
return nullptr;
}));
base::FilePath file1 =
util::GetMyFilesFolderForProfile(profile).AppendASCII("foo.jpeg");
base::FilePath file2 =
util::GetMyFilesFolderForProfile(profile).AppendASCII("bar.png");
GURL url1;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, file1, util::GetFileManagerURL(), &url1));
GURL url2;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, file2, util::GetFileManagerURL(), &url2));
std::vector<storage::FileSystemURL> files;
files.push_back(storage::FileSystemURL::CreateForTest(url1));
files.push_back(storage::FileSystemURL::CreateForTest(url2));
ExecuteFileTask(profile, task_descriptor, files, base::DoNothing());
run_loop.Run();
}
// Launch a Chrome app with a real file and wait for it to ping back.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExecuteChromeApp) {
if (profile_type() == TestProfileType::kGuest) {
// The app can't install in guest mode.
return;
}
Profile* const profile = browser()->profile();
auto extension = InstallTiffHandlerChromeApp(profile);
TaskDescriptor task_descriptor(extension->id(), TASK_TYPE_FILE_HANDLER,
"tiffAction");
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
path = path.AppendASCII("chromeos/file_manager/test_small.tiff");
{
base::ScopedAllowBlockingForTesting allow_blocking;
EXPECT_TRUE(base::PathExists(path));
}
// Copy the file into My Files.
file_manager::test::FolderInMyFiles folder(profile);
folder.Add({path});
base::FilePath path_in_my_files = folder.files()[0];
GURL tiff_url;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, path_in_my_files, util::GetFileManagerURL(), &tiff_url));
std::vector<storage::FileSystemURL> files;
files.push_back(storage::FileSystemURL::CreateForTest(tiff_url));
content::DOMMessageQueue message_queue;
ExecuteFileTask(profile, task_descriptor, files, base::DoNothing());
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
ASSERT_EQ("\"Received tiffAction with: test_small.tiff\"", message);
}
INSTANTIATE_SYSTEM_WEB_APP_MANAGER_TEST_SUITE_ALL_PROFILE_TYPES_P(
FileTasksBrowserTest);
} // namespace file_tasks
} // namespace file_manager