1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/web_applications/components/web_app_file_handler_registration.h"
6
7 #include <set>
8 #include <string>
9 #include <vector>
10
11 #include "base/files/file_util.h"
12 #include "base/files/scoped_temp_dir.h"
13 #include "base/path_service.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/task/thread_pool/thread_pool_instance.h"
16 #include "base/test/test_reg_util_win.h"
17 #include "base/test/test_timeouts.h"
18 #include "base/win/windows_version.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/profiles/profile_attributes_storage.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "chrome/browser/web_applications/chrome_pwa_launcher/chrome_pwa_launcher_util.h"
23 #include "chrome/browser/web_applications/components/web_app_handler_registration_utils_win.h"
24 #include "chrome/browser/web_applications/test/test_file_handler_manager.h"
25 #include "chrome/common/chrome_constants.h"
26 #include "chrome/installer/util/shell_util.h"
27 #include "chrome/test/base/testing_browser_process.h"
28 #include "chrome/test/base/testing_profile.h"
29 #include "chrome/test/base/testing_profile_manager.h"
30 #include "components/services/app_service/public/cpp/file_handler.h"
31 #include "content/public/test/browser_task_environment.h"
32 #include "testing/gtest/include/gtest/gtest.h"
33
34 namespace {
35
GetFileHandlersWithFileExtensions(const std::vector<std::string> & file_extensions)36 apps::FileHandlers GetFileHandlersWithFileExtensions(
37 const std::vector<std::string>& file_extensions) {
38 apps::FileHandlers file_handlers;
39 for (const auto& file_extension : file_extensions) {
40 apps::FileHandler file_handler;
41 apps::FileHandler::AcceptEntry accept_entry;
42 accept_entry.file_extensions.insert(file_extension);
43 file_handler.accept.push_back(accept_entry);
44 file_handlers.push_back(file_handler);
45 }
46 return file_handlers;
47 }
48
49 } // namespace
50
51 namespace web_app {
52
53 constexpr char kAppName[] = "app name";
54
55 class WebAppFileHandlerRegistrationWinTest : public testing::Test {
56 protected:
WebAppFileHandlerRegistrationWinTest()57 WebAppFileHandlerRegistrationWinTest() {}
58
SetUp()59 void SetUp() override {
60 // Set up fake windows registry
61 ASSERT_NO_FATAL_FAILURE(
62 registry_override_.OverrideRegistry(HKEY_LOCAL_MACHINE));
63 ASSERT_NO_FATAL_FAILURE(
64 registry_override_.OverrideRegistry(HKEY_CURRENT_USER));
65 testing_profile_manager_ = std::make_unique<TestingProfileManager>(
66 TestingBrowserProcess::GetGlobal());
67 ASSERT_TRUE(testing_profile_manager_->SetUp());
68 profile_ =
69 testing_profile_manager_->CreateTestingProfile(chrome::kInitialProfile);
70 }
TearDown()71 void TearDown() override {
72 profile_ = nullptr;
73 testing_profile_manager_->DeleteAllTestingProfiles();
74 }
75
profile()76 Profile* profile() { return profile_; }
profile_manager()77 ProfileManager* profile_manager() {
78 return testing_profile_manager_->profile_manager();
79 }
testing_profile_manager()80 TestingProfileManager* testing_profile_manager() {
81 return testing_profile_manager_.get();
82 }
app_id() const83 const AppId& app_id() const { return app_id_; }
84
85 // Returns true if Chrome extension with AppId |app_id| has its corresponding
86 // prog_id registered in Windows registry to handle files with extension
87 // |file_ext|, false otherwise.
ProgIdRegisteredForFileExtension(const std::string & file_ext,const AppId & app_id,Profile * profile)88 bool ProgIdRegisteredForFileExtension(const std::string& file_ext,
89 const AppId& app_id,
90 Profile* profile) {
91 base::string16 key_name(ShellUtil::kRegClasses);
92 key_name.push_back(base::FilePath::kSeparators[0]);
93 key_name.append(base::UTF8ToUTF16(file_ext));
94 key_name.push_back(base::FilePath::kSeparators[0]);
95 key_name.append(ShellUtil::kRegOpenWithProgids);
96 base::win::RegKey key;
97 std::wstring value;
98 EXPECT_EQ(ERROR_SUCCESS,
99 key.Open(HKEY_CURRENT_USER, key_name.c_str(), KEY_READ));
100 base::string16 prog_id = GetProgIdForApp(profile->GetPath(), app_id);
101 return key.ReadValue(prog_id.c_str(), &value) == ERROR_SUCCESS &&
102 value == L"";
103 }
104
AddAndVerifyFileAssociations(Profile * profile,const std::string & app_name,const char * app_name_extension)105 void AddAndVerifyFileAssociations(Profile* profile,
106 const std::string& app_name,
107 const char* app_name_extension) {
108 std::string sanitized_app_name(app_name);
109 sanitized_app_name.append(app_name_extension);
110 base::FilePath expected_app_launcher_path =
111 GetLauncherPathForApp(profile, app_id(), sanitized_app_name);
112 apps::FileHandlers file_handlers =
113 GetFileHandlersWithFileExtensions({".txt", ".doc"});
114
115 RegisterFileHandlersWithOs(app_id(), app_name, profile, file_handlers);
116
117 base::ThreadPoolInstance::Get()->FlushForTesting();
118 base::RunLoop().RunUntilIdle();
119 base::ThreadPoolInstance::Get()->FlushForTesting();
120
121 base::FilePath registered_app_path = ShellUtil::GetApplicationPathForProgId(
122 GetProgIdForApp(profile->GetPath(), app_id()));
123 EXPECT_TRUE(base::PathExists(registered_app_path));
124 EXPECT_EQ(registered_app_path, expected_app_launcher_path);
125 // .txt and .doc should have |app_name| in their Open With lists.
126 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile));
127 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile));
128 }
129
130 // Gets the launcher file path for |sanitized_app_name|. If not
131 // on Win7, the name will have the ".exe" extension.
GetAppSpecificLauncherFilePath(const std::string & sanitized_app_name)132 base::FilePath GetAppSpecificLauncherFilePath(
133 const std::string& sanitized_app_name) {
134 base::FilePath app_specific_launcher_filepath(
135 base::ASCIIToUTF16(sanitized_app_name));
136 if (base::win::GetVersion() > base::win::Version::WIN7) {
137 app_specific_launcher_filepath =
138 app_specific_launcher_filepath.AddExtension(L"exe");
139 }
140 return app_specific_launcher_filepath;
141 }
142
143 // Returns the expected app launcher path inside the subdirectory for
144 // |app_id|.
GetLauncherPathForApp(Profile * profile,const AppId app_id,const std::string & sanitized_app_name)145 base::FilePath GetLauncherPathForApp(Profile* profile,
146 const AppId app_id,
147 const std::string& sanitized_app_name) {
148 base::FilePath web_app_dir(GetOsIntegrationResourcesDirectoryForApp(
149 profile->GetPath(), app_id, GURL()));
150 base::FilePath app_specific_launcher_filepath =
151 GetAppSpecificLauncherFilePath(sanitized_app_name);
152
153 return web_app_dir.Append(app_specific_launcher_filepath);
154 }
155
156 private:
157 registry_util::RegistryOverrideManager registry_override_;
158 base::ScopedTempDir temp_version_dir_;
159 content::BrowserTaskEnvironment task_environment_{
160 content::BrowserTaskEnvironment::IO_MAINLOOP};
161 TestingProfile* profile_ = nullptr;
162 std::unique_ptr<TestingProfileManager> testing_profile_manager_;
163 const AppId app_id_ = "app_id";
164 };
165
166
TEST_F(WebAppFileHandlerRegistrationWinTest,RegisterFileHandlersForWebApp)167 TEST_F(WebAppFileHandlerRegistrationWinTest, RegisterFileHandlersForWebApp) {
168 AddAndVerifyFileAssociations(profile(), kAppName, "");
169 }
170
171 // When an app is registered in one profile, and then is registered in a second
172 // profile, the open with context menu items for both app registrations should
173 // include the profile name, e.g., "app name (Default)" and "app name (Profile
174 // 2)".
TEST_F(WebAppFileHandlerRegistrationWinTest,RegisterFileHandlersForWebAppIn2Profiles)175 TEST_F(WebAppFileHandlerRegistrationWinTest,
176 RegisterFileHandlersForWebAppIn2Profiles) {
177 AddAndVerifyFileAssociations(profile(), kAppName, "");
178
179 Profile* profile2 =
180 testing_profile_manager()->CreateTestingProfile("Profile 2");
181 ProfileAttributesStorage& storage =
182 profile_manager()->GetProfileAttributesStorage();
183 ASSERT_EQ(2u, storage.GetNumberOfProfiles());
184 AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
185
186 ShellUtil::FileAssociationsAndAppName file_associations_and_app_name(
187 ShellUtil::GetFileAssociationsAndAppName(
188 GetProgIdForApp(profile()->GetPath(), app_id())));
189 ASSERT_FALSE(file_associations_and_app_name.app_name.empty());
190 // Profile 1's app name should now include the profile in the name.
191 std::string app_name_str =
192 base::UTF16ToUTF8(file_associations_and_app_name.app_name);
193 EXPECT_EQ(app_name_str, "app name (Default)");
194 // Profile 1's app_launcher should include the profile in its name.
195 base::FilePath profile1_app_specific_launcher_path =
196 GetAppSpecificLauncherFilePath("app name (Default)");
197 base::FilePath profile1_launcher_path =
198 ShellUtil::GetApplicationPathForProgId(
199 GetProgIdForApp(profile()->GetPath(), app_id()));
200 EXPECT_EQ(profile1_launcher_path.BaseName(),
201 profile1_app_specific_launcher_path);
202 // Verify that the app is still registered for ".txt" and ".doc" in profile 1.
203 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile()));
204 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile()));
205 }
206
207 // Test that we don't use the gaia name in the file association app name, but
208 // rather, just the local profile name.
TEST_F(WebAppFileHandlerRegistrationWinTest,RegisterFileHandlersForWebAppIn2ProfilesWithGaiaName)209 TEST_F(WebAppFileHandlerRegistrationWinTest,
210 RegisterFileHandlersForWebAppIn2ProfilesWithGaiaName) {
211 AddAndVerifyFileAssociations(profile(), kAppName, "");
212
213 Profile* profile2 =
214 testing_profile_manager()->CreateTestingProfile("Profile 2");
215 ProfileAttributesStorage& storage =
216 profile_manager()->GetProfileAttributesStorage();
217 ProfileAttributesEntry* entry;
218 ASSERT_TRUE(
219 storage.GetProfileAttributesWithPath(profile2->GetPath(), &entry));
220 entry->SetGAIAName(base::ASCIIToUTF16("gaia user"));
221 AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
222 }
223
224 // When an app is registered in two profiles, and then unregistered in one of
225 // them, the remaining registration should no longer be profile-specific. It
226 // should not have the profile name in app_launcher executable name, or the
227 // registered app name.
TEST_F(WebAppFileHandlerRegistrationWinTest,UnRegisterFileHandlersForWebAppIn2Profiles)228 TEST_F(WebAppFileHandlerRegistrationWinTest,
229 UnRegisterFileHandlersForWebAppIn2Profiles) {
230 AddAndVerifyFileAssociations(profile(), kAppName, "");
231 base::FilePath app_specific_launcher_path =
232 ShellUtil::GetApplicationPathForProgId(
233 GetProgIdForApp(profile()->GetPath(), app_id()));
234
235 Profile* profile2 =
236 testing_profile_manager()->CreateTestingProfile("Profile 2");
237 ProfileAttributesStorage& storage =
238 profile_manager()->GetProfileAttributesStorage();
239 ASSERT_EQ(2u, storage.GetNumberOfProfiles());
240 AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
241
242 UnregisterFileHandlersWithOs(app_id(), profile());
243 base::ThreadPoolInstance::Get()->FlushForTesting();
244 base::RunLoop().RunUntilIdle();
245 base::ThreadPoolInstance::Get()->FlushForTesting();
246 EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
247 // Verify that "(Profile 2)" was removed from the web app launcher and
248 // file association registry entries.
249 ShellUtil::FileAssociationsAndAppName file_associations_and_app_name =
250 ShellUtil::GetFileAssociationsAndAppName(
251 GetProgIdForApp(profile2->GetPath(), app_id()));
252 ASSERT_FALSE(file_associations_and_app_name.app_name.empty());
253 // Profile 2's app name should no longer include the profile in the name.
254 std::string app_name_str =
255 base::UTF16ToUTF8(file_associations_and_app_name.app_name);
256 EXPECT_EQ(app_name_str, kAppName);
257 // Profile 2's app_launcher should no longer include the profile in its name.
258 base::FilePath profile2_app_specific_launcher_path =
259 GetAppSpecificLauncherFilePath(kAppName);
260 base::FilePath profile2_launcher_path =
261 ShellUtil::GetApplicationPathForProgId(
262 GetProgIdForApp(profile2->GetPath(), app_id()));
263 EXPECT_EQ(profile2_launcher_path.BaseName(),
264 profile2_app_specific_launcher_path);
265 // Verify that the app is still registered for ".txt" and ".doc" in profile 2.
266 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile2));
267 EXPECT_TRUE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile2));
268 }
269
270 // When an app is registered in three profiles, and then unregistered in one of
271 // them, the remaining registrations should not change.
TEST_F(WebAppFileHandlerRegistrationWinTest,UnRegisterFileHandlersForWebAppIn3Profiles)272 TEST_F(WebAppFileHandlerRegistrationWinTest,
273 UnRegisterFileHandlersForWebAppIn3Profiles) {
274 AddAndVerifyFileAssociations(profile(), kAppName, "");
275 base::FilePath app_specific_launcher_path =
276 ShellUtil::GetApplicationPathForProgId(
277 GetProgIdForApp(profile()->GetPath(), app_id()));
278
279 Profile* profile2 =
280 testing_profile_manager()->CreateTestingProfile("Profile 2");
281 ProfileAttributesStorage& storage =
282 profile_manager()->GetProfileAttributesStorage();
283 ASSERT_EQ(2u, storage.GetNumberOfProfiles());
284 AddAndVerifyFileAssociations(profile2, kAppName, " (Profile 2)");
285
286 Profile* profile3 =
287 testing_profile_manager()->CreateTestingProfile("Profile 3");
288 ASSERT_EQ(3u, storage.GetNumberOfProfiles());
289 AddAndVerifyFileAssociations(profile3, kAppName, " (Profile 3)");
290
291 UnregisterFileHandlersWithOs(app_id(), profile());
292 base::ThreadPoolInstance::Get()->FlushForTesting();
293 base::RunLoop().RunUntilIdle();
294 base::ThreadPoolInstance::Get()->FlushForTesting();
295 EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
296 // Verify that "(Profile 2)" was not removed from the web app launcher and
297 // file association registry entries.
298 ShellUtil::FileAssociationsAndAppName file_associations_and_app_name2(
299 ShellUtil::GetFileAssociationsAndAppName(
300 GetProgIdForApp(profile2->GetPath(), app_id())));
301 ASSERT_FALSE(file_associations_and_app_name2.app_name.empty());
302 // Profile 2's app name should still include the profile name in its name.
303 std::string app_name_str =
304 base::UTF16ToUTF8(file_associations_and_app_name2.app_name);
305 EXPECT_EQ(app_name_str, "app name (Profile 2)");
306
307 // Profile 3's app name should still include the profile name in its name.
308 ShellUtil::FileAssociationsAndAppName file_associations_and_app_name3(
309 ShellUtil::GetFileAssociationsAndAppName(
310 GetProgIdForApp(profile3->GetPath(), app_id())));
311 ASSERT_FALSE(file_associations_and_app_name3.app_name.empty());
312 // Profile 2's app name should still include the profile in the name.
313 app_name_str = base::UTF16ToUTF8(file_associations_and_app_name3.app_name);
314 EXPECT_EQ(app_name_str, "app name (Profile 3)");
315 }
316
TEST_F(WebAppFileHandlerRegistrationWinTest,UnregisterFileHandlersForWebApp)317 TEST_F(WebAppFileHandlerRegistrationWinTest, UnregisterFileHandlersForWebApp) {
318 // Register file handlers, and then verify that unregistering removes
319 // the registry settings and the app-specific launcher.
320 AddAndVerifyFileAssociations(profile(), kAppName, "");
321 base::FilePath app_specific_launcher_path =
322 ShellUtil::GetApplicationPathForProgId(
323 GetProgIdForApp(profile()->GetPath(), app_id()));
324
325 UnregisterFileHandlersWithOs(app_id(), profile());
326 base::ThreadPoolInstance::Get()->FlushForTesting();
327 base::RunLoop().RunUntilIdle();
328 EXPECT_FALSE(base::PathExists(app_specific_launcher_path));
329 EXPECT_FALSE(ProgIdRegisteredForFileExtension(".txt", app_id(), profile()));
330 EXPECT_FALSE(ProgIdRegisteredForFileExtension(".doc", app_id(), profile()));
331
332 ShellUtil::FileAssociationsAndAppName file_associations_and_app_name =
333 ShellUtil::GetFileAssociationsAndAppName(
334 GetProgIdForApp(profile()->GetPath(), app_id()));
335 EXPECT_TRUE(file_associations_and_app_name.app_name.empty());
336 }
337
338 } // namespace web_app
339