user-rules.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  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. /* global uDom */
  19. 'use strict';
  20. /******************************************************************************/
  21. (function() {
  22. /******************************************************************************/
  23. // Switches before, rules after
  24. var directiveSort = function(a, b) {
  25. var aIsSwitch = a.indexOf(':') !== -1;
  26. var bIsSwitch = b.indexOf(':') !== -1;
  27. if ( aIsSwitch === bIsSwitch ) {
  28. return a.localeCompare(b);
  29. }
  30. return aIsSwitch ? -1 : 1;
  31. };
  32. /******************************************************************************/
  33. var processUserRules = function(response) {
  34. var rules, rule, i;
  35. var allRules = {};
  36. var permanentRules = {};
  37. var temporaryRules = {};
  38. var onLeft, onRight;
  39. rules = response.permanentRules.split(/\n+/);
  40. i = rules.length;
  41. while ( i-- ) {
  42. rule = rules[i].trim();
  43. if ( rule.length !== 0 ) {
  44. permanentRules[rule] = allRules[rule] = true;
  45. }
  46. }
  47. rules = response.temporaryRules.split(/\n+/);
  48. i = rules.length;
  49. while ( i-- ) {
  50. rule = rules[i].trim();
  51. if ( rule.length !== 0 ) {
  52. temporaryRules[rule] = allRules[rule] = true;
  53. }
  54. }
  55. var permanentList = document.createDocumentFragment(),
  56. temporaryList = document.createDocumentFragment(),
  57. li;
  58. rules = Object.keys(allRules).sort(directiveSort);
  59. for ( i = 0; i < rules.length; i++ ) {
  60. rule = rules[i];
  61. onLeft = permanentRules.hasOwnProperty(rule);
  62. onRight = temporaryRules.hasOwnProperty(rule);
  63. if ( onLeft && onRight ) {
  64. li = document.createElement('li');
  65. li.textContent = rule;
  66. permanentList.appendChild(li);
  67. li = document.createElement('li');
  68. li.textContent = rule;
  69. temporaryList.appendChild(li);
  70. } else if ( onLeft ) {
  71. li = document.createElement('li');
  72. li.textContent = rule;
  73. permanentList.appendChild(li);
  74. li = document.createElement('li');
  75. li.textContent = rule;
  76. li.className = 'notRight toRemove';
  77. temporaryList.appendChild(li);
  78. } else if ( onRight ) {
  79. li = document.createElement('li');
  80. li.textContent = '\xA0';
  81. permanentList.appendChild(li);
  82. li = document.createElement('li');
  83. li.textContent = rule;
  84. li.className = 'notLeft';
  85. temporaryList.appendChild(li);
  86. }
  87. }
  88. // TODO: build incrementally.
  89. uDom('#diff > .left > ul > li').remove();
  90. document.querySelector('#diff > .left > ul').appendChild(permanentList);
  91. uDom('#diff > .right > ul > li').remove();
  92. document.querySelector('#diff > .right > ul').appendChild(temporaryList);
  93. uDom('#diff').toggleClass('dirty', response.temporaryRules !== response.permanentRules);
  94. };
  95. /******************************************************************************/
  96. // https://github.com/chrisaljoudi/uBlock/issues/757
  97. // Support RequestPolicy rule syntax
  98. var fromRequestPolicy = function(content) {
  99. var matches = /\[origins-to-destinations\]([^\[]+)/.exec(content);
  100. if ( matches === null || matches.length !== 2 ) {
  101. return;
  102. }
  103. return matches[1].trim()
  104. .replace(/\|/g, ' ')
  105. .replace(/\n/g, ' * allow\n');
  106. };
  107. /******************************************************************************/
  108. // https://github.com/gorhill/uMatrix/issues/270
  109. var fromNoScript = function(content) {
  110. var noscript = null;
  111. try {
  112. noscript = JSON.parse(content);
  113. } catch (e) {
  114. }
  115. if (
  116. noscript === null ||
  117. typeof noscript !== 'object' ||
  118. typeof noscript.prefs !== 'object' ||
  119. typeof noscript.prefs.clearClick === 'undefined' ||
  120. typeof noscript.whitelist !== 'string' ||
  121. typeof noscript.V !== 'string'
  122. ) {
  123. return;
  124. }
  125. var out = new Set();
  126. var reBad = /[a-z]+:\w*$/;
  127. var reURL = /[a-z]+:\/\/([0-9a-z.-]+)/;
  128. var directives = noscript.whitelist.split(/\s+/);
  129. var i = directives.length;
  130. var directive, matches;
  131. while ( i-- ) {
  132. directive = directives[i].trim();
  133. if ( directive === '' ) {
  134. continue;
  135. }
  136. if ( reBad.test(directive) ) {
  137. continue;
  138. }
  139. matches = reURL.exec(directive);
  140. if ( matches !== null ) {
  141. directive = matches[1];
  142. }
  143. out.add('* ' + directive + ' * allow');
  144. out.add('* ' + directive + ' script allow');
  145. out.add('* ' + directive + ' frame allow');
  146. }
  147. return Array.from(out).join('\n');
  148. };
  149. /******************************************************************************/
  150. var handleImportFilePicker = function() {
  151. var fileReaderOnLoadHandler = function() {
  152. if ( typeof this.result !== 'string' || this.result === '' ) {
  153. return;
  154. }
  155. var result = fromRequestPolicy(this.result);
  156. if ( result === undefined ) {
  157. result = fromNoScript(this.result);
  158. if ( result === undefined ) {
  159. result = this.result;
  160. }
  161. }
  162. if ( this.result === '' ) { return; }
  163. var request = {
  164. 'what': 'setUserRules',
  165. 'temporaryRules': rulesFromHTML('#diff .right li') + '\n' + result
  166. };
  167. vAPI.messaging.send('user-rules.js', request, processUserRules);
  168. };
  169. var file = this.files[0];
  170. if ( file === undefined || file.name === '' ) {
  171. return;
  172. }
  173. if ( file.type.indexOf('text') !== 0 && file.type !== 'application/json') {
  174. return;
  175. }
  176. var fr = new FileReader();
  177. fr.onload = fileReaderOnLoadHandler;
  178. fr.readAsText(file);
  179. };
  180. /******************************************************************************/
  181. var startImportFilePicker = function() {
  182. var input = document.getElementById('importFilePicker');
  183. // Reset to empty string, this will ensure an change event is properly
  184. // triggered if the user pick a file, even if it is the same as the last
  185. // one picked.
  186. input.value = '';
  187. input.click();
  188. };
  189. /******************************************************************************/
  190. function exportUserRulesToFile() {
  191. vAPI.download({
  192. 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li') + '\n'),
  193. 'filename': uDom('[data-i18n="userRulesDefaultFileName"]').text()
  194. });
  195. }
  196. /******************************************************************************/
  197. var rulesFromHTML = function(selector) {
  198. var rules = [];
  199. var lis = uDom(selector);
  200. var li;
  201. for ( var i = 0; i < lis.length; i++ ) {
  202. li = lis.at(i);
  203. if ( li.hasClassName('toRemove') ) {
  204. rules.push('');
  205. } else {
  206. rules.push(li.text());
  207. }
  208. }
  209. return rules.join('\n');
  210. };
  211. /******************************************************************************/
  212. var revertHandler = function() {
  213. var request = {
  214. 'what': 'setUserRules',
  215. 'temporaryRules': rulesFromHTML('#diff .left li')
  216. };
  217. vAPI.messaging.send('user-rules.js', request, processUserRules);
  218. };
  219. /******************************************************************************/
  220. var commitHandler = function() {
  221. var request = {
  222. 'what': 'setUserRules',
  223. 'permanentRules': rulesFromHTML('#diff .right li')
  224. };
  225. vAPI.messaging.send('user-rules.js', request, processUserRules);
  226. };
  227. /******************************************************************************/
  228. var editStartHandler = function() {
  229. uDom('#diff .right textarea').val(rulesFromHTML('#diff .right li'));
  230. var parent = uDom(this).ancestors('#diff');
  231. parent.toggleClass('edit', true);
  232. };
  233. /******************************************************************************/
  234. var editStopHandler = function() {
  235. var parent = uDom(this).ancestors('#diff');
  236. parent.toggleClass('edit', false);
  237. var request = {
  238. 'what': 'setUserRules',
  239. 'temporaryRules': uDom('#diff .right textarea').val()
  240. };
  241. vAPI.messaging.send('user-rules.js', request, processUserRules);
  242. };
  243. /******************************************************************************/
  244. var editCancelHandler = function() {
  245. var parent = uDom(this).ancestors('#diff');
  246. parent.toggleClass('edit', false);
  247. };
  248. /******************************************************************************/
  249. var temporaryRulesToggler = function() {
  250. var li = uDom(this);
  251. li.toggleClass('toRemove');
  252. var request = {
  253. 'what': 'setUserRules',
  254. 'temporaryRules': rulesFromHTML('#diff .right li')
  255. };
  256. vAPI.messaging.send('user-rules.js', request, processUserRules);
  257. };
  258. /******************************************************************************/
  259. self.cloud.onPush = function() {
  260. return rulesFromHTML('#diff .left li');
  261. };
  262. self.cloud.onPull = function(data, append) {
  263. if ( typeof data !== 'string' ) { return; }
  264. if ( append ) {
  265. data = rulesFromHTML('#diff .right li') + '\n' + data;
  266. }
  267. var request = {
  268. 'what': 'setUserRules',
  269. 'temporaryRules': data
  270. };
  271. vAPI.messaging.send('user-rules.js', request, processUserRules);
  272. };
  273. /******************************************************************************/
  274. uDom.onLoad(function() {
  275. // Handle user interaction
  276. uDom('#importButton').on('click', startImportFilePicker);
  277. uDom('#importFilePicker').on('change', handleImportFilePicker);
  278. uDom('#exportButton').on('click', exportUserRulesToFile);
  279. uDom('#revertButton').on('click', revertHandler);
  280. uDom('#commitButton').on('click', commitHandler);
  281. uDom('#editEnterButton').on('click', editStartHandler);
  282. uDom('#editStopButton').on('click', editStopHandler);
  283. uDom('#editCancelButton').on('click', editCancelHandler);
  284. uDom('#diff > .right > ul').on('click', 'li', temporaryRulesToggler);
  285. vAPI.messaging.send('user-rules.js', { what: 'getUserRules' }, processUserRules);
  286. });
  287. /******************************************************************************/
  288. })();