content-worker.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. const ContentWorker = Object.freeze({
  5. // TODO: Bug 727854 Use same implementation than common JS modules,
  6. // i.e. EventEmitter module
  7. /**
  8. * Create an EventEmitter instance.
  9. */
  10. createEventEmitter: function createEventEmitter(emit) {
  11. let listeners = Object.create(null);
  12. let eventEmitter = Object.freeze({
  13. emit: emit,
  14. on: function on(name, callback) {
  15. if (typeof callback !== "function")
  16. return this;
  17. if (!(name in listeners))
  18. listeners[name] = [];
  19. listeners[name].push(callback);
  20. return this;
  21. },
  22. once: function once(name, callback) {
  23. eventEmitter.on(name, function onceCallback() {
  24. eventEmitter.removeListener(name, onceCallback);
  25. callback.apply(callback, arguments);
  26. });
  27. },
  28. removeListener: function removeListener(name, callback) {
  29. if (!(name in listeners))
  30. return;
  31. let index = listeners[name].indexOf(callback);
  32. if (index == -1)
  33. return;
  34. listeners[name].splice(index, 1);
  35. }
  36. });
  37. function onEvent(name) {
  38. if (!(name in listeners))
  39. return [];
  40. let args = Array.slice(arguments, 1);
  41. let results = [];
  42. for each (let callback in listeners[name]) {
  43. results.push(callback.apply(null, args));
  44. }
  45. return results;
  46. }
  47. function hasListenerFor(name) {
  48. if (!(name in listeners))
  49. return false;
  50. return listeners[name].length > 0;
  51. }
  52. return {
  53. eventEmitter: eventEmitter,
  54. emit: onEvent,
  55. hasListenerFor: hasListenerFor
  56. };
  57. },
  58. /**
  59. * Create an EventEmitter instance to communicate with chrome module
  60. * by passing only strings between compartments.
  61. * This function expects `emitToChrome` function, that allows to send
  62. * events to the chrome module. It returns the EventEmitter as `pipe`
  63. * attribute, and, `onChromeEvent` a function that allows chrome module
  64. * to send event into the EventEmitter.
  65. *
  66. * pipe.emit --> emitToChrome
  67. * onChromeEvent --> callback registered through pipe.on
  68. */
  69. createPipe: function createPipe(emitToChrome) {
  70. function onEvent() {
  71. // Convert to real array
  72. let args = Array.slice(arguments);
  73. // JSON.stringify is buggy with cross-sandbox values,
  74. // it may return "{}" on functions. Use a replacer to match them correctly.
  75. function replacer(k, v) {
  76. return typeof v === "function" ? undefined : v;
  77. }
  78. let str = JSON.stringify(args, replacer);
  79. emitToChrome(str);
  80. }
  81. let { eventEmitter, emit, hasListenerFor } =
  82. ContentWorker.createEventEmitter(onEvent);
  83. return {
  84. pipe: eventEmitter,
  85. onChromeEvent: function onChromeEvent(array) {
  86. // We either receive a stringified array, or a real array.
  87. // We still allow to pass an array of objects, in WorkerSandbox.emitSync
  88. // in order to allow sending DOM node reference between content script
  89. // and modules (only used for context-menu API)
  90. let args = typeof array == "string" ? JSON.parse(array) : array;
  91. return emit.apply(null, args);
  92. },
  93. hasListenerFor: hasListenerFor
  94. };
  95. },
  96. injectConsole: function injectConsole(exports, pipe) {
  97. exports.console = Object.freeze({
  98. log: pipe.emit.bind(null, "console", "log"),
  99. info: pipe.emit.bind(null, "console", "info"),
  100. warn: pipe.emit.bind(null, "console", "warn"),
  101. error: pipe.emit.bind(null, "console", "error"),
  102. debug: pipe.emit.bind(null, "console", "debug"),
  103. exception: pipe.emit.bind(null, "console", "exception"),
  104. trace: pipe.emit.bind(null, "console", "trace")
  105. });
  106. },
  107. injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
  108. // wrapped functions from `'timer'` module.
  109. // Wrapper adds `try catch` blocks to the callbacks in order to
  110. // emit `error` event on a symbiont if exception is thrown in
  111. // the Worker global scope.
  112. // @see http://www.w3.org/TR/workers/#workerutils
  113. // List of all living timeouts/intervals
  114. let _timers = Object.create(null);
  115. // Keep a reference to original timeout functions
  116. let {
  117. setTimeout: chromeSetTimeout,
  118. setInterval: chromeSetInterval,
  119. clearTimeout: chromeClearTimeout,
  120. clearInterval: chromeClearInterval
  121. } = chromeAPI.timers;
  122. function registerTimer(timer) {
  123. let registerMethod = null;
  124. if (timer.kind == "timeout")
  125. registerMethod = chromeSetTimeout;
  126. else if (timer.kind == "interval")
  127. registerMethod = chromeSetInterval;
  128. else
  129. throw new Error("Unknown timer kind: " + timer.kind);
  130. if (typeof timer.fun == 'string') {
  131. let code = timer.fun;
  132. timer.fun = () => chromeAPI.sandbox.evaluate(exports, code);
  133. } else if (typeof timer.fun != 'function') {
  134. throw new Error('Unsupported callback type' + typeof timer.fun);
  135. }
  136. let id = registerMethod(onFire, timer.delay);
  137. function onFire() {
  138. try {
  139. if (timer.kind == "timeout")
  140. delete _timers[id];
  141. timer.fun.apply(null, timer.args);
  142. } catch(e) {
  143. console.exception(e);
  144. let wrapper = {
  145. instanceOfError: instanceOf(e, Error),
  146. value: e,
  147. };
  148. if (wrapper.instanceOfError) {
  149. wrapper.value = {
  150. message: e.message,
  151. fileName: e.fileName,
  152. lineNumber: e.lineNumber,
  153. stack: e.stack,
  154. name: e.name,
  155. };
  156. }
  157. pipe.emit('error', wrapper);
  158. }
  159. }
  160. _timers[id] = timer;
  161. return id;
  162. }
  163. // copied from sdk/lang/type.js since modules are not available here
  164. function instanceOf(value, Type) {
  165. var isConstructorNameSame;
  166. var isConstructorSourceSame;
  167. // If `instanceof` returned `true` we know result right away.
  168. var isInstanceOf = value instanceof Type;
  169. // If `instanceof` returned `false` we do ducktype check since `Type` may be
  170. // from a different sandbox. If a constructor of the `value` or a constructor
  171. // of the value's prototype has same name and source we assume that it's an
  172. // instance of the Type.
  173. if (!isInstanceOf && value) {
  174. isConstructorNameSame = value.constructor.name === Type.name;
  175. isConstructorSourceSame = String(value.constructor) == String(Type);
  176. isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
  177. instanceOf(Object.getPrototypeOf(value), Type);
  178. }
  179. return isInstanceOf;
  180. }
  181. function unregisterTimer(id) {
  182. if (!(id in _timers))
  183. return;
  184. let { kind } = _timers[id];
  185. delete _timers[id];
  186. if (kind == "timeout")
  187. chromeClearTimeout(id);
  188. else if (kind == "interval")
  189. chromeClearInterval(id);
  190. else
  191. throw new Error("Unknown timer kind: " + kind);
  192. }
  193. function disableAllTimers() {
  194. Object.keys(_timers).forEach(unregisterTimer);
  195. }
  196. exports.setTimeout = function ContentScriptSetTimeout(callback, delay) {
  197. return registerTimer({
  198. kind: "timeout",
  199. fun: callback,
  200. delay: delay,
  201. args: Array.slice(arguments, 2)
  202. });
  203. };
  204. exports.clearTimeout = function ContentScriptClearTimeout(id) {
  205. unregisterTimer(id);
  206. };
  207. exports.setInterval = function ContentScriptSetInterval(callback, delay) {
  208. return registerTimer({
  209. kind: "interval",
  210. fun: callback,
  211. delay: delay,
  212. args: Array.slice(arguments, 2)
  213. });
  214. };
  215. exports.clearInterval = function ContentScriptClearInterval(id) {
  216. unregisterTimer(id);
  217. };
  218. // On page-hide, save a list of all existing timers before disabling them,
  219. // in order to be able to restore them on page-show.
  220. // These events are fired when the page goes in/out of bfcache.
  221. // https://developer.mozilla.org/En/Working_with_BFCache
  222. let frozenTimers = [];
  223. pipe.on("pageshow", function onPageShow() {
  224. frozenTimers.forEach(registerTimer);
  225. });
  226. pipe.on("pagehide", function onPageHide() {
  227. frozenTimers = [];
  228. for (let id in _timers)
  229. frozenTimers.push(_timers[id]);
  230. disableAllTimers();
  231. // Some other pagehide listeners may register some timers that won't be
  232. // frozen as this particular pagehide listener is called first.
  233. // So freeze these timers on next cycle.
  234. chromeSetTimeout(function () {
  235. for (let id in _timers)
  236. frozenTimers.push(_timers[id]);
  237. disableAllTimers();
  238. }, 0);
  239. });
  240. // Unregister all timers when the page is destroyed
  241. // (i.e. when it is removed from bfcache)
  242. pipe.on("detach", function clearTimeouts() {
  243. disableAllTimers();
  244. _timers = {};
  245. frozenTimers = [];
  246. });
  247. },
  248. injectMessageAPI: function injectMessageAPI(exports, pipe, console) {
  249. let { eventEmitter: port, emit : portEmit } =
  250. ContentWorker.createEventEmitter(pipe.emit.bind(null, "event"));
  251. pipe.on("event", portEmit);
  252. let self = {
  253. port: port,
  254. postMessage: pipe.emit.bind(null, "message"),
  255. on: pipe.on.bind(null),
  256. once: pipe.once.bind(null),
  257. removeListener: pipe.removeListener.bind(null),
  258. };
  259. Object.defineProperty(exports, "self", {
  260. value: self
  261. });
  262. // Deprecated use of on/postMessage from globals
  263. exports.postMessage = function deprecatedPostMessage() {
  264. console.error("DEPRECATED: The global `postMessage()` function in " +
  265. "content scripts is deprecated in favor of the " +
  266. "`self.postMessage()` function, which works the same. " +
  267. "Replace calls to `postMessage()` with calls to " +
  268. "`self.postMessage()`." +
  269. "For more info on `self.on`, see " +
  270. "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
  271. return self.postMessage.apply(null, arguments);
  272. };
  273. exports.on = function deprecatedOn() {
  274. console.error("DEPRECATED: The global `on()` function in content " +
  275. "scripts is deprecated in favor of the `self.on()` " +
  276. "function, which works the same. Replace calls to `on()` " +
  277. "with calls to `self.on()`" +
  278. "For more info on `self.on`, see " +
  279. "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
  280. return self.on.apply(null, arguments);
  281. };
  282. // Deprecated use of `onMessage` from globals
  283. let onMessage = null;
  284. Object.defineProperty(exports, "onMessage", {
  285. get: function () onMessage,
  286. set: function (v) {
  287. if (onMessage)
  288. self.removeListener("message", onMessage);
  289. console.error("DEPRECATED: The global `onMessage` function in content" +
  290. "scripts is deprecated in favor of the `self.on()` " +
  291. "function. Replace `onMessage = function (data){}` " +
  292. "definitions with calls to `self.on('message', " +
  293. "function (data){})`. " +
  294. "For more info on `self.on`, see " +
  295. "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
  296. onMessage = v;
  297. if (typeof onMessage == "function")
  298. self.on("message", onMessage);
  299. }
  300. });
  301. },
  302. injectOptions: function (exports, options) {
  303. Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) });
  304. },
  305. inject: function (exports, chromeAPI, emitToChrome, options) {
  306. let { pipe, onChromeEvent, hasListenerFor } =
  307. ContentWorker.createPipe(emitToChrome);
  308. ContentWorker.injectConsole(exports, pipe);
  309. ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console);
  310. ContentWorker.injectMessageAPI(exports, pipe, exports.console);
  311. if ( options !== undefined ) {
  312. ContentWorker.injectOptions(exports, options);
  313. }
  314. Object.freeze( exports.self );
  315. return {
  316. emitToContent: onChromeEvent,
  317. hasListenerFor: hasListenerFor
  318. };
  319. }
  320. });