// 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);