SpecialPowersObserverAPI.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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. Components.utils.import("resource://gre/modules/Services.jsm");
  6. Components.utils.import("resource://gre/modules/NetUtil.jsm");
  7. if (typeof(Ci) == 'undefined') {
  8. var Ci = Components.interfaces;
  9. }
  10. if (typeof(Cc) == 'undefined') {
  11. var Cc = Components.classes;
  12. }
  13. this.SpecialPowersError = function(aMsg) {
  14. Error.call(this);
  15. let {stack} = new Error();
  16. this.message = aMsg;
  17. this.name = "SpecialPowersError";
  18. }
  19. SpecialPowersError.prototype = Object.create(Error.prototype);
  20. SpecialPowersError.prototype.toString = function() {
  21. return `${this.name}: ${this.message}`;
  22. };
  23. this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() {
  24. this._crashDumpDir = null;
  25. this._processCrashObserversRegistered = false;
  26. this._chromeScriptListeners = [];
  27. this._extensions = new Map();
  28. }
  29. function parseKeyValuePairs(text) {
  30. var lines = text.split('\n');
  31. var data = {};
  32. for (let i = 0; i < lines.length; i++) {
  33. if (lines[i] == '')
  34. continue;
  35. // can't just .split() because the value might contain = characters
  36. let eq = lines[i].indexOf('=');
  37. if (eq != -1) {
  38. let [key, value] = [lines[i].substring(0, eq),
  39. lines[i].substring(eq + 1)];
  40. if (key && value)
  41. data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
  42. }
  43. }
  44. return data;
  45. }
  46. function parseKeyValuePairsFromFile(file) {
  47. var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
  48. createInstance(Ci.nsIFileInputStream);
  49. fstream.init(file, -1, 0, 0);
  50. var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
  51. createInstance(Ci.nsIConverterInputStream);
  52. is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  53. var str = {};
  54. var contents = '';
  55. while (is.readString(4096, str) != 0) {
  56. contents += str.value;
  57. }
  58. is.close();
  59. fstream.close();
  60. return parseKeyValuePairs(contents);
  61. }
  62. function getTestPlugin(pluginName) {
  63. var ph = Cc["@mozilla.org/plugin/host;1"]
  64. .getService(Ci.nsIPluginHost);
  65. var tags = ph.getPluginTags();
  66. var name = pluginName || "Test Plug-in";
  67. for (var tag of tags) {
  68. if (tag.name == name) {
  69. return tag;
  70. }
  71. }
  72. return null;
  73. }
  74. SpecialPowersObserverAPI.prototype = {
  75. _observe: function(aSubject, aTopic, aData) {
  76. function addDumpIDToMessage(propertyName) {
  77. try {
  78. var id = aSubject.getPropertyAsAString(propertyName);
  79. } catch(ex) {
  80. var id = null;
  81. }
  82. if (id) {
  83. message.dumpIDs.push({id: id, extension: "dmp"});
  84. message.dumpIDs.push({id: id, extension: "extra"});
  85. }
  86. }
  87. switch(aTopic) {
  88. case "plugin-crashed":
  89. case "ipc:content-shutdown":
  90. var message = { type: "crash-observed", dumpIDs: [] };
  91. aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
  92. if (aTopic == "plugin-crashed") {
  93. addDumpIDToMessage("pluginDumpID");
  94. addDumpIDToMessage("browserDumpID");
  95. let pluginID = aSubject.getPropertyAsAString("pluginDumpID");
  96. let extra = this._getExtraData(pluginID);
  97. if (extra && ("additional_minidumps" in extra)) {
  98. let dumpNames = extra.additional_minidumps.split(',');
  99. for (let name of dumpNames) {
  100. message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"});
  101. }
  102. }
  103. } else { // ipc:content-shutdown
  104. addDumpIDToMessage("dumpID");
  105. }
  106. this._sendAsyncMessage("SPProcessCrashService", message);
  107. break;
  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. _getExtraData: function(dumpId) {
  118. let extraFile = this._getCrashDumpDir().clone();
  119. extraFile.append(dumpId + ".extra");
  120. if (!extraFile.exists()) {
  121. return null;
  122. }
  123. return parseKeyValuePairsFromFile(extraFile);
  124. },
  125. _deleteCrashDumpFiles: function(aFilenames) {
  126. var crashDumpDir = this._getCrashDumpDir();
  127. if (!crashDumpDir.exists()) {
  128. return false;
  129. }
  130. var success = aFilenames.length != 0;
  131. aFilenames.forEach(function(crashFilename) {
  132. var file = crashDumpDir.clone();
  133. file.append(crashFilename);
  134. if (file.exists()) {
  135. file.remove(false);
  136. } else {
  137. success = false;
  138. }
  139. });
  140. return success;
  141. },
  142. _findCrashDumpFiles: function(aToIgnore) {
  143. var crashDumpDir = this._getCrashDumpDir();
  144. var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
  145. if (!entries) {
  146. return [];
  147. }
  148. var crashDumpFiles = [];
  149. while (entries.hasMoreElements()) {
  150. var file = entries.getNext().QueryInterface(Ci.nsIFile);
  151. var path = String(file.path);
  152. if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
  153. crashDumpFiles.push(path);
  154. }
  155. }
  156. return crashDumpFiles.concat();
  157. },
  158. _getURI: function (url) {
  159. return Services.io.newURI(url, null, null);
  160. },
  161. _readUrlAsString: function(aUrl) {
  162. // Fetch script content as we can't use scriptloader's loadSubScript
  163. // to evaluate http:// urls...
  164. var scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
  165. .getService(Ci.nsIScriptableInputStream);
  166. var channel = NetUtil.newChannel({
  167. uri: aUrl,
  168. loadUsingSystemPrincipal: true
  169. });
  170. var input = channel.open2();
  171. scriptableStream.init(input);
  172. var str;
  173. var buffer = [];
  174. while ((str = scriptableStream.read(4096))) {
  175. buffer.push(str);
  176. }
  177. var output = buffer.join("");
  178. scriptableStream.close();
  179. input.close();
  180. var status;
  181. try {
  182. channel.QueryInterface(Ci.nsIHttpChannel);
  183. status = channel.responseStatus;
  184. } catch(e) {
  185. /* The channel is not a nsIHttpCHannel, but that's fine */
  186. dump("-*- _readUrlAsString: Got an error while fetching " +
  187. "chrome script '" + aUrl + "': (" + e.name + ") " + e.message + ". " +
  188. "Ignoring.\n");
  189. }
  190. if (status == 404) {
  191. throw new SpecialPowersError(
  192. "Error while executing chrome script '" + aUrl + "':\n" +
  193. "The script doesn't exists. Ensure you have registered it in " +
  194. "'support-files' in your mochitest.ini.");
  195. }
  196. return output;
  197. },
  198. _sendReply: function(aMessage, aReplyName, aReplyMsg) {
  199. let mm = aMessage.target
  200. .QueryInterface(Ci.nsIFrameLoaderOwner)
  201. .frameLoader
  202. .messageManager;
  203. mm.sendAsyncMessage(aReplyName, aReplyMsg);
  204. },
  205. _notifyCategoryAndObservers: function(subject, topic, data) {
  206. const serviceMarker = "service,";
  207. // First create observers from the category manager.
  208. let cm =
  209. Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
  210. let enumerator = cm.enumerateCategory(topic);
  211. let observers = [];
  212. while (enumerator.hasMoreElements()) {
  213. let entry =
  214. enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
  215. let contractID = cm.getCategoryEntry(topic, entry);
  216. let factoryFunction;
  217. if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
  218. contractID = contractID.substring(serviceMarker.length);
  219. factoryFunction = "getService";
  220. }
  221. else {
  222. factoryFunction = "createInstance";
  223. }
  224. try {
  225. let handler = Cc[contractID][factoryFunction]();
  226. if (handler) {
  227. let observer = handler.QueryInterface(Ci.nsIObserver);
  228. observers.push(observer);
  229. }
  230. } catch(e) { }
  231. }
  232. // Next enumerate the registered observers.
  233. enumerator = Services.obs.enumerateObservers(topic);
  234. while (enumerator.hasMoreElements()) {
  235. try {
  236. let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver);
  237. if (observers.indexOf(observer) == -1) {
  238. observers.push(observer);
  239. }
  240. } catch (e) { }
  241. }
  242. observers.forEach(function (observer) {
  243. try {
  244. observer.observe(subject, topic, data);
  245. } catch(e) { }
  246. });
  247. },
  248. /**
  249. * messageManager callback function
  250. * This will get requests from our API in the window and process them in chrome for it
  251. **/
  252. _receiveMessageAPI: function(aMessage) {
  253. // We explicitly return values in the below code so that this function
  254. // doesn't trigger a flurry of warnings about "does not always return
  255. // a value".
  256. switch(aMessage.name) {
  257. case "SPPrefService": {
  258. let prefs = Services.prefs;
  259. let prefType = aMessage.json.prefType.toUpperCase();
  260. let prefName = aMessage.json.prefName;
  261. let prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null;
  262. if (aMessage.json.op == "get") {
  263. if (!prefName || !prefType)
  264. throw new SpecialPowersError("Invalid parameters for get in SPPrefService");
  265. // return null if the pref doesn't exist
  266. if (prefs.getPrefType(prefName) == prefs.PREF_INVALID)
  267. return null;
  268. } else if (aMessage.json.op == "set") {
  269. if (!prefName || !prefType || prefValue === null)
  270. throw new SpecialPowersError("Invalid parameters for set in SPPrefService");
  271. } else if (aMessage.json.op == "clear") {
  272. if (!prefName)
  273. throw new SpecialPowersError("Invalid parameters for clear in SPPrefService");
  274. } else {
  275. throw new SpecialPowersError("Invalid operation for SPPrefService");
  276. }
  277. // Now we make the call
  278. switch(prefType) {
  279. case "BOOL":
  280. if (aMessage.json.op == "get")
  281. return(prefs.getBoolPref(prefName));
  282. else
  283. return(prefs.setBoolPref(prefName, prefValue));
  284. case "INT":
  285. if (aMessage.json.op == "get")
  286. return(prefs.getIntPref(prefName));
  287. else
  288. return(prefs.setIntPref(prefName, prefValue));
  289. case "CHAR":
  290. if (aMessage.json.op == "get")
  291. return(prefs.getCharPref(prefName));
  292. else
  293. return(prefs.setCharPref(prefName, prefValue));
  294. case "COMPLEX":
  295. if (aMessage.json.op == "get")
  296. return(prefs.getComplexValue(prefName, prefValue[0]));
  297. else
  298. return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1]));
  299. case "":
  300. if (aMessage.json.op == "clear") {
  301. prefs.clearUserPref(prefName);
  302. return undefined;
  303. }
  304. }
  305. return undefined; // See comment at the beginning of this function.
  306. }
  307. case "SPProcessCrashService": {
  308. switch (aMessage.json.op) {
  309. case "register-observer":
  310. this._addProcessCrashObservers();
  311. break;
  312. case "unregister-observer":
  313. this._removeProcessCrashObservers();
  314. break;
  315. case "delete-crash-dump-files":
  316. return this._deleteCrashDumpFiles(aMessage.json.filenames);
  317. case "find-crash-dump-files":
  318. return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore);
  319. default:
  320. throw new SpecialPowersError("Invalid operation for SPProcessCrashService");
  321. }
  322. return undefined; // See comment at the beginning of this function.
  323. }
  324. case "SPPermissionManager": {
  325. let msg = aMessage.json;
  326. let principal = msg.principal;
  327. switch (msg.op) {
  328. case "add":
  329. Services.perms.addFromPrincipal(principal, msg.type, msg.permission, msg.expireType, msg.expireTime);
  330. break;
  331. case "remove":
  332. Services.perms.removeFromPrincipal(principal, msg.type);
  333. break;
  334. case "has":
  335. let hasPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type);
  336. return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
  337. case "test":
  338. let testPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type, msg.value);
  339. return testPerm == msg.value;
  340. default:
  341. throw new SpecialPowersError(
  342. "Invalid operation for SPPermissionManager");
  343. }
  344. return undefined; // See comment at the beginning of this function.
  345. }
  346. case "SPSetTestPluginEnabledState": {
  347. var plugin = getTestPlugin(aMessage.data.pluginName);
  348. if (!plugin) {
  349. return undefined;
  350. }
  351. var oldEnabledState = plugin.enabledState;
  352. plugin.enabledState = aMessage.data.newEnabledState;
  353. return oldEnabledState;
  354. }
  355. case "SPObserverService": {
  356. let topic = aMessage.json.observerTopic;
  357. switch (aMessage.json.op) {
  358. case "notify":
  359. let data = aMessage.json.observerData
  360. Services.obs.notifyObservers(null, topic, data);
  361. break;
  362. case "add":
  363. this._registerObservers._self = this;
  364. this._registerObservers._add(topic);
  365. break;
  366. default:
  367. throw new SpecialPowersError("Invalid operation for SPObserverervice");
  368. }
  369. return undefined; // See comment at the beginning of this function.
  370. }
  371. case "SPLoadChromeScript": {
  372. let id = aMessage.json.id;
  373. let jsScript;
  374. let scriptName;
  375. if (aMessage.json.url) {
  376. jsScript = this._readUrlAsString(aMessage.json.url);
  377. scriptName = aMessage.json.url;
  378. } else if (aMessage.json.function) {
  379. jsScript = aMessage.json.function.body;
  380. scriptName = aMessage.json.function.name
  381. || "<loadChromeScript anonymous function>";
  382. } else {
  383. throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
  384. }
  385. // Setup a chrome sandbox that has access to sendAsyncMessage
  386. // and addMessageListener in order to communicate with
  387. // the mochitest.
  388. let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
  389. let sb = Components.utils.Sandbox(systemPrincipal);
  390. let mm = aMessage.target
  391. .QueryInterface(Ci.nsIFrameLoaderOwner)
  392. .frameLoader
  393. .messageManager;
  394. sb.sendAsyncMessage = (name, message) => {
  395. mm.sendAsyncMessage("SPChromeScriptMessage",
  396. { id: id, name: name, message: message });
  397. };
  398. sb.addMessageListener = (name, listener) => {
  399. this._chromeScriptListeners.push({ id: id, name: name, listener: listener });
  400. };
  401. sb.browserElement = aMessage.target;
  402. // Also expose assertion functions
  403. let reporter = function (err, message, stack) {
  404. // Pipe assertions back to parent process
  405. mm.sendAsyncMessage("SPChromeScriptAssert",
  406. { id, name: scriptName, err, message,
  407. stack });
  408. };
  409. Object.defineProperty(sb, "assert", {
  410. get: function () {
  411. let scope = Components.utils.createObjectIn(sb);
  412. Services.scriptloader.loadSubScript("chrome://specialpowers/content/Assert.jsm",
  413. scope);
  414. let assert = new scope.Assert(reporter);
  415. delete sb.assert;
  416. return sb.assert = assert;
  417. },
  418. configurable: true
  419. });
  420. // Evaluate the chrome script
  421. try {
  422. Components.utils.evalInSandbox(jsScript, sb, "1.8", scriptName, 1);
  423. } catch(e) {
  424. throw new SpecialPowersError(
  425. "Error while executing chrome script '" + scriptName + "':\n" +
  426. e + "\n" +
  427. e.fileName + ":" + e.lineNumber);
  428. }
  429. return undefined; // See comment at the beginning of this function.
  430. }
  431. case "SPChromeScriptMessage": {
  432. let id = aMessage.json.id;
  433. let name = aMessage.json.name;
  434. let message = aMessage.json.message;
  435. return this._chromeScriptListeners
  436. .filter(o => (o.name == name && o.id == id))
  437. .map(o => o.listener(message));
  438. }
  439. case "SPImportInMainProcess": {
  440. var message = { hadError: false, errorMessage: null };
  441. try {
  442. Components.utils.import(aMessage.data);
  443. } catch (e) {
  444. message.hadError = true;
  445. message.errorMessage = e.toString();
  446. }
  447. return message;
  448. }
  449. case "SPCleanUpSTSData": {
  450. let origin = aMessage.data.origin;
  451. let flags = aMessage.data.flags;
  452. let uri = Services.io.newURI(origin, null, null);
  453. let sss = Cc["@mozilla.org/ssservice;1"].
  454. getService(Ci.nsISiteSecurityService);
  455. sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
  456. return undefined;
  457. }
  458. case "SPLoadExtension": {
  459. let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
  460. let id = aMessage.data.id;
  461. let ext = aMessage.data.ext;
  462. let extension = Extension.generate(ext);
  463. let resultListener = (...args) => {
  464. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testResult", args});
  465. };
  466. let messageListener = (...args) => {
  467. args.shift();
  468. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testMessage", args});
  469. };
  470. // Register pass/fail handlers.
  471. extension.on("test-result", resultListener);
  472. extension.on("test-eq", resultListener);
  473. extension.on("test-log", resultListener);
  474. extension.on("test-done", resultListener);
  475. extension.on("test-message", messageListener);
  476. this._extensions.set(id, extension);
  477. return undefined;
  478. }
  479. case "SPStartupExtension": {
  480. let {ExtensionData, Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
  481. let id = aMessage.data.id;
  482. let extension = this._extensions.get(id);
  483. let startupListener = (msg, ext) => {
  484. if (ext == extension) {
  485. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionSetId", args: [extension.id]});
  486. Management.off("startup", startupListener);
  487. }
  488. };
  489. Management.on("startup", startupListener);
  490. // Make sure the extension passes the packaging checks when
  491. // they're run on a bare archive rather than a running instance,
  492. // as the add-on manager runs them.
  493. let extensionData = new ExtensionData(extension.rootURI);
  494. extensionData.readManifest().then(
  495. () => {
  496. return extensionData.initAllLocales().then(() => {
  497. if (extensionData.errors.length) {
  498. return Promise.reject("Extension contains packaging errors");
  499. }
  500. });
  501. },
  502. () => {
  503. // readManifest() will throw if we're loading an embedded
  504. // extension, so don't worry about locale errors in that
  505. // case.
  506. }
  507. ).then(() => {
  508. return extension.startup();
  509. }).then(() => {
  510. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []});
  511. }).catch(e => {
  512. dump(`Extension startup failed: ${e}\n${e.stack}`);
  513. Management.off("startup", startupListener);
  514. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionFailed", args: []});
  515. });
  516. return undefined;
  517. }
  518. case "SPExtensionMessage": {
  519. let id = aMessage.data.id;
  520. let extension = this._extensions.get(id);
  521. extension.testMessage(...aMessage.data.args);
  522. return undefined;
  523. }
  524. case "SPUnloadExtension": {
  525. let id = aMessage.data.id;
  526. let extension = this._extensions.get(id);
  527. this._extensions.delete(id);
  528. extension.shutdown();
  529. this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionUnloaded", args: []});
  530. return undefined;
  531. }
  532. case "SPClearAppPrivateData": {
  533. let appId = aMessage.data.appId;
  534. let browserOnly = aMessage.data.browserOnly;
  535. let attributes = { appId: appId };
  536. if (browserOnly) {
  537. attributes.inIsolatedMozBrowser = true;
  538. }
  539. this._notifyCategoryAndObservers(null,
  540. "clear-origin-attributes-data",
  541. JSON.stringify(attributes));
  542. let subject = {
  543. appId: appId,
  544. browserOnly: browserOnly,
  545. QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
  546. };
  547. this._notifyCategoryAndObservers(subject, "webapps-clear-data", null);
  548. return undefined;
  549. }
  550. default:
  551. throw new SpecialPowersError("Unrecognized Special Powers API");
  552. }
  553. // We throw an exception before reaching this explicit return because
  554. // we should never be arriving here anyway.
  555. throw new SpecialPowersError("Unreached code");
  556. return undefined;
  557. }
  558. };