FrameModule.jsm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 The µBlock authors
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. // https://github.com/gorhill/uBlock/issues/800#issuecomment-146580443
  20. var EXPORTED_SYMBOLS = ['contentObserver', 'LocationChangeListener'];
  21. const {interfaces: Ci, utils: Cu} = Components;
  22. Cu.import('resource://gre/modules/Services.jsm');
  23. Cu.import('resource://gre/modules/XPCOMUtils.jsm');
  24. var hostName = Services.io.newURI(Components.stack.filename, null, null).host;
  25. // Cu.import('resource://gre/modules/Console.jsm');
  26. const getMessageManager = function (win) {
  27. let iface = win
  28. .QueryInterface(Ci.nsIInterfaceRequestor)
  29. .getInterface(Ci.nsIDocShell)
  30. .sameTypeRootTreeItem
  31. .QueryInterface(Ci.nsIDocShell)
  32. .QueryInterface(Ci.nsIInterfaceRequestor);
  33. try {
  34. return iface.getInterface(Ci.nsIContentFrameMessageManager);
  35. } catch (ex) {
  36. // This can throw. It appears `shouldLoad` can be called *after* a
  37. // tab has been closed. For example, a case where this happens all
  38. // the time (FF38):
  39. // - Open twitter.com (assuming you have an account and are logged in)
  40. // - Close twitter.com
  41. // There will be an exception raised when `shouldLoad` is called
  42. // to process a XMLHttpRequest with URL `https://twitter.com/i/jot`
  43. // fired from `https://twitter.com/`, *after* the tab is closed.
  44. // In such case, `win` is `about:blank`.
  45. }
  46. return null;
  47. };
  48. var contentObserver = {
  49. classDescription: 'content-policy for ' + hostName,
  50. classID: Components.ID('{c84283d4-9975-41b7-b1a4-f106af56b51d}'),
  51. contractID: '@' + hostName + '/content-policy;1',
  52. ACCEPT: Ci.nsIContentPolicy.ACCEPT,
  53. MAIN_FRAME: Ci.nsIContentPolicy.TYPE_DOCUMENT,
  54. contentBaseURI: 'chrome://' + hostName + '/content/js/',
  55. cpMessageName: hostName + ':shouldLoad',
  56. uniqueSandboxId: 1,
  57. modernFirefox:
  58. ((Services.appinfo.ID === '{9184b6fe-4a5c-484d-8b4b-efbfccbfb514}')
  59. && Services.vc.compare(Services.appinfo.version, '44') > 0),
  60. get componentRegistrar() {
  61. return Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
  62. },
  63. get categoryManager() {
  64. return Components.classes['@mozilla.org/categorymanager;1']
  65. .getService(Ci.nsICategoryManager);
  66. },
  67. QueryInterface: XPCOMUtils.generateQI([
  68. Ci.nsIFactory,
  69. Ci.nsIObserver,
  70. Ci.nsIContentPolicy,
  71. Ci.nsISupportsWeakReference
  72. ]),
  73. createInstance: function (outer, iid) {
  74. if (outer) {
  75. throw Components.results.NS_ERROR_NO_AGGREGATION;
  76. }
  77. return this.QueryInterface(iid);
  78. },
  79. register: function () {
  80. Services.obs.addObserver(this, 'document-element-inserted', true);
  81. if (!this.modernFirefox) {
  82. this.componentRegistrar
  83. .registerFactory(this.classID,
  84. this.classDescription,
  85. this.contractID,
  86. this);
  87. this.categoryManager
  88. .addCategoryEntry('content-policy',
  89. this.contractID,
  90. this.contractID,
  91. false,
  92. true);
  93. }
  94. },
  95. unregister: function () {
  96. Services.obs.removeObserver(this, 'document-element-inserted');
  97. if (!this.modernFirefox) {
  98. this.componentRegistrar
  99. .unregisterFactory(this.classID,
  100. this);
  101. this.categoryManager
  102. .deleteCategoryEntry('content-policy',
  103. this.contractID,
  104. false);
  105. }
  106. },
  107. // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy
  108. // https://bugzil.la/612921
  109. shouldLoad: function (type, location, origin, context) {
  110. if (!context) {
  111. return this.ACCEPT;
  112. }
  113. if (!location.schemeIs('http') && !location.schemeIs('https')) {
  114. return this.ACCEPT;
  115. }
  116. let contextWindow;
  117. if (type === this.MAIN_FRAME) {
  118. contextWindow = context.contentWindow || context;
  119. } else if (type === this.SUB_FRAME) {
  120. contextWindow = context.contentWindow;
  121. } else {
  122. contextWindow = (context.ownerDocument || context).defaultView;
  123. }
  124. // https://github.com/gorhill/uMatrix/issues/706
  125. if (!contextWindow) {
  126. return this.ACCEPT;
  127. }
  128. // The context for the toolbar popup is an iframe element here,
  129. // so check context.top instead of context
  130. if (!contextWindow.top || !contextWindow.location) {
  131. return this.ACCEPT;
  132. }
  133. let messageManager = getMessageManager(contextWindow);
  134. if (messageManager === null) {
  135. return this.ACCEPT;
  136. }
  137. let details = {
  138. rawType: type,
  139. url: location.asciiSpec
  140. };
  141. if (typeof messageManager.sendRpcMessage === 'function') {
  142. // https://bugzil.la/1092216
  143. messageManager.sendRpcMessage(this.cpMessageName, details);
  144. } else {
  145. // Compatibility for older versions
  146. messageManager.sendSyncMessage(this.cpMessageName, details);
  147. }
  148. return this.ACCEPT;
  149. },
  150. initContentScripts: function (win, sandbox) {
  151. let messager = getMessageManager(win);
  152. let sandboxId = hostName + ':sb:' + this.uniqueSandboxId++;
  153. if (sandbox) {
  154. let sandboxName = [
  155. win.location.href.slice(0, 100),
  156. win.document.title.slice(0, 100)
  157. ].join(' | ');
  158. // https://github.com/gorhill/uMatrix/issues/325
  159. // "Pass sameZoneAs to sandbox constructor to make GCs cheaper"
  160. sandbox = Cu.Sandbox([win], {
  161. sameZoneAs: win.top,
  162. sandboxName: sandboxId + '[' + sandboxName + ']',
  163. sandboxPrototype: win,
  164. wantComponents: false,
  165. wantXHRConstructor: false
  166. });
  167. sandbox.injectScript = function (script) {
  168. Services.scriptloader.loadSubScript(script, sandbox);
  169. };
  170. }
  171. else {
  172. sandbox = win;
  173. }
  174. sandbox._sandboxId_ = sandboxId;
  175. sandbox.sendAsyncMessage = messager.sendAsyncMessage;
  176. sandbox.addMessageListener = function (callback) {
  177. if (sandbox._messageListener_) {
  178. sandbox.removeMessageListener();
  179. }
  180. sandbox._messageListener_ = function (message) {
  181. callback(message.data);
  182. };
  183. messager.addMessageListener(sandbox._sandboxId_,
  184. sandbox._messageListener_);
  185. messager.addMessageListener(hostName + ':broadcast',
  186. sandbox._messageListener_);
  187. };
  188. sandbox.removeMessageListener = function () {
  189. try {
  190. messager.removeMessageListener(sandbox._sandboxId_,
  191. sandbox._messageListener_);
  192. messager.removeMessageListener(hostName + ':broadcast',
  193. sandbox._messageListener_);
  194. } catch (ex) {
  195. // It throws sometimes, mostly when the popup closes
  196. }
  197. sandbox._messageListener_ = null;
  198. };
  199. return sandbox;
  200. },
  201. observe: function (doc) {
  202. let win = doc.defaultView;
  203. if (!win) {
  204. return;
  205. }
  206. let loc = win.location;
  207. if (!loc) {
  208. return;
  209. }
  210. // https://github.com/gorhill/uBlock/issues/260
  211. // TODO: We may have to skip more types, for now let's be
  212. // conservative, i.e. let's not test against `text/html`.
  213. if (doc.contentType.lastIndexOf('image/', 0) === 0) {
  214. return;
  215. }
  216. if (loc.protocol !== 'http:'
  217. && loc.protocol !== 'https:'
  218. && loc.protocol !== 'file:') {
  219. if (loc.protocol === 'chrome:' && loc.host === hostName) {
  220. this.initContentScripts(win);
  221. }
  222. // What about data: and about:blank?
  223. return;
  224. }
  225. let lss = Services.scriptloader.loadSubScript;
  226. let sandbox = this.initContentScripts(win, true);
  227. // Can throw with attempts at injecting into non-HTML document.
  228. // Example: https://a.pomf.se/avonjf.webm
  229. try {
  230. lss(this.contentBaseURI + 'vapi-client.js', sandbox);
  231. lss(this.contentBaseURI + 'contentscript-start.js', sandbox);
  232. } catch (ex) {
  233. return; // don't further try to inject anything
  234. }
  235. let docReady = (e) => {
  236. let doc = e.target;
  237. doc.removeEventListener(e.type, docReady, true);
  238. lss(this.contentBaseURI + 'contentscript.js', sandbox);
  239. };
  240. if (doc.readyState === 'loading') {
  241. doc.addEventListener('DOMContentLoaded', docReady, true);
  242. } else {
  243. docReady({
  244. target: doc,
  245. type: 'DOMContentLoaded',
  246. });
  247. }
  248. }
  249. };
  250. var locationChangedMessageName = hostName + ':locationChanged';
  251. var LocationChangeListener = function (docShell) {
  252. if (!docShell) {
  253. return;
  254. }
  255. var requestor = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
  256. var ds = requestor.getInterface(Ci.nsIWebProgress);
  257. var mm = requestor.getInterface(Ci.nsIContentFrameMessageManager);
  258. if (ds && mm && typeof mm.sendAsyncMessage === 'function') {
  259. this.docShell = ds;
  260. this.messageManager = mm;
  261. ds.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
  262. }
  263. };
  264. LocationChangeListener.prototype.QueryInterface = XPCOMUtils.generateQI([
  265. 'nsIWebProgressListener',
  266. 'nsISupportsWeakReference'
  267. ]);
  268. LocationChangeListener.prototype.onLocationChange =
  269. function (webProgress, request, location, flags) {
  270. if (!webProgress.isTopLevel) {
  271. return;
  272. }
  273. this.messageManager.sendAsyncMessage(locationChangedMessageName, {
  274. url: location.asciiSpec,
  275. flags: flags,
  276. });
  277. };
  278. contentObserver.register();