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/files/file_path.h"
15 #include "base/files/file_util.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
;
33 namespace shell_integration_linux
{
37 // Provides mock environment variables values based on a stored map.
38 class MockEnvironment
: public base::Environment
{
42 void Set(const std::string
& name
, const std::string
& value
) {
43 variables_
[name
] = value
;
46 virtual bool GetVar(const char* variable_name
, std::string
* result
) override
{
47 if (ContainsKey(variables_
, variable_name
)) {
48 *result
= variables_
[variable_name
];
55 virtual bool SetVar(const char* variable_name
,
56 const std::string
& new_value
) override
{
61 virtual bool UnSetVar(const char* variable_name
) override
{
67 std::map
<std::string
, std::string
> variables_
;
69 DISALLOW_COPY_AND_ASSIGN(MockEnvironment
);
74 TEST(ShellIntegrationTest
, GetDataWriteLocation
) {
75 base::MessageLoop message_loop
;
76 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
78 // Test that it returns $XDG_DATA_HOME.
81 env
.Set("HOME", "/home/user");
82 env
.Set("XDG_DATA_HOME", "/user/path");
84 ASSERT_TRUE(GetDataWriteLocation(&env
, &path
));
85 EXPECT_EQ(base::FilePath("/user/path"), path
);
88 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
91 env
.Set("HOME", "/home/user");
93 ASSERT_TRUE(GetDataWriteLocation(&env
, &path
));
94 EXPECT_EQ(base::FilePath("/home/user/.local/share"), path
);
97 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it fails.
101 ASSERT_FALSE(GetDataWriteLocation(&env
, &path
));
105 TEST(ShellIntegrationTest
, GetDataSearchLocations
) {
106 base::MessageLoop message_loop
;
107 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
109 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
112 env
.Set("HOME", "/home/user");
113 env
.Set("XDG_DATA_HOME", "/user/path");
114 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
116 GetDataSearchLocations(&env
),
117 ElementsAre(base::FilePath("/user/path"),
118 base::FilePath("/system/path/1"),
119 base::FilePath("/system/path/2")));
122 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
125 env
.Set("HOME", "/home/user");
126 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
128 GetDataSearchLocations(&env
),
129 ElementsAre(base::FilePath("/home/user/.local/share"),
130 base::FilePath("/system/path/1"),
131 base::FilePath("/system/path/2")));
134 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
138 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
140 GetDataSearchLocations(&env
),
141 ElementsAre(base::FilePath("/system/path/1"),
142 base::FilePath("/system/path/2")));
145 // Test that $XDG_DATA_DIRS falls back to the two default paths.
148 env
.Set("HOME", "/home/user");
149 env
.Set("XDG_DATA_HOME", "/user/path");
151 GetDataSearchLocations(&env
),
152 ElementsAre(base::FilePath("/user/path"),
153 base::FilePath("/usr/local/share"),
154 base::FilePath("/usr/share")));
158 TEST(ShellIntegrationTest
, GetExistingShortcutLocations
) {
159 base::FilePath
kProfilePath("Profile 1");
160 const char kExtensionId
[] = "test_extension";
161 const char kTemplateFilename
[] = "chrome-test_extension-Profile_1.desktop";
162 base::FilePath
kTemplateFilepath(kTemplateFilename
);
163 const char kNoDisplayDesktopFile
[] = "[Desktop Entry]\nNoDisplay=true";
165 base::MessageLoop message_loop
;
166 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
168 // No existing shortcuts.
171 web_app::ShortcutLocations result
=
172 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
173 EXPECT_FALSE(result
.on_desktop
);
174 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
175 result
.applications_menu_location
);
177 EXPECT_FALSE(result
.in_quick_launch_bar
);
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
= GetExistingShortcutLocations(
192 &env
, kProfilePath
, kExtensionId
, desktop_path
);
193 EXPECT_TRUE(result
.on_desktop
);
194 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
195 result
.applications_menu_location
);
197 EXPECT_FALSE(result
.in_quick_launch_bar
);
200 // Shortcut in applications directory.
202 base::ScopedTempDir temp_dir
;
203 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
204 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
207 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
208 ASSERT_TRUE(base::CreateDirectory(apps_path
));
209 ASSERT_FALSE(base::WriteFile(
210 apps_path
.AppendASCII(kTemplateFilename
),
212 web_app::ShortcutLocations result
=
213 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
214 EXPECT_FALSE(result
.on_desktop
);
215 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
216 result
.applications_menu_location
);
218 EXPECT_FALSE(result
.in_quick_launch_bar
);
221 // Shortcut in applications directory with NoDisplay=true.
223 base::ScopedTempDir temp_dir
;
224 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
225 base::FilePath apps_path
= temp_dir
.path().AppendASCII("applications");
228 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
229 ASSERT_TRUE(base::CreateDirectory(apps_path
));
230 ASSERT_TRUE(base::WriteFile(
231 apps_path
.AppendASCII(kTemplateFilename
),
232 kNoDisplayDesktopFile
, strlen(kNoDisplayDesktopFile
)));
233 web_app::ShortcutLocations result
=
234 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
235 // Doesn't count as being in applications menu.
236 EXPECT_FALSE(result
.on_desktop
);
237 EXPECT_EQ(web_app::APP_MENU_LOCATION_HIDDEN
,
238 result
.applications_menu_location
);
239 EXPECT_FALSE(result
.in_quick_launch_bar
);
242 // Shortcut on desktop and in applications directory.
244 base::ScopedTempDir temp_dir1
;
245 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
246 base::FilePath desktop_path
= temp_dir1
.path();
248 base::ScopedTempDir temp_dir2
;
249 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
250 base::FilePath apps_path
= temp_dir2
.path().AppendASCII("applications");
253 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
254 ASSERT_FALSE(base::WriteFile(
255 desktop_path
.AppendASCII(kTemplateFilename
),
257 env
.Set("XDG_DATA_HOME", temp_dir2
.path().value());
258 ASSERT_TRUE(base::CreateDirectory(apps_path
));
259 ASSERT_FALSE(base::WriteFile(
260 apps_path
.AppendASCII(kTemplateFilename
),
262 web_app::ShortcutLocations result
= GetExistingShortcutLocations(
263 &env
, kProfilePath
, kExtensionId
, desktop_path
);
264 EXPECT_TRUE(result
.on_desktop
);
265 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
266 result
.applications_menu_location
);
267 EXPECT_FALSE(result
.in_quick_launch_bar
);
271 TEST(ShellIntegrationTest
, GetExistingShortcutContents
) {
272 const char kTemplateFilename
[] = "shortcut-test.desktop";
273 base::FilePath
kTemplateFilepath(kTemplateFilename
);
274 const char kTestData1
[] = "a magical testing string";
275 const char kTestData2
[] = "a different testing string";
277 base::MessageLoop message_loop
;
278 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
280 // Test that it searches $XDG_DATA_HOME/applications.
282 base::ScopedTempDir temp_dir
;
283 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
286 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
287 // Create a file in a non-applications directory. This should be ignored.
288 ASSERT_TRUE(base::WriteFile(
289 temp_dir
.path().AppendASCII(kTemplateFilename
),
290 kTestData2
, strlen(kTestData2
)));
291 ASSERT_TRUE(base::CreateDirectory(
292 temp_dir
.path().AppendASCII("applications")));
293 ASSERT_TRUE(base::WriteFile(
294 temp_dir
.path().AppendASCII("applications")
295 .AppendASCII(kTemplateFilename
),
296 kTestData1
, strlen(kTestData1
)));
297 std::string contents
;
299 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
300 EXPECT_EQ(kTestData1
, contents
);
303 // Test that it falls back to $HOME/.local/share/applications.
305 base::ScopedTempDir temp_dir
;
306 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
309 env
.Set("HOME", temp_dir
.path().value());
310 ASSERT_TRUE(base::CreateDirectory(
311 temp_dir
.path().AppendASCII(".local/share/applications")));
312 ASSERT_TRUE(base::WriteFile(
313 temp_dir
.path().AppendASCII(".local/share/applications")
314 .AppendASCII(kTemplateFilename
),
315 kTestData1
, strlen(kTestData1
)));
316 std::string contents
;
318 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
319 EXPECT_EQ(kTestData1
, contents
);
322 // Test that it searches $XDG_DATA_DIRS/applications.
324 base::ScopedTempDir temp_dir
;
325 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
328 env
.Set("XDG_DATA_DIRS", temp_dir
.path().value());
329 ASSERT_TRUE(base::CreateDirectory(
330 temp_dir
.path().AppendASCII("applications")));
331 ASSERT_TRUE(base::WriteFile(
332 temp_dir
.path().AppendASCII("applications")
333 .AppendASCII(kTemplateFilename
),
334 kTestData2
, strlen(kTestData2
)));
335 std::string contents
;
337 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
338 EXPECT_EQ(kTestData2
, contents
);
341 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
343 base::ScopedTempDir temp_dir1
;
344 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
345 base::ScopedTempDir temp_dir2
;
346 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
349 env
.Set("XDG_DATA_DIRS", temp_dir1
.path().value() + ":" +
350 temp_dir2
.path().value());
351 // Create a file in a non-applications directory. This should be ignored.
352 ASSERT_TRUE(base::WriteFile(
353 temp_dir1
.path().AppendASCII(kTemplateFilename
),
354 kTestData1
, strlen(kTestData1
)));
355 // Only create a findable desktop file in the second path.
356 ASSERT_TRUE(base::CreateDirectory(
357 temp_dir2
.path().AppendASCII("applications")));
358 ASSERT_TRUE(base::WriteFile(
359 temp_dir2
.path().AppendASCII("applications")
360 .AppendASCII(kTemplateFilename
),
361 kTestData2
, strlen(kTestData2
)));
362 std::string contents
;
364 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
365 EXPECT_EQ(kTestData2
, contents
);
369 TEST(ShellIntegrationTest
, GetExtensionShortcutFilename
) {
370 base::FilePath
kProfilePath("a/b/c/Profile Name?");
371 const char kExtensionId
[] = "extensionid";
372 EXPECT_EQ(base::FilePath("chrome-extensionid-Profile_Name_.desktop"),
373 GetExtensionShortcutFilename(kProfilePath
, kExtensionId
));
376 TEST(ShellIntegrationTest
, GetExistingProfileShortcutFilenames
) {
377 base::FilePath
kProfilePath("a/b/c/Profile Name?");
378 const char kApp1Filename
[] = "chrome-extension1-Profile_Name_.desktop";
379 const char kApp2Filename
[] = "chrome-extension2-Profile_Name_.desktop";
380 const char kUnrelatedAppFilename
[] = "chrome-extension-Other_Profile.desktop";
382 base::MessageLoop message_loop
;
383 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
385 base::ScopedTempDir temp_dir
;
386 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
389 temp_dir
.path().AppendASCII(kApp1Filename
), "", 0));
392 temp_dir
.path().AppendASCII(kApp2Filename
), "", 0));
393 // This file should not be returned in the results.
396 temp_dir
.path().AppendASCII(kUnrelatedAppFilename
), "", 0));
397 std::vector
<base::FilePath
> paths
=
398 GetExistingProfileShortcutFilenames(kProfilePath
, temp_dir
.path());
399 // Path order is arbitrary. Sort the output for consistency.
400 std::sort(paths
.begin(), paths
.end());
402 ElementsAre(base::FilePath(kApp1Filename
),
403 base::FilePath(kApp2Filename
)));
406 TEST(ShellIntegrationTest
, GetWebShortcutFilename
) {
408 const base::FilePath::CharType
* path
;
411 { FPL("http___foo_.desktop"), "http://foo" },
412 { FPL("http___foo_bar_.desktop"), "http://foo/bar/" },
413 { FPL("http___foo_bar_a=b&c=d.desktop"), "http://foo/bar?a=b&c=d" },
415 // Now we're starting to be more evil...
416 { FPL("http___foo_.desktop"), "http://foo/bar/baz/../../../../../" },
417 { FPL("http___foo_.desktop"), "http://foo/bar/././../baz/././../" },
418 { FPL("http___.._.desktop"), "http://../../../../" },
420 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
421 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName
) + "-" +
423 GetWebShortcutFilename(GURL(test_cases
[i
].url
)).value()) <<
424 " while testing " << test_cases
[i
].url
;
428 TEST(ShellIntegrationTest
, GetDesktopFileContents
) {
429 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
433 const char* icon_name
;
434 const char* categories
;
436 const char* expected_output
;
439 { "http://gmail.com",
441 "chrome-http__gmail.com",
445 "#!/usr/bin/env xdg-open\n"
451 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
452 "Icon=chrome-http__gmail.com\n"
453 "StartupWMClass=gmail.com\n"
456 // Make sure that empty icons are replaced by the chrome icon.
457 { "http://gmail.com",
463 "#!/usr/bin/env xdg-open\n"
469 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
470 #if defined(GOOGLE_CHROME_BUILD)
471 "Icon=google-chrome\n"
473 "Icon=chromium-browser\n"
475 "StartupWMClass=gmail.com\n"
478 // Test adding categories and NoDisplay=true.
479 { "http://gmail.com",
481 "chrome-http__gmail.com",
482 "Graphics;Education;",
485 "#!/usr/bin/env xdg-open\n"
491 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
492 "Icon=chrome-http__gmail.com\n"
493 "Categories=Graphics;Education;\n"
495 "StartupWMClass=gmail.com\n"
498 // Now we're starting to be more evil...
499 { "http://evil.com/evil --join-the-b0tnet",
500 "Ownz0red\nExec=rm -rf /",
501 "chrome-http__evil.com_evil",
505 "#!/usr/bin/env xdg-open\n"
510 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
511 "Exec=/opt/google/chrome/google-chrome "
512 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
513 "Icon=chrome-http__evil.com_evil\n"
514 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
516 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
518 "chrome-http__evil.com_evil",
522 "#!/usr/bin/env xdg-open\n"
527 "Name=Innocent Title\n"
528 "Exec=/opt/google/chrome/google-chrome "
529 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
530 // Note: $ is escaped as \$ within an arg to Exec, and then
531 // the \ is escaped as \\ as all strings in a Desktop file should
532 // be; finally, \\ becomes \\\\ when represented in a C++ string!
533 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
534 "Icon=chrome-http__evil.com_evil\n"
535 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
536 "rm%20-rf%20$HOME%20%3Eownz0red\n"
538 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
540 "chrome-http__evil.com_evil",
544 "#!/usr/bin/env xdg-open\n"
549 "Name=Innocent Title\n"
550 "Exec=/opt/google/chrome/google-chrome "
551 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
552 "%60%20%3E/dev/null\n"
553 "Icon=chrome-http__evil.com_evil\n"
554 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
555 "%60%20%3E_dev_null\n"
559 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
562 test_cases
[i
].expected_output
,
563 GetDesktopFileContents(
565 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
566 GURL(test_cases
[i
].url
),
568 base::ASCIIToUTF16(test_cases
[i
].title
),
569 test_cases
[i
].icon_name
,
571 test_cases
[i
].categories
,
572 test_cases
[i
].nodisplay
));
576 TEST(ShellIntegrationTest
, GetDesktopFileContentsAppList
) {
577 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
578 base::CommandLine
command_line(kChromeExePath
);
579 command_line
.AppendSwitch("--show-app-list");
581 "#!/usr/bin/env xdg-open\n"
586 "Name=Chrome App Launcher\n"
587 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
588 "Icon=chrome_app_list\n"
589 "Categories=Network;WebBrowser;\n"
590 "StartupWMClass=chrome-app-list\n",
591 GetDesktopFileContentsForCommand(
595 base::ASCIIToUTF16("Chrome App Launcher"),
597 "Network;WebBrowser;",
601 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
604 const char* icon_name
;
605 const char* expected_output
;
618 // Make sure that empty icons are replaced by the chrome icon.
626 #if defined(GOOGLE_CHROME_BUILD)
627 "Icon=google-chrome\n"
629 "Icon=chromium-browser\n"
634 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(test_cases
); i
++) {
636 EXPECT_EQ(test_cases
[i
].expected_output
,
637 GetDirectoryFileContents(base::ASCIIToUTF16(test_cases
[i
].title
),
638 test_cases
[i
].icon_name
));
642 } // namespace shell_integration_linux