chromium/ash/webui/common/resources/network/network_proxy.js

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @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 {assert} from '//resources/ash/common/assert.js';
import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
import {ManagedManualProxySettings, ManagedProperties, ManagedProxyLocation, ManagedProxySettings, ManagedStringList, ProxyLocation, ProxySettings} from '//resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {IPConfigType, OncSource, PolicySource} from '//resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {CrPolicyNetworkBehaviorMojo} from './cr_policy_network_behavior_mojo.js';
import {getTemplate} from './network_proxy.html.js';
import {OncMojo} from './onc_mojo.js';

Polymer({
  _template: getTemplate(),
  is: 'network-proxy',

  behaviors: [
    CrPolicyNetworkBehaviorMojo,
    I18nBehavior,
  ],

  properties: {
    /** Whether or not the proxy values can be edited. */
    editable: {
      type: Boolean,
      value: false,
    },

    /** @type {!ManagedProperties|undefined} */
    managedProperties: {
      type: Object,
      observer: 'managedPropertiesChanged_',
    },

    /** Whether shared proxies are allowed. */
    useSharedProxies: {
      type: Boolean,
      value: false,
      observer: 'updateProxy_',
    },

    /**
     * UI visible / edited proxy configuration.
     * @private {!ManagedProxySettings}
     */
    proxy_: {
      type: Object,
      value() {
        return this.createDefaultProxySettings_();
      },
    },

    /**
     * The Web Proxy Auto Discovery URL extracted from managedProperties.
     * @private
     */
    wpad_: {
      type: String,
      value: '',
    },

    /**
     * Whether or not to use the same manual proxy for all protocols.
     * @private
     */
    useSameProxy_: {
      type: Boolean,
      value: false,
      observer: 'useSameProxyChanged_',
    },

    /**
     * Array of proxy configuration types.
     * @private {!Array<string>}
     * @const
     */
    proxyTypes_: {
      type: Array,
      value: ['Direct', 'PAC', 'WPAD', 'Manual'],
      readOnly: true,
    },

    /**
     * The current value of the proxy exclusion input.
     * @private
     */
    proxyExclusionInputValue_: {
      type: String,
      value: '',
    },
  },

  /**
   * Saved Manual properties so that switching to another type does not loose
   * any set properties while the UI is open.
   * @private {!ManagedManualProxySettings|
   *           undefined}
   */
  savedManual_: undefined,

  /**
   * Saved ExcludeDomains properties so that switching to a non-Manual type does
   * not loose any set exclusions while the UI is open.
   * @private {!ManagedStringList|undefined}
   */
  savedExcludeDomains_: undefined,

  /**
   * Set to true while modifying proxy values so that an update does not
   * override the edited values.
   * @private {boolean}
   */
  proxyIsUserModified_: false,

  /** @override */
  attached() {
    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_();
  },

  /**
   * @param {!ManagedProperties|undefined} newValue
   * @param {!ManagedProperties|undefined} oldValue
   * @private
   */
  managedPropertiesChanged_(newValue, oldValue) {
    if ((newValue && newValue.guid) !== (oldValue && oldValue.guid)) {
      // Clear saved manual properties and exclude domains if we're updating
      // to show a different network.
      this.savedManual_ = undefined;
      this.savedExcludeDomains_ = undefined;
    }

    if (this.proxyIsUserModified_ || this.isInputEditInProgress_()) {
      // Ignore updates if any fields have been modified by user or if any
      // input elements are currently being edited.
      return;
    }
    this.updateProxy_();
  },

  /**
   * @return {boolean} True if any input elements are currently being edited.
   * @private
   */
  isInputEditInProgress_: function() {
    if (!this.editable) {
      return false;
    }
    const activeElement = this.shadowRoot.activeElement;
    if (!activeElement) {
      return false;
    }

    // Find property name for current active element.
    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;
    }

    // Input should be considered active only when the property editable.
    return this.isEditable_(property);
  },

  /**
   * @param {?ManagedProxyLocation|undefined} a
   * @param {?ManagedProxyLocation|undefined} b
   * @return {boolean}
   * @private
   */
  proxyMatches_(a, b) {
    return !!a && !!b && a.host.activeValue === b.host.activeValue &&
        a.port.activeValue === b.port.activeValue;
  },

  /**
   * @param {number} port
   * @return {!ManagedProxyLocation}
   * @private
   */
  createDefaultProxyLocation_(port) {
    return {
      host: OncMojo.createManagedString(''),
      port: OncMojo.createManagedInt(port),
    };
  },

  /**
   * Returns a copy of |inputProxy| with all required properties set correctly.
   * @param {!ManagedProxySettings} inputProxy
   * @return {!ManagedProxySettings}
   * @private
   */
  validateProxy_(inputProxy) {
    const proxy =
        /** @type {!ManagedProxySettings} */ (Object.assign({}, 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_ || {};
      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,
          };
    }
    return proxy;
  },

  /** @private */
  updateProxy_() {
    if (!this.managedProperties) {
      return;
    }

    let proxySettings = this.managedProperties.proxySettings;

    // For shared networks with unmanaged proxy settings, ignore any saved proxy
    // settings and use the default value.
    if (this.isShared_() && proxySettings &&
        !this.isControlled(proxySettings.type) && !this.useSharedProxies) {
      proxySettings = null;  // Ignore proxy settings.
    }

    const proxy = proxySettings ? this.validateProxy_(proxySettings) :
                                  this.createDefaultProxySettings_();

    if (proxy.type.activeValue === 'WPAD') {
      // Set the Web Proxy Auto Discovery URL for display purposes.
      const ipv4 = this.managedProperties ?
          OncMojo.getIPConfigForType(
              this.managedProperties, IPConfigType.kIPv4) :
          null;
      this.wpad_ = (ipv4 && ipv4.webProxyAutoDiscoveryUrl) ||
          this.i18n('networkProxyWpadNone');
    }

    // Set this.proxy_ after dom-repeat has been stamped.
    this.async(() => this.setProxy_(proxy));
  },

  /**
   * @param {!ManagedProxySettings} proxy
   * @private
   */
  setProxy_(proxy) {
    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)) {
        // If all four proxies match, enable the 'use same proxy' toggle.
        this.useSameProxy_ = true;
      } else if (
          !manual.secureHttpProxy.host.activeValue &&
          !manual.socks.host.activeValue) {
        // Otherwise if no proxies other than http have a host value, also
        // enable the 'use same proxy' toggle.
        this.useSameProxy_ = true;
      }
    }
    this.proxyIsUserModified_ = false;
  },

  /** @private */
  useSameProxyChanged_() {
    this.proxyIsUserModified_ = true;
  },

  /**
   * @return {!ManagedProxySettings}
   * @private
   */
  createDefaultProxySettings_() {
    return {
      type: OncMojo.createManagedString('Direct'),
    };
  },

  /**
   * @param {?ManagedProxyLocation|undefined}
   *     location
   * @return {!ProxyLocation|undefined}
   * @private
   */
  getProxyLocation_(location) {
    if (!location) {
      return undefined;
    }
    return {
      host: location.host.activeValue,
      port: location.port.activeValue,
    };
  },

  /**
   * Called when the proxy changes in the UI.
   * @private
   */
  sendProxyChange_() {
    const proxyType = OncMojo.getActiveString(this.proxy_.type);
    if (!proxyType || (proxyType === 'PAC' && !this.proxy_.pac)) {
      return;
    }

    const proxy = /** @type {!ProxySettings} */ ({
      type: proxyType,
      excludeDomains: OncMojo.getActiveValue(this.proxy_.excludeDomains),
    });

    if (proxyType === 'Manual') {
      let manual = {};
      if (this.proxy_.manual) {
        this.savedManual_ =
            /** @type{!ManagedManualProxySettings}*/ (
                Object.assign({}, this.proxy_.manual));
        manual = {
          httpProxy: this.getProxyLocation_(this.proxy_.manual.httpProxy),
          secureHttpProxy:
              this.getProxyLocation_(this.proxy_.manual.secureHttpProxy),
          socks: this.getProxyLocation_(this.proxy_.manual.socks),
        };
      }
      if (this.proxy_.excludeDomains) {
        this.savedExcludeDomains_ =
            /** @type{!ManagedStringList}*/ (
                Object.assign({}, this.proxy_.excludeDomains));
      }
      const defaultProxy = manual.httpProxy || {host: '', port: 80};
      if (this.useSameProxy_) {
        manual.secureHttpProxy =
            /** @type {!ProxyLocation} */ (Object.assign({}, defaultProxy));
        manual.socks =
            /** @type {!ProxyLocation} */ (Object.assign({}, defaultProxy));
      } else {
        // Remove properties with empty hosts to unset them.
        if (manual.httpProxy && !manual.httpProxy.host) {
          delete manual.httpProxy;
        }
        if (manual.secureHttpProxy && !manual.secureHttpProxy.host) {
          delete manual.secureHttpProxy;
        }
        if (manual.socks && !manual.socks.host) {
          delete manual.socks;
        }
      }
      proxy.manual = manual;
    } else if (proxyType === 'PAC') {
      proxy.pac = OncMojo.getActiveString(this.proxy_.pac);
    }
    this.fire('proxy-change', proxy);
    this.proxyIsUserModified_ = false;
  },

  /**
   * Event triggered when the selected proxy type changes.
   * @param {!Event} event
   * @private
   */
  onTypeChange_(event) {
    if (!this.proxy_ || !this.proxy_.type) {
      return;
    }
    const target = /** @type {!HTMLSelectElement} */ (event.target);
    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':
        // No addtional values are required, send the type change.
        proxyTypeChangeIsReady = true;
        break;
      case 'PAC':
        elementToFocus = this.$$('#pacInput');
        // If a PAC is already set, send the type change now, otherwise wait
        // until the user provides a PAC value.
        proxyTypeChangeIsReady = !!OncMojo.getActiveString(this.proxy_.pac);
        break;
      case 'Manual':
        // Manual proxy configuration includes multiple input fields, so wait
        // until the 'send' button is clicked.
        proxyTypeChangeIsReady = false;
        elementToFocus = this.$$('#manualProxy network-proxy-input');
        break;
    }

    // If the new proxy type is fully configured, send it, otherwise set
    // |proxyIsUserModified_| to true so that property updates do not
    // overwrite user changes.
    if (proxyTypeChangeIsReady) {
      this.sendProxyChange_();
    } else {
      this.proxyIsUserModified_ = true;
    }

    if (elementToFocus) {
      this.async(() => {
        elementToFocus.focus();
      });
    }
  },

  /** @private */
  onPACChange_() {
    this.sendProxyChange_();
  },

  /** @private */
  onProxyInputChange_() {
    this.proxyIsUserModified_ = true;
  },

  /** @private */
  onAddProxyExclusionTap_() {
    assert(this.proxyExclusionInputValue_);
    this.push(
        'proxy_.excludeDomains.activeValue', this.proxyExclusionInputValue_);
    // Clear input.
    this.proxyExclusionInputValue_ = '';
    this.proxyIsUserModified_ = true;
  },

  /**
   * @param {!Event} event
   * @private
   */
  onAddProxyExclusionKeypress_(event) {
    if (event.key !== 'Enter') {
      return;
    }
    event.stopPropagation();
    this.onAddProxyExclusionTap_();
  },

  /**
   * @param {string} proxyExclusionInputValue
   * @return {boolean}
   * @private
   */
  shouldProxyExclusionButtonBeDisabled_(proxyExclusionInputValue) {
    return !proxyExclusionInputValue;
  },

  /**
   * Event triggered when the proxy exclusion list changes.
   * @param {!Event} event The remove proxy exclusions change event.
   * @private
   */
  onProxyExclusionsChange_(event) {
    this.proxyIsUserModified_ = true;
  },

  /** @private */
  onSaveProxyTap_() {
    this.sendProxyChange_();
  },

  /**
   * @param {string} proxyType The proxy type.
   * @return {string} The description for |proxyType|.
   * @private
   */
  getProxyTypeDesc_(proxyType) {
    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');
  },

  /**
   * @param {string} propertyName
   * @return {boolean} Whether the named property setting is editable.
   * @private
   */
  isEditable_(propertyName) {
    if (!this.editable || (this.isShared_() && !this.useSharedProxies)) {
      return false;
    }
    const property = /** @type {!OncMojo.ManagedProperty|undefined} */ (
        this.get('proxySettings.' + propertyName, this.managedProperties));
    if (!property) {
      return true;  // Default to editable if property is not defined.
    }
    return this.isPropertyEditable_(property);
  },

  /**
   * @param {!OncMojo.ManagedProperty|undefined} property
   * @return {boolean} Whether |property| is editable.
   * @private
   */
  isPropertyEditable_(property) {
    return !!property && !this.isNetworkPolicyEnforced(property) &&
        !this.isExtensionControlled(property);
  },

  /**
   * @return {boolean}
   * @private
   */
  isShared_() {
    if (!this.managedProperties) {
      return false;
    }
    const source = this.managedProperties.source;
    return source === OncSource.kDevice || source === OncSource.kDevicePolicy;
  },

  /**
   * @return {boolean}
   * @private
   */
  isSaveManualProxyEnabled_() {
    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);
  },

  /**
   * @param {string} property The property to test
   * @param {string} value The value to test against
   * @return {boolean} True if property === value
   * @private
   */
  matches_(property, value) {
    return property === value;
  },
});