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