blob: f4dd1571c081890ba89248d4753da34eb172e3f5 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/browser_switcher/alternative_browser_driver.h"
#include <stdlib.h>
#include <string_view>
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/process/launch.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "build/build_config.h"
#include "chrome/browser/browser_switcher/browser_switcher_prefs.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"
namespace browser_switcher {
namespace {
using LaunchCallback = AlternativeBrowserDriver::LaunchCallback;
const char kUrlVarName[] = "${url}";
// TODO(crbug.com/40147515): add ${edge} on macOS/Linux once it's released on
// those platforms.
#if BUILDFLAG(IS_MAC)
const char kChromeExecutableName[] = "Google Chrome";
const char kFirefoxExecutableName[] = "Firefox";
const char kOperaExecutableName[] = "Opera";
const char kSafariExecutableName[] = "Safari";
const char kEdgeExecutableName[] = "Microsoft Edge";
#else
const char kChromeExecutableName[] = "google-chrome";
const char kFirefoxExecutableName[] = "firefox";
const char kOperaExecutableName[] = "opera";
#endif
const char kChromeVarName[] = "${chrome}";
const char kFirefoxVarName[] = "${firefox}";
const char kOperaVarName[] = "${opera}";
#if BUILDFLAG(IS_MAC)
const char kSafariVarName[] = "${safari}";
const char kEdgeVarName[] = "${edge}";
#endif
struct BrowserVarMapping {
const char* var_name;
const char* executable_name;
const char* browser_name;
BrowserType browser_type;
};
const BrowserVarMapping kBrowserVarMappings[] = {
{kChromeVarName, kChromeExecutableName, "", BrowserType::kChrome},
{kFirefoxVarName, kFirefoxExecutableName, "Mozilla Firefox",
BrowserType::kFirefox},
{kOperaVarName, kOperaExecutableName, "Opera", BrowserType::kOpera},
#if BUILDFLAG(IS_MAC)
{kSafariVarName, kSafariExecutableName, "Safari", BrowserType::kSafari},
{kEdgeVarName, kEdgeExecutableName, "Microsoft Edge", BrowserType::kEdge},
#endif
};
bool ExpandUrlVarName(std::string* arg, const GURL& url) {
size_t url_index = arg->find(kUrlVarName);
if (url_index == std::string::npos)
return false;
arg->replace(url_index, strlen(kUrlVarName), url.spec());
return true;
}
void ExpandTilde(std::string* arg) {
if (base::StartsWith(*arg, "~", base::CompareCase::SENSITIVE))
arg->replace(0, 1, getenv("HOME"));
}
void ExpandEnvironmentVariables(std::string* arg) {
static re2::LazyRE2 re = {
"\\$\\{([a-zA-Z_][a-zA-Z_0-9]*)\\}|\\$([a-zA-Z_][a-zA-Z_0-9]*)"};
std::string out;
std::string_view view(*arg);
std::string_view submatch[3] = {};
size_t start = 0;
bool matched = false;
while (re->Match(view, start, arg->size(), re2::RE2::Anchor::UNANCHORED,
submatch, std::size(submatch))) {
out.append(view, start, submatch[0].data() - (arg->data() + start));
if (submatch[0] == kUrlVarName) {
// Don't treat '${url}' as an environment variable, leave it as is.
out.append(kUrlVarName);
} else {
std::string var_name((submatch[1].empty() ? submatch[2] : submatch[1]));
const char* var_value = getenv(var_name.c_str());
if (var_value != nullptr)
out.append(var_value);
}
start = submatch[0].end() - view.begin();
matched = true;
}
if (!matched)
return;
out.append(view.data() + start, view.size() - start);
std::swap(out, *arg);
}
#if BUILDFLAG(IS_MAC)
bool ContainsUrlVarName(const std::vector<std::string>& tokens) {
return base::ranges::any_of(tokens, [](const std::string& token) {
return token.find(kUrlVarName) != std::string::npos;
});
}
#endif // BUILDFLAG(IS_MAC)
void AppendCommandLineArguments(base::CommandLine* cmd_line,
const std::vector<std::string>& raw_args,
const GURL& url,
bool always_append_url) {
bool contains_url = false;
for (const auto& arg : raw_args) {
std::string expanded_arg = arg;
ExpandTilde(&expanded_arg);
ExpandEnvironmentVariables(&expanded_arg);
if (ExpandUrlVarName(&expanded_arg, url))
contains_url = true;
cmd_line->AppendArg(expanded_arg);
}
if (always_append_url && !contains_url)
cmd_line->AppendArg(url.spec());
}
const BrowserVarMapping* FindBrowserMapping(std::string_view path) {
#if BUILDFLAG(IS_MAC)
// Unlike most POSIX platforms, MacOS always has another browser than Chrome,
// so admins don't have to explicitly configure one.
if (path.empty())
path = kSafariVarName;
#endif
for (const auto& mapping : kBrowserVarMappings) {
if (!path.compare(mapping.var_name))
return &mapping;
}
return nullptr;
}
void ExpandPresetBrowsers(std::string* str) {
const auto* mapping = FindBrowserMapping(*str);
if (mapping)
*str = mapping->executable_name;
}
base::CommandLine CreateCommandLine(const GURL& url,
const std::string& original_path,
const std::vector<std::string>& params) {
std::string path = original_path;
ExpandPresetBrowsers(&path);
ExpandTilde(&path);
ExpandEnvironmentVariables(&path);
#if BUILDFLAG(IS_MAC)
// On MacOS, if the path doesn't start with a '/', it's probably not an
// executable path. It is probably a name for an application, e.g. "Safari" or
// "Google Chrome". Those can be launched using the `open(1)' command.
//
// It may use the following syntax (first syntax):
// open -a <browser_path> <url> [--args <browser_params...>]
//
// Or, if |browser_params| contains "${url}" (second syntax):
// open -a <browser_path> --args <browser_params...>
//
// Safari only supports the first syntax.
if (!path.empty() && path[0] != '/') {
base::CommandLine cmd_line(std::vector<std::string>{"open"});
cmd_line.AppendArg("-a");
cmd_line.AppendArg(path);
if (!ContainsUrlVarName(params)) {
// First syntax.
cmd_line.AppendArg(url.spec());
}
if (!params.empty()) {
// First or second syntax, depending on what is in |browser_params_|.
cmd_line.AppendArg("--args");
AppendCommandLineArguments(&cmd_line, params, url,
/* always_append_url */ false);
}
return cmd_line;
}
#endif
base::CommandLine cmd_line(std::vector<std::string>{path});
AppendCommandLineArguments(&cmd_line, params, url,
/* always_append_url */ true);
return cmd_line;
}
void TryLaunchBlocking(GURL url,
std::string path,
std::vector<std::string> params,
LaunchCallback cb) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
CHECK(url.SchemeIsHTTPOrHTTPS() || url.SchemeIsFile());
auto cmd_line = CreateCommandLine(url, path, params);
base::LaunchOptions options;
// Don't close the alternative browser when Chrome exits.
options.new_process_group = true;
const bool success = base::LaunchProcess(cmd_line, options).IsValid();
if (!success)
LOG(ERROR) << "Could not start the alternative browser!";
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
[](bool success, LaunchCallback cb) { std::move(cb).Run(success); },
success, std::move(cb)));
}
} // namespace
AlternativeBrowserDriver::~AlternativeBrowserDriver() = default;
AlternativeBrowserDriverImpl::AlternativeBrowserDriverImpl(
const BrowserSwitcherPrefs* prefs)
: prefs_(prefs) {}
AlternativeBrowserDriverImpl::~AlternativeBrowserDriverImpl() = default;
void AlternativeBrowserDriverImpl::TryLaunch(const GURL& url,
LaunchCallback cb) {
#if !BUILDFLAG(IS_MAC)
if (prefs_->GetAlternativeBrowserPath().empty()) {
LOG(ERROR) << "Alternative browser not configured. "
<< "Aborting browser switch.";
std::move(cb).Run(false);
return;
}
#endif
VLOG(2) << "Launching alternative browser...";
VLOG(2) << " path = " << prefs_->GetAlternativeBrowserPath();
VLOG(2) << " url = " << url.spec();
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(&TryLaunchBlocking, url,
prefs_->GetAlternativeBrowserPath(),
prefs_->GetAlternativeBrowserParameters(), std::move(cb)));
}
std::string AlternativeBrowserDriverImpl::GetBrowserName() const {
std::string path = prefs_->GetAlternativeBrowserPath();
const auto* mapping = FindBrowserMapping(path);
return mapping ? mapping->browser_name : std::string();
}
BrowserType AlternativeBrowserDriverImpl::GetBrowserType() const {
std::string path = prefs_->GetAlternativeBrowserPath();
const auto* mapping = FindBrowserMapping(path);
return mapping ? mapping->browser_type : BrowserType::kUnknown;
}
base::CommandLine AlternativeBrowserDriverImpl::CreateCommandLine(
const GURL& url) {
return browser_switcher::CreateCommandLine(
url, prefs_->GetAlternativeBrowserPath(),
prefs_->GetAlternativeBrowserParameters());
}
} // namespace browser_switcher