blob: fc4bba5fe3a88704e58c3c5fff0e8f54f2acbd76 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {decorate} from 'chrome://resources/js/cr/ui.m.js';
import {Tree, TreeItem} from 'chrome://resources/js/cr/ui/tree.js';
import {$} from 'chrome://resources/js/util.m.js';
import {FrameInfo, FrameInfo_Type, ProcessInternalsHandler, ProcessInternalsHandlerRemote, WebContentsInfo} from './process_internals.mojom-webui.js';
/**
* Reference to the backend providing all the data.
* @type {ProcessInternalsHandlerRemote}
*/
let pageHandler = null;
/**
* @param {string} id Tab id.
* @return {boolean} True if successful.
*/
function selectTab(id) {
const tabContents = document.querySelectorAll('#content > div');
const tabHeaders = $('navigation').querySelectorAll('.tab-header');
let found = false;
for (let i = 0; i < tabContents.length; i++) {
const tabContent = tabContents[i];
const tabHeader = tabHeaders[i];
const isTargetTab = tabContent.id === id;
found = found || isTargetTab;
tabContent.classList.toggle('selected', isTargetTab);
tabHeader.classList.toggle('selected', isTargetTab);
}
if (!found) {
return false;
}
window.location.hash = id;
return true;
}
function onHashChange() {
const hash = window.location.hash.slice(1).toLowerCase();
if (!selectTab(hash)) {
selectTab('general');
}
}
function setupTabs() {
const tabContents = document.querySelectorAll('#content > div');
for (let i = 0; i < tabContents.length; i++) {
const tabContent = tabContents[i];
const tabName = tabContent.querySelector('.content-header').textContent;
const tabHeader = document.createElement('div');
tabHeader.className = 'tab-header';
const button = document.createElement('button');
button.textContent = tabName;
tabHeader.appendChild(button);
tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
$('navigation').appendChild(tabHeader);
}
onHashChange();
}
/**
* Root of the WebContents tree.
* @type {Tree|null}
*/
let treeViewRoot = null;
/**
* Initialize and return |treeViewRoot|.
* @return {Tree} Initialized |treeViewRoot|.
*/
function getTreeViewRoot() {
if (!treeViewRoot) {
decorate('#tree-view', Tree);
treeViewRoot = /** @type {Tree} */ ($('tree-view'));
treeViewRoot.detail = {payload: {}, children: {}};
}
return treeViewRoot;
}
/**
* Initialize and return a tree item representing a FrameInfo object and
* recursively creates its subframe objects.
* @param {FrameInfo} frame
* @return {Array}
*/
function frameToTreeItem(frame) {
// Compose the string which will appear in the entry for this frame.
let itemLabel = `Frame[${frame.processId}:${frame.routingId}:${
frame.agentSchedulingGroupId}]:`;
if (frame.type == FrameInfo_Type.kBackForwardCache) {
itemLabel += ` bfcached`;
} else if (frame.type == FrameInfo_Type.kPrerender) {
itemLabel += ` prerender`;
}
itemLabel += ` SI:${frame.siteInstance.id}`;
if (frame.siteInstance.locked) {
itemLabel += ', locked';
}
if (frame.siteInstance.siteUrl) {
itemLabel += `, site:${frame.siteInstance.siteUrl.url}`;
}
if (frame.siteInstance.processLockUrl) {
itemLabel += `, lock:${frame.siteInstance.processLockUrl.url}`;
}
if (frame.siteInstance.requiresOriginKeyedProcess) {
itemLabel += ', origin-keyed';
}
if (frame.lastCommittedUrl) {
itemLabel += ` | url: ${frame.lastCommittedUrl.url}`;
}
const item =
new TreeItem({label: itemLabel, detail: {payload: {}, children: {}}});
item.mayHaveChildren_ = true;
item.expanded = true;
item.icon = '';
let frameCount = 1;
for (const subframe of frame.subframes) {
const result = frameToTreeItem(subframe);
const subItem = result[0];
const count = result[1];
frameCount += count;
item.add(subItem);
}
return [item, frameCount];
}
/**
* Initialize and return a tree item representing the WebContentsInfo object
* and contains all frames in it as a subtree.
* @param {WebContentsInfo} webContents
* @return {!TreeItem}
*/
function webContentsToTreeItem(webContents) {
let itemLabel = 'WebContents: ';
if (webContents.title.length > 0) {
itemLabel += webContents.title + ', ';
}
const item =
new TreeItem({label: itemLabel, detail: {payload: {}, children: {}}});
item.mayHaveChildren_ = true;
item.expanded = true;
item.icon = '';
const result = frameToTreeItem(webContents.rootFrame);
const rootItem = result[0];
const activeCount = result[1];
item.add(rootItem);
// Add data for all root nodes retrieved from back-forward cache.
let cachedCount = 0;
for (const cachedRoot of webContents.bfcachedRootFrames) {
const cachedResult = frameToTreeItem(cachedRoot);
item.add(cachedResult[0]);
cachedCount++;
}
// Add data for all root nodes in prerendered pages.
let prerenderCount = 0;
for (const cachedRoot of webContents.prerenderRootFrames) {
const cachedResult = frameToTreeItem(cachedRoot);
item.add(cachedResult[0]);
prerenderCount++;
}
// Builds a string according to English pluralization rules:
// buildCountString(0, 'frame') => "0 frames"
// buildCountString(1, 'frame') => "1 frame"
// buildCountString(2, 'frame') => "2 frames"
const buildCountString = ((count, name) => {
return `${count} ${name}` + (count != 1 ? 's' : '');
});
itemLabel += buildCountString(activeCount, 'active frame');
if (cachedCount > 0) {
itemLabel += ', ' + buildCountString(cachedCount, 'bfcached root');
}
if (prerenderCount > 0) {
itemLabel += ', ' + buildCountString(prerenderCount, 'prerender root');
}
item.label = itemLabel;
return item;
}
/**
* This is a callback which is invoked when the data for WebContents
* associated with the browser profile is received from the browser process.
* @param {!Array<!WebContentsInfo>} infos
*/
function populateWebContentsTab(infos) {
const tree = getTreeViewRoot();
// Clear the tree first before populating it with the new content.
tree.innerText = '';
for (const webContents of infos) {
const item = webContentsToTreeItem(webContents);
tree.add(item);
}
}
/**
* Function which retrieves the data for all WebContents associated with the
* current browser profile. The result is passed to populateWebContentsTab.
*/
async function loadWebContentsInfo() {
const {infos} = await pageHandler.getAllWebContentsInfo();
populateWebContentsTab(infos);
}
/**
* Function which retrieves the currently active isolated origins and inserts
* them into the page. It organizes these origins into two lists: persisted
* isolated origins, which are triggered by password entry and apply only
* within the current profile, and global isolated origins, which apply to all
* profiles.
*/
function loadIsolatedOriginInfo() {
// Retrieve any persistent isolated origins for the current profile. Insert
// them into a list on the page if there is at least one such origin.
pageHandler.getUserTriggeredIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
$('user-triggered-isolated-origins').textContent =
'The following origins are isolated because you previously typed a ' +
'password or logged in on these sites (' + originCount + ' total). ' +
'Clear cookies or history to wipe this list; this takes effect ' +
'after a restart.';
const list = document.createElement('ul');
for (const origin of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = origin;
list.appendChild(item);
}
$('user-triggered-isolated-origins').appendChild(list);
});
pageHandler.getWebTriggeredIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
$('web-triggered-isolated-origins').textContent =
'The following origins are isolated based on runtime heuristics ' +
'triggered directly by web pages, such as Cross-Origin-Opener-Policy ' +
'headers. Clear cookies or history to wipe this list; this takes ' +
'effect after a restart.';
const list = document.createElement('ul');
for (const origin of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = origin;
list.appendChild(item);
}
$('web-triggered-isolated-origins').appendChild(list);
});
// Retrieve global isolated origins and insert them into a separate list if
// there is at least one such origin. Since these origins may come from
// multiple sources, include the source info for each origin in parens.
pageHandler.getGloballyIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
$('global-isolated-origins').textContent =
'The following origins are isolated by default for all users (' +
originCount + ' total). A description of how each origin was ' +
' activated is provided in parentheses.';
const list = document.createElement('ul');
for (const originInfo of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = `${originInfo.origin} (${originInfo.source})`;
list.appendChild(item);
}
$('global-isolated-origins').appendChild(list);
});
}
document.addEventListener('DOMContentLoaded', function() {
// Setup Mojo interface to the backend.
pageHandler = ProcessInternalsHandler.getRemote();
// Get the Site Isolation mode and populate it.
pageHandler.getIsolationMode().then((response) => {
$('isolation-mode').innerText = response.mode;
});
loadIsolatedOriginInfo();
// Setup the tabbed UI
setupTabs();
// Start loading the information about WebContents.
loadWebContentsInfo();
$('refresh-button').addEventListener('click', loadWebContentsInfo);
});