You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
12 KiB

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