fune/devtools/server/actors/webconsole/utils.js
Kris Maglione 3af8c4138c Bug 1651519: Remove nsIDOMWindowUtils::currentInnerWindowID. r=nika,remote-protocol-reviewers,webcompat-reviewers,whimboo
It has some properties which make it footgunny, especially in the face of
Fission. Callers should use WindowGlobalChild.innerWindowId instead.

Differential Revision: https://phabricator.services.mozilla.com/D82801
2020-08-17 20:20:50 +00:00

742 lines
20 KiB
JavaScript

/* 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, Cu, Cc } = require("chrome");
// Note that this is only used in WebConsoleCommands, see $0 and screenshot.
if (!isWorker) {
loader.lazyRequireGetter(
this,
"captureScreenshot",
"devtools/shared/screenshot/capture",
true
);
}
const CONSOLE_WORKER_IDS = (exports.CONSOLE_WORKER_IDS = [
"SharedWorker",
"ServiceWorker",
"Worker",
]);
var WebConsoleUtils = {
/**
* Given a message, return one of CONSOLE_WORKER_IDS if it matches
* one of those.
*
* @return string
*/
getWorkerType: function(message) {
const id = message ? message.innerID : null;
return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null;
},
/**
* Clone an object.
*
* @param object object
* The object you want cloned.
* @param boolean recursive
* Tells if you want to dig deeper into the object, to clone
* recursively.
* @param function [filter]
* Optional, filter function, called for every property. Three
* arguments are passed: key, value and object. Return true if the
* property should be added to the cloned object. Return false to skip
* the property.
* @return object
* The cloned object.
*/
cloneObject: function(object, recursive, filter) {
if (typeof object != "object") {
return object;
}
let temp;
if (Array.isArray(object)) {
temp = [];
object.forEach(function(value, index) {
if (!filter || filter(index, value, object)) {
temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
}
});
} else {
temp = {};
for (const key in object) {
const value = object[key];
if (
object.hasOwnProperty(key) &&
(!filter || filter(key, value, object))
) {
temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value;
}
}
}
return temp;
},
/**
* Gets the ID of the inner window of this DOM window.
*
* @param nsIDOMWindow window
* @return integer|null
* Inner ID for the given window, null if we can't access it.
*/
getInnerWindowId: function(window) {
// Might throw with SecurityError: Permission denied to access property
// "windowGlobalChild" on cross-origin object.
try {
return window.windowGlobalChild.innerWindowId;
} catch (e) {
return null;
}
},
/**
* Recursively gather a list of inner window ids given a
* top level window.
*
* @param nsIDOMWindow window
* @return Array
* list of inner window ids.
*/
getInnerWindowIDsForFrames: function(window) {
const innerWindowID = this.getInnerWindowId(window);
if (innerWindowID === null) {
return [];
}
let ids = [innerWindowID];
if (window.frames) {
for (let i = 0; i < window.frames.length; i++) {
const frame = window.frames[i];
ids = ids.concat(this.getInnerWindowIDsForFrames(frame));
}
}
return ids;
},
/**
* Create a grip for the given value. If the value is an object,
* an object wrapper will be created.
*
* @param mixed value
* The value you want to create a grip for, before sending it to the
* client.
* @param function objectWrapper
* If the value is an object then the objectWrapper function is
* invoked to give us an object grip. See this.getObjectGrip().
* @return mixed
* The value grip.
*/
createValueGrip: function(value, objectWrapper) {
switch (typeof value) {
case "boolean":
return value;
case "string":
return objectWrapper(value);
case "number":
if (value === Infinity) {
return { type: "Infinity" };
} else if (value === -Infinity) {
return { type: "-Infinity" };
} else if (Number.isNaN(value)) {
return { type: "NaN" };
} else if (!value && 1 / value === -Infinity) {
return { type: "-0" };
}
return value;
case "undefined":
return { type: "undefined" };
case "object":
if (value === null) {
return { type: "null" };
}
// Fall through.
case "function":
return objectWrapper(value);
default:
console.error(
"Failed to provide a grip for value of " + typeof value + ": " + value
);
return null;
}
},
/**
* Remove any frames in a stack that are above a debugger-triggered evaluation
* and will correspond with devtools server code, which we never want to show
* to the user.
*
* @param array stack
* An array of frames, with the topmost first, and each of which has a
* 'filename' property.
* @return array
* An array of stack frames with any devtools server frames removed.
* The original array is not modified.
*/
removeFramesAboveDebuggerEval(stack) {
const debuggerEvalFilename = "debugger eval code";
// Remove any frames for server code above the last debugger eval frame.
const evalIndex = stack.findIndex(({ filename }, idx, arr) => {
const nextFrame = arr[idx + 1];
return (
filename == debuggerEvalFilename &&
(!nextFrame || nextFrame.filename !== debuggerEvalFilename)
);
});
if (evalIndex != -1) {
return stack.slice(0, evalIndex + 1);
}
// In some cases (e.g. evaluated expression with SyntaxError), we might not have a
// "debugger eval code" frame but still have internal ones. If that's the case, we
// return null as the end user shouldn't see those frames.
if (
stack.some(
({ filename }) =>
filename && filename.startsWith("resource://devtools/")
)
) {
return null;
}
return stack;
},
};
exports.WebConsoleUtils = WebConsoleUtils;
/**
* WebConsole commands manager.
*
* Defines a set of functions /variables ("commands") that are available from
* the Web Console but not from the web page.
*
*/
var WebConsoleCommands = {
_registeredCommands: new Map(),
_originalCommands: new Map(),
/**
* @private
* Reserved for built-in commands. To register a command from the code of an
* add-on, see WebConsoleCommands.register instead.
*
* @see WebConsoleCommands.register
*/
_registerOriginal: function(name, command) {
this.register(name, command);
this._originalCommands.set(name, this.getCommand(name));
},
/**
* Register a new command.
* @param {string} name The command name (exemple: "$")
* @param {(function|object)} command The command to register.
* It can be a function so the command is a function (like "$()"),
* or it can also be a property descriptor to describe a getter / value (like
* "$0").
*
* The command function or the command getter are passed a owner object as
* their first parameter (see the example below).
*
* Note that setters don't work currently and "enumerable" and "configurable"
* are forced to true.
*
* @example
*
* WebConsoleCommands.register("$", function JSTH_$(owner, selector)
* {
* return owner.window.document.querySelector(selector);
* });
*
* WebConsoleCommands.register("$0", {
* get: function(owner) {
* return owner.makeDebuggeeValue(owner.selectedNode);
* }
* });
*/
register: function(name, command) {
this._registeredCommands.set(name, command);
},
/**
* Unregister a command.
*
* If the command being unregister overrode a built-in command,
* the latter is restored.
*
* @param {string} name The name of the command
*/
unregister: function(name) {
this._registeredCommands.delete(name);
if (this._originalCommands.has(name)) {
this.register(name, this._originalCommands.get(name));
}
},
/**
* Returns a command by its name.
*
* @param {string} name The name of the command.
*
* @return {(function|object)} The command.
*/
getCommand: function(name) {
return this._registeredCommands.get(name);
},
/**
* Returns true if a command is registered with the given name.
*
* @param {string} name The name of the command.
*
* @return {boolean} True if the command is registered.
*/
hasCommand: function(name) {
return this._registeredCommands.has(name);
},
};
exports.WebConsoleCommands = WebConsoleCommands;
/*
* Built-in commands.
*
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*/
/**
* Find a node by ID.
*
* @param string id
* The ID of the element you want.
* @return Node or null
* The result of calling document.querySelector(selector).
*/
WebConsoleCommands._registerOriginal("$", function(owner, selector) {
try {
return owner.window.document.querySelector(selector);
} catch (err) {
// Throw an error like `err` but that belongs to `owner.window`.
throw new owner.window.DOMException(err.message, err.name);
}
});
/**
* Find the nodes matching a CSS selector.
*
* @param string selector
* A string that is passed to window.document.querySelectorAll.
* @return NodeList
* Returns the result of document.querySelectorAll(selector).
*/
WebConsoleCommands._registerOriginal("$$", function(owner, selector) {
let nodes;
try {
nodes = owner.window.document.querySelectorAll(selector);
} catch (err) {
// Throw an error like `err` but that belongs to `owner.window`.
throw new owner.window.DOMException(err.message, err.name);
}
// Calling owner.window.Array.from() doesn't work without accessing the
// wrappedJSObject, so just loop through the results instead.
const result = new owner.window.Array();
for (let i = 0; i < nodes.length; i++) {
result.push(nodes[i]);
}
return result;
});
/**
* Returns the result of the last console input evaluation
*
* @return object|undefined
* Returns last console evaluation or undefined
*/
WebConsoleCommands._registerOriginal("$_", {
get: function(owner) {
return owner.consoleActor.getLastConsoleInputEvaluation();
},
});
/**
* Runs an xPath query and returns all matched nodes.
*
* @param string xPath
* xPath search query to execute.
* @param [optional] Node context
* Context to run the xPath query on. Uses window.document if not set.
* @param [optional] string|number resultType
Specify the result type. Default value XPathResult.ANY_TYPE
* @return array of Node
*/
WebConsoleCommands._registerOriginal("$x", function(
owner,
xPath,
context,
resultType = owner.window.XPathResult.ANY_TYPE
) {
const nodes = new owner.window.Array();
// Not waiving Xrays, since we want the original Document.evaluate function,
// instead of anything that's been redefined.
const doc = owner.window.document;
context = context || doc;
switch (resultType) {
case "number":
resultType = owner.window.XPathResult.NUMBER_TYPE;
break;
case "string":
resultType = owner.window.XPathResult.STRING_TYPE;
break;
case "bool":
resultType = owner.window.XPathResult.BOOLEAN_TYPE;
break;
case "node":
resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE;
break;
case "nodes":
resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
break;
}
const results = doc.evaluate(xPath, context, null, resultType, null);
if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) {
return results.numberValue;
}
if (results.resultType === owner.window.XPathResult.STRING_TYPE) {
return results.stringValue;
}
if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) {
return results.booleanValue;
}
if (
results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE ||
results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE
) {
return results.singleNodeValue;
}
if (
results.resultType ===
owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE ||
results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
) {
for (let i = 0; i < results.snapshotLength; i++) {
nodes.push(results.snapshotItem(i));
}
return nodes;
}
let node;
while ((node = results.iterateNext())) {
nodes.push(node);
}
return nodes;
});
/**
* Returns the currently selected object in the highlighter.
*
* @return Object representing the current selection in the
* Inspector, or null if no selection exists.
*/
WebConsoleCommands._registerOriginal("$0", {
get: function(owner) {
return owner.makeDebuggeeValue(owner.selectedNode);
},
});
/**
* Clears the output of the WebConsole.
*/
WebConsoleCommands._registerOriginal("clear", function(owner) {
owner.helperResult = {
type: "clearOutput",
};
});
/**
* Clears the input history of the WebConsole.
*/
WebConsoleCommands._registerOriginal("clearHistory", function(owner) {
owner.helperResult = {
type: "clearHistory",
};
});
/**
* Returns the result of Object.keys(object).
*
* @param object object
* Object to return the property names from.
* @return array of strings
*/
WebConsoleCommands._registerOriginal("keys", function(owner, object) {
// Need to waive Xrays so we can iterate functions and accessor properties
return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window);
});
/**
* Returns the values of all properties on object.
*
* @param object object
* Object to display the values from.
* @return array of string
*/
WebConsoleCommands._registerOriginal("values", function(owner, object) {
const values = [];
// Need to waive Xrays so we can iterate functions and accessor properties
const waived = Cu.waiveXrays(object);
const names = Object.getOwnPropertyNames(waived);
for (const name of names) {
values.push(waived[name]);
}
return Cu.cloneInto(values, owner.window);
});
/**
* Opens a help window in MDN.
*/
WebConsoleCommands._registerOriginal("help", function(owner) {
owner.helperResult = { type: "help" };
});
/**
* Change the JS evaluation scope.
*
* @param DOMElement|string|window window
* The window object to use for eval scope. This can be a string that
* is used to perform document.querySelector(), to find the iframe that
* you want to cd() to. A DOMElement can be given as well, the
* .contentWindow property is used. Lastly, you can directly pass
* a window object. If you call cd() with no arguments, the current
* eval scope is cleared back to its default (the top window).
*/
WebConsoleCommands._registerOriginal("cd", function(owner, window) {
// Log a deprecation warning.
const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
const deprecationMessage =
"The `cd` command will be disabled in a future release. " +
"See https://bugzilla.mozilla.org/show_bug.cgi?id=1605327 for more information.";
scriptError.initWithWindowID(
deprecationMessage,
null,
null,
0,
0,
1,
"content javascript",
owner.window.windowGlobalChild.innerWindowId
);
const Services = require("Services");
Services.console.logMessage(scriptError);
if (!window) {
owner.consoleActor.evalGlobal = null;
owner.helperResult = { type: "cd" };
return;
}
if (typeof window == "string") {
window = owner.window.document.querySelector(window);
}
if (Element.isInstance(window) && window.contentWindow) {
window = window.contentWindow;
}
if (!(window instanceof Ci.nsIDOMWindow)) {
owner.helperResult = {
type: "error",
message: "cdFunctionInvalidArgument",
};
return;
}
owner.consoleActor.evalGlobal = window;
owner.helperResult = { type: "cd" };
});
/**
* Inspects the passed object. This is done by opening the PropertyPanel.
*
* @param object object
* Object to inspect.
*/
WebConsoleCommands._registerOriginal("inspect", function(
owner,
object,
forceExpandInConsole = false
) {
const dbgObj = owner.preprocessDebuggerObject(
owner.makeDebuggeeValue(object)
);
const grip = owner.createValueGrip(dbgObj);
owner.helperResult = {
type: "inspectObject",
input: owner.evalInput,
object: grip,
forceExpandInConsole,
};
});
/**
* Copy the String representation of a value to the clipboard.
*
* @param any value
* A value you want to copy as a string.
* @return void
*/
WebConsoleCommands._registerOriginal("copy", function(owner, value) {
let payload;
try {
if (Element.isInstance(value)) {
payload = value.outerHTML;
} else if (typeof value == "string") {
payload = value;
} else {
payload = JSON.stringify(value, null, " ");
}
} catch (ex) {
payload = "/* " + ex + " */";
}
owner.helperResult = {
type: "copyValueToClipboard",
value: payload,
};
});
/**
* Take a screenshot of a page.
*
* @param object args
* The arguments to be passed to the screenshot
* @return void
*/
WebConsoleCommands._registerOriginal("screenshot", function(owner, args = {}) {
owner.helperResult = (async () => {
// creates data for saving the screenshot
// help is handled on the client side
const value = await captureScreenshot(args, owner.window.document);
return {
type: "screenshotOutput",
value,
// pass args through to the client, so that the client can take care of copying
// and saving the screenshot data on the client machine instead of on the
// remote machine
args,
};
})();
});
/**
* Block specific resource from loading
*
* @param object args
* an object with key "url", i.e. a filter
*
* @return void
*/
WebConsoleCommands._registerOriginal("block", function(owner, args = {}) {
if (!args.url) {
owner.helperResult = {
type: "error",
message: "webconsole.messages.commands.blockArgMissing",
};
return;
}
owner.helperResult = (async () => {
await owner.consoleActor.blockRequest(args);
return {
type: "blockURL",
args,
};
})();
});
/*
* Unblock a blocked a resource
*
* @param object filter
* an object with key "url", i.e. a filter
*
* @return void
*/
WebConsoleCommands._registerOriginal("unblock", function(owner, args = {}) {
if (!args.url) {
owner.helperResult = {
type: "error",
message: "webconsole.messages.commands.blockArgMissing",
};
return;
}
owner.helperResult = (async () => {
await owner.consoleActor.unblockRequest(args);
return {
type: "unblockURL",
args,
};
})();
});
/**
* (Internal only) Add the bindings to |owner.sandbox|.
* This is intended to be used by the WebConsole actor only.
*
* @param object owner
* The owning object.
*/
function addWebConsoleCommands(owner) {
// Not supporting extra commands in workers yet. This should be possible to
// add one by one as long as they don't require jsm, Cu, etc.
const commands = isWorker ? [] : WebConsoleCommands._registeredCommands;
if (!owner) {
throw new Error("The owner is required");
}
for (const [name, command] of commands) {
if (typeof command === "function") {
owner.sandbox[name] = command.bind(undefined, owner);
} else if (typeof command === "object") {
const clone = Object.assign({}, command, {
// We force the enumerability and the configurability (so the
// WebConsoleActor can reconfigure the property).
enumerable: true,
configurable: true,
});
if (typeof command.get === "function") {
clone.get = command.get.bind(undefined, owner);
}
if (typeof command.set === "function") {
clone.set = command.set.bind(undefined, owner);
}
Object.defineProperty(owner.sandbox, name, clone);
}
}
}
exports.addWebConsoleCommands = addWebConsoleCommands;