Commit 954a2188 authored by Robert Woods's avatar Robert Woods Committed by Commit Bot

WebApps: Register MIME types when web app installed on Linux.

In Linux, an association between an app and a particular file is made
on the basis of the file's MIME type. Where an app supports handling
MIME types that aren't known to the OS, those types must be registered
calling `xdg-mime` [1] on an XML file containing a mapping of MIME types
to file extensions per the freedesktop.org Shared MIME-info Database
specification [2]. This CL implements methods for generating a compliant
XML file from a set of file handlers, writing it to a temp file, and
calling `xdg-mime` on that file to register the new MIME types. These
are invoked as part of the web app install process on Linux.

[1] https://linux.die.net/man/1/xdg-mime
[2] https://freedesktop.org/standards/shared-mime-info

Bug: 938103
Change-Id: If7e4cb0063e1688cb69d2b2b503235658c087366
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2101398
Commit-Queue: Robert Woods <robertwoods@google.com>
Reviewed-by: default avatarAlan Cutter <alancutter@chromium.org>
Reviewed-by: default avatarThomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#751938}
parent a4a2d4b2
......@@ -12,6 +12,7 @@
#include <unistd.h>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
......@@ -32,6 +33,7 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/thread.h"
......@@ -645,6 +647,43 @@ std::string GetDirectoryFileContents(const base::string16& title,
#endif
}
base::FilePath GetMimeTypesRegistrationFilename(
const base::FilePath& profile_path,
const web_app::AppId& app_id) {
DCHECK(!profile_path.empty() && !app_id.empty());
// Use a prefix to clearly group files created by Chrome.
std::string filename = base::StringPrintf(
"%s-%s-%s%s", chrome::kBrowserProcessExecutableName, app_id.c_str(),
profile_path.BaseName().value().c_str(), ".xml");
// Replace illegal characters and spaces in |filename|.
base::i18n::ReplaceIllegalCharactersInPath(&filename, '_');
base::ReplaceChars(filename, " ", "_", &filename);
return base::FilePath(filename);
}
std::string GetMimeTypesRegistrationFileContents(
const apps::FileHandlers& file_handlers) {
std::stringstream ss;
ss << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<mime-info "
"xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n";
for (const auto& file_handler : file_handlers) {
for (const auto& accept_entry : file_handler.accept) {
ss << " <mime-type type=\"" << accept_entry.mime_type + "\">\n";
for (const auto& file_extension : accept_entry.file_extensions)
ss << " <glob pattern=\"*" << file_extension << "\"/>\n";
ss << " </mime-type>\n";
}
}
ss << "</mime-info>\n";
return ss.str();
}
} // namespace shell_integration_linux
namespace shell_integration {
......
......@@ -8,7 +8,9 @@
#include <string>
#include "base/files/file_path.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "chrome/common/buildflags.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "url/gurl.h"
#if defined(OS_CHROMEOS)
......@@ -90,6 +92,18 @@ std::string GetDesktopFileContentsForCommand(
std::string GetDirectoryFileContents(const base::string16& title,
const std::string& icon_name);
// Returns the filename for a .xml file, corresponding to a given |app_id|,
// which is passed to `xdg-mime` to register one or more custom MIME types in
// Linux.
base::FilePath GetMimeTypesRegistrationFilename(
const base::FilePath& profile_path,
const web_app::AppId& app_id);
// Returns the contents of a .xml file as specified by |file_handlers|, which is
// passed to `xdg-mime` to register one or more custom MIME types in Linux.
std::string GetMimeTypesRegistrationFileContents(
const apps::FileHandlers& file_handlers);
// Windows that correspond to web apps need to have a deterministic (and
// different) WMClass than normal chrome windows so the window manager groups
// them as a separate application.
......
......@@ -23,7 +23,9 @@
#include "base/test/scoped_path_override.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "chrome/common/chrome_constants.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
......@@ -531,6 +533,81 @@ TEST(ShellIntegrationTest, GetDirectoryFileContents) {
}
}
TEST(ShellIntegrationTest, GetMimeTypesRegistrationFilename) {
const struct {
const char* const profile_path;
const char* const app_id;
const char* const expected_filename;
} test_cases[] = {
{"Default", "app-id", "-app-id-Default.xml"},
{"Default Profile", "app-id", "-app-id-Default_Profile.xml"},
{"foo/Default", "app-id", "-app-id-Default.xml"},
{"Default*Profile", "app-id", "-app-id-Default_Profile.xml"}};
std::string browser_name(chrome::kBrowserProcessExecutableName);
for (const auto& test_case : test_cases) {
const base::FilePath filename =
GetMimeTypesRegistrationFilename(base::FilePath(test_case.profile_path),
web_app::AppId(test_case.app_id));
EXPECT_EQ(browser_name + test_case.expected_filename, filename.value());
}
}
TEST(ShellIntegrationTest, GetMimeTypesRegistrationFileContents) {
apps::FileHandlers file_handlers;
{
apps::FileHandler file_handler;
{
apps::FileHandler::AcceptEntry accept_entry;
accept_entry.mime_type = "application/foo";
accept_entry.file_extensions.insert(".foo");
file_handler.accept.push_back(accept_entry);
}
file_handlers.push_back(file_handler);
}
{
apps::FileHandler file_handler;
{
apps::FileHandler::AcceptEntry accept_entry;
accept_entry.mime_type = "application/foobar";
accept_entry.file_extensions.insert(".foobar");
file_handler.accept.push_back(accept_entry);
}
file_handlers.push_back(file_handler);
}
{
apps::FileHandler file_handler;
{
apps::FileHandler::AcceptEntry accept_entry;
accept_entry.mime_type = "application/bar";
accept_entry.file_extensions.insert(".bar");
accept_entry.file_extensions.insert(".baz");
file_handler.accept.push_back(accept_entry);
}
file_handlers.push_back(file_handler);
}
const std::string file_contents =
GetMimeTypesRegistrationFileContents(file_handlers);
const std::string expected_file_contents =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<mime-info "
"xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n"
" <mime-type type=\"application/foo\">\n"
" <glob pattern=\"*.foo\"/>\n"
" </mime-type>\n"
" <mime-type type=\"application/foobar\">\n"
" <glob pattern=\"*.foobar\"/>\n"
" </mime-type>\n"
" <mime-type type=\"application/bar\">\n"
" <glob pattern=\"*.bar\"/>\n"
" <glob pattern=\"*.baz\"/>\n"
" </mime-type>\n"
"</mime-info>\n";
EXPECT_EQ(file_contents, expected_file_contents);
}
TEST(ShellIntegrationTest, WmClass) {
base::CommandLine command_line((base::FilePath()));
EXPECT_EQ("foo", internal::GetProgramClassName(command_line, "foo.desktop"));
......
......@@ -164,7 +164,10 @@ source_set("unit_tests") {
if (is_desktop_linux) {
# Desktop linux, doesn't count ChromeOS.
sources += [ "web_app_shortcut_linux_unittest.cc" ]
sources += [
"web_app_file_handler_registration_linux_unittest.cc",
"web_app_shortcut_linux_unittest.cc",
]
}
deps = [
......@@ -194,6 +197,10 @@ source_set("browser_tests") {
sources = [ "web_app_url_loader_browsertest.cc" ]
if (is_desktop_linux) {
sources += [ "web_app_file_handler_registration_linux_browsertest.cc" ]
}
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
deps = [
......
......@@ -7,11 +7,12 @@
#include <string>
#include "base/callback.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "components/services/app_service/public/cpp/file_handler.h"
class Profile;
namespace web_app {
// True if file handlers are managed externally by the operating system, and
......@@ -35,6 +36,26 @@ void RegisterFileHandlersWithOs(const AppId& app_id,
// If a shim app was required, also removes the shim app.
void UnregisterFileHandlersWithOs(const AppId& app_id, Profile* profile);
#if defined(OS_LINUX)
using RegisterMimeTypesOnLinuxCallback =
base::OnceCallback<bool(base::FilePath profile_path,
std::string file_contents)>;
// Exposed for testing purposes. Register the set of
// MIME-type-to-file-extensions mappings corresponding to |file_handlers|. File
// I/O and a a callout to the Linux shell are performed asynchronously in a
// |callback|, which is set automatically on the usual install code path.
void RegisterMimeTypesOnLinux(const AppId& app_id,
Profile* profile,
const apps::FileHandlers& file_handlers,
RegisterMimeTypesOnLinuxCallback callback);
// Override the default |callback| passed to RegisterMimeTypesOnLinux. Used in
// automated browser tests.
void SetRegisterMimeTypesOnLinuxCallbackForTesting(
RegisterMimeTypesOnLinuxCallback callback);
#endif
} // namespace web_app
#endif // CHROME_BROWSER_WEB_APPLICATIONS_COMPONENTS_WEB_APP_FILE_HANDLER_REGISTRATION_H_
......@@ -6,6 +6,11 @@
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/no_destructor.h"
#include "chrome/browser/shell_integration_linux.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/app_shortcut_manager.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
......@@ -37,6 +42,38 @@ void UpdateFileHandlerRegistrationInOs(const AppId& app_id, Profile* profile) {
app_id, base::BindOnce(&OnShortcutInfoReceived));
}
void OnRegisterMimeTypes(bool registration_succeeded) {
if (!registration_succeeded)
LOG(ERROR) << "Registering MIME types failed.";
}
bool DoRegisterMimeTypes(base::FilePath filename, std::string file_contents) {
DCHECK(!filename.empty() && !file_contents.empty());
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir())
return false;
base::FilePath temp_file_path(temp_dir.GetPath().Append(filename));
int bytes_written = base::WriteFile(temp_file_path, file_contents.data(),
file_contents.length());
if (bytes_written != static_cast<int>(file_contents.length()))
return false;
std::vector<std::string> argv{"xdg-mime", "install", "--mode", "user",
temp_file_path.value()};
int exit_code;
shell_integration_linux::LaunchXdgUtility(argv, &exit_code);
return exit_code == 0;
}
RegisterMimeTypesOnLinuxCallback& GetRegisterMimeTypesCallbackForTesting() {
static base::NoDestructor<RegisterMimeTypesOnLinuxCallback> instance;
return *instance;
}
} // namespace
bool ShouldRegisterFileHandlersWithOs() {
......@@ -47,6 +84,15 @@ void RegisterFileHandlersWithOs(const AppId& app_id,
const std::string& app_name,
Profile* profile,
const apps::FileHandlers& file_handlers) {
if (!file_handlers.empty()) {
RegisterMimeTypesOnLinuxCallback callback =
GetRegisterMimeTypesCallbackForTesting()
? std::move(GetRegisterMimeTypesCallbackForTesting())
: base::BindOnce(&DoRegisterMimeTypes);
RegisterMimeTypesOnLinux(app_id, profile, file_handlers,
std::move(callback));
}
UpdateFileHandlerRegistrationInOs(app_id, profile);
}
......@@ -61,4 +107,29 @@ void UnregisterFileHandlersWithOs(const AppId& app_id, Profile* profile) {
UpdateFileHandlerRegistrationInOs(app_id, profile);
}
void RegisterMimeTypesOnLinux(const AppId& app_id,
Profile* profile,
const apps::FileHandlers& file_handlers,
RegisterMimeTypesOnLinuxCallback callback) {
DCHECK(!app_id.empty() && !file_handlers.empty());
base::FilePath filename =
shell_integration_linux::GetMimeTypesRegistrationFilename(
profile->GetPath(), app_id);
std::string file_contents =
shell_integration_linux::GetMimeTypesRegistrationFileContents(
file_handlers);
internals::GetShortcutIOTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(std::move(callback), std::move(filename),
std::move(file_contents)),
base::BindOnce(&OnRegisterMimeTypes));
}
void SetRegisterMimeTypesOnLinuxCallbackForTesting(
RegisterMimeTypesOnLinuxCallback callback) {
GetRegisterMimeTypesCallbackForTesting() = std::move(callback);
}
} // namespace web_app
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/components/web_app_file_handler_registration.h"
#include <map>
#include <string>
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/optional.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/shell_integration_linux.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/external_install_options.h"
#include "chrome/browser/web_applications/components/externally_installed_web_app_prefs.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
namespace web_app {
namespace {
using AcceptMap = std::map<std::string, base::flat_set<std::string>>;
apps::FileHandler GetTestFileHandler(const std::string& action,
const AcceptMap& accept_map) {
apps::FileHandler file_handler;
file_handler.action = GURL(action);
for (const auto& elem : accept_map) {
apps::FileHandler::AcceptEntry accept_entry;
accept_entry.mime_type = elem.first;
accept_entry.file_extensions.insert(elem.second.begin(), elem.second.end());
file_handler.accept.push_back(accept_entry);
}
return file_handler;
}
} // namespace
class WebAppFileHandlerRegistrationLinuxBrowserTest
: public InProcessBrowserTest,
public ::testing::WithParamInterface<ProviderType> {
protected:
WebAppFileHandlerRegistrationLinuxBrowserTest() {
if (GetParam() == ProviderType::kWebApps) {
scoped_feature_list_.InitWithFeatures(
{blink::features::kNativeFileSystemAPI,
blink::features::kFileHandlingAPI,
features::kDesktopPWAsWithoutExtensions},
{});
} else if (GetParam() == ProviderType::kBookmarkApps) {
scoped_feature_list_.InitWithFeatures(
{blink::features::kNativeFileSystemAPI,
blink::features::kFileHandlingAPI},
{features::kDesktopPWAsWithoutExtensions});
}
}
AppRegistrar& registrar() {
return WebAppProviderBase::GetProviderBase(browser()->profile())
->registrar();
}
void InstallApp(ExternalInstallOptions install_options) {
result_code_ = web_app::PendingAppManagerInstall(browser()->profile(),
install_options);
}
base::test::ScopedFeatureList scoped_feature_list_;
base::Optional<InstallResultCode> result_code_;
};
// Verify that the MIME type registration callback is called and that
// the caller behaves as expected.
IN_PROC_BROWSER_TEST_P(WebAppFileHandlerRegistrationLinuxBrowserTest,
RegisterMimeTypesOnLinuxCallbackCalledSuccessfully) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL(
"/banners/"
"manifest_test_page.html?manifest=manifest_with_file_handlers.json"));
apps::FileHandlers file_handlers;
file_handlers.push_back(GetTestFileHandler(
"https://site.api/open-foo",
{{"application/foo", {".foo"}}, {"application/foobar", {".foobar"}}}));
file_handlers.push_back(GetTestFileHandler(
"https://site.api/open-bar", {{"application/bar", {".bar", ".baz"}}}));
std::string expected_file_contents =
shell_integration_linux::GetMimeTypesRegistrationFileContents(
file_handlers);
bool path_reached = false;
RegisterMimeTypesOnLinuxCallback callback = base::BindLambdaForTesting(
[&expected_file_contents, &path_reached](base::FilePath filename,
std::string file_contents) {
EXPECT_EQ(file_contents, expected_file_contents);
path_reached = true;
return true;
});
SetRegisterMimeTypesOnLinuxCallbackForTesting(std::move(callback));
InstallApp(CreateInstallOptions(url));
EXPECT_EQ(InstallResultCode::kSuccessNewInstall, result_code_.value());
ASSERT_TRUE(path_reached);
}
INSTANTIATE_TEST_SUITE_P(All,
WebAppFileHandlerRegistrationLinuxBrowserTest,
::testing::Values(ProviderType::kBookmarkApps,
ProviderType::kWebApps),
ProviderTypeParamToString);
} // namespace web_app
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/components/web_app_file_handler_registration.h"
#include <map>
#include <string>
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/test/bind_test_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shell_integration_linux.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace web_app {
typedef WebAppTest WebAppFileHandlerRegistrationLinuxTest;
namespace {
using AcceptMap = std::map<std::string, base::flat_set<std::string>>;
apps::FileHandler GetTestFileHandler(const std::string& action,
const AcceptMap& accept_map) {
apps::FileHandler file_handler;
file_handler.action = GURL(action);
for (const auto& elem : accept_map) {
apps::FileHandler::AcceptEntry accept_entry;
accept_entry.mime_type = elem.first;
accept_entry.file_extensions.insert(elem.second.begin(), elem.second.end());
file_handler.accept.push_back(accept_entry);
}
return file_handler;
}
} // namespace
TEST_F(WebAppFileHandlerRegistrationLinuxTest,
RegisterMimeTypesLocalVariablesAreCorrect) {
Profile* test_profile = profile();
const AppId& app_id("app-id");
apps::FileHandlers file_handlers;
file_handlers.push_back(GetTestFileHandler(
"https://site.api/open-foo",
{{"application/foo", {".foo"}}, {"application/foobar", {".foobar"}}}));
file_handlers.push_back(GetTestFileHandler(
"https://site.api/open-bar", {{"application/bar", {".bar", ".baz"}}}));
base::FilePath expected_filename =
shell_integration_linux::GetMimeTypesRegistrationFilename(
test_profile->GetPath(), app_id);
std::string expected_file_contents =
shell_integration_linux::GetMimeTypesRegistrationFileContents(
file_handlers);
RegisterMimeTypesOnLinuxCallback callback = base::BindLambdaForTesting(
[expected_filename, expected_file_contents](base::FilePath filename,
std::string file_contents) {
EXPECT_EQ(filename, expected_filename);
EXPECT_EQ(file_contents, expected_file_contents);
return true;
});
RegisterMimeTypesOnLinux(app_id, test_profile, file_handlers,
std::move(callback));
}
} // namespace web_app
{
"name": "Manifest with file handlers",
"icons": [
{
"src": "launcher-icon-1x.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "launcher-icon-1-5x.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any badge"
},
{
"src": "launcher-icon-3x.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "launcher-icon-4x.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "image-512px.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "manifest_test_page.html",
"scope": "/",
"display": "standalone",
"orientation": "landscape",
"file_handlers": [
{
"action": "/open-foo",
"accept": {
"application/foo": [".foo"],
"application/foobar": [".foobar"]
}
},{
"action": "/open-bar",
"accept": {
"application/bar": [".bar", ".baz"]
}
}
]
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment