shell_integration_linux.cc revision 2385ea399aae016c0806a4f9ef3c9cfe3d2a39df
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
45using content::BrowserThread;
46
47namespace {
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.
52bool 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
78std::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
132bool 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
173void 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.
183bool 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
229void 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.
253std::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
279const char kDesktopEntry[] = "Desktop Entry";
280
281const char kXdgOpenShebang[] = "#!/usr/bin/env xdg-open";
282
283const char kXdgSettings[] = "xdg-settings";
284const char kXdgSettingsDefaultBrowser[] = "default-web-browser";
285const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler";
286
287const char kDirectoryFilename[] = "chrome-apps.directory";
288
289}  // namespace
290
291namespace {
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.
298bool 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.
323const 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|.
337bool 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|.
371ShellIntegration::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.
415bool 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.
446base::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
463ShellIntegration::DefaultWebClientSetPermission
464    ShellIntegration::CanSetAsDefaultBrowser() {
465  return SET_DEFAULT_UNATTENDED;
466}
467
468// static
469bool ShellIntegration::SetAsDefaultBrowser() {
470  return SetDefaultWebClient(std::string());
471}
472
473// static
474bool ShellIntegration::SetAsDefaultProtocolClient(const std::string& protocol) {
475  return SetDefaultWebClient(protocol);
476}
477
478// static
479ShellIntegration::DefaultWebClientState ShellIntegration::GetDefaultBrowser() {
480  return GetIsDefaultWebClient(std::string());
481}
482
483// static
484std::string ShellIntegration::GetApplicationForProtocol(const GURL& url) {
485  return std::string("xdg-open");
486}
487
488// static
489ShellIntegration::DefaultWebClientState
490ShellIntegration::IsDefaultProtocolClient(const std::string& protocol) {
491  return GetIsDefaultWebClient(protocol);
492}
493
494// static
495bool 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
507namespace ShellIntegrationLinux {
508
509bool 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
524std::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
548std::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
562std::string GetIconName() {
563#if defined(GOOGLE_CHROME_BUILD)
564  return "google-chrome";
565#else  // CHROMIUM_BUILD
566  return "chromium-browser";
567#endif
568}
569
570ShellIntegration::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
581ShellIntegration::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
613bool 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
633base::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
657base::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
674std::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
700std::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
788std::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
823bool 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
901void 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
919void 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