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