user-rules.js 9.1 KB

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