chromium/chrome/browser/resources/offline_pages/offline_internals.ts

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

import './strings.m.js';

import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getRequiredElement} from 'chrome://resources/js/util.js';

import type {IsLogging, OfflineInternalsBrowserProxy, OfflinePage, SavePageRequest} from './offline_internals_browser_proxy.js';
import {OfflineInternalsBrowserProxyImpl} from './offline_internals_browser_proxy.js';

let offlinePages: OfflinePage[] = [];

let savePageRequests: SavePageRequest[] = [];

const browserProxy: OfflineInternalsBrowserProxy =
    OfflineInternalsBrowserProxyImpl.getInstance();

/**
 * Fill stored pages table.
 * @param pages An array object representing stored offline pages.
 */
function fillStoredPages(pages: OfflinePage[]) {
  const storedPagesTable = getRequiredElement('stored-pages');
  storedPagesTable.textContent = '';

  const template =
      getRequiredElement<HTMLTemplateElement>('stored-pages-table-row');
  const td = template.content.querySelectorAll('td');
  for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
    const page = pages[pageIndex]!;
    td[0]!.textContent = (pageIndex + 1).toString();
    const checkbox = td[1]!.querySelector('input');
    assert(checkbox);
    checkbox.setAttribute('value', page.id);

    const link = td[2]!.querySelector('a');
    assert(link);
    link.setAttribute('href', page.onlineUrl);
    const maxUrlCharsPerLine = 50;
    if (page.onlineUrl.length > maxUrlCharsPerLine) {
      link.textContent = '';
      for (let i = 0; i < page.onlineUrl.length; i += maxUrlCharsPerLine) {
        link.textContent += page.onlineUrl.slice(i, i + maxUrlCharsPerLine);
        link.textContent += '\r\n';
      }
    } else {
      link.textContent = page.onlineUrl;
    }

    td[3]!.textContent = page.namespace;
    td[4]!.textContent = (Math.round(Number(page.size) / 1024)).toString();

    const row = document.importNode(template.content, true);
    storedPagesTable.appendChild(row);
  }
  offlinePages = pages;
}

/**
 * Fill requests table.
 * @param requests An array object representing the request queue.
 */
function fillRequestQueue(requests: SavePageRequest[]) {
  const requestQueueTable = getRequiredElement('request-queue');
  requestQueueTable.textContent = '';

  const template =
      getRequiredElement<HTMLTemplateElement>('request-queue-table-row');
  const td = template.content.querySelectorAll('td');
  for (const request of requests) {
    const checkbox = td[0]!.querySelector('input');
    assert(checkbox);
    checkbox.setAttribute('value', request.id);

    td[1]!.textContent = request.onlineUrl;
    td[2]!.textContent = new Date(request.creationTime).toString();
    td[3]!.textContent = request.status;
    td[4]!.textContent = request.requestOrigin;

    const row = document.importNode(template.content, true);
    requestQueueTable.appendChild(row);
  }
  savePageRequests = requests;
}

/**
 * Fills the event logs section.
 * @param logs A list of log strings.
 */
function fillEventLog(logs: string[]) {
  const element = getRequiredElement('logs');
  element.textContent = '';
  for (const log of logs) {
    const logItem = document.createElement('li');
    logItem.textContent = log;
    element.appendChild(logItem);
  }
}

/**
 * Refresh all displayed information.
 */
function refreshAll() {
  browserProxy.getStoredPages().then(fillStoredPages);
  browserProxy.getRequestQueue().then(fillRequestQueue);
  browserProxy.getNetworkStatus().then(function(networkStatus) {
    getRequiredElement('current-status').textContent = networkStatus;
  });
  browserProxy.getLimitlessPrefetchingEnabled().then(function(enabled) {
    getRequiredElement<HTMLInputElement>('limitless-prefetching-checkbox')
        .checked = enabled;
  });
  browserProxy.getPrefetchTestingHeaderValue().then(function(value) {
    switch (value) {
      case 'ForceEnable':
        getRequiredElement<HTMLInputElement>('testing-header-enable').checked =
            true;
        break;
      case 'ForceDisable':
        getRequiredElement<HTMLInputElement>('testing-header-disable').checked =
            true;
        break;
      default:
        getRequiredElement<HTMLInputElement>('testing-header-default').checked =
            true;
    }
  });
  refreshLog();
}

/**
 * Callback when pages are deleted.
 * @param status The status of the request.
 */
function pagesDeleted(status: string) {
  getRequiredElement('page-actions-info').textContent = status;
  browserProxy.getStoredPages().then(fillStoredPages);
}

/**
 * Callback when requests are deleted.
 */
function requestsDeleted(status: string) {
  getRequiredElement('request-queue-actions-info').textContent = status;
  browserProxy.getRequestQueue().then(fillRequestQueue);
}

/**
 * Callback for prefetch actions.
 * @param info The result of performing the prefetch actions.
 */
function setPrefetchResult(info: string) {
  getRequiredElement('prefetch-actions-info').textContent = info;
}

/**
 * Error callback for prefetch actions.
 * @param error The error that resulted from the prefetch call.
 */
function prefetchResultError(error: Error|string) {
  const errorText =
      error && (error as Error).message ? (error as Error).message : error;

  getRequiredElement('prefetch-actions-info').textContent =
      'Error: ' + errorText;
}

/**
 * Downloads all the stored page and request queue information into a file.
 * Also translates all the fields representing datetime into human-readable
 * date strings.
 * TODO(chili): Create a CSV writer that can abstract out the line joining.
 */
function dumpAsJson() {
  const json = JSON.stringify(
      {offlinePages: offlinePages, savePageRequests: savePageRequests},
      function(key, value) {
        return key.endsWith('Time') ? new Date(value).toString() : value;
      },
      2);

  getRequiredElement<HTMLTextAreaElement>('dump-box').value = json;
  getRequiredElement('dump-info').textContent = '';
  getRequiredElement<HTMLDialogElement>('dump-modal').showModal();
  getRequiredElement<HTMLTextAreaElement>('dump-box').select();
}

function closeDump() {
  getRequiredElement<HTMLDialogElement>('dump-modal').close();
  getRequiredElement<HTMLTextAreaElement>('dump-box').value = '';
}

function copyDump() {
  getRequiredElement<HTMLTextAreaElement>('dump-box').select();
  document.execCommand('copy');
  getRequiredElement('dump-info').textContent = 'Copied to clipboard!';
}

/**
 * Updates the status strings.
 * @param logStatus Status of logging.
 */
function updateLogStatus(logStatus: IsLogging) {
  getRequiredElement<HTMLInputElement>('model-checkbox').checked =
      logStatus.modelIsLogging;
  getRequiredElement<HTMLInputElement>('request-checkbox').checked =
      logStatus.queueIsLogging;
  getRequiredElement<HTMLInputElement>('prefetch-checkbox').checked =
      logStatus.prefetchIsLogging;
}

/**
 * Sets all checkboxes with a specific name to the same checked status as the
 * provided source checkbox.
 * @param source The checkbox controlling the checked status.
 * @param checkboxesName The name identifying the checkboxes to set.
 */
function toggleAllCheckboxes(source: HTMLInputElement, checkboxesName: string) {
  const checkboxes = document.getElementsByName(checkboxesName);
  for (const checkbox of checkboxes) {
    (checkbox as HTMLInputElement).checked = source.checked;
  }
}

/**
 * Return the item ids for the selected checkboxes with a given name.
 * @param checkboxesName The name identifying the checkboxes to query.
 * @return An array of selected ids.
 */
function getSelectedIdsFor(checkboxesName: string): string[] {
  const checkboxes = document.querySelectorAll<HTMLInputElement>(
      `input[type="checkbox"][name="${checkboxesName}"]:checked`);
  return Array.from(checkboxes).map(c => c.value);
}

/**
 * Refreshes the logs.
 */
function refreshLog() {
  browserProxy.getEventLogs().then(fillEventLog);
  browserProxy.getLoggingState().then(updateLogStatus);
}

/**
 * Calls scheduleNwake and indicates how long the scheduled delay will be.
 */
function ensureBackgroundTaskScheduledWithDelay() {
  browserProxy.scheduleNwake()
      .then((result: string) => {
        // The delays in these messages should correspond to the scheduling
        // delays defined in PrefetchBackgroundTaskScheduler.java.
        if (getRequiredElement<HTMLInputElement>(
                'limitless-prefetching-checkbox')
                .checked) {
          setPrefetchResult(
              result +
              ' (Limitless mode enabled; background task scheduled to run' +
              ' in a few seconds.)');
        } else {
          setPrefetchResult(
              result +
              ' (Limitless mode disabled; background task scheduled to run' +
              ' in several minutes.)');
        }
      })
      .catch(prefetchResultError);
}

function initialize() {
  const incognito = loadTimeData.getBoolean('isIncognito');
  ['delete-selected-pages', 'delete-selected-requests', 'model-checkbox',
   'request-checkbox', 'refresh']
      .forEach(
          el => getRequiredElement<HTMLInputElement>(el).disabled = incognito);

  getRequiredElement('delete-selected-pages').onclick = function() {
    const pageIds = getSelectedIdsFor('stored');
    browserProxy.deleteSelectedPages(pageIds).then(pagesDeleted);
  };
  getRequiredElement('delete-selected-requests').onclick = function() {
    const requestIds = getSelectedIdsFor('requests');
    browserProxy.deleteSelectedRequests(requestIds).then(requestsDeleted);
  };
  getRequiredElement('refresh').onclick = refreshAll;
  getRequiredElement('dump').onclick = dumpAsJson;
  getRequiredElement('close-dump').onclick = closeDump;
  getRequiredElement('copy-to-clipboard').onclick = copyDump;
  getRequiredElement('model-checkbox').onchange = (evt: Event) => {
    browserProxy.setRecordPageModel((evt.target as HTMLInputElement).checked);
  };
  getRequiredElement('request-checkbox').onchange = (evt: Event) => {
    browserProxy.setRecordRequestQueue(
        (evt.target as HTMLInputElement).checked);
  };
  getRequiredElement('prefetch-checkbox').onchange = (evt: Event) => {
    browserProxy.setRecordPrefetchService(
        (evt.target as HTMLInputElement).checked);
  };
  getRequiredElement('refresh-logs').onclick = refreshLog;
  getRequiredElement('add-to-queue').onclick = function() {
    const saveUrls =
        getRequiredElement<HTMLInputElement>('url').value.split(',');
    let counter = saveUrls.length;
    getRequiredElement('save-url-state').textContent = '';
    for (let i = 0; i < saveUrls.length; i++) {
      browserProxy.addToRequestQueue(saveUrls[i]!).then(function(state) {
        if (state) {
          getRequiredElement('save-url-state').textContent +=
              saveUrls[i] + ' has been added to queue.\n';
          getRequiredElement<HTMLInputElement>('url').value = '';
          counter--;
          if (counter === 0) {
            browserProxy.getRequestQueue().then(fillRequestQueue);
          }
        } else {
          getRequiredElement('save-url-state').textContent +=
              saveUrls[i] + ' failed to be added to queue.\n';
        }
      });
    }
  };
  getRequiredElement('schedule-nwake').onclick = function() {
    browserProxy.scheduleNwake()
        .then(setPrefetchResult)
        .catch(prefetchResultError);
  };
  getRequiredElement('cancel-nwake').onclick = function() {
    browserProxy.cancelNwake()
        .then(setPrefetchResult)
        .catch(prefetchResultError);
  };
  getRequiredElement('generate-page-bundle').onclick = function() {
    browserProxy
        .generatePageBundle(
            getRequiredElement<HTMLInputElement>('generate-urls').value)
        .then(setPrefetchResult)
        .catch(prefetchResultError);
  };
  getRequiredElement('get-operation').onclick = function() {
    browserProxy
        .getOperation(
            getRequiredElement<HTMLInputElement>('operation-name').value)
        .then(setPrefetchResult)
        .catch(prefetchResultError);
  };
  getRequiredElement('download-archive').onclick = function() {
    browserProxy.downloadArchive(
        getRequiredElement<HTMLInputElement>('download-name').value);
  };
  getRequiredElement('toggle-all-stored').onclick = function() {
    toggleAllCheckboxes(
        getRequiredElement<HTMLInputElement>('toggle-all-stored'), 'stored');
  };
  getRequiredElement('toggle-all-requests').onclick = function() {
    toggleAllCheckboxes(
        getRequiredElement<HTMLInputElement>('toggle-all-requests'),
        'requests');
  };
  getRequiredElement('limitless-prefetching-checkbox').onchange =
      (evt: Event) => {
        const checkbox = evt.target as HTMLInputElement;
        browserProxy.setLimitlessPrefetchingEnabled(checkbox.checked);
        if (checkbox.checked) {
          ensureBackgroundTaskScheduledWithDelay();
        }
      };
  // Helper for setting prefetch testing header from a radio button.
  const setPrefetchTestingHeader = function(evt: Event) {
    browserProxy.setPrefetchTestingHeaderValue(
        (evt.target as HTMLInputElement).value);
    ensureBackgroundTaskScheduledWithDelay();
  };
  getRequiredElement('testing-header-default').onchange =
      setPrefetchTestingHeader;
  getRequiredElement('testing-header-enable').onchange =
      setPrefetchTestingHeader;
  getRequiredElement('testing-header-disable').onchange =
      setPrefetchTestingHeader;
  if (!incognito) {
    refreshAll();
  }
}

document.addEventListener('DOMContentLoaded', initialize);