page-mod.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "stable"
  7. };
  8. const observers = require('./system/events');
  9. const { Loader, validationAttributes } = require('./content/loader');
  10. const { Worker } = require('./content/worker');
  11. const { Registry } = require('./util/registry');
  12. const { EventEmitter } = require('./deprecated/events');
  13. const { on, emit } = require('./event/core');
  14. const { validateOptions : validate } = require('./deprecated/api-utils');
  15. const { Cc, Ci } = require('chrome');
  16. const { merge } = require('./util/object');
  17. const { readURISync } = require('./net/url');
  18. const { windowIterator } = require('./deprecated/window-utils');
  19. const { isBrowser, getFrames } = require('./window/utils');
  20. const { getTabs, getTabContentWindow, getTabForContentWindow,
  21. getURI: getTabURI } = require('./tabs/utils');
  22. const { ignoreWindow } = require('sdk/private-browsing/utils');
  23. const { Style } = require("./stylesheet/style");
  24. const { attach, detach } = require("./content/mod");
  25. const { has, hasAny } = require("./util/array");
  26. const { Rules } = require("./util/rules");
  27. // Valid values for `attachTo` option
  28. const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
  29. const mods = new WeakMap();
  30. // contentStyle* / contentScript* are sharing the same validation constraints,
  31. // so they can be mostly reused, except for the messages.
  32. const validStyleOptions = {
  33. contentStyle: merge(Object.create(validationAttributes.contentScript), {
  34. msg: 'The `contentStyle` option must be a string or an array of strings.'
  35. }),
  36. contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), {
  37. msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
  38. })
  39. };
  40. /**
  41. * PageMod constructor (exported below).
  42. * @constructor
  43. */
  44. const PageMod = Loader.compose(EventEmitter, {
  45. on: EventEmitter.required,
  46. _listeners: EventEmitter.required,
  47. attachTo: [],
  48. contentScript: Loader.required,
  49. contentScriptFile: Loader.required,
  50. contentScriptWhen: Loader.required,
  51. contentScriptOptions: Loader.required,
  52. include: null,
  53. constructor: function PageMod(options) {
  54. this._onContent = this._onContent.bind(this);
  55. options = options || {};
  56. let { contentStyle, contentStyleFile } = validate(options, validStyleOptions);
  57. if ('contentScript' in options)
  58. this.contentScript = options.contentScript;
  59. if ('contentScriptFile' in options)
  60. this.contentScriptFile = options.contentScriptFile;
  61. if ('contentScriptOptions' in options)
  62. this.contentScriptOptions = options.contentScriptOptions;
  63. if ('contentScriptWhen' in options)
  64. this.contentScriptWhen = options.contentScriptWhen;
  65. if ('onAttach' in options)
  66. this.on('attach', options.onAttach);
  67. if ('onError' in options)
  68. this.on('error', options.onError);
  69. if ('attachTo' in options) {
  70. if (typeof options.attachTo == 'string')
  71. this.attachTo = [options.attachTo];
  72. else if (Array.isArray(options.attachTo))
  73. this.attachTo = options.attachTo;
  74. else
  75. throw new Error('The `attachTo` option must be a string or an array ' +
  76. 'of strings.');
  77. let isValidAttachToItem = function isValidAttachToItem(item) {
  78. return typeof item === 'string' &&
  79. VALID_ATTACHTO_OPTIONS.indexOf(item) !== -1;
  80. }
  81. if (!this.attachTo.every(isValidAttachToItem))
  82. throw new Error('The `attachTo` option valid accept only following ' +
  83. 'values: '+ VALID_ATTACHTO_OPTIONS.join(', '));
  84. if (!hasAny(this.attachTo, ["top", "frame"]))
  85. throw new Error('The `attachTo` option must always contain at least' +
  86. ' `top` or `frame` value');
  87. }
  88. else {
  89. this.attachTo = ["top", "frame"];
  90. }
  91. let include = options.include;
  92. let rules = this.include = Rules();
  93. if (!include)
  94. throw new Error('The `include` option must always contain atleast one rule');
  95. rules.add.apply(rules, [].concat(include));
  96. if (contentStyle || contentStyleFile) {
  97. this._style = Style({
  98. uri: contentStyleFile,
  99. source: contentStyle
  100. });
  101. }
  102. this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this));
  103. pageModManager.add(this._public);
  104. mods.set(this._public, this);
  105. // `_applyOnExistingDocuments` has to be called after `pageModManager.add()`
  106. // otherwise its calls to `_onContent` method won't do anything.
  107. if ('attachTo' in options && has(options.attachTo, 'existing'))
  108. this._applyOnExistingDocuments();
  109. },
  110. destroy: function destroy() {
  111. if (this._style)
  112. detach(this._style);
  113. for (let i in this.include)
  114. this.include.remove(this.include[i]);
  115. mods.delete(this._public);
  116. pageModManager.remove(this._public);
  117. },
  118. _applyOnExistingDocuments: function _applyOnExistingDocuments() {
  119. let mod = this;
  120. let tabs = getAllTabs();
  121. tabs.forEach(function (tab) {
  122. // Fake a newly created document
  123. let window = getTabContentWindow(tab);
  124. if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab)))
  125. mod._onContent(window);
  126. if (has(mod.attachTo, "frame")) {
  127. getFrames(window).
  128. filter((iframe) => mod.include.matchesAny(iframe.location.href)).
  129. forEach(mod._onContent);
  130. }
  131. });
  132. },
  133. _onContent: function _onContent(window) {
  134. // not registered yet
  135. if (!pageModManager.has(this))
  136. return;
  137. let isTopDocument = window.top === window;
  138. // Is a top level document and `top` is not set, ignore
  139. if (isTopDocument && !has(this.attachTo, "top"))
  140. return;
  141. // Is a frame document and `frame` is not set, ignore
  142. if (!isTopDocument && !has(this.attachTo, "frame"))
  143. return;
  144. if (this._style)
  145. attach(this._style, window);
  146. // Immediatly evaluate content script if the document state is already
  147. // matching contentScriptWhen expectations
  148. let state = window.document.readyState;
  149. if ('start' === this.contentScriptWhen ||
  150. // Is `load` event already dispatched?
  151. 'complete' === state ||
  152. // Is DOMContentLoaded already dispatched and waiting for it?
  153. ('ready' === this.contentScriptWhen && state === 'interactive') ) {
  154. this._createWorker(window);
  155. return;
  156. }
  157. let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
  158. let self = this;
  159. window.addEventListener(eventName, function onReady(event) {
  160. if (event.target.defaultView != window)
  161. return;
  162. window.removeEventListener(eventName, onReady, true);
  163. self._createWorker(window);
  164. }, true);
  165. },
  166. _createWorker: function _createWorker(window) {
  167. let worker = Worker({
  168. window: window,
  169. contentScript: this.contentScript,
  170. contentScriptFile: this.contentScriptFile,
  171. contentScriptOptions: this.contentScriptOptions,
  172. onError: this._onUncaughtError
  173. });
  174. this._emit('attach', worker);
  175. let self = this;
  176. worker.once('detach', function detach() {
  177. worker.destroy();
  178. });
  179. },
  180. _onUncaughtError: function _onUncaughtError(e) {
  181. if (this._listeners('error').length == 1)
  182. console.exception(e);
  183. }
  184. });
  185. exports.PageMod = function(options) PageMod(options)
  186. exports.PageMod.prototype = PageMod.prototype;
  187. const PageModManager = Registry.resolve({
  188. constructor: '_init',
  189. _destructor: '_registryDestructor'
  190. }).compose({
  191. constructor: function PageModRegistry(constructor) {
  192. this._init(PageMod);
  193. observers.on(
  194. 'document-element-inserted',
  195. this._onContentWindow = this._onContentWindow.bind(this)
  196. );
  197. },
  198. _destructor: function _destructor() {
  199. observers.off('document-element-inserted', this._onContentWindow);
  200. this._removeAllListeners();
  201. // We need to do some cleaning er PageMods, like unregistering any
  202. // `contentStyle*`
  203. this._registry.forEach(function(pageMod) {
  204. pageMod.destroy();
  205. });
  206. this._registryDestructor();
  207. },
  208. _onContentWindow: function _onContentWindow({ subject: document }) {
  209. let window = document.defaultView;
  210. // XML documents don't have windows, and we don't yet support them.
  211. if (!window)
  212. return;
  213. // We apply only on documents in tabs of Firefox
  214. if (!getTabForContentWindow(window))
  215. return;
  216. // When the tab is private, only addons with 'private-browsing' flag in
  217. // their package.json can apply content script to private documents
  218. if (ignoreWindow(window)) {
  219. return;
  220. }
  221. this._registry.forEach(function(mod) {
  222. if (mod.include.matchesAny(document.URL))
  223. mods.get(mod)._onContent(window);
  224. });
  225. },
  226. off: function off(topic, listener) {
  227. this.removeListener(topic, listener);
  228. }
  229. });
  230. const pageModManager = PageModManager();
  231. // Returns all tabs on all currently opened windows
  232. function getAllTabs() {
  233. let tabs = [];
  234. // Iterate over all chrome windows
  235. for (let window in windowIterator()) {
  236. if (!isBrowser(window))
  237. continue;
  238. tabs = tabs.concat(getTabs(window));
  239. }
  240. return tabs;
  241. }