1 // Copyright 2019 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_handler_registration_utils_win.h"
6 
7 #include "base/files/file_util.h"
8 #include "base/task/thread_pool/thread_pool_instance.h"
9 #include "base/test/bind.h"
10 #include "base/test/test_reg_util_win.h"
11 #include "base/win/windows_version.h"
12 #include "chrome/browser/profiles/profile_attributes_entry.h"
13 #include "chrome/browser/profiles/profile_attributes_storage.h"
14 #include "chrome/browser/profiles/profile_manager.h"
15 #include "chrome/common/chrome_constants.h"
16 #include "chrome/installer/util/shell_util.h"
17 #include "chrome/test/base/testing_browser_process.h"
18 #include "chrome/test/base/testing_profile.h"
19 #include "chrome/test/base/testing_profile_manager.h"
20 #include "content/public/test/browser_task_environment.h"
21 #include "testing/gtest/include/gtest/gtest.h"
22 
23 namespace web_app {
24 
25 class WebAppHandlerRegistrationUtilsWinTest : public testing::Test {
26  protected:
27   WebAppHandlerRegistrationUtilsWinTest() = default;
28 
SetUp()29   void SetUp() override {
30     // Set up fake windows registry
31     ASSERT_NO_FATAL_FAILURE(
32         registry_override_.OverrideRegistry(HKEY_LOCAL_MACHINE));
33     ASSERT_NO_FATAL_FAILURE(
34         registry_override_.OverrideRegistry(HKEY_CURRENT_USER));
35     testing_profile_manager_ = std::make_unique<TestingProfileManager>(
36         TestingBrowserProcess::GetGlobal());
37     ASSERT_TRUE(testing_profile_manager_->SetUp());
38     profile_ =
39         testing_profile_manager_->CreateTestingProfile(chrome::kInitialProfile);
40   }
TearDown()41   void TearDown() override {
42     profile_ = nullptr;
43     testing_profile_manager_->DeleteAllTestingProfiles();
44   }
45 
profile() const46   Profile* profile() const { return profile_; }
profile_manager() const47   ProfileManager* profile_manager() const {
48     return testing_profile_manager_->profile_manager();
49   }
testing_profile_manager() const50   TestingProfileManager* testing_profile_manager() const {
51     return testing_profile_manager_.get();
52   }
app_id() const53   const AppId& app_id() const { return app_id_; }
app_name() const54   const base::string16& app_name() const { return app_name_; }
55 
56   // Adds a launcher file and OS registry entries for the given app parameters.
RegisterApp(const AppId & app_id,const base::string16 & app_name,const base::string16 & app_name_extension,const base::FilePath & profile_path)57   void RegisterApp(const AppId& app_id,
58                    const base::string16& app_name,
59                    const base::string16& app_name_extension,
60                    const base::FilePath& profile_path) {
61     base::Optional<base::FilePath> launcher_path = CreateAppLauncherFile(
62         app_name, app_name_extension,
63         GetOsIntegrationResourcesDirectoryForApp(profile_path, app_id, GURL()));
64     ASSERT_TRUE(launcher_path.has_value());
65 
66     base::CommandLine launcher_command =
67         GetAppLauncherCommand(app_id, launcher_path.value(), profile_path);
68     base::string16 prog_id = GetProgIdForApp(profile_path, app_id);
69     base::string16 user_visible_app_name(app_name);
70     user_visible_app_name.append(app_name_extension);
71 
72     ASSERT_TRUE(ShellUtil::AddApplicationClass(
73         prog_id, launcher_command, user_visible_app_name, base::string16(),
74         base::FilePath()));
75   }
76 
77   // Tests that an app with |app_id| is registered with the expected name /
78   // extension.
TestRegisteredApp(const AppId & app_id,const base::string16 & expected_app_name,const base::string16 & expected_app_name_extension,const base::FilePath & profile_path)79   void TestRegisteredApp(const AppId& app_id,
80                          const base::string16& expected_app_name,
81                          const base::string16& expected_app_name_extension,
82                          const base::FilePath& profile_path) {
83     // Ensure that the OS registry contains the expected app name.
84     base::string16 expected_user_visible_app_name(app_name());
85     expected_user_visible_app_name.append(expected_app_name_extension);
86     base::string16 app_progid = GetProgIdForApp(profile_path, app_id);
87     ShellUtil::FileAssociationsAndAppName registered_app =
88         ShellUtil::GetFileAssociationsAndAppName(app_progid);
89     EXPECT_EQ(expected_user_visible_app_name, registered_app.app_name);
90 
91     // Ensure that the launcher file contains the expected app name.
92     // On Windows 7 the extension is omitted.
93     base::FilePath expected_launcher_filename =
94         base::win::GetVersion() > base::win::Version::WIN7
95             ? base::FilePath(expected_user_visible_app_name.append(L".exe"))
96             : base::FilePath(expected_user_visible_app_name);
97     base::FilePath registered_launcher_path =
98         ShellUtil::GetApplicationPathForProgId(app_progid);
99     ASSERT_TRUE(base::PathExists(registered_launcher_path));
100     EXPECT_EQ(expected_launcher_filename, registered_launcher_path.BaseName());
101   }
102 
103  private:
104   registry_util::RegistryOverrideManager registry_override_;
105   base::ScopedTempDir temp_version_dir_;
106   content::BrowserTaskEnvironment task_environment_{
107       content::BrowserTaskEnvironment::IO_MAINLOOP};
108   TestingProfile* profile_ = nullptr;
109   std::unique_ptr<TestingProfileManager> testing_profile_manager_;
110   const AppId app_id_ = "app_id";
111   const base::string16 app_name_ = L"app_name";
112 };
113 
TEST_F(WebAppHandlerRegistrationUtilsWinTest,GetAppNameExtensionForNextInstall)114 TEST_F(WebAppHandlerRegistrationUtilsWinTest,
115        GetAppNameExtensionForNextInstall) {
116   // If no installations are present in any profile, the next app name extension
117   // should be an empty string.
118   base::string16 app_name_extension =
119       GetAppNameExtensionForNextInstall(app_id(), profile()->GetPath());
120   EXPECT_EQ(app_name_extension, base::string16());
121 
122   // After registering an app, the next app name should include a
123   // profile-specific extension.
124   RegisterApp(app_id(), app_name(), app_name_extension, profile()->GetPath());
125   Profile* profile2 =
126       testing_profile_manager()->CreateTestingProfile("Profile 2");
127   ProfileAttributesStorage& storage =
128       profile_manager()->GetProfileAttributesStorage();
129   ASSERT_EQ(2u, storage.GetNumberOfProfiles());
130 
131   app_name_extension =
132       GetAppNameExtensionForNextInstall(app_id(), profile2->GetPath());
133   EXPECT_EQ(app_name_extension, L" (Profile 2)");
134 }
135 
136 // Test various attributes of ProgIds returned by GetAppIdForApp.
TEST_F(WebAppHandlerRegistrationUtilsWinTest,GetProgIdForApp)137 TEST_F(WebAppHandlerRegistrationUtilsWinTest, GetProgIdForApp) {
138   // Create a long app_id and verify that the prog id is less
139   // than 39 characters, and only contains alphanumeric characters and
140   // non leading '.'s See
141   // https://docs.microsoft.com/en-us/windows/win32/com/-progid--key.
142   AppId app_id1("app_id12345678901234567890123456789012345678901234");
143   constexpr unsigned int kMaxProgIdLen = 39;
144   base::string16 prog_id1 = GetProgIdForApp(profile()->GetPath(), app_id1);
145   EXPECT_LE(prog_id1.length(), kMaxProgIdLen);
146   for (auto itr = prog_id1.begin(); itr != prog_id1.end(); itr++)
147     EXPECT_TRUE(std::isalnum(*itr) || (*itr == '.' && itr != prog_id1.begin()));
148   AppId app_id2("different_appid");
149   // Check that different app ids in the same profile have different
150   // prog ids.
151   EXPECT_NE(prog_id1, GetProgIdForApp(profile()->GetPath(), app_id2));
152 
153   // Create a different profile, and verify that the prog id for the same
154   // app_id in a different profile is different.
155   TestingProfile profile2;
156   EXPECT_NE(prog_id1, GetProgIdForApp(profile2.GetPath(), app_id1));
157 }
158 
TEST_F(WebAppHandlerRegistrationUtilsWinTest,CheckAndUpdateExternalInstallationsAfterRegistration)159 TEST_F(WebAppHandlerRegistrationUtilsWinTest,
160        CheckAndUpdateExternalInstallationsAfterRegistration) {
161   // Register the same app to profile1 and profile2.
162   Profile* profile1 = profile();
163   RegisterApp(app_id(), app_name(), base::string16(), profile1->GetPath());
164 
165   Profile* profile2 =
166       testing_profile_manager()->CreateTestingProfile("Profile 2");
167 
168   base::string16 app_name_extension(
169       GetAppNameExtensionForNextInstall(app_id(), profile2->GetPath()));
170   RegisterApp(app_id(), app_name(), app_name_extension, profile2->GetPath());
171 
172   // Update installations external to profile 2 (i.e. profile1).
173   CheckAndUpdateExternalInstallations(profile2->GetPath(), app_id());
174   base::ThreadPoolInstance::Get()->FlushForTesting();
175 
176   // Test that the profile1 installation is updated with a profile-specific
177   // name.
178   TestRegisteredApp(
179       app_id(), app_name(),
180       GetAppNameExtensionForNextInstall(app_id(), profile1->GetPath()),
181       profile1->GetPath());
182 }
183 
TEST_F(WebAppHandlerRegistrationUtilsWinTest,CheckAndUpdateExternalInstallationsAfterUnregistration)184 TEST_F(WebAppHandlerRegistrationUtilsWinTest,
185        CheckAndUpdateExternalInstallationsAfterUnregistration) {
186   // Create a profile-specific installation for an app without any duplicate
187   // external installations. This is the state of a profile-specific app that
188   // remains after its external duplicate is unregistered.
189   RegisterApp(app_id(), app_name(), L" (Default)", profile()->GetPath());
190 
191   Profile* profile2 =
192       testing_profile_manager()->CreateTestingProfile("Profile 2");
193   CheckAndUpdateExternalInstallations(profile2->GetPath(), app_id());
194   base::ThreadPoolInstance::Get()->FlushForTesting();
195 
196   // Ensure that after updating from profile2 (which has no installation),
197   // the single app installation is updated with a non profile-specific name.
198   TestRegisteredApp(app_id(), app_name(), base::string16(),
199                     profile()->GetPath());
200 }
201 
TEST_F(WebAppHandlerRegistrationUtilsWinTest,CheckAndUpdateExternalInstallationsWithTwoExternalApps)202 TEST_F(WebAppHandlerRegistrationUtilsWinTest,
203        CheckAndUpdateExternalInstallationsWithTwoExternalApps) {
204   // Register the same profile-specific apps to profile1 and profile2.
205   Profile* profile1 = profile();
206   RegisterApp(app_id(), app_name(), L" (Default)", profile1->GetPath());
207   TestRegisteredApp(app_id(), app_name(), L" (Default)", profile1->GetPath());
208 
209   Profile* profile2 =
210       testing_profile_manager()->CreateTestingProfile("Profile 2");
211   RegisterApp(app_id(), app_name(), L" (Profile 2)", profile2->GetPath());
212   TestRegisteredApp(app_id(), app_name(), L" (Profile 2)", profile2->GetPath());
213 
214   Profile* profile3 =
215       testing_profile_manager()->CreateTestingProfile("Profile 3");
216 
217   // Attempting updates from profile3 when there are already 2 app installations
218   // in other profiles shouldn't change the original 2 installations since they
219   // already have app-specific names.
220   CheckAndUpdateExternalInstallations(profile3->GetPath(), app_id());
221   base::ThreadPoolInstance::Get()->FlushForTesting();
222 
223   TestRegisteredApp(app_id(), app_name(), L" (Default)", profile1->GetPath());
224   TestRegisteredApp(app_id(), app_name(), L" (Profile 2)", profile2->GetPath());
225 }
226 
TEST_F(WebAppHandlerRegistrationUtilsWinTest,CreateAppLauncherFile)227 TEST_F(WebAppHandlerRegistrationUtilsWinTest, CreateAppLauncherFile) {
228   base::string16 app_name_extension = L" extension";
229   base::Optional<base::FilePath> launcher_path =
230       CreateAppLauncherFile(app_name(), app_name_extension,
231                             GetOsIntegrationResourcesDirectoryForApp(
232                                 profile()->GetPath(), app_id(), GURL()));
233   EXPECT_TRUE(launcher_path.has_value());
234   EXPECT_TRUE(base::PathExists(launcher_path.value()));
235 
236   // On Windows 7 the extension is omitted.
237   base::string16 expected_user_visible_app_name(app_name());
238   expected_user_visible_app_name.append(app_name_extension);
239   base::FilePath expected_launcher_filename =
240       base::win::GetVersion() > base::win::Version::WIN7
241           ? base::FilePath(expected_user_visible_app_name.append(L".exe"))
242           : base::FilePath(expected_user_visible_app_name);
243   EXPECT_EQ(launcher_path.value().BaseName(), expected_launcher_filename);
244 }
245 
246 // Test that invalid file name characters in app_name are replaced with '_'.
TEST_F(WebAppHandlerRegistrationUtilsWinTest,AppNameWithInvalidChars)247 TEST_F(WebAppHandlerRegistrationUtilsWinTest, AppNameWithInvalidChars) {
248   // '*' is an invalid char in Windows file names, so it should be replaced
249   // with '_'.
250   base::string16 app_name = L"app*name";
251   // On Windows 7 the extension is omitted.
252   base::FilePath expected_launcher_name =
253       base::win::GetVersion() > base::win::Version::WIN7
254           ? base::FilePath(L"app_name.exe")
255           : base::FilePath(L"app_name");
256   EXPECT_EQ(GetAppSpecificLauncherFilename(app_name), expected_launcher_name);
257 }
258 
259 // Test that an app name that is a reserved filename on Windows has '_'
260 // prepended to it when used as a filename for its launcher.
TEST_F(WebAppHandlerRegistrationUtilsWinTest,AppNameIsReservedFilename)261 TEST_F(WebAppHandlerRegistrationUtilsWinTest, AppNameIsReservedFilename) {
262   // "con" is a reserved filename on Windows, so it should have '_' prepended.
263   base::string16 app_name = L"con";
264   // On Windows 7 the extension is omitted.
265   base::FilePath expected_launcher_name =
266       base::win::GetVersion() > base::win::Version::WIN7
267           ? base::FilePath(L"_con.exe")
268           : base::FilePath(L"_con");
269   EXPECT_EQ(GetAppSpecificLauncherFilename(app_name), expected_launcher_name);
270 }
271 
272 // Test that an app name containing '.' characters has them replaced with '_' on
273 // Windows 7 when used as a filename for its launcher.
TEST_F(WebAppHandlerRegistrationUtilsWinTest,AppNameContainsDot)274 TEST_F(WebAppHandlerRegistrationUtilsWinTest, AppNameContainsDot) {
275   base::string16 app_name = L"some.app.name";
276 
277   // "some.app.name" should become "some_app_name" on Windows 7 and the
278   // extension is also omitted.
279   base::FilePath expected_launcher_name =
280       base::win::GetVersion() > base::win::Version::WIN7
281           ? base::FilePath(L"some.app.name.exe")
282           : base::FilePath(L"some_app_name");
283   EXPECT_EQ(GetAppSpecificLauncherFilename(app_name), expected_launcher_name);
284 }
285 
286 }  // namespace web_app
287