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