FrameModule.jsm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 The µBlock authors
  4. Copyright (C) 2019 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://libregit.org/heckyel/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 === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'
  59. || Services.appinfo.ID === '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}')
  60. && Services.vc.compare(Services.appinfo.version, '44') > 0,
  61. get componentRegistrar() {
  62. return Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
  63. },
  64. get categoryManager() {
  65. return Components.classes['@mozilla.org/categorymanager;1']
  66. .getService(Ci.nsICategoryManager);
  67. },
  68. QueryInterface: XPCOMUtils.generateQI([
  69. Ci.nsIFactory,
  70. Ci.nsIObserver,
  71. Ci.nsIContentPolicy,
  72. Ci.nsISupportsWeakReference
  73. ]),
  74. createInstance: function (outer, iid) {
  75. if (outer) {
  76. throw Components.results.NS_ERROR_NO_AGGREGATION;
  77. }
  78. return this.QueryInterface(iid);
  79. },
  80. register: function () {
  81. Services.obs.addObserver(this, 'document-element-inserted', true);
  82. if (!this.modernFirefox) {
  83. this.componentRegistrar
  84. .registerFactory(this.classID,
  85. this.classDescription,
  86. this.contractID,
  87. this);
  88. this.categoryManager
  89. .addCategoryEntry('content-policy',
  90. this.contractID,
  91. this.contractID,
  92. false,
  93. true);
  94. }
  95. },
  96. unregister: function () {
  97. Services.obs.removeObserver(this, 'document-element-inserted');
  98. if (!this.modernFirefox) {
  99. this.componentRegistrar
  100. .unregisterFactory(this.classID,
  101. this);
  102. this.categoryManager
  103. .deleteCategoryEntry('content-policy',
  104. this.contractID,
  105. false);
  106. }
  107. },
  108. // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy
  109. // https://bugzil.la/612921
  110. shouldLoad: function (type, location, origin, context) {
  111. if (!context) {
  112. return this.ACCEPT;
  113. }
  114. if (!location.schemeIs('http') && !location.schemeIs('https')) {
  115. return this.ACCEPT;
  116. }
  117. let contextWindow;
  118. if (type === this.MAIN_FRAME) {
  119. contextWindow = context.contentWindow || context;
  120. } else if (type === this.SUB_FRAME) {
  121. contextWindow = context.contentWindow;
  122. } else {
  123. contextWindow = (context.ownerDocument || context).defaultView;
  124. }
  125. // https://github.com/gorhill/uMatrix/issues/706
  126. if (!contextWindow) {
  127. return this.ACCEPT;
  128. }
  129. // The context for the toolbar popup is an iframe element here,
  130. // so check context.top instead of context
  131. if (!contextWindow.top || !contextWindow.location) {
  132. return this.ACCEPT;
  133. }
  134. let messageManager = getMessageManager(contextWindow);
  135. if (messageManager === null) {
  136. return this.ACCEPT;
  137. }
  138. let details = {
  139. rawType: type,
  140. url: location.asciiSpec
  141. };
  142. if (typeof messageManager.sendRpcMessage === 'function') {
  143. // https://bugzil.la/1092216
  144. messageManager.sendRpcMessage(this.cpMessageName, details);
  145. } else {
  146. // Compatibility for older versions
  147. messageManager.sendSyncMessage(this.cpMessageName, details);
  148. }
  149. return this.ACCEPT;
  150. },
  151. initContentScripts: function (win, sandbox) {
  152. let messager = getMessageManager(win);
  153. let sandboxId = hostName + ':sb:' + this.uniqueSandboxId++;
  154. if (sandbox) {
  155. let sandboxName = [
  156. win.location.href.slice(0, 100),
  157. win.document.title.slice(0, 100)
  158. ].join(' | ');
  159. // https://github.com/gorhill/uMatrix/issues/325
  160. // "Pass sameZoneAs to sandbox constructor to make GCs cheaper"
  161. sandbox = Cu.Sandbox([win], {
  162. sameZoneAs: win.top,
  163. sandboxName: sandboxId + '[' + sandboxName + ']',
  164. sandboxPrototype: win,
  165. wantComponents: false,
  166. wantXHRConstructor: false
  167. });
  168. sandbox.injectScript = function (script) {
  169. Services.scriptloader.loadSubScript(script, sandbox);
  170. };
  171. }
  172. else {
  173. sandbox = win;
  174. }
  175. sandbox._sandboxId_ = sandboxId;
  176. sandbox.sendAsyncMessage = messager.sendAsyncMessage;
  177. sandbox.addMessageListener = function (callback) {
  178. if (sandbox._messageListener_) {
  179. sandbox.removeMessageListener();
  180. }
  181. sandbox._messageListener_ = function (message) {
  182. callback(message.data);
  183. };
  184. messager.addMessageListener(sandbox._sandboxId_,
  185. sandbox._messageListener_);
  186. messager.addMessageListener(hostName + ':broadcast',
  187. sandbox._messageListener_);
  188. };
  189. sandbox.removeMessageListener = function () {
  190. try {
  191. messager.removeMessageListener(sandbox._sandboxId_,
  192. sandbox._messageListener_);
  193. messager.removeMessageListener(hostName + ':broadcast',
  194. sandbox._messageListener_);
  195. } catch (ex) {
  196. // It throws sometimes, mostly when the popup closes
  197. }
  198. sandbox._messageListener_ = null;
  199. };
  200. return sandbox;
  201. },
  202. observe: function (doc) {
  203. let win = doc.defaultView;
  204. if (!win) {
  205. return;
  206. }
  207. let loc = win.location;
  208. if (!loc) {
  209. return;
  210. }
  211. // https://github.com/gorhill/uBlock/issues/260
  212. // TODO: We may have to skip more types, for now let's be
  213. // conservative, i.e. let's not test against `text/html`.
  214. if (doc.contentType.lastIndexOf('image/', 0) === 0) {
  215. return;
  216. }
  217. if (loc.protocol !== 'http:'
  218. && loc.protocol !== 'https:'
  219. && loc.protocol !== 'file:') {
  220. if (loc.protocol === 'chrome:' && loc.host === hostName) {
  221. this.initContentScripts(win);
  222. }
  223. // What about data: and about:blank?
  224. return;
  225. }
  226. let lss = Services.scriptloader.loadSubScript;
  227. let sandbox = this.initContentScripts(win, true);
  228. // Can throw with attempts at injecting into non-HTML document.
  229. // Example: https://a.pomf.se/avonjf.webm
  230. try {
  231. lss(this.contentBaseURI + 'vapi-client.js', sandbox);
  232. lss(this.contentBaseURI + 'contentscript-start.js', sandbox);
  233. } catch (ex) {
  234. return; // don't further try to inject anything
  235. }
  236. let docReady = (e) => {
  237. let doc = e.target;
  238. doc.removeEventListener(e.type, docReady, true);
  239. lss(this.contentBaseURI + 'contentscript.js', sandbox);
  240. };
  241. if (doc.readyState === 'loading') {
  242. doc.addEventListener('DOMContentLoaded', docReady, true);
  243. } else {
  244. docReady({
  245. target: doc,
  246. type: 'DOMContentLoaded',
  247. });
  248. }
  249. }
  250. };
  251. var locationChangedMessageName = hostName + ':locationChanged';
  252. var LocationChangeListener = function (docShell) {
  253. if (!docShell) {
  254. return;
  255. }
  256. var requestor = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
  257. var ds = requestor.getInterface(Ci.nsIWebProgress);
  258. var mm = requestor.getInterface(Ci.nsIContentFrameMessageManager);
  259. if (ds && mm && typeof mm.sendAsyncMessage === 'function') {
  260. this.docShell = ds;
  261. this.messageManager = mm;
  262. ds.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
  263. }
  264. };
  265. LocationChangeListener.prototype.QueryInterface = XPCOMUtils.generateQI([
  266. 'nsIWebProgressListener',
  267. 'nsISupportsWeakReference'
  268. ]);
  269. LocationChangeListener.prototype.onLocationChange =
  270. function (webProgress, request, location, flags) {
  271. if (!webProgress.isTopLevel) {
  272. return;
  273. }
  274. this.messageManager.sendAsyncMessage(locationChangedMessageName, {
  275. url: location.asciiSpec,
  276. flags: flags,
  277. });
  278. };
  279. contentObserver.register();