/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js"); const { HarUtils, } = require("resource://devtools/client/netmonitor/src/har/har-utils.js"); const { HarBuilder, } = require("resource://devtools/client/netmonitor/src/har/har-builder.js"); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FileUtils: "resource://gre/modules/FileUtils.sys.mjs", }); var uid = 1; // Helper tracer. Should be generic sharable by other modules (bug 1171927) const trace = { log() {}, }; /** * This object represents the main public API designed to access * Network export logic. Clients, such as the Network panel itself, * should use this API to export collected HTTP data from the panel. */ const HarExporter = { // Public API /** * Save collected HTTP data from the Network panel into HAR file. * * @param Object options * Configuration object * * The following options are supported: * * - includeResponseBodies {Boolean}: If set to true, HTTP response bodies * are also included in the HAR file (can produce significantly bigger * amount of data). * * - items {Array}: List of Network requests to be exported. * * - jsonp {Boolean}: If set to true the export format is HARP (support * for JSONP syntax). * * - jsonpCallback {String}: Default name of JSONP callback (used for * HARP format). * * - compress {Boolean}: If set to true the final HAR file is zipped. * This represents great disk-space optimization. * * - defaultFileName {String}: Default name of the target HAR file. * The default file name supports the format specifier %date to output the * current date/time. * * - defaultLogDir {String}: Default log directory for automated logs. * * - id {String}: ID of the page (used in the HAR file). * * - title {String}: Title of the page (used in the HAR file). * * - forceExport {Boolean}: The result HAR file is created even if * there are no HTTP entries. * * - isSingleRequest {Boolean}: Set to true if only a single request. */ async save(options) { // Set default options related to save operation. const defaultFileName = Services.prefs.getCharPref( "devtools.netmonitor.har.defaultFileName" ); const compress = Services.prefs.getBoolPref( "devtools.netmonitor.har.compress" ); trace.log("HarExporter.save; " + defaultFileName, options); let data = await this.fetchHarData(options); const host = new URL(options.connector.currentTarget.url); if (typeof options.isSingleRequest != "boolean") { options.isSingleRequest = false; } const fileName = HarUtils.getHarFileName( defaultFileName, options.jsonp, compress, host.hostname, options.isSingleRequest && options.items.length ? new URL(options.items[0].url).pathname : "" ); data = new TextEncoder().encode(data); if (compress) { const file = await DevToolsUtils.showSaveFileDialog(window, fileName, [ "*.zip", ]); this._zip(file, fileName.replace(/\.zip$/, ""), data.buffer); } else { DevToolsUtils.saveAs(window, data, fileName); } }, /** * Helper to save the har file into a .zip file * * @param {nsIFIle} file The final zip file * @param {string} fileName Name of the har file within the zip file * @param {ArrayBuffer} buffer Content of the har file */ _zip(file, fileName, buffer) { const ZipWriter = Components.Constructor( "@mozilla.org/zipwriter;1", "nsIZipWriter" ); const zipW = new ZipWriter(); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, lazy.FileUtils.PERMS_FILE); // Open the file in write mode only and reset the size of any existing file const MODE_WRONLY = 0x02; const MODE_TRUNCATE = 0x20; zipW.open(file, MODE_WRONLY | MODE_TRUNCATE); const stream = Cc[ "@mozilla.org/io/arraybuffer-input-stream;1" ].createInstance(Ci.nsIArrayBufferInputStream); stream.setData(buffer, 0, buffer.byteLength); // Needs to be in microseconds for some reason. const time = Date.now() * 1000; zipW.addEntryStream(fileName, time, 0, stream, false); zipW.close(); }, /** * Copy HAR string into the clipboard. * * @param Object options * Configuration object, see save() for detailed description. */ copy(options) { return this.fetchHarData(options).then(jsonString => { clipboardHelper.copyString(jsonString); return jsonString; }); }, /** * Get HAR data as JSON object. * * @param Object options * Configuration object, see save() for detailed description. */ getHar(options) { return this.fetchHarData(options).then(data => { return data ? JSON.parse(data) : null; }); }, // Helpers fetchHarData(options) { // Generate page ID options.id = options.id || uid++; // Set default generic HAR export options. if (typeof options.jsonp != "boolean") { options.jsonp = Services.prefs.getBoolPref( "devtools.netmonitor.har.jsonp" ); } if (typeof options.includeResponseBodies != "boolean") { options.includeResponseBodies = Services.prefs.getBoolPref( "devtools.netmonitor.har.includeResponseBodies" ); } if (typeof options.jsonpCallback != "boolean") { options.jsonpCallback = Services.prefs.getCharPref( "devtools.netmonitor.har.jsonpCallback" ); } if (typeof options.forceExport != "boolean") { options.forceExport = Services.prefs.getBoolPref( "devtools.netmonitor.har.forceExport" ); } if (typeof options.supportsMultiplePages != "boolean") { options.supportsMultiplePages = Services.prefs.getBoolPref( "devtools.netmonitor.har.multiple-pages" ); } // Build HAR object. return this.buildHarData(options) .then(har => { // Do not export an empty HAR file, unless the user // explicitly says so (using the forceExport option). if (!har.log.entries.length && !options.forceExport) { return Promise.resolve(); } let jsonString = this.stringify(har); if (!jsonString) { return Promise.resolve(); } // If JSONP is wanted, wrap the string in a function call if (options.jsonp) { // This callback name is also used in HAR Viewer by default. // http://www.softwareishard.com/har/viewer/ const callbackName = options.jsonpCallback || "onInputData"; jsonString = callbackName + "(" + jsonString + ");"; } return jsonString; }) .catch(function onError(err) { console.error(err); }); }, /** * Build HAR data object. This object contains all HTTP data * collected by the Network panel. The process is asynchronous * since it can involve additional RDP communication (e.g. resolving * long strings). */ async buildHarData(options) { // Disconnect from redux actions/store. options.connector.enableActions(false); // Build HAR object from collected data. const builder = new HarBuilder(options); const result = await builder.build(); // Connect to redux actions again. options.connector.enableActions(true); return result; }, /** * Build JSON string from the HAR data object. */ stringify(har) { if (!har) { return null; } try { return JSON.stringify(har, null, " "); } catch (err) { console.error(err); return undefined; } }, }; // Exports from this module exports.HarExporter = HarExporter;