|
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 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
7 |
|
8 if (typeof(Ci) == 'undefined') { |
|
9 var Ci = Components.interfaces; |
|
10 } |
|
11 |
|
12 if (typeof(Cc) == 'undefined') { |
|
13 var Cc = Components.classes; |
|
14 } |
|
15 |
|
16 /** |
|
17 * Special Powers Exception - used to throw exceptions nicely |
|
18 **/ |
|
19 function SpecialPowersException(aMsg) { |
|
20 this.message = aMsg; |
|
21 this.name = "SpecialPowersException"; |
|
22 } |
|
23 |
|
24 SpecialPowersException.prototype.toString = function() { |
|
25 return this.name + ': "' + this.message + '"'; |
|
26 }; |
|
27 |
|
28 this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() { |
|
29 this._crashDumpDir = null; |
|
30 this._processCrashObserversRegistered = false; |
|
31 this._chromeScriptListeners = []; |
|
32 } |
|
33 |
|
34 function parseKeyValuePairs(text) { |
|
35 var lines = text.split('\n'); |
|
36 var data = {}; |
|
37 for (let i = 0; i < lines.length; i++) { |
|
38 if (lines[i] == '') |
|
39 continue; |
|
40 |
|
41 // can't just .split() because the value might contain = characters |
|
42 let eq = lines[i].indexOf('='); |
|
43 if (eq != -1) { |
|
44 let [key, value] = [lines[i].substring(0, eq), |
|
45 lines[i].substring(eq + 1)]; |
|
46 if (key && value) |
|
47 data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\"); |
|
48 } |
|
49 } |
|
50 return data; |
|
51 } |
|
52 |
|
53 function parseKeyValuePairsFromFile(file) { |
|
54 var fstream = Cc["@mozilla.org/network/file-input-stream;1"]. |
|
55 createInstance(Ci.nsIFileInputStream); |
|
56 fstream.init(file, -1, 0, 0); |
|
57 var is = Cc["@mozilla.org/intl/converter-input-stream;1"]. |
|
58 createInstance(Ci.nsIConverterInputStream); |
|
59 is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); |
|
60 var str = {}; |
|
61 var contents = ''; |
|
62 while (is.readString(4096, str) != 0) { |
|
63 contents += str.value; |
|
64 } |
|
65 is.close(); |
|
66 fstream.close(); |
|
67 return parseKeyValuePairs(contents); |
|
68 } |
|
69 |
|
70 SpecialPowersObserverAPI.prototype = { |
|
71 |
|
72 _observe: function(aSubject, aTopic, aData) { |
|
73 function addDumpIDToMessage(propertyName) { |
|
74 try { |
|
75 var id = aSubject.getPropertyAsAString(propertyName); |
|
76 } catch(ex) { |
|
77 var id = null; |
|
78 } |
|
79 if (id) { |
|
80 message.dumpIDs.push({id: id, extension: "dmp"}); |
|
81 message.dumpIDs.push({id: id, extension: "extra"}); |
|
82 } |
|
83 } |
|
84 |
|
85 switch(aTopic) { |
|
86 case "plugin-crashed": |
|
87 case "ipc:content-shutdown": |
|
88 var message = { type: "crash-observed", dumpIDs: [] }; |
|
89 aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); |
|
90 if (aTopic == "plugin-crashed") { |
|
91 addDumpIDToMessage("pluginDumpID"); |
|
92 addDumpIDToMessage("browserDumpID"); |
|
93 |
|
94 let pluginID = aSubject.getPropertyAsAString("pluginDumpID"); |
|
95 let extra = this._getExtraData(pluginID); |
|
96 if (extra && ("additional_minidumps" in extra)) { |
|
97 let dumpNames = extra.additional_minidumps.split(','); |
|
98 for (let name of dumpNames) { |
|
99 message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"}); |
|
100 } |
|
101 } |
|
102 } else { // ipc:content-shutdown |
|
103 addDumpIDToMessage("dumpID"); |
|
104 } |
|
105 this._sendAsyncMessage("SPProcessCrashService", message); |
|
106 break; |
|
107 } |
|
108 }, |
|
109 |
|
110 _getCrashDumpDir: function() { |
|
111 if (!this._crashDumpDir) { |
|
112 this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); |
|
113 this._crashDumpDir.append("minidumps"); |
|
114 } |
|
115 return this._crashDumpDir; |
|
116 }, |
|
117 |
|
118 _getExtraData: function(dumpId) { |
|
119 let extraFile = this._getCrashDumpDir().clone(); |
|
120 extraFile.append(dumpId + ".extra"); |
|
121 if (!extraFile.exists()) { |
|
122 return null; |
|
123 } |
|
124 return parseKeyValuePairsFromFile(extraFile); |
|
125 }, |
|
126 |
|
127 _deleteCrashDumpFiles: function(aFilenames) { |
|
128 var crashDumpDir = this._getCrashDumpDir(); |
|
129 if (!crashDumpDir.exists()) { |
|
130 return false; |
|
131 } |
|
132 |
|
133 var success = aFilenames.length != 0; |
|
134 aFilenames.forEach(function(crashFilename) { |
|
135 var file = crashDumpDir.clone(); |
|
136 file.append(crashFilename); |
|
137 if (file.exists()) { |
|
138 file.remove(false); |
|
139 } else { |
|
140 success = false; |
|
141 } |
|
142 }); |
|
143 return success; |
|
144 }, |
|
145 |
|
146 _findCrashDumpFiles: function(aToIgnore) { |
|
147 var crashDumpDir = this._getCrashDumpDir(); |
|
148 var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; |
|
149 if (!entries) { |
|
150 return []; |
|
151 } |
|
152 |
|
153 var crashDumpFiles = []; |
|
154 while (entries.hasMoreElements()) { |
|
155 var file = entries.getNext().QueryInterface(Ci.nsIFile); |
|
156 var path = String(file.path); |
|
157 if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { |
|
158 crashDumpFiles.push(path); |
|
159 } |
|
160 } |
|
161 return crashDumpFiles.concat(); |
|
162 }, |
|
163 |
|
164 _getURI: function (url) { |
|
165 return Services.io.newURI(url, null, null); |
|
166 }, |
|
167 |
|
168 _readUrlAsString: function(aUrl) { |
|
169 // Fetch script content as we can't use scriptloader's loadSubScript |
|
170 // to evaluate http:// urls... |
|
171 var scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"] |
|
172 .getService(Ci.nsIScriptableInputStream); |
|
173 var channel = Services.io.newChannel(aUrl, null, null); |
|
174 var input = channel.open(); |
|
175 scriptableStream.init(input); |
|
176 |
|
177 var str; |
|
178 var buffer = []; |
|
179 |
|
180 while ((str = scriptableStream.read(4096))) { |
|
181 buffer.push(str); |
|
182 } |
|
183 |
|
184 var output = buffer.join(""); |
|
185 |
|
186 scriptableStream.close(); |
|
187 input.close(); |
|
188 |
|
189 var status; |
|
190 try { |
|
191 channel.QueryInterface(Ci.nsIHttpChannel); |
|
192 status = channel.responseStatus; |
|
193 } catch(e) { |
|
194 /* The channel is not a nsIHttpCHannel, but that's fine */ |
|
195 dump("-*- _readUrlAsString: Got an error while fetching " + |
|
196 "chrome script '" + aUrl + "': (" + e.name + ") " + e.message + ". " + |
|
197 "Ignoring.\n"); |
|
198 } |
|
199 |
|
200 if (status == 404) { |
|
201 throw new SpecialPowersException( |
|
202 "Error while executing chrome script '" + aUrl + "':\n" + |
|
203 "The script doesn't exists. Ensure you have registered it in " + |
|
204 "'support-files' in your mochitest.ini."); |
|
205 } |
|
206 |
|
207 return output; |
|
208 }, |
|
209 |
|
210 /** |
|
211 * messageManager callback function |
|
212 * This will get requests from our API in the window and process them in chrome for it |
|
213 **/ |
|
214 _receiveMessageAPI: function(aMessage) { |
|
215 // We explicitly return values in the below code so that this function |
|
216 // doesn't trigger a flurry of warnings about "does not always return |
|
217 // a value". |
|
218 switch(aMessage.name) { |
|
219 case "SPPrefService": |
|
220 var prefs = Services.prefs; |
|
221 var prefType = aMessage.json.prefType.toUpperCase(); |
|
222 var prefName = aMessage.json.prefName; |
|
223 var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; |
|
224 |
|
225 if (aMessage.json.op == "get") { |
|
226 if (!prefName || !prefType) |
|
227 throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); |
|
228 |
|
229 // return null if the pref doesn't exist |
|
230 if (prefs.getPrefType(prefName) == prefs.PREF_INVALID) |
|
231 return null; |
|
232 } else if (aMessage.json.op == "set") { |
|
233 if (!prefName || !prefType || prefValue === null) |
|
234 throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); |
|
235 } else if (aMessage.json.op == "clear") { |
|
236 if (!prefName) |
|
237 throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); |
|
238 } else { |
|
239 throw new SpecialPowersException("Invalid operation for SPPrefService"); |
|
240 } |
|
241 |
|
242 // Now we make the call |
|
243 switch(prefType) { |
|
244 case "BOOL": |
|
245 if (aMessage.json.op == "get") |
|
246 return(prefs.getBoolPref(prefName)); |
|
247 else |
|
248 return(prefs.setBoolPref(prefName, prefValue)); |
|
249 case "INT": |
|
250 if (aMessage.json.op == "get") |
|
251 return(prefs.getIntPref(prefName)); |
|
252 else |
|
253 return(prefs.setIntPref(prefName, prefValue)); |
|
254 case "CHAR": |
|
255 if (aMessage.json.op == "get") |
|
256 return(prefs.getCharPref(prefName)); |
|
257 else |
|
258 return(prefs.setCharPref(prefName, prefValue)); |
|
259 case "COMPLEX": |
|
260 if (aMessage.json.op == "get") |
|
261 return(prefs.getComplexValue(prefName, prefValue[0])); |
|
262 else |
|
263 return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1])); |
|
264 case "": |
|
265 if (aMessage.json.op == "clear") { |
|
266 prefs.clearUserPref(prefName); |
|
267 return undefined; |
|
268 } |
|
269 } |
|
270 return undefined; // See comment at the beginning of this function. |
|
271 |
|
272 case "SPProcessCrashService": |
|
273 switch (aMessage.json.op) { |
|
274 case "register-observer": |
|
275 this._addProcessCrashObservers(); |
|
276 break; |
|
277 case "unregister-observer": |
|
278 this._removeProcessCrashObservers(); |
|
279 break; |
|
280 case "delete-crash-dump-files": |
|
281 return this._deleteCrashDumpFiles(aMessage.json.filenames); |
|
282 case "find-crash-dump-files": |
|
283 return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); |
|
284 default: |
|
285 throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); |
|
286 } |
|
287 return undefined; // See comment at the beginning of this function. |
|
288 |
|
289 case "SPPermissionManager": |
|
290 let msg = aMessage.json; |
|
291 |
|
292 let secMan = Services.scriptSecurityManager; |
|
293 let principal = secMan.getAppCodebasePrincipal(this._getURI(msg.url), msg.appId, msg.isInBrowserElement); |
|
294 |
|
295 switch (msg.op) { |
|
296 case "add": |
|
297 Services.perms.addFromPrincipal(principal, msg.type, msg.permission); |
|
298 break; |
|
299 case "remove": |
|
300 Services.perms.removeFromPrincipal(principal, msg.type); |
|
301 break; |
|
302 case "has": |
|
303 let hasPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type); |
|
304 if (hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION) |
|
305 return true; |
|
306 return false; |
|
307 break; |
|
308 case "test": |
|
309 let testPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type, msg.value); |
|
310 if (testPerm == msg.value) { |
|
311 return true; |
|
312 } |
|
313 return false; |
|
314 break; |
|
315 default: |
|
316 throw new SpecialPowersException("Invalid operation for " + |
|
317 "SPPermissionManager"); |
|
318 } |
|
319 return undefined; // See comment at the beginning of this function. |
|
320 |
|
321 case "SPWebAppService": |
|
322 let Webapps = {}; |
|
323 Components.utils.import("resource://gre/modules/Webapps.jsm", Webapps); |
|
324 switch (aMessage.json.op) { |
|
325 case "set-launchable": |
|
326 let val = Webapps.DOMApplicationRegistry.allAppsLaunchable; |
|
327 Webapps.DOMApplicationRegistry.allAppsLaunchable = aMessage.json.launchable; |
|
328 return val; |
|
329 default: |
|
330 throw new SpecialPowersException("Invalid operation for SPWebAppsService"); |
|
331 } |
|
332 return undefined; // See comment at the beginning of this function. |
|
333 |
|
334 case "SPObserverService": |
|
335 switch (aMessage.json.op) { |
|
336 case "notify": |
|
337 let topic = aMessage.json.observerTopic; |
|
338 let data = aMessage.json.observerData |
|
339 Services.obs.notifyObservers(null, topic, data); |
|
340 break; |
|
341 default: |
|
342 throw new SpecialPowersException("Invalid operation for SPObserverervice"); |
|
343 } |
|
344 return undefined; // See comment at the beginning of this function. |
|
345 |
|
346 case "SPLoadChromeScript": |
|
347 var url = aMessage.json.url; |
|
348 var id = aMessage.json.id; |
|
349 |
|
350 var jsScript = this._readUrlAsString(url); |
|
351 |
|
352 // Setup a chrome sandbox that has access to sendAsyncMessage |
|
353 // and addMessageListener in order to communicate with |
|
354 // the mochitest. |
|
355 var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); |
|
356 var sb = Components.utils.Sandbox(systemPrincipal); |
|
357 var mm = aMessage.target |
|
358 .QueryInterface(Ci.nsIFrameLoaderOwner) |
|
359 .frameLoader |
|
360 .messageManager; |
|
361 sb.sendAsyncMessage = (name, message) => { |
|
362 mm.sendAsyncMessage("SPChromeScriptMessage", |
|
363 { id: id, name: name, message: message }); |
|
364 }; |
|
365 sb.addMessageListener = (name, listener) => { |
|
366 this._chromeScriptListeners.push({ id: id, name: name, listener: listener }); |
|
367 }; |
|
368 |
|
369 // Also expose assertion functions |
|
370 let reporter = function (err, message, stack) { |
|
371 // Pipe assertions back to parent process |
|
372 mm.sendAsyncMessage("SPChromeScriptAssert", |
|
373 { id: id, url: url, err: err, message: message, |
|
374 stack: stack }); |
|
375 }; |
|
376 Object.defineProperty(sb, "assert", { |
|
377 get: function () { |
|
378 let scope = Components.utils.createObjectIn(sb); |
|
379 Services.scriptloader.loadSubScript("resource://specialpowers/Assert.jsm", |
|
380 scope); |
|
381 |
|
382 let assert = new scope.Assert(reporter); |
|
383 delete sb.assert; |
|
384 return sb.assert = assert; |
|
385 }, |
|
386 configurable: true |
|
387 }); |
|
388 |
|
389 // Evaluate the chrome script |
|
390 try { |
|
391 Components.utils.evalInSandbox(jsScript, sb, "1.8", url, 1); |
|
392 } catch(e) { |
|
393 throw new SpecialPowersException("Error while executing chrome " + |
|
394 "script '" + url + "':\n" + e + "\n" + |
|
395 e.fileName + ":" + e.lineNumber); |
|
396 } |
|
397 return undefined; // See comment at the beginning of this function. |
|
398 |
|
399 case "SPChromeScriptMessage": |
|
400 var id = aMessage.json.id; |
|
401 var name = aMessage.json.name; |
|
402 var message = aMessage.json.message; |
|
403 this._chromeScriptListeners |
|
404 .filter(o => (o.name == name && o.id == id)) |
|
405 .forEach(o => o.listener(message)); |
|
406 return undefined; // See comment at the beginning of this function. |
|
407 |
|
408 default: |
|
409 throw new SpecialPowersException("Unrecognized Special Powers API"); |
|
410 } |
|
411 |
|
412 // We throw an exception before reaching this explicit return because |
|
413 // we should never be arriving here anyway. |
|
414 throw new SpecialPowersException("Unreached code"); |
|
415 return undefined; |
|
416 } |
|
417 }; |
|
418 |