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/environment.h"
13 #include "base/file_util.h"
14 #include "base/files/file_path.h"
15 #include "base/files/scoped_temp_dir.h"
16 #include "base/message_loop/message_loop.h"
17 #include "base/stl_util.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/test/scoped_path_override.h"
21 #include "chrome/browser/web_applications/web_app.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 ShellIntegration::ShortcutLocations result
=
170 ShellIntegrationLinux::GetExistingShortcutLocations(
171 &env
, kProfilePath
, kExtensionId
);
172 EXPECT_FALSE(result
.on_desktop
);
173 EXPECT_EQ(ShellIntegration::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(file_util::WriteFile(
189 desktop_path
.AppendASCII(kTemplateFilename
),
191 ShellIntegration::ShortcutLocations result
=
192 ShellIntegrationLinux::GetExistingShortcutLocations(
193 &env
, kProfilePath
, kExtensionId
, desktop_path
);
194 EXPECT_TRUE(result
.on_desktop
);
195 EXPECT_EQ(ShellIntegration::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(file_util::WriteFile(
212 apps_path
.AppendASCII(kTemplateFilename
),
214 ShellIntegration::ShortcutLocations result
=
215 ShellIntegrationLinux::GetExistingShortcutLocations(
216 &env
, kProfilePath
, kExtensionId
);
217 EXPECT_FALSE(result
.on_desktop
);
218 EXPECT_EQ(ShellIntegration::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(file_util::WriteFile(
235 apps_path
.AppendASCII(kTemplateFilename
),
236 kNoDisplayDesktopFile
, strlen(kNoDisplayDesktopFile
)));
237 ShellIntegration::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(ShellIntegration::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(file_util::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(file_util::WriteFile(
266 apps_path
.AppendASCII(kTemplateFilename
),
268 ShellIntegration::ShortcutLocations result
=
269 ShellIntegrationLinux::GetExistingShortcutLocations(
270 &env
, kProfilePath
, kExtensionId
, desktop_path
);
271 EXPECT_TRUE(result
.on_desktop
);
272 EXPECT_EQ(ShellIntegration::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(file_util::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(file_util::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(file_util::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(file_util::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(file_util::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(file_util::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());
401 file_util::WriteFile(
402 temp_dir
.path().AppendASCII(kApp1Filename
), "", 0));
404 file_util::WriteFile(
405 temp_dir
.path().AppendASCII(kApp2Filename
), "", 0));
406 // This file should not be returned in the results.
408 file_util::WriteFile(
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
;
450 const char* expected_output
;
453 { "http://gmail.com",
455 "chrome-http__gmail.com",
458 "#!/usr/bin/env xdg-open\n"
464 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
465 "Icon=chrome-http__gmail.com\n"
466 "StartupWMClass=gmail.com\n"
469 // Make sure that empty icons are replaced by the chrome icon.
470 { "http://gmail.com",
475 "#!/usr/bin/env xdg-open\n"
481 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
482 #if defined(GOOGLE_CHROME_BUILD)
483 "Icon=google-chrome\n"
485 "Icon=chromium-browser\n"
487 "StartupWMClass=gmail.com\n"
490 // Test adding NoDisplay=true.
491 { "http://gmail.com",
493 "chrome-http__gmail.com",
496 "#!/usr/bin/env xdg-open\n"
502 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
503 "Icon=chrome-http__gmail.com\n"
505 "StartupWMClass=gmail.com\n"
508 // Now we're starting to be more evil...
509 { "http://evil.com/evil --join-the-b0tnet",
510 "Ownz0red\nExec=rm -rf /",
511 "chrome-http__evil.com_evil",
514 "#!/usr/bin/env xdg-open\n"
519 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
520 "Exec=/opt/google/chrome/google-chrome "
521 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
522 "Icon=chrome-http__evil.com_evil\n"
523 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
525 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
527 "chrome-http__evil.com_evil",
530 "#!/usr/bin/env xdg-open\n"
535 "Name=Innocent Title\n"
536 "Exec=/opt/google/chrome/google-chrome "
537 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
538 // Note: $ is escaped as \$ within an arg to Exec, and then
539 // the \ is escaped as \\ as all strings in a Desktop file should
540 // be; finally, \\ becomes \\\\ when represented in a C++ string!
541 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
542 "Icon=chrome-http__evil.com_evil\n"
543 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
544 "rm%20-rf%20$HOME%20%3Eownz0red\n"
546 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
548 "chrome-http__evil.com_evil",
551 "#!/usr/bin/env xdg-open\n"
556 "Name=Innocent Title\n"
557 "Exec=/opt/google/chrome/google-chrome "
558 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
559 "%60%20%3E/dev/null\n"
560 "Icon=chrome-http__evil.com_evil\n"
561 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
562 "%60%20%3E_dev_null\n"
566 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
569 test_cases
[i
].expected_output
,
570 ShellIntegrationLinux::GetDesktopFileContents(
572 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
573 GURL(test_cases
[i
].url
),
576 base::ASCIIToUTF16(test_cases
[i
].title
),
577 test_cases
[i
].icon_name
,
579 test_cases
[i
].nodisplay
,
584 TEST(ShellIntegrationTest
, GetDesktopFileContentsAppList
) {
585 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
587 "#!/usr/bin/env xdg-open\n"
592 "Name=Chrome App Launcher\n"
593 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
594 "Icon=chrome_app_list\n"
595 "StartupWMClass=chrome-app-list\n",
596 ShellIntegrationLinux::GetDesktopFileContents(
602 base::ASCIIToUTF16("Chrome App Launcher"),
609 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
612 const char* icon_name
;
613 const char* expected_output
;
626 // Make sure that empty icons are replaced by the chrome icon.
634 #if defined(GOOGLE_CHROME_BUILD)
635 "Icon=google-chrome\n"
637 "Icon=chromium-browser\n"
642 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
645 test_cases
[i
].expected_output
,
646 ShellIntegrationLinux::GetDirectoryFileContents(
647 base::ASCIIToUTF16(test_cases
[i
].title
),
648 test_cases
[i
].icon_name
));