1 // Copyright 2013 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_linux.h"
11 #include "base/base_paths.h"
12 #include "base/command_line.h"
13 #include "base/environment.h"
14 #include "base/file_util.h"
15 #include "base/files/file_path.h"
16 #include "base/files/scoped_temp_dir.h"
17 #include "base/message_loop/message_loop.h"
18 #include "base/stl_util.h"
19 #include "base/strings/string_util.h"
20 #include "base/strings/utf_string_conversions.h"
21 #include "base/test/scoped_path_override.h"
22 #include "chrome/common/chrome_constants.h"
23 #include "content/public/test/test_browser_thread.h"
24 #include "testing/gmock/include/gmock/gmock.h"
25 #include "testing/gtest/include/gtest/gtest.h"
28 #define FPL FILE_PATH_LITERAL
30 using content::BrowserThread
;
31 using ::testing::ElementsAre
;
35 // Provides mock environment variables values based on a stored map.
36 class MockEnvironment
: public base::Environment
{
40 void Set(const std::string
& name
, const std::string
& value
) {
41 variables_
[name
] = value
;
44 virtual bool GetVar(const char* variable_name
, std::string
* result
) OVERRIDE
{
45 if (ContainsKey(variables_
, variable_name
)) {
46 *result
= variables_
[variable_name
];
53 virtual bool SetVar(const char* variable_name
,
54 const std::string
& new_value
) OVERRIDE
{
59 virtual bool UnSetVar(const char* variable_name
) OVERRIDE
{
65 std::map
<std::string
, std::string
> variables_
;
67 DISALLOW_COPY_AND_ASSIGN(MockEnvironment
);
72 TEST(ShellIntegrationTest
, GetDataWriteLocation
) {
73 base::MessageLoop message_loop
;
74 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
76 // Test that it returns $XDG_DATA_HOME.
79 env
.Set("HOME", "/home/user");
80 env
.Set("XDG_DATA_HOME", "/user/path");
82 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
83 EXPECT_EQ(base::FilePath("/user/path"), path
);
86 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
89 env
.Set("HOME", "/home/user");
91 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
92 EXPECT_EQ(base::FilePath("/home/user/.local/share"), path
);
95 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it fails.
99 ASSERT_FALSE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
103 TEST(ShellIntegrationTest
, GetDataSearchLocations
) {
104 base::MessageLoop message_loop
;
105 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
107 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
110 env
.Set("HOME", "/home/user");
111 env
.Set("XDG_DATA_HOME", "/user/path");
112 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
114 ShellIntegrationLinux::GetDataSearchLocations(&env
),
115 ElementsAre(base::FilePath("/user/path"),
116 base::FilePath("/system/path/1"),
117 base::FilePath("/system/path/2")));
120 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
123 env
.Set("HOME", "/home/user");
124 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
126 ShellIntegrationLinux::GetDataSearchLocations(&env
),
127 ElementsAre(base::FilePath("/home/user/.local/share"),
128 base::FilePath("/system/path/1"),
129 base::FilePath("/system/path/2")));
132 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
136 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
138 ShellIntegrationLinux::GetDataSearchLocations(&env
),
139 ElementsAre(base::FilePath("/system/path/1"),
140 base::FilePath("/system/path/2")));
143 // Test that $XDG_DATA_DIRS falls back to the two default paths.
146 env
.Set("HOME", "/home/user");
147 env
.Set("XDG_DATA_HOME", "/user/path");
149 ShellIntegrationLinux::GetDataSearchLocations(&env
),
150 ElementsAre(base::FilePath("/user/path"),
151 base::FilePath("/usr/local/share"),
152 base::FilePath("/usr/share")));
156 TEST(ShellIntegrationTest
, GetExistingShortcutLocations
) {
157 base::FilePath
kProfilePath("Profile 1");
158 const char kExtensionId
[] = "test_extension";
159 const char kTemplateFilename
[] = "chrome-test_extension-Profile_1.desktop";
160 base::FilePath
kTemplateFilepath(kTemplateFilename
);
161 const char kNoDisplayDesktopFile
[] = "[Desktop Entry]\nNoDisplay=true";
163 base::MessageLoop message_loop
;
164 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
166 // No existing shortcuts.
169 web_app::ShortcutLocations result
=
170 ShellIntegrationLinux::GetExistingShortcutLocations(
171 &env
, kProfilePath
, kExtensionId
);
172 EXPECT_FALSE(result
.on_desktop
);
173 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
174 result
.applications_menu_location
);
176 EXPECT_FALSE(result
.in_quick_launch_bar
);
177 EXPECT_FALSE(result
.hidden
);
180 // Shortcut on desktop.
182 base::ScopedTempDir temp_dir
;
183 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
184 base::FilePath desktop_path
= temp_dir
.path();
187 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
188 ASSERT_FALSE(base::WriteFile(
189 desktop_path
.AppendASCII(kTemplateFilename
),
191 web_app::ShortcutLocations result
=
192 ShellIntegrationLinux::GetExistingShortcutLocations(
193 &env
, kProfilePath
, kExtensionId
, desktop_path
);
194 EXPECT_TRUE(result
.on_desktop
);
195 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
196 result
.applications_menu_location
);
198 EXPECT_FALSE(result
.in_quick_launch_bar
);
199 EXPECT_FALSE(result
.hidden
);
202 // Shortcut in applications directory.
204 base::ScopedTempDir temp_dir
;
205 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
206 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
209 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
210 ASSERT_TRUE(base::CreateDirectory(apps_path
));
211 ASSERT_FALSE(base::WriteFile(
212 apps_path
.AppendASCII(kTemplateFilename
),
214 web_app::ShortcutLocations result
=
215 ShellIntegrationLinux::GetExistingShortcutLocations(
216 &env
, kProfilePath
, kExtensionId
);
217 EXPECT_FALSE(result
.on_desktop
);
218 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
219 result
.applications_menu_location
);
221 EXPECT_FALSE(result
.in_quick_launch_bar
);
222 EXPECT_FALSE(result
.hidden
);
225 // Shortcut in applications directory with NoDisplay=true.
227 base::ScopedTempDir temp_dir
;
228 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
229 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
232 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
233 ASSERT_TRUE(base::CreateDirectory(apps_path
));
234 ASSERT_TRUE(base::WriteFile(
235 apps_path
.AppendASCII(kTemplateFilename
),
236 kNoDisplayDesktopFile
, strlen(kNoDisplayDesktopFile
)));
237 web_app::ShortcutLocations result
=
238 ShellIntegrationLinux::GetExistingShortcutLocations(
239 &env
, kProfilePath
, kExtensionId
);
240 // Doesn't count as being in applications menu.
241 EXPECT_FALSE(result
.on_desktop
);
242 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
243 result
.applications_menu_location
);
244 EXPECT_FALSE(result
.in_quick_launch_bar
);
245 EXPECT_TRUE(result
.hidden
);
248 // Shortcut on desktop and in applications directory.
250 base::ScopedTempDir temp_dir1
;
251 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
252 base::FilePath desktop_path
= temp_dir1
.path();
254 base::ScopedTempDir temp_dir2
;
255 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
256 base::FilePath apps_path
= temp_dir2
.path().AppendASCII("applications");
259 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
260 ASSERT_FALSE(base::WriteFile(
261 desktop_path
.AppendASCII(kTemplateFilename
),
263 env
.Set("XDG_DATA_HOME", temp_dir2
.path().value());
264 ASSERT_TRUE(base::CreateDirectory(apps_path
));
265 ASSERT_FALSE(base::WriteFile(
266 apps_path
.AppendASCII(kTemplateFilename
),
268 web_app::ShortcutLocations result
=
269 ShellIntegrationLinux::GetExistingShortcutLocations(
270 &env
, kProfilePath
, kExtensionId
, desktop_path
);
271 EXPECT_TRUE(result
.on_desktop
);
272 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
273 result
.applications_menu_location
);
274 EXPECT_FALSE(result
.in_quick_launch_bar
);
275 EXPECT_FALSE(result
.hidden
);
279 TEST(ShellIntegrationTest
, GetExistingShortcutContents
) {
280 const char kTemplateFilename
[] = "shortcut-test.desktop";
281 base::FilePath
kTemplateFilepath(kTemplateFilename
);
282 const char kTestData1
[] = "a magical testing string";
283 const char kTestData2
[] = "a different testing string";
285 base::MessageLoop message_loop
;
286 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
288 // Test that it searches $XDG_DATA_HOME/applications.
290 base::ScopedTempDir temp_dir
;
291 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
294 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
295 // Create a file in a non-applications directory. This should be ignored.
296 ASSERT_TRUE(base::WriteFile(
297 temp_dir
.path().AppendASCII(kTemplateFilename
),
298 kTestData2
, strlen(kTestData2
)));
299 ASSERT_TRUE(base::CreateDirectory(
300 temp_dir
.path().AppendASCII("applications")));
301 ASSERT_TRUE(base::WriteFile(
302 temp_dir
.path().AppendASCII("applications")
303 .AppendASCII(kTemplateFilename
),
304 kTestData1
, strlen(kTestData1
)));
305 std::string contents
;
307 ShellIntegrationLinux::GetExistingShortcutContents(
308 &env
, kTemplateFilepath
, &contents
));
309 EXPECT_EQ(kTestData1
, contents
);
312 // Test that it falls back to $HOME/.local/share/applications.
314 base::ScopedTempDir temp_dir
;
315 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
318 env
.Set("HOME", temp_dir
.path().value());
319 ASSERT_TRUE(base::CreateDirectory(
320 temp_dir
.path().AppendASCII(".local/share/applications")));
321 ASSERT_TRUE(base::WriteFile(
322 temp_dir
.path().AppendASCII(".local/share/applications")
323 .AppendASCII(kTemplateFilename
),
324 kTestData1
, strlen(kTestData1
)));
325 std::string contents
;
327 ShellIntegrationLinux::GetExistingShortcutContents(
328 &env
, kTemplateFilepath
, &contents
));
329 EXPECT_EQ(kTestData1
, contents
);
332 // Test that it searches $XDG_DATA_DIRS/applications.
334 base::ScopedTempDir temp_dir
;
335 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
338 env
.Set("XDG_DATA_DIRS", temp_dir
.path().value());
339 ASSERT_TRUE(base::CreateDirectory(
340 temp_dir
.path().AppendASCII("applications")));
341 ASSERT_TRUE(base::WriteFile(
342 temp_dir
.path().AppendASCII("applications")
343 .AppendASCII(kTemplateFilename
),
344 kTestData2
, strlen(kTestData2
)));
345 std::string contents
;
347 ShellIntegrationLinux::GetExistingShortcutContents(
348 &env
, kTemplateFilepath
, &contents
));
349 EXPECT_EQ(kTestData2
, contents
);
352 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
354 base::ScopedTempDir temp_dir1
;
355 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
356 base::ScopedTempDir temp_dir2
;
357 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
360 env
.Set("XDG_DATA_DIRS", temp_dir1
.path().value() + ":" +
361 temp_dir2
.path().value());
362 // Create a file in a non-applications directory. This should be ignored.
363 ASSERT_TRUE(base::WriteFile(
364 temp_dir1
.path().AppendASCII(kTemplateFilename
),
365 kTestData1
, strlen(kTestData1
)));
366 // Only create a findable desktop file in the second path.
367 ASSERT_TRUE(base::CreateDirectory(
368 temp_dir2
.path().AppendASCII("applications")));
369 ASSERT_TRUE(base::WriteFile(
370 temp_dir2
.path().AppendASCII("applications")
371 .AppendASCII(kTemplateFilename
),
372 kTestData2
, strlen(kTestData2
)));
373 std::string contents
;
375 ShellIntegrationLinux::GetExistingShortcutContents(
376 &env
, kTemplateFilepath
, &contents
));
377 EXPECT_EQ(kTestData2
, contents
);
381 TEST(ShellIntegrationTest
, GetExtensionShortcutFilename
) {
382 base::FilePath
kProfilePath("a/b/c/Profile Name?");
383 const char kExtensionId
[] = "extensionid";
384 EXPECT_EQ(base::FilePath("chrome-extensionid-Profile_Name_.desktop"),
385 ShellIntegrationLinux::GetExtensionShortcutFilename(
386 kProfilePath
, kExtensionId
));
389 TEST(ShellIntegrationTest
, GetExistingProfileShortcutFilenames
) {
390 base::FilePath
kProfilePath("a/b/c/Profile Name?");
391 const char kApp1Filename
[] = "chrome-extension1-Profile_Name_.desktop";
392 const char kApp2Filename
[] = "chrome-extension2-Profile_Name_.desktop";
393 const char kUnrelatedAppFilename
[] = "chrome-extension-Other_Profile.desktop";
395 base::MessageLoop message_loop
;
396 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
398 base::ScopedTempDir temp_dir
;
399 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
402 temp_dir
.path().AppendASCII(kApp1Filename
), "", 0));
405 temp_dir
.path().AppendASCII(kApp2Filename
), "", 0));
406 // This file should not be returned in the results.
409 temp_dir
.path().AppendASCII(kUnrelatedAppFilename
), "", 0));
410 std::vector
<base::FilePath
> paths
=
411 ShellIntegrationLinux::GetExistingProfileShortcutFilenames(
412 kProfilePath
, temp_dir
.path());
413 // Path order is arbitrary. Sort the output for consistency.
414 std::sort(paths
.begin(), paths
.end());
416 ElementsAre(base::FilePath(kApp1Filename
),
417 base::FilePath(kApp2Filename
)));
420 TEST(ShellIntegrationTest
, GetWebShortcutFilename
) {
422 const base::FilePath::CharType
* path
;
425 { FPL("http___foo_.desktop"), "http://foo" },
426 { FPL("http___foo_bar_.desktop"), "http://foo/bar/" },
427 { FPL("http___foo_bar_a=b&c=d.desktop"), "http://foo/bar?a=b&c=d" },
429 // Now we're starting to be more evil...
430 { FPL("http___foo_.desktop"), "http://foo/bar/baz/../../../../../" },
431 { FPL("http___foo_.desktop"), "http://foo/bar/././../baz/././../" },
432 { FPL("http___.._.desktop"), "http://../../../../" },
434 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
435 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName
) + "-" +
437 ShellIntegrationLinux::GetWebShortcutFilename(
438 GURL(test_cases
[i
].url
)).value()) <<
439 " while testing " << test_cases
[i
].url
;
443 TEST(ShellIntegrationTest
, GetDesktopFileContents
) {
444 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
448 const char* icon_name
;
449 const char* categories
;
451 const char* expected_output
;
454 { "http://gmail.com",
456 "chrome-http__gmail.com",
460 "#!/usr/bin/env xdg-open\n"
466 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
467 "Icon=chrome-http__gmail.com\n"
468 "StartupWMClass=gmail.com\n"
471 // Make sure that empty icons are replaced by the chrome icon.
472 { "http://gmail.com",
478 "#!/usr/bin/env xdg-open\n"
484 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
485 #if defined(GOOGLE_CHROME_BUILD)
486 "Icon=google-chrome\n"
488 "Icon=chromium-browser\n"
490 "StartupWMClass=gmail.com\n"
493 // Test adding categories and NoDisplay=true.
494 { "http://gmail.com",
496 "chrome-http__gmail.com",
497 "Graphics;Education;",
500 "#!/usr/bin/env xdg-open\n"
506 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
507 "Icon=chrome-http__gmail.com\n"
508 "Categories=Graphics;Education;\n"
510 "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",
520 "#!/usr/bin/env xdg-open\n"
525 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
526 "Exec=/opt/google/chrome/google-chrome "
527 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
528 "Icon=chrome-http__evil.com_evil\n"
529 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
531 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
533 "chrome-http__evil.com_evil",
537 "#!/usr/bin/env xdg-open\n"
542 "Name=Innocent Title\n"
543 "Exec=/opt/google/chrome/google-chrome "
544 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
545 // Note: $ is escaped as \$ within an arg to Exec, and then
546 // the \ is escaped as \\ as all strings in a Desktop file should
547 // be; finally, \\ becomes \\\\ when represented in a C++ string!
548 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
549 "Icon=chrome-http__evil.com_evil\n"
550 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
551 "rm%20-rf%20$HOME%20%3Eownz0red\n"
553 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
555 "chrome-http__evil.com_evil",
559 "#!/usr/bin/env xdg-open\n"
564 "Name=Innocent Title\n"
565 "Exec=/opt/google/chrome/google-chrome "
566 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
567 "%60%20%3E/dev/null\n"
568 "Icon=chrome-http__evil.com_evil\n"
569 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
570 "%60%20%3E_dev_null\n"
574 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
577 test_cases
[i
].expected_output
,
578 ShellIntegrationLinux::GetDesktopFileContents(
580 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
581 GURL(test_cases
[i
].url
),
583 base::ASCIIToUTF16(test_cases
[i
].title
),
584 test_cases
[i
].icon_name
,
586 test_cases
[i
].categories
,
587 test_cases
[i
].nodisplay
));
591 TEST(ShellIntegrationTest
, GetDesktopFileContentsAppList
) {
592 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
593 CommandLine
command_line(kChromeExePath
);
594 command_line
.AppendSwitch("--show-app-list");
596 "#!/usr/bin/env xdg-open\n"
601 "Name=Chrome App Launcher\n"
602 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
603 "Icon=chrome_app_list\n"
604 "Categories=Network;WebBrowser;\n"
605 "StartupWMClass=chrome-app-list\n",
606 ShellIntegrationLinux::GetDesktopFileContentsForCommand(
610 base::ASCIIToUTF16("Chrome App Launcher"),
612 "Network;WebBrowser;",
616 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
619 const char* icon_name
;
620 const char* expected_output
;
633 // Make sure that empty icons are replaced by the chrome icon.
641 #if defined(GOOGLE_CHROME_BUILD)
642 "Icon=google-chrome\n"
644 "Icon=chromium-browser\n"
649 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
652 test_cases
[i
].expected_output
,
653 ShellIntegrationLinux::GetDirectoryFileContents(
654 base::ASCIIToUTF16(test_cases
[i
].title
),
655 test_cases
[i
].icon_name
));