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

// Copyright 2015 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 network nameserver options.
 */

import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '//resources/ash/common/cr_elements/cr_input/cr_input.js';
import '//resources/ash/common/cr_elements/cr_radio_button/cr_radio_button.js';
import '//resources/ash/common/cr_elements/cr_radio_group/cr_radio_group.js';
import '//resources/ash/common/cr_elements/policy/cr_policy_indicator.js';
import '//resources/ash/common/cr_elements/md_select.css.js';
import './network_shared.css.js';

import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
import {ManagedProperties} from '//resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {IPConfigType} 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_nameservers.html.js';
import {OncMojo} from './onc_mojo.js';

/**
 * UI configuration options for nameservers.
 * @enum {string}
 */
const NameserversType = {
  AUTOMATIC: 'automatic',
  CUSTOM: 'custom',
  GOOGLE: 'google',
};

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

  behaviors: [I18nBehavior, CrPolicyNetworkBehaviorMojo],

  properties: {
    disabled: {
      type: Boolean,
      value: false,
    },

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

    /**
     * Array of nameserver addresses stored as strings.
     * @private {!Array<string>}
     */
    nameservers_: {
      type: Array,
      value() {
        return [];
      },
    },

    /**
     * The selected nameserver type.
     * @private {!NameserversType}
     */
    nameserversType_: {
      type: String,
      value: NameserversType.AUTOMATIC,
    },

    /**
     * Enum values for |nameserversType_|.
     * @private {NameserversType}
     */
    nameserversTypeEnum_: {
      readOnly: true,
      type: Object,
      value: NameserversType,
    },

    /** @private */
    googleNameserversText_: {
      type: String,
      value() {
        return this
            .i18nAdvanced(
                'networkNameserversGoogle', {substitutions: [], tags: ['a']})
            .toString();
      },
    },

    /** @private */
    canChangeConfigType_: {
      type: Boolean,
      computed: 'computeCanChangeConfigType_(managedProperties)',
    },
  },

  /** @const */
  GOOGLE_NAMESERVERS: [
    '8.8.4.4',
    '8.8.8.8',
  ],

  /** @const */
  EMPTY_NAMESERVER: '0.0.0.0',

  /** @const */
  MAX_NAMESERVERS: 4,

  /**
   * Saved nameservers from the NameserversType.CUSTOM tab. If this is empty, it
   * means that the user has not entered any custom nameservers yet.
   * @private {!Array<string>}
   */
  savedCustomNameservers_: [],

  /**
   * The last manually performed selection of the nameserver type. If this is
   * null, no explicit selection has been done for this network yet.
   * @private {?NameserversType}
   */
  savedNameserversType_: null,

  /*
   * Returns the nameserver type CrRadioGroupElement.
   * @return {?HTMLElement}
   */
  getNameserverRadioButtons() {
    return /** @type {?HTMLElement} */ (this.$$('#nameserverType'));
  },

  /**
   * Returns true if the nameservers in |nameservers1| match the nameservers in
   * |nameservers2|, ignoring order and empty / 0.0.0.0 entries.
   * @param {!Array<string>} nameservers1
   * @param {!Array<string>} nameservers2
   */
  nameserversMatch_(nameservers1, nameservers2) {
    const nonEmptySortedNameservers1 =
        this.clearEmptyNameServers_(nameservers1).sort();
    const nonEmptySortedNameservers2 =
        this.clearEmptyNameServers_(nameservers2).sort();
    if (nonEmptySortedNameservers1.length !==
        nonEmptySortedNameservers2.length) {
      return false;
    }
    for (let i = 0; i < nonEmptySortedNameservers1.length; i++) {
      if (nonEmptySortedNameservers1[i] !== nonEmptySortedNameservers2[i]) {
        return false;
      }
    }
    return true;
  },

  /**
   * Returns true if |nameservers| contains any all google nameserver entries
   * and only google nameserver entries or empty entries.
   * @param {!Array<string>} nameservers
   * @private
   */
  isGoogleNameservers_(nameservers) {
    return this.nameserversMatch_(nameservers, this.GOOGLE_NAMESERVERS);
  },

  /**
   * Returns the nameservers enforced by policy. If nameservers are not being
   * enforced, returns null.
   * @return {Array<string>|null}
   */
  getPolicyEnforcedNameservers_() {
    const staticIpConfig =
        this.managedProperties && this.managedProperties.staticIpConfig;
    if (!staticIpConfig || !staticIpConfig.nameServers) {
      return null;
    }
    return /** @type {Array<string>|null} */ (
        this.getEnforcedPolicyValue(staticIpConfig.nameServers));
  },

  /**
   * Returns the nameservers recommended by policy. If nameservers are not being
   * recommended, returns null. Note: also returns null if nameservers are being
   * enforced by policy.
   * @return {Array<string>|null}
   */
  getPolicyRecommendedNameservers_() {
    const staticIpConfig =
        this.managedProperties && this.managedProperties.staticIpConfig;
    if (!staticIpConfig || !staticIpConfig.nameServers) {
      return null;
    }
    return /** @type {Array<string>|null} */ (
        this.getRecommendedPolicyValue(staticIpConfig.nameServers));
  },

  /** @private */
  managedPropertiesChanged_(newValue, oldValue) {
    if (!this.managedProperties) {
      return;
    }

    if (!oldValue || newValue.guid !== oldValue.guid) {
      this.savedCustomNameservers_ = [];
      this.savedNameserversType_ = null;
    }

    // Update the 'nameservers' property.
    let nameservers = [];
    const ipv4 =
        OncMojo.getIPConfigForType(this.managedProperties, IPConfigType.kIPv4);
    if (ipv4 && ipv4.nameServers) {
      nameservers = ipv4.nameServers.slice();
    }

    // Update the 'nameserversType' property.
    const configType =
        OncMojo.getActiveValue(this.managedProperties.nameServersConfigType);
    /** @type {NameserversType} */ let type;
    if (configType === 'Static') {
      if (this.isGoogleNameservers_(nameservers) &&
          this.savedNameserversType_ !== NameserversType.CUSTOM) {
        type = NameserversType.GOOGLE;
        nameservers = this.GOOGLE_NAMESERVERS;  // Use consistent order.
      } else {
        type = NameserversType.CUSTOM;
      }
    } else {
      type = NameserversType.AUTOMATIC;
      nameservers = this.clearEmptyNameServers_(nameservers);
    }
    // When a network is connected, we receive connection strength updates and
    // that prevents users from making any custom updates to network
    // nameservers. These below conditions allow connection strength updates to
    // be applied only if network is not connected or if nameservers type is set
    // to auto or if we are receiving the update for the first time.
    if (type !== NameserversType.CUSTOM || !oldValue ||
        newValue.guid !== (oldValue && oldValue.guid) ||
        !OncMojo.connectionStateIsConnected(
            this.managedProperties.connectionState)) {
      this.setNameservers_(type, nameservers, false /* send */);
    }
  },

  /**
   * @param {!NameserversType} nameserversType
   * @param {!Array<string>} nameservers
   * @param {boolean} sendNameservers If true, send the nameservers once they
   *     have been set in the UI.
   * @private
   */
  setNameservers_(nameserversType, nameservers, sendNameservers) {
    if (nameserversType === NameserversType.CUSTOM) {
      // Add empty entries for unset custom nameservers.
      for (let i = nameservers.length; i < this.MAX_NAMESERVERS; ++i) {
        nameservers[i] = this.EMPTY_NAMESERVER;
      }
    } else {
      nameservers = this.clearEmptyNameServers_(nameservers);
    }
    this.nameservers_ = nameservers;
    this.nameserversType_ = nameserversType;
    if (sendNameservers) {
      this.sendNameServers_();
    }
  },

  /**
   * @param {!ManagedProperties} managedProperties
   * @return {boolean} True if the nameservers config type type can be changed.
   * @private
   */
  computeCanChangeConfigType_(managedProperties) {
    if (!managedProperties) {
      return false;
    }
    if (this.isNetworkPolicyEnforced(managedProperties.nameServersConfigType)) {
      return false;
    }
    return true;
  },

  /**
   * @param {string} nameserversType
   * @param {!ManagedProperties} managedProperties
   * @return {boolean} True if the nameservers are editable.
   * @private
   */
  canEditCustomNameServers_(nameserversType, managedProperties) {
    if (!managedProperties) {
      return false;
    }
    if (nameserversType !== NameserversType.CUSTOM) {
      return false;
    }
    if (this.isNetworkPolicyEnforced(managedProperties.nameServersConfigType)) {
      return false;
    }
    if (managedProperties.staticIpConfig &&
        managedProperties.staticIpConfig.nameServers &&
        this.isNetworkPolicyEnforced(
            managedProperties.staticIpConfig.nameServers)) {
      return false;
    }
    return true;
  },

  /**
   * @param {NameserversType} nameserversType
   * @param {NameserversType} type
   * @param {!Array<string>} nameservers
   * @return {boolean}
   * @private
   */
  showNameservers_(nameserversType, type, nameservers) {
    if (nameserversType !== type) {
      return false;
    }
    return type === NameserversType.CUSTOM || nameservers.length > 0;
  },

  /**
   * @param {!Array<string>} nameservers
   * @return {string}
   * @private
   */
  getNameserversString_(nameservers) {
    return nameservers.join(', ');
  },

  /**
   * Returns currently configured custom nameservers, to be used when toggling
   * to 'custom' from 'automatic' or 'google', prefer nameservers in the
   * following priority:
   *
   * 1) policy-enforced nameservers,
   * 2) previously manually entered nameservers (|savedCustomNameservers_|),
   * 3) policy-recommended nameservers,
   * 4) active nameservers (e.g. from DHCP).
   * @return {!Array<string>} nameservers
   * @private
   */
  getCustomNameServers_() {
    const policyEnforcedNameservers = this.getPolicyEnforcedNameservers_();
    if (policyEnforcedNameservers !== null) {
      return policyEnforcedNameservers.slice();
    }

    if (this.savedCustomNameservers_.length > 0) {
      return this.savedCustomNameservers_;
    }

    const policyRecommendedNameservers =
        this.getPolicyRecommendedNameservers_();
    if (policyRecommendedNameservers !== null) {
      return policyRecommendedNameservers.slice();
    }

    return this.nameservers_;
  },

  /**
   * Event triggered when the selected type changes. Updates nameservers and
   * sends the change value if necessary.
   * @private
   */
  onTypeChange_() {
    const type = this.$$('#nameserverType').selected;
    this.nameserversType_ = type;
    this.savedNameserversType_ = type;
    if (type === NameserversType.CUSTOM) {
      this.setNameservers_(type, this.getCustomNameServers_(), true /* send */);
      return;
    }
    this.sendNameServers_();
  },

  /**
   * Event triggered when a |nameservers_| value changes through the custom
   * namservers UI.
   * This gets called after data-binding updates a |nameservers_[i]| entry.
   * This saves the custom nameservers and reflects that change to the backend
   * (sending the custom nameservers).
   * @private
   */
  onValueChange_() {
    this.savedCustomNameservers_ = this.nameservers_.slice();
    this.sendNameServers_();
  },

  /**
   * Sends the current nameservers type (for automatic) or value.
   * @private
   */
  sendNameServers_() {
    const type = this.nameserversType_;

    if (type === NameserversType.CUSTOM) {
      this.fire('nameservers-change', {
        field: 'nameServers',
        value: this.nameservers_,
      });
    } else if (type === NameserversType.GOOGLE) {
      this.nameservers_ = this.GOOGLE_NAMESERVERS;
      this.fire('nameservers-change', {
        field: 'nameServers',
        value: this.GOOGLE_NAMESERVERS,
      });
    } else {  // type === NameserversType.AUTOMATIC
      // If not connected, properties will clear. Otherwise they may or may not
      // change so leave them as-is.
      if (!OncMojo.connectionStateIsConnected(
              this.managedProperties.connectionState)) {
        this.nameservers_ = [];
      } else {
        this.nameservers_ = this.clearEmptyNameServers_(this.nameservers_);
      }
      this.fire('nameservers-change', {
        field: 'nameServersConfigType',
        value: 'DHCP',
      });
    }
  },

  /**
   * @param {!Array<string>} nameservers
   * @return {!Array<string>}
   * @private
   */
  clearEmptyNameServers_(nameservers) {
    return nameservers.filter(
        (nameserver) => (!!nameserver && nameserver !== this.EMPTY_NAMESERVER));
  },

  /**
   * @param {!Event} event
   * @private
   */
  doNothing_(event) {
    event.stopPropagation();
  },

  /**
   * @param {number} index
   * @return {string} Accessibility label for nameserver input with given index.
   * @private
   */
  getCustomNameServerInputA11yLabel_(index) {
    return this.i18n('networkNameserversCustomInputA11yLabel', index + 1);
  },
});