|
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 "use strict"; |
|
6 |
|
7 module.metadata = { |
|
8 "stability": "unstable" |
|
9 }; |
|
10 |
|
11 const { EventEmitter } = require('../deprecated/events'); |
|
12 const { validateOptions } = require('../deprecated/api-utils'); |
|
13 const { isValidURI, URL } = require('../url'); |
|
14 const file = require('../io/file'); |
|
15 const { contract } = require('../util/contract'); |
|
16 const { isString, instanceOf } = require('../lang/type'); |
|
17 |
|
18 const LOCAL_URI_SCHEMES = ['resource', 'data']; |
|
19 |
|
20 // Returns `null` if `value` is `null` or `undefined`, otherwise `value`. |
|
21 function ensureNull(value) value == null ? null : value |
|
22 |
|
23 // map of property validations |
|
24 const valid = { |
|
25 contentURL: { |
|
26 map: function(url) !url ? ensureNull(url) : url.toString(), |
|
27 is: ['undefined', 'null', 'string'], |
|
28 ok: function (url) { |
|
29 if (url === null) |
|
30 return true; |
|
31 return isValidURI(url); |
|
32 }, |
|
33 msg: 'The `contentURL` option must be a valid URL.' |
|
34 }, |
|
35 contentScriptFile: { |
|
36 is: ['undefined', 'null', 'string', 'array', 'object'], |
|
37 map: ensureNull, |
|
38 ok: function(value) { |
|
39 if (value === null) |
|
40 return true; |
|
41 |
|
42 value = [].concat(value); |
|
43 |
|
44 // Make sure every item is a string or an |
|
45 // URL instance, and also a local file URL. |
|
46 return value.every(function (item) { |
|
47 |
|
48 if (!isString(item) && !(item instanceof URL)) |
|
49 return false; |
|
50 |
|
51 try { |
|
52 return ~LOCAL_URI_SCHEMES.indexOf(URL(item).scheme); |
|
53 } |
|
54 catch(e) { |
|
55 return false; |
|
56 } |
|
57 }); |
|
58 |
|
59 }, |
|
60 msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.' |
|
61 }, |
|
62 contentScript: { |
|
63 is: ['undefined', 'null', 'string', 'array'], |
|
64 map: ensureNull, |
|
65 ok: function(value) { |
|
66 return !Array.isArray(value) || value.every( |
|
67 function(item) { return typeof item === 'string' } |
|
68 ); |
|
69 }, |
|
70 msg: 'The `contentScript` option must be a string or an array of strings.' |
|
71 }, |
|
72 contentScriptWhen: { |
|
73 is: ['string'], |
|
74 ok: function(value) { return ~['start', 'ready', 'end'].indexOf(value) }, |
|
75 map: function(value) { |
|
76 return value || 'end'; |
|
77 }, |
|
78 msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' |
|
79 }, |
|
80 contentScriptOptions: { |
|
81 ok: function(value) { |
|
82 if ( value === undefined ) { return true; } |
|
83 try { JSON.parse( JSON.stringify( value ) ); } catch(e) { return false; } |
|
84 return true; |
|
85 }, |
|
86 map: function(value) 'undefined' === getTypeOf(value) ? null : value, |
|
87 msg: 'The contentScriptOptions should be a jsonable value.' |
|
88 } |
|
89 }; |
|
90 exports.validationAttributes = valid; |
|
91 |
|
92 /** |
|
93 * Shortcut function to validate property with validation. |
|
94 * @param {Object|Number|String} suspect |
|
95 * value to validate |
|
96 * @param {Object} validation |
|
97 * validation rule passed to `api-utils` |
|
98 */ |
|
99 function validate(suspect, validation) validateOptions( |
|
100 { $: suspect }, |
|
101 { $: validation } |
|
102 ).$ |
|
103 |
|
104 function Allow(script) ({ |
|
105 get script() script, |
|
106 set script(value) script = !!value |
|
107 }) |
|
108 |
|
109 /** |
|
110 * Trait is intended to be used in some composition. It provides set of core |
|
111 * properties and bounded validations to them. Trait is useful for all the |
|
112 * compositions providing high level APIs for interaction with content. |
|
113 * Property changes emit `"propertyChange"` events on instances. |
|
114 */ |
|
115 const Loader = EventEmitter.compose({ |
|
116 /** |
|
117 * Permissions for the content, with the following keys: |
|
118 * @property {Object} [allow = { script: true }] |
|
119 * @property {Boolean} [allow.script = true] |
|
120 * Whether or not to execute script in the content. Defaults to true. |
|
121 */ |
|
122 get allow() this._allow || (this._allow = Allow(true)), |
|
123 set allow(value) this.allow.script = value && value.script, |
|
124 _allow: null, |
|
125 /** |
|
126 * The content to load. Either a string of HTML or a URL. |
|
127 * @type {String} |
|
128 */ |
|
129 get contentURL() this._contentURL, |
|
130 set contentURL(value) { |
|
131 value = validate(value, valid.contentURL); |
|
132 if (this._contentURL != value) { |
|
133 this._emit('propertyChange', { |
|
134 contentURL: this._contentURL = value |
|
135 }); |
|
136 } |
|
137 }, |
|
138 _contentURL: null, |
|
139 /** |
|
140 * When to load the content scripts. |
|
141 * Possible values are "end" (default), which loads them once all page |
|
142 * contents have been loaded, "ready", which loads them once DOM nodes are |
|
143 * ready (ie like DOMContentLoaded event), and "start", which loads them once |
|
144 * the `window` object for the page has been created, but before any scripts |
|
145 * specified by the page have been loaded. |
|
146 * Property change emits `propertyChange` event on instance with this key |
|
147 * and new value. |
|
148 * @type {'start'|'ready'|'end'} |
|
149 */ |
|
150 get contentScriptWhen() this._contentScriptWhen, |
|
151 set contentScriptWhen(value) { |
|
152 value = validate(value, valid.contentScriptWhen); |
|
153 if (value !== this._contentScriptWhen) { |
|
154 this._emit('propertyChange', { |
|
155 contentScriptWhen: this._contentScriptWhen = value |
|
156 }); |
|
157 } |
|
158 }, |
|
159 _contentScriptWhen: 'end', |
|
160 /** |
|
161 * Options avalaible from the content script as `self.options`. |
|
162 * The value of options can be of any type (object, array, string, etc.) |
|
163 * but only jsonable values will be available as frozen objects from the |
|
164 * content script. |
|
165 * Property change emits `propertyChange` event on instance with this key |
|
166 * and new value. |
|
167 * @type {Object} |
|
168 */ |
|
169 get contentScriptOptions() this._contentScriptOptions, |
|
170 set contentScriptOptions(value) this._contentScriptOptions = value, |
|
171 _contentScriptOptions: null, |
|
172 /** |
|
173 * The URLs of content scripts. |
|
174 * Property change emits `propertyChange` event on instance with this key |
|
175 * and new value. |
|
176 * @type {String[]} |
|
177 */ |
|
178 get contentScriptFile() this._contentScriptFile, |
|
179 set contentScriptFile(value) { |
|
180 value = validate(value, valid.contentScriptFile); |
|
181 if (value != this._contentScriptFile) { |
|
182 this._emit('propertyChange', { |
|
183 contentScriptFile: this._contentScriptFile = value |
|
184 }); |
|
185 } |
|
186 }, |
|
187 _contentScriptFile: null, |
|
188 /** |
|
189 * The texts of content script. |
|
190 * Property change emits `propertyChange` event on instance with this key |
|
191 * and new value. |
|
192 * @type {String|undefined} |
|
193 */ |
|
194 get contentScript() this._contentScript, |
|
195 set contentScript(value) { |
|
196 value = validate(value, valid.contentScript); |
|
197 if (value != this._contentScript) { |
|
198 this._emit('propertyChange', { |
|
199 contentScript: this._contentScript = value |
|
200 }); |
|
201 } |
|
202 }, |
|
203 _contentScript: null |
|
204 }); |
|
205 exports.Loader = Loader; |
|
206 |
|
207 exports.contract = contract(valid); |