import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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 : [];

View file

@ -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);
}
}
];

View file

@ -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 ]);
}
}
];

View file

@ -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 ]);
}
}
];

View file

@ -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);
}
}
];

View file

@ -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;
}
}
];

View file

@ -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);
}
}
];

View file

@ -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
}
];

View file

@ -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);
}
};

View file

@ -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]);
}
}
];

View file

@ -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;
}
}
];

View file

@ -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");
}
}
];

View file

@ -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;
}
}];

View file

@ -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};
}
}
];

View file

@ -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();
}
}
];

View file

@ -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',
)

View file

@ -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 '';
}
}
];

View file

@ -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 };
}
}
];

View file

@ -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;
}
}
];

View file

@ -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 ]);
}
}
];

View file

@ -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};
}
}
];

View file

@ -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);
}
});

View file

@ -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,
}
});
}
}
];