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