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