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