shell_integration_linux.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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/file_util.h" 25#include "base/files/file_enumerator.h" 26#include "base/files/file_path.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 53using content::BrowserThread; 54 55namespace { 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. 59const 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. 64bool 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 90std::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 144bool 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 185void 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. 195bool 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 241void 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// Quote a string such that it appears as one verbatim argument for the Exec 264// key in a desktop file. 265std::string QuoteArgForDesktopFileExec(const std::string& arg) { 266 // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html 267 268 // Quoting is only necessary if the argument has a reserved character. 269 if (arg.find_first_of(" \t\n\"'\\><~|&;$*?#()`") == std::string::npos) 270 return arg; // No quoting necessary. 271 272 std::string quoted = "\""; 273 for (size_t i = 0; i < arg.size(); ++i) { 274 // Note that the set of backslashed characters is smaller than the 275 // set of reserved characters. 276 switch (arg[i]) { 277 case '"': 278 case '`': 279 case '$': 280 case '\\': 281 quoted += '\\'; 282 break; 283 } 284 quoted += arg[i]; 285 } 286 quoted += '"'; 287 288 return quoted; 289} 290 291// Quote a command line so it is suitable for use as the Exec key in a desktop 292// file. Note: This should be used instead of GetCommandLineString, which does 293// not properly quote the string; this function is designed for the Exec key. 294std::string QuoteCommandLineForDesktopFileExec( 295 const CommandLine& command_line) { 296 // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html 297 298 std::string quoted_path = ""; 299 const CommandLine::StringVector& argv = command_line.argv(); 300 for (CommandLine::StringVector::const_iterator i = argv.begin(); 301 i != argv.end(); ++i) { 302 if (i != argv.begin()) 303 quoted_path += " "; 304 quoted_path += QuoteArgForDesktopFileExec(*i); 305 } 306 307 return quoted_path; 308} 309 310const char kDesktopEntry[] = "Desktop Entry"; 311 312const char kXdgOpenShebang[] = "#!/usr/bin/env xdg-open"; 313 314const char kXdgSettings[] = "xdg-settings"; 315const char kXdgSettingsDefaultBrowser[] = "default-web-browser"; 316const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler"; 317 318const char kDirectoryFilename[] = "chrome-apps.directory"; 319 320#if defined(GOOGLE_CHROME_BUILD) 321const char kAppListDesktopName[] = "chrome-app-list"; 322#else // CHROMIUM_BUILD 323const char kAppListDesktopName[] = "chromium-app-list"; 324#endif 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(shell_integration_linux::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(shell_integration_linux::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#if defined(USE_GLIB) 448 // An empty file causes a crash with glib <= 2.32, so special case here. 449 if (shortcut_contents.empty()) 450 return false; 451 452 GKeyFile* key_file = g_key_file_new(); 453 GError* err = NULL; 454 if (!g_key_file_load_from_data(key_file, shortcut_contents.c_str(), 455 shortcut_contents.size(), G_KEY_FILE_NONE, 456 &err)) { 457 LOG(WARNING) << "Unable to read desktop file template: " << err->message; 458 g_error_free(err); 459 g_key_file_free(key_file); 460 return false; 461 } 462 463 bool nodisplay = false; 464 char* nodisplay_c_string = g_key_file_get_string(key_file, kDesktopEntry, 465 "NoDisplay", &err); 466 if (nodisplay_c_string) { 467 if (!g_strcmp0(nodisplay_c_string, "true")) 468 nodisplay = true; 469 g_free(nodisplay_c_string); 470 } else { 471 g_error_free(err); 472 } 473 474 g_key_file_free(key_file); 475 return nodisplay; 476#else 477 NOTIMPLEMENTED(); 478 return false; 479#endif 480} 481 482// Gets the path to the Chrome executable or wrapper script. 483// Returns an empty path if the executable path could not be found. 484base::FilePath GetChromeExePath() { 485 // Try to get the name of the wrapper script that launched Chrome. 486 scoped_ptr<base::Environment> environment(base::Environment::Create()); 487 std::string wrapper_script; 488 if (environment->GetVar("CHROME_WRAPPER", &wrapper_script)) { 489 return base::FilePath(wrapper_script); 490 } 491 492 // Just return the name of the executable path for Chrome. 493 base::FilePath chrome_exe_path; 494 PathService::Get(base::FILE_EXE, &chrome_exe_path); 495 return chrome_exe_path; 496} 497 498} // namespace 499 500// static 501ShellIntegration::DefaultWebClientSetPermission 502ShellIntegration::CanSetAsDefaultBrowser() { 503 return SET_DEFAULT_UNATTENDED; 504} 505 506// static 507bool ShellIntegration::SetAsDefaultBrowser() { 508 return SetDefaultWebClient(std::string()); 509} 510 511// static 512bool ShellIntegration::SetAsDefaultProtocolClient( 513 const std::string& protocol) { 514 return SetDefaultWebClient(protocol); 515} 516 517// static 518ShellIntegration::DefaultWebClientState 519ShellIntegration::GetDefaultBrowser() { 520 return GetIsDefaultWebClient(std::string()); 521} 522 523// static 524base::string16 ShellIntegration::GetApplicationNameForProtocol( 525 const GURL& url) { 526 return base::ASCIIToUTF16("xdg-open"); 527} 528 529// static 530ShellIntegration::DefaultWebClientState 531ShellIntegration::IsDefaultProtocolClient(const std::string& protocol) { 532 return GetIsDefaultWebClient(protocol); 533} 534 535// static 536bool ShellIntegration::IsFirefoxDefaultBrowser() { 537 std::vector<std::string> argv; 538 argv.push_back(kXdgSettings); 539 argv.push_back("get"); 540 argv.push_back(kXdgSettingsDefaultBrowser); 541 542 std::string browser; 543 // We don't care about the return value here. 544 base::GetAppOutput(CommandLine(argv), &browser); 545 return browser.find("irefox") != std::string::npos; 546} 547 548namespace shell_integration_linux { 549 550bool GetDataWriteLocation(base::Environment* env, base::FilePath* search_path) { 551 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 552 553 std::string xdg_data_home; 554 std::string home; 555 if (env->GetVar("XDG_DATA_HOME", &xdg_data_home) && !xdg_data_home.empty()) { 556 *search_path = base::FilePath(xdg_data_home); 557 return true; 558 } else if (env->GetVar("HOME", &home) && !home.empty()) { 559 *search_path = base::FilePath(home).Append(".local").Append("share"); 560 return true; 561 } 562 return false; 563} 564 565std::vector<base::FilePath> GetDataSearchLocations(base::Environment* env) { 566 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 567 568 std::vector<base::FilePath> search_paths; 569 570 base::FilePath write_location; 571 if (GetDataWriteLocation(env, &write_location)) 572 search_paths.push_back(write_location); 573 574 std::string xdg_data_dirs; 575 if (env->GetVar("XDG_DATA_DIRS", &xdg_data_dirs) && !xdg_data_dirs.empty()) { 576 base::StringTokenizer tokenizer(xdg_data_dirs, ":"); 577 while (tokenizer.GetNext()) { 578 base::FilePath data_dir(tokenizer.token()); 579 search_paths.push_back(data_dir); 580 } 581 } else { 582 search_paths.push_back(base::FilePath("/usr/local/share")); 583 search_paths.push_back(base::FilePath("/usr/share")); 584 } 585 586 return search_paths; 587} 588 589std::string GetProgramClassName() { 590 DCHECK(CommandLine::InitializedForCurrentProcess()); 591 // Get the res_name component from argv[0]. 592 const CommandLine* command_line = CommandLine::ForCurrentProcess(); 593 std::string class_name = command_line->GetProgram().BaseName().value(); 594 if (!class_name.empty()) 595 class_name[0] = base::ToUpperASCII(class_name[0]); 596 return class_name; 597} 598 599std::string GetDesktopName(base::Environment* env) { 600#if defined(GOOGLE_CHROME_BUILD) 601 chrome::VersionInfo::Channel product_channel( 602 chrome::VersionInfo::GetChannel()); 603 switch (product_channel) { 604 case chrome::VersionInfo::CHANNEL_DEV: 605 return "google-chrome-unstable.desktop"; 606 case chrome::VersionInfo::CHANNEL_BETA: 607 return "google-chrome-beta.desktop"; 608 default: 609 return "google-chrome.desktop"; 610 } 611#else // CHROMIUM_BUILD 612 // Allow $CHROME_DESKTOP to override the built-in value, so that development 613 // versions can set themselves as the default without interfering with 614 // non-official, packaged versions using the built-in value. 615 std::string name; 616 if (env->GetVar("CHROME_DESKTOP", &name) && !name.empty()) 617 return name; 618 return "chromium-browser.desktop"; 619#endif 620} 621 622std::string GetIconName() { 623#if defined(GOOGLE_CHROME_BUILD) 624 return "google-chrome"; 625#else // CHROMIUM_BUILD 626 return "chromium-browser"; 627#endif 628} 629 630web_app::ShortcutLocations GetExistingShortcutLocations( 631 base::Environment* env, 632 const base::FilePath& profile_path, 633 const std::string& extension_id) { 634 base::FilePath desktop_path; 635 // If Get returns false, just leave desktop_path empty. 636 PathService::Get(base::DIR_USER_DESKTOP, &desktop_path); 637 return GetExistingShortcutLocations(env, profile_path, extension_id, 638 desktop_path); 639} 640 641web_app::ShortcutLocations GetExistingShortcutLocations( 642 base::Environment* env, 643 const base::FilePath& profile_path, 644 const std::string& extension_id, 645 const base::FilePath& desktop_path) { 646 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 647 648 base::FilePath shortcut_filename = GetExtensionShortcutFilename( 649 profile_path, extension_id); 650 DCHECK(!shortcut_filename.empty()); 651 web_app::ShortcutLocations locations; 652 653 // Determine whether there is a shortcut on desktop. 654 if (!desktop_path.empty()) { 655 locations.on_desktop = 656 base::PathExists(desktop_path.Append(shortcut_filename)); 657 } 658 659 // Determine whether there is a shortcut in the applications directory. 660 std::string shortcut_contents; 661 if (GetExistingShortcutContents(env, shortcut_filename, &shortcut_contents)) { 662 // Whether this counts as "hidden" or "APP_MENU_LOCATION_SUBDIR_CHROMEAPPS" 663 // depends on whether it contains NoDisplay=true. Since these shortcuts are 664 // for apps, they are always in the "Chrome Apps" directory. 665 if (GetNoDisplayFromDesktopFile(shortcut_contents)) { 666 locations.hidden = true; 667 } else { 668 locations.applications_menu_location = 669 web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; 670 } 671 } 672 673 return locations; 674} 675 676bool 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 696base::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 file_util::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 720base::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 file_util::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 737std::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 file_util::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 763std::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 780std::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 861std::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 901bool 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 // The 'applications_menu_location' and 'hidden' locations are actually the 915 // same place ('applications'). 916 if (creation_locations.applications_menu_location != 917 web_app::APP_MENU_LOCATION_NONE || 918 creation_locations.hidden) 919 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 920 } else { 921 shortcut_filename = GetWebShortcutFilename(shortcut_info.url); 922 } 923 if (shortcut_filename.empty()) 924 return false; 925 926 std::string icon_name = 927 CreateShortcutIcon(shortcut_info.favicon, shortcut_filename); 928 929 std::string app_name = 930 web_app::GenerateApplicationNameFromInfo(shortcut_info); 931 932 bool success = true; 933 934 base::FilePath chrome_exe_path = GetChromeExePath(); 935 if (chrome_exe_path.empty()) { 936 LOG(WARNING) << "Could not get executable path."; 937 return false; 938 } 939 940 if (creation_locations.on_desktop) { 941 std::string contents = GetDesktopFileContents( 942 chrome_exe_path, 943 app_name, 944 shortcut_info.url, 945 shortcut_info.extension_id, 946 shortcut_info.title, 947 icon_name, 948 shortcut_info.profile_path, 949 "", 950 false); 951 success = CreateShortcutOnDesktop(shortcut_filename, contents); 952 } 953 954 if (creation_locations.applications_menu_location != 955 web_app::APP_MENU_LOCATION_NONE || 956 creation_locations.hidden) { 957 base::FilePath directory_filename; 958 std::string directory_contents; 959 switch (creation_locations.applications_menu_location) { 960 case web_app::APP_MENU_LOCATION_NONE: 961 case web_app::APP_MENU_LOCATION_ROOT: 962 break; 963 case web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS: 964 directory_filename = base::FilePath(kDirectoryFilename); 965 directory_contents = GetDirectoryFileContents( 966 ShellIntegration::GetAppShortcutsSubdirName(), ""); 967 break; 968 default: 969 NOTREACHED(); 970 break; 971 } 972 // Set NoDisplay=true if hidden but not in the applications menu. This will 973 // hide the application from user-facing menus. 974 std::string contents = GetDesktopFileContents( 975 chrome_exe_path, 976 app_name, 977 shortcut_info.url, 978 shortcut_info.extension_id, 979 shortcut_info.title, 980 icon_name, 981 shortcut_info.profile_path, 982 "", 983 creation_locations.applications_menu_location == 984 web_app::APP_MENU_LOCATION_NONE); 985 success = CreateShortcutInApplicationsMenu( 986 shortcut_filename, contents, directory_filename, directory_contents) && 987 success; 988 } 989 990 return success; 991} 992 993bool CreateAppListDesktopShortcut( 994 const std::string& wm_class, 995 const std::string& title) { 996 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 997 998 base::FilePath desktop_name(kAppListDesktopName); 999 base::FilePath shortcut_filename = desktop_name.AddExtension("desktop"); 1000 1001 // We do not want duplicate shortcuts. Delete any that already exist and 1002 // replace them. 1003 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 1004 1005 base::FilePath chrome_exe_path = GetChromeExePath(); 1006 if (chrome_exe_path.empty()) { 1007 LOG(WARNING) << "Could not get executable path."; 1008 return false; 1009 } 1010 1011 gfx::ImageFamily icon_images; 1012 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); 1013 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); 1014 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); 1015 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_48)); 1016 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); 1017 std::string icon_name = CreateShortcutIcon(icon_images, desktop_name); 1018 1019 CommandLine command_line(chrome_exe_path); 1020 command_line.AppendSwitch(switches::kShowAppList); 1021 std::string contents = 1022 GetDesktopFileContentsForCommand(command_line, 1023 wm_class, 1024 GURL(), 1025 base::UTF8ToUTF16(title), 1026 icon_name, 1027 kAppListCategories, 1028 false); 1029 return CreateShortcutInApplicationsMenu( 1030 shortcut_filename, contents, base::FilePath(), ""); 1031} 1032 1033void DeleteDesktopShortcuts(const base::FilePath& profile_path, 1034 const std::string& extension_id) { 1035 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1036 1037 base::FilePath shortcut_filename = GetExtensionShortcutFilename( 1038 profile_path, extension_id); 1039 DCHECK(!shortcut_filename.empty()); 1040 1041 DeleteShortcutOnDesktop(shortcut_filename); 1042 // Delete shortcuts from |kDirectoryFilename|. 1043 // Note that it is possible that shortcuts were not created in the Chrome Apps 1044 // directory. It doesn't matter: this will still delete the shortcut even if 1045 // it isn't in the directory. 1046 DeleteShortcutInApplicationsMenu(shortcut_filename, 1047 base::FilePath(kDirectoryFilename)); 1048} 1049 1050void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) { 1051 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1052 1053 scoped_ptr<base::Environment> env(base::Environment::Create()); 1054 1055 // Delete shortcuts from Desktop. 1056 base::FilePath desktop_path; 1057 if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) { 1058 std::vector<base::FilePath> shortcut_filenames_desktop = 1059 GetExistingProfileShortcutFilenames(profile_path, desktop_path); 1060 for (std::vector<base::FilePath>::const_iterator it = 1061 shortcut_filenames_desktop.begin(); 1062 it != shortcut_filenames_desktop.end(); ++it) { 1063 DeleteShortcutOnDesktop(*it); 1064 } 1065 } 1066 1067 // Delete shortcuts from |kDirectoryFilename|. 1068 base::FilePath applications_menu; 1069 if (GetDataWriteLocation(env.get(), &applications_menu)) { 1070 applications_menu = applications_menu.AppendASCII("applications"); 1071 std::vector<base::FilePath> shortcut_filenames_app_menu = 1072 GetExistingProfileShortcutFilenames(profile_path, applications_menu); 1073 for (std::vector<base::FilePath>::const_iterator it = 1074 shortcut_filenames_app_menu.begin(); 1075 it != shortcut_filenames_app_menu.end(); ++it) { 1076 DeleteShortcutInApplicationsMenu(*it, 1077 base::FilePath(kDirectoryFilename)); 1078 } 1079 } 1080} 1081 1082} // namespace shell_integration_linux 1083