1 // Copyright (c) 2012 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. 4 5 #include "chrome/browser/shell_integration_linux.h" 6 7 #include <fcntl.h> 8 9 #if defined(USE_GLIB) 10 #include <glib.h> 11 #endif 12 13 #include <stdlib.h> 14 #include <sys/stat.h> 15 #include <sys/types.h> 16 #include <unistd.h> 17 18 #include <string> 19 #include <vector> 20 21 #include "base/base_paths.h" 22 #include "base/command_line.h" 23 #include "base/environment.h" 24 #include "base/files/file_enumerator.h" 25 #include "base/files/file_path.h" 26 #include "base/files/file_util.h" 27 #include "base/files/scoped_temp_dir.h" 28 #include "base/i18n/file_util_icu.h" 29 #include "base/memory/ref_counted_memory.h" 30 #include "base/memory/scoped_ptr.h" 31 #include "base/message_loop/message_loop.h" 32 #include "base/path_service.h" 33 #include "base/posix/eintr_wrapper.h" 34 #include "base/process/kill.h" 35 #include "base/process/launch.h" 36 #include "base/strings/string_number_conversions.h" 37 #include "base/strings/string_tokenizer.h" 38 #include "base/strings/string_util.h" 39 #include "base/strings/utf_string_conversions.h" 40 #include "base/threading/thread.h" 41 #include "base/threading/thread_restrictions.h" 42 #include "build/build_config.h" 43 #include "chrome/browser/shell_integration.h" 44 #include "chrome/common/chrome_constants.h" 45 #include "chrome/common/chrome_switches.h" 46 #include "chrome/common/chrome_version_info.h" 47 #include "content/public/browser/browser_thread.h" 48 #include "grit/chrome_unscaled_resources.h" 49 #include "ui/base/resource/resource_bundle.h" 50 #include "ui/gfx/image/image_family.h" 51 #include "url/gurl.h" 52 53 using content::BrowserThread; 54 55 namespace { 56 57 // The Categories for the App Launcher desktop shortcut. Should be the same as 58 // the Chrome desktop shortcut, so they are in the same sub-menu. 59 const char kAppListCategories[] = "Network;WebBrowser;"; 60 61 // Helper to launch xdg scripts. We don't want them to ask any questions on the 62 // terminal etc. The function returns true if the utility launches and exits 63 // cleanly, in which case |exit_code| returns the utility's exit code. 64 bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) { 65 // xdg-settings internally runs xdg-mime, which uses mv to move newly-created 66 // files on top of originals after making changes to them. In the event that 67 // the original files are owned by another user (e.g. root, which can happen 68 // if they are updated within sudo), mv will prompt the user to confirm if 69 // standard input is a terminal (otherwise it just does it). So make sure it's 70 // not, to avoid locking everything up waiting for mv. 71 *exit_code = EXIT_FAILURE; 72 int devnull = open("/dev/null", O_RDONLY); 73 if (devnull < 0) 74 return false; 75 base::FileHandleMappingVector no_stdin; 76 no_stdin.push_back(std::make_pair(devnull, STDIN_FILENO)); 77 78 base::ProcessHandle handle; 79 base::LaunchOptions options; 80 options.fds_to_remap = &no_stdin; 81 if (!base::LaunchProcess(argv, options, &handle)) { 82 close(devnull); 83 return false; 84 } 85 close(devnull); 86 87 return base::WaitForExitCode(handle, exit_code); 88 } 89 90 std::string CreateShortcutIcon(const gfx::ImageFamily& icon_images, 91 const base::FilePath& shortcut_filename) { 92 if (icon_images.empty()) 93 return std::string(); 94 95 // TODO(phajdan.jr): Report errors from this function, possibly as infobars. 96 base::ScopedTempDir temp_dir; 97 if (!temp_dir.CreateUniqueTempDir()) 98 return std::string(); 99 100 base::FilePath temp_file_path = temp_dir.path().Append( 101 shortcut_filename.ReplaceExtension("png")); 102 std::string icon_name = temp_file_path.BaseName().RemoveExtension().value(); 103 104 for (gfx::ImageFamily::const_iterator it = icon_images.begin(); 105 it != icon_images.end(); ++it) { 106 int width = it->Width(); 107 scoped_refptr<base::RefCountedMemory> png_data = it->As1xPNGBytes(); 108 if (png_data->size() == 0) { 109 // If the bitmap could not be encoded to PNG format, skip it. 110 LOG(WARNING) << "Could not encode icon " << icon_name << ".png at size " 111 << width << "."; 112 continue; 113 } 114 int bytes_written = base::WriteFile(temp_file_path, 115 png_data->front_as<char>(), 116 png_data->size()); 117 118 if (bytes_written != static_cast<int>(png_data->size())) 119 return std::string(); 120 121 std::vector<std::string> argv; 122 argv.push_back("xdg-icon-resource"); 123 argv.push_back("install"); 124 125 // Always install in user mode, even if someone runs the browser as root 126 // (people do that). 127 argv.push_back("--mode"); 128 argv.push_back("user"); 129 130 argv.push_back("--size"); 131 argv.push_back(base::IntToString(width)); 132 133 argv.push_back(temp_file_path.value()); 134 argv.push_back(icon_name); 135 int exit_code; 136 if (!LaunchXdgUtility(argv, &exit_code) || exit_code) { 137 LOG(WARNING) << "Could not install icon " << icon_name << ".png at size " 138 << width << "."; 139 } 140 } 141 return icon_name; 142 } 143 144 bool CreateShortcutOnDesktop(const base::FilePath& shortcut_filename, 145 const std::string& contents) { 146 // Make sure that we will later call openat in a secure way. 147 DCHECK_EQ(shortcut_filename.BaseName().value(), shortcut_filename.value()); 148 149 base::FilePath desktop_path; 150 if (!PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) 151 return false; 152 153 int desktop_fd = open(desktop_path.value().c_str(), O_RDONLY | O_DIRECTORY); 154 if (desktop_fd < 0) 155 return false; 156 157 int fd = openat(desktop_fd, shortcut_filename.value().c_str(), 158 O_CREAT | O_EXCL | O_WRONLY, 159 S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 160 if (fd < 0) { 161 if (IGNORE_EINTR(close(desktop_fd)) < 0) 162 PLOG(ERROR) << "close"; 163 return false; 164 } 165 166 ssize_t bytes_written = base::WriteFileDescriptor(fd, contents.data(), 167 contents.length()); 168 if (IGNORE_EINTR(close(fd)) < 0) 169 PLOG(ERROR) << "close"; 170 171 if (bytes_written != static_cast<ssize_t>(contents.length())) { 172 // Delete the file. No shortuct is better than corrupted one. Use unlinkat 173 // to make sure we're deleting the file in the directory we think we are. 174 // Even if an attacker manager to put something other at 175 // |shortcut_filename| we'll just undo his action. 176 unlinkat(desktop_fd, shortcut_filename.value().c_str(), 0); 177 } 178 179 if (IGNORE_EINTR(close(desktop_fd)) < 0) 180 PLOG(ERROR) << "close"; 181 182 return true; 183 } 184 185 void DeleteShortcutOnDesktop(const base::FilePath& shortcut_filename) { 186 base::FilePath desktop_path; 187 if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) 188 base::DeleteFile(desktop_path.Append(shortcut_filename), false); 189 } 190 191 // Creates a shortcut with |shortcut_filename| and |contents| in the system 192 // applications menu. If |directory_filename| is non-empty, creates a sub-menu 193 // with |directory_filename| and |directory_contents|, and stores the shortcut 194 // under the sub-menu. 195 bool CreateShortcutInApplicationsMenu(const base::FilePath& shortcut_filename, 196 const std::string& contents, 197 const base::FilePath& directory_filename, 198 const std::string& directory_contents) { 199 base::ScopedTempDir temp_dir; 200 if (!temp_dir.CreateUniqueTempDir()) 201 return false; 202 203 base::FilePath temp_directory_path; 204 if (!directory_filename.empty()) { 205 temp_directory_path = temp_dir.path().Append(directory_filename); 206 207 int bytes_written = base::WriteFile(temp_directory_path, 208 directory_contents.data(), 209 directory_contents.length()); 210 211 if (bytes_written != static_cast<int>(directory_contents.length())) 212 return false; 213 } 214 215 base::FilePath temp_file_path = temp_dir.path().Append(shortcut_filename); 216 217 int bytes_written = base::WriteFile(temp_file_path, contents.data(), 218 contents.length()); 219 220 if (bytes_written != static_cast<int>(contents.length())) 221 return false; 222 223 std::vector<std::string> argv; 224 argv.push_back("xdg-desktop-menu"); 225 argv.push_back("install"); 226 227 // Always install in user mode, even if someone runs the browser as root 228 // (people do that). 229 argv.push_back("--mode"); 230 argv.push_back("user"); 231 232 // If provided, install the shortcut file inside the given directory. 233 if (!directory_filename.empty()) 234 argv.push_back(temp_directory_path.value()); 235 argv.push_back(temp_file_path.value()); 236 int exit_code; 237 LaunchXdgUtility(argv, &exit_code); 238 return exit_code == 0; 239 } 240 241 void DeleteShortcutInApplicationsMenu( 242 const base::FilePath& shortcut_filename, 243 const base::FilePath& directory_filename) { 244 std::vector<std::string> argv; 245 argv.push_back("xdg-desktop-menu"); 246 argv.push_back("uninstall"); 247 248 // Uninstall in user mode, to match the install. 249 argv.push_back("--mode"); 250 argv.push_back("user"); 251 252 // The file does not need to exist anywhere - xdg-desktop-menu will uninstall 253 // items from the menu with a matching name. 254 // If |directory_filename| is supplied, this will also remove the item from 255 // the directory, and remove the directory if it is empty. 256 if (!directory_filename.empty()) 257 argv.push_back(directory_filename.value()); 258 argv.push_back(shortcut_filename.value()); 259 int exit_code; 260 LaunchXdgUtility(argv, &exit_code); 261 } 262 263 #if defined(USE_GLIB) 264 // Quote a string such that it appears as one verbatim argument for the Exec 265 // key in a desktop file. 266 std::string QuoteArgForDesktopFileExec(const std::string& arg) { 267 // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html 268 269 // Quoting is only necessary if the argument has a reserved character. 270 if (arg.find_first_of(" \t\n\"'\\><~|&;$*?#()`") == std::string::npos) 271 return arg; // No quoting necessary. 272 273 std::string quoted = "\""; 274 for (size_t i = 0; i < arg.size(); ++i) { 275 // Note that the set of backslashed characters is smaller than the 276 // set of reserved characters. 277 switch (arg[i]) { 278 case '"': 279 case '`': 280 case '$': 281 case '\\': 282 quoted += '\\'; 283 break; 284 } 285 quoted += arg[i]; 286 } 287 quoted += '"'; 288 289 return quoted; 290 } 291 292 // Quote a command line so it is suitable for use as the Exec key in a desktop 293 // file. Note: This should be used instead of GetCommandLineString, which does 294 // not properly quote the string; this function is designed for the Exec key. 295 std::string QuoteCommandLineForDesktopFileExec( 296 const CommandLine& command_line) { 297 // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html 298 299 std::string quoted_path = ""; 300 const CommandLine::StringVector& argv = command_line.argv(); 301 for (CommandLine::StringVector::const_iterator i = argv.begin(); 302 i != argv.end(); ++i) { 303 if (i != argv.begin()) 304 quoted_path += " "; 305 quoted_path += QuoteArgForDesktopFileExec(*i); 306 } 307 308 return quoted_path; 309 } 310 311 const char kDesktopEntry[] = "Desktop Entry"; 312 313 const char kXdgOpenShebang[] = "#!/usr/bin/env xdg-open"; 314 #endif 315 316 const char kXdgSettings[] = "xdg-settings"; 317 const char kXdgSettingsDefaultBrowser[] = "default-web-browser"; 318 const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler"; 319 320 const char kDirectoryFilename[] = "chrome-apps.directory"; 321 322 #if defined(GOOGLE_CHROME_BUILD) 323 const char kAppListDesktopName[] = "chrome-app-list"; 324 #else // CHROMIUM_BUILD 325 const char kAppListDesktopName[] = "chromium-app-list"; 326 #endif 327 328 // Utility function to get the path to the version of a script shipped with 329 // Chrome. |script| gives the name of the script. |chrome_version| returns the 330 // path to the Chrome version of the script, and the return value of the 331 // function is true if the function is successful and the Chrome version is 332 // not the script found on the PATH. 333 bool GetChromeVersionOfScript(const std::string& script, 334 std::string* chrome_version) { 335 // Get the path to the Chrome version. 336 base::FilePath chrome_dir; 337 if (!PathService::Get(base::DIR_EXE, &chrome_dir)) 338 return false; 339 340 base::FilePath chrome_version_path = chrome_dir.Append(script); 341 *chrome_version = chrome_version_path.value(); 342 343 // Check if this is different to the one on path. 344 std::vector<std::string> argv; 345 argv.push_back("which"); 346 argv.push_back(script); 347 std::string path_version; 348 if (base::GetAppOutput(CommandLine(argv), &path_version)) { 349 // Remove trailing newline 350 path_version.erase(path_version.length() - 1, 1); 351 base::FilePath path_version_path(path_version); 352 return (chrome_version_path != path_version_path); 353 } 354 return false; 355 } 356 357 // Value returned by xdg-settings if it can't understand our request. 358 const int EXIT_XDG_SETTINGS_SYNTAX_ERROR = 1; 359 360 // We delegate the difficulty of setting the default browser and default url 361 // scheme handler in Linux desktop environments to an xdg utility, xdg-settings. 362 363 // When calling this script we first try to use the script on PATH. If that 364 // fails we then try to use the script that we have included. This gives 365 // scripts on the system priority over ours, as distribution vendors may have 366 // tweaked the script, but still allows our copy to be used if the script on the 367 // system fails, as the system copy may be missing capabilities of the Chrome 368 // copy. 369 370 // If |protocol| is empty this function sets Chrome as the default browser, 371 // otherwise it sets Chrome as the default handler application for |protocol|. 372 bool SetDefaultWebClient(const std::string& protocol) { 373 #if defined(OS_CHROMEOS) 374 return true; 375 #else 376 scoped_ptr<base::Environment> env(base::Environment::Create()); 377 378 std::vector<std::string> argv; 379 argv.push_back(kXdgSettings); 380 argv.push_back("set"); 381 if (protocol.empty()) { 382 argv.push_back(kXdgSettingsDefaultBrowser); 383 } else { 384 argv.push_back(kXdgSettingsDefaultSchemeHandler); 385 argv.push_back(protocol); 386 } 387 argv.push_back(shell_integration_linux::GetDesktopName(env.get())); 388 389 int exit_code; 390 bool ran_ok = LaunchXdgUtility(argv, &exit_code); 391 if (ran_ok && exit_code == EXIT_XDG_SETTINGS_SYNTAX_ERROR) { 392 if (GetChromeVersionOfScript(kXdgSettings, &argv[0])) { 393 ran_ok = LaunchXdgUtility(argv, &exit_code); 394 } 395 } 396 397 return ran_ok && exit_code == EXIT_SUCCESS; 398 #endif 399 } 400 401 // If |protocol| is empty this function checks if Chrome is the default browser, 402 // otherwise it checks if Chrome is the default handler application for 403 // |protocol|. 404 ShellIntegration::DefaultWebClientState GetIsDefaultWebClient( 405 const std::string& protocol) { 406 #if defined(OS_CHROMEOS) 407 return ShellIntegration::UNKNOWN_DEFAULT; 408 #else 409 base::ThreadRestrictions::AssertIOAllowed(); 410 411 scoped_ptr<base::Environment> env(base::Environment::Create()); 412 413 std::vector<std::string> argv; 414 argv.push_back(kXdgSettings); 415 argv.push_back("check"); 416 if (protocol.empty()) { 417 argv.push_back(kXdgSettingsDefaultBrowser); 418 } else { 419 argv.push_back(kXdgSettingsDefaultSchemeHandler); 420 argv.push_back(protocol); 421 } 422 argv.push_back(shell_integration_linux::GetDesktopName(env.get())); 423 424 std::string reply; 425 int success_code; 426 bool ran_ok = base::GetAppOutputWithExitCode(CommandLine(argv), &reply, 427 &success_code); 428 if (ran_ok && success_code == EXIT_XDG_SETTINGS_SYNTAX_ERROR) { 429 if (GetChromeVersionOfScript(kXdgSettings, &argv[0])) { 430 ran_ok = base::GetAppOutputWithExitCode(CommandLine(argv), &reply, 431 &success_code); 432 } 433 } 434 435 if (!ran_ok || success_code != EXIT_SUCCESS) { 436 // xdg-settings failed: we can't determine or set the default browser. 437 return ShellIntegration::UNKNOWN_DEFAULT; 438 } 439 440 // Allow any reply that starts with "yes". 441 return (reply.find("yes") == 0) ? ShellIntegration::IS_DEFAULT : 442 ShellIntegration::NOT_DEFAULT; 443 #endif 444 } 445 446 // Get the value of NoDisplay from the [Desktop Entry] section of a .desktop 447 // file, given in |shortcut_contents|. If the key is not found, returns false. 448 bool GetNoDisplayFromDesktopFile(const std::string& shortcut_contents) { 449 #if defined(USE_GLIB) 450 // An empty file causes a crash with glib <= 2.32, so special case here. 451 if (shortcut_contents.empty()) 452 return false; 453 454 GKeyFile* key_file = g_key_file_new(); 455 GError* err = NULL; 456 if (!g_key_file_load_from_data(key_file, shortcut_contents.c_str(), 457 shortcut_contents.size(), G_KEY_FILE_NONE, 458 &err)) { 459 LOG(WARNING) << "Unable to read desktop file template: " << err->message; 460 g_error_free(err); 461 g_key_file_free(key_file); 462 return false; 463 } 464 465 bool nodisplay = false; 466 char* nodisplay_c_string = g_key_file_get_string(key_file, kDesktopEntry, 467 "NoDisplay", &err); 468 if (nodisplay_c_string) { 469 if (!g_strcmp0(nodisplay_c_string, "true")) 470 nodisplay = true; 471 g_free(nodisplay_c_string); 472 } else { 473 g_error_free(err); 474 } 475 476 g_key_file_free(key_file); 477 return nodisplay; 478 #else 479 NOTIMPLEMENTED(); 480 return false; 481 #endif 482 } 483 484 // Gets the path to the Chrome executable or wrapper script. 485 // Returns an empty path if the executable path could not be found. 486 base::FilePath GetChromeExePath() { 487 // Try to get the name of the wrapper script that launched Chrome. 488 scoped_ptr<base::Environment> environment(base::Environment::Create()); 489 std::string wrapper_script; 490 if (environment->GetVar("CHROME_WRAPPER", &wrapper_script)) { 491 return base::FilePath(wrapper_script); 492 } 493 494 // Just return the name of the executable path for Chrome. 495 base::FilePath chrome_exe_path; 496 PathService::Get(base::FILE_EXE, &chrome_exe_path); 497 return chrome_exe_path; 498 } 499 500 } // namespace 501 502 // static 503 ShellIntegration::DefaultWebClientSetPermission 504 ShellIntegration::CanSetAsDefaultBrowser() { 505 return SET_DEFAULT_UNATTENDED; 506 } 507 508 // static 509 bool ShellIntegration::SetAsDefaultBrowser() { 510 return SetDefaultWebClient(std::string()); 511 } 512 513 // static 514 bool ShellIntegration::SetAsDefaultProtocolClient( 515 const std::string& protocol) { 516 return SetDefaultWebClient(protocol); 517 } 518 519 // static 520 ShellIntegration::DefaultWebClientState 521 ShellIntegration::GetDefaultBrowser() { 522 return GetIsDefaultWebClient(std::string()); 523 } 524 525 // static 526 base::string16 ShellIntegration::GetApplicationNameForProtocol( 527 const GURL& url) { 528 return base::ASCIIToUTF16("xdg-open"); 529 } 530 531 // static 532 ShellIntegration::DefaultWebClientState 533 ShellIntegration::IsDefaultProtocolClient(const std::string& protocol) { 534 return GetIsDefaultWebClient(protocol); 535 } 536 537 // static 538 bool ShellIntegration::IsFirefoxDefaultBrowser() { 539 std::vector<std::string> argv; 540 argv.push_back(kXdgSettings); 541 argv.push_back("get"); 542 argv.push_back(kXdgSettingsDefaultBrowser); 543 544 std::string browser; 545 // We don't care about the return value here. 546 base::GetAppOutput(CommandLine(argv), &browser); 547 return browser.find("irefox") != std::string::npos; 548 } 549 550 namespace shell_integration_linux { 551 552 bool GetDataWriteLocation(base::Environment* env, base::FilePath* search_path) { 553 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 554 555 std::string xdg_data_home; 556 std::string home; 557 if (env->GetVar("XDG_DATA_HOME", &xdg_data_home) && !xdg_data_home.empty()) { 558 *search_path = base::FilePath(xdg_data_home); 559 return true; 560 } else if (env->GetVar("HOME", &home) && !home.empty()) { 561 *search_path = base::FilePath(home).Append(".local").Append("share"); 562 return true; 563 } 564 return false; 565 } 566 567 std::vector<base::FilePath> GetDataSearchLocations(base::Environment* env) { 568 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 569 570 std::vector<base::FilePath> search_paths; 571 572 base::FilePath write_location; 573 if (GetDataWriteLocation(env, &write_location)) 574 search_paths.push_back(write_location); 575 576 std::string xdg_data_dirs; 577 if (env->GetVar("XDG_DATA_DIRS", &xdg_data_dirs) && !xdg_data_dirs.empty()) { 578 base::StringTokenizer tokenizer(xdg_data_dirs, ":"); 579 while (tokenizer.GetNext()) { 580 base::FilePath data_dir(tokenizer.token()); 581 search_paths.push_back(data_dir); 582 } 583 } else { 584 search_paths.push_back(base::FilePath("/usr/local/share")); 585 search_paths.push_back(base::FilePath("/usr/share")); 586 } 587 588 return search_paths; 589 } 590 591 std::string GetProgramClassName() { 592 DCHECK(CommandLine::InitializedForCurrentProcess()); 593 // Get the res_name component from argv[0]. 594 const CommandLine* command_line = CommandLine::ForCurrentProcess(); 595 std::string class_name = command_line->GetProgram().BaseName().value(); 596 if (!class_name.empty()) 597 class_name[0] = base::ToUpperASCII(class_name[0]); 598 return class_name; 599 } 600 601 std::string GetDesktopName(base::Environment* env) { 602 #if defined(GOOGLE_CHROME_BUILD) 603 chrome::VersionInfo::Channel product_channel( 604 chrome::VersionInfo::GetChannel()); 605 switch (product_channel) { 606 case chrome::VersionInfo::CHANNEL_DEV: 607 return "google-chrome-unstable.desktop"; 608 case chrome::VersionInfo::CHANNEL_BETA: 609 return "google-chrome-beta.desktop"; 610 default: 611 return "google-chrome.desktop"; 612 } 613 #else // CHROMIUM_BUILD 614 // Allow $CHROME_DESKTOP to override the built-in value, so that development 615 // versions can set themselves as the default without interfering with 616 // non-official, packaged versions using the built-in value. 617 std::string name; 618 if (env->GetVar("CHROME_DESKTOP", &name) && !name.empty()) 619 return name; 620 return "chromium-browser.desktop"; 621 #endif 622 } 623 624 std::string GetIconName() { 625 #if defined(GOOGLE_CHROME_BUILD) 626 return "google-chrome"; 627 #else // CHROMIUM_BUILD 628 return "chromium-browser"; 629 #endif 630 } 631 632 web_app::ShortcutLocations GetExistingShortcutLocations( 633 base::Environment* env, 634 const base::FilePath& profile_path, 635 const std::string& extension_id) { 636 base::FilePath desktop_path; 637 // If Get returns false, just leave desktop_path empty. 638 PathService::Get(base::DIR_USER_DESKTOP, &desktop_path); 639 return GetExistingShortcutLocations(env, profile_path, extension_id, 640 desktop_path); 641 } 642 643 web_app::ShortcutLocations GetExistingShortcutLocations( 644 base::Environment* env, 645 const base::FilePath& profile_path, 646 const std::string& extension_id, 647 const base::FilePath& desktop_path) { 648 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 649 650 base::FilePath shortcut_filename = GetExtensionShortcutFilename( 651 profile_path, extension_id); 652 DCHECK(!shortcut_filename.empty()); 653 web_app::ShortcutLocations locations; 654 655 // Determine whether there is a shortcut on desktop. 656 if (!desktop_path.empty()) { 657 locations.on_desktop = 658 base::PathExists(desktop_path.Append(shortcut_filename)); 659 } 660 661 // Determine whether there is a shortcut in the applications directory. 662 std::string shortcut_contents; 663 if (GetExistingShortcutContents(env, shortcut_filename, &shortcut_contents)) { 664 // If the shortcut contents contain NoDisplay=true, it should be hidden. 665 // Otherwise since these shortcuts are for apps, they are always in the 666 // "Chrome Apps" directory. 667 locations.applications_menu_location = 668 GetNoDisplayFromDesktopFile(shortcut_contents) 669 ? web_app::APP_MENU_LOCATION_HIDDEN 670 : web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; 671 } 672 673 return locations; 674 } 675 676 bool GetExistingShortcutContents(base::Environment* env, 677 const base::FilePath& desktop_filename, 678 std::string* output) { 679 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 680 681 std::vector<base::FilePath> search_paths = GetDataSearchLocations(env); 682 683 for (std::vector<base::FilePath>::const_iterator i = search_paths.begin(); 684 i != search_paths.end(); ++i) { 685 base::FilePath path = i->Append("applications").Append(desktop_filename); 686 VLOG(1) << "Looking for desktop file in " << path.value(); 687 if (base::PathExists(path)) { 688 VLOG(1) << "Found desktop file at " << path.value(); 689 return base::ReadFileToString(path, output); 690 } 691 } 692 693 return false; 694 } 695 696 base::FilePath GetWebShortcutFilename(const GURL& url) { 697 // Use a prefix, because xdg-desktop-menu requires it. 698 std::string filename = 699 std::string(chrome::kBrowserProcessExecutableName) + "-" + url.spec(); 700 base::i18n::ReplaceIllegalCharactersInPath(&filename, '_'); 701 702 base::FilePath desktop_path; 703 if (!PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) 704 return base::FilePath(); 705 706 base::FilePath filepath = desktop_path.Append(filename); 707 base::FilePath alternative_filepath(filepath.value() + ".desktop"); 708 for (size_t i = 1; i < 100; ++i) { 709 if (base::PathExists(base::FilePath(alternative_filepath))) { 710 alternative_filepath = base::FilePath( 711 filepath.value() + "_" + base::IntToString(i) + ".desktop"); 712 } else { 713 return base::FilePath(alternative_filepath).BaseName(); 714 } 715 } 716 717 return base::FilePath(); 718 } 719 720 base::FilePath GetExtensionShortcutFilename(const base::FilePath& profile_path, 721 const std::string& extension_id) { 722 DCHECK(!extension_id.empty()); 723 724 // Use a prefix, because xdg-desktop-menu requires it. 725 std::string filename(chrome::kBrowserProcessExecutableName); 726 filename.append("-") 727 .append(extension_id) 728 .append("-") 729 .append(profile_path.BaseName().value()); 730 base::i18n::ReplaceIllegalCharactersInPath(&filename, '_'); 731 // Spaces in filenames break xdg-desktop-menu 732 // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605). 733 base::ReplaceChars(filename, " ", "_", &filename); 734 return base::FilePath(filename.append(".desktop")); 735 } 736 737 std::vector<base::FilePath> GetExistingProfileShortcutFilenames( 738 const base::FilePath& profile_path, 739 const base::FilePath& directory) { 740 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 741 // Use a prefix, because xdg-desktop-menu requires it. 742 std::string prefix(chrome::kBrowserProcessExecutableName); 743 prefix.append("-"); 744 std::string suffix("-"); 745 suffix.append(profile_path.BaseName().value()); 746 base::i18n::ReplaceIllegalCharactersInPath(&suffix, '_'); 747 // Spaces in filenames break xdg-desktop-menu 748 // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605). 749 base::ReplaceChars(suffix, " ", "_", &suffix); 750 std::string glob = prefix + "*" + suffix + ".desktop"; 751 752 base::FileEnumerator files(directory, false, base::FileEnumerator::FILES, 753 glob); 754 base::FilePath shortcut_file = files.Next(); 755 std::vector<base::FilePath> shortcut_paths; 756 while (!shortcut_file.empty()) { 757 shortcut_paths.push_back(shortcut_file.BaseName()); 758 shortcut_file = files.Next(); 759 } 760 return shortcut_paths; 761 } 762 763 std::string GetDesktopFileContents( 764 const base::FilePath& chrome_exe_path, 765 const std::string& app_name, 766 const GURL& url, 767 const std::string& extension_id, 768 const base::string16& title, 769 const std::string& icon_name, 770 const base::FilePath& profile_path, 771 const std::string& categories, 772 bool no_display) { 773 CommandLine cmd_line = ShellIntegration::CommandLineArgsForLauncher( 774 url, extension_id, profile_path); 775 cmd_line.SetProgram(chrome_exe_path); 776 return GetDesktopFileContentsForCommand(cmd_line, app_name, url, title, 777 icon_name, categories, no_display); 778 } 779 780 std::string GetDesktopFileContentsForCommand( 781 const CommandLine& command_line, 782 const std::string& app_name, 783 const GURL& url, 784 const base::string16& title, 785 const std::string& icon_name, 786 const std::string& categories, 787 bool no_display) { 788 #if defined(USE_GLIB) 789 // Although not required by the spec, Nautilus on Ubuntu Karmic creates its 790 // launchers with an xdg-open shebang. Follow that convention. 791 std::string output_buffer = std::string(kXdgOpenShebang) + "\n"; 792 793 // See http://standards.freedesktop.org/desktop-entry-spec/latest/ 794 GKeyFile* key_file = g_key_file_new(); 795 796 // Set keys with fixed values. 797 g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0"); 798 g_key_file_set_string(key_file, kDesktopEntry, "Terminal", "false"); 799 g_key_file_set_string(key_file, kDesktopEntry, "Type", "Application"); 800 801 // Set the "Name" key. 802 std::string final_title = base::UTF16ToUTF8(title); 803 // Make sure no endline characters can slip in and possibly introduce 804 // additional lines (like Exec, which makes it a security risk). Also 805 // use the URL as a default when the title is empty. 806 if (final_title.empty() || 807 final_title.find("\n") != std::string::npos || 808 final_title.find("\r") != std::string::npos) { 809 final_title = url.spec(); 810 } 811 g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str()); 812 813 // Set the "Exec" key. 814 std::string final_path = QuoteCommandLineForDesktopFileExec(command_line); 815 g_key_file_set_string(key_file, kDesktopEntry, "Exec", final_path.c_str()); 816 817 // Set the "Icon" key. 818 if (!icon_name.empty()) { 819 g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str()); 820 } else { 821 g_key_file_set_string(key_file, kDesktopEntry, "Icon", 822 GetIconName().c_str()); 823 } 824 825 // Set the "Categories" key. 826 if (!categories.empty()) { 827 g_key_file_set_string( 828 key_file, kDesktopEntry, "Categories", categories.c_str()); 829 } 830 831 // Set the "NoDisplay" key. 832 if (no_display) 833 g_key_file_set_string(key_file, kDesktopEntry, "NoDisplay", "true"); 834 835 std::string wmclass = web_app::GetWMClassFromAppName(app_name); 836 g_key_file_set_string(key_file, kDesktopEntry, "StartupWMClass", 837 wmclass.c_str()); 838 839 gsize length = 0; 840 gchar* data_dump = g_key_file_to_data(key_file, &length, NULL); 841 if (data_dump) { 842 // If strlen(data_dump[0]) == 0, this check will fail. 843 if (data_dump[0] == '\n') { 844 // Older versions of glib produce a leading newline. If this is the case, 845 // remove it to avoid double-newline after the shebang. 846 output_buffer += (data_dump + 1); 847 } else { 848 output_buffer += data_dump; 849 } 850 g_free(data_dump); 851 } 852 853 g_key_file_free(key_file); 854 return output_buffer; 855 #else 856 NOTIMPLEMENTED(); 857 return std::string(); 858 #endif 859 } 860 861 std::string GetDirectoryFileContents(const base::string16& title, 862 const std::string& icon_name) { 863 #if defined(USE_GLIB) 864 // See http://standards.freedesktop.org/desktop-entry-spec/latest/ 865 GKeyFile* key_file = g_key_file_new(); 866 867 g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0"); 868 g_key_file_set_string(key_file, kDesktopEntry, "Type", "Directory"); 869 std::string final_title = base::UTF16ToUTF8(title); 870 g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str()); 871 if (!icon_name.empty()) { 872 g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str()); 873 } else { 874 g_key_file_set_string(key_file, kDesktopEntry, "Icon", 875 GetIconName().c_str()); 876 } 877 878 gsize length = 0; 879 gchar* data_dump = g_key_file_to_data(key_file, &length, NULL); 880 std::string output_buffer; 881 if (data_dump) { 882 // If strlen(data_dump[0]) == 0, this check will fail. 883 if (data_dump[0] == '\n') { 884 // Older versions of glib produce a leading newline. If this is the case, 885 // remove it to avoid double-newline after the shebang. 886 output_buffer += (data_dump + 1); 887 } else { 888 output_buffer += data_dump; 889 } 890 g_free(data_dump); 891 } 892 893 g_key_file_free(key_file); 894 return output_buffer; 895 #else 896 NOTIMPLEMENTED(); 897 return std::string(); 898 #endif 899 } 900 901 bool CreateDesktopShortcut( 902 const web_app::ShortcutInfo& shortcut_info, 903 const web_app::ShortcutLocations& creation_locations) { 904 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 905 906 base::FilePath shortcut_filename; 907 if (!shortcut_info.extension_id.empty()) { 908 shortcut_filename = GetExtensionShortcutFilename( 909 shortcut_info.profile_path, shortcut_info.extension_id); 910 // For extensions we do not want duplicate shortcuts. So, delete any that 911 // already exist and replace them. 912 if (creation_locations.on_desktop) 913 DeleteShortcutOnDesktop(shortcut_filename); 914 915 if (creation_locations.applications_menu_location != 916 web_app::APP_MENU_LOCATION_NONE) { 917 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 918 } 919 } else { 920 shortcut_filename = GetWebShortcutFilename(shortcut_info.url); 921 } 922 if (shortcut_filename.empty()) 923 return false; 924 925 std::string icon_name = 926 CreateShortcutIcon(shortcut_info.favicon, shortcut_filename); 927 928 std::string app_name = 929 web_app::GenerateApplicationNameFromInfo(shortcut_info); 930 931 bool success = true; 932 933 base::FilePath chrome_exe_path = GetChromeExePath(); 934 if (chrome_exe_path.empty()) { 935 LOG(WARNING) << "Could not get executable path."; 936 return false; 937 } 938 939 if (creation_locations.on_desktop) { 940 std::string contents = GetDesktopFileContents( 941 chrome_exe_path, 942 app_name, 943 shortcut_info.url, 944 shortcut_info.extension_id, 945 shortcut_info.title, 946 icon_name, 947 shortcut_info.profile_path, 948 "", 949 false); 950 success = CreateShortcutOnDesktop(shortcut_filename, contents); 951 } 952 953 if (creation_locations.applications_menu_location == 954 web_app::APP_MENU_LOCATION_NONE) { 955 return success; 956 } 957 958 base::FilePath directory_filename; 959 std::string directory_contents; 960 switch (creation_locations.applications_menu_location) { 961 case web_app::APP_MENU_LOCATION_ROOT: 962 case web_app::APP_MENU_LOCATION_HIDDEN: 963 break; 964 case web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS: 965 directory_filename = base::FilePath(kDirectoryFilename); 966 directory_contents = GetDirectoryFileContents( 967 ShellIntegration::GetAppShortcutsSubdirName(), ""); 968 break; 969 default: 970 NOTREACHED(); 971 break; 972 } 973 974 // Set NoDisplay=true if hidden. This will hide the application from 975 // user-facing menus. 976 std::string contents = GetDesktopFileContents( 977 chrome_exe_path, 978 app_name, 979 shortcut_info.url, 980 shortcut_info.extension_id, 981 shortcut_info.title, 982 icon_name, 983 shortcut_info.profile_path, 984 "", 985 creation_locations.applications_menu_location == 986 web_app::APP_MENU_LOCATION_HIDDEN); 987 success = CreateShortcutInApplicationsMenu( 988 shortcut_filename, contents, directory_filename, directory_contents) && 989 success; 990 991 return success; 992 } 993 994 bool CreateAppListDesktopShortcut( 995 const std::string& wm_class, 996 const std::string& title) { 997 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 998 999 base::FilePath desktop_name(kAppListDesktopName); 1000 base::FilePath shortcut_filename = desktop_name.AddExtension("desktop"); 1001 1002 // We do not want duplicate shortcuts. Delete any that already exist and 1003 // replace them. 1004 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 1005 1006 base::FilePath chrome_exe_path = GetChromeExePath(); 1007 if (chrome_exe_path.empty()) { 1008 LOG(WARNING) << "Could not get executable path."; 1009 return false; 1010 } 1011 1012 gfx::ImageFamily icon_images; 1013 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); 1014 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); 1015 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); 1016 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_48)); 1017 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); 1018 std::string icon_name = CreateShortcutIcon(icon_images, desktop_name); 1019 1020 CommandLine command_line(chrome_exe_path); 1021 command_line.AppendSwitch(switches::kShowAppList); 1022 std::string contents = 1023 GetDesktopFileContentsForCommand(command_line, 1024 wm_class, 1025 GURL(), 1026 base::UTF8ToUTF16(title), 1027 icon_name, 1028 kAppListCategories, 1029 false); 1030 return CreateShortcutInApplicationsMenu( 1031 shortcut_filename, contents, base::FilePath(), ""); 1032 } 1033 1034 void DeleteDesktopShortcuts(const base::FilePath& profile_path, 1035 const std::string& extension_id) { 1036 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1037 1038 base::FilePath shortcut_filename = GetExtensionShortcutFilename( 1039 profile_path, extension_id); 1040 DCHECK(!shortcut_filename.empty()); 1041 1042 DeleteShortcutOnDesktop(shortcut_filename); 1043 // Delete shortcuts from |kDirectoryFilename|. 1044 // Note that it is possible that shortcuts were not created in the Chrome Apps 1045 // directory. It doesn't matter: this will still delete the shortcut even if 1046 // it isn't in the directory. 1047 DeleteShortcutInApplicationsMenu(shortcut_filename, 1048 base::FilePath(kDirectoryFilename)); 1049 } 1050 1051 void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) { 1052 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1053 1054 scoped_ptr<base::Environment> env(base::Environment::Create()); 1055 1056 // Delete shortcuts from Desktop. 1057 base::FilePath desktop_path; 1058 if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) { 1059 std::vector<base::FilePath> shortcut_filenames_desktop = 1060 GetExistingProfileShortcutFilenames(profile_path, desktop_path); 1061 for (std::vector<base::FilePath>::const_iterator it = 1062 shortcut_filenames_desktop.begin(); 1063 it != shortcut_filenames_desktop.end(); ++it) { 1064 DeleteShortcutOnDesktop(*it); 1065 } 1066 } 1067 1068 // Delete shortcuts from |kDirectoryFilename|. 1069 base::FilePath applications_menu; 1070 if (GetDataWriteLocation(env.get(), &applications_menu)) { 1071 applications_menu = applications_menu.AppendASCII("applications"); 1072 std::vector<base::FilePath> shortcut_filenames_app_menu = 1073 GetExistingProfileShortcutFilenames(profile_path, applications_menu); 1074 for (std::vector<base::FilePath>::const_iterator it = 1075 shortcut_filenames_app_menu.begin(); 1076 it != shortcut_filenames_app_menu.end(); ++it) { 1077 DeleteShortcutInApplicationsMenu(*it, 1078 base::FilePath(kDirectoryFilename)); 1079 } 1080 } 1081 } 1082 1083 } // namespace shell_integration_linux 1084