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