* @fileoverview Polymer element for displaying and editing network proxy
* values.
*/
import '//resources/ash/common/cr_elements/cr_button/cr_button.js';
import '//resources/ash/common/cr_elements/cr_input/cr_input.js';
import '//resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import '//resources/ash/common/cr_elements/cr_hidden_style.css.js';
import '//resources/ash/common/cr_elements/md_select.css.js';
import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import './network_proxy_exclusions.js';
import './network_proxy_input.js';
import './network_shared.css.js';
import type {I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import type {ManagedManualProxySettings, ManagedProperties, ManagedProxyLocation, ManagedProxySettings, ManagedStringList, ManualProxySettings, ProxyLocation, ProxySettings} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {IPConfigType, OncSource, PolicySource} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {microTask, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {CrPolicyNetworkBehaviorMojoInterface} from './cr_policy_network_behavior_mojo.js';
import {CrPolicyNetworkBehaviorMojo} from './cr_policy_network_behavior_mojo.js';
import {getTemplate} from './network_proxy.html.js';
import {OncMojo} from './onc_mojo.js';
function createDefaultProxySettings(): ManagedProxySettings {
return {
type: OncMojo.createManagedString('Direct'),
manual: null,
excludeDomains: null,
pac: null,
};
}
type Constructor<T> = new (...args: any[]) => T;
const NetworkProxyElementBase =
mixinBehaviors([CrPolicyNetworkBehaviorMojo], I18nMixin(PolymerElement)) as
Constructor<PolymerElement&I18nMixinInterface&
CrPolicyNetworkBehaviorMojoInterface>;
export class NetworkProxyElement extends NetworkProxyElementBase {
static get is() {
return 'network-proxy' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
editable: {
type: Boolean,
value: false,
},
managedProperties: {
type: Object,
observer: 'managedPropertiesChanged_',
},
useSharedProxies: {
type: Boolean,
value: false,
observer: 'updateProxy_',
},
* UI visible / edited proxy configuration.
*/
proxy_: {
type: Object,
value() {
return createDefaultProxySettings();
},
},
* The Web Proxy Auto Discovery URL extracted from managedProperties.
*/
wpad_: {
type: String,
value: '',
},
* Whether or not to use the same manual proxy for all protocols.
*/
useSameProxy_: {
type: Boolean,
value: false,
observer: 'useSameProxyChanged_',
},
* Array of proxy configuration types.
*/
proxyTypes_: {
type: Array,
value: ['Direct', 'PAC', 'WPAD', 'Manual'],
readOnly: true,
},
* The current value of the proxy exclusion input.
*/
proxyExclusionInputValue_: {
type: String,
value: '',
},
* Set to true while modifying proxy values so that an update does not
* override the edited values.
*/
proxyIsUserModified_: {
type: Boolean,
value: false,
},
};
}
editable: boolean;
managedProperties: ManagedProperties|undefined;
useSharedProxies: boolean;
private proxy_: ManagedProxySettings;
private wpad_: string;
private useSameProxy_: boolean;
private proxyTypes_: [];
private proxyExclusionInputValue_: string;
private proxyIsUserModified_: boolean;
* Saved ExcludeDomains properties so that switching to a non-Manual type
* does not loose any set exclusions while the UI is open.
*/
private savedManual_: ManagedManualProxySettings|undefined = undefined;
* Saved Manual properties so that switching to another type does not loose
* any set properties while the UI is open.
*/
private savedExcludeDomains_: ManagedStringList|undefined = undefined;
override connectedCallback() {
super.connectedCallback();
this.reset();
}
* Called any time the page is refreshed or navigated to so that the proxy
* is updated correctly.
*/
reset() {
this.proxyIsUserModified_ = false;
this.updateProxy_();
}
private managedPropertiesChanged_(
newValue: ManagedProperties|undefined,
oldValue: ManagedProperties|undefined) {
if ((newValue && newValue.guid) !== (oldValue && oldValue.guid)) {
this.savedManual_ = undefined;
this.savedExcludeDomains_ = undefined;
}
if (this.proxyIsUserModified_ || this.isInputEditInProgress_()) {
return;
}
this.updateProxy_();
}
private isInputEditInProgress_(): boolean {
if (!this.editable) {
return false;
}
const activeElement = this.shadowRoot!.activeElement;
if (!activeElement) {
return false;
}
let property = null;
switch (activeElement.id) {
case 'sameProxyInput':
case 'httpProxyInput':
property = 'manual.httpProxy.host';
break;
case 'secureHttpProxyInput':
property = 'manual.secureHttpProxy.host';
break;
case 'socksProxyInput':
property = 'manual.socks.host';
break;
case 'pacInput':
property = 'pac';
break;
}
if (!property) {
return false;
}
return this.isEditable_(property);
}
private proxyMatches_(
a: ManagedProxyLocation|undefined|null,
b: ManagedProxyLocation|undefined|null): boolean {
return !!a && !!b && a.host.activeValue === b.host.activeValue &&
a.port.activeValue === b.port.activeValue;
}
private createDefaultProxyLocation_(port: number): ManagedProxyLocation {
return {
host: OncMojo.createManagedString(''),
port: OncMojo.createManagedInt(port),
};
}
* Returns a copy of |inputProxy| with all required properties set correctly.
*/
private validateProxy_(inputProxy: ManagedProxySettings):
ManagedProxySettings {
const proxy = {...inputProxy};
const type = proxy.type.activeValue;
if (type === 'PAC') {
if (!proxy.pac) {
proxy.pac = OncMojo.createManagedString('');
}
} else if (type === 'Manual') {
proxy.manual = proxy.manual || this.savedManual_ || {
httpProxy: null,
secureHttpProxy: null,
ftpProxy: null,
socks: null,
};
assert(proxy.manual);
if (!proxy.manual.httpProxy) {
proxy.manual.httpProxy = this.createDefaultProxyLocation_(80);
}
if (!proxy.manual.secureHttpProxy) {
proxy.manual.secureHttpProxy = this.createDefaultProxyLocation_(80);
}
if (!proxy.manual.socks) {
proxy.manual.socks = this.createDefaultProxyLocation_(1080);
}
proxy.excludeDomains =
proxy.excludeDomains || this.savedExcludeDomains_ || {
activeValue: [],
policySource: PolicySource.kNone,
policyValue: null,
};
}
return proxy;
}
private updateProxy_(): void {
if (!this.managedProperties) {
return;
}
let proxySettings = this.managedProperties.proxySettings;
if (this.isShared_() && proxySettings &&
!this.isControlled(proxySettings.type) && !this.useSharedProxies) {
proxySettings = null;
}
const proxy = proxySettings ? this.validateProxy_(proxySettings) :
createDefaultProxySettings();
if (proxy.type.activeValue === 'WPAD') {
const ipv4 = this.managedProperties ?
OncMojo.getIPConfigForType(
this.managedProperties, IPConfigType.kIPv4) :
null;
this.wpad_ = (ipv4 && ipv4.webProxyAutoDiscoveryUrl) ||
this.i18n('networkProxyWpadNone');
}
microTask.run(() => this.setProxy_(proxy));
}
private setProxy_(proxy: ManagedProxySettings): void {
this.proxy_ = proxy;
if (proxy.manual) {
const manual = proxy.manual;
const httpProxy = manual.httpProxy;
if (this.proxyMatches_(httpProxy, manual.secureHttpProxy) &&
this.proxyMatches_(httpProxy, manual.socks)) {
this.useSameProxy_ = true;
} else if (
!manual.secureHttpProxy?.host?.activeValue &&
!manual.socks?.host?.activeValue) {
this.useSameProxy_ = true;
}
}
this.proxyIsUserModified_ = false;
}
private useSameProxyChanged_(): void {
this.proxyIsUserModified_ = true;
}
private getProxyLocation_(location: ManagedProxyLocation|undefined|
null): ProxyLocation|null {
if (!location) {
return null;
}
return {
host: location.host.activeValue,
port: location.port.activeValue,
};
}
* Called when the proxy changes in the UI.
*/
private sendProxyChange_(): void {
const proxyType = OncMojo.getActiveString(this.proxy_.type);
if (!proxyType || (proxyType === 'PAC' && !this.proxy_.pac)) {
return;
}
const proxy: ProxySettings = {
type: proxyType,
excludeDomains: OncMojo.getActiveValue(this.proxy_.excludeDomains) as
string[] ||
null,
manual: null,
pac: null,
};
if (proxyType === 'Manual') {
let manual: ManualProxySettings = {
httpProxy: null,
secureHttpProxy: null,
ftpProxy: null,
socks: null,
};
if (this.proxy_.manual) {
this.savedManual_ = {...this.proxy_.manual};
manual = {
httpProxy: this.getProxyLocation_(this.proxy_.manual.httpProxy),
secureHttpProxy:
this.getProxyLocation_(this.proxy_.manual.secureHttpProxy),
ftpProxy: null,
socks: this.getProxyLocation_(this.proxy_.manual.socks),
};
}
if (this.proxy_.excludeDomains) {
this.savedExcludeDomains_ = {...this.proxy_.excludeDomains};
}
const defaultProxy = manual.httpProxy || {host: '', port: 80};
if (this.useSameProxy_) {
manual.secureHttpProxy = {...defaultProxy};
manual.socks = {...defaultProxy};
} else {
if (manual.httpProxy && !manual.httpProxy.host) {
manual.httpProxy = null;
}
if (manual.secureHttpProxy && !manual.secureHttpProxy.host) {
manual.secureHttpProxy = null;
}
if (manual.socks && !manual.socks.host) {
manual.socks = null;
}
}
proxy.manual = manual;
} else if (proxyType === 'PAC') {
proxy.pac = OncMojo.getActiveString(this.proxy_.pac);
}
this.dispatchEvent(new CustomEvent('proxy-change', {
bubbles: true,
composed: true,
detail: proxy,
}));
this.proxyIsUserModified_ = false;
}
* Event triggered when the selected proxy type changes.
*/
private onTypeChange_(event: Event): void {
if (!this.proxy_ || !this.proxy_.type) {
return;
}
const target = event.target as HTMLSelectElement;
const type = target.value;
this.proxy_.type.activeValue = type;
this.set('proxy_', this.validateProxy_(this.proxy_));
let proxyTypeChangeIsReady;
let elementToFocus;
switch (type) {
case 'Direct':
case 'WPAD':
proxyTypeChangeIsReady = true;
break;
case 'PAC':
elementToFocus = this.shadowRoot!.querySelector('#pacInput');
proxyTypeChangeIsReady = !!OncMojo.getActiveString(this.proxy_.pac);
break;
case 'Manual':
proxyTypeChangeIsReady = false;
elementToFocus =
this.shadowRoot!.querySelector('#manualProxy network-proxy-input');
break;
}
if (proxyTypeChangeIsReady) {
this.sendProxyChange_();
} else {
this.proxyIsUserModified_ = true;
}
if (elementToFocus) {
microTask.run(() => elementToFocus.focus());
}
}
private onPacChange_(): void {
this.sendProxyChange_();
}
private onProxyInputChange_(): void {
this.proxyIsUserModified_ = true;
}
private onAddProxyExclusionClicked_(): void {
assert(this.proxyExclusionInputValue_);
this.push(
'proxy_.excludeDomains.activeValue', this.proxyExclusionInputValue_);
this.proxyExclusionInputValue_ = '';
this.proxyIsUserModified_ = true;
}
private onAddProxyExclusionKeypress_(event: KeyboardEvent): void {
if (event.key !== 'Enter') {
return;
}
event.stopPropagation();
this.onAddProxyExclusionClicked_();
}
private shouldProxyExclusionButtonBeDisabled_(proxyExclusionInputValue:
string): boolean {
return !proxyExclusionInputValue;
}
* Event triggered when the proxy exclusion list changes.
*/
private onProxyExclusionsChange_(): void {
this.proxyIsUserModified_ = true;
}
private onSaveProxyClicked_(): void {
this.sendProxyChange_();
}
private getProxyTypeDesc_(proxyType: string): string {
if (proxyType === 'Manual') {
return this.i18n('networkProxyTypeManual');
}
if (proxyType === 'PAC') {
return this.i18n('networkProxyTypePac');
}
if (proxyType === 'WPAD') {
return this.i18n('networkProxyTypeWpad');
}
return this.i18n('networkProxyTypeDirect');
}
private isEditable_(propertyName: string): boolean {
if (!this.editable || (this.isShared_() && !this.useSharedProxies)) {
return false;
}
const property =
this.get('proxySettings.' + propertyName, this.managedProperties);
if (!property) {
return true;
}
return this.isPropertyEditable_(property);
}
private isPropertyEditable_(property: OncMojo.ManagedProperty|
undefined): boolean {
return !!property && !this.isNetworkPolicyEnforced(property) &&
!this.isExtensionControlled(property);
}
private isShared_(): boolean {
if (!this.managedProperties) {
return false;
}
const source = this.managedProperties.source;
return source === OncSource.kDevice || source === OncSource.kDevicePolicy;
}
private isSaveManualProxyEnabled_(): boolean {
if (!this.proxyIsUserModified_) {
return false;
}
const manual = this.proxy_.manual;
const httpHost = this.get('httpProxy.host.activeValue', manual);
if (this.useSameProxy_) {
return !!httpHost;
}
return !!httpHost ||
!!this.get('secureHttpProxy.host.activeValue', manual) ||
!!this.get('socks.host.activeValue', manual);
}
private matches_(property: string, value: string): boolean {
return property === value;
}
}
declare global {
interface HTMLElementTagNameMap {
[NetworkProxyElement.is]: NetworkProxyElement;
}
}
customElements.define(NetworkProxyElement.is, NetworkProxyElement);