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