blob: a6ab7e54b893833bca62910235f87cca82dc6de8 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/shell_integration_linux.h"
#include <fcntl.h>
#include <glib.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/eintr_wrapper.h"
#include "base/environment.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/i18n/file_util_icu.h"
#include "base/message_loop.h"
#include "base/path_service.h"
#include "base/process_util.h"
#include "base/scoped_temp_dir.h"
#include "base/string_number_conversions.h"
#include "base/string_tokenizer.h"
#include "base/threading/thread.h"
#include "base/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "content/public/browser/browser_thread.h"
#include "googleurl/src/gurl.h"
#include "ui/gfx/codec/png_codec.h"
using content::BrowserThread;
namespace {
// Helper to launch xdg scripts. We don't want them to ask any questions on the
// terminal etc. The function returns true if the utility launches and exits
// cleanly, in which case |exit_code| returns the utility's exit code.
bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) {
// xdg-settings internally runs xdg-mime, which uses mv to move newly-created
// files on top of originals after making changes to them. In the event that
// the original files are owned by another user (e.g. root, which can happen
// if they are updated within sudo), mv will prompt the user to confirm if
// standard input is a terminal (otherwise it just does it). So make sure it's
// not, to avoid locking everything up waiting for mv.
*exit_code = EXIT_FAILURE;
int devnull = open("/dev/null", O_RDONLY);
if (devnull < 0)
return false;
base::FileHandleMappingVector no_stdin;
no_stdin.push_back(std::make_pair(devnull, STDIN_FILENO));
base::ProcessHandle handle;
base::LaunchOptions options;
options.fds_to_remap = &no_stdin;
if (!base::LaunchProcess(argv, options, &handle)) {
close(devnull);
return false;
}
close(devnull);
return base::WaitForExitCode(handle, exit_code);
}
std::string CreateShortcutIcon(
const ShellIntegration::ShortcutInfo& shortcut_info,
const FilePath& shortcut_filename) {
if (shortcut_info.favicon.IsEmpty())
return std::string();
// TODO(phajdan.jr): Report errors from this function, possibly as infobars.
ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir())
return std::string();
FilePath temp_file_path = temp_dir.path().Append(
shortcut_filename.ReplaceExtension("png"));
std::vector<unsigned char> png_data;
const SkBitmap* bitmap = shortcut_info.favicon.ToSkBitmap();
gfx::PNGCodec::EncodeBGRASkBitmap(*bitmap, false, &png_data);
int bytes_written = file_util::WriteFile(temp_file_path,
reinterpret_cast<char*>(png_data.data()), png_data.size());
if (bytes_written != static_cast<int>(png_data.size()))
return std::string();
std::vector<std::string> argv;
argv.push_back("xdg-icon-resource");
argv.push_back("install");
// Always install in user mode, even if someone runs the browser as root
// (people do that).
argv.push_back("--mode");
argv.push_back("user");
argv.push_back("--size");
argv.push_back(base::IntToString(bitmap->width()));
argv.push_back(temp_file_path.value());
std::string icon_name = temp_file_path.BaseName().RemoveExtension().value();
argv.push_back(icon_name);
int exit_code;
LaunchXdgUtility(argv, &exit_code);
return icon_name;
}
bool CreateShortcutOnDesktop(const FilePath& shortcut_filename,
const std::string& contents) {
// Make sure that we will later call openat in a secure way.
DCHECK_EQ(shortcut_filename.BaseName().value(), shortcut_filename.value());
FilePath desktop_path;
if (!PathService::Get(chrome::DIR_USER_DESKTOP, &desktop_path))
return false;
int desktop_fd = open(desktop_path.value().c_str(), O_RDONLY | O_DIRECTORY);
if (desktop_fd < 0)
return false;
int fd = openat(desktop_fd, shortcut_filename.value().c_str(),
O_CREAT | O_EXCL | O_WRONLY,
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (fd < 0) {
if (HANDLE_EINTR(close(desktop_fd)) < 0)
PLOG(ERROR) << "close";
return false;
}
ssize_t bytes_written = file_util::WriteFileDescriptor(fd, contents.data(),
contents.length());
if (HANDLE_EINTR(close(fd)) < 0)
PLOG(ERROR) << "close";
if (bytes_written != static_cast<ssize_t>(contents.length())) {
// Delete the file. No shortuct is better than corrupted one. Use unlinkat
// to make sure we're deleting the file in the directory we think we are.
// Even if an attacker manager to put something other at
// |shortcut_filename| we'll just undo his action.
unlinkat(desktop_fd, shortcut_filename.value().c_str(), 0);
}
if (HANDLE_EINTR(close(desktop_fd)) < 0)
PLOG(ERROR) << "close";
return true;
}
bool CreateShortcutInApplicationsMenu(const FilePath& shortcut_filename,
const std::string& contents) {
ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir())
return false;
FilePath temp_file_path = temp_dir.path().Append(shortcut_filename);
int bytes_written = file_util::WriteFile(temp_file_path, contents.data(),
contents.length());
if (bytes_written != static_cast<int>(contents.length()))
return false;
std::vector<std::string> argv;
argv.push_back("xdg-desktop-menu");
argv.push_back("install");
// Always install in user mode, even if someone runs the browser as root
// (people do that).
argv.push_back("--mode");
argv.push_back("user");
argv.push_back(temp_file_path.value());
int exit_code;
LaunchXdgUtility(argv, &exit_code);
return exit_code == 0;
}
// Quote a string such that it appears as one verbatim argument for the Exec
// key in a desktop file.
std::string QuoteArgForDesktopFileExec(const std::string& arg) {
// http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
// Quoting is only necessary if the argument has a reserved character.
if (arg.find_first_of(" \t\n\"'\\><~|&;$*?#()`") == std::string::npos)
return arg; // No quoting necessary.
std::string quoted = "\"";
for (size_t i = 0; i < arg.size(); ++i) {
// Note that the set of backslashed characters is smaller than the
// set of reserved characters.
switch (arg[i]) {
case '"':
case '`':
case '$':
case '\\':
quoted += '\\';
break;
}
quoted += arg[i];
}
quoted += '"';
return quoted;
}
// Remove keys from the [Desktop Entry] that would be wrong if copied verbatim
// into the new .desktop file.
const char* kDesktopKeysToDelete[] = {
"GenericName",
"Comment",
"MimeType",
"X-Ayatana-Desktop-Shortcuts",
"StartupWMClass",
NULL
};
const char kDesktopEntry[] = "Desktop Entry";
const char kXdgOpenShebang[] = "#!/usr/bin/env xdg-open";
const char kXdgSettings[] = "xdg-settings";
const char kXdgSettingsDefaultBrowser[] = "default-web-browser";
const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler";
} // namespace
namespace {
// Utility function to get the path to the version of a script shipped with
// Chrome. |script| gives the name of the script. |chrome_version| returns the
// path to the Chrome version of the script, and the return value of the
// function is true if the function is successful and the Chrome version is
// not the script found on the PATH.
bool GetChromeVersionOfScript(const std::string& script,
std::string* chrome_version) {
// Get the path to the Chrome version.
FilePath chrome_dir;
if (!PathService::Get(base::DIR_EXE, &chrome_dir))
return false;
FilePath chrome_version_path = chrome_dir.Append(script);
*chrome_version = chrome_version_path.value();
// Check if this is different to the one on path.
std::vector<std::string> argv;
argv.push_back("which");
argv.push_back(script);
std::string path_version;
if (base::GetAppOutput(CommandLine(argv), &path_version)) {
// Remove trailing newline
path_version.erase(path_version.length() - 1, 1);
FilePath path_version_path(path_version);
return (chrome_version_path != path_version_path);
}
return false;
}
// Value returned by xdg-settings if it can't understand our request.
const int EXIT_XDG_SETTINGS_SYNTAX_ERROR = 1;
// We delegate the difficulty of setting the default browser and default url
// scheme handler in Linux desktop environments to an xdg utility, xdg-settings.
// When calling this script we first try to use the script on PATH. If that
// fails we then try to use the script that we have included. This gives
// scripts on the system priority over ours, as distribution vendors may have
// tweaked the script, but still allows our copy to be used if the script on the
// system fails, as the system copy may be missing capabilities of the Chrome
// copy.
// If |protocol| is empty this function sets Chrome as the default browser,
// otherwise it sets Chrome as the default handler application for |protocol|.
bool SetDefaultWebClient(const std::string& protocol) {
#if defined(OS_CHROMEOS)
return true;
#else
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
scoped_ptr<base::Environment> env(base::Environment::Create());
std::vector<std::string> argv;
argv.push_back(kXdgSettings);
argv.push_back("set");
if (protocol.empty()) {
argv.push_back(kXdgSettingsDefaultBrowser);
} else {
argv.push_back(kXdgSettingsDefaultSchemeHandler);
argv.push_back(protocol);
}
argv.push_back(ShellIntegrationLinux::GetDesktopName(env.get()));
int exit_code;
bool ran_ok = LaunchXdgUtility(argv, &exit_code);
if (ran_ok && exit_code == EXIT_XDG_SETTINGS_SYNTAX_ERROR) {
if (GetChromeVersionOfScript(kXdgSettings, &argv[0])) {
ran_ok = LaunchXdgUtility(argv, &exit_code);
}
}
return ran_ok && exit_code == EXIT_SUCCESS;
#endif
}
// If |protocol| is empty this function checks if Chrome is the default browser,
// otherwise it checks if Chrome is the default handler application for
// |protocol|.
ShellIntegration::DefaultWebClientState GetIsDefaultWebClient(
const std::string& protocol) {
#if defined(OS_CHROMEOS)
return ShellIntegration::IS_DEFAULT_WEB_CLIENT;
#else
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
scoped_ptr<base::Environment> env(base::Environment::Create());
std::vector<std::string> argv;
argv.push_back(kXdgSettings);
argv.push_back("check");
if (protocol.empty()) {
argv.push_back(kXdgSettingsDefaultBrowser);
} else {
argv.push_back(kXdgSettingsDefaultSchemeHandler);
argv.push_back(protocol);
}
argv.push_back(ShellIntegrationLinux::GetDesktopName(env.get()));
std::string reply;
int success_code;
bool ran_ok = base::GetAppOutputWithExitCode(CommandLine(argv), &reply,
&success_code);
if (ran_ok && success_code == EXIT_XDG_SETTINGS_SYNTAX_ERROR) {
if (GetChromeVersionOfScript(kXdgSettings, &argv[0])) {
ran_ok = base::GetAppOutputWithExitCode(CommandLine(argv), &reply,
&success_code);
}
}
if (!ran_ok || success_code != EXIT_SUCCESS) {
// xdg-settings failed: we can't determine or set the default browser.
return ShellIntegration::UNKNOWN_DEFAULT_WEB_CLIENT;
}
// Allow any reply that starts with "yes".
return (reply.find("yes") == 0) ? ShellIntegration::IS_DEFAULT_WEB_CLIENT :
ShellIntegration::NOT_DEFAULT_WEB_CLIENT;
#endif
}
} // namespace
// static
bool ShellIntegration::CanSetAsDefaultBrowser() {
return true;
}
// static
bool ShellIntegration::SetAsDefaultBrowser() {
return SetDefaultWebClient("");
}
// static
bool ShellIntegration::SetAsDefaultProtocolClient(const std::string& protocol) {
return SetDefaultWebClient(protocol);
}
// static
ShellIntegration::DefaultWebClientState ShellIntegration::IsDefaultBrowser() {
return GetIsDefaultWebClient("");
}
// static
ShellIntegration::DefaultWebClientState
ShellIntegration::IsDefaultProtocolClient(const std::string& protocol) {
return GetIsDefaultWebClient(protocol);
}
// static
bool ShellIntegration::IsFirefoxDefaultBrowser() {
std::vector<std::string> argv;
argv.push_back(kXdgSettings);
argv.push_back("get");
argv.push_back(kXdgSettingsDefaultBrowser);
std::string browser;
// We don't care about the return value here.
base::GetAppOutput(CommandLine(argv), &browser);
return browser.find("irefox") != std::string::npos;
}
namespace ShellIntegrationLinux {
std::string GetDesktopName(base::Environment* env) {
#if defined(GOOGLE_CHROME_BUILD)
return "google-chrome.desktop";
#else // CHROMIUM_BUILD
// Allow $CHROME_DESKTOP to override the built-in value, so that development
// versions can set themselves as the default without interfering with
// non-official, packaged versions using the built-in value.
std::string name;
if (env->GetVar("CHROME_DESKTOP", &name) && !name.empty())
return name;
return "chromium-browser.desktop";
#endif
}
bool GetDesktopShortcutTemplate(base::Environment* env,
std::string* output) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
std::vector<FilePath> search_paths;
std::string xdg_data_home;
if (env->GetVar("XDG_DATA_HOME", &xdg_data_home) &&
!xdg_data_home.empty()) {
search_paths.push_back(FilePath(xdg_data_home));
}
std::string xdg_data_dirs;
if (env->GetVar("XDG_DATA_DIRS", &xdg_data_dirs) &&
!xdg_data_dirs.empty()) {
StringTokenizer tokenizer(xdg_data_dirs, ":");
while (tokenizer.GetNext()) {
FilePath data_dir(tokenizer.token());
search_paths.push_back(data_dir);
search_paths.push_back(data_dir.Append("applications"));
}
}
// Add some fallback paths for systems which don't have XDG_DATA_DIRS or have
// it incomplete.
search_paths.push_back(FilePath("/usr/share/applications"));
search_paths.push_back(FilePath("/usr/local/share/applications"));
std::string template_filename(GetDesktopName(env));
for (std::vector<FilePath>::const_iterator i = search_paths.begin();
i != search_paths.end(); ++i) {
FilePath path = (*i).Append(template_filename);
VLOG(1) << "Looking for desktop file template in " << path.value();
if (file_util::PathExists(path)) {
VLOG(1) << "Found desktop file template at " << path.value();
return file_util::ReadFileToString(path, output);
}
}
LOG(ERROR) << "Could not find desktop file template.";
return false;
}
FilePath GetDesktopShortcutFilename(const GURL& url) {
// Use a prefix, because xdg-desktop-menu requires it.
std::string filename =
std::string(chrome::kBrowserProcessExecutableName) + "-" + url.spec();
file_util::ReplaceIllegalCharactersInPath(&filename, '_');
FilePath desktop_path;
if (!PathService::Get(chrome::DIR_USER_DESKTOP, &desktop_path))
return FilePath();
FilePath filepath = desktop_path.Append(filename);
FilePath alternative_filepath(filepath.value() + ".desktop");
for (size_t i = 1; i < 100; ++i) {
if (file_util::PathExists(FilePath(alternative_filepath))) {
alternative_filepath = FilePath(
filepath.value() + "_" + base::IntToString(i) + ".desktop");
} else {
return FilePath(alternative_filepath).BaseName();
}
}
return FilePath();
}
std::string GetDesktopFileContents(
const std::string& template_contents,
const std::string& app_name,
const GURL& url,
const std::string& extension_id,
const bool is_platform_app,
const FilePath& extension_path,
const string16& title,
const std::string& icon_name) {
if (template_contents.empty())
return std::string(kXdgOpenShebang) + "\n";
// See http://standards.freedesktop.org/desktop-entry-spec/latest/
// http://developer.gnome.org/glib/unstable/glib-Key-value-file-parser.html
GKeyFile* key_file = g_key_file_new();
GError* err = NULL;
// Loading the data will strip translations and comments from the desktop
// file (which we want to do!)
if (!g_key_file_load_from_data(
key_file,
template_contents.c_str(),
template_contents.size(),
G_KEY_FILE_NONE,
&err)) {
NOTREACHED() << "Unable to read desktop file template:" << err->message;
g_error_free(err);
return std::string(kXdgOpenShebang) + "\n";
}
// Remove all sections except for the Desktop Entry
gsize length = 0;
gchar** groups = g_key_file_get_groups(key_file, &length);
for (gsize i = 0; i < length; ++i) {
if (strcmp(groups[i], kDesktopEntry) != 0) {
g_key_file_remove_group(key_file, groups[i], NULL);
}
}
g_strfreev(groups);
// Remove keys that we won't need.
for (const char** current_key = kDesktopKeysToDelete; *current_key;
++current_key) {
g_key_file_remove_key(key_file, kDesktopEntry, *current_key, NULL);
}
// Set the "Name" key.
std::string final_title = UTF16ToUTF8(title);
// Make sure no endline characters can slip in and possibly introduce
// additional lines (like Exec, which makes it a security risk). Also
// use the URL as a default when the title is empty.
if (final_title.empty() ||
final_title.find("\n") != std::string::npos ||
final_title.find("\r") != std::string::npos) {
final_title = url.spec();
}
g_key_file_set_string(key_file, kDesktopEntry, "Name", final_title.c_str());
// Set the "Exec" key.
char* exec_c_string = g_key_file_get_string(key_file, kDesktopEntry, "Exec",
NULL);
if (exec_c_string) {
std::string exec_string(exec_c_string);
g_free(exec_c_string);
StringTokenizer exec_tokenizer(exec_string, " ");
std::string final_path;
while (exec_tokenizer.GetNext() && exec_tokenizer.token() != "%U") {
if (!final_path.empty())
final_path += " ";
final_path += exec_tokenizer.token();
}
CommandLine cmd_line(CommandLine::NO_PROGRAM);
cmd_line = ShellIntegration::CommandLineArgsForLauncher(
url, extension_id, is_platform_app);
const CommandLine::SwitchMap& switch_map = cmd_line.GetSwitches();
for (CommandLine::SwitchMap::const_iterator i = switch_map.begin();
i != switch_map.end(); ++i) {
if (i->second.empty()) {
final_path += " --" + i->first;
} else {
final_path += " " + QuoteArgForDesktopFileExec("--" + i->first +
"=" + i->second);
}
}
g_key_file_set_string(key_file, kDesktopEntry, "Exec", final_path.c_str());
}
// Set the "Icon" key.
if (!icon_name.empty())
g_key_file_set_string(key_file, kDesktopEntry, "Icon", icon_name.c_str());
#if defined(TOOLKIT_GTK)
std::string wmclass = web_app::GetWMClassFromAppName(app_name);
g_key_file_set_string(key_file, kDesktopEntry, "StartupWMClass",
wmclass.c_str());
#endif
// Although not required by the spec, Nautilus on Ubuntu Karmic creates its
// launchers with an xdg-open shebang. Follow that convention.
std::string output_buffer = kXdgOpenShebang;
length = 0;
gchar* data_dump = g_key_file_to_data(key_file, &length, NULL);
if (data_dump) {
output_buffer += data_dump;
g_free(data_dump);
}
g_key_file_free(key_file);
return output_buffer;
}
bool CreateDesktopShortcut(
const ShellIntegration::ShortcutInfo& shortcut_info,
const std::string& shortcut_template) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
FilePath shortcut_filename =
ShellIntegrationLinux::GetDesktopShortcutFilename(shortcut_info.url);
if (shortcut_filename.empty())
return false;
std::string icon_name =CreateShortcutIcon(shortcut_info, shortcut_filename);
std::string app_name =
web_app::GenerateApplicationNameFromInfo(shortcut_info);
std::string contents = ShellIntegrationLinux::GetDesktopFileContents(
shortcut_template,
app_name,
shortcut_info.url,
shortcut_info.extension_id,
shortcut_info.is_platform_app,
shortcut_info.extension_path,
shortcut_info.title,
icon_name);
bool success = true;
if (shortcut_info.create_on_desktop)
success = CreateShortcutOnDesktop(shortcut_filename, contents);
if (shortcut_info.create_in_applications_menu)
success = CreateShortcutInApplicationsMenu(shortcut_filename, contents) &&
success;
return success;
}
} // namespace ShellIntegrationLinux