1 // Copyright (c) 2012 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.
5 #include "chrome/browser/shell_integration.h"
11 #include "base/base_paths.h"
12 #include "base/file_util.h"
13 #include "base/files/file_path.h"
14 #include "base/files/scoped_temp_dir.h"
15 #include "base/message_loop/message_loop.h"
16 #include "base/stl_util.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/test/scoped_path_override.h"
20 #include "chrome/browser/web_applications/web_app.h"
21 #include "chrome/common/chrome_constants.h"
22 #include "content/public/test/test_browser_thread.h"
23 #include "testing/gmock/include/gmock/gmock.h"
24 #include "testing/gtest/include/gtest/gtest.h"
27 #if defined(OS_POSIX) && !defined(OS_MACOSX)
28 #include "base/environment.h"
29 #include "chrome/browser/shell_integration_linux.h"
32 #define FPL FILE_PATH_LITERAL
34 using content::BrowserThread
;
35 using ::testing::ElementsAre
;
37 #if defined(OS_POSIX) && !defined(OS_MACOSX)
40 // Provides mock environment variables values based on a stored map.
41 class MockEnvironment
: public base::Environment
{
45 void Set(const std::string
& name
, const std::string
& value
) {
46 variables_
[name
] = value
;
49 virtual bool GetVar(const char* variable_name
, std::string
* result
) OVERRIDE
{
50 if (ContainsKey(variables_
, variable_name
)) {
51 *result
= variables_
[variable_name
];
58 virtual bool SetVar(const char* variable_name
,
59 const std::string
& new_value
) OVERRIDE
{
64 virtual bool UnSetVar(const char* variable_name
) OVERRIDE
{
70 std::map
<std::string
, std::string
> variables_
;
72 DISALLOW_COPY_AND_ASSIGN(MockEnvironment
);
77 TEST(ShellIntegrationTest
, GetDataWriteLocation
) {
78 base::MessageLoop message_loop
;
79 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
81 // Test that it returns $XDG_DATA_HOME.
84 env
.Set("HOME", "/home/user");
85 env
.Set("XDG_DATA_HOME", "/user/path");
87 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
88 EXPECT_EQ(base::FilePath("/user/path"), path
);
91 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
94 env
.Set("HOME", "/home/user");
96 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
97 EXPECT_EQ(base::FilePath("/home/user/.local/share"), path
);
100 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it fails.
104 ASSERT_FALSE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
108 TEST(ShellIntegrationTest
, GetDataSearchLocations
) {
109 base::MessageLoop message_loop
;
110 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
112 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
115 env
.Set("HOME", "/home/user");
116 env
.Set("XDG_DATA_HOME", "/user/path");
117 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
119 ShellIntegrationLinux::GetDataSearchLocations(&env
),
120 ElementsAre(base::FilePath("/user/path"),
121 base::FilePath("/system/path/1"),
122 base::FilePath("/system/path/2")));
125 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
128 env
.Set("HOME", "/home/user");
129 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
131 ShellIntegrationLinux::GetDataSearchLocations(&env
),
132 ElementsAre(base::FilePath("/home/user/.local/share"),
133 base::FilePath("/system/path/1"),
134 base::FilePath("/system/path/2")));
137 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
141 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
143 ShellIntegrationLinux::GetDataSearchLocations(&env
),
144 ElementsAre(base::FilePath("/system/path/1"),
145 base::FilePath("/system/path/2")));
148 // Test that $XDG_DATA_DIRS falls back to the two default paths.
151 env
.Set("HOME", "/home/user");
152 env
.Set("XDG_DATA_HOME", "/user/path");
154 ShellIntegrationLinux::GetDataSearchLocations(&env
),
155 ElementsAre(base::FilePath("/user/path"),
156 base::FilePath("/usr/local/share"),
157 base::FilePath("/usr/share")));
161 TEST(ShellIntegrationTest
, GetExistingShortcutLocations
) {
162 base::FilePath
kProfilePath("Profile 1");
163 const char kExtensionId
[] = "test_extension";
164 const char kTemplateFilename
[] = "chrome-test_extension-Profile_1.desktop";
165 base::FilePath
kTemplateFilepath(kTemplateFilename
);
166 const char kNoDisplayDesktopFile
[] = "[Desktop Entry]\nNoDisplay=true";
168 base::MessageLoop message_loop
;
169 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
171 // No existing shortcuts.
174 ShellIntegration::ShortcutLocations result
=
175 ShellIntegrationLinux::GetExistingShortcutLocations(
176 &env
, kProfilePath
, kExtensionId
);
177 EXPECT_FALSE(result
.on_desktop
);
178 EXPECT_FALSE(result
.in_applications_menu
);
179 EXPECT_FALSE(result
.in_quick_launch_bar
);
180 EXPECT_FALSE(result
.hidden
);
183 // Shortcut on desktop.
185 base::ScopedTempDir temp_dir
;
186 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
187 base::FilePath desktop_path
= temp_dir
.path();
190 ASSERT_TRUE(file_util::CreateDirectory(desktop_path
));
191 ASSERT_FALSE(file_util::WriteFile(
192 desktop_path
.AppendASCII(kTemplateFilename
),
194 ShellIntegration::ShortcutLocations result
=
195 ShellIntegrationLinux::GetExistingShortcutLocations(
196 &env
, kProfilePath
, kExtensionId
, desktop_path
);
197 EXPECT_TRUE(result
.on_desktop
);
198 EXPECT_FALSE(result
.in_applications_menu
);
199 EXPECT_FALSE(result
.in_quick_launch_bar
);
200 EXPECT_FALSE(result
.hidden
);
203 // Shortcut in applications directory.
205 base::ScopedTempDir temp_dir
;
206 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
207 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
210 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
211 ASSERT_TRUE(file_util::CreateDirectory(apps_path
));
212 ASSERT_FALSE(file_util::WriteFile(
213 apps_path
.AppendASCII(kTemplateFilename
),
215 ShellIntegration::ShortcutLocations result
=
216 ShellIntegrationLinux::GetExistingShortcutLocations(
217 &env
, kProfilePath
, kExtensionId
);
218 EXPECT_FALSE(result
.on_desktop
);
219 EXPECT_TRUE(result
.in_applications_menu
);
220 EXPECT_FALSE(result
.in_quick_launch_bar
);
221 EXPECT_FALSE(result
.hidden
);
224 // Shortcut in applications directory with NoDisplay=true.
226 base::ScopedTempDir temp_dir
;
227 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
228 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
231 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
232 ASSERT_TRUE(file_util::CreateDirectory(apps_path
));
233 ASSERT_TRUE(file_util::WriteFile(
234 apps_path
.AppendASCII(kTemplateFilename
),
235 kNoDisplayDesktopFile
, strlen(kNoDisplayDesktopFile
)));
236 ShellIntegration::ShortcutLocations result
=
237 ShellIntegrationLinux::GetExistingShortcutLocations(
238 &env
, kProfilePath
, kExtensionId
);
239 // Doesn't count as being in applications menu.
240 EXPECT_FALSE(result
.on_desktop
);
241 EXPECT_FALSE(result
.in_applications_menu
);
242 EXPECT_FALSE(result
.in_quick_launch_bar
);
243 EXPECT_TRUE(result
.hidden
);
246 // Shortcut on desktop and in applications directory.
248 base::ScopedTempDir temp_dir1
;
249 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
250 base::FilePath desktop_path
= temp_dir1
.path();
252 base::ScopedTempDir temp_dir2
;
253 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
254 base::FilePath apps_path
= temp_dir2
.path().AppendASCII("applications");
257 ASSERT_TRUE(file_util::CreateDirectory(desktop_path
));
258 ASSERT_FALSE(file_util::WriteFile(
259 desktop_path
.AppendASCII(kTemplateFilename
),
261 env
.Set("XDG_DATA_HOME", temp_dir2
.path().value());
262 ASSERT_TRUE(file_util::CreateDirectory(apps_path
));
263 ASSERT_FALSE(file_util::WriteFile(
264 apps_path
.AppendASCII(kTemplateFilename
),
266 ShellIntegration::ShortcutLocations result
=
267 ShellIntegrationLinux::GetExistingShortcutLocations(
268 &env
, kProfilePath
, kExtensionId
, desktop_path
);
269 EXPECT_TRUE(result
.on_desktop
);
270 EXPECT_TRUE(result
.in_applications_menu
);
271 EXPECT_FALSE(result
.in_quick_launch_bar
);
272 EXPECT_FALSE(result
.hidden
);
276 TEST(ShellIntegrationTest
, GetExistingShortcutContents
) {
277 const char kTemplateFilename
[] = "shortcut-test.desktop";
278 base::FilePath
kTemplateFilepath(kTemplateFilename
);
279 const char kTestData1
[] = "a magical testing string";
280 const char kTestData2
[] = "a different testing string";
282 base::MessageLoop message_loop
;
283 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
285 // Test that it searches $XDG_DATA_HOME/applications.
287 base::ScopedTempDir temp_dir
;
288 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
291 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
292 // Create a file in a non-applications directory. This should be ignored.
293 ASSERT_TRUE(file_util::WriteFile(
294 temp_dir
.path().AppendASCII(kTemplateFilename
),
295 kTestData2
, strlen(kTestData2
)));
296 ASSERT_TRUE(file_util::CreateDirectory(
297 temp_dir
.path().AppendASCII("applications")));
298 ASSERT_TRUE(file_util::WriteFile(
299 temp_dir
.path().AppendASCII("applications")
300 .AppendASCII(kTemplateFilename
),
301 kTestData1
, strlen(kTestData1
)));
302 std::string contents
;
304 ShellIntegrationLinux::GetExistingShortcutContents(
305 &env
, kTemplateFilepath
, &contents
));
306 EXPECT_EQ(kTestData1
, contents
);
309 // Test that it falls back to $HOME/.local/share/applications.
311 base::ScopedTempDir temp_dir
;
312 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
315 env
.Set("HOME", temp_dir
.path().value());
316 ASSERT_TRUE(file_util::CreateDirectory(
317 temp_dir
.path().AppendASCII(".local/share/applications")));
318 ASSERT_TRUE(file_util::WriteFile(
319 temp_dir
.path().AppendASCII(".local/share/applications")
320 .AppendASCII(kTemplateFilename
),
321 kTestData1
, strlen(kTestData1
)));
322 std::string contents
;
324 ShellIntegrationLinux::GetExistingShortcutContents(
325 &env
, kTemplateFilepath
, &contents
));
326 EXPECT_EQ(kTestData1
, contents
);
329 // Test that it searches $XDG_DATA_DIRS/applications.
331 base::ScopedTempDir temp_dir
;
332 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
335 env
.Set("XDG_DATA_DIRS", temp_dir
.path().value());
336 ASSERT_TRUE(file_util::CreateDirectory(
337 temp_dir
.path().AppendASCII("applications")));
338 ASSERT_TRUE(file_util::WriteFile(
339 temp_dir
.path().AppendASCII("applications")
340 .AppendASCII(kTemplateFilename
),
341 kTestData2
, strlen(kTestData2
)));
342 std::string contents
;
344 ShellIntegrationLinux::GetExistingShortcutContents(
345 &env
, kTemplateFilepath
, &contents
));
346 EXPECT_EQ(kTestData2
, contents
);
349 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
351 base::ScopedTempDir temp_dir1
;
352 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
353 base::ScopedTempDir temp_dir2
;
354 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
357 env
.Set("XDG_DATA_DIRS", temp_dir1
.path().value() + ":" +
358 temp_dir2
.path().value());
359 // Create a file in a non-applications directory. This should be ignored.
360 ASSERT_TRUE(file_util::WriteFile(
361 temp_dir1
.path().AppendASCII(kTemplateFilename
),
362 kTestData1
, strlen(kTestData1
)));
363 // Only create a findable desktop file in the second path.
364 ASSERT_TRUE(file_util::CreateDirectory(
365 temp_dir2
.path().AppendASCII("applications")));
366 ASSERT_TRUE(file_util::WriteFile(
367 temp_dir2
.path().AppendASCII("applications")
368 .AppendASCII(kTemplateFilename
),
369 kTestData2
, strlen(kTestData2
)));
370 std::string contents
;
372 ShellIntegrationLinux::GetExistingShortcutContents(
373 &env
, kTemplateFilepath
, &contents
));
374 EXPECT_EQ(kTestData2
, contents
);
378 TEST(ShellIntegrationTest
, GetExtensionShortcutFilename
) {
379 base::FilePath
kProfilePath("a/b/c/Profile Name?");
380 const char kExtensionId
[] = "extensionid";
381 EXPECT_EQ(base::FilePath("chrome-extensionid-Profile_Name_.desktop"),
382 ShellIntegrationLinux::GetExtensionShortcutFilename(
383 kProfilePath
, kExtensionId
));
386 TEST(ShellIntegrationTest
, GetExistingProfileShortcutFilenames
) {
387 base::FilePath
kProfilePath("a/b/c/Profile Name?");
388 const char kApp1Filename
[] = "chrome-extension1-Profile_Name_.desktop";
389 const char kApp2Filename
[] = "chrome-extension2-Profile_Name_.desktop";
390 const char kUnrelatedAppFilename
[] = "chrome-extension-Other_Profile.desktop";
392 base::MessageLoop message_loop
;
393 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
395 base::ScopedTempDir temp_dir
;
396 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
398 file_util::WriteFile(
399 temp_dir
.path().AppendASCII(kApp1Filename
), "", 0));
401 file_util::WriteFile(
402 temp_dir
.path().AppendASCII(kApp2Filename
), "", 0));
403 // This file should not be returned in the results.
405 file_util::WriteFile(
406 temp_dir
.path().AppendASCII(kUnrelatedAppFilename
), "", 0));
407 std::vector
<base::FilePath
> paths
=
408 ShellIntegrationLinux::GetExistingProfileShortcutFilenames(
409 kProfilePath
, temp_dir
.path());
410 // Path order is arbitrary. Sort the output for consistency.
411 std::sort(paths
.begin(), paths
.end());
413 ElementsAre(base::FilePath(kApp1Filename
),
414 base::FilePath(kApp2Filename
)));
417 TEST(ShellIntegrationTest
, GetWebShortcutFilename
) {
419 const base::FilePath::CharType
* path
;
422 { FPL("http___foo_.desktop"), "http://foo" },
423 { FPL("http___foo_bar_.desktop"), "http://foo/bar/" },
424 { FPL("http___foo_bar_a=b&c=d.desktop"), "http://foo/bar?a=b&c=d" },
426 // Now we're starting to be more evil...
427 { FPL("http___foo_.desktop"), "http://foo/bar/baz/../../../../../" },
428 { FPL("http___foo_.desktop"), "http://foo/bar/././../baz/././../" },
429 { FPL("http___.._.desktop"), "http://../../../../" },
431 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
432 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName
) + "-" +
434 ShellIntegrationLinux::GetWebShortcutFilename(
435 GURL(test_cases
[i
].url
)).value()) <<
436 " while testing " << test_cases
[i
].url
;
440 TEST(ShellIntegrationTest
, GetDesktopFileContents
) {
441 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
445 const char* icon_name
;
447 const char* expected_output
;
450 { "http://gmail.com",
452 "chrome-http__gmail.com",
455 "#!/usr/bin/env xdg-open\n"
461 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
462 "Icon=chrome-http__gmail.com\n"
463 #if !defined(USE_AURA)
464 // Aura Chrome does not (yet) set WMClass, so we only expect
465 // StartupWMClass on non-Aura builds.
466 "StartupWMClass=gmail.com\n"
470 // Make sure that empty icons are replaced by the chrome icon.
471 { "http://gmail.com",
476 "#!/usr/bin/env xdg-open\n"
482 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
483 "Icon=chromium-browser\n"
484 #if !defined(USE_AURA)
485 // Aura Chrome does not (yet) set WMClass, so we only expect
486 // StartupWMClass on non-Aura builds.
487 "StartupWMClass=gmail.com\n"
491 // Test adding NoDisplay=true.
492 { "http://gmail.com",
494 "chrome-http__gmail.com",
497 "#!/usr/bin/env xdg-open\n"
503 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
504 "Icon=chrome-http__gmail.com\n"
506 #if !defined(USE_AURA)
507 // Aura Chrome does not (yet) set WMClass, so we only expect
508 // StartupWMClass on non-Aura builds.
509 "StartupWMClass=gmail.com\n"
513 // Now we're starting to be more evil...
514 { "http://evil.com/evil --join-the-b0tnet",
515 "Ownz0red\nExec=rm -rf /",
516 "chrome-http__evil.com_evil",
519 "#!/usr/bin/env xdg-open\n"
524 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
525 "Exec=/opt/google/chrome/google-chrome "
526 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
527 "Icon=chrome-http__evil.com_evil\n"
528 #if !defined(USE_AURA)
529 // Aura Chrome does not (yet) set WMClass, so we only expect
530 // StartupWMClass on non-Aura builds.
531 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
534 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
536 "chrome-http__evil.com_evil",
539 "#!/usr/bin/env xdg-open\n"
544 "Name=Innocent Title\n"
545 "Exec=/opt/google/chrome/google-chrome "
546 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
547 // Note: $ is escaped as \$ within an arg to Exec, and then
548 // the \ is escaped as \\ as all strings in a Desktop file should
549 // be; finally, \\ becomes \\\\ when represented in a C++ string!
550 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
551 "Icon=chrome-http__evil.com_evil\n"
552 #if !defined(USE_AURA)
553 // Aura Chrome does not (yet) set WMClass, so we only expect
554 // StartupWMClass on non-Aura builds.
555 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
556 "rm%20-rf%20$HOME%20%3Eownz0red\n"
559 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
561 "chrome-http__evil.com_evil",
564 "#!/usr/bin/env xdg-open\n"
569 "Name=Innocent Title\n"
570 "Exec=/opt/google/chrome/google-chrome "
571 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
572 "%60%20%3E/dev/null\n"
573 "Icon=chrome-http__evil.com_evil\n"
574 #if !defined(USE_AURA)
575 // Aura Chrome does not (yet) set WMClass, so we only expect
576 // StartupWMClass on non-Aura builds.
577 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
578 "%60%20%3E_dev_null\n"
583 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
586 test_cases
[i
].expected_output
,
587 ShellIntegrationLinux::GetDesktopFileContents(
589 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
590 GURL(test_cases
[i
].url
),
593 ASCIIToUTF16(test_cases
[i
].title
),
594 test_cases
[i
].icon_name
,
596 test_cases
[i
].nodisplay
));
600 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
603 const char* icon_name
;
604 const char* expected_output
;
617 // Make sure that empty icons are replaced by the chrome icon.
625 "Icon=chromium-browser\n"
629 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
632 test_cases
[i
].expected_output
,
633 ShellIntegrationLinux::GetDirectoryFileContents(
634 ASCIIToUTF16(test_cases
[i
].title
),
635 test_cases
[i
].icon_name
));