Commit e4b16bb5 authored by David Bienvenu's avatar David Bienvenu Committed by Commit Bot

desktop-pwas: Multi-profile support for pwa file registration.

When the same app is installed in multiple profiles, the open with
context menu needs to distinguish between the different profiles.
We do this by adding the profile name in parentheses after the app name,
e.g., "foo app (Profile1)". This CL detects when registering and
unregistering an app changes the app from being installed in
multiple profiles or just a single profile, and updates the app name.

This is similar to the way we badge/unbadge pinned taskbar icons when
the user switches between having multiple profiles and
having a single-profile.

Bug: 960245
Change-Id: I4c2a50c55cd8ab45be05eb023b162afc4f634cd0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2023283
Commit-Queue: David Bienvenu <davidbienvenu@chromium.org>
Reviewed-by: default avatarAlexey Baskakov <loyso@chromium.org>
Reviewed-by: default avatarGreg Thompson <grt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738802}
parent d97c6e27
......@@ -18,7 +18,11 @@
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/win/windows_version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/web_applications/components/web_app_file_handler_registration.h"
#include "chrome/browser/web_applications/components/web_app_shortcut.h"
#include "chrome/browser/web_applications/components/web_app_shortcut_win.h"
......@@ -59,6 +63,60 @@ base::FilePath GetAppSpecificLauncherFilename(const base::string16& app_name) {
return base::FilePath(sanitized_app_name);
}
// Returns true if the app with id |app_id| is currently installed in one or
// more profiles, excluding |curr_profile|, and has its web_app launcher
// registered with Windows as a handler for the file extensions it supports.
// Sets |only_profile_with_app_installed| to the path of profile that is the
// only profile with the app installed, an empty path otherwise. If the app is
// only installed in exactly one other profile, it will need its app name
// updated.
bool IsWebAppLauncherRegisteredWithWindows(
const web_app::AppId& app_id,
Profile* curr_profile,
base::FilePath* only_profile_with_app_installed) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
auto* storage = &profile_manager->GetProfileAttributesStorage();
bool found_app = false;
std::vector<ProfileAttributesEntry*> entries =
storage->GetAllProfilesAttributes();
for (ProfileAttributesEntry* entry : entries) {
base::FilePath profile_path = entry->GetPath();
if (profile_path == curr_profile->GetPath())
continue;
base::string16 profile_prog_id =
web_app::GetProgIdForApp(profile_path, app_id);
base::FilePath shim_app_path =
ShellUtil::GetApplicationPathForProgId(profile_prog_id);
if (shim_app_path.empty())
continue;
*only_profile_with_app_installed =
found_app ? base::FilePath() : profile_path;
found_app = true;
if (only_profile_with_app_installed->empty())
break;
}
return found_app;
}
// Construct a string that is used to specify which profile a web
// app is installed for. The string is of the form "( <profile name>)".
base::string16 GetAppNameExtensionForProfile(
const base::FilePath& profile_path) {
base::string16 app_name_extension;
ProfileManager* profile_manager = g_browser_process->profile_manager();
ProfileAttributesStorage& storage =
profile_manager->GetProfileAttributesStorage();
ProfileAttributesEntry* entry;
bool has_entry = storage.GetProfileAttributesWithPath(profile_path, &entry);
if (has_entry) {
app_name_extension.append(STRING16_LITERAL(" ("));
app_name_extension.append(entry->GetName());
app_name_extension.append(STRING16_LITERAL(")"));
}
return app_name_extension;
}
} // namespace
namespace web_app {
......@@ -74,10 +132,11 @@ bool ShouldRegisterFileHandlersWithOs() {
// the allowed characters in a prog_id. Since the prog_id is stored in the
// Windows registry, the mapping between a given profile+app_id and a prog_id
// can not be changed.
base::string16 GetProgIdForApp(Profile* profile, const AppId& app_id) {
base::string16 GetProgIdForApp(const base::FilePath& profile_path,
const AppId& app_id) {
base::string16 prog_id = install_static::GetBaseAppId();
std::string app_specific_part(
base::UTF16ToUTF8(profile->GetPath().BaseName().value()));
base::UTF16ToUTF8(profile_path.BaseName().value()));
app_specific_part.append(app_id);
uint32_t hash = base::PersistentHash(app_specific_part);
prog_id.push_back('.');
......@@ -89,16 +148,21 @@ void RegisterFileHandlersWithOsTask(
const AppId& app_id,
const std::string& app_name,
const base::FilePath& profile_path,
const base::string16& app_prog_id,
const std::set<std::string>& file_extensions) {
const std::set<base::string16>& file_extensions,
const base::string16& app_name_extension) {
base::FilePath web_app_path =
GetWebAppDataDirectory(profile_path, app_id, GURL());
base::string16 utf16_app_name = base::UTF8ToUTF16(app_name);
base::FilePath icon_path =
internals::GetIconFilePath(web_app_path, utf16_app_name);
base::FilePath pwa_launcher_path = GetChromePwaLauncherPath();
base::FilePath app_specific_launcher_path =
web_app_path.Append(GetAppSpecificLauncherFilename(utf16_app_name));
base::string16 user_visible_app_name(utf16_app_name);
// Specialize utf16_app_name to be per-profile, if there are other profiles
// which have the web app installed. Also, migrate the registration
// for the other profile(s), if needed.
user_visible_app_name.append(app_name_extension);
base::FilePath app_specific_launcher_path = web_app_path.Append(
GetAppSpecificLauncherFilename(user_visible_app_name));
// Create a hard link to the chrome pwa launcher app. Delete any pre-existing
// version of the file first.
......@@ -113,16 +177,34 @@ void RegisterFileHandlersWithOsTask(
app_specific_launcher_command.AppendSwitchPath(switches::kProfileDirectory,
profile_path.BaseName());
app_specific_launcher_command.AppendSwitchASCII(switches::kAppId, app_id);
std::set<base::string16> file_exts;
// Copy |file_extensions| to a string16 set in O(n) time by hinting that
// the appended elements should go at the end of the set.
std::transform(file_extensions.begin(), file_extensions.end(),
std::inserter(file_exts, file_exts.end()),
[](const std::string& ext) { return base::UTF8ToUTF16(ext); });
ShellUtil::AddFileAssociations(
app_prog_id, app_specific_launcher_command, utf16_app_name,
utf16_app_name + STRING16_LITERAL(" File"), icon_path, file_exts);
GetProgIdForApp(profile_path, app_id), app_specific_launcher_command,
user_visible_app_name, utf16_app_name + STRING16_LITERAL(" File"),
icon_path, file_extensions);
}
// Remove existing registration for an app, and reregister it. This is called
// when the app name changes.
void ReRegisterFileHandlersWithOs(
const AppId& app_id,
const std::string& app_name,
const base::FilePath& profile_path,
const std::set<base::string16>& file_extensions,
const base::string16& prog_id,
const base::string16& app_name_extension) {
base::PostTask(FROM_HERE,
{base::ThreadPool(), base::MayBlock(),
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(IgnoreResult(&base::DeleteFile),
ShellUtil::GetApplicationPathForProgId(prog_id),
/*recursively=*/false));
ShellUtil::DeleteFileAssociations(prog_id);
base::PostTask(
FROM_HERE,
{base::ThreadPool(), base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&RegisterFileHandlersWithOsTask, app_id, app_name,
profile_path, file_extensions, app_name_extension));
}
void RegisterFileHandlersWithOs(const AppId& app_id,
......@@ -130,13 +212,34 @@ void RegisterFileHandlersWithOs(const AppId& app_id,
Profile* profile,
const std::set<std::string>& file_extensions,
const std::set<std::string>& mime_types) {
base::PostTask(
FROM_HERE,
base::string16 app_name_extension;
base::FilePath only_profile_with_app_installed;
// Determine if there is exactly one other profile with the
// app installed, before doing this new registration.
if (IsWebAppLauncherRegisteredWithWindows(app_id, profile,
&only_profile_with_app_installed))
app_name_extension = GetAppNameExtensionForProfile(profile->GetPath());
std::set<base::string16> file_extensions16;
// Copy |file_extensions| to a string16 set in O(n) time by hinting that
// the appended elements should go at the end of the set.
std::transform(file_extensions.begin(), file_extensions.end(),
std::inserter(file_extensions16, file_extensions16.end()),
[](const std::string& ext) { return base::UTF8ToUTF16(ext); });
base::PostTask(FROM_HERE,
{base::ThreadPool(), base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&RegisterFileHandlersWithOsTask, app_id, app_name,
profile->GetPath(), GetProgIdForApp(profile, app_id),
file_extensions));
base::BindOnce(&RegisterFileHandlersWithOsTask, app_id,
app_name, profile->GetPath(), file_extensions16,
app_name_extension));
// If there's another profile that needs its name changed, reregister it.
if (!only_profile_with_app_installed.empty()) {
ReRegisterFileHandlersWithOs(
app_id, app_name, only_profile_with_app_installed, file_extensions16,
GetProgIdForApp(only_profile_with_app_installed, app_id),
GetAppNameExtensionForProfile(only_profile_with_app_installed));
}
}
void UnregisterFileHandlersWithOs(const AppId& app_id, Profile* profile) {
......@@ -145,10 +248,9 @@ void UnregisterFileHandlersWithOs(const AppId& app_id, Profile* profile) {
// the registry, since the app-specific-launcher path is retrieved from the
// registry.
base::string16 prog_id = GetProgIdForApp(profile, app_id);
base::string16 prog_id = GetProgIdForApp(profile->GetPath(), app_id);
base::FilePath app_specific_launcher_path =
ShellUtil::GetApplicationPathForProgId(prog_id);
ShellUtil::DeleteFileAssociations(prog_id);
// Need to delete the hardlink file as well, since extension uninstall
......@@ -161,6 +263,43 @@ void UnregisterFileHandlersWithOs(const AppId& app_id, Profile* profile) {
base::BindOnce(IgnoreResult(&base::DeleteFile),
app_specific_launcher_path, /*recursively=*/false));
}
base::FilePath only_profile_with_app_installed;
if (IsWebAppLauncherRegisteredWithWindows(app_id, profile,
&only_profile_with_app_installed) &&
!only_profile_with_app_installed.empty()) {
base::string16 remaining_prog_id =
GetProgIdForApp(only_profile_with_app_installed, app_id);
// Before deleting the file associations for the remaining app registration,
// remember the app name and file extensions.
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name =
ShellUtil::GetFileAssociationsAndAppName(remaining_prog_id);
if (file_associations_and_app_name.app_name.empty()) {
// If we can't get the file associations, just leave the remaining app
// registration as is.
return;
}
base::string16 app_name_extension =
GetAppNameExtensionForProfile(only_profile_with_app_installed);
// If |app_name| ends with " (<profile name>)", remove it.
std::string new_app_name;
if (base::EndsWith(file_associations_and_app_name.app_name,
app_name_extension, base::CompareCase::SENSITIVE) &&
file_associations_and_app_name.app_name.size() >
app_name_extension.size()) {
new_app_name = base::UTF16ToUTF8(
base::StringPiece16(file_associations_and_app_name.app_name.c_str(),
file_associations_and_app_name.app_name.size() -
app_name_extension.size()));
} else {
// We probably don't have to reregister the app, but it's probably safer
// to make sure the app name is correct.
new_app_name = base::UTF16ToUTF8(file_associations_and_app_name.app_name);
}
ReRegisterFileHandlersWithOs(
app_id, new_app_name, only_profile_with_app_installed,
file_associations_and_app_name.file_associations, remaining_prog_id,
base::string16());
}
}
void UpdateChromeExePath(const base::FilePath& user_data_dir) {
......
......@@ -14,8 +14,9 @@
namespace web_app {
// Returns the Windows ProgId for the web app with the passed |app_id| in
// |profile|.
base::string16 GetProgIdForApp(Profile* profile, const AppId& app_id);
// |profile_path|.
base::string16 GetProgIdForApp(const base::FilePath& profile_path,
const AppId& app_id);
// The name of "Last Browser" file, where UpdateChromeExePath() stores the path
// of the last Chrome executable to use the containing user-data directory.
......
......@@ -15,11 +15,16 @@
#include "base/test/test_reg_util_win.h"
#include "base/test/test_timeouts.h"
#include "base/win/windows_version.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/web_applications/components/web_app_shortcut_win.h"
#include "chrome/browser/web_applications/test/test_file_handler_manager.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/installer/util/shell_util.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
......@@ -67,6 +72,8 @@ TEST_F(UpdateChromeExePathTest, UpdateChromeExePath) {
EXPECT_EQ(GetLastBrowserPathFromFile(), GetCurrentExePath());
}
constexpr char kAppName[] = "app name";
class WebAppFileHandlerRegistrationWinTest : public testing::Test {
protected:
WebAppFileHandlerRegistrationWinTest() {}
......@@ -81,15 +88,36 @@ class WebAppFileHandlerRegistrationWinTest : public testing::Test {
// hand. TODO(davidbienvenu): Remove this once cl/1815220 lands.
base::File pwa_launcher(GetChromePwaLauncherPath(),
base::File::FLAG_CREATE);
testing_profile_manager_ = std::make_unique<TestingProfileManager>(
TestingBrowserProcess::GetGlobal());
ASSERT_TRUE(testing_profile_manager_->SetUp());
profile_ =
testing_profile_manager_->CreateTestingProfile(chrome::kInitialProfile);
}
void TearDown() override {
profile_ = nullptr;
testing_profile_manager_->DeleteAllTestingProfiles();
}
Profile* profile() { return profile_; }
ProfileManager* profile_manager() {
return testing_profile_manager_->profile_manager();
}
TestingProfileManager* testing_profile_manager() {
return testing_profile_manager_.get();
}
const AppId& app_id() const { return app_id_; }
Profile* profile() { return &profile_; }
const std::set<std::string>& file_extensions() const {
return file_extensions_;
}
// Returns true if Chrome extension with AppId |app_id| has its corresponding
// prog_id registered in Windows registry to handle files with extension
// |file_ext|, false otherwise.
bool ProgIdRegisteredForFileExtension(const std::string& file_ext,
const AppId& app_id) {
const AppId& app_id,
Profile* profile) {
base::string16 key_name(ShellUtil::kRegClasses);
key_name.push_back(base::FilePath::kSeparators[0]);
key_name.append(base::UTF8ToUTF16(file_ext));
......@@ -99,11 +127,45 @@ class WebAppFileHandlerRegistrationWinTest : public testing::Test {
std::wstring value;
EXPECT_EQ(ERROR_SUCCESS,
key.Open(HKEY_CURRENT_USER, key_name.c_str(), KEY_READ));
base::string16 prog_id = GetProgIdForApp(profile(), app_id);
base::string16 prog_id = GetProgIdForApp(profile->GetPath(), app_id);
return key.ReadValue(prog_id.c_str(), &value) == ERROR_SUCCESS &&
value == L"";
}
void AddAndVerifyFileAssociations(Profile* profile,
const std::string& app_name,
const char* app_name_extension) {
std::string sanitized_app_name(app_name);
sanitized_app_name.append(app_name_extension);
base::FilePath expected_app_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(profile, app_id(),
sanitized_app_name);
RegisterFileHandlersWithOs(app_id(), app_name, profile, file_extensions(),
/*mime_types=*/{});
base::ThreadPoolInstance::Get()->FlushForTesting();
base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile->GetPath(), app_id()));
EXPECT_TRUE(base::PathExists(registered_app_path));
EXPECT_EQ(registered_app_path, expected_app_launcher_path);
// .txt and .doc should have |app_name| in their Open With lists.
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile));
}
// Gets the launcher file path for |sanitized_app_name|. If not
// on Win7, the name will have the ".exe" extension.
base::FilePath GetAppSpecificLauncherFilePath(
const std::string& sanitized_app_name) {
base::FilePath app_specific_launcher_filepath(
base::ASCIIToUTF16(sanitized_app_name));
if (base::win::GetVersion() > base::win::Version::WIN7) {
app_specific_launcher_filepath =
app_specific_launcher_filepath.AddExtension(L"exe");
}
return app_specific_launcher_filepath;
}
// Creates a "Web Applications" directory containing a subdirectory for
// |app_id| inside |profile|'s data directory, then returns the expected app-
// launcher path inside the subdirectory for |app_id|.
......@@ -116,22 +178,20 @@ class WebAppFileHandlerRegistrationWinTest : public testing::Test {
// Make sure web app dir exists. Normally installing an extension would
// handle this.
EXPECT_TRUE(base::CreateDirectory(web_app_dir));
base::FilePath app_specific_launcher_filepath =
GetAppSpecificLauncherFilePath(sanitized_app_name);
base::FilePath app_specific_launcher_filename(
base::ASCIIToUTF16(sanitized_app_name));
if (base::win::GetVersion() > base::win::Version::WIN7) {
app_specific_launcher_filename =
app_specific_launcher_filename.AddExtension(L"exe");
}
return web_app_dir.Append(app_specific_launcher_filename);
return web_app_dir.Append(app_specific_launcher_filepath);
}
private:
registry_util::RegistryOverrideManager registry_override_;
content::BrowserTaskEnvironment task_environment_{
content::BrowserTaskEnvironment::IO_MAINLOOP};
TestingProfile profile_;
TestingProfile* profile_ = nullptr;
std::unique_ptr<TestingProfileManager> testing_profile_manager_;
const AppId app_id_ = "app_id";
const std::set<std::string> file_extensions_ = {"txt", "doc"};
};
// Test various attributes of ProgIds returned by GetAppIdForApp.
......@@ -142,85 +202,187 @@ TEST_F(WebAppFileHandlerRegistrationWinTest, GetProgIdForApp) {
// https://docs.microsoft.com/en-us/windows/win32/com/-progid--key.
AppId app_id1("app_id12345678901234567890123456789012345678901234");
constexpr unsigned int kMaxProgIdLen = 39;
base::string16 prog_id1 = GetProgIdForApp(profile(), app_id1);
base::string16 prog_id1 = GetProgIdForApp(profile()->GetPath(), app_id1);
EXPECT_LE(prog_id1.length(), kMaxProgIdLen);
for (auto itr = prog_id1.begin(); itr != prog_id1.end(); itr++)
EXPECT_TRUE(std::isalnum(*itr) || (*itr == '.' && itr != prog_id1.begin()));
AppId app_id2("different_appid");
// Check that different app ids in the same profile have different
// prog ids.
EXPECT_NE(prog_id1, GetProgIdForApp(profile(), app_id2));
EXPECT_NE(prog_id1, GetProgIdForApp(profile()->GetPath(), app_id2));
// Create a different profile, and verify that the prog id for the same
// app_id in a different profile is different.
TestingProfile profile2;
EXPECT_NE(prog_id1, GetProgIdForApp(&profile2, app_id1));
EXPECT_NE(prog_id1, GetProgIdForApp(profile2.GetPath(), app_id1));
}
TEST_F(WebAppFileHandlerRegistrationWinTest, RegisterFileHandlersForWebApp) {
// Set up a test profile.
std::set<std::string> file_extensions({"txt", "doc"});
AppId app_id("app_id");
std::string app_name("app name");
AddAndVerifyFileAssociations(profile(), kAppName, "");
}
// When an app is registered in one profile, and then is registered in a second
// profile, the open with context menu items for both app registrations should
// include the profile name, e.g., "app name (Default)" and "app name (Profile
// 2)".
TEST_F(WebAppFileHandlerRegistrationWinTest,
RegisterFileHandlersForWebAppIn2Profiles) {
AddAndVerifyFileAssociations(profile(), kAppName, "");
Profile* profile2 =
testing_profile_manager()->CreateTestingProfile("Profile 2");
ProfileAttributesStorage& storage =
profile_manager()->GetProfileAttributesStorage();
ASSERT_EQ(2u, storage.GetNumberOfProfiles());
AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name(
ShellUtil::GetFileAssociationsAndAppName(
GetProgIdForApp(profile()->GetPath(), app_id())));
ASSERT_FALSE(file_associations_and_app_name.app_name.empty());
// Profile 1's app name should now include the profile in the name.
std::string app_name_str =
base::UTF16ToUTF8(file_associations_and_app_name.app_name);
EXPECT_EQ(app_name_str, "app name (Default)");
// Profile 1's app_launcher should include the profile in its name.
base::FilePath profile1_app_specific_launcher_path =
GetAppSpecificLauncherFilePath("app name (Default)");
base::FilePath profile1_launcher_path =
ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile()->GetPath(), app_id()));
EXPECT_EQ(profile1_launcher_path.BaseName(),
profile1_app_specific_launcher_path);
// Verify that the app is still registered for ".txt" and ".doc" in profile 1.
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile()));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile()));
}
// When an app is registered in two profiles, and then unregistered in one of
// them, the remaining registration should no longer be profile-specific. It
// should not have the profile name in app_launcher executable name, or the
// registered app name.
TEST_F(WebAppFileHandlerRegistrationWinTest,
UnRegisterFileHandlersForWebAppIn2Profiles) {
AddAndVerifyFileAssociations(profile(), kAppName, "");
base::FilePath app_specific_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id, app_name);
ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile()->GetPath(), app_id()));
RegisterFileHandlersWithOs(app_id, app_name, profile(), file_extensions,
/*mime_types=*/{});
Profile* profile2 =
testing_profile_manager()->CreateTestingProfile("Profile 2");
ProfileAttributesStorage& storage =
profile_manager()->GetProfileAttributesStorage();
ASSERT_EQ(2u, storage.GetNumberOfProfiles());
AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
UnregisterFileHandlersWithOs(app_id(), profile());
base::ThreadPoolInstance::Get()->FlushForTesting();
base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile(), app_id));
EXPECT_FALSE(registered_app_path.empty());
EXPECT_TRUE(base::PathExists(app_specific_launcher_path));
EXPECT_EQ(app_specific_launcher_path, registered_app_path);
EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
// Verify that "(Profile 2)" was removed from the web app launcher and
// file association registry entries.
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name =
ShellUtil::GetFileAssociationsAndAppName(
GetProgIdForApp(profile2->GetPath(), app_id()));
ASSERT_FALSE(file_associations_and_app_name.app_name.empty());
// Profile 2's app name should no longer include the profile in the name.
std::string app_name_str =
base::UTF16ToUTF8(file_associations_and_app_name.app_name);
EXPECT_EQ(app_name_str, kAppName);
// Profile 2's app_launcher should no longer include the profile in its name.
base::FilePath profile2_app_specific_launcher_path =
GetAppSpecificLauncherFilePath(kAppName);
base::FilePath profile2_launcher_path =
ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile2->GetPath(), app_id()));
EXPECT_EQ(profile2_launcher_path.BaseName(),
profile2_app_specific_launcher_path);
// Verify that the app is still registered for ".txt" and ".doc" in profile 2.
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile2));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile2));
}
// .txt should have |app_name| in its Open With list.
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id));
// When an app is registered in three profiles, and then unregistered in one of
// them, the remaining registrations should not change.
TEST_F(WebAppFileHandlerRegistrationWinTest,
UnRegisterFileHandlersForWebAppIn3Profiles) {
AddAndVerifyFileAssociations(profile(), kAppName, "");
base::FilePath app_specific_launcher_path =
ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile()->GetPath(), app_id()));
Profile* profile2 =
testing_profile_manager()->CreateTestingProfile("Profile 2");
ProfileAttributesStorage& storage =
profile_manager()->GetProfileAttributesStorage();
ASSERT_EQ(2u, storage.GetNumberOfProfiles());
AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
Profile* profile3 =
testing_profile_manager()->CreateTestingProfile("Profile 3");
ASSERT_EQ(3u, storage.GetNumberOfProfiles());
AddAndVerifyFileAssociations(profile3, kAppName, " (Profile 3)");
UnregisterFileHandlersWithOs(app_id(), profile());
base::ThreadPoolInstance::Get()->FlushForTesting();
EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
// Verify that "(Profile 2)" was not removed from the web app launcher and
// file association registry entries.
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name2(
ShellUtil::GetFileAssociationsAndAppName(
GetProgIdForApp(profile2->GetPath(), app_id())));
ASSERT_FALSE(file_associations_and_app_name2.app_name.empty());
// Profile 2's app name should still include the profile name in its name.
std::string app_name_str =
base::UTF16ToUTF8(file_associations_and_app_name2.app_name);
EXPECT_EQ(app_name_str, "app name (Profile 2)");
// Profile 3's app name should still include the profile name in its name.
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name3(
ShellUtil::GetFileAssociationsAndAppName(
GetProgIdForApp(profile3->GetPath(), app_id())));
ASSERT_FALSE(file_associations_and_app_name3.app_name.empty());
// Profile 2's app name should still include the profile in the name.
app_name_str = base::UTF16ToUTF8(file_associations_and_app_name3.app_name);
EXPECT_EQ(app_name_str, "app name (Profile 3)");
}
TEST_F(WebAppFileHandlerRegistrationWinTest, UnregisterFileHandlersForWebApp) {
// Register file handlers, and then verify that unregistering removes
// the registry settings and the app-specific launcher.
std::set<std::string> file_extensions({"txt", "doc"});
AppId app_id("app_id");
std::string app_name("app name");
AddAndVerifyFileAssociations(profile(), kAppName, "");
base::FilePath app_specific_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id, app_name);
RegisterFileHandlersWithOs(app_id, app_name, profile(), file_extensions,
/*mime_types=*/{});
base::ThreadPoolInstance::Get()->FlushForTesting();
EXPECT_TRUE(base::PathExists(app_specific_launcher_path));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id));
EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id));
ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile()->GetPath(), app_id()));
UnregisterFileHandlersWithOs(app_id, profile());
UnregisterFileHandlersWithOs(app_id(), profile());
base::ThreadPoolInstance::Get()->FlushForTesting();
EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
EXPECT_FALSE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile()));
EXPECT_FALSE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile()));
EXPECT_FALSE(ProgIdRegisteredForFileExtension(".txt", app_id));
EXPECT_FALSE(ProgIdRegisteredForFileExtension(".doc", app_id));
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name =
ShellUtil::GetFileAssociationsAndAppName(
GetProgIdForApp(profile()->GetPath(), app_id()));
EXPECT_TRUE(file_associations_and_app_name.app_name.empty());
}
// Test that invalid file name characters in app_name are replaced with '_'.
TEST_F(WebAppFileHandlerRegistrationWinTest, AppNameWithInvalidChars) {
std::set<std::string> file_extensions({"txt"});
AppId app_id("app_id");
// '*' is an invalid char in Windows file names, so it should be replaced
// with '_'.
std::string app_name("app*name");
base::FilePath app_specific_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id,
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id(),
"app_name");
RegisterFileHandlersWithOs(app_id, app_name, profile(), file_extensions,
RegisterFileHandlersWithOs(app_id(), app_name, profile(), file_extensions,
/*mime_types=*/{});
base::ThreadPoolInstance::Get()->FlushForTesting();
base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile(), app_id));
GetProgIdForApp(profile()->GetPath(), app_id()));
EXPECT_FALSE(registered_app_path.empty());
EXPECT_TRUE(base::PathExists(app_specific_launcher_path));
EXPECT_EQ(app_specific_launcher_path, registered_app_path);
......@@ -230,19 +392,18 @@ TEST_F(WebAppFileHandlerRegistrationWinTest, AppNameWithInvalidChars) {
// prepended to it when used as a filename for its launcher.
TEST_F(WebAppFileHandlerRegistrationWinTest, AppNameIsReservedFilename) {
std::set<std::string> file_extensions({"txt"});
AppId app_id("app_id");
// "con" is a reserved filename on Windows, so it should have '_' prepended.
std::string app_name("con");
base::FilePath app_specific_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id, "_con");
CreateDataDirectoryAndGetLauncherPathForApp(profile(), app_id(), "_con");
RegisterFileHandlersWithOs(app_id, app_name, profile(), file_extensions,
RegisterFileHandlersWithOs(app_id(), app_name, profile(), file_extensions,
/*mime_types=*/{});
base::ThreadPoolInstance::Get()->FlushForTesting();
base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile(), app_id));
GetProgIdForApp(profile()->GetPath(), app_id()));
EXPECT_FALSE(registered_app_path.empty());
EXPECT_TRUE(base::PathExists(app_specific_launcher_path));
EXPECT_EQ(app_specific_launcher_path, registered_app_path);
......@@ -252,22 +413,21 @@ TEST_F(WebAppFileHandlerRegistrationWinTest, AppNameIsReservedFilename) {
// Windows 7 when used as a filename for its launcher.
TEST_F(WebAppFileHandlerRegistrationWinTest, AppNameContainsDot) {
std::set<std::string> file_extensions({"txt"});
AppId app_id("app_id");
// "some.app.name" should become "some_app_name" on Windows 7.
std::string app_name("some.app.name");
base::FilePath app_specific_launcher_path =
CreateDataDirectoryAndGetLauncherPathForApp(
profile(), app_id,
profile(), app_id(),
base::win::GetVersion() > base::win::Version::WIN7 ? "some.app.name"
: "some_app_name");
RegisterFileHandlersWithOs(app_id, app_name, profile(), file_extensions,
RegisterFileHandlersWithOs(app_id(), app_name, profile(), file_extensions,
/*mime_types=*/{});
base::ThreadPoolInstance::Get()->FlushForTesting();
base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
GetProgIdForApp(profile(), app_id));
GetProgIdForApp(profile()->GetPath(), app_id()));
EXPECT_FALSE(registered_app_path.empty());
EXPECT_TRUE(base::PathExists(app_specific_launcher_path));
EXPECT_EQ(app_specific_launcher_path, registered_app_path);
......
......@@ -1541,6 +1541,13 @@ ShellUtil::ShortcutProperties::ShortcutProperties(
ShellUtil::ShortcutProperties::~ShortcutProperties() {
}
ShellUtil::FileAssociationsAndAppName::FileAssociationsAndAppName() = default;
ShellUtil::FileAssociationsAndAppName::FileAssociationsAndAppName(
FileAssociationsAndAppName&& other) = default;
ShellUtil::FileAssociationsAndAppName::~FileAssociationsAndAppName() = default;
bool ShellUtil::QuickIsChromeRegisteredInHKLM(const base::FilePath& chrome_exe,
const base::string16& suffix) {
return QuickIsChromeRegistered(chrome_exe, suffix,
......@@ -2479,6 +2486,43 @@ bool ShellUtil::DeleteFileAssociations(const base::string16& prog_id) {
WorkItem::kWow64Default);
}
// static
ShellUtil::FileAssociationsAndAppName ShellUtil::GetFileAssociationsAndAppName(
const base::string16& prog_id) {
FileAssociationsAndAppName file_associations_and_app_name;
// Get list of handled file extensions from value FileExtensions at
// HKEY_CURRENT_USER\Software\Classes\|prog_id|.
base::string16 prog_id_path(kRegClasses);
prog_id_path.push_back(base::FilePath::kSeparators[0]);
prog_id_path.append(prog_id);
RegKey file_extensions_key(HKEY_CURRENT_USER, prog_id_path.c_str(),
KEY_QUERY_VALUE);
base::string16 handled_file_extensions;
if (file_extensions_key.ReadValue(
L"FileExtensions", &handled_file_extensions) != ERROR_SUCCESS) {
return FileAssociationsAndAppName();
}
std::vector<base::StringPiece16> file_associations_vec =
base::SplitStringPiece(base::StringPiece16(handled_file_extensions),
base::StringPiece16(L";"), base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const auto& file_extension : file_associations_vec) {
// Skip over the leading '.' so that we return the same
// extensions as were passed to AddFileAssociations.
file_associations_and_app_name.file_associations.emplace(
file_extension.substr(1));
}
prog_id_path.append(kRegApplication);
RegKey prog_id_key(HKEY_CURRENT_USER, prog_id_path.c_str(), KEY_QUERY_VALUE);
if (prog_id_key.ReadValue(kRegApplicationName,
&file_associations_and_app_name.app_name) !=
ERROR_SUCCESS) {
return FileAssociationsAndAppName();
}
return file_associations_and_app_name;
}
// static
base::FilePath ShellUtil::GetApplicationPathForProgId(
const base::string16& prog_id) {
......
......@@ -222,6 +222,15 @@ class ShellUtil {
uint32_t options;
};
struct FileAssociationsAndAppName {
FileAssociationsAndAppName();
FileAssociationsAndAppName(FileAssociationsAndAppName&& other);
~FileAssociationsAndAppName();
std::set<base::string16> file_associations;
base::string16 app_name;
};
// Relative path of the URL Protocol registry entry (prefixed with '\').
static const wchar_t* kRegURLProtocol;
......@@ -651,6 +660,12 @@ class ShellUtil {
// with this name will be deleted.
static bool DeleteFileAssociations(const base::string16& prog_id);
// Returns the app name and file associations registered for a particular
// application in the Windows registry. If there is no entry in the registry
// for |prog_id|, nothing will be returned.
static FileAssociationsAndAppName GetFileAssociationsAndAppName(
const base::string16& prog_id);
// Retrieves the file path of the application registered as the
// shell->open->command for |prog_id|. This only queries the user's
// registered applications in HKCU. If |prog_id| is for an app that is
......
......@@ -940,6 +940,22 @@ TEST_F(ShellUtilRegistryTest, DeleteFileAssociations) {
EXPECT_EQ(L"SomeOtherApp", value);
}
TEST_F(ShellUtilRegistryTest, GetFileAssociationsAndAppName) {
ShellUtil::FileAssociationsAndAppName empty_file_associations_and_app_name(
ShellUtil::GetFileAssociationsAndAppName(kTestProgid));
EXPECT_TRUE(empty_file_associations_and_app_name.app_name.empty());
// Add file associations and test that GetFileAssociationsAndAppName returns
// the registered file associations and app name.
ASSERT_TRUE(ShellUtil::AddFileAssociations(
kTestProgid, OpenCommand(), kTestApplicationName, kTestFileTypeName,
base::FilePath(kTestIconPath), FileExtensions()));
ShellUtil::FileAssociationsAndAppName file_associations_and_app_name(
ShellUtil::GetFileAssociationsAndAppName(kTestProgid));
EXPECT_EQ(file_associations_and_app_name.app_name, kTestApplicationName);
EXPECT_EQ(file_associations_and_app_name.file_associations, FileExtensions());
}
TEST_F(ShellUtilRegistryTest, GetApplicationForProgId) {
// Create file associations.
ASSERT_TRUE(ShellUtil::AddFileAssociations(
......
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