traits-worker.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  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. /**
  5. *
  6. * `deprecated/traits-worker` was previously `content/worker` and kept
  7. * only due to `deprecated/symbiont` using it, which is necessary for
  8. * `widget`, until that reaches deprecation EOL.
  9. *
  10. */
  11. "use strict";
  12. module.metadata = {
  13. "stability": "deprecated"
  14. };
  15. const { Trait } = require('./traits');
  16. const { EventEmitter, EventEmitterTrait } = require('./events');
  17. const { Ci, Cu, Cc } = require('chrome');
  18. const timer = require('../timers');
  19. const { URL } = require('../url');
  20. const unload = require('../system/unload');
  21. const observers = require('../system/events');
  22. const { Cortex } = require('./cortex');
  23. const { sandbox, evaluate, load } = require("../loader/sandbox");
  24. const { merge } = require('../util/object');
  25. const xulApp = require("../system/xul-app");
  26. const { getInnerId } = require("../window/utils")
  27. const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
  28. "17.0a2", "*");
  29. const { getTabForWindow } = require('../tabs/helpers');
  30. const { getTabForContentWindow } = require('../tabs/utils');
  31. /* Trick the linker in order to ensure shipping these files in the XPI.
  32. require('../content/content-worker.js');
  33. Then, retrieve URL of these files in the XPI:
  34. */
  35. let prefix = module.uri.split('deprecated/traits-worker.js')[0];
  36. const CONTENT_WORKER_URL = prefix + 'content/content-worker.js';
  37. // Fetch additional list of domains to authorize access to for each content
  38. // script. It is stored in manifest `metadata` field which contains
  39. // package.json data. This list is originaly defined by authors in
  40. // `permissions` attribute of their package.json addon file.
  41. const permissions = require('@loader/options').metadata['permissions'] || {};
  42. const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
  43. const JS_VERSION = '1.8';
  44. const ERR_DESTROYED =
  45. "Couldn't find the worker to receive this message. " +
  46. "The script may not be initialized yet, or may already have been unloaded.";
  47. const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
  48. "until it is visible again.";
  49. const WorkerSandbox = EventEmitter.compose({
  50. /**
  51. * Emit a message to the worker content sandbox
  52. */
  53. emit: function emit() {
  54. // First ensure having a regular array
  55. // (otherwise, `arguments` would be mapped to an object by `stringify`)
  56. let array = Array.slice(arguments);
  57. // JSON.stringify is buggy with cross-sandbox values,
  58. // it may return "{}" on functions. Use a replacer to match them correctly.
  59. function replacer(k, v) {
  60. return typeof v === "function" ? undefined : v;
  61. }
  62. // Ensure having an asynchronous behavior
  63. let self = this;
  64. timer.setTimeout(function () {
  65. self._emitToContent(JSON.stringify(array, replacer));
  66. }, 0);
  67. },
  68. /**
  69. * Synchronous version of `emit`.
  70. * /!\ Should only be used when it is strictly mandatory /!\
  71. * Doesn't ensure passing only JSON values.
  72. * Mainly used by context-menu in order to avoid breaking it.
  73. */
  74. emitSync: function emitSync() {
  75. let args = Array.slice(arguments);
  76. return this._emitToContent(args);
  77. },
  78. /**
  79. * Tells if content script has at least one listener registered for one event,
  80. * through `self.on('xxx', ...)`.
  81. * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
  82. */
  83. hasListenerFor: function hasListenerFor(name) {
  84. return this._hasListenerFor(name);
  85. },
  86. /**
  87. * Method called by the worker sandbox when it needs to send a message
  88. */
  89. _onContentEvent: function onContentEvent(args) {
  90. // As `emit`, we ensure having an asynchronous behavior
  91. let self = this;
  92. timer.setTimeout(function () {
  93. // We emit event to chrome/addon listeners
  94. self._emit.apply(self, JSON.parse(args));
  95. }, 0);
  96. },
  97. /**
  98. * Configures sandbox and loads content scripts into it.
  99. * @param {Worker} worker
  100. * content worker
  101. */
  102. constructor: function WorkerSandbox(worker) {
  103. this._addonWorker = worker;
  104. // Ensure that `emit` has always the right `this`
  105. this.emit = this.emit.bind(this);
  106. this.emitSync = this.emitSync.bind(this);
  107. // We receive a wrapped window, that may be an xraywrapper if it's content
  108. let window = worker._window;
  109. let proto = window;
  110. // Eventually use expanded principal sandbox feature, if some are given.
  111. //
  112. // But prevent it when the Worker isn't used for a content script but for
  113. // injecting `addon` object into a Panel, Widget, ... scope.
  114. // That's because:
  115. // 1/ It is useless to use multiple domains as the worker is only used
  116. // to communicate with the addon,
  117. // 2/ By using it it would prevent the document to have access to any JS
  118. // value of the worker. As JS values coming from multiple domain principals
  119. // can't be accessed by "mono-principals" (principal with only one domain).
  120. // Even if this principal is for a domain that is specified in the multiple
  121. // domain principal.
  122. let principals = window;
  123. let wantGlobalProperties = []
  124. if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
  125. principals = EXPANDED_PRINCIPALS.concat(window);
  126. // We have to replace XHR constructor of the content document
  127. // with a custom cross origin one, automagically added by platform code:
  128. delete proto.XMLHttpRequest;
  129. wantGlobalProperties.push("XMLHttpRequest");
  130. }
  131. // Instantiate trusted code in another Sandbox in order to prevent content
  132. // script from messing with standard classes used by proxy and API code.
  133. let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
  134. apiSandbox.console = console;
  135. // Create the sandbox and bind it to window in order for content scripts to
  136. // have access to all standard globals (window, document, ...)
  137. let content = this._sandbox = sandbox(principals, {
  138. sandboxPrototype: proto,
  139. wantXrays: true,
  140. wantGlobalProperties: wantGlobalProperties,
  141. sameZoneAs: window,
  142. metadata: { SDKContentScript: true }
  143. });
  144. // We have to ensure that window.top and window.parent are the exact same
  145. // object than window object, i.e. the sandbox global object. But not
  146. // always, in case of iframes, top and parent are another window object.
  147. let top = window.top === window ? content : content.top;
  148. let parent = window.parent === window ? content : content.parent;
  149. merge(content, {
  150. // We need "this === window === top" to be true in toplevel scope:
  151. get window() content,
  152. get top() top,
  153. get parent() parent,
  154. // Use the Greasemonkey naming convention to provide access to the
  155. // unwrapped window object so the content script can access document
  156. // JavaScript values.
  157. // NOTE: this functionality is experimental and may change or go away
  158. // at any time!
  159. get unsafeWindow() window.wrappedJSObject
  160. });
  161. // Load trusted code that will inject content script API.
  162. // We need to expose JS objects defined in same principal in order to
  163. // avoid having any kind of wrapper.
  164. load(apiSandbox, CONTENT_WORKER_URL);
  165. // prepare a clean `self.options`
  166. let options = 'contentScriptOptions' in worker ?
  167. JSON.stringify( worker.contentScriptOptions ) :
  168. undefined;
  169. // Then call `inject` method and communicate with this script
  170. // by trading two methods that allow to send events to the other side:
  171. // - `onEvent` called by content script
  172. // - `result.emitToContent` called by addon script
  173. // Bug 758203: We have to explicitely define `__exposedProps__` in order
  174. // to allow access to these chrome object attributes from this sandbox with
  175. // content priviledges
  176. // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
  177. let chromeAPI = {
  178. timers: {
  179. setTimeout: timer.setTimeout,
  180. setInterval: timer.setInterval,
  181. clearTimeout: timer.clearTimeout,
  182. clearInterval: timer.clearInterval,
  183. __exposedProps__: {
  184. setTimeout: 'r',
  185. setInterval: 'r',
  186. clearTimeout: 'r',
  187. clearInterval: 'r'
  188. }
  189. },
  190. sandbox: {
  191. evaluate: evaluate,
  192. __exposedProps__: {
  193. evaluate: 'r',
  194. }
  195. },
  196. __exposedProps__: {
  197. timers: 'r',
  198. sandbox: 'r',
  199. }
  200. };
  201. let onEvent = this._onContentEvent.bind(this);
  202. // `ContentWorker` is defined in CONTENT_WORKER_URL file
  203. let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
  204. this._emitToContent = result.emitToContent;
  205. this._hasListenerFor = result.hasListenerFor;
  206. // Handle messages send by this script:
  207. let self = this;
  208. // console.xxx calls
  209. this.on("console", function consoleListener(kind) {
  210. console[kind].apply(console, Array.slice(arguments, 1));
  211. });
  212. // self.postMessage calls
  213. this.on("message", function postMessage(data) {
  214. // destroyed?
  215. if (self._addonWorker)
  216. self._addonWorker._emit('message', data);
  217. });
  218. // self.port.emit calls
  219. this.on("event", function portEmit(name, args) {
  220. // destroyed?
  221. if (self._addonWorker)
  222. self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
  223. });
  224. // unwrap, recreate and propagate async Errors thrown from content-script
  225. this.on("error", function onError({instanceOfError, value}) {
  226. if (self._addonWorker) {
  227. let error = value;
  228. if (instanceOfError) {
  229. error = new Error(value.message, value.fileName, value.lineNumber);
  230. error.stack = value.stack;
  231. error.name = value.name;
  232. }
  233. self._addonWorker._emit('error', error);
  234. }
  235. });
  236. // Inject `addon` global into target document if document is trusted,
  237. // `addon` in document is equivalent to `self` in content script.
  238. if (worker._injectInDocument) {
  239. let win = window.wrappedJSObject ? window.wrappedJSObject : window;
  240. Object.defineProperty(win, "addon", {
  241. value: content.self
  242. }
  243. );
  244. }
  245. // Inject our `console` into target document if worker doesn't have a tab
  246. // (e.g Panel, PageWorker, Widget).
  247. // `worker.tab` can't be used because bug 804935.
  248. if (!getTabForContentWindow(window)) {
  249. let win = window.wrappedJSObject ? window.wrappedJSObject : window;
  250. // export our chrome console to content window, using the same approach
  251. // of `ConsoleAPI`:
  252. // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
  253. //
  254. // and described here:
  255. // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
  256. let con = Cu.createObjectIn(win);
  257. let genPropDesc = function genPropDesc(fun) {
  258. return { enumerable: true, configurable: true, writable: true,
  259. value: console[fun] };
  260. }
  261. const properties = {
  262. log: genPropDesc('log'),
  263. info: genPropDesc('info'),
  264. warn: genPropDesc('warn'),
  265. error: genPropDesc('error'),
  266. debug: genPropDesc('debug'),
  267. trace: genPropDesc('trace'),
  268. dir: genPropDesc('dir'),
  269. group: genPropDesc('group'),
  270. groupCollapsed: genPropDesc('groupCollapsed'),
  271. groupEnd: genPropDesc('groupEnd'),
  272. time: genPropDesc('time'),
  273. timeEnd: genPropDesc('timeEnd'),
  274. profile: genPropDesc('profile'),
  275. profileEnd: genPropDesc('profileEnd'),
  276. __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
  277. value: function() {} }
  278. };
  279. Object.defineProperties(con, properties);
  280. Cu.makeObjectPropsNormal(con);
  281. win.console = con;
  282. };
  283. // The order of `contentScriptFile` and `contentScript` evaluation is
  284. // intentional, so programs can load libraries like jQuery from script URLs
  285. // and use them in scripts.
  286. let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
  287. : null,
  288. contentScript = ('contentScript' in worker) ? worker.contentScript : null;
  289. if (contentScriptFile) {
  290. if (Array.isArray(contentScriptFile))
  291. this._importScripts.apply(this, contentScriptFile);
  292. else
  293. this._importScripts(contentScriptFile);
  294. }
  295. if (contentScript) {
  296. this._evaluate(
  297. Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
  298. );
  299. }
  300. },
  301. destroy: function destroy() {
  302. this.emitSync("detach");
  303. this._sandbox = null;
  304. this._addonWorker = null;
  305. },
  306. /**
  307. * JavaScript sandbox where all the content scripts are evaluated.
  308. * {Sandbox}
  309. */
  310. _sandbox: null,
  311. /**
  312. * Reference to the addon side of the worker.
  313. * @type {Worker}
  314. */
  315. _addonWorker: null,
  316. /**
  317. * Evaluates code in the sandbox.
  318. * @param {String} code
  319. * JavaScript source to evaluate.
  320. * @param {String} [filename='javascript:' + code]
  321. * Name of the file
  322. */
  323. _evaluate: function(code, filename) {
  324. try {
  325. evaluate(this._sandbox, code, filename || 'javascript:' + code);
  326. }
  327. catch(e) {
  328. this._addonWorker._emit('error', e);
  329. }
  330. },
  331. /**
  332. * Imports scripts to the sandbox by reading files under urls and
  333. * evaluating its source. If exception occurs during evaluation
  334. * `"error"` event is emitted on the worker.
  335. * This is actually an analog to the `importScript` method in web
  336. * workers but in our case it's not exposed even though content
  337. * scripts may be able to do it synchronously since IO operation
  338. * takes place in the UI process.
  339. */
  340. _importScripts: function _importScripts(url) {
  341. let urls = Array.slice(arguments, 0);
  342. for each (let contentScriptFile in urls) {
  343. try {
  344. let uri = URL(contentScriptFile);
  345. if (uri.scheme === 'resource')
  346. load(this._sandbox, String(uri));
  347. else
  348. throw Error("Unsupported `contentScriptFile` url: " + String(uri));
  349. }
  350. catch(e) {
  351. this._addonWorker._emit('error', e);
  352. }
  353. }
  354. }
  355. });
  356. /**
  357. * Message-passing facility for communication between code running
  358. * in the content and add-on process.
  359. * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
  360. */
  361. const Worker = EventEmitter.compose({
  362. on: Trait.required,
  363. _removeAllListeners: Trait.required,
  364. // List of messages fired before worker is initialized
  365. get _earlyEvents() {
  366. delete this._earlyEvents;
  367. this._earlyEvents = [];
  368. return this._earlyEvents;
  369. },
  370. /**
  371. * Sends a message to the worker's global scope. Method takes single
  372. * argument, which represents data to be sent to the worker. The data may
  373. * be any primitive type value or `JSON`. Call of this method asynchronously
  374. * emits `message` event with data value in the global scope of this
  375. * symbiont.
  376. *
  377. * `message` event listeners can be set either by calling
  378. * `self.on` with a first argument string `"message"` or by
  379. * implementing `onMessage` function in the global scope of this worker.
  380. * @param {Number|String|JSON} data
  381. */
  382. postMessage: function (data) {
  383. let args = ['message'].concat(Array.slice(arguments));
  384. if (!this._inited) {
  385. this._earlyEvents.push(args);
  386. return;
  387. }
  388. processMessage.apply(this, args);
  389. },
  390. /**
  391. * EventEmitter, that behaves (calls listeners) asynchronously.
  392. * A way to send customized messages to / from the worker.
  393. * Events from in the worker can be observed / emitted via
  394. * worker.on / worker.emit.
  395. */
  396. get port() {
  397. // We generate dynamically this attribute as it needs to be accessible
  398. // before Worker.constructor gets called. (For ex: Panel)
  399. // create an event emitter that receive and send events from/to the worker
  400. this._port = EventEmitterTrait.create({
  401. emit: this._emitEventToContent.bind(this)
  402. });
  403. // expose wrapped port, that exposes only public properties:
  404. // We need to destroy this getter in order to be able to set the
  405. // final value. We need to update only public port attribute as we never
  406. // try to access port attribute from private API.
  407. delete this._public.port;
  408. this._public.port = Cortex(this._port);
  409. // Replicate public port to the private object
  410. delete this.port;
  411. this.port = this._public.port;
  412. return this._port;
  413. },
  414. /**
  415. * Same object than this.port but private API.
  416. * Allow access to _emit, in order to send event to port.
  417. */
  418. _port: null,
  419. /**
  420. * Emit a custom event to the content script,
  421. * i.e. emit this event on `self.port`
  422. */
  423. _emitEventToContent: function () {
  424. let args = ['event'].concat(Array.slice(arguments));
  425. if (!this._inited) {
  426. this._earlyEvents.push(args);
  427. return;
  428. }
  429. processMessage.apply(this, args);
  430. },
  431. // Is worker connected to the content worker sandbox ?
  432. _inited: false,
  433. // Is worker being frozen? i.e related document is frozen in bfcache.
  434. // Content script should not be reachable if frozen.
  435. _frozen: true,
  436. constructor: function Worker(options) {
  437. options = options || {};
  438. if ('contentScriptFile' in options)
  439. this.contentScriptFile = options.contentScriptFile;
  440. if ('contentScriptOptions' in options)
  441. this.contentScriptOptions = options.contentScriptOptions;
  442. if ('contentScript' in options)
  443. this.contentScript = options.contentScript;
  444. this._setListeners(options);
  445. unload.ensure(this._public, "destroy");
  446. // Ensure that worker._port is initialized for contentWorker to be able
  447. // to send events during worker initialization.
  448. this.port;
  449. this._documentUnload = this._documentUnload.bind(this);
  450. this._pageShow = this._pageShow.bind(this);
  451. this._pageHide = this._pageHide.bind(this);
  452. if ("window" in options) this._attach(options.window);
  453. },
  454. _setListeners: function(options) {
  455. if ('onError' in options)
  456. this.on('error', options.onError);
  457. if ('onMessage' in options)
  458. this.on('message', options.onMessage);
  459. if ('onDetach' in options)
  460. this.on('detach', options.onDetach);
  461. },
  462. _attach: function(window) {
  463. this._window = window;
  464. // Track document unload to destroy this worker.
  465. // We can't watch for unload event on page's window object as it
  466. // prevents bfcache from working:
  467. // https://developer.mozilla.org/En/Working_with_BFCache
  468. this._windowID = getInnerId(this._window);
  469. observers.on("inner-window-destroyed", this._documentUnload);
  470. // Listen to pagehide event in order to freeze the content script
  471. // while the document is frozen in bfcache:
  472. this._window.addEventListener("pageshow", this._pageShow, true);
  473. this._window.addEventListener("pagehide", this._pageHide, true);
  474. // will set this._contentWorker pointing to the private API:
  475. this._contentWorker = WorkerSandbox(this);
  476. // Mainly enable worker.port.emit to send event to the content worker
  477. this._inited = true;
  478. this._frozen = false;
  479. // Process all events and messages that were fired before the
  480. // worker was initialized.
  481. this._earlyEvents.forEach((function (args) {
  482. processMessage.apply(this, args);
  483. }).bind(this));
  484. },
  485. _documentUnload: function _documentUnload({ subject, data }) {
  486. let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
  487. if (innerWinID != this._windowID) return false;
  488. this._workerCleanup();
  489. return true;
  490. },
  491. _pageShow: function _pageShow() {
  492. this._contentWorker.emitSync("pageshow");
  493. this._emit("pageshow");
  494. this._frozen = false;
  495. },
  496. _pageHide: function _pageHide() {
  497. this._contentWorker.emitSync("pagehide");
  498. this._emit("pagehide");
  499. this._frozen = true;
  500. },
  501. get url() {
  502. // this._window will be null after detach
  503. return this._window ? this._window.document.location.href : null;
  504. },
  505. get tab() {
  506. // this._window will be null after detach
  507. if (this._window)
  508. return getTabForWindow(this._window);
  509. return null;
  510. },
  511. /**
  512. * Tells content worker to unload itself and
  513. * removes all the references from itself.
  514. */
  515. destroy: function destroy() {
  516. this._workerCleanup();
  517. this._inited = true;
  518. this._removeAllListeners();
  519. },
  520. /**
  521. * Remove all internal references to the attached document
  522. * Tells _port to unload itself and removes all the references from itself.
  523. */
  524. _workerCleanup: function _workerCleanup() {
  525. // maybe unloaded before content side is created
  526. // As Symbiont call worker.constructor on document load
  527. if (this._contentWorker)
  528. this._contentWorker.destroy();
  529. this._contentWorker = null;
  530. if (this._window) {
  531. this._window.removeEventListener("pageshow", this._pageShow, true);
  532. this._window.removeEventListener("pagehide", this._pageHide, true);
  533. }
  534. this._window = null;
  535. // This method may be called multiple times,
  536. // avoid dispatching `detach` event more than once
  537. if (this._windowID) {
  538. this._windowID = null;
  539. observers.off("inner-window-destroyed", this._documentUnload);
  540. this._earlyEvents.length = 0;
  541. this._emit("detach");
  542. }
  543. this._inited = false;
  544. },
  545. /**
  546. * Receive an event from the content script that need to be sent to
  547. * worker.port. Provide a way for composed object to catch all events.
  548. */
  549. _onContentScriptEvent: function _onContentScriptEvent() {
  550. this._port._emit.apply(this._port, arguments);
  551. },
  552. /**
  553. * Reference to the content side of the worker.
  554. * @type {WorkerGlobalScope}
  555. */
  556. _contentWorker: null,
  557. /**
  558. * Reference to the window that is accessible from
  559. * the content scripts.
  560. * @type {Object}
  561. */
  562. _window: null,
  563. /**
  564. * Flag to enable `addon` object injection in document. (bug 612726)
  565. * @type {Boolean}
  566. */
  567. _injectInDocument: false
  568. });
  569. /**
  570. * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
  571. * queue when fired before the content is loaded. Sends arguments to
  572. * contentWorker if able
  573. */
  574. function processMessage () {
  575. if (!this._contentWorker)
  576. throw new Error(ERR_DESTROYED);
  577. if (this._frozen)
  578. throw new Error(ERR_FROZEN);
  579. this._contentWorker.emit.apply(null, Array.slice(arguments));
  580. }
  581. exports.Worker = Worker;