Home | History | Annotate | Download | only in browser
      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 
     40 namespace {
     41 
     42 // Helper to launch xdg scripts. We don't want them to ask any questions on the
     43 // terminal etc.
     44 bool 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 
     69 std::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 
    110 void 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 
    151 void 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.
    181 std::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.)
    211 std::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
    228 std::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
    248 bool 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
    260 ShellIntegration::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
    282 bool 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
    295 bool 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
    339 FilePath 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
    364 std::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
    440 void 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