shell_integration_linux.cc revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/shell_integration_linux.h"
6
7#include <fcntl.h>
8#include <glib.h>
9#include <stdlib.h>
10#include <sys/stat.h>
11#include <sys/types.h>
12#include <unistd.h>
13
14#include <string>
15#include <vector>
16
17#include "base/base_paths.h"
18#include "base/command_line.h"
19#include "base/environment.h"
20#include "base/file_util.h"
21#include "base/files/file_enumerator.h"
22#include "base/files/file_path.h"
23#include "base/files/scoped_temp_dir.h"
24#include "base/i18n/file_util_icu.h"
25#include "base/memory/ref_counted_memory.h"
26#include "base/memory/scoped_ptr.h"
27#include "base/message_loop/message_loop.h"
28#include "base/path_service.h"
29#include "base/posix/eintr_wrapper.h"
30#include "base/process/kill.h"
31#include "base/process/launch.h"
32#include "base/strings/string_number_conversions.h"
33#include "base/strings/string_tokenizer.h"
34#include "base/strings/string_util.h"
35#include "base/strings/utf_string_conversions.h"
36#include "base/threading/thread.h"
37#include "base/threading/thread_restrictions.h"
38#include "build/build_config.h"
39#include "chrome/browser/web_applications/web_app.h"
40#include "chrome/common/chrome_constants.h"
41#include "chrome/common/chrome_version_info.h"
42#include "content/public/browser/browser_thread.h"
43#include "ui/gfx/image/image_family.h"
44#include "url/gurl.h"
45
46using content::BrowserThread;
47
48namespace {
49
50// Helper to launch xdg scripts. We don't want them to ask any questions on the
51// terminal etc. The function returns true if the utility launches and exits
52// cleanly, in which case |exit_code| returns the utility's exit code.
53bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) {
54  // xdg-settings internally runs xdg-mime, which uses mv to move newly-created
55  // files on top of originals after making changes to them. In the event that
56  // the original files are owned by another user (e.g. root, which can happen
57  // if they are updated within sudo), mv will prompt the user to confirm if
58  // standard input is a terminal (otherwise it just does it). So make sure it's
59  // not, to avoid locking everything up waiting for mv.
60  *exit_code = EXIT_FAILURE;
61  int devnull = open("/dev/null", O_RDONLY);
62  if (devnull < 0)
63    return false;
64  base::FileHandleMappingVector no_stdin;
65  no_stdin.push_back(std::make_pair(devnull, STDIN_FILENO));
66
67  base::ProcessHandle handle;
68  base::LaunchOptions options;
69  options.fds_to_remap = &no_stdin;
70  if (!base::LaunchProcess(argv, options, &handle)) {
71    close(devnull);
72    return false;
73  }
74  close(devnull);
75
76  return base::WaitForExitCode(handle, exit_code);
77}
78
79std::string CreateShortcutIcon(
80    const ShellIntegration::ShortcutInfo& shortcut_info,
81    const base::FilePath& shortcut_filename) {
82  if (shortcut_info.favicon.empty())
83    return std::string();
84
85  // TODO(phajdan.jr): Report errors from this function, possibly as infobars.
86  base::ScopedTempDir temp_dir;
87  if (!temp_dir.CreateUniqueTempDir())
88    return std::string();
89
90  base::FilePath temp_file_path = temp_dir.path().Append(
91      shortcut_filename.ReplaceExtension("png"));
92  std::string icon_name = temp_file_path.BaseName().RemoveExtension().value();
93
94  for (gfx::ImageFamily::const_iterator it = shortcut_info.favicon.begin();
95       it != shortcut_info.favicon.end(); ++it) {
96    int width = it->Width();
97    scoped_refptr<base::RefCountedMemory> png_data = it->As1xPNGBytes();
98    if (png_data->size() == 0) {
99      // If the bitmap could not be encoded to PNG format, skip it.
100      LOG(WARNING) << "Could not encode icon " << icon_name << ".png at size "
101                   << width << ".";
102      continue;
103    }
104    int bytes_written = file_util::WriteFile(temp_file_path,
105        reinterpret_cast<const char*>(png_data->front()), png_data->size());
106
107    if (bytes_written != static_cast<int>(png_data->size()))
108      return std::string();
109
110    std::vector<std::string> argv;
111    argv.push_back("xdg-icon-resource");
112    argv.push_back("install");
113
114    // Always install in user mode, even if someone runs the browser as root
115    // (people do that).
116    argv.push_back("--mode");
117    argv.push_back("user");
118
119    argv.push_back("--size");
120    argv.push_back(base::IntToString(width));
121
122    argv.push_back(temp_file_path.value());
123    argv.push_back(icon_name);
124    int exit_code;
125    if (!LaunchXdgUtility(argv, &exit_code) || exit_code) {
126      LOG(WARNING) << "Could not install icon " << icon_name << ".png at size "
127                   << width << ".";
128    }
129  }
130  return icon_name;
131}
132
133bool CreateShortcutOnDesktop(const base::FilePath& shortcut_filename,
134                             const std::string& contents) {
135  // Make sure that we will later call openat in a secure way.
136  DCHECK_EQ(shortcut_filename.BaseName().value(), shortcut_filename.value());
137
138  base::FilePath desktop_path;
139  if (!PathService::Get(base::DIR_USER_DESKTOP, &desktop_path))
140    return false;
141
142  int desktop_fd = open(desktop_path.value().c_str(), O_RDONLY | O_DIRECTORY);
143  if (desktop_fd < 0)
144    return false;
145
146  int fd = openat(desktop_fd, shortcut_filename.value().c_str(),
147                  O_CREAT | O_EXCL | O_WRONLY,
148                  S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
149  if (fd < 0) {
150    if (HANDLE_EINTR(close(desktop_fd)) < 0)
151      PLOG(ERROR) << "close";
152    return false;
153  }
154
155  ssize_t bytes_written = file_util::WriteFileDescriptor(fd, contents.data(),
156                                                         contents.length());
157  if (HANDLE_EINTR(close(fd)) < 0)
158    PLOG(ERROR) << "close";
159
160  if (bytes_written != static_cast<ssize_t>(contents.length())) {
161    // Delete the file. No shortuct is better than corrupted one. Use unlinkat
162    // to make sure we're deleting the file in the directory we think we are.
163    // Even if an attacker manager to put something other at
164    // |shortcut_filename| we'll just undo his action.
165    unlinkat(desktop_fd, shortcut_filename.value().c_str(), 0);
166  }
167
168  if (HANDLE_EINTR(close(desktop_fd)) < 0)
169    PLOG(ERROR) << "close";
170
171  return true;
172}
173
174void DeleteShortcutOnDesktop(const base::FilePath& shortcut_filename) {
175  base::FilePath desktop_path;
176  if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path))
177    base::DeleteFile(desktop_path.Append(shortcut_filename), false);
178}
179
180// Creates a shortcut with |shortcut_filename| and |contents| in the system
181// applications menu. If |directory_filename| is non-empty, creates a sub-menu
182// with |directory_filename| and |directory_contents|, and stores the shortcut
183// under the sub-menu.
184bool CreateShortcutInApplicationsMenu(const base::FilePath& shortcut_filename,
185                                      const std::string& contents,
186                                      const base::FilePath& directory_filename,
187                                      const std::string& directory_contents) {
188  base::ScopedTempDir temp_dir;
189  if (!temp_dir.CreateUniqueTempDir())
190    return false;
191
192  base::FilePath temp_directory_path;
193  if (!directory_filename.empty()) {
194    temp_directory_path = temp_dir.path().Append(directory_filename);
195
196    int bytes_written = file_util::WriteFile(temp_directory_path,
197                                             directory_contents.data(),
198                                             directory_contents.length());
199
200    if (bytes_written != static_cast<int>(directory_contents.length()))
201      return false;
202  }
203
204  base::FilePath temp_file_path = temp_dir.path().Append(shortcut_filename);
205
206  int bytes_written = file_util::WriteFile(temp_file_path, contents.data(),
207                                           contents.length());
208
209  if (bytes_written != static_cast<int>(contents.length()))
210    return false;
211
212  std::vector<std::string> argv;
213  argv.push_back("xdg-desktop-menu");
214  argv.push_back("install");
215
216  // Always install in user mode, even if someone runs the browser as root
217  // (people do that).
218  argv.push_back("--mode");
219  argv.push_back("user");
220
221  // If provided, install the shortcut file inside the given directory.
222  if (!directory_filename.empty())
223    argv.push_back(temp_directory_path.value());
224  argv.push_back(temp_file_path.value());
225  int exit_code;
226  LaunchXdgUtility(argv, &exit_code);
227  return exit_code == 0;
228}
229
230void DeleteShortcutInApplicationsMenu(
231    const base::FilePath& shortcut_filename,
232    const base::FilePath& directory_filename) {
233  std::vector<std::string> argv;
234  argv.push_back("xdg-desktop-menu");
235  argv.push_back("uninstall");
236
237  // Uninstall in user mode, to match the install.
238  argv.push_back("--mode");
239  argv.push_back("user");
240
241  // The file does not need to exist anywhere - xdg-desktop-menu will uninstall
242  // items from the menu with a matching name.
243  // If |directory_filename| is supplied, this will also remove the item from
244  // the directory, and remove the directory if it is empty.
245  if (!directory_filename.empty())
246    argv.push_back(directory_filename.value());
247  argv.push_back(shortcut_filename.value());
248  int exit_code;
249  LaunchXdgUtility(argv, &exit_code);
250}
251
252// Quote a string such that it appears as one verbatim argument for the Exec
253// key in a desktop file.
254std::string QuoteArgForDesktopFileExec(const std::string& arg) {
255  // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
256
257  // Quoting is only necessary if the argument has a reserved character.
258  if (arg.find_first_of(" \t\n\"'\\><~|&;$*?#()`") == std::string::npos)
259    return arg;  // No quoting necessary.
260
261  std::string quoted = "\"";
262  for (size_t i = 0; i < arg.size(); ++i) {
263    // Note that the set of backslashed characters is smaller than the
264    // set of reserved characters.
265    switch (arg[i]) {
266      case '"':
267      case '`':
268      case '$':
269      case '\\':
270        quoted += '\\';
271        break;
272    }
273    quoted += arg[i];
274  }
275  quoted += '"';
276
277  return quoted;
278}
279
280const char kDesktopEntry[] = "Desktop Entry";
281
282const char kXdgOpenShebang[] = "#!/usr/bin/env xdg-open";
283
284const char kXdgSettings[] = "xdg-settings";
285const char kXdgSettingsDefaultBrowser[] = "default-web-browser";
286const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler";
287
288const char kDirectoryFilename[] = "chrome-apps.directory";
289
290}  // namespace
291
292namespace {
293
294// Utility function to get the path to the version of a script shipped with
295// Chrome. |script| gives the name of the script. |chrome_version| returns the
296// path to the Chrome version of the script, and the return value of the
297// function is true if the function is successful and the Chrome version is
298// not the script found on the PATH.
299bool GetChromeVersionOfScript(const std::string& script,
300                               std::string* chrome_version) {
301  // Get the path to the Chrome version.
302  base::FilePath chrome_dir;
303  if (!PathService::Get(base::DIR_EXE, &chrome_dir))
304    return false;
305
306  base::FilePath chrome_version_path = chrome_dir.Append(script);
307  *chrome_version = chrome_version_path.value();
308
309  // Check if this is different to the one on path.
310  std::vector<std::string> argv;
311  argv.push_back("which");
312  argv.push_back(script);
313  std::string path_version;
314  if (base::GetAppOutput(CommandLine(argv), &path_version)) {
315    // Remove trailing newline
316    path_version.erase(path_version.length() - 1, 1);
317    base::FilePath path_version_path(path_version);
318    return (chrome_version_path != path_version_path);
319  }
320  return false;
321}
322
323// Value returned by xdg-settings if it can't understand our request.
324const int EXIT_XDG_SETTINGS_SYNTAX_ERROR = 1;
325
326// We delegate the difficulty of setting the default browser and default url
327// scheme handler in Linux desktop environments to an xdg utility, xdg-settings.
328
329// When calling this script we first try to use the script on PATH. If that
330// fails we then try to use the script that we have included. This gives
331// scripts on the system priority over ours, as distribution vendors may have
332// tweaked the script, but still allows our copy to be used if the script on the
333// system fails, as the system copy may be missing capabilities of the Chrome
334// copy.
335
336// If |protocol| is empty this function sets Chrome as the default browser,
337// otherwise it sets Chrome as the default handler application for |protocol|.
338bool SetDefaultWebClient(const std::string& protocol) {
339#if defined(OS_CHROMEOS)
340  return true;
341#else
342  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
343
344  scoped_ptr<base::Environment> env(base::Environment::Create());
345
346  std::vector<std::string> argv;
347  argv.push_back(kXdgSettings);
348  argv.push_back("set");
349  if (protocol.empty()) {
350    argv.push_back(kXdgSettingsDefaultBrowser);
351  } else {
352    argv.push_back(kXdgSettingsDefaultSchemeHandler);
353    argv.push_back(protocol);
354  }
355  argv.push_back(ShellIntegrationLinux::GetDesktopName(env.get()));
356
357  int exit_code;
358  bool ran_ok = LaunchXdgUtility(argv, &exit_code);
359  if (ran_ok && exit_code == EXIT_XDG_SETTINGS_SYNTAX_ERROR) {
360    if (GetChromeVersionOfScript(kXdgSettings, &argv[0])) {
361      ran_ok = LaunchXdgUtility(argv, &exit_code);
362    }
363  }
364
365  return ran_ok && exit_code == EXIT_SUCCESS;
366#endif
367}
368
369// If |protocol| is empty this function checks if Chrome is the default browser,
370// otherwise it checks if Chrome is the default handler application for
371// |protocol|.
372ShellIntegration::DefaultWebClientState GetIsDefaultWebClient(
373    const std::string& protocol) {
374#if defined(OS_CHROMEOS)
375  return ShellIntegration::IS_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  }
440
441  g_key_file_free(key_file);
442  return nodisplay;
443}
444
445// Gets the path to the Chrome executable or wrapper script.
446// Returns an empty path if the executable path could not be found.
447base::FilePath GetChromeExePath() {
448  // Try to get the name of the wrapper script that launched Chrome.
449  scoped_ptr<base::Environment> environment(base::Environment::Create());
450  std::string wrapper_script;
451  if (environment->GetVar("CHROME_WRAPPER", &wrapper_script)) {
452    return base::FilePath(wrapper_script);
453  }
454
455  // Just return the name of the executable path for Chrome.
456  base::FilePath chrome_exe_path;
457  PathService::Get(base::FILE_EXE, &chrome_exe_path);
458  return chrome_exe_path;
459}
460
461} // namespace
462
463// static
464ShellIntegration::DefaultWebClientSetPermission
465    ShellIntegration::CanSetAsDefaultBrowser() {
466  return SET_DEFAULT_UNATTENDED;
467}
468
469// static
470bool ShellIntegration::SetAsDefaultBrowser() {
471  return SetDefaultWebClient(std::string());
472}
473
474// static
475bool ShellIntegration::SetAsDefaultProtocolClient(const std::string& protocol) {
476  return SetDefaultWebClient(protocol);
477}
478
479// static
480ShellIntegration::DefaultWebClientState ShellIntegration::GetDefaultBrowser() {
481  return GetIsDefaultWebClient(std::string());
482}
483
484// static
485std::string ShellIntegration::GetApplicationForProtocol(const GURL& url) {
486  return std::string("xdg-open");
487}
488
489// static
490ShellIntegration::DefaultWebClientState
491ShellIntegration::IsDefaultProtocolClient(const std::string& protocol) {
492  return GetIsDefaultWebClient(protocol);
493}
494
495// static
496bool ShellIntegration::IsFirefoxDefaultBrowser() {
497  std::vector<std::string> argv;
498  argv.push_back(kXdgSettings);
499  argv.push_back("get");
500  argv.push_back(kXdgSettingsDefaultBrowser);
501
502  std::string browser;
503  // We don't care about the return value here.
504  base::GetAppOutput(CommandLine(argv), &browser);
505  return browser.find("irefox") != std::string::npos;
506}
507
508namespace ShellIntegrationLinux {
509
510bool GetDataWriteLocation(base::Environment* env, base::FilePath* search_path) {
511  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
512
513  std::string xdg_data_home;
514  std::string home;
515  if (env->GetVar("XDG_DATA_HOME", &xdg_data_home) && !xdg_data_home.empty()) {
516    *search_path = base::FilePath(xdg_data_home);
517    return true;
518  } else if (env->GetVar("HOME", &home) && !home.empty()) {
519    *search_path = base::FilePath(home).Append(".local").Append("share");
520    return true;
521  }
522  return false;
523}
524
525std::vector<base::FilePath> GetDataSearchLocations(base::Environment* env) {
526  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
527
528  std::vector<base::FilePath> search_paths;
529
530  base::FilePath write_location;
531  if (GetDataWriteLocation(env, &write_location))
532    search_paths.push_back(write_location);
533
534  std::string xdg_data_dirs;
535  if (env->GetVar("XDG_DATA_DIRS", &xdg_data_dirs) && !xdg_data_dirs.empty()) {
536    base::StringTokenizer tokenizer(xdg_data_dirs, ":");
537    while (tokenizer.GetNext()) {
538      base::FilePath data_dir(tokenizer.token());
539      search_paths.push_back(data_dir);
540    }
541  } else {
542    search_paths.push_back(base::FilePath("/usr/local/share"));
543    search_paths.push_back(base::FilePath("/usr/share"));
544  }
545
546  return search_paths;
547}
548
549std::string GetProgramClassName() {
550  DCHECK(CommandLine::InitializedForCurrentProcess());
551  // Get the res_name component from argv[0].
552  const CommandLine* command_line = CommandLine::ForCurrentProcess();
553  std::string class_name = command_line->GetProgram().BaseName().value();
554  if (!class_name.empty())
555    class_name[0] = base::ToUpperASCII(class_name[0]);
556  return class_name;
557}
558
559std::string GetDesktopName(base::Environment* env) {
560#if defined(GOOGLE_CHROME_BUILD)
561  std::string version_string_modifier(
562      chrome::VersionInfo::GetVersionStringModifier());
563  if (version_string_modifier.empty())
564    return "google-chrome.desktop";
565
566  return std::string("google-chrome-") + version_string_modifier + ".desktop";
567#else  // CHROMIUM_BUILD
568  // Allow $CHROME_DESKTOP to override the built-in value, so that development
569  // versions can set themselves as the default without interfering with
570  // non-official, packaged versions using the built-in value.
571  std::string name;
572  if (env->GetVar("CHROME_DESKTOP", &name) && !name.empty())
573    return name;
574  return "chromium-browser.desktop";
575#endif
576}
577
578std::string GetIconName() {
579#if defined(GOOGLE_CHROME_BUILD)
580  return "google-chrome";
581#else  // CHROMIUM_BUILD
582  return "chromium-browser";
583#endif
584}
585
586ShellIntegration::ShortcutLocations GetExistingShortcutLocations(
587    base::Environment* env,
588    const base::FilePath& profile_path,
589    const std::string& extension_id) {
590  base::FilePath desktop_path;
591  // If Get returns false, just leave desktop_path empty.
592  PathService::Get(base::DIR_USER_DESKTOP, &desktop_path);
593  return GetExistingShortcutLocations(env, profile_path, extension_id,
594                                             desktop_path);
595}
596
597ShellIntegration::ShortcutLocations GetExistingShortcutLocations(
598    base::Environment* env,
599    const base::FilePath& profile_path,
600    const std::string& extension_id,
601    const base::FilePath& desktop_path) {
602  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
603
604  base::FilePath shortcut_filename = GetExtensionShortcutFilename(
605      profile_path, extension_id);
606  DCHECK(!shortcut_filename.empty());
607  ShellIntegration::ShortcutLocations locations;
608
609  // Determine whether there is a shortcut on desktop.
610  if (!desktop_path.empty()) {
611    locations.on_desktop =
612        base::PathExists(desktop_path.Append(shortcut_filename));
613  }
614
615  // Determine whether there is a shortcut in the applications directory.
616  std::string shortcut_contents;
617  if (GetExistingShortcutContents(env, shortcut_filename, &shortcut_contents)) {
618    // Whether this counts as "hidden" or "in_applications_menu" depends on
619    // whether it contains NoDisplay=true.
620    if (GetNoDisplayFromDesktopFile(shortcut_contents))
621      locations.hidden = true;
622    else
623      locations.in_applications_menu = true;
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  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  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 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 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    if (creation_locations.in_applications_menu || creation_locations.hidden)
851      DeleteShortcutInApplicationsMenu(shortcut_filename, base::FilePath());
852  } else {
853    shortcut_filename = GetWebShortcutFilename(shortcut_info.url);
854  }
855  if (shortcut_filename.empty())
856    return false;
857
858  std::string icon_name = CreateShortcutIcon(shortcut_info, shortcut_filename);
859
860  std::string app_name =
861      web_app::GenerateApplicationNameFromInfo(shortcut_info);
862
863  bool success = true;
864
865  base::FilePath chrome_exe_path = GetChromeExePath();
866  if (chrome_exe_path.empty()) {
867    LOG(WARNING) << "Could not get executable path.";
868    return false;
869  }
870
871  if (creation_locations.on_desktop) {
872    std::string contents = ShellIntegrationLinux::GetDesktopFileContents(
873        chrome_exe_path,
874        app_name,
875        shortcut_info.url,
876        shortcut_info.extension_id,
877        shortcut_info.extension_path,
878        shortcut_info.title,
879        icon_name,
880        shortcut_info.profile_path,
881        false);
882    success = CreateShortcutOnDesktop(shortcut_filename, contents);
883  }
884
885  // The 'in_applications_menu' and 'hidden' locations are actually the same
886  // place ('applications').
887  if (creation_locations.in_applications_menu || creation_locations.hidden) {
888    base::FilePath directory_filename;
889    std::string directory_contents;
890    if (!creation_locations.applications_menu_subdir.empty()) {
891      directory_filename = base::FilePath(kDirectoryFilename);
892      directory_contents = ShellIntegrationLinux::GetDirectoryFileContents(
893          creation_locations.applications_menu_subdir, "");
894    }
895    // Set NoDisplay=true if hidden but not in_applications_menu. This will hide
896    // the application from user-facing menus.
897    std::string contents = ShellIntegrationLinux::GetDesktopFileContents(
898        chrome_exe_path,
899        app_name,
900        shortcut_info.url,
901        shortcut_info.extension_id,
902        shortcut_info.extension_path,
903        shortcut_info.title,
904        icon_name,
905        shortcut_info.profile_path,
906        !creation_locations.in_applications_menu);
907    success = CreateShortcutInApplicationsMenu(
908        shortcut_filename, contents, directory_filename, directory_contents) &&
909        success;
910  }
911
912  return success;
913}
914
915void DeleteDesktopShortcuts(const base::FilePath& profile_path,
916                            const std::string& extension_id) {
917  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
918
919  base::FilePath shortcut_filename = GetExtensionShortcutFilename(
920      profile_path, extension_id);
921  DCHECK(!shortcut_filename.empty());
922
923  DeleteShortcutOnDesktop(shortcut_filename);
924  // Delete shortcuts from |kDirectoryFilename|.
925  // Note that it is possible that shortcuts were not created in the Chrome Apps
926  // directory (depending on the value of |applications_menu_subdir| when they
927  // were created). It doesn't matter: this will still delete the shortcut even
928  // if it isn't in the directory.
929  DeleteShortcutInApplicationsMenu(shortcut_filename,
930                                   base::FilePath(kDirectoryFilename));
931}
932
933void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) {
934  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
935
936  scoped_ptr<base::Environment> env(base::Environment::Create());
937
938  // Delete shortcuts from Desktop.
939  base::FilePath desktop_path;
940  if (PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) {
941    std::vector<base::FilePath> shortcut_filenames_desktop =
942        GetExistingProfileShortcutFilenames(profile_path, desktop_path);
943    for (std::vector<base::FilePath>::const_iterator it =
944         shortcut_filenames_desktop.begin();
945         it != shortcut_filenames_desktop.end(); ++it) {
946      DeleteShortcutOnDesktop(*it);
947    }
948  }
949
950  // Delete shortcuts from |kDirectoryFilename|.
951  base::FilePath applications_menu;
952  if (GetDataWriteLocation(env.get(), &applications_menu)) {
953    applications_menu = applications_menu.AppendASCII("applications");
954    std::vector<base::FilePath> shortcut_filenames_app_menu =
955        GetExistingProfileShortcutFilenames(profile_path, applications_menu);
956    for (std::vector<base::FilePath>::const_iterator it =
957         shortcut_filenames_app_menu.begin();
958         it != shortcut_filenames_app_menu.end(); ++it) {
959      DeleteShortcutInApplicationsMenu(*it,
960                                       base::FilePath(kDirectoryFilename));
961    }
962  }
963}
964
965}  // namespace ShellIntegrationLinux
966