|  | // 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. | 
|  |  | 
|  | #include "chrome/browser/browser_switcher/alternative_browser_driver.h" | 
|  |  | 
|  | #include <stdlib.h> | 
|  |  | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/logging.h" | 
|  | #include "base/process/launch.h" | 
|  | #include "base/ranges/algorithm.h" | 
|  | #include "base/strings/string_piece.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/1124758): 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; | 
|  | re2::StringPiece submatch[3] = {nullptr}; | 
|  | size_t start = 0; | 
|  | bool matched = false; | 
|  | while (re->Match(*arg, start, arg->size(), re2::RE2::Anchor::UNANCHORED, | 
|  | submatch, std::size(submatch))) { | 
|  | out.append(*arg, 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() - arg->data(); | 
|  | matched = true; | 
|  | } | 
|  | if (!matched) | 
|  | return; | 
|  | out.append(arg->data() + start, arg->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(base::StringPiece 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 |