diff options
Diffstat (limited to 'addon-sdk/source/lib/sdk/page-mod.js')
-rw-r--r-- | addon-sdk/source/lib/sdk/page-mod.js | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js new file mode 100644 index 000000000..538be2732 --- /dev/null +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -0,0 +1,190 @@ +/* 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": "stable" +}; + +const { contract: loaderContract } = require('./content/loader'); +const { contract } = require('./util/contract'); +const { WorkerHost, connect } = require('./content/utils'); +const { Class } = require('./core/heritage'); +const { Disposable } = require('./core/disposable'); +const { Worker } = require('./content/worker'); +const { EventTarget } = require('./event/target'); +const { on, emit, once, setListeners } = require('./event/core'); +const { isRegExp, isUndefined } = require('./lang/type'); +const { merge, omit } = require('./util/object'); +const { remove, has, hasAny } = require("./util/array"); +const { Rules } = require("./util/rules"); +const { processes, frames, remoteRequire } = require('./remote/parent'); +remoteRequire('sdk/content/page-mod'); + +const pagemods = new Map(); +const workers = new Map(); +const models = new WeakMap(); +var modelFor = (mod) => models.get(mod); +var workerFor = (mod) => workers.get(mod)[0]; + +// Helper functions +var isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string'; + +var PAGEMOD_ID = 0; + +// Validation Contracts +const modOptions = { + // contentStyle* / contentScript* are sharing the same validation constraints, + // so they can be mostly reused, except for the messages. + contentStyle: merge(Object.create(loaderContract.rules.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }), + include: { + is: ['string', 'array', 'regexp'], + ok: (rule) => { + if (isRegExpOrString(rule)) + return true; + if (Array.isArray(rule) && rule.length > 0) + return rule.every(isRegExpOrString); + return false; + }, + msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.' + }, + exclude: { + is: ['string', 'array', 'regexp', 'undefined'], + ok: (rule) => { + if (isRegExpOrString(rule) || isUndefined(rule)) + return true; + if (Array.isArray(rule) && rule.length > 0) + return rule.every(isRegExpOrString); + return false; + }, + msg: 'If set, the `exclude` option must always contain at least one ' + + 'rule as a string, regular expression, or an array of strings and ' + + 'regular expressions.' + }, + attachTo: { + is: ['string', 'array', 'undefined'], + map: function (attachTo) { + if (!attachTo) return ['top', 'frame']; + if (typeof attachTo === 'string') return [attachTo]; + return attachTo; + }, + ok: function (attachTo) { + return hasAny(attachTo, ['top', 'frame']) && + attachTo.every(has.bind(null, ['top', 'frame', 'existing'])); + }, + msg: 'The `attachTo` option must be a string or an array of strings. ' + + 'The only valid options are "existing", "top" and "frame", and must ' + + 'contain at least "top" or "frame" values.' + }, +}; + +const modContract = contract(merge({}, loaderContract.rules, modOptions)); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Class({ + implements: [ + modContract.properties(modelFor), + EventTarget, + Disposable, + ], + extends: WorkerHost(workerFor), + setup: function PageMod(options) { + let mod = this; + let model = modContract(options); + models.set(this, model); + model.id = PAGEMOD_ID++; + + let include = model.include; + model.include = Rules(); + model.include.add.apply(model.include, [].concat(include)); + + let exclude = isUndefined(model.exclude) ? [] : model.exclude; + model.exclude = Rules(); + model.exclude.add.apply(model.exclude, [].concat(exclude)); + + // Set listeners on {PageMod} itself, not the underlying worker, + // like `onMessage`, as it'll get piped. + setListeners(this, options); + + pagemods.set(model.id, this); + workers.set(this, []); + + function serializeRules(rules) { + for (let rule of rules) { + yield isRegExp(rule) ? { type: "regexp", pattern: rule.source, flags: rule.flags } + : { type: "string", value: rule }; + } + } + + model.childOptions = omit(model, ["include", "exclude", "contentScriptOptions"]); + model.childOptions.include = [...serializeRules(model.include)]; + model.childOptions.exclude = [...serializeRules(model.exclude)]; + model.childOptions.contentScriptOptions = model.contentScriptOptions ? + JSON.stringify(model.contentScriptOptions) : + null; + + processes.port.emit('sdk/page-mod/create', model.childOptions); + }, + + dispose: function(reason) { + processes.port.emit('sdk/page-mod/destroy', modelFor(this).id); + pagemods.delete(modelFor(this).id); + workers.delete(this); + }, + + destroy: function(reason) { + // Explicit destroy call, i.e. not via unload so destroy the workers + let list = workers.get(this); + if (!list) + return; + + // Triggers dispose which will cause the child page-mod to be destroyed + Disposable.prototype.destroy.call(this, reason); + + // Destroy any active workers + for (let worker of list) + worker.destroy(reason); + } +}); +exports.PageMod = PageMod; + +// Whenever a new process starts send over the list of page-mods +processes.forEvery(process => { + for (let mod of pagemods.values()) + process.port.emit('sdk/page-mod/create', modelFor(mod).childOptions); +}); + +frames.port.on('sdk/page-mod/worker-create', (frame, modId, workerOptions) => { + let mod = pagemods.get(modId); + if (!mod) + return; + + // Attach the parent side of the worker to the child + let worker = Worker(); + + workers.get(mod).unshift(worker); + worker.on('*', (event, ...args) => { + // page-mod's "attach" event needs to be passed a worker + if (event === 'attach') + emit(mod, event, worker) + else + emit(mod, event, ...args); + }); + + worker.on('detach', () => { + let array = workers.get(mod); + if (array) + remove(array, worker); + }); + + connect(worker, frame, workerOptions); +}); |