| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import '/strings.m.js'; |
| import 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar.js'; |
| import 'chrome://resources/cr_elements/cr_button/cr_button.js'; |
| import 'chrome://resources/cr_elements/cr_input/cr_input.js'; |
| |
| import type {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js'; |
| import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js'; |
| import {addWebUiListener} from 'chrome://resources/js/cr.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| |
| import {getCss} from './app.css.js'; |
| import {getHtml} from './app.html.js'; |
| import type {BrowserSwitchInternalsProxy, Decision, RuleSet, RuleSetList, RulesetSources, TimestampPair} from './browser_switch_internals_proxy.js'; |
| import {BrowserSwitchInternalsProxyImpl} from './browser_switch_internals_proxy.js'; |
| |
| interface XmlSiteListItem { |
| policyName: string; |
| url: string; |
| } |
| |
| interface RuleItem { |
| rule: string; |
| rulesetName: string; |
| } |
| |
| const AppElementBase = I18nMixinLit(CrLitElement); |
| |
| export class AppElement extends AppElementBase { |
| static get is() { |
| return 'browser-switch-internals-app'; |
| } |
| |
| static override get styles() { |
| return getCss(); |
| } |
| |
| override render() { |
| return getHtml.bind(this)(); |
| } |
| |
| static override get properties() { |
| return { |
| isBrowserSwitcherEnabled_: {type: Boolean}, |
| showSearch_: {type: Boolean}, |
| lastFetch_: {type: String}, |
| nextFetch_: {type: String}, |
| urlCheckerInput_: {type: String}, |
| urlCheckerOutput_: {type: Array}, |
| greyListRules_: {type: Array}, |
| siteListRules_: {type: Array}, |
| xmlSiteLists_: {type: Array}, |
| }; |
| } |
| |
| protected isBrowserSwitcherEnabled_: boolean = true; |
| protected showSearch_: boolean = false; |
| protected greyListRules_: RuleItem[] = []; |
| protected siteListRules_: RuleItem[] = []; |
| protected xmlSiteLists_: XmlSiteListItem[] = []; |
| protected urlCheckerInput_: string = ''; |
| protected urlCheckerOutput_: string[] = []; |
| protected lastFetch_: string = ''; |
| protected nextFetch_: string = ''; |
| |
| override firstUpdated() { |
| this.updateEverything(); |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| addWebUiListener('data-changed', () => this.updateEverything()); |
| }); |
| } |
| |
| override updated(changedProperties: PropertyValues<this>) { |
| super.updated(changedProperties); |
| |
| const changedPrivateProperties = |
| changedProperties as Map<PropertyKey, unknown>; |
| |
| if (changedPrivateProperties.has('urlCheckerInput_')) { |
| this.checkUrl_(this.urlCheckerInput_); |
| } |
| } |
| |
| getRuleBrowserName(rule: string) { |
| return rule.startsWith('!') ? getBrowserName() : getAltBrowserName(); |
| } |
| |
| getPolicyFromRuleset(ruleSetName: string) { |
| const rulesetToPolicy: Record<string, string> = { |
| gpo: 'BrowserSwitcherUrlList', |
| ieem: 'BrowserSwitcherUseIeSitelist', |
| external_sitelist: 'BrowserSwitcherExternalSitelistUrl', |
| external_greylist: 'BrowserSwitcherExternalGreylistUrl', |
| }; |
| return rulesetToPolicy[ruleSetName]; |
| } |
| |
| /** |
| * Updates the content of all tables after receiving data from the backend. |
| */ |
| updateTables(rulesets: RuleSetList) { |
| this.siteListRules_ = []; |
| this.greyListRules_ = []; |
| |
| const listNameToProperty: Record<string, RuleItem[]> = { |
| sitelist: this.siteListRules_, |
| greylist: this.greyListRules_, |
| }; |
| |
| for (const [rulesetName, ruleset] of Object.entries(rulesets)) { |
| for (const [listName, rules] of Object.entries(ruleset as RuleSet)) { |
| listNameToProperty[listName]!.push(...rules.map((rule: string) => ({ |
| rulesetName, |
| rule, |
| }))); |
| } |
| } |
| } |
| |
| /** |
| * Takes the json from the url checker and makes it readable. |
| */ |
| urlOutputText(decision: Decision): string[] { |
| let opensIn = ''; |
| const altBrowserName = getAltBrowserName(); |
| const browserName = getBrowserName(); |
| |
| switch (decision.action) { |
| case 'stay': |
| opensIn = this.i18n('openBrowser', browserName) + '\n'; |
| break; |
| case 'go': |
| opensIn = this.i18n('openBrowser', altBrowserName) + '\n'; |
| break; |
| } |
| |
| let reason = ''; |
| if (decision.matching_rule) { |
| if (decision.matching_rule.startsWith('!')) { |
| reason += this.i18n( |
| 'openBrowserInvertRuleReason', |
| JSON.stringify(decision.matching_rule)) + |
| '\n'; |
| } else { |
| const list = decision.reason === 'sitelist' ? |
| this.i18n('forceOpenTitle') : |
| this.i18n('ignoreTitle'); |
| reason += this.i18n( |
| 'openBrowserRuleReason', |
| JSON.stringify(decision.matching_rule), list) + |
| '\n'; |
| } |
| } |
| // if undefined - add nothing to the output |
| |
| switch (decision.reason) { |
| case 'globally_disabled': |
| throw new Error('BrowserSwitcherEnabled policy is set as false!'); |
| case 'protocol': |
| reason += this.i18n('openBrowserProtocolReason') + '\n'; |
| break; |
| case 'default': |
| reason += this.i18n('openBrowserDefaultReason', browserName) + '\n'; |
| break; |
| } |
| |
| return [opensIn, reason]; |
| } |
| |
| private checkUrl_(url: string) { |
| if (!url) { |
| this.urlCheckerOutput_ = []; |
| return; |
| } |
| |
| if (!url.includes('://')) { |
| url = 'http://' + url; |
| } |
| |
| getProxy() |
| .getDecision(url) |
| .then((decision) => { |
| // URL is valid. |
| this.urlCheckerOutput_ = this.urlOutputText(decision); |
| }) |
| .catch((errorMessage) => { |
| // URL is invalid. |
| console.warn(errorMessage); |
| this.urlCheckerOutput_ = [this.i18n('invalidURL')]; |
| }); |
| } |
| |
| refreshXml() { |
| getProxy().refreshXml(); |
| } |
| |
| /** |
| * Update the paragraphs under the "XML sitelists" section. |
| */ |
| updateTimestamps(timestamps: TimestampPair|null) { |
| if (!timestamps) { |
| return; |
| } |
| const {last_fetch, next_fetch} = timestamps; |
| |
| this.lastFetch_ = last_fetch !== 0 ? formatTime(last_fetch) : ''; |
| this.nextFetch_ = next_fetch !== 0 ? formatTime(next_fetch) : ''; |
| } |
| |
| /** |
| * Update the table under the "XML sitelists" section. |
| */ |
| updateXmlTable(rulesetSources: RulesetSources) { |
| this.xmlSiteLists_ = |
| Object.entries(rulesetSources) |
| .map(([prefName, url]) => ({ |
| // Hacky name guessing |
| policyName: 'BrowserSwitcher' + |
| snakeCaseToUpperCamelCase(prefName.split('.')[1]!), |
| url: url || this.i18n('notConfigured'), |
| })); |
| } |
| |
| /** |
| * Called by C++ when we need to update everything on the page. |
| */ |
| async updateEverything() { |
| this.isBrowserSwitcherEnabled_ = |
| await getProxy().isBrowserSwitcherEnabled(); |
| if (this.isBrowserSwitcherEnabled_) { |
| getProxy().getAllRulesets().then( |
| (rulesets) => this.updateTables(rulesets)); |
| getProxy().getTimestamps().then( |
| (timestamps) => this.updateTimestamps(timestamps)); |
| getProxy().getRulesetSources().then( |
| (sources) => this.updateXmlTable(sources)); |
| } |
| } |
| |
| /** |
| * Section: XML configuration source |
| * Shows information about the last time XML sitelists were downloaded. |
| */ |
| protected getXmlSitelistsLastDownloadLabel(): string { |
| return this.i18n('xmlSitelistLastDownloadDate', this.lastFetch_); |
| } |
| |
| /** |
| * Section: XML configuration source |
| * Shows information about the next download time of XML sitelists. |
| */ |
| protected getXmlSitelistsNextDownloadLabel(): string { |
| return this.i18n('xmlSitelistNextDownloadDate', this.nextFetch_); |
| } |
| |
| /** |
| * Section: Ignore |
| * Paragraph that informs that the URLs that are affected by the lists |
| * BrowserSwitcherExternalGreylistUrl and BrowserSwitcherUrlGreylist |
| * will not trigger a browser switch. |
| */ |
| protected getIgnoreUrlMatchingLabel(): string { |
| return this.i18n('ignoreParagraph2', getBrowserName(), getAltBrowserName()); |
| } |
| |
| protected onUrlCheckerInputInput_(e: Event) { |
| this.urlCheckerInput_ = (e.target as CrInputElement).value; |
| } |
| } |
| |
| customElements.define(AppElement.is, AppElement); |
| |
| /** |
| * Converts 'this_word' to 'ThisWord' |
| */ |
| function snakeCaseToUpperCamelCase(symbol: string): string { |
| if (!symbol) { |
| return symbol; |
| } |
| return symbol.replace(/(?:^|_)([a-z])/g, (_, letter) => { |
| return letter.toUpperCase(); |
| }); |
| } |
| |
| /** |
| * Formats |date| as "HH:MM:SS". |
| */ |
| function formatTime(dateNumber: number): string { |
| const date = new Date(dateNumber); |
| const hh = date.getHours().toString().padStart(2, '0'); |
| const mm = date.getMinutes().toString().padStart(2, '0'); |
| const ss = date.getSeconds().toString().padStart(2, '0'); |
| return `${hh}:${mm}:${ss}`; |
| } |
| |
| /** |
| * Gets the English name of the alternate browser. |
| */ |
| function getAltBrowserName(): string { |
| // TODO (crbug.com/1258133): if you change the AlternativeBrowserPath |
| // policy, then loadTimeData can contain stale data. It won't update |
| // until you refresh (despite the rest of the page auto-updating). |
| return loadTimeData.getString('altBrowserName') || 'alternative browser'; |
| } |
| |
| /** |
| * Gets the English name of the browser. |
| */ |
| function getBrowserName(): string { |
| // TODO (crbug.com/1258133): if you change the AlternativeBrowserPath |
| // policy, then loadTimeData can contain stale data. It won't update |
| // until you refresh (despite the rest of the page auto-updating). |
| return loadTimeData.getString('browserName'); |
| } |
| |
| function getProxy(): BrowserSwitchInternalsProxy { |
| return BrowserSwitchInternalsProxyImpl.getInstance(); |
| } |