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