summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/framescript
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/framescript
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-37d5300335d81cecbecc99812747a657588c63eb.tar
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz
UXP-37d5300335d81cecbecc99812747a657588c63eb.zip
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/framescript')
-rw-r--r--toolkit/jetpack/framescript/FrameScriptManager.jsm27
-rw-r--r--toolkit/jetpack/framescript/content.jsm94
-rw-r--r--toolkit/jetpack/framescript/context-menu.js215
-rw-r--r--toolkit/jetpack/framescript/manager.js26
-rw-r--r--toolkit/jetpack/framescript/util.js25
5 files changed, 387 insertions, 0 deletions
diff --git a/toolkit/jetpack/framescript/FrameScriptManager.jsm b/toolkit/jetpack/framescript/FrameScriptManager.jsm
new file mode 100644
index 000000000..1ce6ceb07
--- /dev/null
+++ b/toolkit/jetpack/framescript/FrameScriptManager.jsm
@@ -0,0 +1,27 @@
+/* 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 globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"].
+ getService(Components.interfaces.nsIMessageListenerManager);
+
+// Load frame scripts from the same dir as this module.
+// Since this JSM will be loaded using require(), PATH will be
+// overridden while running tests, just like any other module.
+const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', '');
+
+// Builds a unique loader ID for this runtime. We prefix with the SDK path so
+// overriden versions of the SDK don't conflict
+var LOADER_ID = 0;
+this.getNewLoaderID = () => {
+ return PATH + ":" + LOADER_ID++;
+}
+
+const frame_script = function(contentFrame, PATH) {
+ let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {});
+ registerContentFrame(contentFrame);
+}
+globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true);
+
+this.EXPORTED_SYMBOLS = ['getNewLoaderID'];
diff --git a/toolkit/jetpack/framescript/content.jsm b/toolkit/jetpack/framescript/content.jsm
new file mode 100644
index 000000000..eaee26be3
--- /dev/null
+++ b/toolkit/jetpack/framescript/content.jsm
@@ -0,0 +1,94 @@
+/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+const { Services } = Cu.import('resource://gre/modules/Services.jsm');
+
+const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+ getService(Ci.nsISyncMessageSender);
+
+this.EXPORTED_SYMBOLS = ["registerContentFrame"];
+
+// This may be an overriden version of the SDK so use the PATH as a key for the
+// initial messages before we have a loaderID.
+const PATH = __URI__.replace('framescript/content.jsm', '');
+
+const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {});
+
+// one Loader instance per addon (per @loader/options to be precise)
+var addons = new Map();
+
+// Tell the parent that a new process is ready
+cpmm.sendAsyncMessage('sdk/remote/process/start', {
+ modulePath: PATH
+});
+
+// Load a child process module loader with the given loader options
+cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => {
+ if (modulePath != PATH)
+ return;
+
+ // During startup races can mean we get a second load message
+ if (addons.has(loaderID))
+ return;
+
+ options.waiveInterposition = true;
+
+ let loader = Loader.Loader(options);
+ let addon = {
+ loader,
+ require: Loader.Require(loader, { id: 'LoaderHelper' }),
+ }
+ addons.set(loaderID, addon);
+
+ cpmm.sendAsyncMessage('sdk/remote/process/attach', {
+ loaderID,
+ processID: Services.appinfo.processID,
+ isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ });
+
+ addon.child = addon.require('sdk/remote/child');
+
+ for (let contentFrame of frames.values())
+ addon.child.registerContentFrame(contentFrame);
+});
+
+// Unload a child process loader
+cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => {
+ if (!addons.has(loaderID))
+ return;
+
+ let addon = addons.get(loaderID);
+ Loader.unload(addon.loader, reason);
+
+ // We want to drop the reference to the loader but never allow creating a new
+ // loader with the same ID
+ addons.set(loaderID, {});
+})
+
+
+var frames = new Set();
+
+this.registerContentFrame = contentFrame => {
+ contentFrame.addEventListener("unload", () => {
+ unregisterContentFrame(contentFrame);
+ }, false);
+
+ frames.add(contentFrame);
+
+ for (let addon of addons.values()) {
+ if ("child" in addon)
+ addon.child.registerContentFrame(contentFrame);
+ }
+};
+
+function unregisterContentFrame(contentFrame) {
+ frames.delete(contentFrame);
+
+ for (let addon of addons.values()) {
+ if ("child" in addon)
+ addon.child.unregisterContentFrame(contentFrame);
+ }
+}
diff --git a/toolkit/jetpack/framescript/context-menu.js b/toolkit/jetpack/framescript/context-menu.js
new file mode 100644
index 000000000..3915b7cd8
--- /dev/null
+++ b/toolkit/jetpack/framescript/context-menu.js
@@ -0,0 +1,215 @@
+/* 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 { query, constant, cache } = require("sdk/lang/functional");
+const { pairs, each, map, object } = require("sdk/util/sequence");
+const { nodeToMessageManager } = require("./util");
+
+// Decorator function that takes `f` function and returns one that attempts
+// to run `f` with given arguments. In case of exception error is logged
+// and `fallback` is returned instead.
+const Try = (fn, fallback=null) => (...args) => {
+ try {
+ return fn(...args);
+ } catch(error) {
+ console.error(error);
+ return fallback;
+ }
+};
+
+// Decorator funciton that takes `f` function and returns one that returns
+// JSON cloned result of whatever `f` returns for given arguments.
+const JSONReturn = f => (...args) => JSON.parse(JSON.stringify(f(...args)));
+
+const Null = constant(null);
+
+// Table of readers mapped to field names they're going to be reading.
+const readers = Object.create(null);
+// Read function takes "contextmenu" event target `node` and returns table of
+// read field names mapped to appropriate values. Read uses above defined read
+// table to read data for all registered readers.
+const read = node =>
+ object(...map(([id, read]) => [id, read(node, id)], pairs(readers)));
+
+// Table of built-in readers, each takes a descriptor and returns a reader:
+// descriptor -> node -> JSON
+const parsers = Object.create(null)
+// Function takes a descriptor of the remotely defined reader and parsese it
+// to construct a local reader that's going to read out data from context menu
+// target.
+const parse = descriptor => {
+ const parser = parsers[descriptor.category];
+ if (!parser) {
+ console.error("Unknown reader descriptor was received", descriptor, `"${descriptor.category}"`);
+ return Null
+ }
+ return Try(parser(descriptor));
+}
+
+// TODO: Test how chrome's mediaType behaves to try and match it's behavior.
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const SVG_NS = "http://www.w3.org/2000/svg";
+
+// Firefox always creates a HTMLVideoElement when loading an ogg file
+// directly. If the media is actually audio, be smarter and provide a
+// context menu with audio operations.
+// Source: https://github.com/mozilla/gecko-dev/blob/28c2fca3753c5371643843fc2f2f205146b083b7/browser/base/content/nsContextMenu.js#L632-L637
+const isVideoLoadingAudio = node =>
+ node.readyState >= node.HAVE_METADATA &&
+ (node.videoWidth == 0 || node.videoHeight == 0)
+
+const isVideo = node =>
+ node instanceof node.ownerDocument.defaultView.HTMLVideoElement &&
+ !isVideoLoadingAudio(node);
+
+const isAudio = node => {
+ const {HTMLVideoElement, HTMLAudioElement} = node.ownerDocument.defaultView;
+ return node instanceof HTMLAudioElement ? true :
+ node instanceof HTMLVideoElement ? isVideoLoadingAudio(node) :
+ false;
+};
+
+const isImage = ({namespaceURI, localName}) =>
+ namespaceURI === HTML_NS && localName === "img" ? true :
+ namespaceURI === XUL_NS && localName === "image" ? true :
+ namespaceURI === SVG_NS && localName === "image" ? true :
+ false;
+
+parsers["reader/MediaType()"] = constant(node =>
+ isImage(node) ? "image" :
+ isAudio(node) ? "audio" :
+ isVideo(node) ? "video" :
+ null);
+
+
+const readLink = node =>
+ node.namespaceURI === HTML_NS && node.localName === "a" ? node.href :
+ readLink(node.parentNode);
+
+parsers["reader/LinkURL()"] = constant(node =>
+ node.matches("a, a *") ? readLink(node) : null);
+
+// Reader that reads out `true` if "contextmenu" `event.target` matches
+// `descriptor.selector` and `false` if it does not.
+parsers["reader/SelectorMatch()"] = ({selector}) =>
+ node => node.matches(selector);
+
+// Accessing `selectionStart` and `selectionEnd` properties on non
+// editable input nodes throw exceptions, there for we need this util
+// function to guard us against them.
+const getInputSelection = node => {
+ try {
+ if ("selectionStart" in node && "selectionEnd" in node) {
+ const {selectionStart, selectionEnd} = node;
+ return {selectionStart, selectionEnd}
+ }
+ }
+ catch(_) {}
+
+ return null;
+}
+
+// Selection reader does not really cares about descriptor so it is
+// a constant function returning selection reader. Selection reader
+// returns string of the selected text or `null` if there is no selection.
+parsers["reader/Selection()"] = constant(node => {
+ const selection = node.ownerDocument.getSelection();
+ if (!selection.isCollapsed) {
+ return selection.toString();
+ }
+ // If target node is editable (text, input, textarea, etc..) document does
+ // not really handles selections there. There for we fallback to checking
+ // `selectionStart` `selectionEnd` properties and if they are present we
+ // extract selections manually from the `node.value`.
+ else {
+ const selection = getInputSelection(node);
+ const isSelected = selection &&
+ Number.isInteger(selection.selectionStart) &&
+ Number.isInteger(selection.selectionEnd) &&
+ selection.selectionStart !== selection.selectionEnd;
+ return isSelected ? node.value.substring(selection.selectionStart,
+ selection.selectionEnd) :
+ null;
+ }
+});
+
+// Query reader just reads out properties from the node, so we just use `query`
+// utility function.
+parsers["reader/Query()"] = ({path}) => JSONReturn(query(path));
+// Attribute reader just reads attribute of the event target node.
+parsers["reader/Attribute()"] = ({name}) => node => node.getAttribute(name);
+
+// Extractor reader defines generates a reader out of serialized function, who's
+// return value is JSON cloned. Note: We do know source will evaluate to function
+// as that's what we serialized on the other end, it's also ok if generated function
+// is going to throw as registered readers are wrapped in try catch to avoid breakting
+// unrelated readers.
+parsers["reader/Extractor()"] = ({source}) =>
+ JSONReturn(new Function("return (" + source + ")")());
+
+// If the context-menu target node or any of its ancestors is one of these,
+// Firefox uses a tailored context menu, and so the page context doesn't apply.
+// There for `reader/isPage()` will read `false` in that case otherwise it's going
+// to read `true`.
+const nonPageElements = ["a", "applet", "area", "button", "canvas", "object",
+ "embed", "img", "input", "map", "video", "audio", "menu",
+ "option", "select", "textarea", "[contenteditable=true]"];
+const nonPageSelector = nonPageElements.
+ concat(nonPageElements.map(tag => `${tag} *`)).
+ join(", ");
+
+// Note: isPageContext implementation could have actually used SelectorMatch reader,
+// but old implementation was also checked for collapsed selection there for to keep
+// the behavior same we end up implementing a new reader.
+parsers["reader/isPage()"] = constant(node =>
+ node.ownerDocument.defaultView.getSelection().isCollapsed &&
+ !node.matches(nonPageSelector));
+
+// Reads `true` if node is in an iframe otherwise returns true.
+parsers["reader/isFrame()"] = constant(node =>
+ !!node.ownerDocument.defaultView.frameElement);
+
+parsers["reader/isEditable()"] = constant(node => {
+ const selection = getInputSelection(node);
+ return selection ? !node.readOnly && !node.disabled : node.isContentEditable;
+});
+
+
+// TODO: Add some reader to read out tab id.
+
+const onReadersUpdate = message => {
+ each(([id, descriptor]) => {
+ if (descriptor) {
+ readers[id] = parse(descriptor);
+ }
+ else {
+ delete readers[id];
+ }
+ }, pairs(message.data));
+};
+exports.onReadersUpdate = onReadersUpdate;
+
+
+const onContextMenu = event => {
+ if (!event.defaultPrevented) {
+ const manager = nodeToMessageManager(event.target);
+ manager.sendSyncMessage("sdk/context-menu/read", read(event.target), readers);
+ }
+};
+exports.onContextMenu = onContextMenu;
+
+
+const onContentFrame = (frame) => {
+ // Listen for contextmenu events in on this frame.
+ frame.addEventListener("contextmenu", onContextMenu);
+ // Listen to registered reader changes and update registry.
+ frame.addMessageListener("sdk/context-menu/readers", onReadersUpdate);
+
+ // Request table of readers (if this is loaded in a new process some table
+ // changes may be missed, this is way to sync up).
+ frame.sendAsyncMessage("sdk/context-menu/readers?");
+};
+exports.onContentFrame = onContentFrame;
diff --git a/toolkit/jetpack/framescript/manager.js b/toolkit/jetpack/framescript/manager.js
new file mode 100644
index 000000000..1f261e1fa
--- /dev/null
+++ b/toolkit/jetpack/framescript/manager.js
@@ -0,0 +1,26 @@
+/* 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";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const mime = "application/javascript";
+const requireURI = module.uri.replace("framescript/manager.js",
+ "toolkit/require.js");
+
+const requireLoadURI = `data:${mime},this["Components"].utils.import("${requireURI}")`
+
+// Loads module with given `id` into given `messageManager` via shared module loader. If `init`
+// string is passed, will call module export with that name and pass frame script environment
+// of the `messageManager` into it. Since module will load only once per process (which is
+// once for chrome proces & second for content process) it is useful to have an init function
+// to setup event listeners on each content frame.
+const loadModule = (messageManager, id, allowDelayed, init) => {
+ const moduleLoadURI = `${requireLoadURI}.require("${id}")`
+ const uri = init ? `${moduleLoadURI}.${init}(this)` : moduleLoadURI;
+ messageManager.loadFrameScript(uri, allowDelayed);
+};
+exports.loadModule = loadModule;
diff --git a/toolkit/jetpack/framescript/util.js b/toolkit/jetpack/framescript/util.js
new file mode 100644
index 000000000..fb6834608
--- /dev/null
+++ b/toolkit/jetpack/framescript/util.js
@@ -0,0 +1,25 @@
+/* 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";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+
+const { Ci } = require("chrome");
+
+const windowToMessageManager = window =>
+ window.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDocShell).
+ sameTypeRootTreeItem.
+ QueryInterface(Ci.nsIDocShell).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIContentFrameMessageManager);
+exports.windowToMessageManager = windowToMessageManager;
+
+const nodeToMessageManager = node =>
+ windowToMessageManager(node.ownerDocument.defaultView);
+exports.nodeToMessageManager = nodeToMessageManager;