910e62b5创建于 1月15日历史提交
// Copyright 2019 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/ui/webui/browser_switch/browser_switch_ui.h"

#include <memory>
#include <string_view>

#include "base/containers/to_value_list.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.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_prefs.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_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/browser_switch_resources.h"
#include "chrome/grit/browser_switch_resources_map.h"
#include "chrome/grit/generated_resources.h"
#include "components/grit/components_resources.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_service.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 "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/webui/webui_util.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, /*navigation_handle_callback=*/{});
}

// 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;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [profile, &tab_count](BrowserWindowInterface* browser) {
        if (browser->GetProfile()->GetOriginalProfile() != profile) {
          return true;
        }
        tab_count += browser->GetTabStripModel()->count();
        return tab_count <= 1;  // Continue only if we haven't found 2+ tabs yet
      });
  return tab_count <= 1;
}

// Returns a dictionary like:
//
// {
//   "sitelist": ["example.com", ...],
//   "greylist": ["example.net", ...]
// }
base::Value::Dict RuleSetToDict(const browser_switcher::RuleSet& ruleset) {
  base::Value::List sitelist;
  for (const auto& rule : ruleset.sitelist) {
    sitelist.Append(rule->ToString());
  }

  base::Value::List greylist;
  for (const auto& rule : ruleset.greylist) {
    greylist.Append(rule->ToString());
  }

  return base::Value::Dict()
      .Set("sitelist", std::move(sitelist))
      .Set("greylist", std::move(greylist));
}

browser_switcher::BrowserSwitcherService* GetBrowserSwitcherService(
    content::WebUI* web_ui) {
  return browser_switcher::BrowserSwitcherServiceFactory::GetForBrowserContext(
      web_ui->GetWebContents()->GetBrowserContext());
}

void CreateAndAddBrowserSwitchUIHTMLSource(content::WebUI* web_ui) {
  content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd(
      Profile::FromWebUI(web_ui), chrome::kChromeUIBrowserSwitchHost);

  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},
      {"openBrowserProtocolReason",
       IDS_ABOUT_BROWSER_SWITCH_INTERNALS_OPEN_BROWSER_PROTOCOL_REASON},
      {"openBrowserDefaultReason",
       IDS_ABOUT_BROWSER_SWITCH_INTERNALS_OPEN_BROWSER_DEFAULT_REASON},
      {"openBrowserRuleReason",
       IDS_ABOUT_BROWSER_SWITCH_INTERNALS_OPEN_BROWSER_RULE_REASON},
      {"openBrowserInvertRuleReason",
       IDS_ABOUT_BROWSER_SWITCH_INTERNALS_OPEN_BROWSER_INVERT_RULE_REASON},
      {"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},
      {"exportToJsonButton",
       IDS_ABOUT_BROWSER_SWITCH_INTERNALS_POLICIES_DOWNLOAD_BUTTON},
  };
  source->AddLocalizedStrings(kStrings);

  source->AddLocalizedString("protocolError",
                             IDS_ABOUT_BROWSER_SWITCH_PROTOCOL_ERROR);
  source->AddLocalizedString("title", IDS_ABOUT_BROWSER_SWITCH_TITLE);

  webui::SetupWebUIDataSource(source, kBrowserSwitchResources,
                              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();
}

}  // namespace

BrowserSwitchHandler::BrowserSwitchHandler() = default;

BrowserSwitchHandler::~BrowserSwitchHandler() = default;

void BrowserSwitchHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "launchAlternativeBrowserAndCloseTab",
      base::BindRepeating(
          &BrowserSwitchHandler::HandleLaunchAlternativeBrowserAndCloseTab,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "gotoNewTabPage",
      base::BindRepeating(&BrowserSwitchHandler::HandleGotoNewTabPage,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getAllRulesets",
      base::BindRepeating(&BrowserSwitchHandler::HandleGetAllRulesets,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getDecision",
      base::BindRepeating(&BrowserSwitchHandler::HandleGetDecision,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getTimestamps",
      base::BindRepeating(&BrowserSwitchHandler::HandleGetTimestamps,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getRulesetSources",
      base::BindRepeating(&BrowserSwitchHandler::HandleGetRulesetSources,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "refreshXml", base::BindRepeating(&BrowserSwitchHandler::HandleRefreshXml,
                                        base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "isBrowserSwitcherEnabled",
      base::BindRepeating(&BrowserSwitchHandler::HandleIsBrowserSwitchEnabled,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getBrowserSwitchInternalsJson",
      base::BindRepeating(
          &BrowserSwitchHandler::HandleGetBrowserSwitchInternalsJson,
          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::Value::List& args) {
  AllowJavascript();

  std::string callback_id = args[0].GetString();
  std::string url_spec = args[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[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::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&content::WebContents::ClosePage,
                                  web_ui()->GetWebContents()->GetWeakPtr()));
  }
}

void BrowserSwitchHandler::HandleGotoNewTabPage(const base::Value::List& args) {
  GotoNewTabPage(web_ui()->GetWebContents());
}

void BrowserSwitchHandler::HandleGetAllRulesets(const base::Value::List& args) {
  AllowJavascript();
  auto* service = GetBrowserSwitcherService(web_ui());
  auto retval =
      base::Value::Dict()
          .Set("gpo", RuleSetToDict(service->prefs().GetRules()))
          .Set("ieem", RuleSetToDict(*service->sitelist()->GetIeemSitelist()))
          .Set("external_sitelist",
               RuleSetToDict(*service->sitelist()->GetExternalSitelist()))
          .Set("external_greylist",
               RuleSetToDict(*service->sitelist()->GetExternalGreylist()));
  ResolveJavascriptCallback(args[0], retval);
}

void BrowserSwitchHandler::HandleGetDecision(const base::Value::List& args) {
  AllowJavascript();

  GURL url = GURL(args[1].GetString());
  if (!url.is_valid()) {
    RejectJavascriptCallback(args[0], base::Value());
    return;
  }

  auto* service = GetBrowserSwitcherService(web_ui());
  browser_switcher::Decision decision = service->sitelist()->GetDecision(url);

  std::string_view action_name =
      (decision.action == browser_switcher::kStay) ? "stay" : "go";

  std::string_view 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;
  }

  // clang-format off
  auto retval =
      base::Value::Dict()
          .Set("action", action_name)
          .Set("reason", reason_name);
  // clang-format on

  if (decision.matching_rule) {
    retval.Set("matching_rule", decision.matching_rule->ToString());
  }

  ResolveJavascriptCallback(args[0], retval);
}

void BrowserSwitchHandler::HandleGetTimestamps(const base::Value::List& args) {
  AllowJavascript();

  auto* service = GetBrowserSwitcherService(web_ui());
  auto* downloader = service->sitelist_downloader();

  if (!downloader) {
    ResolveJavascriptCallback(args[0], base::Value());
    return;
  }

  auto retval =
      base::Value::Dict()
          .Set("last_fetch",
               downloader->last_refresh_time().InMillisecondsFSinceUnixEpoch())
          .Set("next_fetch",
               downloader->next_refresh_time().InMillisecondsFSinceUnixEpoch());

  ResolveJavascriptCallback(args[0], retval);
}

void BrowserSwitchHandler::HandleGetRulesetSources(
    const base::Value::List& args) {
  AllowJavascript();

  auto* service = GetBrowserSwitcherService(web_ui());
  std::vector<browser_switcher::RulesetSource> sources =
      service->GetRulesetSources();

  base::Value::Dict 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"; however path
    // expansion is not expected on it as the JavaScript expects to see
    // "browser_switcher.blah" as a key in the object, not a nested hierarchy.
    retval.Set(source.pref_name, std::move(val));
  }
  ResolveJavascriptCallback(args[0], retval);
}

void BrowserSwitchHandler::HandleRefreshXml(const base::Value::List& args) {
  auto* service = GetBrowserSwitcherService(web_ui());
  service->StartDownload(base::TimeDelta());
}

void BrowserSwitchHandler::HandleIsBrowserSwitchEnabled(
    const base::Value::List& args) {
  AllowJavascript();

  auto* service = GetBrowserSwitcherService(web_ui());
  ResolveJavascriptCallback(args[0], base::Value(service->prefs().IsEnabled()));
}

void BrowserSwitchHandler::HandleGetBrowserSwitchInternalsJson(
    const base::Value::List& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  const base::Value& callback_id = args[0];
  ResolveJavascriptCallback(callback_id,
                            base::Value(GetBrowserSwitchInternalsJson()));
}

std::string BrowserSwitchHandler::GetBrowserSwitchInternalsJson() {
  auto* service = GetBrowserSwitcherService(web_ui());
  auto& service_prefs = service->prefs();
  base::Value::Dict dict;

  // Policies
  base::Value::Dict policies;

  policies.Set(policy::key::kBrowserSwitcherEnabled,
               base::Value(service_prefs.IsEnabled()));
  policies.Set(policy::key::kAlternativeBrowserPath,
               base::Value(service_prefs.GetAlternativeBrowserPath()));
  policies.Set(policy::key::kAlternativeBrowserParameters,
               base::Value(base::ToValueList(
                   service_prefs.GetAlternativeBrowserParameters())));
  policies.Set(policy::key::kBrowserSwitcherKeepLastChromeTab,
               base::Value(service_prefs.KeepLastTab()));
  policies.Set(policy::key::kBrowserSwitcherDelay,
               base::Value(service_prefs.GetDelay()));
  policies.Set(policy::key::kBrowserSwitcherExternalSitelistUrl,
               base::Value(service_prefs.GetExternalSitelistUrl().spec()));
  policies.Set(policy::key::kBrowserSwitcherExternalGreylistUrl,
               base::Value(service_prefs.GetExternalGreylistUrl().spec()));
  policies.Set(policy::key::kBrowserSwitcherParsingMode,
               base::Value(static_cast<int>(service_prefs.GetParsingMode())));

#if BUILDFLAG(IS_WIN)
  policies.Set(policy::key::kBrowserSwitcherUseIeSitelist,
               base::Value(service_prefs.UseIeSitelist()));
  policies.Set(policy::key::kBrowserSwitcherChromePath,
               base::Value(service_prefs.GetChromePath().LossyDisplayName()));
  policies.Set(
      policy::key::kBrowserSwitcherChromeParameters,
      base::Value(base::ToValueList(service_prefs.GetChromeParameters())));
#endif

  dict.Set("policies", std::move(policies));

  // Alternative Browser
  dict.Set("alternativeBrowserName", service->driver()->GetBrowserName());
  dict.Set("alternativeBrowserPath", service_prefs.GetAlternativeBrowserPath());

  // Rulesets
  base::Value::Dict rulesets;
  rulesets.Set("gpo", RuleSetToDict(service->prefs().GetRules()));
  rulesets.Set("ieem", RuleSetToDict(*service->sitelist()->GetIeemSitelist()));
  rulesets.Set("external_sitelist",
               RuleSetToDict(*service->sitelist()->GetExternalSitelist()));
  rulesets.Set("external_greylist",
               RuleSetToDict(*service->sitelist()->GetExternalGreylist()));
  dict.Set("rulesets", std::move(rulesets));

  // Ruleset Sources
  std::vector<browser_switcher::RulesetSource> sources =
      service->GetRulesetSources();
  base::Value::Dict rulesetSources;
  for (const auto& source : sources) {
    rulesetSources.Set(source.pref_name, source.url.is_valid()
                                             ? base::Value(source.url.spec())
                                             : base::Value());
  }
  dict.Set("rulesetSources", std::move(rulesetSources));

  // XML Timestamps
  auto* downloader = service->sitelist_downloader();
  if (downloader) {
    base::Value::Dict timestamps;
    timestamps.Set("lastFetch",
                   base::TimeFormatHTTP(downloader->last_refresh_time()));
    timestamps.Set("nextFetch",
                   base::TimeFormatHTTP(downloader->next_refresh_time()));
    dict.Set("xmlTimestamps", std::move(timestamps));
  }

  std::string json;
  base::JSONWriter::WriteWithOptions(
      dict, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json);
  return json;
}

bool BrowserSwitchUIConfig::IsWebUIEnabled(
    content::BrowserContext* browser_context) {
  return browser_context && !browser_context->IsOffTheRecord();
}

BrowserSwitchUI::BrowserSwitchUI(content::WebUI* web_ui)
    : WebUIController(web_ui) {
  web_ui->AddMessageHandler(std::make_unique<BrowserSwitchHandler>());

  // Set up the chrome://browser-switch source.
  CreateAndAddBrowserSwitchUIHTMLSource(web_ui);
}