page-worker.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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 { Class } = require('./core/heritage');
  9. const { on, emit, off, setListeners } = require('./event/core');
  10. const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
  11. const { detach, attach, destroy, WorkerHost } = require('./content/utils');
  12. const { Worker } = require('./content/worker');
  13. const { Disposable } = require('./core/disposable');
  14. const { EventTarget } = require('./event/target');
  15. const { unload } = require('./system/unload');
  16. const { events, streamEventsFrom } = require('./content/events');
  17. const { getAttachEventType } = require('./content/utils');
  18. const { window } = require('./addon/window');
  19. const { getParentWindow } = require('./window/utils');
  20. const { create: makeFrame, getDocShell } = require('./frame/utils');
  21. const { contract } = require('./util/contract');
  22. const { contract: loaderContract } = require('./content/loader');
  23. const { has } = require('./util/array');
  24. const { Rules } = require('./util/rules');
  25. const { merge } = require('./util/object');
  26. const views = WeakMap();
  27. const workers = WeakMap();
  28. const pages = WeakMap();
  29. const readyEventNames = [
  30. 'DOMContentLoaded',
  31. 'document-element-inserted',
  32. 'load'
  33. ];
  34. function workerFor(page) workers.get(page)
  35. function pageFor(view) pages.get(view)
  36. function viewFor(page) views.get(page)
  37. function isDisposed (page) !views.get(page, false)
  38. let pageContract = contract(merge({
  39. allow: {
  40. is: ['object', 'undefined', 'null'],
  41. map: function (allow) { return { script: !allow || allow.script !== false }}
  42. },
  43. onMessage: {
  44. is: ['function', 'undefined']
  45. },
  46. include: {
  47. is: ['string', 'array', 'undefined']
  48. },
  49. contentScriptWhen: {
  50. is: ['string', 'undefined']
  51. }
  52. }, loaderContract.rules));
  53. function enableScript (page) {
  54. getDocShell(viewFor(page)).allowJavascript = true;
  55. }
  56. function disableScript (page) {
  57. getDocShell(viewFor(page)).allowJavascript = false;
  58. }
  59. function Allow (page) {
  60. return {
  61. get script() { return getDocShell(viewFor(page)).allowJavascript; },
  62. set script(value) { return value ? enableScript(page) : disableScript(page); }
  63. };
  64. }
  65. function injectWorker ({page}) {
  66. let worker = workerFor(page);
  67. let view = viewFor(page);
  68. if (isValidURL(page, view.contentDocument.URL))
  69. attach(worker, view.contentWindow);
  70. }
  71. function isValidURL(page, url) !page.rules || page.rules.matchesAny(url)
  72. const Page = Class({
  73. implements: [
  74. EventTarget,
  75. Disposable
  76. ],
  77. extends: WorkerHost(workerFor),
  78. setup: function Page(options) {
  79. let page = this;
  80. options = pageContract(options);
  81. let view = makeFrame(window.document, {
  82. nodeName: 'iframe',
  83. type: 'content',
  84. uri: options.contentURL,
  85. allowJavascript: options.allow.script,
  86. allowPlugins: true,
  87. allowAuth: true
  88. });
  89. ['contentScriptFile', 'contentScript', 'contentScriptWhen']
  90. .forEach(prop => page[prop] = options[prop]);
  91. views.set(this, view);
  92. pages.set(view, this);
  93. // Set listeners on the {Page} object itself, not the underlying worker,
  94. // like `onMessage`, as it gets piped
  95. setListeners(this, options);
  96. let worker = new Worker(stripListeners(options));
  97. workers.set(this, worker);
  98. pipe(worker, this);
  99. if (this.include || options.include) {
  100. this.rules = Rules();
  101. this.rules.add.apply(this.rules, [].concat(this.include || options.include));
  102. }
  103. },
  104. get allow() { return Allow(this); },
  105. set allow(value) {
  106. let allowJavascript = pageContract({ allow: value }).allow.script;
  107. return allowJavascript ? enableScript(this) : disableScript(this);
  108. },
  109. get contentURL() { return viewFor(this).getAttribute('src'); },
  110. set contentURL(value) {
  111. if (!isValidURL(this, value)) return;
  112. let view = viewFor(this);
  113. let contentURL = pageContract({ contentURL: value }).contentURL;
  114. view.setAttribute('src', contentURL);
  115. },
  116. dispose: function () {
  117. if (isDisposed(this)) return;
  118. let view = viewFor(this);
  119. if (view.parentNode) view.parentNode.removeChild(view);
  120. views.delete(this);
  121. destroy(workers.get(this));
  122. },
  123. toString: function () { return '[object Page]' }
  124. });
  125. exports.Page = Page;
  126. let pageEvents = streamMerge([events, streamEventsFrom(window)]);
  127. let readyEvents = filter(pageEvents, isReadyEvent);
  128. let formattedEvents = map(readyEvents, function({target, type}) {
  129. return { type: type, page: pageFromDoc(target) };
  130. });
  131. let pageReadyEvents = filter(formattedEvents, function({page, type}) {
  132. return getAttachEventType(page) === type});
  133. on(pageReadyEvents, 'data', injectWorker);
  134. function isReadyEvent ({type}) {
  135. return has(readyEventNames, type);
  136. }
  137. /*
  138. * Takes a document, finds its doc shell tree root and returns the
  139. * matching Page instance if found
  140. */
  141. function pageFromDoc(doc) {
  142. let parentWindow = getParentWindow(doc.defaultView), page;
  143. if (!parentWindow) return;
  144. let frames = parentWindow.document.getElementsByTagName('iframe');
  145. for (let i = frames.length; i--;)
  146. if (frames[i].contentDocument === doc && (page = pageFor(frames[i])))
  147. return page;
  148. return null;
  149. }