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