michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: if (typeof(Ci) == 'undefined') { michael@0: var Ci = Components.interfaces; michael@0: } michael@0: michael@0: if (typeof(Cc) == 'undefined') { michael@0: var Cc = Components.classes; michael@0: } michael@0: michael@0: /** michael@0: * Special Powers Exception - used to throw exceptions nicely michael@0: **/ michael@0: function SpecialPowersException(aMsg) { michael@0: this.message = aMsg; michael@0: this.name = "SpecialPowersException"; michael@0: } michael@0: michael@0: SpecialPowersException.prototype.toString = function() { michael@0: return this.name + ': "' + this.message + '"'; michael@0: }; michael@0: michael@0: this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() { michael@0: this._crashDumpDir = null; michael@0: this._processCrashObserversRegistered = false; michael@0: this._chromeScriptListeners = []; michael@0: } michael@0: michael@0: function parseKeyValuePairs(text) { michael@0: var lines = text.split('\n'); michael@0: var data = {}; michael@0: for (let i = 0; i < lines.length; i++) { michael@0: if (lines[i] == '') michael@0: continue; michael@0: michael@0: // can't just .split() because the value might contain = characters michael@0: let eq = lines[i].indexOf('='); michael@0: if (eq != -1) { michael@0: let [key, value] = [lines[i].substring(0, eq), michael@0: lines[i].substring(eq + 1)]; michael@0: if (key && value) michael@0: data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\"); michael@0: } michael@0: } michael@0: return data; michael@0: } michael@0: michael@0: function parseKeyValuePairsFromFile(file) { michael@0: var fstream = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fstream.init(file, -1, 0, 0); michael@0: var is = Cc["@mozilla.org/intl/converter-input-stream;1"]. michael@0: createInstance(Ci.nsIConverterInputStream); michael@0: is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); michael@0: var str = {}; michael@0: var contents = ''; michael@0: while (is.readString(4096, str) != 0) { michael@0: contents += str.value; michael@0: } michael@0: is.close(); michael@0: fstream.close(); michael@0: return parseKeyValuePairs(contents); michael@0: } michael@0: michael@0: SpecialPowersObserverAPI.prototype = { michael@0: michael@0: _observe: function(aSubject, aTopic, aData) { michael@0: function addDumpIDToMessage(propertyName) { michael@0: try { michael@0: var id = aSubject.getPropertyAsAString(propertyName); michael@0: } catch(ex) { michael@0: var id = null; michael@0: } michael@0: if (id) { michael@0: message.dumpIDs.push({id: id, extension: "dmp"}); michael@0: message.dumpIDs.push({id: id, extension: "extra"}); michael@0: } michael@0: } michael@0: michael@0: switch(aTopic) { michael@0: case "plugin-crashed": michael@0: case "ipc:content-shutdown": michael@0: var message = { type: "crash-observed", dumpIDs: [] }; michael@0: aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); michael@0: if (aTopic == "plugin-crashed") { michael@0: addDumpIDToMessage("pluginDumpID"); michael@0: addDumpIDToMessage("browserDumpID"); michael@0: michael@0: let pluginID = aSubject.getPropertyAsAString("pluginDumpID"); michael@0: let extra = this._getExtraData(pluginID); michael@0: if (extra && ("additional_minidumps" in extra)) { michael@0: let dumpNames = extra.additional_minidumps.split(','); michael@0: for (let name of dumpNames) { michael@0: message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"}); michael@0: } michael@0: } michael@0: } else { // ipc:content-shutdown michael@0: addDumpIDToMessage("dumpID"); michael@0: } michael@0: this._sendAsyncMessage("SPProcessCrashService", message); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _getCrashDumpDir: function() { michael@0: if (!this._crashDumpDir) { michael@0: this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: this._crashDumpDir.append("minidumps"); michael@0: } michael@0: return this._crashDumpDir; michael@0: }, michael@0: michael@0: _getExtraData: function(dumpId) { michael@0: let extraFile = this._getCrashDumpDir().clone(); michael@0: extraFile.append(dumpId + ".extra"); michael@0: if (!extraFile.exists()) { michael@0: return null; michael@0: } michael@0: return parseKeyValuePairsFromFile(extraFile); michael@0: }, michael@0: michael@0: _deleteCrashDumpFiles: function(aFilenames) { michael@0: var crashDumpDir = this._getCrashDumpDir(); michael@0: if (!crashDumpDir.exists()) { michael@0: return false; michael@0: } michael@0: michael@0: var success = aFilenames.length != 0; michael@0: aFilenames.forEach(function(crashFilename) { michael@0: var file = crashDumpDir.clone(); michael@0: file.append(crashFilename); michael@0: if (file.exists()) { michael@0: file.remove(false); michael@0: } else { michael@0: success = false; michael@0: } michael@0: }); michael@0: return success; michael@0: }, michael@0: michael@0: _findCrashDumpFiles: function(aToIgnore) { michael@0: var crashDumpDir = this._getCrashDumpDir(); michael@0: var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; michael@0: if (!entries) { michael@0: return []; michael@0: } michael@0: michael@0: var crashDumpFiles = []; michael@0: while (entries.hasMoreElements()) { michael@0: var file = entries.getNext().QueryInterface(Ci.nsIFile); michael@0: var path = String(file.path); michael@0: if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { michael@0: crashDumpFiles.push(path); michael@0: } michael@0: } michael@0: return crashDumpFiles.concat(); michael@0: }, michael@0: michael@0: _getURI: function (url) { michael@0: return Services.io.newURI(url, null, null); michael@0: }, michael@0: michael@0: _readUrlAsString: function(aUrl) { michael@0: // Fetch script content as we can't use scriptloader's loadSubScript michael@0: // to evaluate http:// urls... michael@0: var scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"] michael@0: .getService(Ci.nsIScriptableInputStream); michael@0: var channel = Services.io.newChannel(aUrl, null, null); michael@0: var input = channel.open(); michael@0: scriptableStream.init(input); michael@0: michael@0: var str; michael@0: var buffer = []; michael@0: michael@0: while ((str = scriptableStream.read(4096))) { michael@0: buffer.push(str); michael@0: } michael@0: michael@0: var output = buffer.join(""); michael@0: michael@0: scriptableStream.close(); michael@0: input.close(); michael@0: michael@0: var status; michael@0: try { michael@0: channel.QueryInterface(Ci.nsIHttpChannel); michael@0: status = channel.responseStatus; michael@0: } catch(e) { michael@0: /* The channel is not a nsIHttpCHannel, but that's fine */ michael@0: dump("-*- _readUrlAsString: Got an error while fetching " + michael@0: "chrome script '" + aUrl + "': (" + e.name + ") " + e.message + ". " + michael@0: "Ignoring.\n"); michael@0: } michael@0: michael@0: if (status == 404) { michael@0: throw new SpecialPowersException( michael@0: "Error while executing chrome script '" + aUrl + "':\n" + michael@0: "The script doesn't exists. Ensure you have registered it in " + michael@0: "'support-files' in your mochitest.ini."); michael@0: } michael@0: michael@0: return output; michael@0: }, michael@0: michael@0: /** michael@0: * messageManager callback function michael@0: * This will get requests from our API in the window and process them in chrome for it michael@0: **/ michael@0: _receiveMessageAPI: function(aMessage) { michael@0: // We explicitly return values in the below code so that this function michael@0: // doesn't trigger a flurry of warnings about "does not always return michael@0: // a value". michael@0: switch(aMessage.name) { michael@0: case "SPPrefService": michael@0: var prefs = Services.prefs; michael@0: var prefType = aMessage.json.prefType.toUpperCase(); michael@0: var prefName = aMessage.json.prefName; michael@0: var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; michael@0: michael@0: if (aMessage.json.op == "get") { michael@0: if (!prefName || !prefType) michael@0: throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); michael@0: michael@0: // return null if the pref doesn't exist michael@0: if (prefs.getPrefType(prefName) == prefs.PREF_INVALID) michael@0: return null; michael@0: } else if (aMessage.json.op == "set") { michael@0: if (!prefName || !prefType || prefValue === null) michael@0: throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); michael@0: } else if (aMessage.json.op == "clear") { michael@0: if (!prefName) michael@0: throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); michael@0: } else { michael@0: throw new SpecialPowersException("Invalid operation for SPPrefService"); michael@0: } michael@0: michael@0: // Now we make the call michael@0: switch(prefType) { michael@0: case "BOOL": michael@0: if (aMessage.json.op == "get") michael@0: return(prefs.getBoolPref(prefName)); michael@0: else michael@0: return(prefs.setBoolPref(prefName, prefValue)); michael@0: case "INT": michael@0: if (aMessage.json.op == "get") michael@0: return(prefs.getIntPref(prefName)); michael@0: else michael@0: return(prefs.setIntPref(prefName, prefValue)); michael@0: case "CHAR": michael@0: if (aMessage.json.op == "get") michael@0: return(prefs.getCharPref(prefName)); michael@0: else michael@0: return(prefs.setCharPref(prefName, prefValue)); michael@0: case "COMPLEX": michael@0: if (aMessage.json.op == "get") michael@0: return(prefs.getComplexValue(prefName, prefValue[0])); michael@0: else michael@0: return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1])); michael@0: case "": michael@0: if (aMessage.json.op == "clear") { michael@0: prefs.clearUserPref(prefName); michael@0: return undefined; michael@0: } michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPProcessCrashService": michael@0: switch (aMessage.json.op) { michael@0: case "register-observer": michael@0: this._addProcessCrashObservers(); michael@0: break; michael@0: case "unregister-observer": michael@0: this._removeProcessCrashObservers(); michael@0: break; michael@0: case "delete-crash-dump-files": michael@0: return this._deleteCrashDumpFiles(aMessage.json.filenames); michael@0: case "find-crash-dump-files": michael@0: return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); michael@0: default: michael@0: throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPPermissionManager": michael@0: let msg = aMessage.json; michael@0: michael@0: let secMan = Services.scriptSecurityManager; michael@0: let principal = secMan.getAppCodebasePrincipal(this._getURI(msg.url), msg.appId, msg.isInBrowserElement); michael@0: michael@0: switch (msg.op) { michael@0: case "add": michael@0: Services.perms.addFromPrincipal(principal, msg.type, msg.permission); michael@0: break; michael@0: case "remove": michael@0: Services.perms.removeFromPrincipal(principal, msg.type); michael@0: break; michael@0: case "has": michael@0: let hasPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type); michael@0: if (hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION) michael@0: return true; michael@0: return false; michael@0: break; michael@0: case "test": michael@0: let testPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type, msg.value); michael@0: if (testPerm == msg.value) { michael@0: return true; michael@0: } michael@0: return false; michael@0: break; michael@0: default: michael@0: throw new SpecialPowersException("Invalid operation for " + michael@0: "SPPermissionManager"); michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPWebAppService": michael@0: let Webapps = {}; michael@0: Components.utils.import("resource://gre/modules/Webapps.jsm", Webapps); michael@0: switch (aMessage.json.op) { michael@0: case "set-launchable": michael@0: let val = Webapps.DOMApplicationRegistry.allAppsLaunchable; michael@0: Webapps.DOMApplicationRegistry.allAppsLaunchable = aMessage.json.launchable; michael@0: return val; michael@0: default: michael@0: throw new SpecialPowersException("Invalid operation for SPWebAppsService"); michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPObserverService": michael@0: switch (aMessage.json.op) { michael@0: case "notify": michael@0: let topic = aMessage.json.observerTopic; michael@0: let data = aMessage.json.observerData michael@0: Services.obs.notifyObservers(null, topic, data); michael@0: break; michael@0: default: michael@0: throw new SpecialPowersException("Invalid operation for SPObserverervice"); michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPLoadChromeScript": michael@0: var url = aMessage.json.url; michael@0: var id = aMessage.json.id; michael@0: michael@0: var jsScript = this._readUrlAsString(url); michael@0: michael@0: // Setup a chrome sandbox that has access to sendAsyncMessage michael@0: // and addMessageListener in order to communicate with michael@0: // the mochitest. michael@0: var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); michael@0: var sb = Components.utils.Sandbox(systemPrincipal); michael@0: var mm = aMessage.target michael@0: .QueryInterface(Ci.nsIFrameLoaderOwner) michael@0: .frameLoader michael@0: .messageManager; michael@0: sb.sendAsyncMessage = (name, message) => { michael@0: mm.sendAsyncMessage("SPChromeScriptMessage", michael@0: { id: id, name: name, message: message }); michael@0: }; michael@0: sb.addMessageListener = (name, listener) => { michael@0: this._chromeScriptListeners.push({ id: id, name: name, listener: listener }); michael@0: }; michael@0: michael@0: // Also expose assertion functions michael@0: let reporter = function (err, message, stack) { michael@0: // Pipe assertions back to parent process michael@0: mm.sendAsyncMessage("SPChromeScriptAssert", michael@0: { id: id, url: url, err: err, message: message, michael@0: stack: stack }); michael@0: }; michael@0: Object.defineProperty(sb, "assert", { michael@0: get: function () { michael@0: let scope = Components.utils.createObjectIn(sb); michael@0: Services.scriptloader.loadSubScript("resource://specialpowers/Assert.jsm", michael@0: scope); michael@0: michael@0: let assert = new scope.Assert(reporter); michael@0: delete sb.assert; michael@0: return sb.assert = assert; michael@0: }, michael@0: configurable: true michael@0: }); michael@0: michael@0: // Evaluate the chrome script michael@0: try { michael@0: Components.utils.evalInSandbox(jsScript, sb, "1.8", url, 1); michael@0: } catch(e) { michael@0: throw new SpecialPowersException("Error while executing chrome " + michael@0: "script '" + url + "':\n" + e + "\n" + michael@0: e.fileName + ":" + e.lineNumber); michael@0: } michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: case "SPChromeScriptMessage": michael@0: var id = aMessage.json.id; michael@0: var name = aMessage.json.name; michael@0: var message = aMessage.json.message; michael@0: this._chromeScriptListeners michael@0: .filter(o => (o.name == name && o.id == id)) michael@0: .forEach(o => o.listener(message)); michael@0: return undefined; // See comment at the beginning of this function. michael@0: michael@0: default: michael@0: throw new SpecialPowersException("Unrecognized Special Powers API"); michael@0: } michael@0: michael@0: // We throw an exception before reaching this explicit return because michael@0: // we should never be arriving here anyway. michael@0: throw new SpecialPowersException("Unreached code"); michael@0: return undefined; michael@0: } michael@0: }; michael@0: