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"
14 #include "base/base_paths.h"
15 #include "base/command_line.h"
16 #include "base/environment.h"
17 #include "base/files/file_path.h"
18 #include "base/files/file_util.h"
19 #include "base/files/scoped_temp_dir.h"
20 #include "base/stl_util.h"
21 #include "base/strings/string_util.h"
22 #include "base/strings/utf_string_conversions.h"
23 #include "base/test/scoped_path_override.h"
24 #include "build/branding_buildflags.h"
25 #include "chrome/browser/web_applications/components/web_app_helpers.h"
26 #include "chrome/browser/web_applications/components/web_app_id.h"
27 #include "chrome/common/chrome_constants.h"
28 #include "components/services/app_service/public/cpp/file_handler.h"
29 #include "content/public/test/browser_task_environment.h"
30 #include "testing/gmock/include/gmock/gmock.h"
31 #include "testing/gtest/include/gtest/gtest.h"
34 using ::testing::ElementsAre;
36 namespace shell_integration_linux {
40 // Provides mock environment variables values based on a stored map.
41 class MockEnvironment : public base::Environment {
45 void Set(base::StringPiece name, const std::string& value) {
46 variables_[name.as_string()] = value;
49 bool GetVar(base::StringPiece variable_name, std::string* result) override {
50 if (base::Contains(variables_, variable_name.as_string())) {
51 *result = variables_[variable_name.as_string()];
58 bool SetVar(base::StringPiece variable_name,
59 const std::string& new_value) override {
64 bool UnSetVar(base::StringPiece variable_name) override {
70 std::map<std::string, std::string> variables_;
72 DISALLOW_COPY_AND_ASSIGN(MockEnvironment);
75 // This helps EXPECT_THAT(..., ElementsAre(...)) print out more meaningful
77 std::vector<std::string> FilePathsToStrings(
78 const std::vector<base::FilePath>& paths) {
79 std::vector<std::string> values;
80 for (const auto& path : paths)
81 values.push_back(path.value());
87 TEST(ShellIntegrationTest, GetDataWriteLocation) {
88 content::BrowserTaskEnvironment task_environment;
90 // Test that it returns $XDG_DATA_HOME.
93 base::ScopedPathOverride home_override(base::DIR_HOME,
94 base::FilePath("/home/user"),
97 env.Set("XDG_DATA_HOME", "/user/path");
98 base::FilePath path = GetDataWriteLocation(&env);
99 EXPECT_EQ("/user/path", path.value());
102 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
105 base::ScopedPathOverride home_override(base::DIR_HOME,
106 base::FilePath("/home/user"),
107 true /* absolute? */,
108 false /* create? */);
109 base::FilePath path = GetDataWriteLocation(&env);
110 EXPECT_EQ("/home/user/.local/share", path.value());
114 TEST(ShellIntegrationTest, GetDataSearchLocations) {
115 content::BrowserTaskEnvironment task_environment;
117 // Test that it returns $XDG_DATA_HOME + $XDG_DATA_DIRS.
120 base::ScopedPathOverride home_override(base::DIR_HOME,
121 base::FilePath("/home/user"),
122 true /* absolute? */,
123 false /* create? */);
124 env.Set("XDG_DATA_HOME", "/user/path");
125 env.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
127 FilePathsToStrings(GetDataSearchLocations(&env)),
128 ElementsAre("/user/path",
133 // Test that $XDG_DATA_HOME falls back to $HOME/.local/share.
136 base::ScopedPathOverride home_override(base::DIR_HOME,
137 base::FilePath("/home/user"),
138 true /* absolute? */,
139 false /* create? */);
140 env.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
142 FilePathsToStrings(GetDataSearchLocations(&env)),
143 ElementsAre("/home/user/.local/share",
148 // Test that if neither $XDG_DATA_HOME nor $HOME are specified, it still
152 env.Set("XDG_DATA_DIRS", "/system/path/1:/system/path/2");
153 std::vector<std::string> results =
154 FilePathsToStrings(GetDataSearchLocations(&env));
155 ASSERT_EQ(3U, results.size());
156 EXPECT_FALSE(results[0].empty());
157 EXPECT_EQ("/system/path/1", results[1]);
158 EXPECT_EQ("/system/path/2", results[2]);
161 // Test that $XDG_DATA_DIRS falls back to the two default paths.
164 base::ScopedPathOverride home_override(base::DIR_HOME,
165 base::FilePath("/home/user"),
166 true /* absolute? */,
167 false /* create? */);
168 env.Set("XDG_DATA_HOME", "/user/path");
170 FilePathsToStrings(GetDataSearchLocations(&env)),
171 ElementsAre("/user/path",
177 TEST(ShellIntegrationTest, GetExistingShortcutContents) {
178 const char kTemplateFilename[] = "shortcut-test.desktop";
179 base::FilePath kTemplateFilepath(kTemplateFilename);
180 const char kTestData1[] = "a magical testing string";
181 const char kTestData2[] = "a different testing string";
183 content::BrowserTaskEnvironment task_environment;
185 // Test that it searches $XDG_DATA_HOME/applications.
187 base::ScopedTempDir temp_dir;
188 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
191 env.Set("XDG_DATA_HOME", temp_dir.GetPath().value());
192 // Create a file in a non-applications directory. This should be ignored.
193 ASSERT_TRUE(base::WriteFile(temp_dir.GetPath().Append(kTemplateFilename),
196 base::CreateDirectory(temp_dir.GetPath().Append("applications")));
197 ASSERT_TRUE(base::WriteFile(
198 temp_dir.GetPath().Append("applications").Append(kTemplateFilename),
200 std::string contents;
202 GetExistingShortcutContents(&env, kTemplateFilepath, &contents));
203 EXPECT_EQ(kTestData1, contents);
206 // Test that it falls back to $HOME/.local/share/applications.
208 base::ScopedTempDir temp_dir;
209 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
212 base::ScopedPathOverride home_override(base::DIR_HOME, temp_dir.GetPath(),
213 true /* absolute? */,
214 false /* create? */);
215 ASSERT_TRUE(base::CreateDirectory(
216 temp_dir.GetPath().Append(".local/share/applications")));
217 ASSERT_TRUE(base::WriteFile(temp_dir.GetPath()
218 .Append(".local/share/applications")
219 .Append(kTemplateFilename),
221 std::string contents;
223 GetExistingShortcutContents(&env, kTemplateFilepath, &contents));
224 EXPECT_EQ(kTestData1, contents);
227 // Test that it searches $XDG_DATA_DIRS/applications.
229 base::ScopedTempDir temp_dir;
230 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
233 env.Set("XDG_DATA_DIRS", temp_dir.GetPath().value());
235 base::CreateDirectory(temp_dir.GetPath().Append("applications")));
236 ASSERT_TRUE(base::WriteFile(
237 temp_dir.GetPath().Append("applications").Append(kTemplateFilename),
239 std::string contents;
241 GetExistingShortcutContents(&env, kTemplateFilepath, &contents));
242 EXPECT_EQ(kTestData2, contents);
245 // Test that it searches $X/applications for each X in $XDG_DATA_DIRS.
247 base::ScopedTempDir temp_dir1;
248 ASSERT_TRUE(temp_dir1.CreateUniqueTempDir());
249 base::ScopedTempDir temp_dir2;
250 ASSERT_TRUE(temp_dir2.CreateUniqueTempDir());
253 env.Set("XDG_DATA_DIRS",
254 temp_dir1.GetPath().value() + ":" + temp_dir2.GetPath().value());
255 // Create a file in a non-applications directory. This should be ignored.
256 ASSERT_TRUE(base::WriteFile(temp_dir1.GetPath().Append(kTemplateFilename),
258 // Only create a findable desktop file in the second path.
260 base::CreateDirectory(temp_dir2.GetPath().Append("applications")));
261 ASSERT_TRUE(base::WriteFile(
262 temp_dir2.GetPath().Append("applications").Append(kTemplateFilename),
264 std::string contents;
266 GetExistingShortcutContents(&env, kTemplateFilepath, &contents));
267 EXPECT_EQ(kTestData2, contents);
271 TEST(ShellIntegrationTest, GetExistingProfileShortcutFilenames) {
272 base::FilePath kProfilePath("a/b/c/Profile Name?");
273 const char kApp1Filename[] = "chrome-extension1-Profile_Name_.desktop";
274 const char kApp2Filename[] = "chrome-extension2-Profile_Name_.desktop";
275 const char kUnrelatedAppFilename[] = "chrome-extension-Other_Profile.desktop";
277 content::BrowserTaskEnvironment task_environment;
279 base::ScopedTempDir temp_dir;
280 ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
281 ASSERT_TRUE(base::WriteFile(temp_dir.GetPath().Append(kApp1Filename), ""));
282 ASSERT_TRUE(base::WriteFile(temp_dir.GetPath().Append(kApp2Filename), ""));
283 // This file should not be returned in the results.
285 base::WriteFile(temp_dir.GetPath().Append(kUnrelatedAppFilename), ""));
286 std::vector<base::FilePath> paths =
287 GetExistingProfileShortcutFilenames(kProfilePath, temp_dir.GetPath());
288 // Path order is arbitrary. Sort the output for consistency.
289 std::sort(paths.begin(), paths.end());
291 ElementsAre(base::FilePath(kApp1Filename),
292 base::FilePath(kApp2Filename)));
295 TEST(ShellIntegrationTest, GetWebShortcutFilename) {
297 const char* const path;
298 const char* const url;
300 { "http___foo_.desktop", "http://foo" },
301 { "http___foo_bar_.desktop", "http://foo/bar/" },
302 { "http___foo_bar_a=b&c=d.desktop", "http://foo/bar?a=b&c=d" },
304 // Now we're starting to be more evil...
305 { "http___foo_.desktop", "http://foo/bar/baz/../../../../../" },
306 { "http___foo_.desktop", "http://foo/bar/././../baz/././../" },
307 { "http___.._.desktop", "http://../../../../" },
309 for (size_t i = 0; i < base::size(test_cases); i++) {
310 EXPECT_EQ(std::string(chrome::kBrowserProcessExecutableName) + "-" +
312 GetWebShortcutFilename(GURL(test_cases[i].url)).value()) <<
313 " while testing " << test_cases[i].url;
317 TEST(ShellIntegrationTest, GetDesktopFileContents) {
318 const base::FilePath kChromeExePath("/opt/google/chrome/google-chrome");
320 const char* const url;
321 const char* const title;
322 const char* const icon_name;
323 const char* const categories;
324 const char* const mime_type;
326 const char* const expected_output;
329 {"http://gmail.com", "GMail", "chrome-http__gmail.com", "", "", false,
331 "#!/usr/bin/env xdg-open\n"
337 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
338 "Icon=chrome-http__gmail.com\n"
339 "StartupWMClass=gmail.com\n"},
341 // Make sure that empty icons are replaced by the chrome icon.
342 {"http://gmail.com", "GMail", "", "", "", false,
344 "#!/usr/bin/env xdg-open\n"
350 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
351 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
352 "Icon=google-chrome\n"
354 "Icon=chromium-browser\n"
356 "StartupWMClass=gmail.com\n"},
358 // Test adding categories and NoDisplay=true.
359 {"http://gmail.com", "GMail", "chrome-http__gmail.com",
360 "Graphics;Education;", "", true,
362 "#!/usr/bin/env xdg-open\n"
368 "Exec=/opt/google/chrome/google-chrome --app=http://gmail.com/\n"
369 "Icon=chrome-http__gmail.com\n"
370 "Categories=Graphics;Education;\n"
372 "StartupWMClass=gmail.com\n"},
374 // Now we're starting to be more evil...
375 {"http://evil.com/evil --join-the-b0tnet", "Ownz0red\nExec=rm -rf /",
376 "chrome-http__evil.com_evil", "", "", false,
378 "#!/usr/bin/env xdg-open\n"
383 "Name=http://evil.com/evil%20--join-the-b0tnet\n"
384 "Exec=/opt/google/chrome/google-chrome "
385 "--app=http://evil.com/evil%20--join-the-b0tnet\n"
386 "Icon=chrome-http__evil.com_evil\n"
387 "StartupWMClass=evil.com__evil%20--join-the-b0tnet\n"},
388 {"http://evil.com/evil; rm -rf /; \"; rm -rf $HOME >ownz0red",
389 "Innocent Title", "chrome-http__evil.com_evil", "", "", false,
391 "#!/usr/bin/env xdg-open\n"
396 "Name=Innocent Title\n"
397 "Exec=/opt/google/chrome/google-chrome "
398 "\"--app=http://evil.com/evil;%20rm%20-rf%20/;%20%22;%20rm%20"
399 // Note: $ is escaped as \$ within an arg to Exec, and then
400 // the \ is escaped as \\ as all strings in a Desktop file should
401 // be; finally, \\ becomes \\\\ when represented in a C++ string!
402 "-rf%20\\\\$HOME%20%3Eownz0red\"\n"
403 "Icon=chrome-http__evil.com_evil\n"
404 "StartupWMClass=evil.com__evil;%20rm%20-rf%20_;%20%22;%20"
405 "rm%20-rf%20$HOME%20%3Eownz0red\n"},
406 {"http://evil.com/evil | cat `echo ownz0red` >/dev/null",
407 "Innocent Title", "chrome-http__evil.com_evil", "", "", false,
409 "#!/usr/bin/env xdg-open\n"
414 "Name=Innocent Title\n"
415 "Exec=/opt/google/chrome/google-chrome "
416 "--app=http://evil.com/evil%20%7C%20cat%20%60echo%20ownz0red"
417 "%60%20%3E/dev/null\n"
418 "Icon=chrome-http__evil.com_evil\n"
419 "StartupWMClass=evil.com__evil%20%7C%20cat%20%60echo%20ownz0red"
420 "%60%20%3E_dev_null\n"},
421 // Test setting mime type
422 {"https://paint.app", "Paint", "chrome-https__paint.app", "Image",
423 "image/png;image/jpg", false,
425 "#!/usr/bin/env xdg-open\n"
431 "MimeType=image/png;image/jpg\n"
432 "Exec=/opt/google/chrome/google-chrome --app=https://paint.app/ %F\n"
433 "Icon=chrome-https__paint.app\n"
435 "StartupWMClass=paint.app\n"},
437 // Test evil mime type.
438 {"https://paint.app", "Evil Paint", "chrome-https__paint.app", "Image",
439 "image/png\nExec=rm -rf /", false,
441 "#!/usr/bin/env xdg-open\n"
447 "Exec=/opt/google/chrome/google-chrome --app=https://paint.app/\n"
448 "Icon=chrome-https__paint.app\n"
450 "StartupWMClass=paint.app\n"}};
452 for (size_t i = 0; i < base::size(test_cases); i++) {
455 test_cases[i].expected_output,
456 GetDesktopFileContents(
458 web_app::GenerateApplicationNameFromURL(GURL(test_cases[i].url)),
459 GURL(test_cases[i].url), std::string(),
460 base::ASCIIToUTF16(test_cases[i].title), test_cases[i].icon_name,
461 base::FilePath(), test_cases[i].categories, test_cases[i].mime_type,
462 test_cases[i].nodisplay));
466 TEST(ShellIntegrationTest, GetDesktopFileContentsAppList) {
467 const base::FilePath kChromeExePath("/opt/google/chrome/google-chrome");
468 base::CommandLine command_line(kChromeExePath);
469 command_line.AppendSwitch("--show-app-list");
471 "#!/usr/bin/env xdg-open\n"
476 "Name=Chrome App Launcher\n"
477 "Exec=/opt/google/chrome/google-chrome --show-app-list\n"
478 "Icon=chrome_app_list\n"
479 "Categories=Network;WebBrowser;\n"
480 "StartupWMClass=chrome-app-list\n",
481 GetDesktopFileContentsForCommand(
482 command_line, "chrome-app-list", GURL(),
483 base::ASCIIToUTF16("Chrome App Launcher"), "chrome_app_list",
484 "Network;WebBrowser;", "", false));
487 TEST(ShellIntegrationTest, GetDirectoryFileContents) {
489 const char* const title;
490 const char* const icon_name;
491 const char* const expected_output;
494 {"Chrome Apps", "chrome-apps",
500 "Icon=chrome-apps\n"},
502 // Make sure that empty icons are replaced by the chrome icon.
509 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
510 "Icon=google-chrome\n"
512 "Icon=chromium-browser\n"
517 for (size_t i = 0; i < base::size(test_cases); i++) {
519 EXPECT_EQ(test_cases[i].expected_output,
520 GetDirectoryFileContents(base::ASCIIToUTF16(test_cases[i].title),
521 test_cases[i].icon_name));
525 TEST(ShellIntegrationTest, GetMimeTypesRegistrationFilename) {
527 const char* const profile_path;
528 const char* const app_id;
529 const char* const expected_filename;
531 {"Default", "app-id", "-app-id-Default.xml"},
532 {"Default Profile", "app-id", "-app-id-Default_Profile.xml"},
533 {"foo/Default", "app-id", "-app-id-Default.xml"},
534 {"Default*Profile", "app-id", "-app-id-Default_Profile.xml"}};
535 std::string browser_name(chrome::kBrowserProcessExecutableName);
537 for (const auto& test_case : test_cases) {
538 const base::FilePath filename =
539 GetMimeTypesRegistrationFilename(base::FilePath(test_case.profile_path),
540 web_app::AppId(test_case.app_id));
541 EXPECT_EQ(browser_name + test_case.expected_filename, filename.value());
545 TEST(ShellIntegrationTest, GetMimeTypesRegistrationFileContents) {
546 apps::FileHandlers file_handlers;
548 apps::FileHandler file_handler;
550 apps::FileHandler::AcceptEntry accept_entry;
551 accept_entry.mime_type = "application/foo";
552 accept_entry.file_extensions.insert(".foo");
553 file_handler.accept.push_back(accept_entry);
555 file_handlers.push_back(file_handler);
558 apps::FileHandler file_handler;
560 apps::FileHandler::AcceptEntry accept_entry;
561 accept_entry.mime_type = "application/foobar";
562 accept_entry.file_extensions.insert(".foobar");
563 file_handler.accept.push_back(accept_entry);
565 file_handlers.push_back(file_handler);
568 apps::FileHandler file_handler;
570 apps::FileHandler::AcceptEntry accept_entry;
571 accept_entry.mime_type = "application/bar";
572 accept_entry.file_extensions.insert(".bar");
573 accept_entry.file_extensions.insert(".baz");
574 file_handler.accept.push_back(accept_entry);
576 file_handlers.push_back(file_handler);
579 const std::string file_contents =
580 GetMimeTypesRegistrationFileContents(file_handlers);
581 const std::string expected_file_contents =
582 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
584 "xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n"
585 " <mime-type type=\"application/foo\">\n"
586 " <glob pattern=\"*.foo\"/>\n"
588 " <mime-type type=\"application/foobar\">\n"
589 " <glob pattern=\"*.foobar\"/>\n"
591 " <mime-type type=\"application/bar\">\n"
592 " <glob pattern=\"*.bar\"/>\n"
593 " <glob pattern=\"*.baz\"/>\n"
597 EXPECT_EQ(file_contents, expected_file_contents);
600 TEST(ShellIntegrationTest, WmClass) {
601 base::CommandLine command_line((base::FilePath()));
602 EXPECT_EQ("foo", internal::GetProgramClassName(command_line, "foo.desktop"));
603 EXPECT_EQ("Foo", internal::GetProgramClassClass(command_line, "foo.desktop"));
605 command_line.AppendSwitchASCII("class", "baR");
606 EXPECT_EQ("foo", internal::GetProgramClassName(command_line, "foo.desktop"));
607 EXPECT_EQ("baR", internal::GetProgramClassClass(command_line, "foo.desktop"));
609 command_line = base::CommandLine(base::FilePath());
610 command_line.AppendSwitchASCII("user-data-dir", "/tmp/baz");
611 EXPECT_EQ("foo (/tmp/baz)",
612 internal::GetProgramClassName(command_line, "foo.desktop"));
613 EXPECT_EQ("Foo", internal::GetProgramClassClass(command_line, "foo.desktop"));
616 } // namespace shell_integration_linux