shell_integration_linux.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
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 // If the shortcut contents contain NoDisplay=true, it should be hidden. 663 // Otherwise since these shortcuts are for apps, they are always in the 664 // "Chrome Apps" directory. 665 locations.applications_menu_location = 666 GetNoDisplayFromDesktopFile(shortcut_contents) 667 ? web_app::APP_MENU_LOCATION_HIDDEN 668 : web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; 669 } 670 671 return locations; 672} 673 674bool GetExistingShortcutContents(base::Environment* env, 675 const base::FilePath& desktop_filename, 676 std::string* output) { 677 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 678 679 std::vector<base::FilePath> search_paths = GetDataSearchLocations(env); 680 681 for (std::vector<base::FilePath>::const_iterator i = search_paths.begin(); 682 i != search_paths.end(); ++i) { 683 base::FilePath path = i->Append("applications").Append(desktop_filename); 684 VLOG(1) << "Looking for desktop file in " << path.value(); 685 if (base::PathExists(path)) { 686 VLOG(1) << "Found desktop file at " << path.value(); 687 return base::ReadFileToString(path, output); 688 } 689 } 690 691 return false; 692} 693 694base::FilePath GetWebShortcutFilename(const GURL& url) { 695 // Use a prefix, because xdg-desktop-menu requires it. 696 std::string filename = 697 std::string(chrome::kBrowserProcessExecutableName) + "-" + url.spec(); 698 file_util::ReplaceIllegalCharactersInPath(&filename, '_'); 699 700 base::FilePath desktop_path; 701 if (!PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) 702 return base::FilePath(); 703 704 base::FilePath filepath = desktop_path.Append(filename); 705 base::FilePath alternative_filepath(filepath.value() + ".desktop"); 706 for (size_t i = 1; i < 100; ++i) { 707 if (base::PathExists(base::FilePath(alternative_filepath))) { 708 alternative_filepath = base::FilePath( 709 filepath.value() + "_" + base::IntToString(i) + ".desktop"); 710 } else { 711 return base::FilePath(alternative_filepath).BaseName(); 712 } 713 } 714 715 return base::FilePath(); 716} 717 718base::FilePath GetExtensionShortcutFilename(const base::FilePath& profile_path, 719 const std::string& extension_id) { 720 DCHECK(!extension_id.empty()); 721 722 // Use a prefix, because xdg-desktop-menu requires it. 723 std::string filename(chrome::kBrowserProcessExecutableName); 724 filename.append("-") 725 .append(extension_id) 726 .append("-") 727 .append(profile_path.BaseName().value()); 728 file_util::ReplaceIllegalCharactersInPath(&filename, '_'); 729 // Spaces in filenames break xdg-desktop-menu 730 // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605). 731 base::ReplaceChars(filename, " ", "_", &filename); 732 return base::FilePath(filename.append(".desktop")); 733} 734 735std::vector<base::FilePath> GetExistingProfileShortcutFilenames( 736 const base::FilePath& profile_path, 737 const base::FilePath& directory) { 738 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 739 // Use a prefix, because xdg-desktop-menu requires it. 740 std::string prefix(chrome::kBrowserProcessExecutableName); 741 prefix.append("-"); 742 std::string suffix("-"); 743 suffix.append(profile_path.BaseName().value()); 744 file_util::ReplaceIllegalCharactersInPath(&suffix, '_'); 745 // Spaces in filenames break xdg-desktop-menu 746 // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605). 747 base::ReplaceChars(suffix, " ", "_", &suffix); 748 std::string glob = prefix + "*" + suffix + ".desktop"; 749 750 base::FileEnumerator files(directory, false, base::FileEnumerator::FILES, 751 glob); 752 base::FilePath shortcut_file = files.Next(); 753 std::vector<base::FilePath> shortcut_paths; 754 while (!shortcut_file.empty()) { 755 shortcut_paths.push_back(shortcut_file.BaseName()); 756 shortcut_file = files.Next(); 757 } 758 return shortcut_paths; 759} 760 761std::string GetDesktopFileContents( 762 const base::FilePath& chrome_exe_path, 763 const std::string& app_name, 764 const GURL& url, 765 const std::string& extension_id, 766 const base::string16& title, 767 const std::string& icon_name, 768 const base::FilePath& profile_path, 769 const std::string& categories, 770 bool no_display) { 771 CommandLine cmd_line = ShellIntegration::CommandLineArgsForLauncher( 772 url, extension_id, profile_path); 773 cmd_line.SetProgram(chrome_exe_path); 774 return GetDesktopFileContentsForCommand(cmd_line, app_name, url, title, 775 icon_name, categories, no_display); 776} 777 778std::string GetDesktopFileContentsForCommand( 779 const CommandLine& command_line, 780 const std::string& app_name, 781 const GURL& url, 782 const base::string16& title, 783 const std::string& icon_name, 784 const std::string& categories, 785 bool no_display) { 786#if defined(USE_GLIB) 787 // Although not required by the spec, Nautilus on Ubuntu Karmic creates its 788 // launchers with an xdg-open shebang. Follow that convention. 789 std::string output_buffer = std::string(kXdgOpenShebang) + "\n"; 790 791 // See http://standards.freedesktop.org/desktop-entry-spec/latest/ 792 GKeyFile* key_file = g_key_file_new(); 793 794 // Set keys with fixed values. 795 g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0"); 796 g_key_file_set_string(key_file, kDesktopEntry, "Terminal", "false"); 797 g_key_file_set_string(key_file, kDesktopEntry, "Type", "Application"); 798 799 // Set the "Name" key. 800 std::string final_title = base::UTF16ToUTF8(title); 801 // Make sure no endline characters can slip in and possibly introduce 802 // additional lines (like Exec, which makes it a security risk). Also 803 // use the URL as a default when the title is empty. 804 if (final_title.empty() || 805 final_title.find("\n") != std::string::npos || 806 final_title.find("\r") != std::string::npos) { 807 final_title = url.spec(); 808 } 809 g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str()); 810 811 // Set the "Exec" key. 812 std::string final_path = QuoteCommandLineForDesktopFileExec(command_line); 813 g_key_file_set_string(key_file, kDesktopEntry, "Exec", final_path.c_str()); 814 815 // Set the "Icon" key. 816 if (!icon_name.empty()) { 817 g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str()); 818 } else { 819 g_key_file_set_string(key_file, kDesktopEntry, "Icon", 820 GetIconName().c_str()); 821 } 822 823 // Set the "Categories" key. 824 if (!categories.empty()) { 825 g_key_file_set_string( 826 key_file, kDesktopEntry, "Categories", categories.c_str()); 827 } 828 829 // Set the "NoDisplay" key. 830 if (no_display) 831 g_key_file_set_string(key_file, kDesktopEntry, "NoDisplay", "true"); 832 833 std::string wmclass = web_app::GetWMClassFromAppName(app_name); 834 g_key_file_set_string(key_file, kDesktopEntry, "StartupWMClass", 835 wmclass.c_str()); 836 837 gsize length = 0; 838 gchar* data_dump = g_key_file_to_data(key_file, &length, NULL); 839 if (data_dump) { 840 // If strlen(data_dump[0]) == 0, this check will fail. 841 if (data_dump[0] == '\n') { 842 // Older versions of glib produce a leading newline. If this is the case, 843 // remove it to avoid double-newline after the shebang. 844 output_buffer += (data_dump + 1); 845 } else { 846 output_buffer += data_dump; 847 } 848 g_free(data_dump); 849 } 850 851 g_key_file_free(key_file); 852 return output_buffer; 853#else 854 NOTIMPLEMENTED(); 855 return std::string(); 856#endif 857} 858 859std::string GetDirectoryFileContents(const base::string16& title, 860 const std::string& icon_name) { 861#if defined(USE_GLIB) 862 // See http://standards.freedesktop.org/desktop-entry-spec/latest/ 863 GKeyFile* key_file = g_key_file_new(); 864 865 g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0"); 866 g_key_file_set_string(key_file, kDesktopEntry, "Type", "Directory"); 867 std::string final_title = base::UTF16ToUTF8(title); 868 g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str()); 869 if (!icon_name.empty()) { 870 g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str()); 871 } else { 872 g_key_file_set_string(key_file, kDesktopEntry, "Icon", 873 GetIconName().c_str()); 874 } 875 876 gsize length = 0; 877 gchar* data_dump = g_key_file_to_data(key_file, &length, NULL); 878 std::string output_buffer; 879 if (data_dump) { 880 // If strlen(data_dump[0]) == 0, this check will fail. 881 if (data_dump[0] == '\n') { 882 // Older versions of glib produce a leading newline. If this is the case, 883 // remove it to avoid double-newline after the shebang. 884 output_buffer += (data_dump + 1); 885 } else { 886 output_buffer += data_dump; 887 } 888 g_free(data_dump); 889 } 890 891 g_key_file_free(key_file); 892 return output_buffer; 893#else 894 NOTIMPLEMENTED(); 895 return std::string(); 896#endif 897} 898 899bool CreateDesktopShortcut( 900 const web_app::ShortcutInfo& shortcut_info, 901 const web_app::ShortcutLocations& creation_locations) { 902 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 903 904 base::FilePath shortcut_filename; 905 if (!shortcut_info.extension_id.empty()) { 906 shortcut_filename = GetExtensionShortcutFilename( 907 shortcut_info.profile_path, shortcut_info.extension_id); 908 // For extensions we do not want duplicate shortcuts. So, delete any that 909 // already exist and replace them. 910 if (creation_locations.on_desktop) 911 DeleteShortcutOnDesktop(shortcut_filename); 912 913 if (creation_locations.applications_menu_location != 914 web_app::APP_MENU_LOCATION_NONE) { 915 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 916 } 917 } else { 918 shortcut_filename = GetWebShortcutFilename(shortcut_info.url); 919 } 920 if (shortcut_filename.empty()) 921 return false; 922 923 std::string icon_name = 924 CreateShortcutIcon(shortcut_info.favicon, shortcut_filename); 925 926 std::string app_name = 927 web_app::GenerateApplicationNameFromInfo(shortcut_info); 928 929 bool success = true; 930 931 base::FilePath chrome_exe_path = GetChromeExePath(); 932 if (chrome_exe_path.empty()) { 933 LOG(WARNING) << "Could not get executable path."; 934 return false; 935 } 936 937 if (creation_locations.on_desktop) { 938 std::string contents = GetDesktopFileContents( 939 chrome_exe_path, 940 app_name, 941 shortcut_info.url, 942 shortcut_info.extension_id, 943 shortcut_info.title, 944 icon_name, 945 shortcut_info.profile_path, 946 "", 947 false); 948 success = CreateShortcutOnDesktop(shortcut_filename, contents); 949 } 950 951 if (creation_locations.applications_menu_location == 952 web_app::APP_MENU_LOCATION_NONE) { 953 return success; 954 } 955 956 base::FilePath directory_filename; 957 std::string directory_contents; 958 switch (creation_locations.applications_menu_location) { 959 case web_app::APP_MENU_LOCATION_ROOT: 960 case web_app::APP_MENU_LOCATION_HIDDEN: 961 break; 962 case web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS: 963 directory_filename = base::FilePath(kDirectoryFilename); 964 directory_contents = GetDirectoryFileContents( 965 ShellIntegration::GetAppShortcutsSubdirName(), ""); 966 break; 967 default: 968 NOTREACHED(); 969 break; 970 } 971 972 // Set NoDisplay=true if hidden. This will hide the application from 973 // 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_HIDDEN); 985 success = CreateShortcutInApplicationsMenu( 986 shortcut_filename, contents, directory_filename, directory_contents) && 987 success; 988 989 return success; 990} 991 992bool CreateAppListDesktopShortcut( 993 const std::string& wm_class, 994 const std::string& title) { 995 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 996 997 base::FilePath desktop_name(kAppListDesktopName); 998 base::FilePath shortcut_filename = desktop_name.AddExtension("desktop"); 999 1000 // We do not want duplicate shortcuts. Delete any that already exist and 1001 // replace them. 1002 DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath()); 1003 1004 base::FilePath chrome_exe_path = GetChromeExePath(); 1005 if (chrome_exe_path.empty()) { 1006 LOG(WARNING) << "Could not get executable path."; 1007 return false; 1008 } 1009 1010 gfx::ImageFamily icon_images; 1011 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance(); 1012 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_16)); 1013 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_32)); 1014 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_48)); 1015 icon_images.Add(*resource_bundle.GetImageSkiaNamed(IDR_APP_LIST_256)); 1016 std::string icon_name = CreateShortcutIcon(icon_images, desktop_name); 1017 1018 CommandLine command_line(chrome_exe_path); 1019 command_line.AppendSwitch(switches::kShowAppList); 1020 std::string contents = 1021 GetDesktopFileContentsForCommand(command_line, 1022 wm_class, 1023 GURL(), 1024 base::UTF8ToUTF16(title), 1025 icon_name, 1026 kAppListCategories, 1027 false); 1028 return CreateShortcutInApplicationsMenu( 1029 shortcut_filename, contents, base::FilePath(), ""); 1030} 1031 1032void DeleteDesktopShortcuts(const base::FilePath& profile_path, 1033 const std::string& extension_id) { 1034 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1035 1036 base::FilePath shortcut_filename = GetExtensionShortcutFilename( 1037 profile_path, extension_id); 1038 DCHECK(!shortcut_filename.empty()); 1039 1040 DeleteShortcutOnDesktop(shortcut_filename); 1041 // Delete shortcuts from |kDirectoryFilename|. 1042 // Note that it is possible that shortcuts were not created in the Chrome Apps 1043 // directory. It doesn't matter: this will still delete the shortcut even if 1044 // it isn't in the directory. 1045 DeleteShortcutInApplicationsMenu(shortcut_filename, 1046 base::FilePath(kDirectoryFilename)); 1047} 1048 1049void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) { 1050 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 1051 1052 scoped_ptr<base::Environment> env(base::Environment::Create()); 1053 1054 // Delete shortcuts from Desktop. 1055 base::FilePath desktop_path; 1056 if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) { 1057 std::vector<base::FilePath> shortcut_filenames_desktop = 1058 GetExistingProfileShortcutFilenames(profile_path, desktop_path); 1059 for (std::vector<base::FilePath>::const_iterator it = 1060 shortcut_filenames_desktop.begin(); 1061 it != shortcut_filenames_desktop.end(); ++it) { 1062 DeleteShortcutOnDesktop(*it); 1063 } 1064 } 1065 1066 // Delete shortcuts from |kDirectoryFilename|. 1067 base::FilePath applications_menu; 1068 if (GetDataWriteLocation(env.get(), &applications_menu)) { 1069 applications_menu = applications_menu.AppendASCII("applications"); 1070 std::vector<base::FilePath> shortcut_filenames_app_menu = 1071 GetExistingProfileShortcutFilenames(profile_path, applications_menu); 1072 for (std::vector<base::FilePath>::const_iterator it = 1073 shortcut_filenames_app_menu.begin(); 1074 it != shortcut_filenames_app_menu.end(); ++it) { 1075 DeleteShortcutInApplicationsMenu(*it, 1076 base::FilePath(kDirectoryFilename)); 1077 } 1078 } 1079} 1080 1081} // namespace shell_integration_linux 1082