1// Copyright (c) 2011 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.h"
6
7#include <fcntl.h>
8#include <stdlib.h>
9#include <sys/stat.h>
10#include <sys/types.h>
11#include <unistd.h>
12
13#include <string>
14#include <vector>
15
16#include "base/command_line.h"
17#include "base/eintr_wrapper.h"
18#include "base/environment.h"
19#include "base/file_path.h"
20#include "base/file_util.h"
21#include "base/i18n/file_util_icu.h"
22#include "base/memory/scoped_temp_dir.h"
23#include "base/message_loop.h"
24#include "base/path_service.h"
25#include "base/process_util.h"
26#include "base/string_number_conversions.h"
27#include "base/string_tokenizer.h"
28#include "base/string_util.h"
29#include "base/task.h"
30#include "base/threading/thread.h"
31#include "base/utf_string_conversions.h"
32#include "build/build_config.h"
33#include "chrome/browser/web_applications/web_app.h"
34#include "chrome/common/chrome_constants.h"
35#include "chrome/common/chrome_paths.h"
36#include "content/browser/browser_thread.h"
37#include "googleurl/src/gurl.h"
38#include "ui/gfx/codec/png_codec.h"
39
40namespace {
41
42// Helper to launch xdg scripts. We don't want them to ask any questions on the
43// terminal etc.
44bool LaunchXdgUtility(const std::vector<std::string>& argv) {
45  // xdg-settings internally runs xdg-mime, which uses mv to move newly-created
46  // files on top of originals after making changes to them. In the event that
47  // the original files are owned by another user (e.g. root, which can happen
48  // if they are updated within sudo), mv will prompt the user to confirm if
49  // standard input is a terminal (otherwise it just does it). So make sure it's
50  // not, to avoid locking everything up waiting for mv.
51  int devnull = open("/dev/null", O_RDONLY);
52  if (devnull < 0)
53    return false;
54  base::file_handle_mapping_vector no_stdin;
55  no_stdin.push_back(std::make_pair(devnull, STDIN_FILENO));
56
57  base::ProcessHandle handle;
58  if (!base::LaunchApp(argv, no_stdin, false, &handle)) {
59    close(devnull);
60    return false;
61  }
62  close(devnull);
63
64  int success_code;
65  base::WaitForExitCode(handle, &success_code);
66  return success_code == EXIT_SUCCESS;
67}
68
69std::string CreateShortcutIcon(
70    const ShellIntegration::ShortcutInfo& shortcut_info,
71    const FilePath& shortcut_filename) {
72  if (shortcut_info.favicon.isNull())
73    return std::string();
74
75  // TODO(phajdan.jr): Report errors from this function, possibly as infobars.
76  ScopedTempDir temp_dir;
77  if (!temp_dir.CreateUniqueTempDir())
78    return std::string();
79
80  FilePath temp_file_path = temp_dir.path().Append(
81      shortcut_filename.ReplaceExtension("png"));
82
83  std::vector<unsigned char> png_data;
84  gfx::PNGCodec::EncodeBGRASkBitmap(shortcut_info.favicon, false, &png_data);
85  int bytes_written = file_util::WriteFile(temp_file_path,
86      reinterpret_cast<char*>(png_data.data()), png_data.size());
87
88  if (bytes_written != static_cast<int>(png_data.size()))
89    return std::string();
90
91  std::vector<std::string> argv;
92  argv.push_back("xdg-icon-resource");
93  argv.push_back("install");
94
95  // Always install in user mode, even if someone runs the browser as root
96  // (people do that).
97  argv.push_back("--mode");
98  argv.push_back("user");
99
100  argv.push_back("--size");
101  argv.push_back(base::IntToString(shortcut_info.favicon.width()));
102
103  argv.push_back(temp_file_path.value());
104  std::string icon_name = temp_file_path.BaseName().RemoveExtension().value();
105  argv.push_back(icon_name);
106  LaunchXdgUtility(argv);
107  return icon_name;
108}
109
110void CreateShortcutOnDesktop(const FilePath& shortcut_filename,
111                             const std::string& contents) {
112  // TODO(phajdan.jr): Report errors from this function, possibly as infobars.
113
114  // Make sure that we will later call openat in a secure way.
115  DCHECK_EQ(shortcut_filename.BaseName().value(), shortcut_filename.value());
116
117  FilePath desktop_path;
118  if (!PathService::Get(chrome::DIR_USER_DESKTOP, &desktop_path))
119    return;
120
121  int desktop_fd = open(desktop_path.value().c_str(), O_RDONLY | O_DIRECTORY);
122  if (desktop_fd < 0)
123    return;
124
125  int fd = openat(desktop_fd, shortcut_filename.value().c_str(),
126                  O_CREAT | O_EXCL | O_WRONLY,
127                  S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
128  if (fd < 0) {
129    if (HANDLE_EINTR(close(desktop_fd)) < 0)
130      PLOG(ERROR) << "close";
131    return;
132  }
133
134  ssize_t bytes_written = file_util::WriteFileDescriptor(fd, contents.data(),
135                                                         contents.length());
136  if (HANDLE_EINTR(close(fd)) < 0)
137    PLOG(ERROR) << "close";
138
139  if (bytes_written != static_cast<ssize_t>(contents.length())) {
140    // Delete the file. No shortuct is better than corrupted one. Use unlinkat
141    // to make sure we're deleting the file in the directory we think we are.
142    // Even if an attacker manager to put something other at
143    // |shortcut_filename| we'll just undo his action.
144    unlinkat(desktop_fd, shortcut_filename.value().c_str(), 0);
145  }
146
147  if (HANDLE_EINTR(close(desktop_fd)) < 0)
148    PLOG(ERROR) << "close";
149}
150
151void CreateShortcutInApplicationsMenu(const FilePath& shortcut_filename,
152                                      const std::string& contents) {
153  // TODO(phajdan.jr): Report errors from this function, possibly as infobars.
154  ScopedTempDir temp_dir;
155  if (!temp_dir.CreateUniqueTempDir())
156    return;
157
158  FilePath temp_file_path = temp_dir.path().Append(shortcut_filename);
159
160  int bytes_written = file_util::WriteFile(temp_file_path, contents.data(),
161                                           contents.length());
162
163  if (bytes_written != static_cast<int>(contents.length()))
164    return;
165
166  std::vector<std::string> argv;
167  argv.push_back("xdg-desktop-menu");
168  argv.push_back("install");
169
170  // Always install in user mode, even if someone runs the browser as root
171  // (people do that).
172  argv.push_back("--mode");
173  argv.push_back("user");
174
175  argv.push_back(temp_file_path.value());
176  LaunchXdgUtility(argv);
177}
178
179// Quote a string such that it appears as one verbatim argument for the Exec
180// key in a desktop file.
181std::string QuoteArgForDesktopFileExec(const std::string& arg) {
182  // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
183
184  // Quoting is only necessary if the argument has a reserved character.
185  if (arg.find_first_of(" \t\n\"'\\><~|&;$*?#()`") == std::string::npos)
186    return arg;  // No quoting necessary.
187
188  std::string quoted = "\"";
189  for (size_t i = 0; i < arg.size(); ++i) {
190    // Note that the set of backslashed characters is smaller than the
191    // set of reserved characters.
192    switch (arg[i]) {
193      case '"':
194      case '`':
195      case '$':
196      case '\\':
197        quoted += '\\';
198        break;
199    }
200    quoted += arg[i];
201  }
202  quoted += '"';
203
204  return quoted;
205}
206
207// Escape a string if needed for the right side of a Key=Value
208// construct in a desktop file.  (Note that for Exec= lines this
209// should be used in conjunction with QuoteArgForDesktopFileExec,
210// possibly escaping a backslash twice.)
211std::string EscapeStringForDesktopFile(const std::string& arg) {
212  // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s03.html
213  if (arg.find('\\') == std::string::npos)
214    return arg;
215
216  std::string escaped;
217  for (size_t i = 0; i < arg.size(); ++i) {
218    if (arg[i] == '\\')
219      escaped += '\\';
220    escaped += arg[i];
221  }
222  return escaped;
223}
224
225}  // namespace
226
227// static
228std::string ShellIntegration::GetDesktopName(base::Environment* env) {
229#if defined(GOOGLE_CHROME_BUILD)
230  return "google-chrome.desktop";
231#else  // CHROMIUM_BUILD
232  // Allow $CHROME_DESKTOP to override the built-in value, so that development
233  // versions can set themselves as the default without interfering with
234  // non-official, packaged versions using the built-in value.
235  std::string name;
236  if (env->GetVar("CHROME_DESKTOP", &name) && !name.empty())
237    return name;
238  return "chromium-browser.desktop";
239#endif
240}
241
242// We delegate the difficulty of setting the default browser in Linux desktop
243// environments to a new xdg utility, xdg-settings. We have to include a copy of
244// it for this to work, obviously, but that's actually the suggested approach
245// for xdg utilities anyway.
246
247// static
248bool ShellIntegration::SetAsDefaultBrowser() {
249  scoped_ptr<base::Environment> env(base::Environment::Create());
250
251  std::vector<std::string> argv;
252  argv.push_back("xdg-settings");
253  argv.push_back("set");
254  argv.push_back("default-web-browser");
255  argv.push_back(GetDesktopName(env.get()));
256  return LaunchXdgUtility(argv);
257}
258
259// static
260ShellIntegration::DefaultBrowserState ShellIntegration::IsDefaultBrowser() {
261  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
262
263  scoped_ptr<base::Environment> env(base::Environment::Create());
264
265  std::vector<std::string> argv;
266  argv.push_back("xdg-settings");
267  argv.push_back("check");
268  argv.push_back("default-web-browser");
269  argv.push_back(GetDesktopName(env.get()));
270
271  std::string reply;
272  if (!base::GetAppOutput(CommandLine(argv), &reply)) {
273    // xdg-settings failed: we can't determine or set the default browser.
274    return UNKNOWN_DEFAULT_BROWSER;
275  }
276
277  // Allow any reply that starts with "yes".
278  return (reply.find("yes") == 0) ? IS_DEFAULT_BROWSER : NOT_DEFAULT_BROWSER;
279}
280
281// static
282bool ShellIntegration::IsFirefoxDefaultBrowser() {
283  std::vector<std::string> argv;
284  argv.push_back("xdg-settings");
285  argv.push_back("get");
286  argv.push_back("default-web-browser");
287
288  std::string browser;
289  // We don't care about the return value here.
290  base::GetAppOutput(CommandLine(argv), &browser);
291  return browser.find("irefox") != std::string::npos;
292}
293
294// static
295bool ShellIntegration::GetDesktopShortcutTemplate(
296    base::Environment* env, std::string* output) {
297  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
298
299  std::vector<FilePath> search_paths;
300
301  std::string xdg_data_home;
302  if (env->GetVar("XDG_DATA_HOME", &xdg_data_home) &&
303      !xdg_data_home.empty()) {
304    search_paths.push_back(FilePath(xdg_data_home));
305  }
306
307  std::string xdg_data_dirs;
308  if (env->GetVar("XDG_DATA_DIRS", &xdg_data_dirs) &&
309      !xdg_data_dirs.empty()) {
310    StringTokenizer tokenizer(xdg_data_dirs, ":");
311    while (tokenizer.GetNext()) {
312      FilePath data_dir(tokenizer.token());
313      search_paths.push_back(data_dir);
314      search_paths.push_back(data_dir.Append("applications"));
315    }
316  }
317
318  // Add some fallback paths for systems which don't have XDG_DATA_DIRS or have
319  // it incomplete.
320  search_paths.push_back(FilePath("/usr/share/applications"));
321  search_paths.push_back(FilePath("/usr/local/share/applications"));
322
323  std::string template_filename(GetDesktopName(env));
324  for (std::vector<FilePath>::const_iterator i = search_paths.begin();
325       i != search_paths.end(); ++i) {
326    FilePath path = (*i).Append(template_filename);
327    VLOG(1) << "Looking for desktop file template in " << path.value();
328    if (file_util::PathExists(path)) {
329      VLOG(1) << "Found desktop file template at " << path.value();
330      return file_util::ReadFileToString(path, output);
331    }
332  }
333
334  LOG(ERROR) << "Could not find desktop file template.";
335  return false;
336}
337
338// static
339FilePath ShellIntegration::GetDesktopShortcutFilename(const GURL& url) {
340  // Use a prefix, because xdg-desktop-menu requires it.
341  std::string filename =
342      std::string(chrome::kBrowserProcessExecutableName) + "-" + url.spec();
343  file_util::ReplaceIllegalCharactersInPath(&filename, '_');
344
345  FilePath desktop_path;
346  if (!PathService::Get(chrome::DIR_USER_DESKTOP, &desktop_path))
347    return FilePath();
348
349  FilePath filepath = desktop_path.Append(filename);
350  FilePath alternative_filepath(filepath.value() + ".desktop");
351  for (size_t i = 1; i < 100; ++i) {
352    if (file_util::PathExists(FilePath(alternative_filepath))) {
353      alternative_filepath = FilePath(
354          filepath.value() + "_" + base::IntToString(i) + ".desktop");
355    } else {
356      return FilePath(alternative_filepath).BaseName();
357    }
358  }
359
360  return FilePath();
361}
362
363// static
364std::string ShellIntegration::GetDesktopFileContents(
365    const std::string& template_contents,
366    const std::string& app_name,
367    const GURL& url,
368    const std::string& extension_id,
369    const string16& title,
370    const std::string& icon_name) {
371  // See http://standards.freedesktop.org/desktop-entry-spec/latest/
372  // Although not required by the spec, Nautilus on Ubuntu Karmic creates its
373  // launchers with an xdg-open shebang. Follow that convention.
374  std::string output_buffer("#!/usr/bin/env xdg-open\n");
375  StringTokenizer tokenizer(template_contents, "\n");
376  while (tokenizer.GetNext()) {
377    if (tokenizer.token().substr(0, 5) == "Exec=") {
378      std::string exec_path = tokenizer.token().substr(5);
379      StringTokenizer exec_tokenizer(exec_path, " ");
380      std::string final_path;
381      while (exec_tokenizer.GetNext() && exec_tokenizer.token() != "%U") {
382        if (!final_path.empty())
383          final_path += " ";
384        final_path += exec_tokenizer.token();
385      }
386      CommandLine cmd_line =
387          ShellIntegration::CommandLineArgsForLauncher(url, extension_id);
388      const CommandLine::SwitchMap& switch_map = cmd_line.GetSwitches();
389      for (CommandLine::SwitchMap::const_iterator i = switch_map.begin();
390           i != switch_map.end(); ++i) {
391        if (i->second.empty()) {
392          final_path += " --" + i->first;
393        } else {
394          final_path += " " + QuoteArgForDesktopFileExec("--" + i->first +
395                                                         "=" + i->second);
396        }
397      }
398      output_buffer += std::string("Exec=") +
399                       EscapeStringForDesktopFile(final_path) + "\n";
400    } else if (tokenizer.token().substr(0, 5) == "Name=") {
401      std::string final_title = UTF16ToUTF8(title);
402      // Make sure no endline characters can slip in and possibly introduce
403      // additional lines (like Exec, which makes it a security risk). Also
404      // use the URL as a default when the title is empty.
405      if (final_title.empty() ||
406          final_title.find("\n") != std::string::npos ||
407          final_title.find("\r") != std::string::npos) {
408        final_title = url.spec();
409      }
410      output_buffer += StringPrintf("Name=%s\n", final_title.c_str());
411    } else if (tokenizer.token().substr(0, 11) == "GenericName" ||
412               tokenizer.token().substr(0, 7) == "Comment" ||
413               tokenizer.token().substr(0, 1) == "#") {
414      // Skip comment lines.
415    } else if (tokenizer.token().substr(0, 9) == "MimeType=") {
416      // Skip MimeType lines, they are only relevant for a web browser
417      // shortcut, not a web application shortcut.
418    } else if (tokenizer.token().substr(0, 15) == "StartupWMClass=") {
419      // Skip StartupWMClass; it will certainly be wrong since we emit a
420      // different one based on the app name below.
421    } else if (tokenizer.token().substr(0, 5) == "Icon=" &&
422               !icon_name.empty()) {
423      output_buffer += StringPrintf("Icon=%s\n", icon_name.c_str());
424    } else {
425      output_buffer += tokenizer.token() + "\n";
426    }
427  }
428
429#if defined(TOOLKIT_USES_GTK)
430  std::string wmclass = web_app::GetWMClassFromAppName(app_name);
431  if (!wmclass.empty()) {
432    output_buffer += StringPrintf("StartupWMClass=%s\n", wmclass.c_str());
433  }
434#endif
435
436  return output_buffer;
437}
438
439// static
440void ShellIntegration::CreateDesktopShortcut(
441    const ShortcutInfo& shortcut_info, const std::string& shortcut_template) {
442  // TODO(phajdan.jr): Report errors from this function, possibly as infobars.
443
444  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
445
446  FilePath shortcut_filename = GetDesktopShortcutFilename(shortcut_info.url);
447  if (shortcut_filename.empty())
448    return;
449
450  std::string icon_name = CreateShortcutIcon(shortcut_info, shortcut_filename);
451
452  std::string app_name =
453      web_app::GenerateApplicationNameFromInfo(shortcut_info);
454  std::string contents = GetDesktopFileContents(
455      shortcut_template,
456      app_name,
457      shortcut_info.url,
458      shortcut_info.extension_id,
459      shortcut_info.title,
460      icon_name);
461
462  if (shortcut_info.create_on_desktop)
463    CreateShortcutOnDesktop(shortcut_filename, contents);
464
465  if (shortcut_info.create_in_applications_menu)
466    CreateShortcutInApplicationsMenu(shortcut_filename, contents);
467}
468