diff options
Diffstat (limited to 'tools/addon-sdk-1.12/lib/sdk/page-mod.js')
-rw-r--r-- | tools/addon-sdk-1.12/lib/sdk/page-mod.js | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.12/lib/sdk/page-mod.js b/tools/addon-sdk-1.12/lib/sdk/page-mod.js new file mode 100644 index 0000000..8bd4e5f --- /dev/null +++ b/tools/addon-sdk-1.12/lib/sdk/page-mod.js @@ -0,0 +1,380 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 observers = require('./deprecated/observer-service'); +const { Loader, validationAttributes } = require('./content/loader'); +const { Worker } = require('./content/worker'); +const { EventEmitter } = require('./deprecated/events'); +const { List } = require('./deprecated/list'); +const { Registry } = require('./util/registry'); +const { MatchPattern } = require('./page-mod/match-pattern'); +const { validateOptions : validate } = require('./deprecated/api-utils'); +const { Cc, Ci } = require('chrome'); +const { merge } = require('./util/object'); +const { readURISync } = require('./net/url'); +const { windowIterator } = require('./deprecated/window-utils'); +const { isBrowser, getFrames } = require('./window/utils'); +const { getTabs, getTabContentWindow, getTabForContentWindow, + getURI: getTabURI } = require('./tabs/utils'); +const { has, hasAny } = require('./util/array'); + +const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]. + getService(Ci.nsIStyleSheetService); + +const USER_SHEET = styleSheetService.USER_SHEET; + +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + +// Valid values for `attachTo` option +const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; + +// contentStyle* / contentScript* are sharing the same validation constraints, +// so they can be mostly reused, except for the messages. +const validStyleOptions = { + contentStyle: merge(Object.create(validationAttributes.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }) +}; + +// rules registry +const RULES = {}; + +const Rules = EventEmitter.resolve({ toString: null }).compose(List, { + add: function() Array.slice(arguments).forEach(function onAdd(rule) { + if (this._has(rule)) + return; + // registering rule to the rules registry + if (!(rule in RULES)) + RULES[rule] = new MatchPattern(rule); + this._add(rule); + this._emit('add', rule); + }.bind(this)), + remove: function() Array.slice(arguments).forEach(function onRemove(rule) { + if (!this._has(rule)) + return; + this._remove(rule); + this._emit('remove', rule); + }.bind(this)), +}); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Loader.compose(EventEmitter, { + on: EventEmitter.required, + _listeners: EventEmitter.required, + attachTo: [], + contentScript: Loader.required, + contentScriptFile: Loader.required, + contentScriptWhen: Loader.required, + contentScriptOptions: Loader.required, + include: null, + constructor: function PageMod(options) { + this._onContent = this._onContent.bind(this); + options = options || {}; + + let { contentStyle, contentStyleFile } = validate(options, validStyleOptions); + + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('onAttach' in options) + this.on('attach', options.onAttach); + if ('onError' in options) + this.on('error', options.onError); + if ('attachTo' in options) { + if (typeof options.attachTo == 'string') + this.attachTo = [options.attachTo]; + else if (Array.isArray(options.attachTo)) + this.attachTo = options.attachTo; + else + throw new Error('The `attachTo` option must be a string or an array ' + + 'of strings.'); + + let isValidAttachToItem = function isValidAttachToItem(item) { + return typeof item === 'string' && + VALID_ATTACHTO_OPTIONS.indexOf(item) !== -1; + } + if (!this.attachTo.every(isValidAttachToItem)) + throw new Error('The `attachTo` option valid accept only following ' + + 'values: '+ VALID_ATTACHTO_OPTIONS.join(', ')); + if (!hasAny(this.attachTo, ["top", "frame"])) + throw new Error('The `attachTo` option must always contain at least' + + ' `top` or `frame` value'); + } + else { + this.attachTo = ["top", "frame"]; + } + + let include = options.include; + let rules = this.include = Rules(); + rules.on('add', this._onRuleAdd = this._onRuleAdd.bind(this)); + rules.on('remove', this._onRuleRemove = this._onRuleRemove.bind(this)); + + if (Array.isArray(include)) + rules.add.apply(null, include); + else + rules.add(include); + + let styleRules = ""; + + if (contentStyleFile) + styleRules = [].concat(contentStyleFile).map(readURISync).join(""); + + if (contentStyle) + styleRules += [].concat(contentStyle).join(""); + + if (styleRules) { + this._onRuleUpdate = this._onRuleUpdate.bind(this); + + this._styleRules = styleRules; + + this._registerStyleSheet(); + rules.on('add', this._onRuleUpdate); + rules.on('remove', this._onRuleUpdate); + } + + this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this)); + pageModManager.add(this._public); + + this._loadingWindows = []; + + // `_applyOnExistingDocuments` has to be called after `pageModManager.add()` + // otherwise its calls to `_onContent` method won't do anything. + if ('attachTo' in options && has(options.attachTo, 'existing')) + this._applyOnExistingDocuments(); + }, + + destroy: function destroy() { + + this._unregisterStyleSheet(); + + this.include.removeListener('add', this._onRuleUpdate); + this.include.removeListener('remove', this._onRuleUpdate); + + for each (let rule in this.include) + this.include.remove(rule); + pageModManager.remove(this._public); + this._loadingWindows = []; + + }, + + _loadingWindows: [], + + _applyOnExistingDocuments: function _applyOnExistingDocuments() { + let mod = this; + // Returns true if the tab match one rule + function isMatchingURI(uri) { + // Use Array.some as `include` isn't a native array + return Array.some(mod.include, function (rule) { + return RULES[rule].test(uri); + }); + } + let tabs = getAllTabs().filter(function (tab) { + return isMatchingURI(getTabURI(tab)); + }); + + tabs.forEach(function (tab) { + // Fake a newly created document + let window = getTabContentWindow(tab); + if (has(mod.attachTo, "top")) + mod._onContent(window); + if (has(mod.attachTo, "frame")) + getFrames(window).forEach(mod._onContent); + }); + }, + + _onContent: function _onContent(window) { + // not registered yet + if (!pageModManager.has(this)) + return; + + let isTopDocument = window.top === window; + // Is a top level document and `top` is not set, ignore + if (isTopDocument && !has(this.attachTo, "top")) + return; + // Is a frame document and `frame` is not set, ignore + if (!isTopDocument && !has(this.attachTo, "frame")) + return; + + // Immediatly evaluate content script if the document state is already + // matching contentScriptWhen expectations + let state = window.document.readyState; + if ('start' === this.contentScriptWhen || + // Is `load` event already dispatched? + 'complete' === state || + // Is DOMContentLoaded already dispatched and waiting for it? + ('ready' === this.contentScriptWhen && state === 'interactive') ) { + this._createWorker(window); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + window.addEventListener(eventName, function onReady(event) { + if (event.target.defaultView != window) + return; + window.removeEventListener(eventName, onReady, true); + + self._createWorker(window); + }, true); + }, + _createWorker: function _createWorker(window) { + let worker = Worker({ + window: window, + contentScript: this.contentScript, + contentScriptFile: this.contentScriptFile, + contentScriptOptions: this.contentScriptOptions, + onError: this._onUncaughtError + }); + this._emit('attach', worker); + let self = this; + worker.once('detach', function detach() { + worker.destroy(); + }); + }, + _onRuleAdd: function _onRuleAdd(url) { + pageModManager.on(url, this._onContent); + }, + _onRuleRemove: function _onRuleRemove(url) { + pageModManager.off(url, this._onContent); + }, + _onUncaughtError: function _onUncaughtError(e) { + if (this._listeners('error').length == 1) + console.exception(e); + }, + _onRuleUpdate: function _onRuleUpdate(){ + this._registerStyleSheet(); + }, + + _registerStyleSheet : function _registerStyleSheet() { + let rules = this.include; + let styleRules = this._styleRules; + + let documentRules = []; + + this._unregisterStyleSheet(); + + for each (let rule in rules) { + let pattern = RULES[rule]; + + if (!pattern) + continue; + + if (pattern.regexp) + documentRules.push("regexp(\"" + pattern.regexp.source + "\")"); + else if (pattern.exactURL) + documentRules.push("url(" + pattern.exactURL + ")"); + else if (pattern.domain) + documentRules.push("domain(" + pattern.domain + ")"); + else if (pattern.urlPrefix) + documentRules.push("url-prefix(" + pattern.urlPrefix + ")"); + else if (pattern.anyWebPage) { + documentRules.push("regexp(\"^(https?|ftp)://.*?\")"); + break; + } + } + + let uri = "data:text/css;charset=utf-8,"; + if (documentRules.length > 0) + uri += encodeURIComponent("@-moz-document " + + documentRules.join(",") + " {" + styleRules + "}"); + else + uri += encodeURIComponent(styleRules); + + this._registeredStyleURI = io.newURI(uri, null, null); + + styleSheetService.loadAndRegisterSheet( + this._registeredStyleURI, + USER_SHEET + ); + }, + + _unregisterStyleSheet : function () { + let uri = this._registeredStyleURI; + + if (uri && styleSheetService.sheetRegistered(uri, USER_SHEET)) + styleSheetService.unregisterSheet(uri, USER_SHEET); + + this._registeredStyleURI = null; + } +}); +exports.PageMod = function(options) PageMod(options) +exports.PageMod.prototype = PageMod.prototype; + +const PageModManager = Registry.resolve({ + constructor: '_init', + _destructor: '_registryDestructor' +}).compose({ + constructor: function PageModRegistry(constructor) { + this._init(PageMod); + observers.add( + 'document-element-inserted', + this._onContentWindow = this._onContentWindow.bind(this) + ); + }, + _destructor: function _destructor() { + observers.remove('document-element-inserted', this._onContentWindow); + this._removeAllListeners(); + for (let rule in RULES) { + delete RULES[rule]; + } + + // We need to do some cleaning er PageMods, like unregistering any + // `contentStyle*` + this._registry.forEach(function(pageMod) { + pageMod.destroy(); + }); + + this._registryDestructor(); + }, + _onContentWindow: function _onContentWindow(document) { + let window = document.defaultView; + // XML documents don't have windows, and we don't yet support them. + if (!window) + return; + // We apply only on documents in tabs of Firefox + if (!getTabForContentWindow(window)) + return; + + for (let rule in RULES) + if (RULES[rule].test(document.URL)) + this._emit(rule, window); + }, + off: function off(topic, listener) { + this.removeListener(topic, listener); + if (!this._listeners(topic).length) + delete RULES[topic]; + } +}); +const pageModManager = PageModManager(); + +// Returns all tabs on all currently opened windows +function getAllTabs() { + let tabs = []; + // Iterate over all chrome windows + for (let window in windowIterator()) { + if (!isBrowser(window)) + continue; + tabs = tabs.concat(getTabs(window)); + } + return tabs; +} |