| // 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. |
| |
| import 'chrome://resources/cr_elements/cr_button/cr_button.js'; |
| import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js'; |
| import 'chrome://resources/cr_elements/cr_input/cr_input.js'; |
| import 'chrome://resources/cr_elements/icons.html.js'; |
| import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; |
| import '/strings.m.js'; |
| |
| import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js'; |
| import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js'; |
| import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.js'; |
| import {assert} from 'chrome://resources/js/assert.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 {getHtml} from './runtime_hosts_dialog.html.js'; |
| import {getCss as getSharedStyleCss} from './shared_style.css.js'; |
| import {sitePermissionsPatternRegExp} from './site_permissions/site_permissions_edit_url_dialog.js'; |
| import {SiteSettingsMixin} from './site_permissions/site_settings_mixin.js'; |
| |
| // A RegExp to roughly match acceptable patterns entered by the user. |
| // exec'ing() this RegExp will match the following groups: |
| // 0: Full matched string. |
| // 1: Scheme + scheme separator (e.g., 'https://'). |
| // 2: Scheme only (e.g., 'https'). |
| // 3: Match subdomains ('*.'). |
| // 4: Hostname (e.g., 'example.com'). |
| // 5: Port, including ':' separator (e.g., ':80'). |
| // 6: Path, include '/' separator (e.g., '/*'). |
| const runtimeHostsPatternRegExp = new RegExp( |
| '^' + |
| // Scheme; optional. |
| '((http|https|\\*)://)?' + |
| // Include subdomains specifier; optional. |
| '(\\*\\.)?' + |
| // Hostname or localhost, required. |
| '([a-z0-9\\.-]+\\.[a-z0-9]+|localhost)' + |
| // Port, optional. |
| '(:[0-9]+)?' + |
| // Path, optional but if present must be '/' or '/*'. |
| '(\\/\\*|\\/)?' + |
| '$'); |
| |
| export function getPatternFromSite(site: string): string { |
| const res = runtimeHostsPatternRegExp.exec(site)!; |
| assert(res); |
| const scheme = res[1] || '*://'; |
| const host = (res[3] || '') + res[4]; |
| const port = res[5] || ''; |
| const path = '/*'; |
| return scheme + host + port + path; |
| } |
| |
| // Returns the sublist of `userSites` which match the pattern specified by |
| // `host`. |
| export function getMatchingUserSpecifiedSites( |
| userSites: string[], host: string): string[] { |
| if (!runtimeHostsPatternRegExp.test(host)) { |
| return []; |
| } |
| |
| const newHostRes = runtimeHostsPatternRegExp.exec(host); |
| assert(newHostRes); |
| |
| const matchAllSchemes = !newHostRes[1] || newHostRes[1] === '*://'; |
| const matchAllSubdomains = newHostRes[3] === '*.'; |
| |
| // For each restricted site, break it down into |
| // `sitePermissionsPatternRegExp` components and check against components |
| // from `newHostRes`. |
| return userSites.filter((userSite: string) => { |
| const siteRes = sitePermissionsPatternRegExp.exec(userSite); |
| assert(siteRes); |
| |
| // Check if schemes match, unless `newHostRes` has a wildcard scheme. |
| if (!matchAllSchemes && newHostRes[1] !== siteRes[1]) { |
| return false; |
| } |
| |
| // Check if host names match. If `matchAllSubdomains` is specified, check |
| // that `newHostRes[4]` is a suffix of `siteRes[3]` |
| if (matchAllSubdomains && !siteRes[3]!.endsWith(newHostRes[4]!)) { |
| return false; |
| } |
| if (!matchAllSubdomains && siteRes[3] !== newHostRes[4]) { |
| return false; |
| } |
| |
| // Ports match if: |
| // - both are unspecified |
| // - both are specified and are an exact match |
| // - specified for `restrictedSite` but not `this,site_` |
| return !newHostRes[5] || newHostRes[5] === siteRes[4]; |
| }); |
| } |
| |
| export interface ExtensionsRuntimeHostsDialogElement { |
| $: { |
| dialog: CrDialogElement, |
| submit: CrButtonElement, |
| }; |
| } |
| |
| const ExtensionsRuntimeHostsDialogElementBase = |
| I18nMixinLit(SiteSettingsMixin(CrLitElement)); |
| |
| export class ExtensionsRuntimeHostsDialogElement extends |
| ExtensionsRuntimeHostsDialogElementBase { |
| static get is() { |
| return 'extensions-runtime-hosts-dialog'; |
| } |
| |
| static override get styles() { |
| return getSharedStyleCss(); |
| } |
| |
| override render() { |
| return getHtml.bind(this)(); |
| } |
| |
| static override get properties() { |
| return { |
| itemId: {type: String}, |
| |
| /** |
| * The site that this entry is currently managing. Only non-empty if this |
| * is for editing an existing entry. |
| */ |
| currentSite: {type: String}, |
| |
| /** |
| * Whether the dialog should update the host access to be "on specific |
| * sites" before adding a new host permission. |
| */ |
| updateHostAccess: {type: Boolean}, |
| |
| /** The site to add an exception for. */ |
| site_: {type: String}, |
| |
| /** Whether the currently-entered input is valid. */ |
| inputInvalid_: {type: Boolean}, |
| |
| /** |
| * the list of user specified restricted sites that match with `site_` if |
| * `site_` is valid. |
| */ |
| matchingRestrictedSites_: {type: Array}, |
| }; |
| } |
| |
| accessor itemId: string = ''; |
| accessor currentSite: string|null = null; |
| accessor updateHostAccess: boolean = false; |
| protected accessor site_: string = ''; |
| protected accessor inputInvalid_: boolean = false; |
| protected accessor matchingRestrictedSites_: string[] = []; |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| if (this.currentSite !== null && this.currentSite !== undefined) { |
| this.site_ = this.currentSite; |
| this.validate_(); |
| } |
| this.$.dialog.showModal(); |
| } |
| |
| override willUpdate(changedProperties: PropertyValues<this>) { |
| super.willUpdate(changedProperties); |
| |
| const changedPrivateProperties = |
| changedProperties as Map<PropertyKey, unknown>; |
| if (changedProperties.has('restrictedSites') || |
| changedPrivateProperties.has('site_')) { |
| this.matchingRestrictedSites_ = this.computeMatchingRestrictedSites_(); |
| } |
| } |
| |
| isOpen(): boolean { |
| return this.$.dialog.open; |
| } |
| |
| /** |
| * Validates that the pattern entered is valid. |
| */ |
| protected validate_() { |
| // If input is empty, disable the action button, but don't show the red |
| // invalid message. |
| if (this.site_.trim().length === 0) { |
| this.inputInvalid_ = false; |
| return; |
| } |
| |
| this.inputInvalid_ = !runtimeHostsPatternRegExp.test(this.site_); |
| } |
| |
| protected onSiteChanged_(e: CustomEvent<{value: string}>) { |
| this.site_ = e.detail.value; |
| } |
| |
| protected computeDialogTitle_(): string { |
| const stringId = this.currentSite === null ? 'runtimeHostsDialogTitle' : |
| 'hostPermissionsEdit'; |
| return loadTimeData.getString(stringId); |
| } |
| |
| protected computeSubmitButtonDisabled_(): boolean { |
| return this.inputInvalid_ || this.site_ === undefined || |
| this.site_.trim().length === 0; |
| } |
| |
| protected computeSubmitButtonLabel_(): string { |
| const stringId = this.currentSite === null ? 'add' : 'save'; |
| return loadTimeData.getString(stringId); |
| } |
| |
| private computeMatchingRestrictedSites_(): string[] { |
| return getMatchingUserSpecifiedSites(this.restrictedSites, this.site_); |
| } |
| |
| protected onCancelClick_() { |
| this.$.dialog.cancel(); |
| } |
| |
| /** |
| * The tap handler for the submit button (adds the pattern and closes |
| * the dialog). |
| */ |
| protected onSubmitClick_() { |
| chrome.metricsPrivate.recordUserAction( |
| 'Extensions.Settings.Hosts.AddHostDialogSubmitted'); |
| if (this.currentSite !== null) { |
| this.handleEdit_(); |
| } else { |
| this.handleAdd_(); |
| } |
| } |
| |
| /** |
| * Handles adding a new site entry. |
| */ |
| private handleAdd_() { |
| assert(!this.currentSite); |
| |
| if (this.updateHostAccess) { |
| this.delegate.setItemHostAccess( |
| this.itemId, chrome.developerPrivate.HostAccess.ON_SPECIFIC_SITES); |
| } |
| |
| this.addPermission_(); |
| } |
| |
| /** |
| * Handles editing an existing site entry. |
| */ |
| private handleEdit_() { |
| assert(this.currentSite); |
| assert( |
| !this.updateHostAccess, |
| 'Editing host permissions should only be possible if the host ' + |
| 'access is already set to specific sites.'); |
| |
| if (this.currentSite === this.site_) { |
| // No change in values, so no need to update anything. |
| this.$.dialog.close(); |
| return; |
| } |
| |
| // Editing an existing entry is done by removing the current site entry, |
| // and then adding the new one. |
| this.delegate.removeRuntimeHostPermission(this.itemId, this.currentSite) |
| .then(() => { |
| this.addPermission_(); |
| }); |
| } |
| |
| /** |
| * Adds the runtime host permission through the delegate. If successful, |
| * closes the dialog; otherwise displays the invalid input message. |
| */ |
| private addPermission_() { |
| const pattern = getPatternFromSite(this.site_); |
| const restrictedSites = this.matchingRestrictedSites_; |
| this.delegate.addRuntimeHostPermission(this.itemId, pattern) |
| .then( |
| () => { |
| if (restrictedSites.length) { |
| this.delegate.removeUserSpecifiedSites( |
| chrome.developerPrivate.SiteSet.USER_RESTRICTED, |
| restrictedSites); |
| } |
| this.$.dialog.close(); |
| }, |
| () => { |
| this.inputInvalid_ = true; |
| }); |
| } |
| |
| /** |
| * Returns a warning message containing the first restricted site that |
| * overlaps with `this.site_`, or an empty string if there are no matching |
| * restricted sites. |
| */ |
| protected computeMatchingRestrictedSitesWarning_(): string { |
| return this.matchingRestrictedSites_.length ? |
| this.i18n( |
| 'matchingRestrictedSitesWarning', |
| this.matchingRestrictedSites_[0]!, |
| ) : |
| ''; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'extensions-runtime-hosts-dialog': ExtensionsRuntimeHostsDialogElement; |
| } |
| } |
| |
| customElements.define( |
| ExtensionsRuntimeHostsDialogElement.is, |
| ExtensionsRuntimeHostsDialogElement); |