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 (IGNORE_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 (IGNORE_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 (IGNORE_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 "APP_MENU_LOCATION_SUBDIR_CHROMEAPPS"
616    // depends on whether it contains NoDisplay=true. Since these shortcuts are
617    // for apps, they are always in the "Chrome Apps" directory.
618    if (GetNoDisplayFromDesktopFile(shortcut_contents)) {
619      locations.hidden = true;
620    } else {
621      locations.applications_menu_location =
622          ShellIntegration::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS;
623    }
624  }
625
626  return locations;
627}
628
629bool GetExistingShortcutContents(base::Environment* env,
630                                 const base::FilePath& desktop_filename,
631                                 std::string* output) {
632  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
633
634  std::vector<base::FilePath> search_paths = GetDataSearchLocations(env);
635
636  for (std::vector<base::FilePath>::const_iterator i = search_paths.begin();
637       i != search_paths.end(); ++i) {
638    base::FilePath path = i->Append("applications").Append(desktop_filename);
639    VLOG(1) << "Looking for desktop file in " << path.value();
640    if (base::PathExists(path)) {
641      VLOG(1) << "Found desktop file at " << path.value();
642      return base::ReadFileToString(path, output);
643    }
644  }
645
646  return false;
647}
648
649base::FilePath GetWebShortcutFilename(const GURL& url) {
650  // Use a prefix, because xdg-desktop-menu requires it.
651  std::string filename =
652      std::string(chrome::kBrowserProcessExecutableName) + "-" + url.spec();
653  file_util::ReplaceIllegalCharactersInPath(&filename, '_');
654
655  base::FilePath desktop_path;
656  if (!PathService::Get(base::DIR_USER_DESKTOP, &desktop_path))
657    return base::FilePath();
658
659  base::FilePath filepath = desktop_path.Append(filename);
660  base::FilePath alternative_filepath(filepath.value() + ".desktop");
661  for (size_t i = 1; i < 100; ++i) {
662    if (base::PathExists(base::FilePath(alternative_filepath))) {
663      alternative_filepath = base::FilePath(
664          filepath.value() + "_" + base::IntToString(i) + ".desktop");
665    } else {
666      return base::FilePath(alternative_filepath).BaseName();
667    }
668  }
669
670  return base::FilePath();
671}
672
673base::FilePath GetExtensionShortcutFilename(const base::FilePath& profile_path,
674                                            const std::string& extension_id) {
675  DCHECK(!extension_id.empty());
676
677  // Use a prefix, because xdg-desktop-menu requires it.
678  std::string filename(chrome::kBrowserProcessExecutableName);
679  filename.append("-")
680      .append(extension_id)
681      .append("-")
682      .append(profile_path.BaseName().value());
683  file_util::ReplaceIllegalCharactersInPath(&filename, '_');
684  // Spaces in filenames break xdg-desktop-menu
685  // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605).
686  base::ReplaceChars(filename, " ", "_", &filename);
687  return base::FilePath(filename.append(".desktop"));
688}
689
690std::vector<base::FilePath> GetExistingProfileShortcutFilenames(
691    const base::FilePath& profile_path,
692    const base::FilePath& directory) {
693  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
694  // Use a prefix, because xdg-desktop-menu requires it.
695  std::string prefix(chrome::kBrowserProcessExecutableName);
696  prefix.append("-");
697  std::string suffix("-");
698  suffix.append(profile_path.BaseName().value());
699  file_util::ReplaceIllegalCharactersInPath(&suffix, '_');
700  // Spaces in filenames break xdg-desktop-menu
701  // (see https://bugs.freedesktop.org/show_bug.cgi?id=66605).
702  base::ReplaceChars(suffix, " ", "_", &suffix);
703  std::string glob = prefix + "*" + suffix + ".desktop";
704
705  base::FileEnumerator files(directory, false, base::FileEnumerator::FILES,
706                             glob);
707  base::FilePath shortcut_file = files.Next();
708  std::vector<base::FilePath> shortcut_paths;
709  while (!shortcut_file.empty()) {
710    shortcut_paths.push_back(shortcut_file.BaseName());
711    shortcut_file = files.Next();
712  }
713  return shortcut_paths;
714}
715
716std::string GetDesktopFileContents(
717    const base::FilePath& chrome_exe_path,
718    const std::string& app_name,
719    const GURL& url,
720    const std::string& extension_id,
721    const base::FilePath& extension_path,
722    const base::string16& title,
723    const std::string& icon_name,
724    const base::FilePath& profile_path,
725    bool no_display) {
726  // Although not required by the spec, Nautilus on Ubuntu Karmic creates its
727  // launchers with an xdg-open shebang. Follow that convention.
728  std::string output_buffer = std::string(kXdgOpenShebang) + "\n";
729
730  // See http://standards.freedesktop.org/desktop-entry-spec/latest/
731  GKeyFile* key_file = g_key_file_new();
732
733  // Set keys with fixed values.
734  g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0");
735  g_key_file_set_string(key_file, kDesktopEntry, "Terminal", "false");
736  g_key_file_set_string(key_file, kDesktopEntry, "Type", "Application");
737
738  // Set the "Name" key.
739  std::string final_title = UTF16ToUTF8(title);
740  // Make sure no endline characters can slip in and possibly introduce
741  // additional lines (like Exec, which makes it a security risk). Also
742  // use the URL as a default when the title is empty.
743  if (final_title.empty() ||
744      final_title.find("\n") != std::string::npos ||
745      final_title.find("\r") != std::string::npos) {
746    final_title = url.spec();
747  }
748  g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str());
749
750  // Set the "Exec" key.
751  std::string final_path = chrome_exe_path.value();
752  CommandLine cmd_line(CommandLine::NO_PROGRAM);
753  cmd_line = ShellIntegration::CommandLineArgsForLauncher(
754      url, extension_id, profile_path);
755  const CommandLine::SwitchMap& switch_map = cmd_line.GetSwitches();
756  for (CommandLine::SwitchMap::const_iterator i = switch_map.begin();
757       i != switch_map.end(); ++i) {
758    if (i->second.empty()) {
759      final_path += " --" + i->first;
760    } else {
761      final_path += " " + QuoteArgForDesktopFileExec("--" + i->first +
762                                                     "=" + i->second);
763    }
764  }
765
766  g_key_file_set_string(key_file, kDesktopEntry, "Exec", final_path.c_str());
767
768  // Set the "Icon" key.
769  if (!icon_name.empty()) {
770    g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str());
771  } else {
772    g_key_file_set_string(key_file, kDesktopEntry, "Icon",
773                          GetIconName().c_str());
774  }
775
776  // Set the "NoDisplay" key.
777  if (no_display)
778    g_key_file_set_string(key_file, kDesktopEntry, "NoDisplay", "true");
779
780  std::string wmclass = web_app::GetWMClassFromAppName(app_name);
781  g_key_file_set_string(key_file, kDesktopEntry, "StartupWMClass",
782                        wmclass.c_str());
783
784  gsize length = 0;
785  gchar* data_dump = g_key_file_to_data(key_file, &length, NULL);
786  if (data_dump) {
787    // If strlen(data_dump[0]) == 0, this check will fail.
788    if (data_dump[0] == '\n') {
789      // Older versions of glib produce a leading newline. If this is the case,
790      // remove it to avoid double-newline after the shebang.
791      output_buffer += (data_dump + 1);
792    } else {
793      output_buffer += data_dump;
794    }
795    g_free(data_dump);
796  }
797
798  g_key_file_free(key_file);
799  return output_buffer;
800}
801
802std::string GetDirectoryFileContents(const base::string16& title,
803                                     const std::string& icon_name) {
804  // See http://standards.freedesktop.org/desktop-entry-spec/latest/
805  GKeyFile* key_file = g_key_file_new();
806
807  g_key_file_set_string(key_file, kDesktopEntry, "Version", "1.0");
808  g_key_file_set_string(key_file, kDesktopEntry, "Type", "Directory");
809  std::string final_title = UTF16ToUTF8(title);
810  g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str());
811  if (!icon_name.empty()) {
812    g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str());
813  } else {
814    g_key_file_set_string(key_file, kDesktopEntry, "Icon",
815                          GetIconName().c_str());
816  }
817
818  gsize length = 0;
819  gchar* data_dump = g_key_file_to_data(key_file, &length, NULL);
820  std::string output_buffer;
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
837bool CreateDesktopShortcut(
838    const ShellIntegration::ShortcutInfo& shortcut_info,
839    const ShellIntegration::ShortcutLocations& creation_locations) {
840  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
841
842  base::FilePath shortcut_filename;
843  if (!shortcut_info.extension_id.empty()) {
844    shortcut_filename = GetExtensionShortcutFilename(
845        shortcut_info.profile_path, shortcut_info.extension_id);
846    // For extensions we do not want duplicate shortcuts. So, delete any that
847    // already exist and replace them.
848    if (creation_locations.on_desktop)
849      DeleteShortcutOnDesktop(shortcut_filename);
850    // The 'applications_menu_location' and 'hidden' locations are actually the
851    // same place ('applications').
852    if (creation_locations.applications_menu_location !=
853            ShellIntegration::APP_MENU_LOCATION_NONE ||
854        creation_locations.hidden)
855      DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath());
856  } else {
857    shortcut_filename = GetWebShortcutFilename(shortcut_info.url);
858  }
859  if (shortcut_filename.empty())
860    return false;
861
862  std::string icon_name = CreateShortcutIcon(shortcut_info, shortcut_filename);
863
864  std::string app_name =
865      web_app::GenerateApplicationNameFromInfo(shortcut_info);
866
867  bool success = true;
868
869  base::FilePath chrome_exe_path = GetChromeExePath();
870  if (chrome_exe_path.empty()) {
871    LOG(WARNING) << "Could not get executable path.";
872    return false;
873  }
874
875  if (creation_locations.on_desktop) {
876    std::string contents = ShellIntegrationLinux::GetDesktopFileContents(
877        chrome_exe_path,
878        app_name,
879        shortcut_info.url,
880        shortcut_info.extension_id,
881        shortcut_info.extension_path,
882        shortcut_info.title,
883        icon_name,
884        shortcut_info.profile_path,
885        false);
886    success = CreateShortcutOnDesktop(shortcut_filename, contents);
887  }
888
889  if (creation_locations.applications_menu_location !=
890          ShellIntegration::APP_MENU_LOCATION_NONE ||
891      creation_locations.hidden) {
892    base::FilePath directory_filename;
893    std::string directory_contents;
894    switch (creation_locations.applications_menu_location) {
895      case ShellIntegration::APP_MENU_LOCATION_NONE:
896      case ShellIntegration::APP_MENU_LOCATION_ROOT:
897        break;
898      case ShellIntegration::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS:
899        directory_filename = base::FilePath(kDirectoryFilename);
900        directory_contents = ShellIntegrationLinux::GetDirectoryFileContents(
901            ShellIntegration::GetAppShortcutsSubdirName(), "");
902        break;
903      default:
904        NOTREACHED();
905        break;
906    }
907    // Set NoDisplay=true if hidden but not in the applications menu. This will
908    // hide the application from user-facing menus.
909    std::string contents = ShellIntegrationLinux::GetDesktopFileContents(
910        chrome_exe_path,
911        app_name,
912        shortcut_info.url,
913        shortcut_info.extension_id,
914        shortcut_info.extension_path,
915        shortcut_info.title,
916        icon_name,
917        shortcut_info.profile_path,
918        creation_locations.applications_menu_location ==
919            ShellIntegration::APP_MENU_LOCATION_NONE);
920    success = CreateShortcutInApplicationsMenu(
921        shortcut_filename, contents, directory_filename, directory_contents) &&
922        success;
923  }
924
925  return success;
926}
927
928void DeleteDesktopShortcuts(const base::FilePath& profile_path,
929                            const std::string& extension_id) {
930  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
931
932  base::FilePath shortcut_filename = GetExtensionShortcutFilename(
933      profile_path, extension_id);
934  DCHECK(!shortcut_filename.empty());
935
936  DeleteShortcutOnDesktop(shortcut_filename);
937  // Delete shortcuts from |kDirectoryFilename|.
938  // Note that it is possible that shortcuts were not created in the Chrome Apps
939  // directory. It doesn't matter: this will still delete the shortcut even if
940  // it isn't in the directory.
941  DeleteShortcutInApplicationsMenu(shortcut_filename,
942                                   base::FilePath(kDirectoryFilename));
943}
944
945void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) {
946  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
947
948  scoped_ptr<base::Environment> env(base::Environment::Create());
949
950  // Delete shortcuts from Desktop.
951  base::FilePath desktop_path;
952  if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) {
953    std::vector<base::FilePath> shortcut_filenames_desktop =
954        GetExistingProfileShortcutFilenames(profile_path, desktop_path);
955    for (std::vector<base::FilePath>::const_iterator it =
956         shortcut_filenames_desktop.begin();
957         it != shortcut_filenames_desktop.end(); ++it) {
958      DeleteShortcutOnDesktop(*it);
959    }
960  }
961
962  // Delete shortcuts from |kDirectoryFilename|.
963  base::FilePath applications_menu;
964  if (GetDataWriteLocation(env.get(), &applications_menu)) {
965    applications_menu = applications_menu.AppendASCII("applications");
966    std::vector<base::FilePath> shortcut_filenames_app_menu =
967        GetExistingProfileShortcutFilenames(profile_path, applications_menu);
968    for (std::vector<base::FilePath>::const_iterator it =
969         shortcut_filenames_app_menu.begin();
970         it != shortcut_filenames_app_menu.end(); ++it) {
971      DeleteShortcutInApplicationsMenu(*it,
972                                       base::FilePath(kDirectoryFilename));
973    }
974  }
975}
976
977}  // namespace ShellIntegrationLinux
978