├── .jshintrc ├── chrome └── skin │ └── classic │ └── icon-16.png ├── chrome.manifest ├── .jpmignore ├── .gitignore ├── index.js ├── lib ├── har-driver-front.js ├── main.js ├── trigger-toolbox.js ├── trigger-toolbox-overlay.js └── har-driver-actor.js ├── license.txt ├── package.json └── README.md /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": "true", 3 | "predef": [ "require", "exports", "module" ], 4 | "curly": "true" 5 | } 6 | -------------------------------------------------------------------------------- /chrome/skin/classic/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/har-export-trigger/HEAD/chrome/skin/classic/icon-16.png -------------------------------------------------------------------------------- /chrome.manifest: -------------------------------------------------------------------------------- 1 | content harexporttrigger chrome/content/ 2 | skin harexporttrigger classic/1.0 chrome/skin/classic/shared/ 3 | locale harexporttrigger en-US chrome/locale/en-US/ 4 | -------------------------------------------------------------------------------- /.jpmignore: -------------------------------------------------------------------------------- 1 | # Doc Files 2 | /docs/ 3 | 4 | # Test Files 5 | /test/ 6 | 7 | # GIT 8 | /.git/ 9 | 10 | # Other files 11 | .gitignore 12 | .jpmignore 13 | .jshintrc 14 | .project 15 | 16 | # Existing packages 17 | *.xpi 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Junk that could exist anywhere: 3 | .DS_Store 4 | *.swp 5 | *.tmp 6 | .*.gz 7 | *.patch 8 | *~ 9 | 10 | # Temporary files created by Eclipse 11 | .tmp* 12 | 13 | # Editor junk 14 | *.project 15 | /.pydevproject 16 | /.settings/ 17 | /.settings.xml 18 | /.settings.xml.old 19 | /.idea/ 20 | *.iws 21 | *.ids 22 | *.iml 23 | *.ipr 24 | 25 | # Build Files 26 | /build/ 27 | /release/ 28 | *.graphml 29 | *.xpi 30 | 31 | # Files from NPM 32 | /node_modules/ 33 | 34 | # Extensions 35 | /firebug@software.joehewitt.com 36 | 37 | bootstrap.js 38 | install.rdf 39 | 40 | # Bash 41 | *.sh 42 | *.bat 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | /** 6 | * This file is specified as the 'main' module in package.json 7 | * MDN: https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/package_json 8 | */ 9 | let FBTrace = require("firebug.sdk/lib/core/trace.js").FBTrace; 10 | let { main, Loader, override } = require("toolkit/loader"); 11 | 12 | // Get default loader options and create one new 'FBTrace' global, 13 | // so it's automatically available in every loaded module. 14 | // Note that FBTrace global should be used for debugging purposes only. 15 | let options = require("@loader/options"); 16 | 17 | let defaultGlobals = override(require("sdk/system/globals"), { 18 | FBTrace: FBTrace 19 | }); 20 | 21 | options = override(options, { 22 | globals: defaultGlobals, 23 | 24 | // See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1123268 25 | modules: override(options.modules || {}, { 26 | "sdk/addon/window": require("sdk/addon/window") 27 | }) 28 | }); 29 | 30 | // Create custom loader with modified options. 31 | let loader = Loader(options); 32 | let program = main(loader, "./lib/main.js"); 33 | 34 | // Exports from this module 35 | exports.main = program.main; 36 | exports.onUnload = program.onUnload; 37 | -------------------------------------------------------------------------------- /lib/har-driver-front.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | module.metadata = { 6 | "stability": "stable" 7 | }; 8 | 9 | // Add-on SDK 10 | const { Cu } = require("chrome"); 11 | 12 | // DevTools 13 | const DevTools = require("firebug.sdk/lib/core/devtools.js"); 14 | const { Front, FrontClass } = DevTools.Protocol; 15 | 16 | // Firebug SDK 17 | const { Trace, TraceError } = require("firebug.sdk/lib/core/trace.js").get(module.id); 18 | 19 | // HARExportTrigger 20 | const { HarDriverActor } = require("./har-driver-actor.js"); 21 | 22 | /** 23 | * @front This object represents client side for the backend actor. 24 | * 25 | * Read more about Protocol API: 26 | * https://github.com/mozilla/gecko-dev/blob/master/toolkit/devtools/server/docs/protocol.js.md 27 | */ 28 | var HarDriverFront = FrontClass(HarDriverActor, 29 | /** @lends HarDriverFront */ 30 | { 31 | // Initialization 32 | 33 | initialize: function(client, form) { 34 | Front.prototype.initialize.apply(this, arguments); 35 | 36 | Trace.sysout("HarDriverFront.initialize;", this); 37 | 38 | this.actorID = form[HarDriverActor.prototype.typeName]; 39 | this.manage(this); 40 | }, 41 | }); 42 | 43 | // Exports from this module 44 | exports.HarDriverFront = HarDriverFront; 45 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | module.metadata = { 6 | "stability": "stable" 7 | }; 8 | 9 | // Add-on SDK 10 | const { Cu, Ci } = require("chrome"); 11 | const { Trace, TraceError } = require("firebug.sdk/lib/core/trace.js").get(module.id); 12 | const { ToolboxChrome } = require("firebug.sdk/lib/toolbox-chrome.js"); 13 | 14 | // HARExportTrigger 15 | const { TriggerToolboxOverlay } = require("./trigger-toolbox-overlay.js"); 16 | const { TriggerToolbox } = require("./trigger-toolbox.js"); 17 | 18 | /** 19 | * Entry point of the extension. Both 'main' and 'onUnload' methods are 20 | * exported from this module and executed automatically by Add-ons SDK. 21 | */ 22 | function main(options, callbacks) { 23 | ToolboxChrome.initialize(options); 24 | 25 | ToolboxChrome.registerToolboxOverlay(TriggerToolboxOverlay); 26 | TriggerToolbox.initialize(); 27 | } 28 | 29 | /** 30 | * Executed on browser shutdown or when the extension is 31 | * uninstalled/removed/disabled. 32 | */ 33 | function onUnload(reason) { 34 | ToolboxChrome.unregisterToolboxOverlay(TriggerToolboxOverlay); 35 | TriggerToolbox.shutdown(reason); 36 | 37 | ToolboxChrome.shutdown(reason); 38 | } 39 | 40 | // Exports from this module 41 | exports.main = main; 42 | exports.onUnload = onUnload; 43 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2009, Mozilla Foundation 4 | All rights reserved. 5 | 6 | Redistribution and use of this software in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above 10 | copyright notice, this list of conditions and the 11 | following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the 15 | following disclaimer in the documentation and/or other 16 | materials provided with the distribution. 17 | 18 | * Neither the name of Mozilla Foundation nor the names of its 19 | contributors may be used to endorse or promote products 20 | derived from this software without specific prior 21 | written permission of Mozilla Foundation. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 24 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 25 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 29 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 30 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "HAR Export Trigger", 3 | "name": "harexporttrigger", 4 | "version": "0.5.0-beta.10", 5 | "id": "harexporttrigger@getfirebug.com", 6 | "description": "Trigger HAR export any time directly from within a page", 7 | "main": "index.js", 8 | "icon": "chrome://harexporttrigger/skin/icon-16.png", 9 | "author": "Firebug Working Group", 10 | "contributors": [ 11 | "Jan Odvarko (Mozilla Corp.)" 12 | ], 13 | "homepage": "https://github.com/firebug/har-export-trigger/wiki", 14 | "forum": "https://groups.google.com/forum/#!forum/firebug", 15 | "engines": { 16 | "firefox": ">=42.0a1" 17 | }, 18 | "license": "BSD License", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/firebug/har-export-trigger.git" 22 | }, 23 | "dependencies": { 24 | "firebug.sdk": "0.x" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/firebug/har-export-trigger/issues" 28 | }, 29 | "preferences-branch": "netmonitor.har", 30 | "preferences": [{ 31 | "name": "contentAPIToken", 32 | "title": "Content API Token", 33 | "description": "Set to true to expose HAR trigger API into the content", 34 | "type": "string", 35 | "value": "" 36 | }, { 37 | "name": "autoConnect", 38 | "title": "Automatically connect to the backend", 39 | "description": "Automatically connect to the backend, the Toolbox doesn't have to be open.", 40 | "type": "bool", 41 | "value": false 42 | }, { 43 | "name": "enableAutomation", 44 | "title": "Enable automatic collecting of HAR data", 45 | "description": "Flip this option to enable automatic collecting of HAR data, so HAR export can be triggered when necessary", 46 | "type": "bool", 47 | "value": false 48 | }] 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAR Export Trigger 2 | 3 | **On November 14, with the launch of Firefox Quantum (AKA 57), support for old school extensions will stop in Firefox. It means that this extension will no longer work. 4 | New version of this extension built on top of WebExtensions API is available here: 5 | [https://github.com/devtools-html/har-export-trigger](https://github.com/devtools-html/har-export-trigger)** 6 | 7 | Firefox add-on improving automated HAR (HTTP Archive) export of collected 8 | data from the Network panel. This add-on is built on top of native developer 9 | tools in Firefox. Firebug is not needed for this add-on. 10 | 11 | The add-on exports HAR API directly to the page. Any automated system can 12 | be consequently built on top of the API and trigger HAR export using a simple 13 | JavaScript call at any time. It can be also nicely integrated with e.g. 14 | Selenium to implement automated HAR export robots for existing automated test 15 | suites. 16 | 17 | Visit [Home Page](http://www.softwareishard.com/blog/har-export-trigger/) 18 | 19 | License 20 | ------- 21 | HAR Export Trigger is free and open source software distributed under the 22 | [BSD License](https://github.com/firebug/har-export-trigger/blob/master/license.txt). 23 | 24 | Requirements 25 | ------------ 26 | You need Firefox 42+ to run this extension. 27 | 28 | Download 29 | -------- 30 | See the latest [release](https://github.com/firebug/har-export-trigger/releases) 31 | 32 | How To Use 33 | ---------- 34 | This extension exposes HAR API into the content allowing pages to trigger 35 | HAR export as needed. To ensure that API is properly exposed into the 36 | page content you need to yet set the following preference 37 | in your Firefox profile (any string value that is passed into API calls): 38 | 39 | `extensions.netmonitor.har.contentAPIToken` 40 | 41 | To start automated collecting of HTTP data you need to set 42 | the following preference to true: 43 | 44 | `extensions.netmonitor.har.enableAutomation` 45 | 46 | You might also want to set the following preference to true, 47 | so the developer Toolbox doesn't have to be opened. 48 | 49 | `extensions.netmonitor.har.autoConnect` 50 | 51 | --- 52 | 53 | An example script on your page can look like as follows: 54 | 55 | ``` 56 | var options = { 57 | token: "test", // Value of the token in your preferences 58 | getData: true, // True if you want to get HAR data as a string 59 | }; 60 | 61 | HAR.triggerExport(options).then(result => { 62 | console.log(result.data); 63 | }); 64 | ``` 65 | 66 | * Check out [a test page](http://janodvarko.cz/har/tests/har-export-trigger/har-export-api.html) 67 | * See more [HAR API examples](https://github.com/firebug/har-export-trigger/wiki/Examples) 68 | 69 | Build & Run HAR Export Trigger 70 | ------------------------------ 71 | Following instructions describe how to build the extension 72 | from the source and run on your machine. 73 | 74 | 1. Install JPM: `npm install jpm -g` (read more about [installing jpm](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#Installation)) 75 | 2. Get the source: `git clone https://github.com/firebug/har-export-trigger.git` 76 | 3. Install required NPM modules: `npm install` 77 | 4. Run `jpm run -b nightly` in the source directory (learn more about [jpm commands](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#Command_reference)) 78 | 79 | Further Resources 80 | ----------------- 81 | * Home Page: http://www.softwareishard.com/blog/har-export-trigger/ 82 | * HAR Spec: https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html 83 | * HAR Spec (original): http://www.softwareishard.com/blog/har-12-spec/ 84 | * HTTP Archive Viewer: http://www.softwareishard.com/blog/har-viewer/ 85 | * HAR Discussion Group: http://groups.google.com/group/http-archive-specification/ 86 | 87 | -------------------------------------------------------------------------------- /lib/trigger-toolbox.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | module.metadata = { 6 | "stability": "stable" 7 | }; 8 | 9 | // Add-on SDK 10 | const options = require("@loader/options"); 11 | const { Cu } = require("chrome"); 12 | const { Class } = require("sdk/core/heritage"); 13 | const { prefs } = require("sdk/simple-prefs"); 14 | const { defer, resolve } = require("sdk/core/promise"); 15 | const { emit } = require("sdk/event/core"); 16 | 17 | // Firebug.SDK 18 | const { DebuggerServer, DebuggerClient, devtools, safeRequire } = require("firebug.sdk/lib/core/devtools.js"); 19 | const { Trace, TraceError } = require("firebug.sdk/lib/core/trace.js").get(module.id); 20 | const { TriggerToolboxOverlay } = require("./trigger-toolbox-overlay.js"); 21 | 22 | // Platform 23 | const { HarAutomation } = safeRequire(devtools, 24 | "devtools/client/netmonitor/har/har-automation", 25 | "devtools/netmonitor/har/har-automation" 26 | ); 27 | 28 | // Constants 29 | const TargetFactory = devtools.TargetFactory; 30 | 31 | /** 32 | * TODO: docs 33 | */ 34 | const TriggerToolbox = 35 | /** @lends TriggerToolbox */ 36 | { 37 | // Initialization 38 | 39 | initialize: function() { 40 | this.onTabListChanged = this.onTabListChanged.bind(this); 41 | 42 | // Map 43 | this.overlays = new Map(); 44 | 45 | if (!prefs.autoConnect) { 46 | return; 47 | } 48 | 49 | this.connect().then(client => { 50 | this.onReady(client); 51 | }); 52 | }, 53 | 54 | onReady: function(client) { 55 | Trace.sysout("TriggerToolbox.onReady;", client); 56 | 57 | this.client = client; 58 | this.client.addListener("tabListChanged", this.onTabListChanged); 59 | 60 | // Ensure that initial connection for the default tab is created. 61 | this.onTabListChanged(); 62 | }, 63 | 64 | shutdown: function() { 65 | this.close(); 66 | }, 67 | 68 | // Connect/close 69 | 70 | connect: function() { 71 | let deferred = defer(); 72 | 73 | if (!DebuggerServer.initialized) { 74 | DebuggerServer.init(); 75 | DebuggerServer.addBrowserActors(); 76 | } 77 | 78 | let client = new DebuggerClient(DebuggerServer.connectPipe()); 79 | client.connect(() => { 80 | Trace.sysout("TriggerToolbox.connect; DONE", client); 81 | deferred.resolve(client); 82 | }); 83 | 84 | return deferred.promise; 85 | }, 86 | 87 | close: function() { 88 | Trace.sysout("TriggerToolbox.close;"); 89 | 90 | if (!this.target) { 91 | return resolve(); 92 | } 93 | 94 | if (this.destroyer) { 95 | return this.destroyer.promise; 96 | } 97 | 98 | this.destroyer = defer(); 99 | 100 | this.client.close(() => { 101 | this.destroyer.resolve(); 102 | }); 103 | 104 | return this.destroyer.promise; 105 | }, 106 | 107 | // Events 108 | 109 | /** 110 | * Handle 'tabListChanged' event and attach the selected tab. 111 | * Note that there is an extra connection created for each tab. 112 | * So, network events ('tabListChanged' and 'networkEventUpdate') 113 | * are sent only to the attached automation.collector object. 114 | * 115 | * xxxHonza: if we remove the check in HarCollector.onNetworkEventUpdate 116 | * method (labeled as: 'Skip events from unknown actors') we might 117 | * do everything through one connection. But this needs testing. 118 | */ 119 | onTabListChanged: function(eventId, packet) { 120 | Trace.sysout("TriggerToolbox.onTabListChanged;", arguments); 121 | 122 | // Execute 'listTabs' to make sure that 'tabListChanged' event 123 | // will be sent the next time (this is historical complexity 124 | // of the backend). This must be done after every 'tabListChanged'. 125 | this.client.listTabs(response => { 126 | if (response.error) { 127 | Trace.sysout("TriggerToolbox.onTabListChanged; ERROR " + 128 | response.message, response); 129 | return; 130 | } 131 | 132 | let currentTab = response.tabs[response.selected]; 133 | Trace.sysout("TriggerToolbox.onTabListChanged; " + 134 | "(initial connection): " + currentTab.actor, response); 135 | 136 | // Bail out if the tab already has its own connection. 137 | if (this.overlays.has(currentTab.actor)) { 138 | return; 139 | } 140 | 141 | // Create new connection for the current tab. 142 | this.connect().then(client => { 143 | // Execute list of tabs for the new connection (it'll maintain 144 | // it's own tab actors on the backend). 145 | client.listTabs(response => { 146 | let tabForm = response.tabs[response.selected]; 147 | let tabActor = tabForm.actor; 148 | 149 | Trace.sysout("TriggerToolbox.onTabListChanged; " + 150 | "current tab: " + tabActor, tabForm); 151 | 152 | // Attach to the current tab using the new connection. 153 | this.attachTab(tabForm, client).then(result => { 154 | this.overlays.set(currentTab.actor, result); 155 | 156 | Trace.sysout("TriggerToolbox.onTabListChanged; tab attached: " + 157 | currentTab.actor, this.overlays); 158 | }); 159 | }); 160 | }); 161 | }); 162 | }, 163 | 164 | onTabNavigated: function(packet) { 165 | Trace.sysout("TriggerToolbox.onTabNavigated; " + packet.from, packet); 166 | }, 167 | 168 | onTabDetached: function(packet) { 169 | Trace.sysout("TriggerToolbox.onTabDetached; " + packet.from, packet); 170 | 171 | var tabActor = packet.from; 172 | 173 | // Destroy the automation object and close its connection. 174 | var entry = this.overlays.get(tabActor); 175 | if (entry) { 176 | entry.overlay.destroy(); 177 | entry.automation.destroy(); 178 | entry.client.close(); 179 | 180 | this.overlays.delete(tabActor); 181 | } 182 | }, 183 | 184 | /** 185 | * Attach to given tab. 186 | */ 187 | attachTab: function(tab, client) { 188 | Trace.sysout("TriggerToolbox.attachTab; " + tab.actor); 189 | 190 | let config = { 191 | form: tab, 192 | client: client, 193 | chrome: false, 194 | }; 195 | 196 | // Create target, automation object and the toolbox overlay object 197 | // This is what the real Toolbox does (but the Toolbox 198 | // isn't available at the moment). 199 | return TargetFactory.forRemoteTab(config).then(target => { 200 | Trace.sysout("TriggerToolbox.attachTab; target", target); 201 | 202 | // Simulate the Toolbox object since the TriggerToolboxOverlay 203 | // is based on it. 204 | // xxxHonza: If TriggerToolboxOverlay is based on the target 205 | // things would be easier. 206 | var toolbox = { 207 | target: target, 208 | getPanel: function() {}, 209 | on: function() {} 210 | }; 211 | 212 | var automation = new HarAutomation(toolbox); 213 | 214 | // Create toolbox overlay (just like for the real Toolbox). 215 | let options = { 216 | toolbox: toolbox, 217 | automation: automation, 218 | } 219 | 220 | // Instantiate the toolbox overlay and simulate onReady event. 221 | var overlay = new TriggerToolboxOverlay(options); 222 | overlay.onReady({}); 223 | 224 | Trace.sysout("TriggerToolbox.onTabSelected; New automation", options); 225 | 226 | return { 227 | overlay: overlay, 228 | automation: automation, 229 | client: client 230 | }; 231 | }); 232 | } 233 | }; 234 | 235 | // Exports from this module 236 | exports.TriggerToolbox = TriggerToolbox; 237 | -------------------------------------------------------------------------------- /lib/trigger-toolbox-overlay.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | module.metadata = { 6 | "stability": "stable" 7 | }; 8 | 9 | // Add-on SDK 10 | const options = require("@loader/options"); 11 | const { Cu, Ci } = require("chrome"); 12 | const { Class } = require("sdk/core/heritage"); 13 | const { defer, resolve } = require("sdk/core/promise"); 14 | const { on, off, emit } = require("sdk/event/core"); 15 | const { prefs } = require("sdk/simple-prefs"); 16 | 17 | // Platform 18 | const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); 19 | 20 | // DevTools 21 | const { devtools, makeInfallible, safeRequire } = require("firebug.sdk/lib/core/devtools.js"); 22 | 23 | // https://bugzilla.mozilla.org/show_bug.cgi?id=912121 24 | const { get: getHarOverlay } = safeRequire(devtools, 25 | "devtools/client/netmonitor/har/toolbox-overlay", 26 | "devtools/netmonitor/har/toolbox-overlay"); 27 | 28 | // Firebug SDK 29 | const { Trace, TraceError } = require("firebug.sdk/lib/core/trace.js").get(module.id); 30 | const { ToolboxOverlay } = require("firebug.sdk/lib/toolbox-overlay.js"); 31 | const { Rdp } = require("firebug.sdk/lib/core/rdp.js"); 32 | const { Options } = require("firebug.sdk/lib/core/options.js"); 33 | 34 | // HARExportTrigger 35 | const { HarDriverFront } = require("./har-driver-front"); 36 | 37 | // URL of the {@HarDriverActor} module. This module will be 38 | // installed and loaded on the backend. 39 | const actorModuleUrl = options.prefixURI + "lib/har-driver-actor.js"; 40 | 41 | /** 42 | * @overlay This object represents an overlay for the Toolbox. The 43 | * overlay is created when the Toolbox is opened and destroyed when 44 | * the Toolbox is closed. There is one instance of the overlay per 45 | * Toolbox, and so there can be more overlay instances created per 46 | * one browser session. 47 | * 48 | * This extension uses the overlay to register and attach/detach the 49 | * backend actor. 50 | */ 51 | const TriggerToolboxOverlay = Class( 52 | /** @lends TriggerToolboxOverlay */ 53 | { 54 | extends: ToolboxOverlay, 55 | 56 | overlayId: "TriggerToolboxOverlay", 57 | 58 | // Initialization 59 | 60 | initialize: function(options) { 61 | ToolboxOverlay.prototype.initialize.apply(this, arguments); 62 | 63 | Trace.sysout("TriggerToolboxOverlay.initialize;", options); 64 | 65 | this.automation = options.automation; 66 | }, 67 | 68 | destroy: function() { 69 | ToolboxOverlay.prototype.destroy.apply(this, arguments); 70 | 71 | Trace.sysout("TriggerToolboxOverlay.destroy;", arguments); 72 | }, 73 | 74 | // Events 75 | 76 | onReady: function(options) { 77 | ToolboxOverlay.prototype.onReady.apply(this, arguments); 78 | 79 | Trace.sysout("TriggerToolboxOverlay.onReady;", options); 80 | 81 | // Platform support is needed here. 82 | // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1184889 83 | if (typeof getHarOverlay != "function") { 84 | Cu.reportError("Platform support needed, see Bug: " + 85 | "https://bugzilla.mozilla.org/show_bug.cgi?id=1184889"); 86 | return; 87 | } 88 | 89 | // Call make remote to make sure the target.client exists. 90 | let target = this.toolbox.target; 91 | target.makeRemote().then(() => { 92 | // The 'devtools.netmonitor.har.enableAutoExportToFile' option doesn't 93 | // have to be set if users don't want to auto export to file after 94 | // every page load. 95 | // But, if users want to use HAR content API to trigger HAR export 96 | // when needed, HAR automation needs to be activated. Let's do it now 97 | // if 'extensions.netmonitor.har.enableAutomation' preference is true. 98 | if (prefs.enableAutomation && !this.automation) { 99 | Trace.sysout("TriggerToolboxOverlay.onReady; Init automation"); 100 | 101 | // Initialize automation. 102 | let harOverlay = getHarOverlay(this.toolbox); 103 | if (!harOverlay) { 104 | TraceError.sysout("TriggerToolboxOverlay.onReady; ERROR " + 105 | "No HAR Overlay!"); 106 | return; 107 | } 108 | 109 | if (!harOverlay.automation) { 110 | harOverlay.initAutomation(); 111 | } 112 | this.automation = harOverlay.automation; 113 | } 114 | 115 | this.patchAutomation(this.automation); 116 | 117 | // This is a bit hacky, but the HarAutomation starts monitoring 118 | // after target.makeRemote() promise is resolved. 119 | // It's resolved after the parent target.makeRemote() (we are just within) 120 | // finishes. 121 | // So, let's register another promise handler and reset the collector 122 | // after the HarAutomation.startMonitoring() is actually executed. 123 | target.makeRemote().then(() => { 124 | // Make sure the collector exists. The collector is automatically 125 | // created when the page load begins, but the toolbox can be opened 126 | // in the middle of page session (after page load event). 127 | // And HAR API consumer might want to export any time. 128 | if (this.automation && !this.automation.collector) { 129 | this.automation.resetCollector(); 130 | } 131 | }); 132 | 133 | this.attach().then(front => { 134 | Trace.sysout("TriggerToolboxOverlay.onReady; HAR driver ready!"); 135 | }); 136 | }); 137 | }, 138 | 139 | /** 140 | * xxxHonza: this needs better platform API. 141 | * See also: https://github.com/firebug/har-export-trigger/issues/10 142 | */ 143 | patchAutomation: function(automation) { 144 | if (!automation) { 145 | return; 146 | } 147 | 148 | let self = this; 149 | automation.pageLoadDone = function(response) { 150 | Trace.sysout("HarAutomation.patchAutomation;", response); 151 | 152 | if (this.collector) { 153 | this.collector.waitForHarLoad().then(collector => { 154 | self.onPageLoadDone(response); 155 | return this.autoExport(); 156 | }); 157 | } 158 | } 159 | 160 | // xxxHonza: needs testing 161 | /*automation.pageLoadBegin = function(response) { 162 | // If the persist log preference is on do not clear the HAR log. 163 | // It'll be collecting all data as the tab content is reloaded 164 | // or navigated to different location. 165 | // See also: https://github.com/firebug/har-export-trigger/issues/14 166 | let persist = Options.getPref("devtools.webconsole.persistlog"); 167 | if (!persist) { 168 | this.resetCollector(); 169 | } 170 | }*/ 171 | }, 172 | 173 | onPageLoadDone: function(response) { 174 | Trace.sysout("TriggerToolboxOverlay.onPageLoadDone;", response); 175 | 176 | this.front.pageLoadDone(); 177 | }, 178 | 179 | // Backend 180 | 181 | /** 182 | * Attach to the backend actor. 183 | */ 184 | attach: makeInfallible(function() { 185 | Trace.sysout("TriggerToolboxOverlay.attach;"); 186 | 187 | if (this.deferredAttach) { 188 | return this.deferredAttach.promise; 189 | } 190 | 191 | let config = { 192 | prefix: HarDriverFront.prototype.typeName, 193 | actorClass: "HarDriverActor", 194 | frontClass: HarDriverFront, 195 | moduleUrl: actorModuleUrl 196 | }; 197 | 198 | this.deferredAttach = defer(); 199 | let client = this.toolbox.target.client; 200 | 201 | // Register as tab actor. 202 | Rdp.registerTabActor(client, config).then(({registrar, front}) => { 203 | Trace.sysout("TriggerToolboxOverlay.attach; READY", this); 204 | 205 | // xxxHonza: Unregister at shutdown 206 | this.registrar = registrar; 207 | this.front = front; 208 | 209 | this.front.setToken(prefs.contentAPIToken).then(() => { 210 | emit(this, "attach", front); 211 | 212 | // Listen to API calls. Every time the page executes 213 | // HAR API, corresponding event is sent from the backend. 214 | front.on("trigger-export", this.triggerExport.bind(this)); 215 | front.on("clear", this.clear.bind(this)); 216 | 217 | this.deferredAttach.resolve(front); 218 | }); 219 | }); 220 | 221 | return this.deferredAttach.promise; 222 | }), 223 | 224 | // Content API 225 | 226 | /** 227 | * Handle RDP event from the backend. HAR.triggerExport() has been 228 | * executed in the page and existing data in the Network panel 229 | * need to be exported. 230 | */ 231 | triggerExport: function(data) { 232 | Trace.sysout("TriggerToolboxOverlay.triggerExport;", data); 233 | 234 | if (!this.automation) { 235 | let pref1 = "devtools.netmonitor.har.enableAutoExportToFile"; 236 | let pref2 = "extensions.netmonitor.har.enableAutomation"; 237 | 238 | if (!this.automation) { 239 | Cu.reportError("You need to set '" + pref1 + "' or '" + pref2 + 240 | "' pref to enable HAR export through the API " + 241 | "(browser restart is required)"); 242 | } 243 | return; 244 | } 245 | 246 | if (!this.automation.collector) { 247 | Cu.reportError("The HAR collector doesn't exist. Page reload required."); 248 | return; 249 | } 250 | 251 | // Trigger HAR export now! Use executeExport() not triggerExport() 252 | // since we don't want to have the default name automatically provided. 253 | this.automation.executeExport(data).then(jsonString => { 254 | var har = jsonString; 255 | try { 256 | if (jsonString) { 257 | har = JSON.parse(jsonString); 258 | } 259 | } catch (err) { 260 | Trace.sysout("TriggerToolboxOverlay.triggerExport; ERROR " + 261 | "Failed to parse HAR log " + err); 262 | } 263 | 264 | // Send event back to the backend notifying that it has 265 | // finished. If 'getData' is true include also the HAR string. 266 | // The content API call will be resolved as soon as the packet 267 | // arrives on the backend. 268 | if (data.id) { 269 | this.front.exportDone({ 270 | id: data.id, 271 | data: data.getData ? jsonString : undefined, 272 | }); 273 | } 274 | }); 275 | }, 276 | 277 | /** 278 | * Handle RDP event from the backend. HAR.clear() has been 279 | * executed in the page and the Network panel content 280 | * needs to be cleared. 281 | */ 282 | clear: function() { 283 | Trace.sysout("TriggerToolboxOverlay.clear;"); 284 | 285 | let panel = this.toolbox.getPanel("netmonitor"); 286 | 287 | // Clean up also the HAR collector. 288 | this.automation.resetCollector(); 289 | 290 | // Clear the Network panel content. The panel doesn't 291 | // have to exist if the user doesn't select it yet. 292 | if (panel) { 293 | let view = panel.panelWin.NetMonitorView; 294 | view.RequestsMenu.clear(); 295 | }; 296 | }, 297 | }); 298 | 299 | // Exports from this module 300 | exports.TriggerToolboxOverlay = TriggerToolboxOverlay; 301 | -------------------------------------------------------------------------------- /lib/har-driver-actor.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | /** 6 | * This module is loaded on the backend (can be a remote device) where 7 | * some module or features (such as Tracing console) don't have to 8 | * be available. Also Firebug SDK isn't available on the backend. 9 | */ 10 | 11 | // Add-on SDK 12 | const { Cu, Ci, components } = require("chrome"); 13 | const Events = require("sdk/event/core"); 14 | 15 | // Platform 16 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 17 | 18 | function safeImport(...args) { 19 | for (var i=0; i {}}; 68 | 69 | /** 70 | * Helper actor state watcher. 71 | * expectState has been introduced in Fx42 72 | * TODO: const { expectState } = require("devtools/server/actors/common"); 73 | */ 74 | function expectState(expectedState, method) { 75 | return function(...args) { 76 | if (this.state !== expectedState) { 77 | Trace.sysout("actor.expectState; ERROR wrong state, expected '" + 78 | expectedState + "', but current state is '" + this.state + "'" + 79 | ", method: " + method); 80 | 81 | let msg = "Wrong State: Expected '" + expectedState + "', but current " + 82 | "state is '" + this.state + "'"; 83 | 84 | return Promise.reject(new Error(msg)); 85 | } 86 | 87 | try { 88 | return method.apply(this, args); 89 | } catch (err) { 90 | Cu.reportError("actor.js; expectState EXCEPTION " + err, err); 91 | } 92 | }; 93 | } 94 | 95 | /** 96 | * @actor 97 | * 98 | * Read more about Protocol API: 99 | * https://github.com/mozilla/gecko-dev/blob/master/toolkit/devtools/server/docs/protocol.js.md 100 | */ 101 | var HarDriverActor = ActorClass( 102 | /** @lends HarDriverActor */ 103 | { 104 | typeName: "harExportDriver", 105 | 106 | /** 107 | * Events emitted by this actor. 108 | */ 109 | events: { 110 | "trigger-export": { 111 | type: "trigger-export", 112 | data: Arg(0, "json") 113 | }, 114 | "clear": { 115 | type: "clear", 116 | }, 117 | }, 118 | 119 | exportsInProgress: new Map(), 120 | 121 | // Initialization 122 | 123 | initialize: function(conn, parent) { 124 | Trace.sysout("HarDriverActor.initialize; parent: " + 125 | parent.actorID + ", conn: " + conn.prefix, this); 126 | 127 | Actor.prototype.initialize.call(this, conn); 128 | 129 | this.parent = parent; 130 | this.state = "detached"; 131 | }, 132 | 133 | /** 134 | * The destroy is only called automatically by the framework (parent actor) 135 | * if an actor is instantiated by a parent actor. 136 | */ 137 | destroy: function() { 138 | Trace.sysout("HarDriverActor.destroy; state: " + this.state, arguments); 139 | 140 | if (this.state === "attached") { 141 | this.detach(); 142 | } 143 | 144 | Actor.prototype.destroy.call(this); 145 | }, 146 | 147 | /** 148 | * Automatically executed by the framework when the parent connection 149 | * is closed. 150 | */ 151 | disconnect: function() { 152 | Trace.sysout("HarDriverActor.disconnect; state: " + this.state, arguments); 153 | 154 | if (this.state === "attached") { 155 | this.detach(); 156 | } 157 | }, 158 | 159 | /** 160 | * Attach to this actor. Executed when the front (client) is attaching 161 | * to this actor. 162 | */ 163 | attach: method(expectState("detached", function() { 164 | Trace.sysout("HarDriverActor.attach;", arguments); 165 | 166 | this.state = "attached"; 167 | }), { 168 | request: {}, 169 | response: { 170 | type: "attached" 171 | } 172 | }), 173 | 174 | /** 175 | * Set UI stylesheet for anonymous content (sent from the client). 176 | */ 177 | setToken: method(expectState("attached", function(token) { 178 | Trace.sysout("HarDriverActor.setToken;", arguments); 179 | 180 | this.token = token; 181 | 182 | const notifyMask = Ci.nsIWebProgress.NOTIFY_STATUS | 183 | Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | 184 | Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT; 185 | 186 | let win = this.parent.originalWindow; 187 | 188 | // Do not overwrite HAR object. It might be already there if 189 | // autoConntect is on or there might be an existing object 190 | // with the same on the page. 191 | if (win.wrappedJSObject.HAR) { 192 | return; 193 | } 194 | 195 | // The client just attached to this actor, let's expose 196 | // HAR API to the content. 197 | this.parent.webProgress.addProgressListener(this, notifyMask); 198 | this.api = new ExportDriverApi(this); 199 | this.exposeToContentInternal(this.parent.originalWindow); 200 | }), { 201 | request: { 202 | token: Arg(0, "string"), 203 | }, 204 | response: { 205 | type: "api-exposed" 206 | } 207 | }), 208 | 209 | /** 210 | * Detach from this actor. Executed when the front (client) detaches 211 | * from this actor. 212 | */ 213 | detach: method(expectState("attached", function() { 214 | Trace.sysout("HarDriverActor.detach;", arguments); 215 | 216 | this.state = "detached"; 217 | 218 | if (this.api) { 219 | this.parent.webProgress.removeProgressListener(this); 220 | } 221 | }), { 222 | request: {}, 223 | response: { 224 | type: "detached" 225 | } 226 | }), 227 | 228 | /** 229 | * The client calls this method when page is loaded. 230 | */ 231 | pageLoadDone: method(expectState("attached", function() { 232 | let win = this.parent.originalWindow; 233 | let event = new win.MessageEvent("har-page-ready"); 234 | win.dispatchEvent(event); 235 | return true; 236 | }), { 237 | request: {}, 238 | response: {} 239 | }), 240 | 241 | /** 242 | * The client calls this method when HAR export has finished. 243 | * It allows to resolve associated promise and let the content 244 | * caller know that the export is done. 245 | */ 246 | exportDone: method(expectState("attached", function(options) { 247 | if (!options.id) { 248 | return false; 249 | } 250 | 251 | // The HAR export is identified by ID and there should be 252 | // corresponding promise in the exports-in-progress array. 253 | let resolve = this.exportsInProgress.get(options.id); 254 | if (!resolve) { 255 | return false; 256 | } 257 | 258 | // Let's resolve the promise. If 'getData' property has been 259 | // set the result HAR string is also passed to the caller. 260 | let win = this.parent.originalWindow; 261 | let result = new win.Object(); 262 | result.data = options.data; 263 | resolve(result); 264 | 265 | return true; 266 | }), { 267 | request: { 268 | options: Arg(0, "json"), 269 | }, 270 | response: { 271 | result: RetVal("boolean") 272 | } 273 | }), 274 | 275 | // Internals 276 | 277 | exposeToContentInternal: function(win) { 278 | if (win.hasOwnProperty("HAR")) { 279 | return; 280 | } 281 | 282 | exportIntoContentScope(win, this.api, "HAR"); 283 | 284 | let event = new win.MessageEvent("har-api-ready"); 285 | win.dispatchEvent(event); 286 | }, 287 | 288 | // onWebProgressListener 289 | 290 | QueryInterface: XPCOMUtils.generateQI([ 291 | Ci.nsIWebProgressListener, 292 | Ci.nsISupportsWeakReference, 293 | Ci.nsISupports, 294 | ]), 295 | 296 | onStateChange: method(expectState("attached", function(aProgress, aRequest, 297 | aFlag, aStatus) { 298 | 299 | let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START; 300 | let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP; 301 | let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; 302 | let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; 303 | let isTransferring = aFlag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; 304 | 305 | let win = aProgress.DOMWindow; 306 | if (isDocument && (isTransferring || isStop)) { 307 | this.exposeToContentInternal(win); 308 | } 309 | })), 310 | }); 311 | 312 | // Export Driver Content API 313 | 314 | /** 315 | * This object implements content API. Every call is checked 316 | * against the "contentAPIToken" that needs to be set in 317 | * Firefox preferences. If the token doesn't match the API 318 | * is not executed. 319 | */ 320 | function ExportDriverApi(actor) { 321 | let exportID = 0; 322 | let win = actor.parent.originalWindow; 323 | let exportsInProgress = actor.exportsInProgress; 324 | 325 | function securityCheck(method) { 326 | return function(options) { 327 | if (options.token != actor.token) { 328 | let pref = "extensions.netmonitor.har.contentAPIToken"; 329 | let msg = "Security check didn't pass. You need to set '" + 330 | pref + "' pref to match the string token passed into " + 331 | "HAR object API call (browser restart is required)"; 332 | Cu.reportError(msg); 333 | return win.Promise.reject(msg); 334 | } 335 | 336 | try { 337 | return method.apply(this, arguments); 338 | } catch (err) { 339 | Cu.reportError(err); 340 | } 341 | }; 342 | } 343 | 344 | /** 345 | * Trigger HAR export. 346 | */ 347 | this.triggerExport = securityCheck(function(options) { 348 | let id = ++exportID; 349 | 350 | let promise = new win.Promise( 351 | function(resolve, reject) { 352 | exportsInProgress.set(id, resolve); 353 | } 354 | ); 355 | 356 | Events.emit(actor, "trigger-export", { 357 | id: id, 358 | fileName: options.fileName, 359 | compress: options.compress, 360 | title: options.title, 361 | jsonp: options.jsonp, 362 | includeResponseBodies: options.includeResponseBodies, 363 | jsonpCallback: options.jsonpCallback, 364 | forceExport: options.forceExport, 365 | getData: options.getData, 366 | }); 367 | 368 | return promise; 369 | }); 370 | 371 | /** 372 | * Clean up the Network monitor panel. 373 | */ 374 | this.clear = securityCheck(function(options) { 375 | Events.emit(actor, "clear"); 376 | }); 377 | } 378 | 379 | // Helpers 380 | 381 | function exportIntoContentScope(win, obj, defineAs) { 382 | let clone = Cu.createObjectIn(win, { 383 | defineAs: defineAs 384 | }); 385 | 386 | let props = Object.getOwnPropertyNames(obj); 387 | 388 | for (var i=0; i