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/browser/web_applications/web_app.h"
23 #include "chrome/common/chrome_constants.h"
24 #include "content/public/test/test_browser_thread.h"
25 #include "testing/gmock/include/gmock/gmock.h"
26 #include "testing/gtest/include/gtest/gtest.h"
29 #define FPL FILE_PATH_LITERAL
31 using content::BrowserThread
;
32 using ::testing::ElementsAre
;
36 // Provides mock environment variables values based on a stored map.
37 class MockEnvironment
: public base::Environment
{
41 void Set(const std::string
& name
, const std::string
& value
) {
42 variables_
[name
] = value
;
45 virtual bool GetVar(const char* variable_name
, std::string
* result
) OVERRIDE
{
46 if (ContainsKey(variables_
, variable_name
)) {
47 *result
= variables_
[variable_name
];
54 virtual bool SetVar(const char* variable_name
,
55 const std::string
& new_value
) OVERRIDE
{
60 virtual bool UnSetVar(const char* variable_name
) OVERRIDE
{
66 std::map
<std::string
, std::string
> variables_
;
68 DISALLOW_COPY_AND_ASSIGN(MockEnvironment
);
73 TEST(ShellIntegrationTest
, GetDataWriteLocation
) {
74 base::MessageLoop message_loop
;
75 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
77 // Test that it returns $XDG_DATA_HOME.
80 env
.Set("HOME", "/home/user");
81 env
.Set("XDG_DATA_HOME", "/user/path");
83 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
84 EXPECT_EQ(base::FilePath("/user/path"), path
);
87 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
90 env
.Set("HOME", "/home/user");
92 ASSERT_TRUE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
93 EXPECT_EQ(base::FilePath("/home/user/.local/share"), path
);
96 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it fails.
100 ASSERT_FALSE(ShellIntegrationLinux::GetDataWriteLocation(&env
, &path
));
104 TEST(ShellIntegrationTest
, GetDataSearchLocations
) {
105 base::MessageLoop message_loop
;
106 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
108 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
111 env
.Set("HOME", "/home/user");
112 env
.Set("XDG_DATA_HOME", "/user/path");
113 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
115 ShellIntegrationLinux::GetDataSearchLocations(&env
),
116 ElementsAre(base::FilePath("/user/path"),
117 base::FilePath("/system/path/1"),
118 base::FilePath("/system/path/2")));
121 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
124 env
.Set("HOME", "/home/user");
125 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
127 ShellIntegrationLinux::GetDataSearchLocations(&env
),
128 ElementsAre(base::FilePath("/home/user/.local/share"),
129 base::FilePath("/system/path/1"),
130 base::FilePath("/system/path/2")));
133 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
137 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
139 ShellIntegrationLinux::GetDataSearchLocations(&env
),
140 ElementsAre(base::FilePath("/system/path/1"),
141 base::FilePath("/system/path/2")));
144 // Test that $XDG_DATA_DIRS falls back to the two default paths.
147 env
.Set("HOME", "/home/user");
148 env
.Set("XDG_DATA_HOME", "/user/path");
150 ShellIntegrationLinux::GetDataSearchLocations(&env
),
151 ElementsAre(base::FilePath("/user/path"),
152 base::FilePath("/usr/local/share"),
153 base::FilePath("/usr/share")));
157 TEST(ShellIntegrationTest
, GetExistingShortcutLocations
) {
158 base::FilePath
kProfilePath("Profile 1");
159 const char kExtensionId
[] = "test_extension";
160 const char kTemplateFilename
[] = "chrome-test_extension-Profile_1.desktop";
161 base::FilePath
kTemplateFilepath(kTemplateFilename
);
162 const char kNoDisplayDesktopFile
[] = "[Desktop Entry]\nNoDisplay=true";
164 base::MessageLoop message_loop
;
165 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
167 // No existing shortcuts.
170 ShellIntegration::ShortcutLocations result
=
171 ShellIntegrationLinux::GetExistingShortcutLocations(
172 &env
, kProfilePath
, kExtensionId
);
173 EXPECT_FALSE(result
.on_desktop
);
174 EXPECT_EQ(ShellIntegration::APP_MENU_LOCATION_NONE
,
175 result
.applications_menu_location
);
177 EXPECT_FALSE(result
.in_quick_launch_bar
);
178 EXPECT_FALSE(result
.hidden
);
181 // Shortcut on desktop.
183 base::ScopedTempDir temp_dir
;
184 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
185 base::FilePath desktop_path
= temp_dir
.path();
188 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
189 ASSERT_FALSE(base::WriteFile(
190 desktop_path
.AppendASCII(kTemplateFilename
),
192 ShellIntegration::ShortcutLocations result
=
193 ShellIntegrationLinux::GetExistingShortcutLocations(
194 &env
, kProfilePath
, kExtensionId
, desktop_path
);
195 EXPECT_TRUE(result
.on_desktop
);
196 EXPECT_EQ(ShellIntegration::APP_MENU_LOCATION_NONE
,
197 result
.applications_menu_location
);
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(base::CreateDirectory(apps_path
));
212 ASSERT_FALSE(base::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_EQ(ShellIntegration::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
220 result
.applications_menu_location
);
222 EXPECT_FALSE(result
.in_quick_launch_bar
);
223 EXPECT_FALSE(result
.hidden
);
226 // Shortcut in applications directory with NoDisplay=true.
228 base::ScopedTempDir temp_dir
;
229 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
230 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
233 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
234 ASSERT_TRUE(base::CreateDirectory(apps_path
));
235 ASSERT_TRUE(base::WriteFile(
236 apps_path
.AppendASCII(kTemplateFilename
),
237 kNoDisplayDesktopFile
, strlen(kNoDisplayDesktopFile
)));
238 ShellIntegration::ShortcutLocations result
=
239 ShellIntegrationLinux::GetExistingShortcutLocations(
240 &env
, kProfilePath
, kExtensionId
);
241 // Doesn't count as being in applications menu.
242 EXPECT_FALSE(result
.on_desktop
);
243 EXPECT_EQ(ShellIntegration::APP_MENU_LOCATION_NONE
,
244 result
.applications_menu_location
);
245 EXPECT_FALSE(result
.in_quick_launch_bar
);
246 EXPECT_TRUE(result
.hidden
);
249 // Shortcut on desktop and in applications directory.
251 base::ScopedTempDir temp_dir1
;
252 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
253 base::FilePath desktop_path
= temp_dir1
.path();
255 base::ScopedTempDir temp_dir2
;
256 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
257 base::FilePath apps_path
= temp_dir2
.path().AppendASCII("applications");
260 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
261 ASSERT_FALSE(base::WriteFile(
262 desktop_path
.AppendASCII(kTemplateFilename
),
264 env
.Set("XDG_DATA_HOME", temp_dir2
.path().value());
265 ASSERT_TRUE(base::CreateDirectory(apps_path
));
266 ASSERT_FALSE(base::WriteFile(
267 apps_path
.AppendASCII(kTemplateFilename
),
269 ShellIntegration::ShortcutLocations result
=
270 ShellIntegrationLinux::GetExistingShortcutLocations(
271 &env
, kProfilePath
, kExtensionId
, desktop_path
);
272 EXPECT_TRUE(result
.on_desktop
);
273 EXPECT_EQ(ShellIntegration::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
274 result
.applications_menu_location
);
275 EXPECT_FALSE(result
.in_quick_launch_bar
);
276 EXPECT_FALSE(result
.hidden
);
280 TEST(ShellIntegrationTest
, GetExistingShortcutContents
) {
281 const char kTemplateFilename
[] = "shortcut-test.desktop";
282 base::FilePath
kTemplateFilepath(kTemplateFilename
);
283 const char kTestData1
[] = "a magical testing string";
284 const char kTestData2
[] = "a different testing string";
286 base::MessageLoop message_loop
;
287 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
289 // Test that it searches $XDG_DATA_HOME/applications.
291 base::ScopedTempDir temp_dir
;
292 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
295 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
296 // Create a file in a non-applications directory. This should be ignored.
297 ASSERT_TRUE(base::WriteFile(
298 temp_dir
.path().AppendASCII(kTemplateFilename
),
299 kTestData2
, strlen(kTestData2
)));
300 ASSERT_TRUE(base::CreateDirectory(
301 temp_dir
.path().AppendASCII("applications")));
302 ASSERT_TRUE(base::WriteFile(
303 temp_dir
.path().AppendASCII("applications")
304 .AppendASCII(kTemplateFilename
),
305 kTestData1
, strlen(kTestData1
)));
306 std::string contents
;
308 ShellIntegrationLinux::GetExistingShortcutContents(
309 &env
, kTemplateFilepath
, &contents
));
310 EXPECT_EQ(kTestData1
, contents
);
313 // Test that it falls back to $HOME/.local/share/applications.
315 base::ScopedTempDir temp_dir
;
316 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
319 env
.Set("HOME", temp_dir
.path().value());
320 ASSERT_TRUE(base::CreateDirectory(
321 temp_dir
.path().AppendASCII(".local/share/applications")));
322 ASSERT_TRUE(base::WriteFile(
323 temp_dir
.path().AppendASCII(".local/share/applications")
324 .AppendASCII(kTemplateFilename
),
325 kTestData1
, strlen(kTestData1
)));
326 std::string contents
;
328 ShellIntegrationLinux::GetExistingShortcutContents(
329 &env
, kTemplateFilepath
, &contents
));
330 EXPECT_EQ(kTestData1
, contents
);
333 // Test that it searches $XDG_DATA_DIRS/applications.
335 base::ScopedTempDir temp_dir
;
336 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
339 env
.Set("XDG_DATA_DIRS", temp_dir
.path().value());
340 ASSERT_TRUE(base::CreateDirectory(
341 temp_dir
.path().AppendASCII("applications")));
342 ASSERT_TRUE(base::WriteFile(
343 temp_dir
.path().AppendASCII("applications")
344 .AppendASCII(kTemplateFilename
),
345 kTestData2
, strlen(kTestData2
)));
346 std::string contents
;
348 ShellIntegrationLinux::GetExistingShortcutContents(
349 &env
, kTemplateFilepath
, &contents
));
350 EXPECT_EQ(kTestData2
, contents
);
353 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
355 base::ScopedTempDir temp_dir1
;
356 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
357 base::ScopedTempDir temp_dir2
;
358 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
361 env
.Set("XDG_DATA_DIRS", temp_dir1
.path().value() + ":" +
362 temp_dir2
.path().value());
363 // Create a file in a non-applications directory. This should be ignored.
364 ASSERT_TRUE(base::WriteFile(
365 temp_dir1
.path().AppendASCII(kTemplateFilename
),
366 kTestData1
, strlen(kTestData1
)));
367 // Only create a findable desktop file in the second path.
368 ASSERT_TRUE(base::CreateDirectory(
369 temp_dir2
.path().AppendASCII("applications")));
370 ASSERT_TRUE(base::WriteFile(
371 temp_dir2
.path().AppendASCII("applications")
372 .AppendASCII(kTemplateFilename
),
373 kTestData2
, strlen(kTestData2
)));
374 std::string contents
;
376 ShellIntegrationLinux::GetExistingShortcutContents(
377 &env
, kTemplateFilepath
, &contents
));
378 EXPECT_EQ(kTestData2
, contents
);
382 TEST(ShellIntegrationTest
, GetExtensionShortcutFilename
) {
383 base::FilePath
kProfilePath("a/b/c/Profile Name?");
384 const char kExtensionId
[] = "extensionid";
385 EXPECT_EQ(base::FilePath("chrome-extensionid-Profile_Name_.desktop"),
386 ShellIntegrationLinux::GetExtensionShortcutFilename(
387 kProfilePath
, kExtensionId
));
390 TEST(ShellIntegrationTest
, GetExistingProfileShortcutFilenames
) {
391 base::FilePath
kProfilePath("a/b/c/Profile Name?");
392 const char kApp1Filename
[] = "chrome-extension1-Profile_Name_.desktop";
393 const char kApp2Filename
[] = "chrome-extension2-Profile_Name_.desktop";
394 const char kUnrelatedAppFilename
[] = "chrome-extension-Other_Profile.desktop";
396 base::MessageLoop message_loop
;
397 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
399 base::ScopedTempDir temp_dir
;
400 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
403 temp_dir
.path().AppendASCII(kApp1Filename
), "", 0));
406 temp_dir
.path().AppendASCII(kApp2Filename
), "", 0));
407 // This file should not be returned in the results.
410 temp_dir
.path().AppendASCII(kUnrelatedAppFilename
), "", 0));
411 std::vector
<base::FilePath
> paths
=
412 ShellIntegrationLinux::GetExistingProfileShortcutFilenames(
413 kProfilePath
, temp_dir
.path());
414 // Path order is arbitrary. Sort the output for consistency.
415 std::sort(paths
.begin(), paths
.end());
417 ElementsAre(base::FilePath(kApp1Filename
),
418 base::FilePath(kApp2Filename
)));
421 TEST(ShellIntegrationTest
, GetWebShortcutFilename
) {
423 const base::FilePath::CharType
* path
;
426 { FPL("http___foo_.desktop"), "http://foo" },
427 { FPL("http___foo_bar_.desktop"), "http://foo/bar/" },
428 { FPL("http___foo_bar_a=b&c=d.desktop"), "http://foo/bar?a=b&c=d" },
430 // Now we're starting to be more evil...
431 { FPL("http___foo_.desktop"), "http://foo/bar/baz/../../../../../" },
432 { FPL("http___foo_.desktop"), "http://foo/bar/././../baz/././../" },
433 { FPL("http___.._.desktop"), "http://../../../../" },
435 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
436 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName
) + "-" +
438 ShellIntegrationLinux::GetWebShortcutFilename(
439 GURL(test_cases
[i
].url
)).value()) <<
440 " while testing " << test_cases
[i
].url
;
444 TEST(ShellIntegrationTest
, GetDesktopFileContents
) {
445 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
449 const char* icon_name
;
451 const char* expected_output
;
454 { "http://gmail.com",
456 "chrome-http__gmail.com",
459 "#!/usr/bin/env xdg-open\n"
465 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
466 "Icon=chrome-http__gmail.com\n"
467 "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 #if defined(GOOGLE_CHROME_BUILD)
484 "Icon=google-chrome\n"
486 "Icon=chromium-browser\n"
488 "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 "StartupWMClass=gmail.com\n"
509 // Now we're starting to be more evil...
510 { "http://evil.com/evil --join-the-b0tnet",
511 "Ownz0red\nExec=rm -rf /",
512 "chrome-http__evil.com_evil",
515 "#!/usr/bin/env xdg-open\n"
520 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
521 "Exec=/opt/google/chrome/google-chrome "
522 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
523 "Icon=chrome-http__evil.com_evil\n"
524 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
526 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
528 "chrome-http__evil.com_evil",
531 "#!/usr/bin/env xdg-open\n"
536 "Name=Innocent Title\n"
537 "Exec=/opt/google/chrome/google-chrome "
538 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
539 // Note: $ is escaped as \$ within an arg to Exec, and then
540 // the \ is escaped as \\ as all strings in a Desktop file should
541 // be; finally, \\ becomes \\\\ when represented in a C++ string!
542 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
543 "Icon=chrome-http__evil.com_evil\n"
544 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
545 "rm%20-rf%20$HOME%20%3Eownz0red\n"
547 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
549 "chrome-http__evil.com_evil",
552 "#!/usr/bin/env xdg-open\n"
557 "Name=Innocent Title\n"
558 "Exec=/opt/google/chrome/google-chrome "
559 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
560 "%60%20%3E/dev/null\n"
561 "Icon=chrome-http__evil.com_evil\n"
562 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
563 "%60%20%3E_dev_null\n"
567 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
570 test_cases
[i
].expected_output
,
571 ShellIntegrationLinux::GetDesktopFileContents(
573 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
574 GURL(test_cases
[i
].url
),
576 base::ASCIIToUTF16(test_cases
[i
].title
),
577 test_cases
[i
].icon_name
,
579 test_cases
[i
].nodisplay
));
583 TEST(ShellIntegrationTest
, GetDesktopFileContentsAppList
) {
584 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
585 CommandLine
command_line(kChromeExePath
);
586 command_line
.AppendSwitch("--show-app-list");
588 "#!/usr/bin/env xdg-open\n"
593 "Name=Chrome App Launcher\n"
594 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
595 "Icon=chrome_app_list\n"
596 "StartupWMClass=chrome-app-list\n",
597 ShellIntegrationLinux::GetDesktopFileContentsForCommand(
601 base::ASCIIToUTF16("Chrome App Launcher"),
606 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
609 const char* icon_name
;
610 const char* expected_output
;
623 // Make sure that empty icons are replaced by the chrome icon.
631 #if defined(GOOGLE_CHROME_BUILD)
632 "Icon=google-chrome\n"
634 "Icon=chromium-browser\n"
639 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
642 test_cases
[i
].expected_output
,
643 ShellIntegrationLinux::GetDirectoryFileContents(
644 base::ASCIIToUTF16(test_cases
[i
].title
),
645 test_cases
[i
].icon_name
));