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