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"
12 #include "base/base_paths.h"
13 #include "base/command_line.h"
14 #include "base/environment.h"
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/files/scoped_temp_dir.h"
18 #include "base/message_loop/message_loop.h"
19 #include "base/stl_util.h"
20 #include "base/strings/string_util.h"
21 #include "base/strings/utf_string_conversions.h"
22 #include "base/test/scoped_path_override.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 using content::BrowserThread
;
30 using ::testing::ElementsAre
;
32 namespace shell_integration_linux
{
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 bool GetVar(const char* variable_name
, std::string
* result
) override
{
46 if (ContainsKey(variables_
, variable_name
)) {
47 *result
= variables_
[variable_name
];
54 bool SetVar(const char* variable_name
,
55 const std::string
& new_value
) override
{
60 bool UnSetVar(const char* variable_name
) override
{
66 std::map
<std::string
, std::string
> variables_
;
68 DISALLOW_COPY_AND_ASSIGN(MockEnvironment
);
71 // This helps EXPECT_THAT(..., ElementsAre(...)) print out more meaningful
73 std::vector
<std::string
> FilePathsToStrings(
74 const std::vector
<base::FilePath
>& paths
) {
75 std::vector
<std::string
> values
;
76 for (const auto& path
: paths
)
77 values
.push_back(path
.value());
81 bool WriteEmptyFile(const base::FilePath
& path
) {
82 return base::WriteFile(path
, "", 0) == 0;
85 bool WriteString(const base::FilePath
& path
, const base::StringPiece
& str
) {
86 int bytes_written
= base::WriteFile(path
, str
.data(), str
.size());
87 if (bytes_written
< 0)
90 return static_cast<size_t>(bytes_written
) == str
.size();
95 TEST(ShellIntegrationTest
, GetDataWriteLocation
) {
96 base::MessageLoop message_loop
;
97 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
99 // Test that it returns $XDG_DATA_HOME.
102 base::ScopedPathOverride
home_override(base::DIR_HOME
,
103 base::FilePath("/home/user"),
104 true /* absolute? */,
105 false /* create? */);
106 env
.Set("XDG_DATA_HOME", "/user/path");
107 base::FilePath path
= GetDataWriteLocation(&env
);
108 EXPECT_EQ("/user/path", path
.value());
111 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
114 base::ScopedPathOverride
home_override(base::DIR_HOME
,
115 base::FilePath("/home/user"),
116 true /* absolute? */,
117 false /* create? */);
118 base::FilePath path
= GetDataWriteLocation(&env
);
119 EXPECT_EQ("/home/user/.local/share", path
.value());
123 TEST(ShellIntegrationTest
, GetDataSearchLocations
) {
124 base::MessageLoop message_loop
;
125 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
127 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
130 base::ScopedPathOverride
home_override(base::DIR_HOME
,
131 base::FilePath("/home/user"),
132 true /* absolute? */,
133 false /* create? */);
134 env
.Set("XDG_DATA_HOME", "/user/path");
135 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
137 FilePathsToStrings(GetDataSearchLocations(&env
)),
138 ElementsAre("/user/path",
143 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
146 base::ScopedPathOverride
home_override(base::DIR_HOME
,
147 base::FilePath("/home/user"),
148 true /* absolute? */,
149 false /* create? */);
150 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
152 FilePathsToStrings(GetDataSearchLocations(&env
)),
153 ElementsAre("/home/user/.local/share",
158 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
162 env
.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
163 std::vector
<std::string
> results
=
164 FilePathsToStrings(GetDataSearchLocations(&env
));
165 ASSERT_EQ(3U, results
.size());
166 EXPECT_FALSE(results
[0].empty());
167 EXPECT_EQ("/system/path/1", results
[1]);
168 EXPECT_EQ("/system/path/2", results
[2]);
171 // Test that $XDG_DATA_DIRS falls back to the two default paths.
174 base::ScopedPathOverride
home_override(base::DIR_HOME
,
175 base::FilePath("/home/user"),
176 true /* absolute? */,
177 false /* create? */);
178 env
.Set("XDG_DATA_HOME", "/user/path");
180 FilePathsToStrings(GetDataSearchLocations(&env
)),
181 ElementsAre("/user/path",
187 TEST(ShellIntegrationTest
, GetExistingShortcutLocations
) {
188 base::FilePath
kProfilePath("Profile 1");
189 const char kExtensionId
[] = "test_extension";
190 const char kTemplateFilename
[] = "chrome-test_extension-Profile_1.desktop";
191 base::FilePath
kTemplateFilepath(kTemplateFilename
);
192 const char kNoDisplayDesktopFile
[] = "[Desktop Entry]\nNoDisplay=true";
194 base::MessageLoop message_loop
;
195 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
197 // No existing shortcuts.
200 web_app::ShortcutLocations result
=
201 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
202 EXPECT_FALSE(result
.on_desktop
);
203 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
204 result
.applications_menu_location
);
206 EXPECT_FALSE(result
.in_quick_launch_bar
);
209 // Shortcut on desktop.
211 base::ScopedTempDir temp_dir
;
212 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
213 base::FilePath desktop_path
= temp_dir
.path();
216 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
217 ASSERT_TRUE(WriteEmptyFile(desktop_path
.Append(kTemplateFilename
)));
218 web_app::ShortcutLocations result
= GetExistingShortcutLocations(
219 &env
, kProfilePath
, kExtensionId
, desktop_path
);
220 EXPECT_TRUE(result
.on_desktop
);
221 EXPECT_EQ(web_app::APP_MENU_LOCATION_NONE
,
222 result
.applications_menu_location
);
224 EXPECT_FALSE(result
.in_quick_launch_bar
);
227 // Shortcut in applications directory.
229 base::ScopedTempDir temp_dir
;
230 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
231 base::FilePath apps_path
= temp_dir
.path().Append("applications");
234 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
235 ASSERT_TRUE(base::CreateDirectory(apps_path
));
236 ASSERT_TRUE(WriteEmptyFile(apps_path
.Append(kTemplateFilename
)));
237 web_app::ShortcutLocations result
=
238 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
239 EXPECT_FALSE(result
.on_desktop
);
240 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
241 result
.applications_menu_location
);
243 EXPECT_FALSE(result
.in_quick_launch_bar
);
246 // Shortcut in applications directory with NoDisplay=true.
248 base::ScopedTempDir temp_dir
;
249 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
250 base::FilePath apps_path
= temp_dir
.path().Append("applications");
253 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
254 ASSERT_TRUE(base::CreateDirectory(apps_path
));
255 ASSERT_TRUE(WriteString(apps_path
.Append(kTemplateFilename
),
256 kNoDisplayDesktopFile
));
257 web_app::ShortcutLocations result
=
258 GetExistingShortcutLocations(&env
, kProfilePath
, kExtensionId
);
259 // Doesn't count as being in applications menu.
260 EXPECT_FALSE(result
.on_desktop
);
261 EXPECT_EQ(web_app::APP_MENU_LOCATION_HIDDEN
,
262 result
.applications_menu_location
);
263 EXPECT_FALSE(result
.in_quick_launch_bar
);
266 // Shortcut on desktop and in applications directory.
268 base::ScopedTempDir temp_dir1
;
269 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
270 base::FilePath desktop_path
= temp_dir1
.path();
272 base::ScopedTempDir temp_dir2
;
273 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
274 base::FilePath apps_path
= temp_dir2
.path().Append("applications");
277 ASSERT_TRUE(base::CreateDirectory(desktop_path
));
278 ASSERT_TRUE(WriteEmptyFile(desktop_path
.Append(kTemplateFilename
)));
279 env
.Set("XDG_DATA_HOME", temp_dir2
.path().value());
280 ASSERT_TRUE(base::CreateDirectory(apps_path
));
281 ASSERT_TRUE(WriteEmptyFile(apps_path
.Append(kTemplateFilename
)));
282 web_app::ShortcutLocations result
= GetExistingShortcutLocations(
283 &env
, kProfilePath
, kExtensionId
, desktop_path
);
284 EXPECT_TRUE(result
.on_desktop
);
285 EXPECT_EQ(web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS
,
286 result
.applications_menu_location
);
287 EXPECT_FALSE(result
.in_quick_launch_bar
);
291 TEST(ShellIntegrationTest
, GetExistingShortcutContents
) {
292 const char kTemplateFilename
[] = "shortcut-test.desktop";
293 base::FilePath
kTemplateFilepath(kTemplateFilename
);
294 const char kTestData1
[] = "a magical testing string";
295 const char kTestData2
[] = "a different testing string";
297 base::MessageLoop message_loop
;
298 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
300 // Test that it searches $XDG_DATA_HOME/applications.
302 base::ScopedTempDir temp_dir
;
303 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
306 env
.Set("XDG_DATA_HOME", temp_dir
.path().value());
307 // Create a file in a non-applications directory. This should be ignored.
308 ASSERT_TRUE(WriteString(temp_dir
.path().Append(kTemplateFilename
),
310 ASSERT_TRUE(base::CreateDirectory(
311 temp_dir
.path().Append("applications")));
312 ASSERT_TRUE(WriteString(
313 temp_dir
.path().Append("applications").Append(kTemplateFilename
),
315 std::string contents
;
317 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
318 EXPECT_EQ(kTestData1
, contents
);
321 // Test that it falls back to $HOME/.local/share/applications.
323 base::ScopedTempDir temp_dir
;
324 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
327 base::ScopedPathOverride
home_override(base::DIR_HOME
,
329 true /* absolute? */,
330 false /* create? */);
331 ASSERT_TRUE(base::CreateDirectory(
332 temp_dir
.path().Append(".local/share/applications")));
333 ASSERT_TRUE(WriteString(
334 temp_dir
.path().Append(".local/share/applications")
335 .Append(kTemplateFilename
),
337 std::string contents
;
339 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
340 EXPECT_EQ(kTestData1
, contents
);
343 // Test that it searches $XDG_DATA_DIRS/applications.
345 base::ScopedTempDir temp_dir
;
346 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
349 env
.Set("XDG_DATA_DIRS", temp_dir
.path().value());
350 ASSERT_TRUE(base::CreateDirectory(
351 temp_dir
.path().Append("applications")));
352 ASSERT_TRUE(WriteString(
353 temp_dir
.path().Append("applications").Append(kTemplateFilename
),
355 std::string contents
;
357 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
358 EXPECT_EQ(kTestData2
, contents
);
361 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
363 base::ScopedTempDir temp_dir1
;
364 ASSERT_TRUE(temp_dir1
.CreateUniqueTempDir());
365 base::ScopedTempDir temp_dir2
;
366 ASSERT_TRUE(temp_dir2
.CreateUniqueTempDir());
369 env
.Set("XDG_DATA_DIRS", temp_dir1
.path().value() + ":" +
370 temp_dir2
.path().value());
371 // Create a file in a non-applications directory. This should be ignored.
372 ASSERT_TRUE(WriteString(temp_dir1
.path().Append(kTemplateFilename
),
374 // Only create a findable desktop file in the second path.
375 ASSERT_TRUE(base::CreateDirectory(
376 temp_dir2
.path().Append("applications")));
377 ASSERT_TRUE(WriteString(
378 temp_dir2
.path().Append("applications").Append(kTemplateFilename
),
380 std::string contents
;
382 GetExistingShortcutContents(&env
, kTemplateFilepath
, &contents
));
383 EXPECT_EQ(kTestData2
, contents
);
387 TEST(ShellIntegrationTest
, GetExtensionShortcutFilename
) {
388 base::FilePath
kProfilePath("a/b/c/Profile Name?");
389 const char kExtensionId
[] = "extensionid";
390 EXPECT_EQ(base::FilePath("chrome-extensionid-Profile_Name_.desktop"),
391 GetExtensionShortcutFilename(kProfilePath
, kExtensionId
));
394 TEST(ShellIntegrationTest
, GetExistingProfileShortcutFilenames
) {
395 base::FilePath
kProfilePath("a/b/c/Profile Name?");
396 const char kApp1Filename
[] = "chrome-extension1-Profile_Name_.desktop";
397 const char kApp2Filename
[] = "chrome-extension2-Profile_Name_.desktop";
398 const char kUnrelatedAppFilename
[] = "chrome-extension-Other_Profile.desktop";
400 base::MessageLoop message_loop
;
401 content::TestBrowserThread
file_thread(BrowserThread::FILE, &message_loop
);
403 base::ScopedTempDir temp_dir
;
404 ASSERT_TRUE(temp_dir
.CreateUniqueTempDir());
405 ASSERT_TRUE(WriteEmptyFile(temp_dir
.path().Append(kApp1Filename
)));
406 ASSERT_TRUE(WriteEmptyFile(temp_dir
.path().Append(kApp2Filename
)));
407 // This file should not be returned in the results.
408 ASSERT_TRUE(WriteEmptyFile(temp_dir
.path().Append(kUnrelatedAppFilename
)));
409 std::vector
<base::FilePath
> paths
=
410 GetExistingProfileShortcutFilenames(kProfilePath
, temp_dir
.path());
411 // Path order is arbitrary. Sort the output for consistency.
412 std::sort(paths
.begin(), paths
.end());
414 ElementsAre(base::FilePath(kApp1Filename
),
415 base::FilePath(kApp2Filename
)));
418 TEST(ShellIntegrationTest
, GetWebShortcutFilename
) {
420 const char* const path
;
421 const char* const url
;
423 { "http___foo_.desktop", "http://foo" },
424 { "http___foo_bar_.desktop", "http://foo/bar/" },
425 { "http___foo_bar_a=b&c=d.desktop", "http://foo/bar?a=b&c=d" },
427 // Now we're starting to be more evil...
428 { "http___foo_.desktop", "http://foo/bar/baz/../../../../../" },
429 { "http___foo_.desktop", "http://foo/bar/././../baz/././../" },
430 { "http___.._.desktop", "http://../../../../" },
432 for (size_t i
= 0; i
< arraysize(test_cases
); i
++) {
433 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName
) + "-" +
435 GetWebShortcutFilename(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");
443 const char* const url
;
444 const char* const title
;
445 const char* const icon_name
;
446 const char* const categories
;
448 const char* const expected_output
;
451 { "http://gmail.com",
453 "chrome-http__gmail.com",
457 "#!/usr/bin/env xdg-open\n"
463 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
464 "Icon=chrome-http__gmail.com\n"
465 "StartupWMClass=gmail.com\n"
468 // Make sure that empty icons are replaced by the chrome icon.
469 { "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 categories and NoDisplay=true.
491 { "http://gmail.com",
493 "chrome-http__gmail.com",
494 "Graphics;Education;",
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"
505 "Categories=Graphics;Education;\n"
507 "StartupWMClass=gmail.com\n"
510 // Now we're starting to be more evil...
511 { "http://evil.com/evil --join-the-b0tnet",
512 "Ownz0red\nExec=rm -rf /",
513 "chrome-http__evil.com_evil",
517 "#!/usr/bin/env xdg-open\n"
522 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
523 "Exec=/opt/google/chrome/google-chrome "
524 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
525 "Icon=chrome-http__evil.com_evil\n"
526 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"
528 { "http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
530 "chrome-http__evil.com_evil",
534 "#!/usr/bin/env xdg-open\n"
539 "Name=Innocent Title\n"
540 "Exec=/opt/google/chrome/google-chrome "
541 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
542 // Note: $ is escaped as \$ within an arg to Exec, and then
543 // the \ is escaped as \\ as all strings in a Desktop file should
544 // be; finally, \\ becomes \\\\ when represented in a C++ string!
545 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
546 "Icon=chrome-http__evil.com_evil\n"
547 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
548 "rm%20-rf%20$HOME%20%3Eownz0red\n"
550 { "http://evil.com/evil | cat `echo ownz0red` >/dev/null",
552 "chrome-http__evil.com_evil",
556 "#!/usr/bin/env xdg-open\n"
561 "Name=Innocent Title\n"
562 "Exec=/opt/google/chrome/google-chrome "
563 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
564 "%60%20%3E/dev/null\n"
565 "Icon=chrome-http__evil.com_evil\n"
566 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
567 "%60%20%3E_dev_null\n"
571 for (size_t i
= 0; i
< arraysize(test_cases
); i
++) {
574 test_cases
[i
].expected_output
,
575 GetDesktopFileContents(
577 web_app::GenerateApplicationNameFromURL(GURL(test_cases
[i
].url
)),
578 GURL(test_cases
[i
].url
),
580 base::ASCIIToUTF16(test_cases
[i
].title
),
581 test_cases
[i
].icon_name
,
583 test_cases
[i
].categories
,
584 test_cases
[i
].nodisplay
));
588 TEST(ShellIntegrationTest
, GetDesktopFileContentsAppList
) {
589 const base::FilePath
kChromeExePath("/opt/google/chrome/google-chrome");
590 base::CommandLine
command_line(kChromeExePath
);
591 command_line
.AppendSwitch("--show-app-list");
593 "#!/usr/bin/env xdg-open\n"
598 "Name=Chrome App Launcher\n"
599 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
600 "Icon=chrome_app_list\n"
601 "Categories=Network;WebBrowser;\n"
602 "StartupWMClass=chrome-app-list\n",
603 GetDesktopFileContentsForCommand(
607 base::ASCIIToUTF16("Chrome App Launcher"),
609 "Network;WebBrowser;",
613 TEST(ShellIntegrationTest
, GetDirectoryFileContents
) {
615 const char* const title
;
616 const char* const icon_name
;
617 const char* const expected_output
;
630 // Make sure that empty icons are replaced by the chrome icon.
638 #if defined(GOOGLE_CHROME_BUILD)
639 "Icon=google-chrome\n"
641 "Icon=chromium-browser\n"
646 for (size_t i
= 0; i
< arraysize(test_cases
); i
++) {
648 EXPECT_EQ(test_cases
[i
].expected_output
,
649 GetDirectoryFileContents(base::ASCIIToUTF16(test_cases
[i
].title
),
650 test_cases
[i
].icon_name
));
654 } // namespace shell_integration_linux