mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
320
devtools/shared/gcli/commands/addon.js
Normal file
320
devtools/shared/gcli/commands/addon.js
Normal 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 : [];
|
||||
186
devtools/shared/gcli/commands/appcache.js
Normal file
186
devtools/shared/gcli/commands/appcache.js
Normal 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);
|
||||
}
|
||||
}
|
||||
];
|
||||
219
devtools/shared/gcli/commands/calllog.js
Normal file
219
devtools/shared/gcli/commands/calllog.js
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
178
devtools/shared/gcli/commands/cmd.js
Normal file
178
devtools/shared/gcli/commands/cmd.js
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
300
devtools/shared/gcli/commands/cookie.js
Normal file
300
devtools/shared/gcli/commands/cookie.js
Normal 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);
|
||||
}
|
||||
}
|
||||
];
|
||||
201
devtools/shared/gcli/commands/csscoverage.js
Normal file
201
devtools/shared/gcli/commands/csscoverage.js
Normal 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;
|
||||
}
|
||||
}
|
||||
];
|
||||
77
devtools/shared/gcli/commands/folder.js
Normal file
77
devtools/shared/gcli/commands/folder.js
Normal 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);
|
||||
}
|
||||
}
|
||||
];
|
||||
158
devtools/shared/gcli/commands/highlight.js
Normal file
158
devtools/shared/gcli/commands/highlight.js
Normal 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
|
||||
}
|
||||
];
|
||||
179
devtools/shared/gcli/commands/index.js
Normal file
179
devtools/shared/gcli/commands/index.js
Normal 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);
|
||||
}
|
||||
};
|
||||
86
devtools/shared/gcli/commands/inject.js
Normal file
86
devtools/shared/gcli/commands/inject.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
];
|
||||
134
devtools/shared/gcli/commands/jsb.js
Normal file
134
devtools/shared/gcli/commands/jsb.js
Normal 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;
|
||||
}
|
||||
}
|
||||
];
|
||||
106
devtools/shared/gcli/commands/listen.js
Normal file
106
devtools/shared/gcli/commands/listen.js
Normal 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");
|
||||
}
|
||||
}
|
||||
];
|
||||
83
devtools/shared/gcli/commands/mdn.js
Normal file
83
devtools/shared/gcli/commands/mdn.js
Normal 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;
|
||||
}
|
||||
}];
|
||||
112
devtools/shared/gcli/commands/measure.js
Normal file
112
devtools/shared/gcli/commands/measure.js
Normal 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};
|
||||
}
|
||||
}
|
||||
];
|
||||
56
devtools/shared/gcli/commands/media.js
Normal file
56
devtools/shared/gcli/commands/media.js
Normal 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();
|
||||
}
|
||||
}
|
||||
];
|
||||
30
devtools/shared/gcli/commands/moz.build
Normal file
30
devtools/shared/gcli/commands/moz.build
Normal 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',
|
||||
)
|
||||
276
devtools/shared/gcli/commands/pagemod.js
Normal file
276
devtools/shared/gcli/commands/pagemod.js
Normal 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 '';
|
||||
}
|
||||
}
|
||||
];
|
||||
201
devtools/shared/gcli/commands/paintflashing.js
Normal file
201
devtools/shared/gcli/commands/paintflashing.js
Normal 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 };
|
||||
}
|
||||
}
|
||||
];
|
||||
24
devtools/shared/gcli/commands/qsa.js
Normal file
24
devtools/shared/gcli/commands/qsa.js
Normal 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;
|
||||
}
|
||||
}
|
||||
];
|
||||
77
devtools/shared/gcli/commands/restart.js
Normal file
77
devtools/shared/gcli/commands/restart.js
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
];
|
||||
110
devtools/shared/gcli/commands/rulers.js
Normal file
110
devtools/shared/gcli/commands/rulers.js
Normal 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};
|
||||
}
|
||||
}
|
||||
];
|
||||
579
devtools/shared/gcli/commands/screenshot.js
Normal file
579
devtools/shared/gcli/commands/screenshot.js
Normal 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);
|
||||
}
|
||||
});
|
||||
328
devtools/shared/gcli/commands/security.js
Normal file
328
devtools/shared/gcli/commands/security.js
Normal 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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue