storage.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  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. /* global objectAssign, publicSuffixList */
  19. 'use strict';
  20. Components.utils.import('chrome://ematrix/content/lib/PublicSuffixList.jsm');
  21. Components.utils.import('chrome://ematrix/content/lib/Tools.jsm');
  22. ηMatrix.getBytesInUse = function () {
  23. let ηm = this;
  24. let getBytesInUseHandler = function (bytesInUse) {
  25. ηm.storageUsed = bytesInUse;
  26. };
  27. // Not all WebExtension implementations support getBytesInUse().
  28. // ηMatrix: not really our business, but does it impact us?
  29. if (typeof vAPI.storage.getBytesInUse === 'function') {
  30. vAPI.storage.getBytesInUse(null, getBytesInUseHandler);
  31. } else {
  32. ηm.storageUsed = undefined;
  33. }
  34. };
  35. ηMatrix.saveUserSettings = function () {
  36. this.XAL.keyvalSetMany(this.userSettings,
  37. this.getBytesInUse.bind(this));
  38. };
  39. ηMatrix.loadUserSettings = function (callback) {
  40. let ηm = this;
  41. if (typeof callback !== 'function') {
  42. callback = this.noopFunc;
  43. }
  44. var settingsLoaded = function (store) {
  45. // console.log('storage.js > loaded user settings');
  46. ηm.userSettings = store;
  47. callback(ηm.userSettings);
  48. };
  49. vAPI.storage.get(this.userSettings, settingsLoaded);
  50. };
  51. ηMatrix.loadRawSettings = function () {
  52. let ηm = this;
  53. let onLoaded = function (bin) {
  54. if (!bin || bin.rawSettings instanceof Object === false) {
  55. return;
  56. }
  57. for (let key of Object.keys(bin.rawSettings)) {
  58. if (ηm.rawSettings.hasOwnProperty(key) === false
  59. || typeof bin.rawSettings[key] !== typeof ηm.rawSettings[key]) {
  60. continue;
  61. }
  62. ηm.rawSettings[key] = bin.rawSettings[key];
  63. }
  64. ηm.rawSettingsWriteTime = Date.now();
  65. };
  66. vAPI.storage.get('rawSettings', onLoaded);
  67. };
  68. ηMatrix.saveRawSettings = function (rawSettings, callback) {
  69. let keys = Object.keys(rawSettings);
  70. if (keys.length === 0) {
  71. if (typeof callback === 'function') {
  72. callback();
  73. }
  74. return;
  75. }
  76. for (let key of keys) {
  77. if (this.rawSettingsDefault.hasOwnProperty(key)
  78. && typeof rawSettings[key] === typeof this.rawSettingsDefault[key]) {
  79. this.rawSettings[key] = rawSettings[key];
  80. }
  81. }
  82. vAPI.storage.set({
  83. rawSettings: this.rawSettings
  84. }, callback);
  85. this.rawSettingsWriteTime = Date.now();
  86. };
  87. ηMatrix.rawSettingsFromString = function (raw) {
  88. let result = {};
  89. let lineIter = new Tools.LineIterator(raw);
  90. while (lineIter.eot() === false) {
  91. let line = lineIter.next().trim();
  92. let matches = /^(\S+)(\s+(.+))?$/.exec(line);
  93. if (matches === null) {
  94. continue;
  95. }
  96. let name = matches[1];
  97. if (this.rawSettingsDefault.hasOwnProperty(name) === false) {
  98. continue;
  99. }
  100. let value = (matches[2] || '').trim();
  101. switch (typeof this.rawSettingsDefault[name]) {
  102. case 'boolean':
  103. if (value === 'true') {
  104. value = true;
  105. } else if (value === 'false') {
  106. value = false;
  107. } else {
  108. value = this.rawSettingsDefault[name];
  109. }
  110. break;
  111. case 'string':
  112. if (value === '') {
  113. value = this.rawSettingsDefault[name];
  114. }
  115. break;
  116. case 'number':
  117. value = parseInt(value, 10);
  118. if (isNaN(value)) {
  119. value = this.rawSettingsDefault[name];
  120. }
  121. break;
  122. default:
  123. break;
  124. }
  125. if (this.rawSettings[name] !== value) {
  126. result[name] = value;
  127. }
  128. }
  129. this.saveRawSettings(result);
  130. };
  131. ηMatrix.stringFromRawSettings = function () {
  132. let out = [];
  133. for (let key of Object.keys(this.rawSettings).sort()) {
  134. out.push(key + ' ' + this.rawSettings[key]);
  135. }
  136. return out.join('\n');
  137. };
  138. // save white/blacklist
  139. ηMatrix.saveMatrix = function () {
  140. ηMatrix.XAL.keyvalSetOne('userMatrix', this.pMatrix.toString());
  141. };
  142. ηMatrix.loadMatrix = function (callback) {
  143. if (typeof callback !== 'function') {
  144. callback = this.noopFunc;
  145. }
  146. let ηm = this;
  147. let onLoaded = function (bin) {
  148. if (bin.hasOwnProperty('userMatrix')) {
  149. ηm.pMatrix.fromString(bin.userMatrix);
  150. ηm.tMatrix.assign(ηm.pMatrix);
  151. callback();
  152. }
  153. };
  154. this.XAL.keyvalGetOne('userMatrix', onLoaded);
  155. };
  156. ηMatrix.listKeysFromCustomHostsFiles = function (raw) {
  157. let out = new Set();
  158. let reIgnore = /^[!#]/;
  159. let reValid = /^[a-z-]+:\/\/\S+/;
  160. let lineIter = new Tools.LineIterator(raw);
  161. while (lineIter.eot() === false) {
  162. let location = lineIter.next().trim();
  163. if (reIgnore.test(location) || !reValid.test(location)) {
  164. continue;
  165. }
  166. out.add(location);
  167. }
  168. return Tools.setToArray(out);
  169. };
  170. ηMatrix.getAvailableHostsFiles = function (callback) {
  171. let ηm = this;
  172. let availableHostsFiles = {};
  173. // Custom filter lists.
  174. let importedListKeys =
  175. this.listKeysFromCustomHostsFiles(ηm.userSettings.externalHostsFiles);
  176. let i = importedListKeys.length;
  177. while (i--) {
  178. let listKey = importedListKeys[i];
  179. let entry = {
  180. content: 'filters',
  181. contentURL: listKey,
  182. external: true,
  183. submitter: 'user',
  184. title: listKey
  185. };
  186. availableHostsFiles[listKey] = entry;
  187. this.assets.registerAssetSource(listKey, entry);
  188. }
  189. // selected lists
  190. let onSelectedHostsFilesLoaded = function (bin) {
  191. // Now get user's selection of lists
  192. for (let assetKey in bin.liveHostsFiles) {
  193. let availableEntry = availableHostsFiles[assetKey];
  194. if (availableEntry === undefined) {
  195. continue;
  196. }
  197. let liveEntry = bin.liveHostsFiles[assetKey];
  198. availableEntry.off = liveEntry.off || false;
  199. if (liveEntry.entryCount !== undefined) {
  200. availableEntry.entryCount = liveEntry.entryCount;
  201. }
  202. if (liveEntry.entryUsedCount !== undefined) {
  203. availableEntry.entryUsedCount = liveEntry.entryUsedCount;
  204. }
  205. // This may happen if the list name was pulled from the list content
  206. if (availableEntry.title === '' && liveEntry.title !== undefined) {
  207. availableEntry.title = liveEntry.title;
  208. }
  209. }
  210. // Remove unreferenced imported filter lists.
  211. let dict = new Set(importedListKeys);
  212. for (let assetKey in availableHostsFiles) {
  213. let entry = availableHostsFiles[assetKey];
  214. if (entry.submitter !== 'user') {
  215. continue;
  216. }
  217. if (dict.has(assetKey)) {
  218. continue;
  219. }
  220. delete availableHostsFiles[assetKey];
  221. ηm.assets.unregisterAssetSource(assetKey);
  222. ηm.assets.remove(assetKey);
  223. }
  224. callback(availableHostsFiles);
  225. };
  226. // built-in lists
  227. let onBuiltinHostsFilesLoaded = function (entries) {
  228. for (let assetKey in entries) {
  229. if (entries.hasOwnProperty(assetKey) === false) {
  230. continue;
  231. }
  232. let entry = entries[assetKey];
  233. if (entry.content !== 'filters') {
  234. continue;
  235. }
  236. availableHostsFiles[assetKey] = Object.assign({}, entry);
  237. }
  238. // Now get user's selection of lists
  239. vAPI.storage.get({
  240. 'liveHostsFiles': availableHostsFiles
  241. }, onSelectedHostsFilesLoaded);
  242. };
  243. this.assets.metadata(onBuiltinHostsFilesLoaded);
  244. };
  245. ηMatrix.loadHostsFiles = function (callback) {
  246. let ηm = ηMatrix;
  247. let hostsFileLoadCount;
  248. if (typeof callback !== 'function') {
  249. callback = this.noopFunc;
  250. }
  251. let loadHostsFilesEnd = function () {
  252. ηm.ubiquitousBlacklist.freeze();
  253. vAPI.storage.set({
  254. 'liveHostsFiles': ηm.liveHostsFiles
  255. });
  256. vAPI.messaging.broadcast({
  257. what: 'loadHostsFilesCompleted'
  258. });
  259. ηm.getBytesInUse();
  260. callback();
  261. };
  262. let mergeHostsFile = function (details) {
  263. ηm.mergeHostsFile(details);
  264. hostsFileLoadCount -= 1;
  265. if (hostsFileLoadCount === 0) {
  266. loadHostsFilesEnd();
  267. }
  268. };
  269. let loadHostsFilesStart = function (hostsFiles) {
  270. ηm.liveHostsFiles = hostsFiles;
  271. ηm.ubiquitousBlacklist.reset();
  272. let locations = Object.keys(hostsFiles);
  273. hostsFileLoadCount = locations.length;
  274. // Load all hosts file which are not disabled.
  275. let location;
  276. while ((location = locations.pop())) {
  277. if (hostsFiles[location].off) {
  278. hostsFileLoadCount -= 1;
  279. continue;
  280. }
  281. ηm.assets.get(location, mergeHostsFile);
  282. }
  283. // https://github.com/gorhill/uMatrix/issues/2
  284. if (hostsFileLoadCount === 0) {
  285. loadHostsFilesEnd();
  286. return;
  287. }
  288. };
  289. this.getAvailableHostsFiles(loadHostsFilesStart);
  290. };
  291. ηMatrix.mergeHostsFile = function (details) {
  292. let usedCount = this.ubiquitousBlacklist.count;
  293. let duplicateCount = this.ubiquitousBlacklist.duplicateCount;
  294. this.mergeHostsFileContent(details.content);
  295. usedCount = this.ubiquitousBlacklist.count - usedCount;
  296. duplicateCount = this.ubiquitousBlacklist.duplicateCount - duplicateCount;
  297. var hostsFilesMeta = this.liveHostsFiles[details.assetKey];
  298. hostsFilesMeta.entryCount = usedCount + duplicateCount;
  299. hostsFilesMeta.entryUsedCount = usedCount;
  300. };
  301. ηMatrix.mergeHostsFileContent = function (rawText) {
  302. let rawEnd = rawText.length;
  303. let ubiquitousBlacklist = this.ubiquitousBlacklist;
  304. let reLocalhost = /(^|\s)(localhost\.localdomain|localhost|local|broadcasthost|0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)(?=\s|$)/g;
  305. let reAsciiSegment = /^[\x21-\x7e]+$/;
  306. let matches;
  307. let lineBeg = 0, lineEnd;
  308. let line;
  309. while (lineBeg < rawEnd) {
  310. lineEnd = rawText.indexOf('\n', lineBeg);
  311. if (lineEnd < 0) {
  312. lineEnd = rawText.indexOf('\r', lineBeg);
  313. if (lineEnd < 0) {
  314. lineEnd = rawEnd;
  315. }
  316. }
  317. // rhill 2014-04-18: The trim is important here, as without it there
  318. // could be a lingering `\r` which would cause problems in the
  319. // following parsing code.
  320. line = rawText.slice(lineBeg, lineEnd).trim();
  321. lineBeg = lineEnd + 1;
  322. // https://github.com/gorhill/httpswitchboard/issues/15
  323. // Ensure localhost et al. don't end up in the ubiquitous blacklist.
  324. line = line
  325. .replace(/#.*$/, '')
  326. .toLowerCase()
  327. .replace(reLocalhost, '')
  328. .trim();
  329. // The filter is whatever sequence of printable ascii character without
  330. // whitespaces
  331. matches = reAsciiSegment.exec(line);
  332. if (!matches || matches.length === 0) {
  333. continue;
  334. }
  335. // Bypass anomalies
  336. // For example, when a filter contains whitespace characters, or
  337. // whatever else outside the range of printable ascii characters.
  338. if (matches[0] !== line) {
  339. continue;
  340. }
  341. line = matches[0];
  342. if (line === '') {
  343. continue;
  344. }
  345. ubiquitousBlacklist.add(line);
  346. }
  347. };
  348. // `switches` contains the filter lists for which the switch must be revisited.
  349. ηMatrix.selectHostsFiles = function (details, callback) {
  350. let ηm = this;
  351. let externalHostsFiles = this.userSettings.externalHostsFiles;
  352. // Hosts file to select
  353. if (Array.isArray(details.toSelect)) {
  354. for (let assetKey in this.liveHostsFiles) {
  355. if (this.liveHostsFiles.hasOwnProperty(assetKey) === false) {
  356. continue;
  357. }
  358. if (details.toSelect.indexOf(assetKey) !== -1) {
  359. this.liveHostsFiles[assetKey].off = false;
  360. } else if ( details.merge !== true ) {
  361. this.liveHostsFiles[assetKey].off = true;
  362. }
  363. }
  364. }
  365. // Imported hosts files to remove
  366. if (Array.isArray(details.toRemove)) {
  367. let removeURLFromHaystack = function (haystack, needle) {
  368. return haystack
  369. .replace(new RegExp('(^|\\n)'
  370. + needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  371. + '(\\n|$)', 'g'),
  372. '\n').trim();
  373. };
  374. for (let i=0, n=details.toRemove.length; i<n; ++i) {
  375. let assetKey = details.toRemove[i];
  376. delete this.liveHostsFiles[assetKey];
  377. externalHostsFiles = removeURLFromHaystack(externalHostsFiles, assetKey);
  378. this.assets.remove(assetKey);
  379. }
  380. }
  381. // Hosts file to import
  382. if (typeof details.toImport === 'string') {
  383. // https://github.com/gorhill/uBlock/issues/1181
  384. // Try mapping the URL of an imported filter list to the assetKey of an
  385. // existing stock list.
  386. let assetKeyFromURL = function (url) {
  387. let needle = url.replace(/^https?:/, '');
  388. let assets = ηm.liveHostsFiles;
  389. for (let assetKey in assets) {
  390. let asset = assets[assetKey];
  391. if (asset.content !== 'filters') {
  392. continue;
  393. }
  394. if (typeof asset.contentURL === 'string') {
  395. if (asset.contentURL.endsWith(needle)) {
  396. return assetKey;
  397. }
  398. continue;
  399. }
  400. if (Array.isArray(asset.contentURL) === false) {
  401. continue;
  402. }
  403. for (let i=0, n=asset.contentURL.length; i<n; ++i) {
  404. if (asset.contentURL[i].endsWith(needle)) {
  405. return assetKey;
  406. }
  407. }
  408. }
  409. return url;
  410. };
  411. let importedSet = new Set(this.listKeysFromCustomHostsFiles(externalHostsFiles));
  412. let toImportSet = new Set(this.listKeysFromCustomHostsFiles(details.toImport));
  413. let iter = toImportSet.values();
  414. for (;;) {
  415. let entry = iter.next();
  416. if (entry.done) {
  417. break;
  418. }
  419. if (importedSet.has(entry.value)) {
  420. continue;
  421. }
  422. let assetKey = assetKeyFromURL(entry.value);
  423. if (assetKey === entry.value) {
  424. importedSet.add(entry.value);
  425. }
  426. this.liveHostsFiles[assetKey] = {
  427. content: 'filters',
  428. contentURL: [ assetKey ],
  429. title: assetKey
  430. };
  431. }
  432. externalHostsFiles = Tools.setToArray(importedSet).sort().join('\n');
  433. }
  434. if (externalHostsFiles !== this.userSettings.externalHostsFiles) {
  435. this.userSettings.externalHostsFiles = externalHostsFiles;
  436. vAPI.storage.set({
  437. externalHostsFiles: externalHostsFiles
  438. });
  439. }
  440. vAPI.storage.set({
  441. 'liveHostsFiles': this.liveHostsFiles
  442. });
  443. if (typeof callback === 'function') {
  444. callback();
  445. }
  446. };
  447. // `switches` contains the preset blacklists for which the switch must be
  448. // revisited.
  449. ηMatrix.reloadHostsFiles = function () {
  450. this.loadHostsFiles();
  451. };
  452. ηMatrix.loadPublicSuffixList = function (callback) {
  453. if (typeof callback !== 'function') {
  454. callback = this.noopFunc;
  455. }
  456. let applyPublicSuffixList = function (details) {
  457. if (!details.error) {
  458. publicSuffixList.parse(details.content, Punycode.toASCII);
  459. }
  460. callback();
  461. };
  462. this.assets.get(this.pslAssetKey, applyPublicSuffixList);
  463. };
  464. ηMatrix.scheduleAssetUpdater = (function () {
  465. let timer = undefined;
  466. let next = 0;
  467. return function (updateDelay) {
  468. if (timer) {
  469. clearTimeout(timer);
  470. timer = undefined;
  471. }
  472. if (updateDelay === 0) {
  473. next = 0;
  474. return;
  475. }
  476. let now = Date.now();
  477. // Use the new schedule if and only if it is earlier than the
  478. // previous one.
  479. if (next !== 0) {
  480. updateDelay = Math.min(updateDelay, Math.max(next - now, 0));
  481. }
  482. next = now + updateDelay;
  483. timer = vAPI.setTimeout(function () {
  484. timer = undefined;
  485. next = 0;
  486. ηMatrix.assets.updateStart({
  487. delay: 120000
  488. });
  489. }, updateDelay);
  490. };
  491. })();
  492. ηMatrix.assetObserver = function (topic, details) {
  493. // Do not update filter list if not in use.
  494. if (topic === 'before-asset-updated') {
  495. if (this.liveHostsFiles.hasOwnProperty(details.assetKey) === false ||
  496. this.liveHostsFiles[details.assetKey].off === true) {
  497. return false;
  498. }
  499. return true;
  500. }
  501. if (topic === 'after-asset-updated') {
  502. vAPI.messaging.broadcast({
  503. what: 'assetUpdated',
  504. key: details.assetKey,
  505. cached: true
  506. });
  507. return;
  508. }
  509. // Update failed.
  510. if (topic === 'asset-update-failed') {
  511. vAPI.messaging.broadcast({
  512. what: 'assetUpdated',
  513. key: details.assetKey,
  514. failed: true
  515. });
  516. return;
  517. }
  518. // Reload all filter lists if needed.
  519. if (topic === 'after-assets-updated') {
  520. if (details.assetKeys.length !== 0) {
  521. this.loadHostsFiles();
  522. }
  523. ηm.scheduleAssetUpdater(ηm.userSettings.autoUpdate ?
  524. 7 * 60 * 100000 :
  525. 0);
  526. vAPI.messaging.broadcast({
  527. what: 'assetsUpdated',
  528. assetKeys: details.assetKeys
  529. });
  530. return;
  531. }
  532. // New asset source became available, if it's a filter list, should we
  533. // auto-select it?
  534. if (topic === 'builtin-asset-source-added') {
  535. if (details.entry.content === 'filters') {
  536. if (details.entry.off !== true) {
  537. this.saveSelectedFilterLists([ details.assetKey ], true);
  538. }
  539. }
  540. return;
  541. }
  542. };