summaryrefslogtreecommitdiffstats
path: root/devtools/shared/gcli/commands
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/gcli/commands')
-rw-r--r--devtools/shared/gcli/commands/addon.js320
-rw-r--r--devtools/shared/gcli/commands/appcache.js186
-rw-r--r--devtools/shared/gcli/commands/calllog.js219
-rw-r--r--devtools/shared/gcli/commands/cmd.js178
-rw-r--r--devtools/shared/gcli/commands/cookie.js300
-rw-r--r--devtools/shared/gcli/commands/csscoverage.js201
-rw-r--r--devtools/shared/gcli/commands/folder.js77
-rw-r--r--devtools/shared/gcli/commands/highlight.js158
-rw-r--r--devtools/shared/gcli/commands/index.js179
-rw-r--r--devtools/shared/gcli/commands/inject.js86
-rw-r--r--devtools/shared/gcli/commands/jsb.js134
-rw-r--r--devtools/shared/gcli/commands/listen.js106
-rw-r--r--devtools/shared/gcli/commands/mdn.js83
-rw-r--r--devtools/shared/gcli/commands/measure.js112
-rw-r--r--devtools/shared/gcli/commands/media.js56
-rw-r--r--devtools/shared/gcli/commands/moz.build30
-rw-r--r--devtools/shared/gcli/commands/pagemod.js276
-rw-r--r--devtools/shared/gcli/commands/paintflashing.js201
-rw-r--r--devtools/shared/gcli/commands/qsa.js24
-rw-r--r--devtools/shared/gcli/commands/restart.js77
-rw-r--r--devtools/shared/gcli/commands/rulers.js110
-rw-r--r--devtools/shared/gcli/commands/screenshot.js579
-rw-r--r--devtools/shared/gcli/commands/security.js328
23 files changed, 4020 insertions, 0 deletions
diff --git a/devtools/shared/gcli/commands/addon.js b/devtools/shared/gcli/commands/addon.js
new file mode 100644
index 000000000..9a38142a3
--- /dev/null
+++ b/devtools/shared/gcli/commands/addon.js
@@ -0,0 +1,320 @@
+/* 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";
+
+/**
+ * You can't require the AddonManager in a child process, but GCLI wants to
+ * check for 'items' in all processes, so we return empty array if the
+ * AddonManager is not available
+ */
+function getAddonManager() {
+ try {
+ return {
+ AddonManager: require("resource://gre/modules/AddonManager.jsm").AddonManager,
+ addonManagerActive: true
+ };
+ }
+ catch (ex) {
+ // Fake up an AddonManager just enough to let the file load
+ return {
+ AddonManager: {
+ getAllAddons() {},
+ getAddonsByTypes() {}
+ },
+ addonManagerActive: false
+ };
+ }
+}
+
+const { Cc, Ci, Cu } = require("chrome");
+const { AddonManager, addonManagerActive } = getAddonManager();
+const l10n = require("gcli/l10n");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+/**
+ * Takes a function that uses a callback as its last parameter, and returns a
+ * new function that returns a promise instead.
+ * This should probably live in async-util
+ */
+const promiseify = function(scope, functionWithLastParamCallback) {
+ return (...args) => {
+ return new Promise(resolve => {
+ args.push((...results) => {
+ resolve(results.length > 1 ? results : results[0]);
+ });
+ functionWithLastParamCallback.apply(scope, args);
+ });
+ }
+};
+
+// Convert callback based functions to promise based ones
+const getAllAddons = promiseify(AddonManager, AddonManager.getAllAddons);
+const getAddonsByTypes = promiseify(AddonManager, AddonManager.getAddonsByTypes);
+
+/**
+ * Return a string array containing the pending operations on an addon
+ */
+function pendingOperations(addon) {
+ let allOperations = [
+ "PENDING_ENABLE", "PENDING_DISABLE", "PENDING_UNINSTALL",
+ "PENDING_INSTALL", "PENDING_UPGRADE"
+ ];
+ return allOperations.reduce(function(operations, opName) {
+ return addon.pendingOperations & AddonManager[opName] ?
+ operations.concat(opName) :
+ operations;
+ }, []);
+}
+
+var items = [
+ {
+ item: "type",
+ name: "addon",
+ parent: "selection",
+ stringifyProperty: "name",
+ cacheable: true,
+ constructor: function() {
+ // Tell GCLI to clear the cache of addons when one is added or removed
+ let listener = {
+ onInstalled: addon => { this.clearCache(); },
+ onUninstalled: addon => { this.clearCache(); },
+ };
+ AddonManager.addAddonListener(listener);
+ },
+ lookup: function() {
+ return getAllAddons().then(addons => {
+ return addons.map(addon => {
+ let name = addon.name + " " + addon.version;
+ name = name.trim().replace(/\s/g, "_");
+ return { name: name, value: addon };
+ });
+ });
+ }
+ },
+ {
+ name: "addon",
+ description: l10n.lookup("addonDesc")
+ },
+ {
+ name: "addon list",
+ description: l10n.lookup("addonListDesc"),
+ returnType: "addonsInfo",
+ params: [{
+ name: "type",
+ type: {
+ name: "selection",
+ data: [ "dictionary", "extension", "locale", "plugin", "theme", "all" ]
+ },
+ defaultValue: "all",
+ description: l10n.lookup("addonListTypeDesc")
+ }],
+ exec: function(args, context) {
+ let types = (args.type === "all") ? null : [ args.type ];
+ return getAddonsByTypes(types).then(addons => {
+ addons = addons.map(function(addon) {
+ return {
+ name: addon.name,
+ version: addon.version,
+ isActive: addon.isActive,
+ pendingOperations: pendingOperations(addon)
+ };
+ });
+ return { addons: addons, type: args.type };
+ });
+ }
+ },
+ {
+ item: "converter",
+ from: "addonsInfo",
+ to: "view",
+ exec: function(addonsInfo, context) {
+ if (!addonsInfo.addons.length) {
+ return context.createView({
+ html: "<p>${message}</p>",
+ data: { message: l10n.lookup("addonNoneOfType") }
+ });
+ }
+
+ let headerLookups = {
+ "dictionary": "addonListDictionaryHeading",
+ "extension": "addonListExtensionHeading",
+ "locale": "addonListLocaleHeading",
+ "plugin": "addonListPluginHeading",
+ "theme": "addonListThemeHeading",
+ "all": "addonListAllHeading"
+ };
+ let header = l10n.lookup(headerLookups[addonsInfo.type] ||
+ "addonListUnknownHeading");
+
+ let operationLookups = {
+ "PENDING_ENABLE": "addonPendingEnable",
+ "PENDING_DISABLE": "addonPendingDisable",
+ "PENDING_UNINSTALL": "addonPendingUninstall",
+ "PENDING_INSTALL": "addonPendingInstall",
+ "PENDING_UPGRADE": "addonPendingUpgrade"
+ };
+ function lookupOperation(opName) {
+ let lookupName = operationLookups[opName];
+ return lookupName ? l10n.lookup(lookupName) : opName;
+ }
+
+ function arrangeAddons(addons) {
+ let enabledAddons = [];
+ let disabledAddons = [];
+ addons.forEach(function(addon) {
+ if (addon.isActive) {
+ enabledAddons.push(addon);
+ } else {
+ disabledAddons.push(addon);
+ }
+ });
+
+ function compareAddonNames(nameA, nameB) {
+ return String.localeCompare(nameA.name, nameB.name);
+ }
+ enabledAddons.sort(compareAddonNames);
+ disabledAddons.sort(compareAddonNames);
+
+ return enabledAddons.concat(disabledAddons);
+ }
+
+ function isActiveForToggle(addon) {
+ return (addon.isActive && ~~addon.pendingOperations.indexOf("PENDING_DISABLE"));
+ }
+
+ return context.createView({
+ html:
+ "<table>" +
+ " <caption>${header}</caption>" +
+ " <tbody>" +
+ " <tr foreach='addon in ${addons}'" +
+ " class=\"gcli-addon-${addon.status}\">" +
+ " <td>${addon.name} ${addon.version}</td>" +
+ " <td>${addon.pendingOperations}</td>" +
+ " <td>" +
+ " <span class='gcli-out-shortcut'" +
+ " data-command='addon ${addon.toggleActionName} ${addon.label}'" +
+ " onclick='${onclick}' ondblclick='${ondblclick}'" +
+ " >${addon.toggleActionMessage}</span>" +
+ " </td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>",
+ data: {
+ header: header,
+ addons: arrangeAddons(addonsInfo.addons).map(function(addon) {
+ return {
+ name: addon.name,
+ label: addon.name.replace(/\s/g, "_") +
+ (addon.version ? "_" + addon.version : ""),
+ status: addon.isActive ? "enabled" : "disabled",
+ version: addon.version,
+ pendingOperations: addon.pendingOperations.length ?
+ (" (" + l10n.lookup("addonPending") + ": "
+ + addon.pendingOperations.map(lookupOperation).join(", ")
+ + ")") :
+ "",
+ toggleActionName: isActiveForToggle(addon) ? "disable": "enable",
+ toggleActionMessage: isActiveForToggle(addon) ?
+ l10n.lookup("addonListOutDisable") :
+ l10n.lookup("addonListOutEnable")
+ };
+ }),
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon enable",
+ description: l10n.lookup("addonEnableDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (args.addon.userDisabled) {
+ args.addon.userDisabled = false;
+ return l10n.lookupFormat("addonEnabled", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyEnabled", [ name ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon disable",
+ description: l10n.lookup("addonDisableDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ // If the addon is not disabled or is set to "click to play" then
+ // disable it. Otherwise display the message "Add-on is already
+ // disabled."
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (!args.addon.userDisabled ||
+ args.addon.userDisabled === AddonManager.STATE_ASK_TO_ACTIVATE) {
+ args.addon.userDisabled = true;
+ return l10n.lookupFormat("addonDisabled", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyDisabled", [ name ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "addon ctp",
+ description: l10n.lookup("addonCtpDesc"),
+ params: [
+ {
+ name: "addon",
+ type: "addon",
+ description: l10n.lookup("addonNameDesc")
+ }
+ ],
+ exec: function(args, context) {
+ let name = (args.addon.name + " " + args.addon.version).trim();
+ if (args.addon.type !== "plugin") {
+ return l10n.lookupFormat("addonCantCtp", [ name ]);
+ }
+
+ if (!args.addon.userDisabled ||
+ args.addon.userDisabled === true) {
+ args.addon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;
+
+ if (args.addon.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE) {
+ // Some plugins (e.g. OpenH264 shipped with Firefox) cannot be set to
+ // click-to-play. Handle this.
+
+ return l10n.lookupFormat("addonNoCtp", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonCtp", [ name ]);
+ }
+
+ return l10n.lookupFormat("addonAlreadyCtp", [ name ]);
+ }
+ }
+];
+
+exports.items = addonManagerActive ? items : [];
diff --git a/devtools/shared/gcli/commands/appcache.js b/devtools/shared/gcli/commands/appcache.js
new file mode 100644
index 000000000..0789fb5a0
--- /dev/null
+++ b/devtools/shared/gcli/commands/appcache.js
@@ -0,0 +1,186 @@
+/* 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 l10n = require("gcli/l10n");
+
+loader.lazyImporter(this, "AppCacheUtils", "resource://devtools/client/shared/AppCacheUtils.jsm");
+
+exports.items = [
+ {
+ item: "command",
+ name: "appcache",
+ description: l10n.lookup("appCacheDesc")
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache validate",
+ description: l10n.lookup("appCacheValidateDesc"),
+ manual: l10n.lookup("appCacheValidateManual"),
+ returnType: "appcacheerrors",
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "uri",
+ description: l10n.lookup("appCacheValidateUriDesc"),
+ defaultValue: null,
+ }
+ ]
+ }],
+ exec: function(args, context) {
+ let utils;
+ let deferred = context.defer();
+
+ if (args.uri) {
+ utils = new AppCacheUtils(args.uri);
+ } else {
+ utils = new AppCacheUtils(context.environment.document);
+ }
+
+ utils.validateManifest().then(function(errors) {
+ deferred.resolve([errors, utils.manifestURI || "-"]);
+ });
+
+ return deferred.promise;
+ }
+ },
+ {
+ item: "converter",
+ from: "appcacheerrors",
+ to: "view",
+ exec: function([errors, manifestURI], context) {
+ if (errors.length == 0) {
+ return context.createView({
+ html: "<span>" + l10n.lookup("appCacheValidatedSuccessfully") + "</span>"
+ });
+ }
+
+ return context.createView({
+ html:
+ "<div>" +
+ " <h4>Manifest URI: ${manifestURI}</h4>" +
+ " <ol>" +
+ " <li foreach='error in ${errors}'>${error.msg}</li>" +
+ " </ol>" +
+ "</div>",
+ data: {
+ errors: errors,
+ manifestURI: manifestURI
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache clear",
+ description: l10n.lookup("appCacheClearDesc"),
+ manual: l10n.lookup("appCacheClearManual"),
+ exec: function(args, context) {
+ let utils = new AppCacheUtils(args.uri);
+ utils.clearAll();
+
+ return l10n.lookup("appCacheClearCleared");
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache list",
+ description: l10n.lookup("appCacheListDesc"),
+ manual: l10n.lookup("appCacheListManual"),
+ returnType: "appcacheentries",
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "string",
+ name: "search",
+ description: l10n.lookup("appCacheListSearchDesc"),
+ defaultValue: null,
+ },
+ ]
+ }],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.listEntries(args.search);
+ }
+ },
+ {
+ item: "converter",
+ from: "appcacheentries",
+ to: "view",
+ exec: function(entries, context) {
+ return context.createView({
+ html: "" +
+ "<ul class='gcli-appcache-list'>" +
+ " <li foreach='entry in ${entries}'>" +
+ " <table class='gcli-appcache-detail'>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListKey") + "</td>" +
+ " <td>${entry.key}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListFetchCount") + "</td>" +
+ " <td>${entry.fetchCount}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListLastFetched") + "</td>" +
+ " <td>${entry.lastFetched}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListLastModified") + "</td>" +
+ " <td>${entry.lastModified}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListExpirationTime") + "</td>" +
+ " <td>${entry.expirationTime}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListDataSize") + "</td>" +
+ " <td>${entry.dataSize}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("appCacheListDeviceID") + "</td>" +
+ " <td>${entry.deviceID} <span class='gcli-out-shortcut' " +
+ "onclick='${onclick}' ondblclick='${ondblclick}' " +
+ "data-command='appcache viewentry ${entry.key}'" +
+ ">" + l10n.lookup("appCacheListViewEntry") + "</span>" +
+ " </td>" +
+ " </tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>",
+ data: {
+ entries: entries,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "appcache viewentry",
+ description: l10n.lookup("appCacheViewEntryDesc"),
+ manual: l10n.lookup("appCacheViewEntryManual"),
+ params: [
+ {
+ type: "string",
+ name: "key",
+ description: l10n.lookup("appCacheViewEntryKey"),
+ defaultValue: null,
+ }
+ ],
+ exec: function(args, context) {
+ let utils = new AppCacheUtils();
+ return utils.viewEntry(args.key);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/calllog.js b/devtools/shared/gcli/commands/calllog.js
new file mode 100644
index 000000000..c0f21aeab
--- /dev/null
+++ b/devtools/shared/gcli/commands/calllog.js
@@ -0,0 +1,219 @@
+/* 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 { Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const gcli = require("gcli/index");
+const Debugger = require("Debugger");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+
+var debuggers = [];
+var chromeDebuggers = [];
+var sandboxes = [];
+
+exports.items = [
+ {
+ name: "calllog",
+ description: l10n.lookup("calllogDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog start",
+ description: l10n.lookup("calllogStartDesc"),
+
+ exec: function(args, context) {
+ let contentWindow = context.environment.window;
+
+ let dbg = new Debugger(contentWindow);
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log("Method call: " + this.callDescription(frame));
+ }.bind(this);
+
+ debuggers.push(dbg);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return l10n.lookup("calllogStartReply");
+ },
+
+ callDescription: function(frame) {
+ let name = "<anonymous>";
+ if (frame.callee.name) {
+ name = frame.callee.name;
+ }
+ else {
+ let desc = frame.callee.getOwnPropertyDescriptor("displayName");
+ if (desc && desc.value && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null) {
+ return uneval(value);
+ }
+ return "[object " + value.class + "]";
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog stop",
+ description: l10n.lookup("calllogStopDesc"),
+
+ exec: function(args, context) {
+ let numDebuggers = debuggers.length;
+ if (numDebuggers == 0) {
+ return l10n.lookup("calllogStopNoLogging");
+ }
+
+ for (let dbg of debuggers) {
+ dbg.onEnterFrame = undefined;
+ }
+ debuggers = [];
+
+ return l10n.lookupFormat("calllogStopReply", [ numDebuggers ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog chromestart",
+ description: l10n.lookup("calllogChromeStartDesc"),
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ params: [
+ {
+ name: "sourceType",
+ type: {
+ name: "selection",
+ data: ["content-variable", "chrome-variable", "jsm", "javascript"]
+ }
+ },
+ {
+ name: "source",
+ type: "string",
+ description: l10n.lookup("calllogChromeSourceTypeDesc"),
+ manual: l10n.lookup("calllogChromeSourceTypeManual"),
+ }
+ ],
+ exec: function(args, context) {
+ let globalObj;
+ let contentWindow = context.environment.window;
+
+ if (args.sourceType == "jsm") {
+ try {
+ globalObj = Cu.import(args.source, {});
+ } catch (e) {
+ return l10n.lookup("callLogChromeInvalidJSM");
+ }
+ } else if (args.sourceType == "content-variable") {
+ if (args.source in contentWindow) {
+ globalObj = Cu.getGlobalForObject(contentWindow[args.source]);
+ } else {
+ throw new Error(l10n.lookup("callLogChromeVarNotFoundContent"));
+ }
+ } else if (args.sourceType == "chrome-variable") {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ if (args.source in chromeWin) {
+ globalObj = Cu.getGlobalForObject(chromeWin[args.source]);
+ } else {
+ return l10n.lookup("callLogChromeVarNotFoundChrome");
+ }
+ } else {
+ let chromeWin = context.environment.chromeDocument.defaultView;
+ let sandbox = new Cu.Sandbox(chromeWin,
+ {
+ sandboxPrototype: chromeWin,
+ wantXrays: false,
+ sandboxName: "gcli-cmd-calllog-chrome"
+ });
+ let returnVal;
+ try {
+ returnVal = Cu.evalInSandbox(args.source, sandbox, "ECMAv5");
+ sandboxes.push(sandbox);
+ } catch(e) {
+ // We need to save the message before cleaning up else e contains a dead
+ // object.
+ let msg = l10n.lookup("callLogChromeEvalException") + ": " + e;
+ Cu.nukeSandbox(sandbox);
+ return msg;
+ }
+
+ if (typeof returnVal == "undefined") {
+ return l10n.lookup("callLogChromeEvalNeedsObject");
+ }
+
+ globalObj = Cu.getGlobalForObject(returnVal);
+ }
+
+ let dbg = new Debugger(globalObj);
+ chromeDebuggers.push(dbg);
+
+ dbg.onEnterFrame = function(frame) {
+ // BUG 773652 - Make the output from the GCLI calllog command nicer
+ contentWindow.console.log(l10n.lookup("callLogChromeMethodCall") +
+ ": " + this.callDescription(frame));
+ }.bind(this);
+
+ let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+
+ return l10n.lookup("calllogChromeStartReply");
+ },
+
+ valueToString: function(value) {
+ if (typeof value !== "object" || value === null)
+ return uneval(value);
+ return "[object " + value.class + "]";
+ },
+
+ callDescription: function(frame) {
+ let name = frame.callee.name || l10n.lookup("callLogChromeAnonFunction");
+ let args = frame.arguments.map(this.valueToString).join(", ");
+ return name + "(" + args + ")";
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "calllog chromestop",
+ description: l10n.lookup("calllogChromeStopDesc"),
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ exec: function(args, context) {
+ let numDebuggers = chromeDebuggers.length;
+ if (numDebuggers == 0) {
+ return l10n.lookup("calllogChromeStopNoLogging");
+ }
+
+ for (let dbg of chromeDebuggers) {
+ dbg.onEnterFrame = undefined;
+ dbg.enabled = false;
+ }
+ for (let sandbox of sandboxes) {
+ Cu.nukeSandbox(sandbox);
+ }
+ chromeDebuggers = [];
+ sandboxes = [];
+
+ return l10n.lookupFormat("calllogChromeStopReply", [ numDebuggers ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/cmd.js b/devtools/shared/gcli/commands/cmd.js
new file mode 100644
index 000000000..1777ed960
--- /dev/null
+++ b/devtools/shared/gcli/commands/cmd.js
@@ -0,0 +1,178 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { TextEncoder, TextDecoder } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
+const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
+
+loader.lazyGetter(this, "prefBranch", function() {
+ let prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
+ return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+});
+
+loader.lazyGetter(this, "supportsString", function() {
+ return Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+});
+
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+
+const PREF_DIR = "devtools.commands.dir";
+
+/**
+ * Load all the .mozcmd files in the directory pointed to by PREF_DIR
+ * @return A promise of an array of items suitable for gcli.addItems or
+ * using in gcli.addItemsByModule
+ */
+function loadItemsFromMozDir() {
+ let dirName = prefBranch.getComplexValue(PREF_DIR,
+ Ci.nsISupportsString).data.trim();
+ if (dirName == "") {
+ return Promise.resolve([]);
+ }
+
+ // replaces ~ with the home directory path in unix and windows
+ if (dirName.indexOf("~") == 0) {
+ let dirService = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+ let homeDirFile = dirService.get("Home", Ci.nsIFile);
+ let homeDir = homeDirFile.path;
+ dirName = dirName.substr(1);
+ dirName = homeDir + dirName;
+ }
+
+ // statPromise resolves to nothing if dirName is a directory, or it
+ // rejects with an error message otherwise
+ let statPromise = OS.File.stat(dirName);
+ statPromise = statPromise.then(
+ function onSuccess(stat) {
+ if (!stat.isDir) {
+ throw new Error("'" + dirName + "' is not a directory.");
+ }
+ },
+ function onFailure(reason) {
+ if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) {
+ throw new Error("'" + dirName + "' does not exist.");
+ } else {
+ throw reason;
+ }
+ }
+ );
+
+ // We need to return (a promise of) an array of items from the *.mozcmd
+ // files in dirName (which we can assume to be a valid directory now)
+ return statPromise.then(() => {
+ let itemPromises = [];
+
+ let iterator = new OS.File.DirectoryIterator(dirName);
+ let iterPromise = iterator.forEach(entry => {
+ if (entry.name.match(/.*\.mozcmd$/) && !entry.isDir) {
+ itemPromises.push(loadCommandFile(entry));
+ }
+ });
+
+ return iterPromise.then(() => {
+ iterator.close();
+ return Promise.all(itemPromises).then((itemsArray) => {
+ return itemsArray.reduce((prev, curr) => {
+ return prev.concat(curr);
+ }, []);
+ });
+ }, reason => { iterator.close(); throw reason; });
+ });
+}
+
+exports.mozDirLoader = function(name) {
+ return loadItemsFromMozDir().then(items => {
+ return { items: items };
+ });
+};
+
+/**
+ * Load the commands from a single file
+ * @param OS.File.DirectoryIterator.Entry entry The DirectoryIterator
+ * Entry of the file containing the commands that we should read
+ */
+function loadCommandFile(entry) {
+ let readPromise = OS.File.read(entry.path);
+ return readPromise = readPromise.then(array => {
+ let decoder = new TextDecoder();
+ let source = decoder.decode(array);
+ var principal = Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(Ci.nsIPrincipal);
+
+ let sandbox = new Cu.Sandbox(principal, {
+ sandboxName: entry.path
+ });
+ let data = Cu.evalInSandbox(source, sandbox, "1.8", entry.name, 1);
+
+ if (!Array.isArray(data)) {
+ console.error("Command file '" + entry.name + "' does not have top level array.");
+ return;
+ }
+
+ return data;
+ });
+}
+
+exports.items = [
+ {
+ name: "cmd",
+ get hidden() {
+ return !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ description: l10n.lookup("cmdDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cmd refresh",
+ description: l10n.lookup("cmdRefreshDesc"),
+ get hidden() {
+ return !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ exec: function(args, context) {
+ gcli.load();
+
+ let dirName = prefBranch.getComplexValue(PREF_DIR,
+ Ci.nsISupportsString).data.trim();
+ return l10n.lookupFormat("cmdStatus3", [ dirName ]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cmd setdir",
+ description: l10n.lookup("cmdSetdirDesc"),
+ manual: l10n.lookup("cmdSetdirManual3"),
+ params: [
+ {
+ name: "directory",
+ description: l10n.lookup("cmdSetdirDirectoryDesc"),
+ type: {
+ name: "file",
+ filetype: "directory",
+ existing: "yes"
+ },
+ defaultValue: null
+ }
+ ],
+ returnType: "string",
+ get hidden() {
+ return true; // !prefBranch.prefHasUserValue(PREF_DIR);
+ },
+ exec: function(args, context) {
+ supportsString.data = args.directory;
+ prefBranch.setComplexValue(PREF_DIR, Ci.nsISupportsString, supportsString);
+
+ gcli.load();
+
+ return l10n.lookupFormat("cmdStatus3", [ args.directory ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/cookie.js b/devtools/shared/gcli/commands/cookie.js
new file mode 100644
index 000000000..f1680042f
--- /dev/null
+++ b/devtools/shared/gcli/commands/cookie.js
@@ -0,0 +1,300 @@
+/* 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";
+
+/**
+ * XXX: bug 1221488 is required to make these commands run on the server.
+ * If we want these commands to run on remote devices/connections, they need to
+ * run on the server (runAt=server). Unfortunately, cookie commands not only
+ * need to run on the server, they also need to access to the parent process to
+ * retrieve and manipulate cookies via nsICookieManager2.
+ * However, server-running commands have no way of accessing the parent process
+ * for now.
+ *
+ * So, because these cookie commands, as of today, only run in the developer
+ * toolbar (the gcli command bar), and because this toolbar is only available on
+ * a local Firefox desktop tab (not in webide or the browser toolbox), we can
+ * make the commands run on the client.
+ * This way, they'll always run in the parent process.
+ */
+
+const { Ci, Cc } = require("chrome");
+const l10n = require("gcli/l10n");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "cookieMgr", function() {
+ return Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+});
+
+/**
+ * Check host value and remove port part as it is not used
+ * for storing cookies.
+ *
+ * Parameter will usually be `new URL(context.environment.target.url).host`
+ */
+function sanitizeHost(host) {
+ if (host == null || host == "") {
+ throw new Error(l10n.lookup("cookieListOutNonePage"));
+ }
+ return host.split(":")[0];
+}
+
+/**
+ * The cookie 'expires' value needs converting into something more readable.
+ *
+ * And the unit of expires is sec, the unit that in argument of Date() needs
+ * millisecond.
+ */
+function translateExpires(expires) {
+ if (expires == 0) {
+ return l10n.lookup("cookieListOutSession");
+ }
+
+ let expires_msec = expires * 1000;
+
+ return (new Date(expires_msec)).toLocaleString();
+}
+
+/**
+ * Check if a given cookie matches a given host
+ */
+function isCookieAtHost(cookie, host) {
+ if (cookie.host == null) {
+ return host == null;
+ }
+ if (cookie.host.startsWith(".")) {
+ return ("." + host).endsWith(cookie.host);
+ }
+ if (cookie.host === "") {
+ return host.startsWith("file://" + cookie.path);
+ }
+ return cookie.host == host;
+}
+
+exports.items = [
+ {
+ name: "cookie",
+ description: l10n.lookup("cookieDesc"),
+ manual: l10n.lookup("cookieManual")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie list",
+ description: l10n.lookup("cookieListDesc"),
+ manual: l10n.lookup("cookieListManual"),
+ returnType: "cookies",
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ let contentWindow = context.environment.window;
+ host = sanitizeHost(host);
+ let enm = cookieMgr.getCookiesFromHost(host, contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+
+ let cookies = [];
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ cookies.push({
+ host: cookie.host,
+ name: cookie.name,
+ value: cookie.value,
+ path: cookie.path,
+ expires: cookie.expires,
+ secure: cookie.secure,
+ httpOnly: cookie.httpOnly,
+ sameDomain: cookie.sameDomain
+ });
+ }
+ }
+
+ return cookies;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie remove",
+ description: l10n.lookup("cookieRemoveDesc"),
+ manual: l10n.lookup("cookieRemoveManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: l10n.lookup("cookieRemoveKeyDesc"),
+ }
+ ],
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ let contentWindow = context.environment.window;
+ host = sanitizeHost(host);
+ let enm = cookieMgr.getCookiesFromHost(host, contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+
+ while (enm.hasMoreElements()) {
+ let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
+ if (isCookieAtHost(cookie, host)) {
+ if (cookie.name == args.name) {
+ cookieMgr.remove(cookie.host, cookie.name, cookie.path,
+ false, cookie.originAttributes);
+ }
+ }
+ }
+ }
+ },
+ {
+ item: "converter",
+ from: "cookies",
+ to: "view",
+ exec: function(cookies, context) {
+ if (cookies.length == 0) {
+ let host = new URL(context.environment.target.url).host;
+ host = sanitizeHost(host);
+ let msg = l10n.lookupFormat("cookieListOutNoneHost", [ host ]);
+ return context.createView({ html: "<span>" + msg + "</span>" });
+ }
+
+ for (let cookie of cookies) {
+ cookie.expires = translateExpires(cookie.expires);
+
+ let noAttrs = !cookie.secure && !cookie.httpOnly && !cookie.sameDomain;
+ cookie.attrs = (cookie.secure ? "secure" : " ") +
+ (cookie.httpOnly ? "httpOnly" : " ") +
+ (cookie.sameDomain ? "sameDomain" : " ") +
+ (noAttrs ? l10n.lookup("cookieListOutNone") : " ");
+ }
+
+ return context.createView({
+ html:
+ "<ul class='gcli-cookielist-list'>" +
+ " <li foreach='cookie in ${cookies}'>" +
+ " <div>${cookie.name}=${cookie.value}</div>" +
+ " <table class='gcli-cookielist-detail'>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutHost") + "</td>" +
+ " <td>${cookie.host}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutPath") + "</td>" +
+ " <td>${cookie.path}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutExpires") + "</td>" +
+ " <td>${cookie.expires}</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td>" + l10n.lookup("cookieListOutAttributes") + "</td>" +
+ " <td>${cookie.attrs}</td>" +
+ " </tr>" +
+ " <tr><td colspan='2'>" +
+ " <span class='gcli-out-shortcut' onclick='${onclick}'" +
+ " data-command='cookie set ${cookie.name} '" +
+ " >" + l10n.lookup("cookieListOutEdit") + "</span>" +
+ " <span class='gcli-out-shortcut'" +
+ " onclick='${onclick}' ondblclick='${ondblclick}'" +
+ " data-command='cookie remove ${cookie.name}'" +
+ " >" + l10n.lookup("cookieListOutRemove") + "</span>" +
+ " </td></tr>" +
+ " </table>" +
+ " </li>" +
+ "</ul>",
+ data: {
+ options: { allowEval: true },
+ cookies: cookies,
+ onclick: context.update,
+ ondblclick: context.updateExec
+ }
+ });
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "cookie set",
+ description: l10n.lookup("cookieSetDesc"),
+ manual: l10n.lookup("cookieSetManual"),
+ params: [
+ {
+ name: "name",
+ type: "string",
+ description: l10n.lookup("cookieSetKeyDesc")
+ },
+ {
+ name: "value",
+ type: "string",
+ description: l10n.lookup("cookieSetValueDesc")
+ },
+ {
+ group: l10n.lookup("cookieSetOptionsDesc"),
+ params: [
+ {
+ name: "path",
+ type: { name: "string", allowBlank: true },
+ defaultValue: "/",
+ description: l10n.lookup("cookieSetPathDesc")
+ },
+ {
+ name: "domain",
+ type: "string",
+ defaultValue: null,
+ description: l10n.lookup("cookieSetDomainDesc")
+ },
+ {
+ name: "secure",
+ type: "boolean",
+ description: l10n.lookup("cookieSetSecureDesc")
+ },
+ {
+ name: "httpOnly",
+ type: "boolean",
+ description: l10n.lookup("cookieSetHttpOnlyDesc")
+ },
+ {
+ name: "session",
+ type: "boolean",
+ description: l10n.lookup("cookieSetSessionDesc")
+ },
+ {
+ name: "expires",
+ type: "string",
+ defaultValue: "Jan 17, 2038",
+ description: l10n.lookup("cookieSetExpiresDesc")
+ },
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ if (context.environment.target.isRemote) {
+ throw new Error("The cookie gcli commands only work in a local tab, " +
+ "see bug 1221488");
+ }
+ let host = new URL(context.environment.target.url).host;
+ host = sanitizeHost(host);
+ let time = Date.parse(args.expires) / 1000;
+ let contentWindow = context.environment.window;
+ cookieMgr.add(args.domain ? "." + args.domain : host,
+ args.path ? args.path : "/",
+ args.name,
+ args.value,
+ args.secure,
+ args.httpOnly,
+ args.session,
+ time,
+ contentWindow.document.
+ nodePrincipal.
+ originAttributes);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/csscoverage.js b/devtools/shared/gcli/commands/csscoverage.js
new file mode 100644
index 000000000..ebbf0baca
--- /dev/null
+++ b/devtools/shared/gcli/commands/csscoverage.js
@@ -0,0 +1,201 @@
+/* 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 { Cc, Ci } = require("chrome");
+
+const domtemplate = require("gcli/util/domtemplate");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
+const l10n = csscoverage.l10n;
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+loader.lazyImporter(this, "Chart", "resource://devtools/client/shared/widgets/Chart.jsm");
+
+/**
+ * The commands/converters for GCLI
+ */
+exports.items = [
+ {
+ name: "csscoverage",
+ hidden: true,
+ description: l10n.lookup("csscoverageDesc"),
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage start",
+ hidden: true,
+ description: l10n.lookup("csscoverageStartDesc2"),
+ params: [
+ {
+ name: "noreload",
+ type: "boolean",
+ description: l10n.lookup("csscoverageStartNoReloadDesc"),
+ manual: l10n.lookup("csscoverageStartNoReloadManual")
+ }
+ ],
+ exec: function*(args, context) {
+ let usage = yield csscoverage.getUsage(context.environment.target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.start(context.environment.chromeWindow,
+ context.environment.target, args.noreload);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage stop",
+ hidden: true,
+ description: l10n.lookup("csscoverageStopDesc2"),
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.stop();
+ yield gDevTools.showToolbox(target, "styleeditor");
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage oneshot",
+ hidden: true,
+ description: l10n.lookup("csscoverageOneShotDesc2"),
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+ yield usage.oneshot();
+ yield gDevTools.showToolbox(target, "styleeditor");
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage toggle",
+ hidden: true,
+ description: l10n.lookup("csscoverageToggleDesc2"),
+ state: {
+ isChecked: function(target) {
+ return csscoverage.getUsage(target).then(usage => {
+ return usage.isRunning();
+ });
+ },
+ onChange: function(target, handler) {
+ csscoverage.getUsage(target).then(usage => {
+ this.handler = ev => { handler("state-change", ev); };
+ usage.on("state-change", this.handler);
+ });
+ },
+ offChange: function(target, handler) {
+ csscoverage.getUsage(target).then(usage => {
+ usage.off("state-change", this.handler);
+ this.handler = undefined;
+ });
+ },
+ },
+ exec: function*(args, context) {
+ let target = context.environment.target;
+ let usage = yield csscoverage.getUsage(target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+
+ yield usage.toggle(context.environment.chromeWindow,
+ context.environment.target);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "csscoverage report",
+ hidden: true,
+ description: l10n.lookup("csscoverageReportDesc2"),
+ exec: function*(args, context) {
+ let usage = yield csscoverage.getUsage(context.environment.target);
+ if (usage == null) {
+ throw new Error(l10n.lookup("csscoverageNoRemoteError"));
+ }
+
+ return {
+ isTypedData: true,
+ type: "csscoveragePageReport",
+ data: yield usage.createPageReport()
+ };
+ }
+ },
+ {
+ item: "converter",
+ from: "csscoveragePageReport",
+ to: "dom",
+ exec: function*(csscoveragePageReport, context) {
+ let target = context.environment.target;
+
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getCurrentPanel();
+
+ let host = panel._panelDoc.querySelector(".csscoverage-report");
+ let templ = panel._panelDoc.querySelector(".csscoverage-template");
+
+ templ = templ.cloneNode(true);
+ templ.hidden = false;
+
+ let data = {
+ preload: csscoveragePageReport.preload,
+ unused: csscoveragePageReport.unused,
+ summary: csscoveragePageReport.summary,
+ onback: () => {
+ // The back button clears and hides .csscoverage-report
+ while (host.hasChildNodes()) {
+ host.removeChild(host.firstChild);
+ }
+ host.hidden = true;
+ }
+ };
+
+ let addOnClick = rule => {
+ rule.onclick = () => {
+ panel.selectStyleSheet(rule.url, rule.start.line);
+ };
+ };
+
+ data.preload.forEach(page => {
+ page.rules.forEach(addOnClick);
+ });
+ data.unused.forEach(page => {
+ page.rules.forEach(addOnClick);
+ });
+
+ let options = { allowEval: true, stack: "styleeditor.xul" };
+ domtemplate.template(templ, data, options);
+
+ while (templ.hasChildNodes()) {
+ host.appendChild(templ.firstChild);
+ }
+
+ // Create a new chart.
+ let container = host.querySelector(".csscoverage-report-chart");
+ let chart = Chart.PieTable(panel._panelDoc, {
+ diameter: 200, // px
+ title: "CSS Usage",
+ data: [
+ { size: data.summary.preload, label: "Used Preload" },
+ { size: data.summary.used, label: "Used" },
+ { size: data.summary.unused, label: "Unused" }
+ ]
+ });
+ container.appendChild(chart.node);
+
+ host.hidden = false;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/folder.js b/devtools/shared/gcli/commands/folder.js
new file mode 100644
index 000000000..22a51420d
--- /dev/null
+++ b/devtools/shared/gcli/commands/folder.js
@@ -0,0 +1,77 @@
+/* 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 { Cc, Ci, Cu, CC } = require("chrome");
+const Services = require("Services");
+const l10n = require("gcli/l10n");
+const dirService = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+
+function showFolder(aPath) {
+ let nsLocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
+ "initWithPath");
+
+ try {
+ let file = new nsLocalFile(aPath);
+
+ if (file.exists()) {
+ file.reveal();
+ return l10n.lookupFormat("folderOpenDirResult", [aPath]);
+ } else {
+ return l10n.lookup("folderInvalidPath");
+ }
+ } catch (e) {
+ return l10n.lookup("folderInvalidPath");
+ }
+}
+
+exports.items = [
+ {
+ name: "folder",
+ description: l10n.lookup("folderDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "folder open",
+ description: l10n.lookup("folderOpenDesc"),
+ params: [
+ {
+ name: "path",
+ type: { name: "string", allowBlank: true },
+ defaultValue: "~",
+ description: l10n.lookup("folderOpenDir")
+ }
+ ],
+ returnType: "string",
+ exec: function(args, context) {
+ let dirName = args.path;
+
+ // replaces ~ with the home directory path in unix and windows
+ if (dirName.indexOf("~") == 0) {
+ let homeDirFile = dirService.get("Home", Ci.nsIFile);
+ let homeDir = homeDirFile.path;
+ dirName = dirName.substr(1);
+ dirName = homeDir + dirName;
+ }
+
+ return showFolder(dirName);
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "folder openprofile",
+ description: l10n.lookup("folderOpenProfileDesc"),
+ returnType: "string",
+ exec: function(args, context) {
+ // Get the profile directory.
+ let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileDir = currProfD.path;
+ return showFolder(profileDir);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/highlight.js b/devtools/shared/gcli/commands/highlight.js
new file mode 100644
index 000000000..cc2353b3b
--- /dev/null
+++ b/devtools/shared/gcli/commands/highlight.js
@@ -0,0 +1,158 @@
+/* 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 l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const {
+ BoxModelHighlighter,
+ HighlighterEnvironment
+} = require("devtools/server/actors/highlighters");
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/shared/locales/gclicommands.properties");
+
+// How many maximum nodes can be highlighted in parallel
+const MAX_HIGHLIGHTED_ELEMENTS = 100;
+
+// Store the environment object used to create highlighters so it can be
+// destroyed later.
+var highlighterEnv;
+
+// Stores the highlighters instances so they can be destroyed later.
+// also export them so tests can access those and assert they got created
+// correctly.
+exports.highlighters = [];
+
+/**
+ * Destroy all existing highlighters
+ */
+function unhighlightAll() {
+ for (let highlighter of exports.highlighters) {
+ highlighter.destroy();
+ }
+ exports.highlighters.length = 0;
+
+ if (highlighterEnv) {
+ highlighterEnv.destroy();
+ highlighterEnv = null;
+ }
+}
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "highlight",
+ description: l10n.lookup("highlightDesc"),
+ manual: l10n.lookup("highlightManual"),
+ params: [
+ {
+ name: "selector",
+ type: "nodelist",
+ description: l10n.lookup("highlightSelectorDesc"),
+ manual: l10n.lookup("highlightSelectorManual")
+ },
+ {
+ group: l10n.lookup("highlightOptionsDesc"),
+ params: [
+ {
+ name: "hideguides",
+ type: "boolean",
+ description: l10n.lookup("highlightHideGuidesDesc"),
+ manual: l10n.lookup("highlightHideGuidesManual")
+ },
+ {
+ name: "showinfobar",
+ type: "boolean",
+ description: l10n.lookup("highlightShowInfoBarDesc"),
+ manual: l10n.lookup("highlightShowInfoBarManual")
+ },
+ {
+ name: "showall",
+ type: "boolean",
+ description: l10n.lookup("highlightShowAllDesc"),
+ manual: l10n.lookup("highlightShowAllManual")
+ },
+ {
+ name: "region",
+ type: {
+ name: "selection",
+ data: ["content", "padding", "border", "margin"]
+ },
+ description: l10n.lookup("highlightRegionDesc"),
+ manual: l10n.lookup("highlightRegionManual"),
+ defaultValue: "border"
+ },
+ {
+ name: "fill",
+ type: "string",
+ description: l10n.lookup("highlightFillDesc"),
+ manual: l10n.lookup("highlightFillManual"),
+ defaultValue: null
+ },
+ {
+ name: "keep",
+ type: "boolean",
+ description: l10n.lookup("highlightKeepDesc"),
+ manual: l10n.lookup("highlightKeepManual")
+ }
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ // Remove all existing highlighters unless told otherwise
+ if (!args.keep) {
+ unhighlightAll();
+ }
+
+ let env = context.environment;
+ highlighterEnv = new HighlighterEnvironment();
+ highlighterEnv.initFromWindow(env.window);
+
+ // Unhighlight on navigate
+ highlighterEnv.once("will-navigate", unhighlightAll);
+
+ let i = 0;
+ for (let node of args.selector) {
+ if (!args.showall && i >= MAX_HIGHLIGHTED_ELEMENTS) {
+ break;
+ }
+
+ let highlighter = new BoxModelHighlighter(highlighterEnv);
+ if (args.fill) {
+ highlighter.regionFill[args.region] = args.fill;
+ }
+ highlighter.show(node, {
+ region: args.region,
+ hideInfoBar: !args.showinfobar,
+ hideGuides: args.hideguides,
+ showOnly: args.region
+ });
+ exports.highlighters.push(highlighter);
+ i++;
+ }
+
+ let highlightText = L10N.getStr("highlightOutputConfirm2");
+ let output = PluralForm.get(args.selector.length, highlightText)
+ .replace("%1$S", args.selector.length);
+ if (args.selector.length > i) {
+ output = l10n.lookupFormat("highlightOutputMaxReached",
+ ["" + args.selector.length, "" + i]);
+ }
+
+ return output;
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "unhighlight",
+ description: l10n.lookup("unhighlightDesc"),
+ manual: l10n.lookup("unhighlightManual"),
+ exec: unhighlightAll
+ }
+];
diff --git a/devtools/shared/gcli/commands/index.js b/devtools/shared/gcli/commands/index.js
new file mode 100644
index 000000000..8fe77482e
--- /dev/null
+++ b/devtools/shared/gcli/commands/index.js
@@ -0,0 +1,179 @@
+/* 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 { createSystem, connectFront, disconnectFront } = require("gcli/system");
+const { GcliFront } = require("devtools/shared/fronts/gcli");
+
+/**
+ * This is the basic list of modules that should be loaded into each
+ * requisition instance whether server side or client side
+ */
+exports.baseModules = [
+ "gcli/types/delegate",
+ "gcli/types/selection",
+ "gcli/types/array",
+
+ "gcli/types/boolean",
+ "gcli/types/command",
+ "gcli/types/date",
+ "gcli/types/file",
+ "gcli/types/javascript",
+ "gcli/types/node",
+ "gcli/types/number",
+ "gcli/types/resource",
+ "gcli/types/setting",
+ "gcli/types/string",
+ "gcli/types/union",
+ "gcli/types/url",
+
+ "gcli/fields/fields",
+ "gcli/fields/delegate",
+ "gcli/fields/selection",
+
+ "gcli/ui/focus",
+ "gcli/ui/intro",
+
+ "gcli/converters/converters",
+ "gcli/converters/basic",
+ "gcli/converters/terminal",
+
+ "gcli/languages/command",
+ "gcli/languages/javascript",
+
+ "gcli/commands/clear",
+ "gcli/commands/context",
+ "gcli/commands/help",
+ "gcli/commands/pref",
+];
+
+/**
+ * Some commands belong to a tool (see getToolModules). This is a list of the
+ * modules that are *not* owned by a tool.
+ */
+exports.devtoolsModules = [
+ "devtools/shared/gcli/commands/addon",
+ "devtools/shared/gcli/commands/appcache",
+ "devtools/shared/gcli/commands/calllog",
+ "devtools/shared/gcli/commands/cmd",
+ "devtools/shared/gcli/commands/cookie",
+ "devtools/shared/gcli/commands/csscoverage",
+ "devtools/shared/gcli/commands/folder",
+ "devtools/shared/gcli/commands/highlight",
+ "devtools/shared/gcli/commands/inject",
+ "devtools/shared/gcli/commands/jsb",
+ "devtools/shared/gcli/commands/listen",
+ "devtools/shared/gcli/commands/mdn",
+ "devtools/shared/gcli/commands/measure",
+ "devtools/shared/gcli/commands/media",
+ "devtools/shared/gcli/commands/pagemod",
+ "devtools/shared/gcli/commands/paintflashing",
+ "devtools/shared/gcli/commands/qsa",
+ "devtools/shared/gcli/commands/restart",
+ "devtools/shared/gcli/commands/rulers",
+ "devtools/shared/gcli/commands/screenshot",
+ "devtools/shared/gcli/commands/security",
+];
+
+/**
+ * Register commands from tools with 'command: [ "some/module" ]' definitions.
+ * The map/reduce incantation squashes the array of arrays to a single array.
+ */
+try {
+ const { defaultTools } = require("devtools/client/definitions");
+ exports.devtoolsToolModules = defaultTools.map(def => def.commands || [])
+ .reduce((prev, curr) => prev.concat(curr), []);
+} catch (e) {
+ // "devtools/client/definitions" is only accessible from Firefox
+ exports.devtoolsToolModules = [];
+}
+
+/**
+ * Register commands from toolbox buttons with 'command: [ "some/module" ]'
+ * definitions. The map/reduce incantation squashes the array of arrays to a
+ * single array.
+ */
+try {
+ const { ToolboxButtons } = require("devtools/client/definitions");
+ exports.devtoolsButtonModules = ToolboxButtons.map(def => def.commands || [])
+ .reduce((prev, curr) => prev.concat(curr), []);
+} catch (e) {
+ // "devtools/client/definitions" is only accessible from Firefox
+ exports.devtoolsButtonModules = [];
+}
+
+/**
+ * Add modules to a system for use in a content process (but don't call load)
+ */
+exports.addAllItemsByModule = function(system) {
+ system.addItemsByModule(exports.baseModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsToolModules, { delayedLoad: true });
+ system.addItemsByModule(exports.devtoolsButtonModules, { delayedLoad: true });
+
+ const { mozDirLoader } = require("devtools/shared/gcli/commands/cmd");
+ system.addItemsByModule("mozcmd", { delayedLoad: true, loader: mozDirLoader });
+};
+
+/**
+ * This is WeakMap<Target, Links> where Links is an object that looks like
+ * { refs: number, promise: Promise<System>, front: GcliFront }
+ */
+var linksForTarget = new WeakMap();
+
+/**
+ * The toolbox uses the following properties on a command to allow it to be
+ * added to the toolbox toolbar
+ */
+var customProperties = [ "buttonId", "buttonClass", "tooltipText" ];
+
+/**
+ * Create a system which connects to a GCLI in a remote target
+ * @return Promise<System> for the given target
+ */
+exports.getSystem = function(target) {
+ const existingLinks = linksForTarget.get(target);
+ if (existingLinks != null) {
+ existingLinks.refs++;
+ return existingLinks.promise;
+ }
+
+ const system = createSystem({ location: "client" });
+
+ exports.addAllItemsByModule(system);
+
+ // Load the client system
+ const links = {
+ refs: 1,
+ system,
+ promise: system.load().then(() => {
+ return GcliFront.create(target).then(front => {
+ links.front = front;
+ return connectFront(system, front, customProperties).then(() => system);
+ });
+ })
+ };
+
+ linksForTarget.set(target, links);
+ return links.promise;
+};
+
+/**
+ * Someone that called getSystem doesn't need it any more, so decrement the
+ * count of users of the system for that target, and destroy if needed
+ */
+exports.releaseSystem = function(target) {
+ const links = linksForTarget.get(target);
+ if (links == null) {
+ throw new Error("releaseSystem called for unknown target");
+ }
+
+ links.refs--;
+ if (links.refs === 0) {
+ disconnectFront(links.system, links.front);
+ links.system.destroy();
+ linksForTarget.delete(target);
+ }
+};
diff --git a/devtools/shared/gcli/commands/inject.js b/devtools/shared/gcli/commands/inject.js
new file mode 100644
index 000000000..85e995eed
--- /dev/null
+++ b/devtools/shared/gcli/commands/inject.js
@@ -0,0 +1,86 @@
+/* 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 Services = require("Services");
+const { listenOnce } = require("devtools/shared/async-utils");
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "inject",
+ description: l10n.lookup("injectDesc"),
+ manual: l10n.lookup("injectManual2"),
+ params: [{
+ name: "library",
+ type: {
+ name: "union",
+ alternatives: [
+ {
+ name: "selection",
+ lookup: [
+ {
+ name: "jQuery",
+ value: {
+ name: "jQuery",
+ src: Services.prefs.getCharPref("devtools.gcli.jquerySrc")
+ }
+ },
+ {
+ name: "lodash",
+ value: {
+ name: "lodash",
+ src: Services.prefs.getCharPref("devtools.gcli.lodashSrc")
+ }
+ },
+ {
+ name: "underscore",
+ value: {
+ name: "underscore",
+ src: Services.prefs.getCharPref("devtools.gcli.underscoreSrc")
+ }
+ }
+ ]
+ },
+ {
+ name: "url"
+ }
+ ]
+ },
+ description: l10n.lookup("injectLibraryDesc")
+ }],
+ exec: function*(args, context) {
+ let document = context.environment.document;
+ let library = args.library;
+ let name = (library.type === "selection") ?
+ library.selection.name : library.url;
+ let src = (library.type === "selection") ?
+ library.selection.src : library.url;
+
+ if (context.environment.window.location.protocol == "https:") {
+ src = src.replace(/^http:/, "https:");
+ }
+
+ try {
+ // Check if URI is valid
+ Services.io.newURI(src, null, null);
+ } catch(e) {
+ return l10n.lookupFormat("injectFailed", [name]);
+ }
+
+ let newSource = document.createElement("script");
+ newSource.setAttribute("src", src);
+
+ let loadPromise = listenOnce(newSource, "load");
+ document.head.appendChild(newSource);
+
+ yield loadPromise;
+
+ return l10n.lookupFormat("injectLoaded", [name]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/jsb.js b/devtools/shared/gcli/commands/jsb.js
new file mode 100644
index 000000000..b56e079d2
--- /dev/null
+++ b/devtools/shared/gcli/commands/jsb.js
@@ -0,0 +1,134 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const XMLHttpRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
+
+loader.lazyImporter(this, "Preferences", "resource://gre/modules/Preferences.jsm");
+loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+
+loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "jsb",
+ description: l10n.lookup("jsbDesc"),
+ returnValue:"string",
+ params: [
+ {
+ name: "url",
+ type: "string",
+ description: l10n.lookup("jsbUrlDesc")
+ },
+ {
+ group: l10n.lookup("jsbOptionsDesc"),
+ params: [
+ {
+ name: "indentSize",
+ type: "number",
+ description: l10n.lookup("jsbIndentSizeDesc"),
+ manual: l10n.lookup("jsbIndentSizeManual"),
+ defaultValue: Preferences.get("devtools.editor.tabsize", 2),
+ },
+ {
+ name: "indentChar",
+ type: {
+ name: "selection",
+ lookup: [
+ { name: "space", value: " " },
+ { name: "tab", value: "\t" }
+ ]
+ },
+ description: l10n.lookup("jsbIndentCharDesc"),
+ manual: l10n.lookup("jsbIndentCharManual"),
+ defaultValue: " ",
+ },
+ {
+ name: "doNotPreserveNewlines",
+ type: "boolean",
+ description: l10n.lookup("jsbDoNotPreserveNewlinesDesc")
+ },
+ {
+ name: "preserveMaxNewlines",
+ type: "number",
+ description: l10n.lookup("jsbPreserveMaxNewlinesDesc"),
+ manual: l10n.lookup("jsbPreserveMaxNewlinesManual"),
+ defaultValue: -1
+ },
+ {
+ name: "jslintHappy",
+ type: "boolean",
+ description: l10n.lookup("jsbJslintHappyDesc"),
+ manual: l10n.lookup("jsbJslintHappyManual")
+ },
+ {
+ name: "braceStyle",
+ type: {
+ name: "selection",
+ data: ["collapse", "expand", "end-expand", "expand-strict"]
+ },
+ description: l10n.lookup("jsbBraceStyleDesc2"),
+ manual: l10n.lookup("jsbBraceStyleManual2"),
+ defaultValue: "collapse"
+ },
+ {
+ name: "noSpaceBeforeConditional",
+ type: "boolean",
+ description: l10n.lookup("jsbNoSpaceBeforeConditionalDesc")
+ },
+ {
+ name: "unescapeStrings",
+ type: "boolean",
+ description: l10n.lookup("jsbUnescapeStringsDesc"),
+ manual: l10n.lookup("jsbUnescapeStringsManual")
+ }
+ ]
+ }
+ ],
+ exec: function(args, context) {
+ let opts = {
+ indent_size: args.indentSize,
+ indent_char: args.indentChar,
+ preserve_newlines: !args.doNotPreserveNewlines,
+ max_preserve_newlines: args.preserveMaxNewlines == -1 ?
+ undefined : args.preserveMaxNewlines,
+ jslint_happy: args.jslintHappy,
+ brace_style: args.braceStyle,
+ space_before_conditional: !args.noSpaceBeforeConditional,
+ unescape_strings: args.unescapeStrings
+ };
+
+ let xhr = new XMLHttpRequest();
+
+ let deferred = context.defer();
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status == 0) {
+ let result = beautify.js(xhr.responseText, opts);
+
+ ScratchpadManager.openScratchpad({text: result});
+
+ deferred.resolve();
+ } else {
+ deferred.reject("Unable to load page to beautify: " + args.url + " " +
+ xhr.status + " " + xhr.statusText);
+ }
+ };
+ }
+ try {
+ xhr.open("GET", args.url, true);
+ xhr.send(null);
+ } catch(e) {
+ return l10n.lookup("jsbInvalidURL");
+ }
+ return deferred.promise;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/listen.js b/devtools/shared/gcli/commands/listen.js
new file mode 100644
index 000000000..7878577fb
--- /dev/null
+++ b/devtools/shared/gcli/commands/listen.js
@@ -0,0 +1,106 @@
+/* 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 { Cc, Ci } = require("chrome");
+const Services = require("Services");
+const l10n = require("gcli/l10n");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DevToolsLoader",
+ "resource://devtools/shared/Loader.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+XPCOMUtils.defineLazyGetter(this, "debuggerServer", () => {
+ // Create a separate loader instance, so that we can be sure to receive
+ // a separate instance of the DebuggingServer from the rest of the
+ // devtools. This allows us to safely use the tools against even the
+ // actors and DebuggingServer itself, especially since we can mark
+ // serverLoader as invisible to the debugger (unlike the usual loader
+ // settings).
+ let serverLoader = new DevToolsLoader();
+ serverLoader.invisibleToDebugger = true;
+ let { DebuggerServer: debuggerServer } = serverLoader.require("devtools/server/main");
+ debuggerServer.init();
+ debuggerServer.addBrowserActors();
+ debuggerServer.allowChromeProcess = !l10n.hiddenByChromePref();
+ return debuggerServer;
+});
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "listen",
+ description: l10n.lookup("listenDesc"),
+ manual: l10n.lookupFormat("listenManual2", [ BRAND_SHORT_NAME ]),
+ params: [
+ {
+ name: "port",
+ type: "number",
+ get defaultValue() {
+ return Services.prefs.getIntPref("devtools.debugger.remote-port");
+ },
+ description: l10n.lookup("listenPortDesc"),
+ },
+ {
+ name: "protocol",
+ get defaultValue() {
+ let webSocket = Services.prefs
+ .getBoolPref("devtools.debugger.remote-websocket");
+ let protocol;
+ if (webSocket === true) {
+ protocol = "websocket";
+ } else {
+ protocol = "mozilla-rdp";
+ }
+ return protocol;
+ },
+ type: {
+ name: "selection",
+ data: [ "mozilla-rdp", "websocket"],
+ },
+ description: l10n.lookup("listenProtocolDesc"),
+ },
+ ],
+ exec: function (args, context) {
+ var listener = debuggerServer.createListener();
+ if (!listener) {
+ throw new Error(l10n.lookup("listenDisabledOutput"));
+ }
+
+ let webSocket = false;
+ if (args.protocol === "websocket") {
+ webSocket = true;
+ } else if (args.protocol === "mozilla-rdp") {
+ webSocket = false;
+ }
+
+ listener.portOrPath = args.port;
+ listener.webSocket = webSocket;
+ listener.open();
+
+ if (debuggerServer.initialized) {
+ return l10n.lookupFormat("listenInitOutput", [ "" + args.port ]);
+ }
+
+ return l10n.lookup("listenNoInitOutput");
+ },
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "unlisten",
+ description: l10n.lookup("unlistenDesc"),
+ manual: l10n.lookup("unlistenManual"),
+ exec: function (args, context) {
+ debuggerServer.closeAllListeners();
+ return l10n.lookup("unlistenOutput");
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/mdn.js b/devtools/shared/gcli/commands/mdn.js
new file mode 100644
index 000000000..57e582e40
--- /dev/null
+++ b/devtools/shared/gcli/commands/mdn.js
@@ -0,0 +1,83 @@
+/* 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 l10n = require("gcli/l10n");
+
+var MdnDocsWidget;
+try {
+ MdnDocsWidget = require("devtools/client/shared/widgets/MdnDocsWidget");
+} catch (e) {
+ // DevTools MdnDocsWidget only available in Firefox Desktop
+}
+
+exports.items = [{
+ name: "mdn",
+ description: l10n.lookup("mdnDesc")
+}, {
+ item: "command",
+ runAt: "client",
+ name: "mdn css",
+ description: l10n.lookup("mdnCssDesc"),
+ returnType: "cssPropertyOutput",
+ params: [{
+ name: "property",
+ type: { name: "string" },
+ defaultValue: null,
+ description: l10n.lookup("mdnCssProp")
+ }],
+ exec: function(args) {
+ if (!MdnDocsWidget) {
+ return null;
+ }
+
+ return MdnDocsWidget.getCssDocs(args.property).then(result => {
+ return {
+ data: result,
+ url: MdnDocsWidget.PAGE_LINK_URL + args.property,
+ property: args.property
+ };
+ }, error => {
+ return { error, property: args.property };
+ });
+ }
+}, {
+ item: "converter",
+ from: "cssPropertyOutput",
+ to: "dom",
+ exec: function(result, context) {
+ let propertyName = result.property;
+
+ let document = context.document;
+ let root = document.createElement("div");
+
+ if (result.error) {
+ // The css property specified doesn't exist.
+ root.appendChild(document.createTextNode(
+ l10n.lookupFormat("mdnCssPropertyNotFound", [ propertyName ]) +
+ " (" + result.error + ")"));
+ } else {
+ let title = document.createElement("h2");
+ title.textContent = propertyName;
+ root.appendChild(title);
+
+ let link = document.createElement("p");
+ link.classList.add("gcli-mdn-url");
+ link.textContent = l10n.lookup("mdnCssVisitPage");
+ root.appendChild(link);
+
+ link.addEventListener("click", () => {
+ let mainWindow = context.environment.chromeWindow;
+ mainWindow.openUILinkIn(result.url, "tab");
+ });
+
+ let summary = document.createElement("p");
+ summary.textContent = result.data.summary;
+ root.appendChild(summary);
+ }
+
+ return root;
+ }
+}];
diff --git a/devtools/shared/gcli/commands/measure.js b/devtools/shared/gcli/commands/measure.js
new file mode 100644
index 000000000..7f6233a95
--- /dev/null
+++ b/devtools/shared/gcli/commands/measure.js
@@ -0,0 +1,112 @@
+/* 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/. */
+ /* globals getOuterId, getBrowserForTab */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+const events = require("sdk/event/core");
+
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const { MeasuringToolHighlighter, HighlighterEnvironment } =
+ require("devtools/server/actors/highlighters");
+
+const highlighters = new WeakMap();
+const visibleHighlighters = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
+
+exports.items = [
+ // The client measure command is used to maintain the toolbar button state
+ // only and redirects to the server command to actually toggle the measuring
+ // tool (see `measure_server` below).
+ {
+ name: "measure",
+ runAt: "client",
+ description: l10n.lookup("measureDesc"),
+ manual: l10n.lookup("measureManual"),
+ buttonId: "command-button-measure",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("measureTooltip"),
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (target, handler) => eventEmitter.on("changed", handler),
+ offChange: (target, handler) => eventEmitter.off("changed", handler)
+ },
+ exec: function*(args, context) {
+ let { target } = context.environment;
+
+ // Pipe the call to the server command.
+ let response = yield context.updateExec("measure_server");
+ let { visible, id } = response.data;
+
+ if (visible) {
+ visibleHighlighters.add(id);
+ } else {
+ visibleHighlighters.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target });
+
+ // Toggle off the button when the page navigates because the measuring
+ // tool is removed automatically by the MeasuringToolHighlighter on the
+ // server then.
+ let onNavigate = () => {
+ visibleHighlighters.delete(id);
+ eventEmitter.emit("changed", { target });
+ };
+ target.off("will-navigate", onNavigate);
+ target.once("will-navigate", onNavigate);
+ }
+ },
+ // The server measure command is hidden by default, it's just used by the
+ // client command.
+ {
+ name: "measure_server",
+ runAt: "server",
+ hidden: true,
+ returnType: "highlighterVisibility",
+ exec: function(args, context) {
+ let env = context.environment;
+ let { document } = env;
+ let id = getOuterId(env.window);
+
+ // Calling the command again after the measuring tool has been shown once,
+ // hides it.
+ if (highlighters.has(document)) {
+ let { highlighter } = highlighters.get(document);
+ highlighter.destroy();
+ return {visible: false, id};
+ }
+
+ // Otherwise, display the measuring tool.
+ let environment = new HighlighterEnvironment();
+ environment.initFromWindow(env.window);
+ let highlighter = new MeasuringToolHighlighter(environment);
+
+ // Store the instance of the measuring tool highlighter for this document
+ // so we can hide it later.
+ highlighters.set(document, { highlighter, environment });
+
+ // Listen to the highlighter's destroy event which may happen if the
+ // window is refreshed or closed with the measuring tool shown.
+ events.once(highlighter, "destroy", () => {
+ if (highlighters.has(document)) {
+ let { environment } = highlighters.get(document);
+ environment.destroy();
+ highlighters.delete(document);
+ }
+ });
+
+ highlighter.show();
+ return {visible: true, id};
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/media.js b/devtools/shared/gcli/commands/media.js
new file mode 100644
index 000000000..908e9eb5e
--- /dev/null
+++ b/devtools/shared/gcli/commands/media.js
@@ -0,0 +1,56 @@
+/* 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 {Ci} = require("chrome");
+const l10n = require("gcli/l10n");
+
+function getContentViewer(context) {
+ let {window} = context.environment;
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .contentViewer;
+}
+
+exports.items = [
+ {
+ name: "media",
+ description: l10n.lookup("mediaDesc")
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "media emulate",
+ description: l10n.lookup("mediaEmulateDesc"),
+ manual: l10n.lookup("mediaEmulateManual"),
+ params: [
+ {
+ name: "type",
+ description: l10n.lookup("mediaEmulateType"),
+ type: {
+ name: "selection",
+ data: [
+ "braille", "embossed", "handheld", "print", "projection",
+ "screen", "speech", "tty", "tv"
+ ]
+ }
+ }
+ ],
+ exec: function(args, context) {
+ let contentViewer = getContentViewer(context);
+ contentViewer.emulateMedium(args.type);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "media reset",
+ description: l10n.lookup("mediaResetDesc"),
+ exec: function(args, context) {
+ let contentViewer = getContentViewer(context);
+ contentViewer.stopEmulatingMedium();
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/moz.build b/devtools/shared/gcli/commands/moz.build
new file mode 100644
index 000000000..e5f3fe91c
--- /dev/null
+++ b/devtools/shared/gcli/commands/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'addon.js',
+ 'appcache.js',
+ 'calllog.js',
+ 'cmd.js',
+ 'cookie.js',
+ 'csscoverage.js',
+ 'folder.js',
+ 'highlight.js',
+ 'index.js',
+ 'inject.js',
+ 'jsb.js',
+ 'listen.js',
+ 'mdn.js',
+ 'measure.js',
+ 'media.js',
+ 'pagemod.js',
+ 'paintflashing.js',
+ 'qsa.js',
+ 'restart.js',
+ 'rulers.js',
+ 'screenshot.js',
+ 'security.js',
+)
diff --git a/devtools/shared/gcli/commands/pagemod.js b/devtools/shared/gcli/commands/pagemod.js
new file mode 100644
index 000000000..184ab1ea3
--- /dev/null
+++ b/devtools/shared/gcli/commands/pagemod.js
@@ -0,0 +1,276 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ name: "pagemod",
+ description: l10n.lookup("pagemodDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod replace",
+ description: l10n.lookup("pagemodReplaceDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceSearchDesc"),
+ },
+ {
+ name: "replace",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceReplaceDesc"),
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceIgnoreCaseDesc"),
+ },
+ {
+ name: "selector",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceSelectorDesc"),
+ defaultValue: "*:not(script):not(style):not(embed):not(object):not(frame):not(iframe):not(frameset)",
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodReplaceRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "attrOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceAttrOnlyDesc"),
+ },
+ {
+ name: "contentOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodReplaceContentOnlyDesc"),
+ },
+ {
+ name: "attributes",
+ type: "string",
+ description: l10n.lookup("pagemodReplaceAttributesDesc"),
+ defaultValue: null,
+ },
+ ],
+ // Make a given string safe to use in a regular expression.
+ escapeRegex: function(aString) {
+ return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ },
+ exec: function(args, context) {
+ let searchTextNodes = !args.attrOnly;
+ let searchAttributes = !args.contentOnly;
+ let regexOptions = args.ignoreCase ? "ig" : "g";
+ let search = new RegExp(this.escapeRegex(args.search), regexOptions);
+ let attributeRegex = null;
+ if (args.attributes) {
+ attributeRegex = new RegExp(args.attributes, regexOptions);
+ }
+
+ let root = args.root || context.environment.document;
+ let elements = root.querySelectorAll(args.selector);
+ elements = Array.prototype.slice.call(elements);
+
+ let replacedTextNodes = 0;
+ let replacedAttributes = 0;
+
+ function replaceAttribute() {
+ replacedAttributes++;
+ return args.replace;
+ }
+ function replaceTextNode() {
+ replacedTextNodes++;
+ return args.replace;
+ }
+
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (searchTextNodes) {
+ for (let y = 0; y < element.childNodes.length; y++) {
+ let node = element.childNodes[y];
+ if (node.nodeType == node.TEXT_NODE) {
+ node.textContent = node.textContent.replace(search, replaceTextNode);
+ }
+ }
+ }
+
+ if (searchAttributes) {
+ if (!element.attributes) {
+ continue;
+ }
+ for (let y = 0; y < element.attributes.length; y++) {
+ let attr = element.attributes[y];
+ if (!attributeRegex || attributeRegex.test(attr.name)) {
+ attr.value = attr.value.replace(search, replaceAttribute);
+ }
+ }
+ }
+ }
+
+ return l10n.lookupFormat("pagemodReplaceResult",
+ [elements.length, replacedTextNodes,
+ replacedAttributes]);
+ }
+ },
+ {
+ name: "pagemod remove",
+ description: l10n.lookup("pagemodRemoveDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod remove element",
+ description: l10n.lookup("pagemodRemoveElementDesc"),
+ params: [
+ {
+ name: "search",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveElementSearchDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodRemoveElementRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "stripOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveElementStripOnlyDesc"),
+ },
+ {
+ name: "ifEmptyOnly",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ let parentNode = element.parentNode;
+ if (!parentNode || !element.removeChild) {
+ continue;
+ }
+ if (args.stripOnly) {
+ while (element.hasChildNodes()) {
+ parentNode.insertBefore(element.childNodes[0], element);
+ }
+ }
+ if (!args.ifEmptyOnly || !element.hasChildNodes()) {
+ element.parentNode.removeChild(element);
+ removed++;
+ }
+ }
+
+ return l10n.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
+ [elements.length, removed]);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "pagemod remove attribute",
+ description: l10n.lookup("pagemodRemoveAttributeDesc"),
+ params: [
+ {
+ name: "searchAttributes",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
+ },
+ {
+ name: "searchElements",
+ type: "string",
+ description: l10n.lookup("pagemodRemoveAttributeSearchElementsDesc"),
+ },
+ {
+ name: "root",
+ type: "node",
+ description: l10n.lookup("pagemodRemoveAttributeRootDesc"),
+ defaultValue: null,
+ },
+ {
+ name: "ignoreCase",
+ type: "boolean",
+ description: l10n.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
+ },
+ ],
+ exec: function(args, context) {
+ let root = args.root || context.environment.document;
+ let regexOptions = args.ignoreCase ? "ig" : "g";
+ let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
+ let elements = root.querySelectorAll(args.searchElements);
+ elements = Array.prototype.slice.call(elements);
+
+ let removed = 0;
+ for (let i = 0; i < elements.length; i++) {
+ let element = elements[i];
+ if (!element.attributes) {
+ continue;
+ }
+
+ var attrs = Array.prototype.slice.call(element.attributes);
+ for (let y = 0; y < attrs.length; y++) {
+ let attr = attrs[y];
+ if (attributeRegex.test(attr.name)) {
+ element.removeAttribute(attr.name);
+ removed++;
+ }
+ }
+ }
+
+ return l10n.lookupFormat("pagemodRemoveAttributeResult",
+ [elements.length, removed]);
+ }
+ },
+ // This command allows the user to export the page to HTML after DOM changes
+ {
+ name: "export",
+ description: l10n.lookup("exportDesc"),
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "export html",
+ description: l10n.lookup("exportHtmlDesc"),
+ params: [
+ {
+ name: "destination",
+ type: {
+ name: "selection",
+ data: [ "window", "stdout", "clipboard" ]
+ },
+ defaultValue: "window"
+ }
+ ],
+ exec: function(args, context) {
+ let html = context.environment.document.documentElement.outerHTML;
+ if (args.destination === "stdout") {
+ return html;
+ }
+
+ if (args.desination === "clipboard") {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(url);
+ return '';
+ }
+
+ let url = "data:text/plain;charset=utf8," + encodeURIComponent(html);
+ context.environment.window.open(url);
+ return '';
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/paintflashing.js b/devtools/shared/gcli/commands/paintflashing.js
new file mode 100644
index 000000000..7e21911a7
--- /dev/null
+++ b/devtools/shared/gcli/commands/paintflashing.js
@@ -0,0 +1,201 @@
+/* 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 { Ci } = require("chrome");
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+var telemetry;
+try {
+ const Telemetry = require("devtools/client/shared/telemetry");
+ telemetry = new Telemetry();
+} catch(e) {
+ // DevTools Telemetry module only available in Firefox
+}
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+
+const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
+
+const enabledPaintFlashing = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? enabledPaintFlashing.has(getBrowserForTab(tab).outerWindowID) : false;
+
+/**
+ * Fire events and telemetry when paintFlashing happens
+ */
+function onPaintFlashingChanged(target, state) {
+ const { flashing, id } = state;
+
+ if (flashing) {
+ enabledPaintFlashing.add(id);
+ } else {
+ enabledPaintFlashing.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target: target });
+ function fireChange() {
+ eventEmitter.emit("changed", { target: target });
+ }
+
+ target.off("navigate", fireChange);
+ target.once("navigate", fireChange);
+
+ if (!telemetry) {
+ return;
+ }
+ if (flashing) {
+ telemetry.toolOpened("paintflashing");
+ } else {
+ telemetry.toolClosed("paintflashing");
+ }
+}
+
+/**
+ * Alter the paintFlashing state of a window and report on the new value.
+ * This works with chrome or content windows.
+ *
+ * This is a bizarre method that you could argue should be broken up into
+ * separate getter and setter functions, however keeping it as one helps
+ * to simplify the commands below.
+ *
+ * @param state {string} One of:
+ * - "on" which does window.paintFlashing = true
+ * - "off" which does window.paintFlashing = false
+ * - "toggle" which does window.paintFlashing = !window.paintFlashing
+ * - "query" which does nothing
+ * @return The new value of the window.paintFlashing flag
+ */
+function setPaintFlashing(window, state) {
+ const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (!["on", "off", "toggle", "query"].includes(state)) {
+ throw new Error(`Unsupported state: ${state}`);
+ }
+
+ if (state === "on") {
+ winUtils.paintFlashing = true;
+ } else if (state === "off") {
+ winUtils.paintFlashing = false;
+ } else if (state === "toggle") {
+ winUtils.paintFlashing = !winUtils.paintFlashing;
+ }
+
+ return winUtils.paintFlashing;
+}
+
+exports.items = [
+ {
+ name: "paintflashing",
+ description: l10n.lookup("paintflashingDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing on",
+ description: l10n.lookup("paintflashingOnDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ description: l10n.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function*(args, context) {
+ if (!args.chrome) {
+ const output = yield context.updateExec("paintflashing_server --state on");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ } else {
+ setPaintFlashing(context.environment.chromeWindow, "on");
+ }
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing off",
+ description: l10n.lookup("paintflashingOffDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ params: [{
+ group: "options",
+ params: [
+ {
+ type: "boolean",
+ name: "chrome",
+ get hidden() {
+ return gcli.hiddenByChromePref();
+ },
+ description: l10n.lookup("paintflashingChromeDesc"),
+ }
+ ]
+ }],
+ exec: function*(args, context) {
+ if (!args.chrome) {
+ const output = yield context.updateExec("paintflashing_server --state off");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ } else {
+ setPaintFlashing(context.environment.chromeWindow, "off");
+ }
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "paintflashing toggle",
+ hidden: true,
+ buttonId: "command-button-paintflashing",
+ buttonClass: "command-button command-button-invertable",
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (_, handler) => eventEmitter.on("changed", handler),
+ offChange: (_, handler) => eventEmitter.off("changed", handler),
+ },
+ tooltipText: l10n.lookup("paintflashingTooltip"),
+ description: l10n.lookup("paintflashingToggleDesc"),
+ manual: l10n.lookup("paintflashingManual"),
+ exec: function*(args, context) {
+ const output = yield context.updateExec("paintflashing_server --state toggle");
+
+ onPaintFlashingChanged(context.environment.target, output.data);
+ }
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "paintflashing_server",
+ hidden: true,
+ params: [
+ {
+ name: "state",
+ type: {
+ name: "selection",
+ data: [ "on", "off", "toggle", "query" ]
+ }
+ },
+ ],
+ returnType: "paintFlashingState",
+ exec: function(args, context) {
+ let { window } = context.environment;
+ let id = getOuterId(window);
+ let flashing = setPaintFlashing(window, args.state);
+
+ return { flashing, id };
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/qsa.js b/devtools/shared/gcli/commands/qsa.js
new file mode 100644
index 000000000..939991f18
--- /dev/null
+++ b/devtools/shared/gcli/commands/qsa.js
@@ -0,0 +1,24 @@
+/* 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 l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "server",
+ name: "qsa",
+ description: l10n.lookup("qsaDesc"),
+ params: [{
+ name: "query",
+ type: "nodelist",
+ description: l10n.lookup("qsaQueryDesc")
+ }],
+ exec: function(args, context) {
+ return args.query.length;
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/restart.js b/devtools/shared/gcli/commands/restart.js
new file mode 100644
index 000000000..cf0e688d3
--- /dev/null
+++ b/devtools/shared/gcli/commands/restart.js
@@ -0,0 +1,77 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const Services = require("Services");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+/**
+ * Restart command
+ *
+ * @param boolean nocache
+ * Disables loading content from cache upon restart.
+ *
+ * Examples :
+ * >> restart
+ * - restarts browser immediately
+ * >> restart --nocache
+ * - restarts immediately and starts Firefox without using cache
+ */
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "restart",
+ description: l10n.lookupFormat("restartBrowserDesc", [ BRAND_SHORT_NAME ]),
+ params: [{
+ group: l10n.lookup("restartBrowserGroupOptions"),
+ params: [
+ {
+ name: "nocache",
+ type: "boolean",
+ description: l10n.lookup("restartBrowserNocacheDesc")
+ },
+ {
+ name: "safemode",
+ type: "boolean",
+ description: l10n.lookup("restartBrowserSafemodeDesc")
+ }
+ ]
+ }],
+ returnType: "string",
+ exec: function Restart(args, context) {
+ let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
+ if (canceled.data) {
+ return l10n.lookup("restartBrowserRequestCancelled");
+ }
+
+ // disable loading content from cache.
+ if (args.nocache) {
+ Services.appinfo.invalidateCachesOnRestart();
+ }
+
+ const appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
+ .getService(Ci.nsIAppStartup);
+
+ if (args.safemode) {
+ // restart in safemode
+ appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
+ } else {
+ // restart normally
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+ }
+
+ return l10n.lookupFormat("restartBrowserRestarting", [ BRAND_SHORT_NAME ]);
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/rulers.js b/devtools/shared/gcli/commands/rulers.js
new file mode 100644
index 000000000..121e975bc
--- /dev/null
+++ b/devtools/shared/gcli/commands/rulers.js
@@ -0,0 +1,110 @@
+/* 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/. */
+/* globals getBrowserForTab */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const eventEmitter = new EventEmitter();
+const events = require("sdk/event/core");
+loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const l10n = require("gcli/l10n");
+require("devtools/server/actors/inspector");
+const { RulersHighlighter, HighlighterEnvironment } =
+ require("devtools/server/actors/highlighters");
+
+const highlighters = new WeakMap();
+const visibleHighlighters = new Set();
+
+const isCheckedFor = (tab) =>
+ tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
+
+exports.items = [
+ // The client rulers command is used to maintain the toolbar button state only
+ // and redirects to the server command to actually toggle the rulers (see
+ // rulers_server below).
+ {
+ name: "rulers",
+ runAt: "client",
+ description: l10n.lookup("rulersDesc"),
+ manual: l10n.lookup("rulersManual"),
+ buttonId: "command-button-rulers",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("rulersTooltip"),
+ state: {
+ isChecked: ({_tab}) => isCheckedFor(_tab),
+ onChange: (target, handler) => eventEmitter.on("changed", handler),
+ offChange: (target, handler) => eventEmitter.off("changed", handler)
+ },
+ exec: function*(args, context) {
+ let { target } = context.environment;
+
+ // Pipe the call to the server command.
+ let response = yield context.updateExec("rulers_server");
+ let { visible, id } = response.data;
+
+ if (visible) {
+ visibleHighlighters.add(id);
+ } else {
+ visibleHighlighters.delete(id);
+ }
+
+ eventEmitter.emit("changed", { target });
+
+ // Toggle off the button when the page navigates because the rulers are
+ // removed automatically by the RulersHighlighter on the server then.
+ let onNavigate = () => {
+ visibleHighlighters.delete(id);
+ eventEmitter.emit("changed", { target });
+ };
+ target.off("will-navigate", onNavigate);
+ target.once("will-navigate", onNavigate);
+ }
+ },
+ // The server rulers command is hidden by default, it's just used by the
+ // client command.
+ {
+ name: "rulers_server",
+ runAt: "server",
+ hidden: true,
+ returnType: "highlighterVisibility",
+ exec: function(args, context) {
+ let env = context.environment;
+ let { document } = env;
+ let id = getOuterId(env.window);
+
+ // Calling the command again after the rulers have been shown once hides
+ // them.
+ if (highlighters.has(document)) {
+ let { highlighter } = highlighters.get(document);
+ highlighter.destroy();
+ return {visible: false, id};
+ }
+
+ // Otherwise, display the rulers.
+ let environment = new HighlighterEnvironment();
+ environment.initFromWindow(env.window);
+ let highlighter = new RulersHighlighter(environment);
+
+ // Store the instance of the rulers highlighter for this document so we
+ // can hide it later.
+ highlighters.set(document, { highlighter, environment });
+
+ // Listen to the highlighter's destroy event which may happen if the
+ // window is refreshed or closed with the rulers shown.
+ events.once(highlighter, "destroy", () => {
+ if (highlighters.has(document)) {
+ let { environment } = highlighters.get(document);
+ environment.destroy();
+ highlighters.delete(document);
+ }
+ });
+
+ highlighter.show();
+ return {visible: true, id};
+ }
+ }
+];
diff --git a/devtools/shared/gcli/commands/screenshot.js b/devtools/shared/gcli/commands/screenshot.js
new file mode 100644
index 000000000..e2f38b6d9
--- /dev/null
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -0,0 +1,579 @@
+/* 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 { Cc, Ci, Cr, Cu } = require("chrome");
+const l10n = require("gcli/l10n");
+const Services = require("Services");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const { getRect } = require("devtools/shared/layout/utils");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+// String used as an indication to generate default file name in the following
+// format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
+const FILENAME_DEFAULT_VALUE = " ";
+
+/*
+ * There are 2 commands and 1 converter here. The 2 commands are nearly
+ * identical except that one runs on the client and one in the server.
+ *
+ * The server command is hidden, and is designed to be called from the client
+ * command.
+ */
+
+/**
+ * Both commands have the same initial filename parameter
+ */
+const filenameParam = {
+ name: "filename",
+ type: {
+ name: "file",
+ filetype: "file",
+ existing: "maybe",
+ },
+ defaultValue: FILENAME_DEFAULT_VALUE,
+ description: l10n.lookup("screenshotFilenameDesc"),
+ manual: l10n.lookup("screenshotFilenameManual")
+};
+
+/**
+ * Both commands have the same set of standard optional parameters
+ */
+const standardParams = {
+ group: l10n.lookup("screenshotGroupOptions"),
+ params: [
+ {
+ name: "clipboard",
+ type: "boolean",
+ description: l10n.lookup("screenshotClipboardDesc"),
+ manual: l10n.lookup("screenshotClipboardManual")
+ },
+ {
+ name: "imgur",
+ type: "boolean",
+ description: l10n.lookup("screenshotImgurDesc"),
+ manual: l10n.lookup("screenshotImgurManual")
+ },
+ {
+ name: "delay",
+ type: { name: "number", min: 0 },
+ defaultValue: 0,
+ description: l10n.lookup("screenshotDelayDesc"),
+ manual: l10n.lookup("screenshotDelayManual")
+ },
+ {
+ name: "dpr",
+ type: { name: "number", min: 0, allowFloat: true },
+ defaultValue: 0,
+ description: l10n.lookup("screenshotDPRDesc"),
+ manual: l10n.lookup("screenshotDPRManual")
+ },
+ {
+ name: "fullpage",
+ type: "boolean",
+ description: l10n.lookup("screenshotFullPageDesc"),
+ manual: l10n.lookup("screenshotFullPageManual")
+ },
+ {
+ name: "selector",
+ type: "node",
+ defaultValue: null,
+ description: l10n.lookup("inspectNodeDesc"),
+ manual: l10n.lookup("inspectNodeManual")
+ }
+ ]
+};
+
+exports.items = [
+ {
+ /**
+ * Format an 'imageSummary' (as output by the screenshot command).
+ * An 'imageSummary' is a simple JSON object that looks like this:
+ *
+ * {
+ * destinations: [ "..." ], // Required array of descriptions of the
+ * // locations of the result image (the command
+ * // can have multiple outputs)
+ * data: "...", // Optional Base64 encoded image data
+ * width:1024, height:768, // Dimensions of the image data, required
+ * // if data != null
+ * filename: "...", // If set, clicking the image will open the
+ * // folder containing the given file
+ * href: "...", // If set, clicking the image will open the
+ * // link in a new tab
+ * }
+ */
+ item: "converter",
+ from: "imageSummary",
+ to: "dom",
+ exec: function(imageSummary, context) {
+ const document = context.document;
+ const root = document.createElement("div");
+
+ // Add a line to the result for each destination
+ imageSummary.destinations.forEach(destination => {
+ const title = document.createElement("div");
+ title.textContent = destination;
+ root.appendChild(title);
+ });
+
+ // Add the thumbnail image
+ if (imageSummary.data != null) {
+ const image = context.document.createElement("div");
+ const previewHeight = parseInt(256 * imageSummary.height / imageSummary.width);
+ const style = "" +
+ "width: 256px;" +
+ "height: " + previewHeight + "px;" +
+ "max-height: 256px;" +
+ "background-image: url('" + imageSummary.data + "');" +
+ "background-size: 256px " + previewHeight + "px;" +
+ "margin: 4px;" +
+ "display: block;";
+ image.setAttribute("style", style);
+ root.appendChild(image);
+ }
+
+ // Click handler
+ if (imageSummary.href || imageSummary.filename) {
+ root.style.cursor = "pointer";
+ root.addEventListener("click", () => {
+ if (imageSummary.href) {
+ let mainWindow = context.environment.chromeWindow;
+ mainWindow.openUILinkIn(imageSummary.href, "tab");
+ } else if (imageSummary.filename) {
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(imageSummary.filename);
+ file.reveal();
+ }
+ });
+ }
+
+ return root;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "screenshot",
+ description: l10n.lookup("screenshotDesc"),
+ manual: l10n.lookup("screenshotManual"),
+ returnType: "imageSummary",
+ buttonId: "command-button-screenshot",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("screenshotTooltipPage"),
+ params: [
+ filenameParam,
+ standardParams,
+ ],
+ exec: function (args, context) {
+ // Re-execute the command on the server
+ const command = context.typed.replace(/^screenshot/, "screenshot_server");
+ let capture = context.updateExec(command).then(output => {
+ return output.error ? Promise.reject(output.data) : output.data;
+ });
+
+ simulateCameraEffect(context.environment.chromeDocument, "shutter");
+ return capture.then(saveScreenshot.bind(null, args, context));
+ },
+ },
+ {
+ item: "command",
+ runAt: "server",
+ name: "screenshot_server",
+ hidden: true,
+ returnType: "imageSummary",
+ params: [ filenameParam, standardParams ],
+ exec: function (args, context) {
+ return captureScreenshot(args, context.environment.document);
+ },
+ }
+];
+
+/**
+ * This function is called to simulate camera effects
+ */
+function simulateCameraEffect(document, effect) {
+ let window = document.defaultView;
+ if (effect === "shutter") {
+ const audioCamera = new window.Audio("resource://devtools/client/themes/audio/shutter.wav");
+ audioCamera.play();
+ }
+ if (effect == "flash") {
+ const frames = Cu.cloneInto({ opacity: [ 0, 1 ] }, window);
+ document.documentElement.animate(frames, 500);
+ }
+}
+
+/**
+ * This function simply handles the --delay argument before calling
+ * createScreenshotData
+ */
+function captureScreenshot(args, document) {
+ if (args.delay > 0) {
+ return new Promise((resolve, reject) => {
+ document.defaultView.setTimeout(() => {
+ createScreenshotData(document, args).then(resolve, reject);
+ }, args.delay * 1000);
+ });
+ }
+ else {
+ return createScreenshotData(document, args);
+ }
+}
+
+/**
+ * There are several possible destinations for the screenshot, SKIP is used
+ * in saveScreenshot() whenever one of them is not used
+ */
+const SKIP = Promise.resolve();
+
+/**
+ * Save the captured screenshot to one of several destinations.
+ */
+function saveScreenshot(args, context, reply) {
+ const fileNeeded = args.filename != FILENAME_DEFAULT_VALUE ||
+ (!args.imgur && !args.clipboard);
+
+ return Promise.all([
+ args.clipboard ? saveToClipboard(context, reply) : SKIP,
+ args.imgur ? uploadToImgur(reply) : SKIP,
+ fileNeeded ? saveToFile(context, reply) : SKIP,
+ ]).then(() => reply);
+}
+
+/**
+ * This does the dirty work of creating a base64 string out of an
+ * area of the browser window
+ */
+function createScreenshotData(document, args) {
+ const window = document.defaultView;
+ let left = 0;
+ let top = 0;
+ let width;
+ let height;
+ const currentX = window.scrollX;
+ const currentY = window.scrollY;
+
+ let filename = getFilename(args.filename);
+
+ if (args.fullpage) {
+ // Bug 961832: GCLI screenshot shows fixed position element in wrong
+ // position if we don't scroll to top
+ window.scrollTo(0,0);
+ width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
+ height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
+ filename = filename.replace(".png", "-fullpage.png");
+ }
+ else if (args.selector) {
+ ({ top, left, width, height } = getRect(window, args.selector, window));
+ }
+ else {
+ left = window.scrollX;
+ top = window.scrollY;
+ width = window.innerWidth;
+ height = window.innerHeight;
+ }
+
+ // Only adjust for scrollbars when considering the full window
+ if (!args.selector) {
+ const winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ winUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
+ width -= scrollbarWidth.value;
+ height -= scrollbarHeight.value;
+ }
+
+ const canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ const ctx = canvas.getContext("2d");
+ const ratio = args.dpr ? args.dpr : window.devicePixelRatio;
+ canvas.width = width * ratio;
+ canvas.height = height * ratio;
+ ctx.scale(ratio, ratio);
+ ctx.drawWindow(window, left, top, width, height, "#fff");
+ const data = canvas.toDataURL("image/png", "");
+
+ // See comment above on bug 961832
+ if (args.fullpage) {
+ window.scrollTo(currentX, currentY);
+ }
+
+ simulateCameraEffect(document, "flash");
+
+ return Promise.resolve({
+ destinations: [],
+ data: data,
+ height: height,
+ width: width,
+ filename: filename,
+ });
+}
+
+/**
+ * We may have a filename specified in args, or we might have to generate
+ * one.
+ */
+function getFilename(defaultName) {
+ // Create a name for the file if not present
+ if (defaultName != FILENAME_DEFAULT_VALUE) {
+ return defaultName;
+ }
+
+ const date = new Date();
+ let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) +
+ "-" + date.getDate();
+ dateString = dateString.split("-").map(function(part) {
+ if (part.length == 1) {
+ part = "0" + part;
+ }
+ return part;
+ }).join("-");
+
+ const timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ return l10n.lookupFormat("screenshotGeneratedFilename",
+ [ dateString, timeString ]) + ".png";
+}
+
+/**
+ * Save the image data to the clipboard. This returns a promise, so it can
+ * be treated exactly like imgur / file processing, but it's really sync
+ * for now.
+ */
+function saveToClipboard(context, reply) {
+ try {
+ const channel = NetUtil.newChannel({
+ uri: reply.data,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ });
+ const input = channel.open2();
+
+ const loadContext = context.environment.chromeWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+
+ const imgTools = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools);
+
+ const container = {};
+ imgTools.decodeImageData(input, channel.contentType, container);
+
+ const wrapped = Cc["@mozilla.org/supports-interface-pointer;1"]
+ .createInstance(Ci.nsISupportsInterfacePointer);
+ wrapped.data = container.value;
+
+ const trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(loadContext);
+ trans.addDataFlavor(channel.contentType);
+ trans.setTransferData(channel.contentType, wrapped, -1);
+
+ const clip = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(Ci.nsIClipboard);
+ clip.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+
+ reply.destinations.push(l10n.lookup("screenshotCopied"));
+ }
+ catch (ex) {
+ console.error(ex);
+ reply.destinations.push(l10n.lookup("screenshotErrorCopying"));
+ }
+
+ return Promise.resolve();
+}
+
+/**
+ * Upload screenshot data to Imgur, returning a promise of a URL (as a string)
+ */
+function uploadToImgur(reply) {
+ return new Promise((resolve, reject) => {
+ const xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ const fd = Cc["@mozilla.org/files/formdata;1"]
+ .createInstance(Ci.nsIDOMFormData);
+ fd.append("image", reply.data.split(",")[1]);
+ fd.append("type", "base64");
+ fd.append("title", reply.filename);
+
+ const postURL = Services.prefs.getCharPref("devtools.gcli.imgurUploadURL");
+ const clientID = "Client-ID " + Services.prefs.getCharPref("devtools.gcli.imgurClientID");
+
+ xhr.open("POST", postURL);
+ xhr.setRequestHeader("Authorization", clientID);
+ xhr.send(fd);
+ xhr.responseType = "json";
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ reply.href = xhr.response.data.link;
+ reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
+ [ reply.href ]));
+ } else {
+ reply.destinations.push(l10n.lookup("screenshotImgurError"));
+ }
+
+ resolve();
+ }
+ };
+ });
+}
+
+/**
+ * Progress listener that forwards calls to a transfer object.
+ *
+ * This is used below in saveToFile to forward progress updates from the
+ * nsIWebBrowserPersist object that does the actual saving to the nsITransfer
+ * which just represents the operation for the Download Manager. This keeps the
+ * Download Manager updated on saving progress and completion, so that it gives
+ * visual feedback from the downloads toolbar button when the save is done.
+ *
+ * It also allows the browser window to show auth prompts if needed (should not
+ * be needed for saving screenshots).
+ *
+ * This code is borrowed directly from contentAreaUtils.js.
+ */
+function DownloadListener(win, transfer) {
+ this.window = win;
+ this.transfer = transfer;
+
+ // For most method calls, forward to the transfer object.
+ for (let name in transfer) {
+ if (name != "QueryInterface" &&
+ name != "onStateChange") {
+ this[name] = (...args) => transfer[name].apply(transfer, args);
+ }
+ }
+
+ // Allow saveToFile to await completion for error handling
+ this._completedDeferred = defer();
+ this.completed = this._completedDeferred.promise;
+}
+
+DownloadListener.prototype = {
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIInterfaceRequestor) ||
+ iid.equals(Ci.nsIWebProgressListener) ||
+ iid.equals(Ci.nsIWebProgressListener2) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ getInterface: function(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt) ||
+ iid.equals(Ci.nsIAuthPrompt2)) {
+ let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Ci.nsIPromptFactory);
+ return ww.getPrompt(this.window, iid);
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ onStateChange: function(webProgress, request, state, status) {
+ // Check if the download has completed
+ if ((state & Ci.nsIWebProgressListener.STATE_STOP) &&
+ (state & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ if (status == Cr.NS_OK) {
+ this._completedDeferred.resolve();
+ } else {
+ this._completedDeferred.reject();
+ }
+ }
+
+ this.transfer.onStateChange.apply(this.transfer, arguments);
+ }
+};
+
+/**
+ * Save the screenshot data to disk, returning a promise which is resolved on
+ * completion.
+ */
+var saveToFile = Task.async(function*(context, reply) {
+ let document = context.environment.chromeDocument;
+ let window = context.environment.chromeWindow;
+
+ // Check there is a .png extension to filename
+ if (!reply.filename.match(/.png$/i)) {
+ reply.filename += ".png";
+ }
+
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let downloadsDirExists = yield OS.File.exists(downloadsDir);
+ if (downloadsDirExists) {
+ // If filename is absolute, it will override the downloads directory and
+ // still be applied as expected.
+ reply.filename = OS.Path.join(downloadsDir, reply.filename);
+ }
+
+ let sourceURI = Services.io.newURI(reply.data, null, null);
+ let targetFile = new FileUtils.File(reply.filename);
+ let targetFileURI = Services.io.newFileURI(targetFile);
+
+ // Create download and track its progress.
+ // This is adapted from saveURL in contentAreaUtils.js, but simplified greatly
+ // and modified to allow saving to arbitrary paths on disk. Using these
+ // objects as opposed to just writing with OS.File allows us to tie into the
+ // download manager to record a download entry and to get visual feedback from
+ // the downloads toolbar button when the save is done.
+ const nsIWBP = Ci.nsIWebBrowserPersist;
+ const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES |
+ nsIWBP.PERSIST_FLAGS_BYPASS_CACHE |
+ nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+ let isPrivate =
+ PrivateBrowsingUtils.isContentWindowPrivate(document.defaultView);
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ persist.persistFlags = flags;
+ let tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+ tr.init(sourceURI,
+ targetFileURI,
+ "",
+ null,
+ null,
+ null,
+ persist,
+ isPrivate);
+ let listener = new DownloadListener(window, tr);
+ persist.progressListener = listener;
+ persist.savePrivacyAwareURI(sourceURI,
+ null,
+ document.documentURIObject,
+ Ci.nsIHttpChannel
+ .REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE,
+ null,
+ null,
+ targetFileURI,
+ isPrivate);
+
+ try {
+ // Await successful completion of the save via the listener
+ yield listener.completed;
+ reply.destinations.push(l10n.lookup("screenshotSavedToFile") +
+ ` "${reply.filename}"`);
+ } catch (ex) {
+ console.error(ex);
+ reply.destinations.push(l10n.lookup("screenshotErrorSavingToFile") + " " +
+ reply.filename);
+ }
+});
diff --git a/devtools/shared/gcli/commands/security.js b/devtools/shared/gcli/commands/security.js
new file mode 100644
index 000000000..eeed03c61
--- /dev/null
+++ b/devtools/shared/gcli/commands/security.js
@@ -0,0 +1,328 @@
+/* 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/. */
+
+/**
+ * The Security devtool supports the following arguments:
+ * * Security CSP
+ * Provides feedback about the current CSP
+ *
+ * * Security referrer
+ * Provides information about the current referrer policy
+ */
+
+"use strict";
+
+const { Cc, Ci, Cu, CC } = require("chrome");
+const l10n = require("gcli/l10n");
+const CSP = Cc["@mozilla.org/cspcontext;1"].getService(Ci.nsIContentSecurityPolicy);
+
+const GOOD_IMG_SRC = "chrome://browser/content/gcli_sec_good.svg";
+const MOD_IMG_SRC = "chrome://browser/content/gcli_sec_moderate.svg";
+const BAD_IMG_SRC = "chrome://browser/content/gcli_sec_bad.svg";
+
+
+// special handling within policy
+const POLICY_REPORT_ONLY = "report-only"
+
+// special handling of directives
+const DIR_UPGRADE_INSECURE = "upgrade-insecure-requests";
+const DIR_BLOCK_ALL_MIXED_CONTENT = "block-all-mixed-content";
+
+// special handling of sources
+const SRC_UNSAFE_INLINE = "'unsafe-inline'";
+const SRC_UNSAFE_EVAL = "'unsafe-eval'";
+
+const WILDCARD_MSG = l10n.lookup("securityCSPRemWildCard");
+const XSS_WARNING_MSG = l10n.lookup("securityCSPPotentialXSS");
+const NO_CSP_ON_PAGE_MSG = l10n.lookup("securityCSPNoCSPOnPage");
+const CONTENT_SECURITY_POLICY_MSG = l10n.lookup("securityCSPHeaderOnPage");
+const CONTENT_SECURITY_POLICY_REPORT_ONLY_MSG = l10n.lookup("securityCSPROHeaderOnPage");
+
+const NEXT_URI_HEADER = l10n.lookup("securityReferrerNextURI");
+const CALCULATED_REFERRER_HEADER = l10n.lookup("securityReferrerCalculatedReferrer");
+/* The official names from the W3C Referrer Policy Draft http://www.w3.org/TR/referrer-policy/ */
+const REFERRER_POLICY_NAMES = [ "None When Downgrade (default)", "None", "Origin Only", "Origin When Cross-Origin", "Unsafe URL" ];
+
+exports.items = [
+ {
+ // --- General Security information
+ name: "security",
+ description: l10n.lookup("securityPrivacyDesc"),
+ manual: l10n.lookup("securityManual")
+ },
+ {
+ // --- CSP specific Security information
+ item: "command",
+ runAt: "server",
+ name: "security csp",
+ description: l10n.lookup("securityCSPDesc"),
+ manual: l10n.lookup("securityCSPManual"),
+ returnType: "securityCSPInfo",
+ exec: function(args, context) {
+
+ var cspJSON = context.environment.document.nodePrincipal.cspJSON;
+ var cspOBJ = JSON.parse(cspJSON);
+
+ var outPolicies = [];
+
+ var policies = cspOBJ["csp-policies"];
+
+ // loop over all the different policies
+ for (var csp in policies) {
+ var curPolicy = policies[csp];
+
+ // loop over all the directive-values within that policy
+ var outDirectives = [];
+ var outHeader = CONTENT_SECURITY_POLICY_MSG;
+ for (var dir in curPolicy) {
+ var curDir = curPolicy[dir];
+
+ // when iterating properties within the obj we might also
+ // encounter the 'report-only' flag, which is not a csp directive.
+ if (dir === POLICY_REPORT_ONLY) {
+ outHeader = curPolicy[POLICY_REPORT_ONLY] === true ?
+ CONTENT_SECURITY_POLICY_REPORT_ONLY_MSG :
+ CONTENT_SECURITY_POLICY_MSG;
+ continue;
+ }
+
+ // loop over all the directive-sources within that directive
+ var outSrcs = [];
+
+ // special case handling for the directives
+ // upgrade-insecure-requests and block-all-mixed-content
+ // which do not include any srcs
+ if (dir === DIR_UPGRADE_INSECURE ||
+ dir === DIR_BLOCK_ALL_MIXED_CONTENT) {
+ outSrcs.push({
+ icon: GOOD_IMG_SRC,
+ src: "", // no src
+ desc: "" // no description
+ });
+ }
+
+ for (var src in curDir) {
+ var curSrc = curDir[src];
+
+ // the default icon and descritpion of the directive-src
+ var outIcon = GOOD_IMG_SRC;
+ var outDesc = "";
+
+ if (curSrc.indexOf("*") > -1) {
+ outIcon = MOD_IMG_SRC;
+ outDesc = WILDCARD_MSG;
+ }
+ if (curSrc == SRC_UNSAFE_INLINE || curSrc == SRC_UNSAFE_EVAL) {
+ outIcon = BAD_IMG_SRC;
+ outDesc = XSS_WARNING_MSG;
+ }
+ outSrcs.push({
+ icon: outIcon,
+ src: curSrc,
+ desc: outDesc
+ });
+ }
+ // append info about that directive to the directives array
+ outDirectives.push({
+ dirValue: dir,
+ dirSrc: outSrcs
+ });
+ }
+ // append info about the policy to the policies array
+ outPolicies.push({
+ header: outHeader,
+ directives: outDirectives
+ });
+ }
+ return outPolicies;
+ }
+ },
+ {
+ item: "converter",
+ from: "securityCSPInfo",
+ to: "view",
+ exec: function(cspInfo, context) {
+ var url = context.environment.target.url;
+
+ if (cspInfo.length == 0) {
+ return context.createView({
+ html:
+ "<table class='gcli-csp-detail' cellspacing='10' valign='top'>" +
+ " <tr>" +
+ " <td> <img src='chrome://browser/content/gcli_sec_bad.svg' width='20px' /> </td> " +
+ " <td>" + NO_CSP_ON_PAGE_MSG + " <b>" + url + "</b></td>" +
+ " </tr>" +
+ "</table>"});
+ }
+
+ return context.createView({
+ html:
+ "<table class='gcli-csp-detail' cellspacing='10' valign='top'>" +
+ // iterate all policies
+ " <tr foreach='csp in ${cspinfo}' >" +
+ " <td> ${csp.header} <b>" + url + "</b><br/><br/>" +
+ " <table class='gcli-csp-dir-detail' valign='top'>" +
+ // >> iterate all directives
+ " <tr foreach='dir in ${csp.directives}' >" +
+ " <td valign='top'> ${dir.dirValue} </td>" +
+ " <td valign='top'>" +
+ " <table class='gcli-csp-src-detail' valign='top'>" +
+ // >> >> iterate all srs
+ " <tr foreach='src in ${dir.dirSrc}' >" +
+ " <td valign='center' width='20px'> <img src= \"${src.icon}\" width='20px' /> </td> " +
+ " <td valign='center' width='200px'> ${src.src} </td>" +
+ " <td valign='center'> ${src.desc} </td>" +
+ " </tr>" +
+ " </table>" +
+ " </td>" +
+ " </tr>" +
+ " </table>" +
+ " </td>" +
+ " </tr>" +
+ "</table>",
+ data: {
+ cspinfo: cspInfo,
+ }
+ });
+ }
+ },
+ {
+ // --- Referrer Policy specific Security information
+ item: "command",
+ runAt: "server",
+ name: "security referrer",
+ description: l10n.lookup("securityReferrerPolicyDesc"),
+ manual: l10n.lookup("securityReferrerPolicyManual"),
+ returnType: "securityReferrerPolicyInfo",
+ exec: function(args, context) {
+ var doc = context.environment.document;
+
+ var referrerPolicy = doc.referrerPolicy;
+
+ var pageURI = doc.documentURIObject;
+ var sameDomainReferrer = "";
+ var otherDomainReferrer = "";
+ var downgradeReferrer = "";
+ var otherDowngradeReferrer = "";
+ var origin = pageURI.prePath;
+
+ switch (referrerPolicy) {
+ case Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER:
+ // sends no referrer
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = "(no referrer)";
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_ORIGIN:
+ // only sends the origin of the referring URL
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = origin;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_ORIGIN_WHEN_XORIGIN:
+ // same as default, but reduced to ORIGIN when cross-origin.
+ sameDomainReferrer = pageURI.spec;
+ otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = origin;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_UNSAFE_URL:
+ // always sends the referrer, even on downgrade.
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = pageURI.spec;
+ break;
+ case Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE:
+ // default state, doesn't send referrer from https->http
+ sameDomainReferrer = otherDomainReferrer = pageURI.spec;
+ downgradeReferrer = otherDowngradeReferrer = "(no referrer)";
+ break;
+ default:
+ // this is a new referrer policy which we do not know about
+ sameDomainReferrer
+ = otherDomainReferrer
+ = downgradeReferrer
+ = otherDowngradeReferrer
+ = "(unknown Referrer Policy)";
+ break;
+ }
+
+ var sameDomainUri = origin + "/*";
+
+ var referrerUrls = [
+ // add the referrer uri 'referrer' we would send when visiting 'uri'
+ {
+ uri: pageURI.scheme+'://example.com/',
+ referrer: otherDomainReferrer,
+ description: l10n.lookup('securityReferrerPolicyOtherDomain')},
+ {
+ uri: sameDomainUri,
+ referrer: sameDomainReferrer,
+ description: l10n.lookup('securityReferrerPolicySameDomain')}
+ ];
+
+ if (pageURI.schemeIs('https')) {
+ // add the referrer we would send on downgrading http->https
+ if (sameDomainReferrer != downgradeReferrer) {
+ referrerUrls.push({
+ uri: "http://"+pageURI.hostPort+"/*",
+ referrer: downgradeReferrer,
+ description:
+ l10n.lookup('securityReferrerPolicySameDomainDowngrade')
+ });
+ }
+ if (otherDomainReferrer != otherDowngradeReferrer) {
+ referrerUrls.push({
+ uri: "http://example.com/",
+ referrer: otherDowngradeReferrer,
+ description:
+ l10n.lookup('securityReferrerPolicyOtherDomainDowngrade')
+ });
+ }
+ }
+
+ return {
+ header: l10n.lookupFormat("securityReferrerPolicyReportHeader",
+ [pageURI.spec]),
+ policyName: REFERRER_POLICY_NAMES[referrerPolicy],
+ urls: referrerUrls
+ }
+ }
+ },
+ {
+ item: "converter",
+ from: "securityReferrerPolicyInfo",
+ to: "view",
+ exec: function(referrerPolicyInfo, context) {
+ return context.createView({
+ html:
+ "<div class='gcli-referrer-policy'>" +
+ " <strong> ${rpi.header} </strong> <br />" +
+ " ${rpi.policyName} <br />" +
+ " <table class='gcli-referrer-policy-detail' cellspacing='10' >" +
+ " <tr>" +
+ " <th> " + NEXT_URI_HEADER + " </th>" +
+ " <th> " + CALCULATED_REFERRER_HEADER + " </th>" +
+ " </tr>" +
+ // iterate all policies
+ " <tr foreach='nextURI in ${rpi.urls}' >" +
+ " <td> ${nextURI.description} (e.g., ${nextURI.uri}) </td>" +
+ " <td> ${nextURI.referrer} </td>" +
+ " </tr>" +
+ " </table>" +
+ "</div>",
+ data: {
+ rpi: referrerPolicyInfo,
+ }
+ });
+ }
+ }
+];