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