blob: 5063bd770c933ff917046a0bc2d395be2b4fda8a [file] [log] [blame]
// Copyright 2018 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/browser_switcher/alternative_browser_driver.h"
#include <windows.h>
#include <ddeml.h>
#include <shellapi.h>
#include <shlobj.h>
#include <wininet.h>
#include "base/files/file_path.h"
#include "base/process/launch.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 "base/win/registry.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 "url/gurl.h"
namespace browser_switcher {
namespace {
using LaunchCallback = AlternativeBrowserDriver::LaunchCallback;
const wchar_t kUrlVarName[] = L"${url}";
const wchar_t kIExploreKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\IEXPLORE.EXE";
const wchar_t kFirefoxKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe";
// Opera does not register itself here for now but it's no harm to keep this.
const wchar_t kOperaKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\opera.exe";
const wchar_t kSafariKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\safari.exe";
const wchar_t kChromeKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe";
const wchar_t kEdgeKey[] =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\msedge.exe";
const wchar_t kIExploreDdeHost[] = L"IExplore";
const wchar_t kChromeVarName[] = L"${chrome}";
const wchar_t kIEVarName[] = L"${ie}";
const wchar_t kFirefoxVarName[] = L"${firefox}";
const wchar_t kOperaVarName[] = L"${opera}";
const wchar_t kSafariVarName[] = L"${safari}";
const wchar_t kEdgeVarName[] = L"${edge}";
// Case-insensitive, typical filenames for popular browsers' executables.
const wchar_t kChromeTypicalExecutable[] = L"chrome.exe";
const wchar_t kIETypicalExecutable[] = L"iexplore.exe";
const wchar_t kFirefoxTypicalExecutable[] = L"firefox.exe";
const wchar_t kOperaTypicalExecutable[] = L"launcher.exe";
const wchar_t kEdgeTypicalExecutable[] = L"msedge.exe";
struct BrowserVarMapping {
const wchar_t* var_name;
const wchar_t* registry_key;
const wchar_t* typical_executable;
const char* browser_name;
BrowserType browser_type;
};
const BrowserVarMapping kBrowserVarMappings[] = {
{kChromeVarName, kChromeKey, kChromeTypicalExecutable, "",
BrowserType::kChrome},
{kIEVarName, kIExploreKey, kIETypicalExecutable, "Internet Explorer",
BrowserType::kIE},
{kFirefoxVarName, kFirefoxKey, kFirefoxTypicalExecutable, "Mozilla Firefox",
BrowserType::kFirefox},
{kOperaVarName, kOperaKey, kOperaTypicalExecutable, "Opera",
BrowserType::kOpera},
{kSafariVarName, kSafariKey, L"", "Safari", BrowserType::kSafari},
{kEdgeVarName, kEdgeKey, kEdgeTypicalExecutable, "Microsoft Edge",
BrowserType::kEdge},
};
// DDE Callback function which is not used in our case at all.
HDDEDATA CALLBACK DdeCallback(UINT type,
UINT format,
HCONV handle,
HSZ string1,
HSZ string2,
HDDEDATA data,
ULONG_PTR data1,
ULONG_PTR data2) {
return NULL;
}
void PercentEncodeCommas(std::wstring* url) {
size_t pos = url->find(L",");
while (pos != std::wstring::npos) {
url->replace(pos, 1, L"%2C");
pos = url->find(L",", pos);
}
}
void PercentUnencodeQuotes(std::wstring* url) {
base::ReplaceSubstringsAfterOffset(url, 0, L"%27", L"'");
}
std::wstring GetBrowserLocation(const wchar_t* regkey_name) {
DCHECK(regkey_name);
base::win::RegKey key;
if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE, regkey_name, KEY_READ) &&
ERROR_SUCCESS != key.Open(HKEY_CURRENT_USER, regkey_name, KEY_READ)) {
LOG(ERROR) << "Could not open registry key " << regkey_name
<< "! Error Code:" << GetLastError();
return std::wstring();
}
std::wstring location;
if (ERROR_SUCCESS != key.ReadValue(NULL, &location))
return std::wstring();
return location;
}
const BrowserVarMapping* FindBrowserMapping(base::WStringPiece path,
bool compare_typical_executable) {
// If |compare_typical_executable| is true: also look at executable filenames,
// to reduce false-negatives when the path is specified explicitly by the
// admin.
if (path.empty())
path = kIEVarName;
for (const auto& mapping : kBrowserVarMappings) {
if (!path.compare(mapping.var_name) ||
(compare_typical_executable && *mapping.typical_executable &&
base::EndsWith(path, mapping.typical_executable,
base::CompareCase::INSENSITIVE_ASCII))) {
return &mapping;
}
}
return nullptr;
}
void ExpandPresetBrowsers(std::wstring* str) {
const auto* mapping = FindBrowserMapping(*str, false);
if (mapping)
*str = GetBrowserLocation(mapping->registry_key);
}
bool ExpandUrlVarName(std::wstring* arg, const std::wstring& url_spec) {
size_t url_index = arg->find(kUrlVarName);
if (url_index == std::wstring::npos)
return false;
arg->replace(url_index, wcslen(kUrlVarName), url_spec);
return true;
}
void ExpandEnvironmentVariables(std::wstring* arg) {
DWORD expanded_size = 0;
expanded_size = ::ExpandEnvironmentStrings(arg->c_str(), NULL, expanded_size);
if (expanded_size == 0)
return;
// The expected buffer length as defined in MSDN is chars + null + 1.
std::unique_ptr<wchar_t[]> out(new wchar_t[expanded_size + 2]);
expanded_size =
::ExpandEnvironmentStrings(arg->c_str(), out.get(), expanded_size);
if (expanded_size != 0)
*arg = out.get();
}
void AppendCommandLineArguments(base::CommandLine* cmd_line,
const std::vector<std::string>& raw_args,
const GURL& url) {
std::wstring url_spec = base::UTF8ToWide(url.spec());
// IE has some quirks with quote characters. Send them verbatim instead
// of percent-encoding them.
PercentUnencodeQuotes(&url_spec);
std::vector<std::wstring> command_line;
bool contains_url = false;
for (const auto& arg : raw_args) {
std::wstring expanded_arg = base::UTF8ToWide(arg);
ExpandEnvironmentVariables(&expanded_arg);
if (ExpandUrlVarName(&expanded_arg, url_spec))
contains_url = true;
cmd_line->AppendArgNative(expanded_arg);
}
if (!contains_url)
cmd_line->AppendArgNative(url_spec);
}
bool IsInternetExplorer(base::StringPiece path) {
// We don't treat IExplore.exe as Internet Explorer here. This way, admins can
// set |AlternativeBrowserPath| to IExplore.exe to disable DDE, if it's
// causing issues or slowness.
return path.empty() || base::EqualsASCII(base::as_u16cstr(kIEVarName), path);
}
bool TryLaunchWithDde(const GURL& url, const std::string& path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
if (!IsInternetExplorer(path))
return false;
DWORD dde_instance = 0;
UINT dml_error =
DdeInitialize(&dde_instance, DdeCallback, CBF_FAIL_ALLSVRXACTIONS, 0);
if (dml_error != DMLERR_NO_ERROR) {
VLOG(1) << "DdeInitialize() failed: " << dml_error;
return false;
}
bool success = false;
HCONV openurl_service_instance;
HCONV activate_service_instance;
{
HSZ service =
DdeCreateStringHandle(dde_instance, kIExploreDdeHost, CP_WINUNICODE);
HSZ openurl_topic =
DdeCreateStringHandle(dde_instance, L"WWW_OpenURL", CP_WINUNICODE);
HSZ activate_topic =
DdeCreateStringHandle(dde_instance, L"WWW_Activate", CP_WINUNICODE);
openurl_service_instance =
DdeConnect(dde_instance, service, openurl_topic, NULL);
activate_service_instance =
DdeConnect(dde_instance, service, activate_topic, NULL);
DdeFreeStringHandle(dde_instance, service);
DdeFreeStringHandle(dde_instance, openurl_topic);
DdeFreeStringHandle(dde_instance, activate_topic);
}
if (openurl_service_instance) {
// Percent-encode commas and spaces because those mean something else
// for the WWW_OpenURL verb and the url is trimmed on the first one.
// Spaces are already encoded by GURL.
std::wstring encoded_url(base::UTF8ToWide(url.spec()));
PercentUnencodeQuotes(&encoded_url);
PercentEncodeCommas(&encoded_url);
success =
DdeClientTransaction(
reinterpret_cast<LPBYTE>(const_cast<wchar_t*>(encoded_url.data())),
encoded_url.size() * sizeof(wchar_t), openurl_service_instance, 0,
0, XTYP_EXECUTE, TIMEOUT_ASYNC, NULL) != 0;
DdeDisconnect(openurl_service_instance);
if (activate_service_instance) {
if (success) {
// Bring window to the front.
wchar_t cmd[] = L"0xFFFFFFFF,0x0";
DdeClientTransaction(reinterpret_cast<LPBYTE>(cmd), sizeof(cmd),
activate_service_instance, 0, 0, XTYP_EXECUTE,
TIMEOUT_ASYNC, NULL);
}
DdeDisconnect(activate_service_instance);
}
}
dml_error = ::DdeGetLastError(dde_instance);
if (dml_error != DMLERR_NO_ERROR)
VLOG(1) << "DDE error: " << dml_error;
DdeUninitialize(dde_instance);
return success;
}
base::CommandLine CreateCommandLine(const GURL& url,
const std::string& utf8_path,
const std::vector<std::string>& params) {
std::wstring path = base::UTF8ToWide(utf8_path);
ExpandPresetBrowsers(&path);
ExpandEnvironmentVariables(&path);
base::CommandLine cmd_line(std::vector<std::wstring>{path});
AppendCommandLineArguments(&cmd_line, params, url);
return cmd_line;
}
bool TryLaunchWithExec(const GURL& url,
const std::string& path,
const std::vector<std::string>& args) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
CHECK(url.SchemeIsHTTPOrHTTPS() || url.SchemeIsFile());
auto cmd_line = CreateCommandLine(url, path, args);
base::LaunchOptions options;
if (!base::LaunchProcess(cmd_line, options).IsValid()) {
LOG(ERROR) << "Could not start the alternative browser! Error: "
<< GetLastError();
return false;
}
return true;
}
void TryLaunchBlocking(GURL url,
std::string path,
std::vector<std::string> params,
LaunchCallback cb) {
const bool success =
(TryLaunchWithDde(url, path) || TryLaunchWithExec(url, path, params));
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) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
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::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath());
const auto* mapping = FindBrowserMapping(path, false);
return mapping ? mapping->browser_name : "alternative browser";
}
BrowserType AlternativeBrowserDriverImpl::GetBrowserType() const {
std::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath());
const auto* mapping = FindBrowserMapping(path, true);
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