|
1 /* ***** BEGIN LICENSE BLOCK ***** |
|
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
|
3 * |
|
4 * The contents of this file are subject to the Mozilla Public License Version |
|
5 * 1.1 (the "License"); you may not use this file except in compliance with |
|
6 * the License. You may obtain a copy of the License at |
|
7 * http://www.mozilla.org/MPL/ |
|
8 * |
|
9 * Software distributed under the License is distributed on an "AS IS" basis, |
|
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
|
11 * for the specific language governing rights and limitations under the |
|
12 * License. |
|
13 * |
|
14 * The Original Code is Jetpack. |
|
15 * |
|
16 * The Initial Developer of the Original Code is Mozilla. |
|
17 * Portions created by the Initial Developer are Copyright (C) 2007 |
|
18 * the Initial Developer. All Rights Reserved. |
|
19 * |
|
20 * Contributor(s): |
|
21 * Atul Varma <atul@mozilla.com> |
|
22 * |
|
23 * Alternatively, the contents of this file may be used under the terms of |
|
24 * either the GNU General Public License Version 2 or later (the "GPL"), or |
|
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
|
26 * in which case the provisions of the GPL or the LGPL are applicable instead |
|
27 * of those above. If you wish to allow use of your version of this file only |
|
28 * under the terms of either the GPL or the LGPL, and not to allow others to |
|
29 * use your version of this file under the terms of the MPL, indicate your |
|
30 * decision by deleting the provisions above and replace them with the notice |
|
31 * and other provisions required by the GPL or the LGPL. If you do not delete |
|
32 * the provisions above, a recipient may use your version of this file under |
|
33 * the terms of any one of the MPL, the GPL or the LGPL. |
|
34 * |
|
35 * ***** END LICENSE BLOCK ***** */ |
|
36 |
|
37 (function(global) { |
|
38 const Cc = Components.classes; |
|
39 const Ci = Components.interfaces; |
|
40 const Cu = Components.utils; |
|
41 const Cr = Components.results; |
|
42 |
|
43 var exports = {}; |
|
44 |
|
45 var ios = Cc['@mozilla.org/network/io-service;1'] |
|
46 .getService(Ci.nsIIOService); |
|
47 |
|
48 var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] |
|
49 .createInstance(Ci.nsIPrincipal); |
|
50 |
|
51 function resolvePrincipal(principal, defaultPrincipal) { |
|
52 if (principal === undefined) |
|
53 return defaultPrincipal; |
|
54 if (principal == "system") |
|
55 return systemPrincipal; |
|
56 return principal; |
|
57 } |
|
58 |
|
59 // The base URI to we use when we're given relative URLs, if any. |
|
60 var baseURI = null; |
|
61 if (global.window) |
|
62 baseURI = ios.newURI(global.location.href, null, null); |
|
63 exports.baseURI = baseURI; |
|
64 |
|
65 // The "parent" chrome URI to use if we're loading code that |
|
66 // needs chrome privileges but may not have a filename that |
|
67 // matches any of SpiderMonkey's defined system filename prefixes. |
|
68 // The latter is needed so that wrappers can be automatically |
|
69 // made for the code. For more information on this, see |
|
70 // bug 418356: |
|
71 // |
|
72 // https://bugzilla.mozilla.org/show_bug.cgi?id=418356 |
|
73 var parentChromeURIString; |
|
74 if (baseURI) |
|
75 // We're being loaded from a chrome-privileged document, so |
|
76 // use its URL as the parent string. |
|
77 parentChromeURIString = baseURI.spec; |
|
78 else |
|
79 // We're being loaded from a chrome-privileged JS module or |
|
80 // SecurableModule, so use its filename (which may itself |
|
81 // contain a reference to a parent). |
|
82 parentChromeURIString = Components.stack.filename; |
|
83 |
|
84 function maybeParentifyFilename(filename) { |
|
85 var doParentifyFilename = true; |
|
86 try { |
|
87 // TODO: Ideally we should just make |
|
88 // nsIChromeRegistry.wrappersEnabled() available from script |
|
89 // and use it here. Until that's in the platform, though, |
|
90 // we'll play it safe and parentify the filename unless |
|
91 // we're absolutely certain things will be ok if we don't. |
|
92 var filenameURI = ios.newURI(options.filename, |
|
93 null, |
|
94 baseURI); |
|
95 if (filenameURI.scheme == 'chrome' && |
|
96 filenameURI.path.indexOf('/content/') == 0) |
|
97 // Content packages will always have wrappers made for them; |
|
98 // if automatic wrappers have been disabled for the |
|
99 // chrome package via a chrome manifest flag, then |
|
100 // this still works too, to the extent that the |
|
101 // content package is insecure anyways. |
|
102 doParentifyFilename = false; |
|
103 } catch (e) {} |
|
104 if (doParentifyFilename) |
|
105 return parentChromeURIString + " -> " + filename; |
|
106 return filename; |
|
107 } |
|
108 |
|
109 function getRootDir(urlStr) { |
|
110 // TODO: This feels hacky, and like there will be edge cases. |
|
111 return urlStr.slice(0, urlStr.lastIndexOf("/") + 1); |
|
112 } |
|
113 |
|
114 exports.SandboxFactory = function SandboxFactory(defaultPrincipal) { |
|
115 // Unless specified otherwise, use a principal with limited |
|
116 // privileges. |
|
117 this._defaultPrincipal = resolvePrincipal(defaultPrincipal, |
|
118 "http://www.mozilla.org"); |
|
119 }, |
|
120 |
|
121 exports.SandboxFactory.prototype = { |
|
122 createSandbox: function createSandbox(options) { |
|
123 var principal = resolvePrincipal(options.principal, |
|
124 this._defaultPrincipal); |
|
125 |
|
126 return { |
|
127 _sandbox: new Cu.Sandbox(principal), |
|
128 _principal: principal, |
|
129 get globalScope() { |
|
130 return this._sandbox; |
|
131 }, |
|
132 defineProperty: function defineProperty(name, value) { |
|
133 this._sandbox[name] = value; |
|
134 }, |
|
135 getProperty: function getProperty(name) { |
|
136 return this._sandbox[name]; |
|
137 }, |
|
138 evaluate: function evaluate(options) { |
|
139 if (typeof(options) == 'string') |
|
140 options = {contents: options}; |
|
141 options = {__proto__: options}; |
|
142 if (typeof(options.contents) != 'string') |
|
143 throw new Error('Expected string for options.contents'); |
|
144 if (options.lineNo === undefined) |
|
145 options.lineNo = 1; |
|
146 if (options.jsVersion === undefined) |
|
147 options.jsVersion = "1.8"; |
|
148 if (typeof(options.filename) != 'string') |
|
149 options.filename = '<string>'; |
|
150 |
|
151 if (this._principal == systemPrincipal) |
|
152 options.filename = maybeParentifyFilename(options.filename); |
|
153 |
|
154 return Cu.evalInSandbox(options.contents, |
|
155 this._sandbox, |
|
156 options.jsVersion, |
|
157 options.filename, |
|
158 options.lineNo); |
|
159 } |
|
160 }; |
|
161 } |
|
162 }; |
|
163 |
|
164 exports.Loader = function Loader(options) { |
|
165 options = {__proto__: options}; |
|
166 if (options.fs === undefined) { |
|
167 var rootPaths = options.rootPath || options.rootPaths; |
|
168 if (rootPaths) { |
|
169 if (rootPaths.constructor.name != "Array") |
|
170 rootPaths = [rootPaths]; |
|
171 var fses = [new exports.LocalFileSystem(path) |
|
172 for each (path in rootPaths)]; |
|
173 options.fs = new exports.CompositeFileSystem(fses); |
|
174 } else |
|
175 options.fs = new exports.LocalFileSystem(); |
|
176 } |
|
177 if (options.sandboxFactory === undefined) |
|
178 options.sandboxFactory = new exports.SandboxFactory( |
|
179 options.defaultPrincipal |
|
180 ); |
|
181 if (options.modules === undefined) |
|
182 options.modules = {}; |
|
183 if (options.globals === undefined) |
|
184 options.globals = {}; |
|
185 |
|
186 this.fs = options.fs; |
|
187 this.sandboxFactory = options.sandboxFactory; |
|
188 this.sandboxes = {}; |
|
189 this.modules = options.modules; |
|
190 this.globals = options.globals; |
|
191 }; |
|
192 |
|
193 exports.Loader.prototype = { |
|
194 _makeRequire: function _makeRequire(rootDir) { |
|
195 var self = this; |
|
196 return function require(module) { |
|
197 if (module == "chrome") { |
|
198 var chrome = { Cc: Components.classes, |
|
199 Ci: Components.interfaces, |
|
200 Cu: Components.utils, |
|
201 Cr: Components.results, |
|
202 Cm: Components.manager, |
|
203 components: Components |
|
204 }; |
|
205 return chrome; |
|
206 } |
|
207 var path = self.fs.resolveModule(rootDir, module); |
|
208 if (!path) |
|
209 throw new Error('Module "' + module + '" not found'); |
|
210 if (!(path in self.modules)) { |
|
211 var options = self.fs.getFile(path); |
|
212 if (options.filename === undefined) |
|
213 options.filename = path; |
|
214 |
|
215 var exports = {}; |
|
216 var sandbox = self.sandboxFactory.createSandbox(options); |
|
217 self.sandboxes[path] = sandbox; |
|
218 for (name in self.globals) |
|
219 sandbox.defineProperty(name, self.globals[name]); |
|
220 sandbox.defineProperty('require', self._makeRequire(path)); |
|
221 sandbox.evaluate("var exports = {};"); |
|
222 let ES5 = self.modules.es5; |
|
223 if (ES5) { |
|
224 let { Object, Array, Function } = sandbox.globalScope; |
|
225 ES5.init(Object, Array, Function); |
|
226 } |
|
227 self.modules[path] = sandbox.getProperty("exports"); |
|
228 sandbox.evaluate(options); |
|
229 } |
|
230 return self.modules[path]; |
|
231 }; |
|
232 }, |
|
233 |
|
234 // This is only really used by unit tests and other |
|
235 // development-related facilities, allowing access to symbols |
|
236 // defined in the global scope of a module. |
|
237 findSandboxForModule: function findSandboxForModule(module) { |
|
238 var path = this.fs.resolveModule(null, module); |
|
239 if (!path) |
|
240 throw new Error('Module "' + module + '" not found'); |
|
241 if (!(path in this.sandboxes)) |
|
242 this.require(module); |
|
243 if (!(path in this.sandboxes)) |
|
244 throw new Error('Internal error: path not in sandboxes: ' + |
|
245 path); |
|
246 return this.sandboxes[path]; |
|
247 }, |
|
248 |
|
249 require: function require(module) { |
|
250 return (this._makeRequire(null))(module); |
|
251 }, |
|
252 |
|
253 runScript: function runScript(options, extraOutput) { |
|
254 if (typeof(options) == 'string') |
|
255 options = {contents: options}; |
|
256 options = {__proto__: options}; |
|
257 var sandbox = this.sandboxFactory.createSandbox(options); |
|
258 if (extraOutput) |
|
259 extraOutput.sandbox = sandbox; |
|
260 for (name in this.globals) |
|
261 sandbox.defineProperty(name, this.globals[name]); |
|
262 sandbox.defineProperty('require', this._makeRequire(null)); |
|
263 return sandbox.evaluate(options); |
|
264 } |
|
265 }; |
|
266 |
|
267 exports.CompositeFileSystem = function CompositeFileSystem(fses) { |
|
268 this.fses = fses; |
|
269 this._pathMap = {}; |
|
270 }; |
|
271 |
|
272 exports.CompositeFileSystem.prototype = { |
|
273 resolveModule: function resolveModule(base, path) { |
|
274 for (var i = 0; i < this.fses.length; i++) { |
|
275 var fs = this.fses[i]; |
|
276 var absPath = fs.resolveModule(base, path); |
|
277 if (absPath) { |
|
278 this._pathMap[absPath] = fs; |
|
279 return absPath; |
|
280 } |
|
281 } |
|
282 return null; |
|
283 }, |
|
284 getFile: function getFile(path) { |
|
285 return this._pathMap[path].getFile(path); |
|
286 } |
|
287 }; |
|
288 |
|
289 exports.LocalFileSystem = function LocalFileSystem(root) { |
|
290 if (root === undefined) { |
|
291 if (!baseURI) |
|
292 throw new Error("Need a root path for module filesystem"); |
|
293 root = baseURI; |
|
294 } |
|
295 if (typeof(root) == 'string') |
|
296 root = ios.newURI(root, null, baseURI); |
|
297 if (root instanceof Ci.nsIFile) |
|
298 root = ios.newFileURI(root); |
|
299 if (!(root instanceof Ci.nsIURI)) |
|
300 throw new Error('Expected nsIFile, nsIURI, or string for root'); |
|
301 |
|
302 this.root = root.spec; |
|
303 this._rootURI = root; |
|
304 this._rootURIDir = getRootDir(root.spec); |
|
305 }; |
|
306 |
|
307 exports.LocalFileSystem.prototype = { |
|
308 resolveModule: function resolveModule(base, path) { |
|
309 path = path + ".js"; |
|
310 |
|
311 var baseURI; |
|
312 if (!base) |
|
313 baseURI = this._rootURI; |
|
314 else |
|
315 baseURI = ios.newURI(base, null, null); |
|
316 var newURI = ios.newURI(path, null, baseURI); |
|
317 var channel = ios.newChannelFromURI(newURI); |
|
318 try { |
|
319 channel.open().close(); |
|
320 } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { |
|
321 return null; |
|
322 } |
|
323 return newURI.spec; |
|
324 }, |
|
325 getFile: function getFile(path) { |
|
326 var channel = ios.newChannel(path, null, null); |
|
327 var iStream = channel.open(); |
|
328 var ciStream = Cc["@mozilla.org/intl/converter-input-stream;1"]. |
|
329 createInstance(Ci.nsIConverterInputStream); |
|
330 var bufLen = 0x8000; |
|
331 ciStream.init(iStream, "UTF-8", bufLen, |
|
332 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); |
|
333 var chunk = {}; |
|
334 var data = ""; |
|
335 while (ciStream.readString(bufLen, chunk) > 0) |
|
336 data += chunk.value; |
|
337 ciStream.close(); |
|
338 iStream.close(); |
|
339 return {contents: data}; |
|
340 } |
|
341 }; |
|
342 |
|
343 if (global.window) { |
|
344 // We're being loaded in a chrome window, or a web page with |
|
345 // UniversalXPConnect privileges. |
|
346 global.SecurableModule = exports; |
|
347 } else if (global.exports) { |
|
348 // We're being loaded in a SecurableModule. |
|
349 for (name in exports) { |
|
350 global.exports[name] = exports[name]; |
|
351 } |
|
352 } else { |
|
353 // We're being loaded in a JS module. |
|
354 global.EXPORTED_SYMBOLS = []; |
|
355 for (name in exports) { |
|
356 global.EXPORTED_SYMBOLS.push(name); |
|
357 global[name] = exports[name]; |
|
358 } |
|
359 } |
|
360 })(this); |