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