chromium/chrome/browser/resources/ash/settings/internet_page/passpoint_subpage.ts

// Copyright 2023 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 details of a Passpoint
 * subscription.
 */

import '../settings_shared.css.js';

import {MojoConnectivityProvider} from 'chrome://resources/ash/common/connectivity/mojo_connectivity_provider.js';
import {PasspointServiceInterface, PasspointSubscription} from 'chrome://resources/ash/common/connectivity/passpoint.mojom-webui.js';
import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js';
import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
import {App, AppType, PageHandlerInterface} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {BrowserProxy as AppManagementComponentBrowserProxy} from 'chrome://resources/cr_components/app_management/browser_proxy.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {CrosNetworkConfigInterface, FilterType, NetworkCertificate, NetworkStateProperties, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {DomRepeatEvent, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {castExists} from '../assert_extras.js';
import {RouteObserverMixin} from '../common/route_observer_mixin.js';
import {Route, Router, routes} from '../router.js';

import {PasspointListenerMixin} from './passpoint_listener_mixin.js';
import {getTemplate} from './passpoint_subpage.html.js';

export class SettingsPasspointSubpageElement extends PasspointListenerMixin
(RouteObserverMixin(I18nMixin(PolymerElement))) {
  static get is() {
    return 'settings-passpoint-subpage' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /** The identifier of the subscription for which details are shown. */
      id_: String,

      /** Passpoint subscription currently displayed. */
      subscription_: Object,

      /** ARC application that provided the subscription. */
      app_: Object,

      /** List of Certificate Authorities available. */
      certs_: Array,

      /** Certificate authority common name. */
      certificateAuthorityName_: {
        type: String,
        computed: 'getCertificateAuthorityName_(certs_)',
      },

      /** Name of the provider of the subscription. */
      providerName_: {
        type: String,
        computed: 'getProviderName_(subscription_, app_)',
      },

      /** List of networks populated with the subscription. */
      networks_: {
        type: Array,
        value() {
          return [];
        },
      },

      /** Tell if the forget dialog should be displayed. */
      showForgetDialog_: Boolean,

      domainsExpanded_: Boolean,
    };
  }

  private app_: App|null;
  private appHandler_: PageHandlerInterface;
  private certs_: NetworkCertificate[];
  private certificateAuthorityName_: string;
  private domainsExpanded_: boolean;
  private id_: string;
  private networkConfig_: CrosNetworkConfigInterface;
  private networks_: NetworkStateProperties[];
  private passpointService_: PasspointServiceInterface;
  private providerName_: string;
  private showForgetDialog_: boolean;
  private subscription_: PasspointSubscription|null;

  constructor() {
    super();
    this.networkConfig_ =
        MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
    this.passpointService_ =
        MojoConnectivityProvider.getInstance().getPasspointService();
    this.appHandler_ = AppManagementComponentBrowserProxy.getInstance().handler;
  }

  close(): void {
    // If the page is already closed, return early to avoid navigating backward
    // erroneously.
    if (!this.id_) {
      return;
    }

    this.id_ = '';
    Router.getInstance().navigateToPreviousRoute();
  }


  /**
   * RouteObserverMixin override
   */
  override currentRouteChanged(route: Route): void {
    if (route !== routes.PASSPOINT_DETAIL) {
      return;
    }

    const queryParams = Router.getInstance().getQueryParameters();
    const id = queryParams.get('id') || '';
    if (!id) {
      console.warn('No Passpoint subscription ID specified for page:' + route);
      this.close();
      return;
    }
    this.id_ = id;
    this.refresh_();
  }

  private async refresh_(): Promise<void> {
    const response =
        await this.passpointService_.getPasspointSubscription(this.id_);
    if (!response.result) {
      console.warn('No subscription found for id ' + this.id_);
      this.close();
      return;
    }
    this.subscription_ = response.result;
    this.refreshCertificates_();
    this.refreshApp_(this.subscription_);
    this.refreshNetworks_(this.subscription_);
  }

  private async refreshCertificates_(): Promise<void> {
    const certs = await this.networkConfig_.getNetworkCertificates();
    this.certs_ = certs.serverCas;
  }

  private async refreshApp_(subscription: PasspointSubscription):
      Promise<void> {
    const response = await this.appHandler_.getApps();
    for (const app of response.apps) {
      if (app.type === AppType.kArc &&
          app.publisherId === subscription.provisioningSource) {
        this.app_ = app;
        return;
      }
    }
  }

  private async refreshNetworks_(subscription: PasspointSubscription):
      Promise<void> {
    const filter = {
      filter: FilterType.kConfigured,
      limit: NO_LIMIT,
      networkType: NetworkType.kWiFi,
    };
    const response = await this.networkConfig_.getNetworkStateList(filter);
    this.networks_ = response.result.filter(network => {
      return network.typeState!.wifi!.passpointId === subscription.id;
    });
  }

  private getCertificateAuthorityName_(): string {
    for (const cert of this.certs_) {
      if (cert.pemOrId === this.subscription_!.trustedCa) {
        return cert.issuedTo;
      }
    }
    return this.i18n('passpointSystemCALabel');
  }

  private hasExpirationDate_(): boolean {
    return this.subscription_!.expirationEpochMs > 0n;
  }

  private getExpirationDate_(subscription: PasspointSubscription): string {
    const date = new Date(Number(subscription.expirationEpochMs));
    return date.toLocaleDateString();
  }

  private getProviderName_(): string {
    if (this.app_ && this.app_.title !== undefined) {
      return this.app_.title!;
    }
    return this.subscription_!.provisioningSource;
  }

  private getPasspointDomainsList_(): string[] {
    return this.subscription_!.domains;
  }

  private getNetworkDisplayName_(networkState: OncMojo.NetworkStateProperties):
      string {
    return OncMojo.getNetworkStateDisplayNameUnsafe(networkState);
  }

  private hasNetworks_(): boolean {
    return this.networks_.length > 0;
  }

  private onAssociatedNetworkClicked_(
      event: DomRepeatEvent<OncMojo.NetworkStateProperties>): void {
    const networkState = event.model.item;
    const showDetailEvent = new CustomEvent(
        'show-detail', {bubbles: true, composed: true, detail: networkState});
    this.dispatchEvent(showDetailEvent);
    event.stopPropagation();
  }

  private getRemovalDialogDescription_(): TrustedHTML {
    return this.i18nAdvanced('passpointRemovalDescription', {
      substitutions: [
        this.subscription_!.friendlyName,
      ],
    });
  }

  private getRemovalDialog_(): HTMLDialogElement {
    return castExists(
        this.shadowRoot!.querySelector<HTMLDialogElement>('#removalDialog'));
  }

  private onForgetClick_(): void {
    this.showForgetDialog_ = true;
  }

  private async onRemovalDialogConfirm_(): Promise<void> {
    this.showForgetDialog_ = false;
    const response =
        await this.passpointService_.deletePasspointSubscription(this.id_);
    if (response.success) {
      this.close();
      return;
    }
  }

  private onRemovalDialogCancel_(): void {
    this.showForgetDialog_ = false;
  }

  override onPasspointSubscriptionRemoved(subscription: PasspointSubscription):
      void {
    if (this.id_ === subscription.id) {
      // The subscription was removed, leave the page.
      this.close();
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [SettingsPasspointSubpageElement.is]: SettingsPasspointSubpageElement;
  }
}

customElements.define(
    SettingsPasspointSubpageElement.is, SettingsPasspointSubpageElement);