blob: 4d64db311ed37389a5ba624aba4f4af7a94bc56e [file] [log] [blame]
// Copyright (c) 2019 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/ui/webui/browser_switch/browser_switch_ui.h"
#include <memory>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/browser_switcher/alternative_browser_driver.h"
#include "chrome/browser/browser_switcher/browser_switcher_service.h"
#include "chrome/browser/browser_switcher/browser_switcher_service_factory.h"
#include "chrome/browser/browser_switcher/browser_switcher_sitelist.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/webui/webui_util.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/browser_switch_resources.h"
#include "chrome/grit/browser_switch_resources_map.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/grit/components_resources.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/browser/web_ui_message_handler.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
namespace {
void GotoNewTabPage(content::WebContents* web_contents) {
GURL url(chrome::kChromeUINewTabURL);
content::OpenURLParams params(url, content::Referrer(),
WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false);
web_contents->OpenURL(params);
}
// Returns true if there's only 1 tab left open in this profile. Incognito
// window tabs count as the same profile.
bool IsLastTab(const Profile* profile) {
profile = profile->GetOriginalProfile();
int tab_count = 0;
for (const Browser* browser : *BrowserList::GetInstance()) {
if (browser->profile()->GetOriginalProfile() != profile)
continue;
tab_count += browser->tab_strip_model()->count();
if (tab_count > 1)
return false;
}
return true;
}
// Returns a dictionary like:
//
// {
// "sitelist": ["example.com", ...],
// "greylist": ["example.net", ...]
// }
base::Value RuleSetToDict(const browser_switcher::RuleSet& ruleset) {
base::Value sitelist(base::Value::Type::LIST);
for (const auto& rule : ruleset.sitelist)
sitelist.Append(rule->ToString());
base::Value greylist(base::Value::Type::LIST);
for (const auto& rule : ruleset.greylist)
greylist.Append(rule->ToString());
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetKey("sitelist", std::move(sitelist));
dict.SetKey("greylist", std::move(greylist));
return dict;
}
browser_switcher::BrowserSwitcherService* GetBrowserSwitcherService(
content::WebUI* web_ui) {
return browser_switcher::BrowserSwitcherServiceFactory::GetForBrowserContext(
web_ui->GetWebContents()->GetBrowserContext());
}
content::WebUIDataSource* CreateBrowserSwitchUIHTMLSource(
content::WebUI* web_ui) {
content::WebUIDataSource* source =
content::WebUIDataSource::Create(chrome::kChromeUIBrowserSwitchHost);
source->DisableTrustedTypesCSP();
auto* service = GetBrowserSwitcherService(web_ui);
source->AddInteger("launchDelay", service->prefs().GetDelay());
std::string alt_browser_name = service->driver()->GetBrowserName();
source->AddString("altBrowserName", alt_browser_name);
source->AddLocalizedString("browserName", IDS_PRODUCT_NAME);
if (alt_browser_name.empty()) {
// Browser name could not be auto-detected. Say "alternative browser"
// instead of naming the browser.
source->AddLocalizedString(
"countdownTitle",
IDS_ABOUT_BROWSER_SWITCH_COUNTDOWN_TITLE_UNKNOWN_BROWSER);
source->AddLocalizedString(
"description", IDS_ABOUT_BROWSER_SWITCH_DESCRIPTION_UNKNOWN_BROWSER);
source->AddLocalizedString(
"errorTitle", IDS_ABOUT_BROWSER_SWITCH_ERROR_TITLE_UNKNOWN_BROWSER);
source->AddLocalizedString(
"genericError", IDS_ABOUT_BROWSER_SWITCH_GENERIC_ERROR_UNKNOWN_BROWSER);
source->AddLocalizedString(
"openingTitle", IDS_ABOUT_BROWSER_SWITCH_OPENING_TITLE_UNKNOWN_BROWSER);
} else {
// Browser name was auto-detected. Name it in the text.
source->AddLocalizedString(
"countdownTitle",
IDS_ABOUT_BROWSER_SWITCH_COUNTDOWN_TITLE_KNOWN_BROWSER);
source->AddLocalizedString(
"description", IDS_ABOUT_BROWSER_SWITCH_DESCRIPTION_KNOWN_BROWSER);
source->AddLocalizedString(
"errorTitle", IDS_ABOUT_BROWSER_SWITCH_ERROR_TITLE_KNOWN_BROWSER);
source->AddLocalizedString(
"genericError", IDS_ABOUT_BROWSER_SWITCH_GENERIC_ERROR_KNOWN_BROWSER);
source->AddLocalizedString(
"openingTitle", IDS_ABOUT_BROWSER_SWITCH_OPENING_TITLE_KNOWN_BROWSER);
}
static constexpr webui::LocalizedString kStrings[] = {
{"switchInternalDescription", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_DESC},
{"switchInternalTitle", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_TITLE},
{"nothingShown", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_NOTHING_SHOWN},
{"switcherDisabled", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_LBS_DISABLED},
{"urlCheckerTitle", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_URL_CHECKER_TITLE},
{"urlCheckerDesc", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_URL_CHECKER_DESC},
{"openBrowser", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_OPEN_BROWSER},
{"invalidURL", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_INVALID_URL},
{"xmlTitle", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_TITLE},
{"xmlDesc", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_DESC},
{"xmlSource", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_SOURCE},
{"notConfigured", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_NOT_CONFIGURED},
{"sitelistNotFetched",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_SITELIST_NOT_FETCHED},
{"sitelistDownloadButton",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_SITELIST_DOWNLOAD_BUTTON},
{"xmlSitelistLastDownloadDate",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_SITELIST_LAST_DOWNLOAD_DATE},
{"xmlSitelistNextDownloadDate",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_XML_SITELIST_NEXT_DOWNLOAD_DATE},
{"forceOpenTitle",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_IN_TITLE},
{"forceOpenDescription",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_IN_DESCRIPTION},
{"forceOpenParagraph1",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_IN_FIRST_PARAGRAPH},
{"forceOpenParagraph2",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_IN_SECOND_PARAGRAPH},
{"forceOpenTableColumnRule",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_TABLE_COLUMN_RULE},
{"forceOpenTableColumnOpensIn",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_TABLE_COLUMN_OPENS_IN},
{"forceOpenTableColumnSource",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_FORCE_OPEN_TABLE_COLUMN_SOURCE},
{"ignoreTitle", IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_TITLE},
{"ignoreDescription",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_DESCRIPTION},
{"ignoreParagraph1",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_FIRST_PARAGRAPH},
{"ignoreParagraph2",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_SECOND_PARAGRAPH},
{"ignoreTableColumnRule",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_TABLE_COLUMN_RULE},
{"ignoreTableColumnSource",
IDS_ABOUT_BROWSER_SWITCH_INTERNALS_IGNORE_TABLE_COLUMN_SOURCE},
};
source->AddLocalizedStrings(kStrings);
source->AddLocalizedString("protocolError",
IDS_ABOUT_BROWSER_SWITCH_PROTOCOL_ERROR);
source->AddLocalizedString("title", IDS_ABOUT_BROWSER_SWITCH_TITLE);
webui::SetupWebUIDataSource(
source,
base::make_span(kBrowserSwitchResources, kBrowserSwitchResourcesSize),
IDR_BROWSER_SWITCH_BROWSER_SWITCH_HTML);
// Setup chrome://browser-switch/internals debug UI.
source->AddResourcePath(
"internals/", IDR_BROWSER_SWITCH_INTERNALS_BROWSER_SWITCH_INTERNALS_HTML);
source->AddResourcePath(
"internals", IDR_BROWSER_SWITCH_INTERNALS_BROWSER_SWITCH_INTERNALS_HTML);
source->UseStringsJs();
return source;
}
} // namespace
class BrowserSwitchHandler : public content::WebUIMessageHandler {
public:
BrowserSwitchHandler();
BrowserSwitchHandler(const BrowserSwitchHandler&) = delete;
BrowserSwitchHandler& operator=(const BrowserSwitchHandler&) = delete;
~BrowserSwitchHandler() override;
// WebUIMessageHandler
void RegisterMessages() override;
void OnJavascriptAllowed() override;
void OnJavascriptDisallowed() override;
private:
void OnAllRulesetsParsed(browser_switcher::BrowserSwitcherService* service);
void OnBrowserSwitcherPrefsChanged(
browser_switcher::BrowserSwitcherPrefs* prefs,
const std::vector<std::string>& changed_prefs);
// For the internals page: tell JS to update all the page contents.
void SendDataChangedEvent();
// Launches the given URL in the configured alternative browser. Acts as a
// bridge for |AlternativeBrowserDriver::TryLaunch()|. Then, if that succeeds,
// closes the current tab.
//
// If it fails, the JavaScript promise is rejected. If it succeeds, the
// JavaScript promise is not resolved, because we close the tab anyways.
void HandleLaunchAlternativeBrowserAndCloseTab(const base::ListValue* args);
void OnLaunchFinished(base::TimeTicks start,
std::string callback_id,
bool success);
// Navigates to the New Tab Page.
void HandleGotoNewTabPage(const base::ListValue* args);
// Resolves a promise with a JSON object with all the LBS rulesets, formatted
// like this:
//
// {
// "gpo": {
// "sitelist": ["example.com", ...],
// "greylist": [...]
// },
// "ieem": { "sitelist": [...], "greylist": [...] },
// "external": { "sitelist": [...], "greylist": [...] }
// }
void HandleGetAllRulesets(const base::ListValue* args);
// Resolves a promise with a JSON object describing the decision for a URL
// (stay/go) + reason. The result is formatted like this:
//
// {
// "action": ("stay"|"go"),
// "reason": ("globally_disabled"|"protocol"|"sitelist"|...),
// "matching_rule": (string|undefined)
// }
void HandleGetDecision(const base::ListValue* args);
// Resolves a promise with the time of the last policy fetch and next policy
// fetch, as JS timestamps.
//
// {
// "last_fetch": 123456789,
// "next_fetch": 234567890
// }
void HandleGetTimestamps(const base::ListValue* args);
// Resolves a promise with the configured sitelist XML download URLs. The keys
// are the name of the pref associated with the sitelist.
//
// {
// "browser_switcher": {
// "use_ie_sitelist": "http://example.com/sitelist.xml",
// "external_sitelist_url": "http://example.com/other_sitelist.xml",
// "external_greylist_url": null
// }
// }
void HandleGetRulesetSources(const base::ListValue* args);
// Immediately re-download and apply XML rules.
void HandleRefreshXml(const base::ListValue* args);
// Resolves a promise with the boolean value describing whether the feature
// is enabled or not which is configured by BrowserSwitcherEnabled key
void HandleIsBrowserSwitchEnabled(const base::ListValue* args);
base::CallbackListSubscription prefs_subscription_;
base::CallbackListSubscription service_subscription_;
base::WeakPtrFactory<BrowserSwitchHandler> weak_ptr_factory_{this};
};
BrowserSwitchHandler::BrowserSwitchHandler() {}
BrowserSwitchHandler::~BrowserSwitchHandler() = default;
void BrowserSwitchHandler::RegisterMessages() {
web_ui()->RegisterDeprecatedMessageCallback(
"launchAlternativeBrowserAndCloseTab",
base::BindRepeating(
&BrowserSwitchHandler::HandleLaunchAlternativeBrowserAndCloseTab,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"gotoNewTabPage",
base::BindRepeating(&BrowserSwitchHandler::HandleGotoNewTabPage,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"getAllRulesets",
base::BindRepeating(&BrowserSwitchHandler::HandleGetAllRulesets,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"getDecision",
base::BindRepeating(&BrowserSwitchHandler::HandleGetDecision,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"getTimestamps",
base::BindRepeating(&BrowserSwitchHandler::HandleGetTimestamps,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"getRulesetSources",
base::BindRepeating(&BrowserSwitchHandler::HandleGetRulesetSources,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"refreshXml", base::BindRepeating(&BrowserSwitchHandler::HandleRefreshXml,
base::Unretained(this)));
web_ui()->RegisterDeprecatedMessageCallback(
"isBrowserSwitcherEnabled",
base::BindRepeating(&BrowserSwitchHandler::HandleIsBrowserSwitchEnabled,
base::Unretained(this)));
}
void BrowserSwitchHandler::OnJavascriptAllowed() {
auto* service = GetBrowserSwitcherService(web_ui());
prefs_subscription_ = service->prefs().RegisterPrefsChangedCallback(
base::BindRepeating(&BrowserSwitchHandler::OnBrowserSwitcherPrefsChanged,
base::Unretained(this)));
service_subscription_ =
service->RegisterAllRulesetsParsedCallback(base::BindRepeating(
&BrowserSwitchHandler::OnAllRulesetsParsed, base::Unretained(this)));
}
void BrowserSwitchHandler::OnJavascriptDisallowed() {
prefs_subscription_ = {};
service_subscription_ = {};
}
void BrowserSwitchHandler::OnAllRulesetsParsed(
browser_switcher::BrowserSwitcherService* service) {
SendDataChangedEvent();
}
void BrowserSwitchHandler::OnBrowserSwitcherPrefsChanged(
browser_switcher::BrowserSwitcherPrefs* prefs,
const std::vector<std::string>& changed_prefs) {
SendDataChangedEvent();
}
void BrowserSwitchHandler::SendDataChangedEvent() {
FireWebUIListener("data-changed");
}
void BrowserSwitchHandler::HandleLaunchAlternativeBrowserAndCloseTab(
const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
std::string callback_id = args->GetListDeprecated()[0].GetString();
std::string url_spec = args->GetListDeprecated()[1].GetString();
GURL url(url_spec);
auto* service = GetBrowserSwitcherService(web_ui());
bool should_switch = service->sitelist()->ShouldSwitch(url);
if (!url.is_valid() || !should_switch) {
// This URL shouldn't open in an alternative browser. Abort launch, because
// something weird is going on (e.g. race condition from a new sitelist
// being loaded).
RejectJavascriptCallback(args->GetListDeprecated()[0], base::Value());
return;
}
service->driver()->TryLaunch(
url, base::BindOnce(&BrowserSwitchHandler::OnLaunchFinished,
weak_ptr_factory_.GetWeakPtr(),
base::TimeTicks::Now(), std::move(callback_id)));
}
void BrowserSwitchHandler::OnLaunchFinished(base::TimeTicks start,
std::string callback_id,
bool success) {
const base::TimeDelta runtime = base::TimeTicks::Now() - start;
UMA_HISTOGRAM_TIMES("BrowserSwitcher.LaunchTime", runtime);
UMA_HISTOGRAM_BOOLEAN("BrowserSwitcher.LaunchSuccess", success);
if (!success) {
RejectJavascriptCallback(base::Value(callback_id), base::Value());
return;
}
auto* service = GetBrowserSwitcherService(web_ui());
auto* profile = Profile::FromWebUI(web_ui());
// We don't need to resolve the promise, because the tab will close (or
// navigate to about:newtab) anyways.
if (service->prefs().KeepLastTab() && IsLastTab(profile)) {
GotoNewTabPage(web_ui()->GetWebContents());
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&content::WebContents::ClosePage,
web_ui()->GetWebContents()->GetWeakPtr()));
}
}
void BrowserSwitchHandler::HandleGotoNewTabPage(const base::ListValue* args) {
GotoNewTabPage(web_ui()->GetWebContents());
}
void BrowserSwitchHandler::HandleGetAllRulesets(const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
auto* service = GetBrowserSwitcherService(web_ui());
base::Value retval(base::Value::Type::DICTIONARY);
auto gpo_dict = RuleSetToDict(service->prefs().GetRules());
retval.SetKey("gpo", std::move(gpo_dict));
auto ieem_dict = RuleSetToDict(*service->sitelist()->GetIeemSitelist());
retval.SetKey("ieem", std::move(ieem_dict));
auto external_sitelist_dict =
RuleSetToDict(*service->sitelist()->GetExternalSitelist());
retval.SetKey("external_sitelist", std::move(external_sitelist_dict));
auto external_greylist_dict =
RuleSetToDict(*service->sitelist()->GetExternalGreylist());
retval.SetKey("external_greylist", std::move(external_greylist_dict));
ResolveJavascriptCallback(args->GetListDeprecated()[0], retval);
}
void BrowserSwitchHandler::HandleGetDecision(const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
GURL url = GURL(args->GetListDeprecated()[1].GetString());
if (!url.is_valid()) {
RejectJavascriptCallback(args->GetListDeprecated()[0], base::Value());
return;
}
auto* service = GetBrowserSwitcherService(web_ui());
browser_switcher::Decision decision = service->sitelist()->GetDecision(url);
base::DictionaryValue retval;
base::StringPiece action_name =
(decision.action == browser_switcher::kStay) ? "stay" : "go";
retval.Set("action", std::make_unique<base::Value>(action_name));
base::StringPiece reason_name;
switch (decision.reason) {
case browser_switcher::kDisabled:
reason_name = "globally_disabled";
break;
case browser_switcher::kProtocol:
reason_name = "protocol";
break;
case browser_switcher::kSitelist:
reason_name = "sitelist";
break;
case browser_switcher::kGreylist:
reason_name = "greylist";
break;
case browser_switcher::kDefault:
reason_name = "default";
break;
}
retval.Set("reason", std::make_unique<base::Value>(reason_name));
if (decision.matching_rule) {
retval.Set("matching_rule", std::make_unique<base::Value>(
decision.matching_rule->ToString()));
}
ResolveJavascriptCallback(args->GetListDeprecated()[0], retval);
}
void BrowserSwitchHandler::HandleGetTimestamps(const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
auto* service = GetBrowserSwitcherService(web_ui());
auto* downloader = service->sitelist_downloader();
if (!downloader) {
ResolveJavascriptCallback(args->GetListDeprecated()[0], base::Value());
return;
}
base::DictionaryValue retval;
retval.Set("last_fetch", std::make_unique<base::Value>(
downloader->last_refresh_time().ToJsTime()));
retval.Set("next_fetch", std::make_unique<base::Value>(
downloader->next_refresh_time().ToJsTime()));
ResolveJavascriptCallback(args->GetListDeprecated()[0], retval);
}
void BrowserSwitchHandler::HandleGetRulesetSources(
const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
auto* service = GetBrowserSwitcherService(web_ui());
std::vector<browser_switcher::RulesetSource> sources =
service->GetRulesetSources();
base::DictionaryValue retval;
for (const auto& source : sources) {
base::Value val;
if (source.url.is_valid())
val = base::Value(source.url.spec());
// |pref_name| is something like "browser_switcher.blah", so this will be in
// a nested object.
retval.SetKey(source.pref_name, std::move(val));
}
ResolveJavascriptCallback(args->GetListDeprecated()[0], retval);
}
void BrowserSwitchHandler::HandleRefreshXml(const base::ListValue* args) {
DCHECK(args);
auto* service = GetBrowserSwitcherService(web_ui());
service->StartDownload(base::TimeDelta());
}
void BrowserSwitchHandler::HandleIsBrowserSwitchEnabled(
const base::ListValue* args) {
DCHECK(args);
AllowJavascript();
auto* service = GetBrowserSwitcherService(web_ui());
ResolveJavascriptCallback(args->GetListDeprecated()[0],
base::Value(service->prefs().IsEnabled()));
}
BrowserSwitchUI::BrowserSwitchUI(content::WebUI* web_ui)
: WebUIController(web_ui) {
web_ui->AddMessageHandler(std::make_unique<BrowserSwitchHandler>());
// Set up the chrome://browser-switch source.
Profile* profile = Profile::FromWebUI(web_ui);
content::WebUIDataSource::Add(profile,
CreateBrowserSwitchUIHTMLSource(web_ui));
}