implementation.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /*
  2. * This file is provided by the addon-developer-support repository at
  3. * https://github.com/thundernest/addon-developer-support
  4. *
  5. * Version: 1.20
  6. * - fix long delay before customize window opens
  7. * - fix non working removal of palette items
  8. *
  9. * Version: 1.19
  10. * - add support for ToolbarPalette
  11. *
  12. * Version: 1.18
  13. * - execute shutdown script also during global app shutdown (fixed)
  14. *
  15. * Version: 1.17
  16. * - execute shutdown script also during global app shutdown
  17. *
  18. * Version: 1.16
  19. * - support for persist
  20. *
  21. * Version: 1.15
  22. * - make (undocumented) startup() async
  23. *
  24. * Version: 1.14
  25. * - support resource urls
  26. *
  27. * Version: 1.12
  28. * - no longer allow to enforce custom "namespace"
  29. * - no longer call it namespace but uniqueRandomID / scopeName
  30. * - expose special objects as the global WL object
  31. * - autoremove injected elements after onUnload has ben executed
  32. *
  33. * Version: 1.9
  34. * - automatically remove all entries added by injectElements
  35. *
  36. * Version: 1.8
  37. * - add injectElements
  38. *
  39. * Version: 1.7
  40. * - add injectCSS
  41. * - add optional enforced namespace
  42. *
  43. * Version: 1.6
  44. * - added mutation observer to be able to inject into browser elements
  45. * - use larger icons as fallback
  46. *
  47. * Author: John Bieling (john@thunderbird.net)
  48. *
  49. * This Source Code Form is subject to the terms of the Mozilla Public
  50. * License, v. 2.0. If a copy of the MPL was not distributed with this
  51. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  52. */
  53. // Import some things we need.
  54. var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
  55. var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
  56. var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
  57. var WindowListener = class extends ExtensionCommon.ExtensionAPI {
  58. getAPI(context) {
  59. // track if this is the background/main context
  60. this.isBackgroundContext = (context.viewType == "background");
  61. this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
  62. this.menu_addonsManager_id ="addonsManager";
  63. this.menu_addonsManager_prefs_id = "addonsManager_prefs_revived";
  64. this.menu_addonPrefs_id = "addonPrefs_revived";
  65. this.registeredWindows = {};
  66. this.pathToStartupScript = null;
  67. this.pathToShutdownScript = null;
  68. this.pathToOptionsPage = null;
  69. this.chromeHandle = null;
  70. this.chromeData = null;
  71. this.resourceData = null;
  72. this.openWindows = [];
  73. const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
  74. const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
  75. let self = this;
  76. return {
  77. WindowListener: {
  78. registerOptionsPage(optionsUrl) {
  79. self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
  80. ? optionsUrl
  81. : context.extension.rootURI.resolve(optionsUrl);
  82. },
  83. registerDefaultPrefs(defaultUrl) {
  84. let url = context.extension.rootURI.resolve(defaultUrl);
  85. let prefsObj = {};
  86. prefsObj.Services = ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
  87. prefsObj.pref = function(aName, aDefault) {
  88. let defaults = Services.prefs.getDefaultBranch("");
  89. switch (typeof aDefault) {
  90. case "string":
  91. return defaults.setCharPref(aName, aDefault);
  92. case "number":
  93. return defaults.setIntPref(aName, aDefault);
  94. case "boolean":
  95. return defaults.setBoolPref(aName, aDefault);
  96. default:
  97. throw new Error("Preference <" + aName + "> has an unsupported type <" + typeof aDefault + ">. Allowed are string, number and boolean.");
  98. }
  99. }
  100. Services.scriptloader.loadSubScript(url, prefsObj, "UTF-8");
  101. },
  102. registerChromeUrl(data) {
  103. if (!self.isBackgroundContext)
  104. throw new Error("The WindowListener API may only be called from the background page.");
  105. let chromeData = [];
  106. let resourceData = [];
  107. for (let entry of data) {
  108. if (entry[0] == "resource") resourceData.push(entry);
  109. else chromeData.push(entry)
  110. }
  111. if (chromeData.length > 0) {
  112. const manifestURI = Services.io.newURI(
  113. "manifest.json",
  114. null,
  115. context.extension.rootURI
  116. );
  117. self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
  118. }
  119. for (let res of resourceData) {
  120. // [ "resource", "shortname" , "path" ]
  121. let uri = Services.io.newURI(
  122. res[2],
  123. null,
  124. context.extension.rootURI
  125. );
  126. resProto.setSubstitutionWithFlags(
  127. res[1],
  128. uri,
  129. resProto.ALLOW_CONTENT_ACCESS
  130. );
  131. }
  132. self.chromeData = chromeData;
  133. self.resourceData = resourceData;
  134. },
  135. registerWindow(windowHref, jsFile) {
  136. if (!self.isBackgroundContext)
  137. throw new Error("The WindowListener API may only be called from the background page.");
  138. if (!self.registeredWindows.hasOwnProperty(windowHref)) {
  139. // path to JS file can either be chrome:// URL or a relative URL
  140. let path = jsFile.startsWith("chrome://")
  141. ? jsFile
  142. : context.extension.rootURI.resolve(jsFile)
  143. self.registeredWindows[windowHref] = path;
  144. } else {
  145. console.error("Window <" +windowHref + "> has already been registered");
  146. }
  147. },
  148. registerStartupScript(aPath) {
  149. if (!self.isBackgroundContext)
  150. throw new Error("The WindowListener API may only be called from the background page.");
  151. self.pathToStartupScript = aPath.startsWith("chrome://")
  152. ? aPath
  153. : context.extension.rootURI.resolve(aPath);
  154. },
  155. registerShutdownScript(aPath) {
  156. if (!self.isBackgroundContext)
  157. throw new Error("The WindowListener API may only be called from the background page.");
  158. self.pathToShutdownScript = aPath.startsWith("chrome://")
  159. ? aPath
  160. : context.extension.rootURI.resolve(aPath);
  161. },
  162. async startListening() {
  163. // async sleep function using Promise
  164. async function sleep(delay) {
  165. let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
  166. return new Promise(function(resolve, reject) {
  167. let event = {
  168. notify: function(timer) {
  169. resolve();
  170. }
  171. }
  172. timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
  173. });
  174. };
  175. if (!self.isBackgroundContext)
  176. throw new Error("The WindowListener API may only be called from the background page.");
  177. // load the registered startup script, if one has been registered
  178. // (mail3:pane may not have been fully loaded yet)
  179. if (self.pathToStartupScript) {
  180. let startupJS = {};
  181. startupJS.WL = {}
  182. startupJS.WL.extension = self.extension;
  183. startupJS.WL.messenger = Array.from(self.extension.views).find(
  184. view => view.viewType === "background").xulBrowser.contentWindow
  185. .wrappedJSObject.browser;
  186. try {
  187. if (self.pathToStartupScript) {
  188. Services.scriptloader.loadSubScript(self.pathToStartupScript, startupJS, "UTF-8");
  189. // delay startup until startup has been finished
  190. console.log("Waiting for async startup() in <" + self.pathToStartupScript + "> to finish.");
  191. if (startupJS.startup) {
  192. await startupJS.startup();
  193. console.log("startup() in <" + self.pathToStartupScript + "> finished");
  194. } else {
  195. console.log("No startup() in <" + self.pathToStartupScript + "> found.");
  196. }
  197. }
  198. } catch (e) {
  199. Components.utils.reportError(e)
  200. }
  201. }
  202. let urls = Object.keys(self.registeredWindows);
  203. if (urls.length > 0) {
  204. // Before registering the window listener, check which windows are already open
  205. self.openWindows = [];
  206. for (let window of Services.wm.getEnumerator(null)) {
  207. self.openWindows.push(window);
  208. }
  209. // Register window listener for all pre-registered windows
  210. ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
  211. // React on all windows and manually reduce to the registered
  212. // windows, so we can do special actions when the main
  213. // messenger window is opened.
  214. //chromeURLs: Object.keys(self.registeredWindows),
  215. async onLoadWindow(window) {
  216. // Create add-on scope
  217. window[self.uniqueRandomID] = {};
  218. // Special action #1: If this is the main messenger window
  219. if (window.location.href == "chrome://messenger/content/messenger.xul" ||
  220. window.location.href == "chrome://messenger/content/messenger.xhtml") {
  221. if (self.pathToOptionsPage) {
  222. try {
  223. // add the add-on options menu if needed
  224. if (!window.document.getElementById(self.menu_addonsManager_prefs_id)) {
  225. let addonprefs = window.MozXULElement.parseXULToFragment(`
  226. <menu id="${self.menu_addonsManager_prefs_id}" label="&addonPrefs.label;">
  227. <menupopup id="${self.menu_addonPrefs_id}">
  228. </menupopup>
  229. </menu>
  230. `, ["chrome://messenger/locale/messenger.dtd"]);
  231. let element_addonsManager = window.document.getElementById(self.menu_addonsManager_id);
  232. element_addonsManager.parentNode.insertBefore(addonprefs, element_addonsManager.nextSibling);
  233. }
  234. // add the options entry
  235. let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
  236. let id = self.menu_addonPrefs_id + "_" + self.uniqueRandomID;
  237. // Get the best size of the icon (16px or bigger)
  238. let iconSizes = Object.keys(self.extension.manifest.icons);
  239. iconSizes.sort((a,b)=>a-b);
  240. let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
  241. let icon = bestSize ? self.extension.manifest.icons[bestSize] : "";
  242. let name = self.extension.manifest.name;
  243. let entry = window.MozXULElement.parseXULToFragment(
  244. `<menuitem class="menuitem-iconic" id="${id}" image="${icon}" label="${name}" />`);
  245. element_addonPrefs.appendChild(entry);
  246. window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions")});
  247. } catch (e) {
  248. Components.utils.reportError(e)
  249. }
  250. }
  251. }
  252. // Special action #2: If this page contains browser elements
  253. let browserElements = window.document.getElementsByTagName("browser");
  254. if (browserElements.length > 0) {
  255. //register a MutationObserver
  256. window[self.uniqueRandomID]._mObserver = new window.MutationObserver(function(mutations) {
  257. mutations.forEach(async function(mutation) {
  258. if (mutation.attributeName == "src" && self.registeredWindows.hasOwnProperty(mutation.target.getAttribute("src"))) {
  259. // When the MutationObserver callsback, the window is still showing "about:black" and it is going
  260. // to unload and then load the new page. Any eventListener attached to the window will be removed
  261. // so we cannot listen for the load event. We have to poll manually to learn when loading has finished.
  262. // On my system it takes 70ms.
  263. let loaded = false;
  264. for (let i=0; i < 100 && !loaded; i++) {
  265. await sleep(100);
  266. let targetWindow = mutation.target.contentWindow.wrappedJSObject;
  267. if (targetWindow && targetWindow.location.href == mutation.target.getAttribute("src") && targetWindow.document.readyState == "complete") {
  268. loaded = true;
  269. break;
  270. }
  271. }
  272. if (loaded) {
  273. let targetWindow = mutation.target.contentWindow.wrappedJSObject;
  274. // Create add-on scope
  275. targetWindow[self.uniqueRandomID] = {};
  276. // Inject with isAddonActivation = false
  277. self._loadIntoWindow(targetWindow, false);
  278. }
  279. }
  280. });
  281. });
  282. for (let element of browserElements) {
  283. if (self.registeredWindows.hasOwnProperty(element.getAttribute("src"))) {
  284. let targetWindow = element.contentWindow.wrappedJSObject;
  285. // Create add-on scope
  286. targetWindow[self.uniqueRandomID] = {};
  287. // Inject with isAddonActivation = true
  288. self._loadIntoWindow(targetWindow, true);
  289. } else {
  290. // Window/Browser is not yet fully loaded, postpone injection via MutationObserver
  291. window[self.uniqueRandomID]._mObserver.observe(element, { attributes: true, childList: false, characterData: false });
  292. }
  293. }
  294. }
  295. // Load JS into window
  296. self._loadIntoWindow(window, self.openWindows.includes(window));
  297. },
  298. onUnloadWindow(window) {
  299. // Remove JS from window, window is being closed, addon is not shut down
  300. self._unloadFromWindow(window, false);
  301. }
  302. });
  303. } else {
  304. console.error("Failed to start listening, no windows registered");
  305. }
  306. },
  307. }
  308. };
  309. }
  310. _loadIntoWindow(window, isAddonActivation) {
  311. if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
  312. try {
  313. let uniqueRandomID = this.uniqueRandomID;
  314. // Add reference to window to add-on scope
  315. window[this.uniqueRandomID].window = window;
  316. window[this.uniqueRandomID].document = window.document;
  317. // Keep track of toolbarpalettes we are injecting into
  318. window[this.uniqueRandomID]._toolbarpalettes = {};
  319. //Create WLDATA object
  320. window[this.uniqueRandomID].WL = {};
  321. window[this.uniqueRandomID].WL.scopeName = this.uniqueRandomID;
  322. // Add helper function to inject CSS to WLDATA object
  323. window[this.uniqueRandomID].WL.injectCSS = function (cssFile) {
  324. let element;
  325. let v = parseInt(Services.appinfo.version.split(".").shift());
  326. // using createElementNS in TB78 delays the insert process and hides any security violation errors
  327. if (v > 68) {
  328. element = window.document.createElement("link");
  329. } else {
  330. let ns = window.document.documentElement.lookupNamespaceURI("html");
  331. element = window.document.createElementNS(ns, "link");
  332. }
  333. element.setAttribute("wlapi_autoinjected", uniqueRandomID);
  334. element.setAttribute("rel", "stylesheet");
  335. element.setAttribute("href", cssFile);
  336. return window.document.documentElement.appendChild(element);
  337. }
  338. // Add helper function to inject XUL to WLDATA object
  339. window[this.uniqueRandomID].WL.injectElements = function (xulString, dtdFiles = [], debug = false) {
  340. let toolbarsToResolve = [];
  341. function checkElements(stringOfIDs) {
  342. let arrayOfIDs = stringOfIDs.split(",").map(e => e.trim());
  343. for (let id of arrayOfIDs) {
  344. let element = window.document.getElementById(id);
  345. if (element) {
  346. return element;
  347. }
  348. }
  349. return null;
  350. }
  351. function injectChildren(elements, container) {
  352. if (debug) console.log(elements);
  353. for (let i = 0; i < elements.length; i++) {
  354. // take care of persists
  355. const uri = window.document.documentURI;
  356. for (const persistentNode of elements[i].querySelectorAll("[persist]")) {
  357. for (const persistentAttribute of persistentNode.getAttribute("persist").trim().split(" ")) {
  358. if (Services.xulStore.hasValue(uri, persistentNode.id, persistentAttribute)) {
  359. persistentNode.setAttribute(
  360. persistentAttribute,
  361. Services.xulStore.getValue(uri, persistentNode.id, persistentAttribute)
  362. );
  363. }
  364. }
  365. }
  366. if (elements[i].hasAttribute("insertafter") && checkElements(elements[i].getAttribute("insertafter"))) {
  367. let insertAfterElement = checkElements(elements[i].getAttribute("insertafter"));
  368. if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertafter " + insertAfterElement.id);
  369. if (elements[i].id && window.document.getElementById(elements[i].id)) {
  370. console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
  371. }
  372. elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
  373. insertAfterElement.parentNode.insertBefore(elements[i], insertAfterElement.nextSibling);
  374. } else if (elements[i].hasAttribute("insertbefore") && checkElements(elements[i].getAttribute("insertbefore"))) {
  375. let insertBeforeElement = checkElements(elements[i].getAttribute("insertbefore"));
  376. if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertbefore " + insertBeforeElement.id);
  377. if (elements[i].id && window.document.getElementById(elements[i].id)) {
  378. console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
  379. }
  380. elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
  381. insertBeforeElement.parentNode.insertBefore(elements[i], insertBeforeElement);
  382. } else if (elements[i].id && window.document.getElementById(elements[i].id)) {
  383. // existing container match, dive into recursivly
  384. if (debug) console.log(elements[i].tagName + "#" + elements[i].id + " is an existing container, injecting into " + elements[i].id);
  385. injectChildren(Array.from(elements[i].children), window.document.getElementById(elements[i].id));
  386. } else if (elements[i].localName === "toolbarpalette") {
  387. // These vanish from the document but still exist via the palette property
  388. if (debug) console.log(elements[i].id + " is a toolbarpalette");
  389. let boxes = [...window.document.getElementsByTagName("toolbox")];
  390. let box = boxes.find(box => box.palette && box.palette.id === elements[i].id);
  391. let palette = box ? box.palette : null;
  392. if (!palette) {
  393. if (debug) console.log(`The palette for ${elements[i].id} could not be found, deferring to later`);
  394. continue;
  395. }
  396. if (debug) console.log(`The toolbox for ${elements[i].id} is ${box.id}`);
  397. toolbarsToResolve.push(...box.querySelectorAll("toolbar"));
  398. toolbarsToResolve.push(...window.document.querySelectorAll(`toolbar[toolboxid="${box.id}"]`));
  399. for (let child of elements[i].children) {
  400. child.setAttribute("wlapi_autoinjected", uniqueRandomID);
  401. }
  402. window[uniqueRandomID]._toolbarpalettes[palette.id] = palette;
  403. injectChildren(Array.from(elements[i].children), palette);
  404. } else {
  405. // append element to the current container
  406. if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": append to " + container.id);
  407. elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
  408. container.appendChild(elements[i]);
  409. }
  410. }
  411. }
  412. if (debug) console.log ("Injecting into root document:");
  413. injectChildren(Array.from(window.MozXULElement.parseXULToFragment(xulString, dtdFiles).children), window.document.documentElement);
  414. for (let bar of toolbarsToResolve) {
  415. let currentset = Services.xulStore.getValue(
  416. window.location,
  417. bar.id,
  418. "currentset"
  419. );
  420. if (currentset) {
  421. bar.currentSet = currentset;
  422. } else if (bar.getAttribute("defaultset")) {
  423. bar.currentSet = bar.getAttribute("defaultset");
  424. }
  425. }
  426. }
  427. // Add extension object to WLDATA object
  428. window[this.uniqueRandomID].WL.extension = this.extension;
  429. // Add messenger object to WLDATA object
  430. window[this.uniqueRandomID].WL.messenger = Array.from(this.extension.views).find(
  431. view => view.viewType === "background").xulBrowser.contentWindow
  432. .wrappedJSObject.browser;
  433. // Load script into add-on scope
  434. Services.scriptloader.loadSubScript(this.registeredWindows[window.location.href], window[this.uniqueRandomID], "UTF-8");
  435. window[this.uniqueRandomID].onLoad(isAddonActivation);
  436. } catch (e) {
  437. Components.utils.reportError(e)
  438. }
  439. }
  440. }
  441. _unloadFromWindow(window, isAddonDeactivation) {
  442. // unload any contained browser elements
  443. if (window.hasOwnProperty(this.uniqueRandomID) && window[this.uniqueRandomID].hasOwnProperty("_mObserver")) {
  444. window[this.uniqueRandomID]._mObserver.disconnect();
  445. let browserElements = window.document.getElementsByTagName("browser");
  446. for (let element of browserElements) {
  447. this._unloadFromWindow(element.contentWindow.wrappedJSObject, isAddonDeactivation);
  448. }
  449. }
  450. if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
  451. // Remove this window from the list of open windows
  452. this.openWindows = this.openWindows.filter(e => (e != window));
  453. if (window[this.uniqueRandomID].onUnload) {
  454. try {
  455. // Call onUnload()
  456. window[this.uniqueRandomID].onUnload(isAddonDeactivation);
  457. } catch (e) {
  458. Components.utils.reportError(e)
  459. }
  460. }
  461. // Remove all auto injected objects
  462. let elements = Array.from(window.document.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
  463. for (let element of elements) {
  464. element.remove();
  465. }
  466. // Remove all autoinjected toolbarpalette items
  467. for (const palette of Object.values(window[this.uniqueRandomID]._toolbarpalettes)) {
  468. let elements = Array.from(palette.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
  469. for (let element of elements) {
  470. element.remove();
  471. }
  472. }
  473. }
  474. // Remove add-on scope, if it exists
  475. if (window.hasOwnProperty(this.uniqueRandomID)) {
  476. delete window[this.uniqueRandomID];
  477. }
  478. }
  479. onShutdown(isAppShutdown) {
  480. // Unload from all still open windows
  481. let urls = Object.keys(this.registeredWindows);
  482. if (urls.length > 0) {
  483. for (let window of Services.wm.getEnumerator(null)) {
  484. //remove our entry in the add-on options menu
  485. if (
  486. this.pathToOptionsPage &&
  487. (window.location.href == "chrome://messenger/content/messenger.xul" ||
  488. window.location.href == "chrome://messenger/content/messenger.xhtml")) {
  489. let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
  490. window.document.getElementById(id).remove();
  491. //do we have to remove the entire add-on options menu?
  492. let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
  493. if (element_addonPrefs.children.length == 0) {
  494. window.document.getElementById(this.menu_addonsManager_prefs_id).remove();
  495. }
  496. }
  497. // if it is app shutdown, it is not just an add-on deactivation
  498. this._unloadFromWindow(window, !isAppShutdown);
  499. }
  500. // Stop listening for new windows.
  501. ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
  502. }
  503. // Load registered shutdown script
  504. let shutdownJS = {};
  505. shutdownJS.extension = this.extension;
  506. try {
  507. if (this.pathToShutdownScript) Services.scriptloader.loadSubScript(this.pathToShutdownScript, shutdownJS, "UTF-8");
  508. } catch (e) {
  509. Components.utils.reportError(e)
  510. }
  511. // Extract all registered chrome content urls
  512. let chromeUrls = [];
  513. if (this.chromeData) {
  514. for (let chromeEntry of this.chromeData) {
  515. if (chromeEntry[0].toLowerCase().trim() == "content") {
  516. chromeUrls.push("chrome://" + chromeEntry[1] + "/");
  517. }
  518. }
  519. }
  520. // Unload JSMs of this add-on
  521. const rootURI = this.extension.rootURI.spec;
  522. for (let module of Cu.loadedModules) {
  523. if (module.startsWith(rootURI) || (module.startsWith("chrome://") && chromeUrls.find(s => module.startsWith(s)))) {
  524. console.log("Unloading: " + module);
  525. Cu.unload(module);
  526. }
  527. }
  528. // Flush all caches
  529. Services.obs.notifyObservers(null, "startupcache-invalidate");
  530. this.registeredWindows = {};
  531. if (this.resourceData) {
  532. const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
  533. for (let res of this.resourceData) {
  534. // [ "resource", "shortname" , "path" ]
  535. resProto.setSubstitution(
  536. res[1],
  537. null,
  538. );
  539. }
  540. }
  541. if (this.chromeHandle) {
  542. this.chromeHandle.destruct();
  543. this.chromeHandle = null;
  544. }
  545. }
  546. };