chromium/ash/webui/common/resources/network/network_ip_config.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 the IP Config properties for
 * a network state.
 */

import '//resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import '//resources/ash/common/cr_elements/policy/cr_policy_indicator.js';
import './network_property_list_mojo.js';
import './network_shared.css.js';

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

/**
 * Returns the routing prefix as a string for a given prefix length. If
 * |prefixLength| is invalid, returns undefined.
 * @param {number} prefixLength The ONC routing prefix length.
 * @return {string|undefined}
 */
const getRoutingPrefixAsNetmask = function(prefixLength) {
  'use strict';
  // Return the empty string for invalid inputs.
  if (prefixLength <= 0 || prefixLength > 32) {
    return undefined;
  }
  let netmask = '';
  for (let i = 0; i < 4; ++i) {
    let remainder = 8;
    if (prefixLength >= 8) {
      prefixLength -= 8;
    } else {
      remainder = prefixLength;
      prefixLength = 0;
    }
    if (i > 0) {
      netmask += '.';
    }
    let value = 0;
    if (remainder !== 0) {
      value = ((2 << (remainder - 1)) - 1) << (8 - remainder);
    }
    netmask += value.toString();
  }
  return netmask;
};

/**
 * Returns the routing prefix length as a number from the netmask string.
 * @param {string|undefined} netmask The netmask string, e.g. 255.255.255.0.
 * @return {number} The corresponding netmask or NO_ROUTING_PREFIX if invalid.
 */
const getRoutingPrefixAsLength = function(netmask) {
  'use strict';
  if (!netmask) {
    return NO_ROUTING_PREFIX;
  }
  const tokens = netmask.split('.');
  if (tokens.length !== 4) {
    return NO_ROUTING_PREFIX;
  }
  let prefixLength = 0;
  for (let i = 0; i < tokens.length; ++i) {
    const token = tokens[i];
    // If we already found the last mask and the current one is not
    // '0' then the netmask is invalid. For example, 255.224.255.0
    if (prefixLength / 8 !== i) {
      if (token !== '0') {
        return NO_ROUTING_PREFIX;
      }
    } else if (token === '255') {
      prefixLength += 8;
    } else if (token === '254') {
      prefixLength += 7;
    } else if (token === '252') {
      prefixLength += 6;
    } else if (token === '248') {
      prefixLength += 5;
    } else if (token === '240') {
      prefixLength += 4;
    } else if (token === '224') {
      prefixLength += 3;
    } else if (token === '192') {
      prefixLength += 2;
    } else if (token === '128') {
      prefixLength += 1;
    } else if (token === '0') {
      prefixLength += 0;
    } else {
      // mask is not a valid number.
      return NO_ROUTING_PREFIX;
    }
  }
  return prefixLength;
};

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

  behaviors: [I18nBehavior, CrPolicyNetworkBehaviorMojo],

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

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

    /**
     * State of 'Configure IP Addresses Automatically'.
     * @private
     */
    automatic_: {
      type: Boolean,
      value: true,
    },

    /**
     * The currently visible IP Config property dictionary.
     * @private {{
     *   ipv4: (OncMojo.IPConfigUIProperties|undefined),
     *   ipv6: (OncMojo.IPConfigUIProperties|undefined)
     * }|undefined}
     */
    ipConfig_: Object,

    /**
     * Array of properties to pass to the property list.
     * @private {!Array<string>}
     */
    ipConfigFields_: {
      type: Array,
      value() {
        return [
          'ipv4.ipAddress',
          'ipv4.netmask',
          'ipv4.gateway',
          'ipv6.ipAddress',
        ];
      },
      readOnly: true,
    },

    /**
     * True if automatically-configured IP address toggle should be visible.
     * @private
     */
    shouldShowAutoIpConfigToggle_: {
      type: Boolean,
      value: true,
      computed: 'computeShouldShowAutoIpConfigToggle_(managedProperties)',
    },
  },

  /**
   * Returns the automatically configure IP CrToggleElement.
   * @return {?CrToggleElement}
   */
  getAutoConfigIpToggle() {
    return /** @type {?CrToggleElement} */ (this.$$('#autoConfigIpToggle'));
  },

  /**
   * Saved static IP configuration properties when switching to 'automatic'.
   * @private {!OncMojo.IPConfigUIProperties|undefined}
   */
  savedStaticIp_: undefined,

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

    const properties = this.managedProperties;
    if (newValue.guid !== (oldValue && oldValue.guid)) {
      this.savedStaticIp_ = undefined;
    }

    // Update the 'automatic' property.
    const ipConfigType = OncMojo.getActiveValue(properties.ipAddressConfigType);
    this.automatic_ = ipConfigType !== 'Static';

    if (properties.ipConfigs || properties.staticIpConfig) {
      if (this.automatic_ || !oldValue ||
          newValue.guid !== (oldValue && oldValue.guid) ||
          !OncMojo.connectionStateIsConnected(properties.connectionState)) {
        // Update the 'ipConfig' property.
        const ipv4 = this.getIPConfigUIProperties_(
            OncMojo.getIPConfigForType(properties, IPConfigType.kIPv4));
        let ipv6 = this.getIPConfigUIProperties_(
            OncMojo.getIPConfigForType(properties, IPConfigType.kIPv6));

        // If connected and the IP address is automatic and set, show message if
        // the ipv6 address is not set.
        if (OncMojo.connectionStateIsConnected(properties.connectionState) &&
            this.automatic_ && ipv4 && ipv4.ipAddress) {
          ipv6 = ipv6 || {type: IPConfigType.kIPv6};
          ipv6.ipAddress = ipv6.ipAddress || this.i18n('ipAddressNotAvailable');
        }
        this.ipConfig_ = {ipv4: ipv4, ipv6: ipv6};
      }
    } else {
      this.ipConfig_ = undefined;
    }
  },

  /**
   * Checks whether IP address config type can be changed.
   * @param {?ManagedProperties} managedProperties
   * @return {boolean}
   * @private
   */
  canChangeIPConfigType_(managedProperties) {
    if (this.disabled || !managedProperties) {
      return false;
    }
    if (managedProperties.type === NetworkType.kCellular) {
      // Cellular IP config properties can not be changed.
      return false;
    }
    const ipConfigType = managedProperties.ipAddressConfigType;
    return !ipConfigType || !this.isNetworkPolicyEnforced(ipConfigType);
  },

  /**
   * Overrides null values of this.ipConfig_.ipv4 with defaults so that
   * this.ipConfig_.ipv4 passes validation after being converted to ONC
   * StaticIPConfig.
   * TODO(https://crbug.com/1148841): Setting defaults here is strange, find
   * some better way.
   * @private
   */
  setIpv4Defaults_() {
    if (!this.ipConfig_ || !this.ipConfig_.ipv4) {
      return;
    }
    if (!this.ipConfig_.ipv4.gateway) {
      this.set('ipConfig_.ipv4.gateway', '192.168.1.1');
    }
    if (!this.ipConfig_.ipv4.ipAddress) {
      this.set('ipConfig_.ipv4.ipAddress', '192.168.1.1');
    }
    if (!this.ipConfig_.ipv4.netmask) {
      this.set('ipConfig_.ipv4.netmask', '255.255.255.0');
    }
  },

  /** @private */
  onAutomaticChange_() {
    if (!this.automatic_) {
      if (!this.ipConfig_) {
        this.ipConfig_ = {};
      }
      if (this.savedStaticIp_) {
        this.ipConfig_.ipv4 = this.savedStaticIp_;
      }
      if (!this.ipConfig_.ipv4) {
        this.ipConfig_.ipv4 = {
          type: IPConfigType.kIPv4,
        };
      }
      this.setIpv4Defaults_();
      this.sendStaticIpConfig_();
      return;
    }

    // Save the static IP configuration when switching to automatic.
    if (this.ipConfig_) {
      this.savedStaticIp_ = this.ipConfig_.ipv4;
    }
    this.fire('ip-change', {
      field: 'ipAddressConfigType',
      value: 'DHCP',
    });
  },

  /**
   * @param {!IPConfigProperties|undefined}
   *     ipconfig
   * @return {!OncMojo.IPConfigUIProperties|undefined} A new
   *     IPConfigUIProperties object with routingPrefix expressed as a netmask
   *     string instead of a prefix length. Returns undefined if |ipconfig|
   *     is not defined.
   * @private
   */
  getIPConfigUIProperties_(ipconfig) {
    if (!ipconfig) {
      return undefined;
    }

    // Copy |ipconfig| properties into |ipconfigUI|.
    const ipconfigUI = {};
    ipconfigUI.gateway = ipconfig.gateway;
    ipconfigUI.ipAddress = ipconfig.ipAddress;
    ipconfigUI.nameServers = ipconfig.nameServers;
    ipconfigUI.type = ipconfig.type;
    ipconfigUI.webProxyAutoDiscoveryUrl = ipconfig.webProxyAutoDiscoveryUrl;

    if (ipconfig.routingPrefix !== NO_ROUTING_PREFIX) {
      ipconfigUI.netmask = getRoutingPrefixAsNetmask(ipconfig.routingPrefix);
    }

    return ipconfigUI;
  },

  /**
   * @param {!OncMojo.IPConfigUIProperties} ipconfigUI
   * @return {!IPConfigProperties} A new
   *     IPConfigProperties object with netmask expressed as a a prefix
   *     length.
   * @private
   */
  getIPConfigProperties_(ipconfigUI) {
    const ipconfig = {};
    ipconfig.gateway = ipconfigUI.gateway;
    ipconfig.ipAddress = ipconfigUI.ipAddress;
    ipconfig.nameServers = ipconfigUI.nameServers;
    ipconfig.routingPrefix = getRoutingPrefixAsLength(ipconfigUI.netmask);
    ipconfig.type = ipconfigUI.type;
    ipconfig.webProxyAutoDiscoveryUrl = ipconfigUI.webProxyAutoDiscoveryUrl;

    return ipconfig;
  },

  /**
   * @return {boolean}
   * @private
   */
  hasIpConfigFields_() {
    if (!this.ipConfig_) {
      return false;
    }
    for (let i = 0; i < this.ipConfigFields_.length; ++i) {
      const key = this.ipConfigFields_[i];
      const value = this.get(key, this.ipConfig_);
      if (value !== undefined && value !== '') {
        return true;
      }
    }
    return false;
  },

  /**
   * @param {?OncMojo.ManagedProperty|undefined} property
   * @return {string|undefined} Edit type to be used in network-property-list.
   * @private
   */
  getIPFieldEditType_(property) {
    return this.isNetworkPolicyEnforced(property) ? undefined : 'String';
  },

  /**
   * @return {Object} An object with the edit type for each editable field.
   * @private
   */
  getIPEditFields_() {
    const staticIpConfig =
        this.managedProperties && this.managedProperties.staticIpConfig;
    if (this.automatic_ || !staticIpConfig) {
      return {};
    }
    return {
      'ipv4.ipAddress': this.getIPFieldEditType_(staticIpConfig.ipAddress),
      // Use routingPrefix instead of netmask because getIPFieldEditType_
      // expects a ManagedProperty and routingPrefix has the same type as
      // netmask.
      'ipv4.netmask': this.getIPFieldEditType_(staticIpConfig.routingPrefix),
      'ipv4.gateway': this.getIPFieldEditType_(staticIpConfig.gateway),
    };
  },

  /**
   * Event triggered when the network property list changes.
   * @param {!CustomEvent<!{field: string, value: string}>} event The
   *     network-property-list change event.
   * @private
   */
  onIPChange_(event) {
    if (!this.ipConfig_) {
      return;
    }
    const field = event.detail.field;
    const value = event.detail.value;
    // Note: |field| includes the 'ipv4.' prefix.
    this.set('ipConfig_.' + field, value);
    this.sendStaticIpConfig_();
  },

  /** @private */
  sendStaticIpConfig_() {
    // This will also set IPAddressConfigType to STATIC.
    this.fire('ip-change', {
      field: 'staticIpConfig',
      value: this.ipConfig_.ipv4 ?
          this.getIPConfigProperties_(this.ipConfig_.ipv4) :
          {},
    });
  },

  /**
   * @return {boolean}
   * @private
   */
  computeShouldShowAutoIpConfigToggle_() {
    if (this.managedProperties.type === NetworkType.kCellular) {
      return false;
    }
    return true;
  },

  /**
   * @return {string}
   * @private
   */
  getFieldsClassList_() {
    let classes = 'property-box single-column stretch';
    if (this.shouldShowAutoIpConfigToggle_) {
      classes += ' indented';
    }
    return classes;
  },
});