chromium/chrome/browser/resources/chromeos/add_supervision/add_supervision_ui.ts

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

import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import './strings.m.js';

import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {AddSupervisionHandler, OAuthTokenFetchStatus} from './add_supervision.mojom-webui.js';
import {AddSupervisionApiServer} from './add_supervision_api_server.js';
import {getTemplate} from './add_supervision_ui.html.js';

declare global {
  interface HTMLElementEventMap {
    'newwindow': chrome.webviewTag.NewWindowEvent;
  }
}

/** List of URL hosts that can be requested by the webview. */
const ALLOWED_HOSTS: string[] = [
  'google.com',
  'gstatic.com',
  'googleapis.com',
  'google-analytics.com',
  // FIFE avatar images (lh3-lh6). See http://go/fife-domains
  'lh3.googleusercontent.com',
  'lh4.googleusercontent.com',
  'lh5.googleusercontent.com',
  'lh6.googleusercontent.com',
];

/**
 * Time in ms to wait before focusing the webview. Refer to the webview's
 * loadstop event listener for details.
 */
const INITIAL_FOCUS_DELAY_MS: number = 50;

/** Returns true if the URL references an HTTP request to localhost. */
export function isLocalHostForTesting(url: URL): boolean {
  return url.protocol === 'http:' && url.hostname === '127.0.0.1';
}

/** Returns true if the URL references one of the allowed hosts. */
function isAllowedHost(url: URL): boolean {
  return url.protocol === 'https:' &&
      ALLOWED_HOSTS.some(
          (allowedHost) =>
              url.host === allowedHost || url.host.endsWith('.' + allowedHost));
}

/** Returns true if the request should be allowed. */
function isAllowedRequest(requestDetails: string): boolean {
  const requestUrl = new URL(requestDetails);
  return isLocalHostForTesting(requestUrl) || isAllowedHost(requestUrl);
}

export interface AddSupervisionUi {
  $: {
    webview: chrome.webviewTag.WebView,
  };
}

export class AddSupervisionUi extends PolymerElement {
  static get is() {
    return 'add-supervision-ui';
  }

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

  static get properties() {
    return {
      webviewLoading: {
        type: Boolean,
        value: true,
      },
    };
  }

  webviewLoading: boolean;
  private server: AddSupervisionApiServer|null;

  override ready() {
    super.ready();
    const addSupervisionHandler = AddSupervisionHandler.getRemote();
    addSupervisionHandler.getOAuthToken().then((result) => {
      // Setup should terminate early if OAuth Token fetching fails.
      if (result.status === OAuthTokenFetchStatus.ERROR) {
        this.showErrorPage();
        return;
      }

      const webviewUrl = loadTimeData.getString('webviewUrl');
      const eventOriginFilter = loadTimeData.getString('eventOriginFilter');
      const webview = this.$.webview;

      const accessToken = result.oauthToken;
      const flowType = loadTimeData.getString('flowType');
      const platformVersion = loadTimeData.getString('platformVersion');
      const languageCode = loadTimeData.getString('languageCode');

      const url = new URL(webviewUrl);
      url.searchParams.set('flow_type', flowType);
      url.searchParams.set('platform_version', platformVersion);
      url.searchParams.set('access_token', accessToken);
      url.searchParams.set('hl', languageCode);

      // Allow guest webview content to open links in new windows.
      webview.addEventListener(
          'newwindow', (e: chrome.webviewTag.NewWindowEvent) => {
            window.open(e.targetUrl);
          });

      // Change loading indicator on load in order to hide loading spinner.
      webview.addEventListener('contentload', () => {
        this.webviewLoading = false;
      });

      // Sets focus on the inner webview, so that ChromeVox users don't need to
      // navigate through multiple containers when linear navigating through the
      // page (https://crbug.com/1231798).
      // We want the dialog content to be automatically announced once loaded.
      // ChromeVox automatically reads all content when it enters a role=dialog
      // div. If the webview is focused too soon, there's no content to read
      // yet. Therefore we wait for it to be loaded first, with a delay (so that
      // the accessibility tree is fully updated).
      webview.addEventListener('loadstop', () => {
        setTimeout(() => webview.focus(), INITIAL_FOCUS_DELAY_MS);
      });

      webview.addEventListener('loadabort', () => {
        this.webviewLoading = false;
        this.showErrorPage();
      });

      // Block any requests to URLs other than one specified
      // by eventOriginFilter.
      webview.request.onBeforeRequest.addListener((details: {url: string}) => {
        return {cancel: !isAllowedRequest(details.url)};
      }, {urls: ['<all_urls>']}, ['blocking']);

      webview.src = url.toString();

      // Set up the server.
      this.server = new AddSupervisionApiServer(
          this, webview, url.toString(), eventOriginFilter);
    });
  }

  showErrorPage() {
    this.dispatchEvent(new CustomEvent('show-error', {
      bubbles: true,
      composed: true,
    }));
  }

  getApiServerForTest(): AddSupervisionApiServer|null {
    return this.server;
  }
}

customElements.define(AddSupervisionUi.is, AddSupervisionUi);