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