| // Copyright 2011 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import type {WebUiListener} from 'chrome://resources/js/cr.js'; |
| import {addWebUiListener, removeWebUiListener} from 'chrome://resources/js/cr.js'; |
| |
| import {aboutInfo} from './about.js'; |
| import type {SyncNode, SyncNodeMap} from './chrome_sync.js'; |
| import {getAllNodes, requestIncludeSpecificsInitialState, requestListOfTypes} from './chrome_sync.js'; |
| import {log} from './sync_log.js'; |
| |
| const dumpToTextButton = document.querySelector<HTMLElement>('#dump-to-text'); |
| assert(dumpToTextButton); |
| const dataDump = document.querySelector<HTMLElement>('#data-dump'); |
| assert(dataDump); |
| dumpToTextButton.addEventListener('click', function() { |
| // TODO(akalin): Add info like Chrome version, OS, date dumped, etc. |
| |
| let data = ''; |
| data += '======\n'; |
| data += 'Status\n'; |
| data += '======\n'; |
| data += JSON.stringify(aboutInfo, null, 2); |
| data += '\n'; |
| data += '\n'; |
| |
| data += '===\n'; |
| data += 'Log\n'; |
| data += '===\n'; |
| data += JSON.stringify(log.entries, null, 2); |
| data += '\n'; |
| |
| dataDump.textContent = data; |
| }); |
| |
| const allFields = [ |
| 'ID', |
| 'IS_UNSYNCED', |
| 'IS_UNAPPLIED_UPDATE', |
| 'BASE_VERSION', |
| 'BASE_VERSION_TIME', |
| 'SERVER_VERSION', |
| 'SERVER_VERSION_TIME', |
| 'PARENT_ID', |
| 'SERVER_PARENT_ID', |
| 'IS_DEL', |
| 'SERVER_IS_DEL', |
| 'dataType', |
| 'SERVER_SPECIFICS', |
| 'SPECIFICS', |
| ]; |
| |
| function versionToDateString(version: string) { |
| // TODO(mmontgomery): ugly? Hacky? Is there a better way? |
| const epochLength = Date.now().toString().length; |
| const epochTime = parseInt(version.slice(0, epochLength), 10); |
| const date = new Date(epochTime); |
| return date.toString(); |
| } |
| |
| /** |
| * @param node A JavaScript represenation of a sync entity. |
| * @return A string representation of the sync entity. |
| */ |
| function serializeNode(node: SyncNode): string[] { |
| const includeSpecifics = |
| document.querySelector<HTMLInputElement>('#include-specifics'); |
| assert(includeSpecifics); |
| return allFields.map(function(field) { |
| let fieldVal; |
| if (field === 'SERVER_VERSION_TIME') { |
| const version = node['SERVER_VERSION']; |
| if (version != null) { |
| fieldVal = versionToDateString(version); |
| } |
| } |
| if (field === 'BASE_VERSION_TIME') { |
| const version = node['BASE_VERSION']; |
| if (version != null) { |
| fieldVal = versionToDateString(version); |
| } |
| } else if ( |
| (field === 'SERVER_SPECIFICS' || field === 'SPECIFICS') && |
| (!includeSpecifics.checked)) { |
| fieldVal = 'REDACTED'; |
| } else if ( |
| (field === 'SERVER_SPECIFICS' || field === 'SPECIFICS') && |
| includeSpecifics.checked) { |
| fieldVal = JSON.stringify(node[field]); |
| } else { |
| fieldVal = (node as unknown as {[key: string]: string})[field]; |
| } |
| return fieldVal || ''; |
| }); |
| } |
| |
| /** |
| * @param type The name of a sync data type. |
| * @return True if the type's checkbox is selected. |
| */ |
| function isSelectedDatatype(type: string): boolean { |
| const typeCheckbox = document.querySelector<HTMLInputElement>(`#${type}`); |
| // Some types, such as 'Top level folder', appear in the list of nodes |
| // but not in the list of selectable items. |
| if (typeCheckbox == null) { |
| return false; |
| } |
| return typeCheckbox.checked; |
| } |
| |
| function makeBlobUrl(data: string): string { |
| const textBlob = new Blob([data], {type: 'octet/stream'}); |
| const blobUrl = window.URL.createObjectURL(textBlob); |
| return blobUrl; |
| } |
| |
| function makeDownloadName() { |
| // Format is sync-data-dump-$epoch-$year-$month-$day-$OS.csv. |
| const now = new Date(); |
| const friendlyDate = |
| [now.getFullYear(), now.getMonth() + 1, now.getDate()].join('-'); |
| const name = [ |
| 'sync-data-dump', |
| friendlyDate, |
| Date.now(), |
| navigator.platform, |
| ].join('-'); |
| return [name, 'csv'].join('.'); |
| } |
| |
| function makeDateUserAgentHeader() { |
| const now = new Date(); |
| const userAgent = window.navigator.userAgent; |
| const dateUaHeader = [now.toISOString(), userAgent].join(','); |
| return dateUaHeader; |
| } |
| |
| /** |
| * Builds a summary of current state and exports it as a downloaded file. |
| * |
| * @param nodesMap Summary of local state by data type. |
| */ |
| function triggerDataDownload(nodesMap: SyncNodeMap) { |
| // Prepend a header with ISO date and useragent. |
| const output = [makeDateUserAgentHeader()]; |
| output.push('====='); |
| |
| const aboutInfoString = JSON.stringify(aboutInfo, null, 2); |
| output.push(aboutInfoString); |
| |
| // Filter out non-selected types. |
| const selectedTypesNodes = nodesMap.filter(function(x) { |
| return isSelectedDatatype(x.type); |
| }); |
| |
| // Serialize the remaining nodes and add them to the output. |
| selectedTypesNodes.forEach(function(typeNodes) { |
| output.push('====='); |
| output.push(typeNodes.nodes.map(serializeNode).join('\n')); |
| }); |
| |
| const outputString = output.join('\n'); |
| |
| const anchor = |
| document.querySelector<HTMLAnchorElement>('#dump-to-file-anchor'); |
| assert(anchor); |
| anchor.href = makeBlobUrl(outputString); |
| anchor.download = makeDownloadName(); |
| anchor.click(); |
| } |
| |
| function createTypesCheckboxes(types: string[]) { |
| const containerElt = |
| document.querySelector<HTMLElement>('#node-type-checkboxes'); |
| assert(containerElt); |
| |
| types.map(function(type: string) { |
| const div = document.createElement('div'); |
| |
| const checkbox = document.createElement('input'); |
| checkbox.id = type; |
| checkbox.type = 'checkbox'; |
| checkbox.checked = true; |
| div.appendChild(checkbox); |
| |
| const label = document.createElement('label'); |
| // Assigning to label.for doesn't work. |
| label.setAttribute('for', type); |
| label.innerText = type; |
| div.appendChild(label); |
| |
| containerElt.appendChild(div); |
| }); |
| } |
| |
| let listOfTypesListener: WebUiListener|null = null; |
| |
| function onReceivedListOfTypes(response: {types: string[]}) { |
| const types = response.types; |
| types.sort(); |
| createTypesCheckboxes(types); |
| assert(listOfTypesListener); |
| removeWebUiListener(listOfTypesListener); |
| } |
| |
| function onReceivedIncludeSpecificsInitialState( |
| response: {includeSpecifics: boolean}) { |
| const captureSpecifics = |
| document.querySelector<HTMLInputElement>('#capture-specifics'); |
| assert(captureSpecifics); |
| captureSpecifics.checked = response.includeSpecifics; |
| } |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| listOfTypesListener = |
| addWebUiListener('onReceivedListOfTypes', onReceivedListOfTypes); |
| requestListOfTypes(); |
| |
| addWebUiListener( |
| 'onReceivedIncludeSpecificsInitialState', |
| onReceivedIncludeSpecificsInitialState); |
| requestIncludeSpecificsInitialState(); |
| }); |
| |
| const dumpToFileLink = document.querySelector<HTMLElement>('#dump-to-file'); |
| assert(dumpToFileLink); |
| dumpToFileLink.addEventListener('click', function() { |
| getAllNodes(triggerDataDownload); |
| }); |