storage.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. /* global LZString */
  2. 'use strict';
  3. const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
  4. /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
  5. /[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
  6. const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
  7. // eslint-disable-next-line no-var
  8. var SLOPPY_REGEXP_PREFIX = '\0';
  9. // CSS transition bug workaround: since we insert styles asynchronously,
  10. // the browsers, especially Firefox, may apply all transitions on page load
  11. const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
  12. const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
  13. // Note, only 'var'-declared variables are visible from another extension page
  14. // eslint-disable-next-line no-var
  15. var cachedStyles = {
  16. list: null, // array of all styles
  17. byId: new Map(), // all styles indexed by id
  18. filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
  19. regexps: new Map(), // compiled style regexps
  20. urlDomains: new Map(), // getDomain() results for 100 last checked urls
  21. needTransitionPatch: new Map(), // FF bug workaround
  22. mutex: {
  23. inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
  24. // (initially 'true' to prevent rogue getStyles before dbExec.initialized)
  25. onDone: [], // to getStyles() are queued and resolved when the first one finishes
  26. },
  27. };
  28. // eslint-disable-next-line no-var
  29. var chromeLocal = {
  30. get(options) {
  31. return new Promise(resolve => {
  32. chrome.storage.local.get(options, data => resolve(data));
  33. });
  34. },
  35. set(data) {
  36. return new Promise(resolve => {
  37. chrome.storage.local.set(data, () => resolve(data));
  38. });
  39. },
  40. remove(keyOrKeys) {
  41. return new Promise(resolve => {
  42. chrome.storage.local.remove(keyOrKeys, resolve);
  43. });
  44. },
  45. getValue(key) {
  46. return chromeLocal.get(key).then(data => data[key]);
  47. },
  48. setValue(key, value) {
  49. return chromeLocal.set({[key]: value});
  50. },
  51. };
  52. // eslint-disable-next-line no-var
  53. var chromeSync = {
  54. get(options) {
  55. return new Promise(resolve => {
  56. chrome.storage.sync.get(options, resolve);
  57. });
  58. },
  59. set(data) {
  60. return new Promise(resolve => {
  61. chrome.storage.sync.set(data, () => resolve(data));
  62. });
  63. },
  64. getLZValue(key) {
  65. return chromeSync.getLZValues([key]).then(data => data[key]);
  66. },
  67. getLZValues(keys) {
  68. return chromeSync.get(keys).then((data = {}) => {
  69. for (const key of keys) {
  70. const value = data[key];
  71. data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
  72. }
  73. return data;
  74. });
  75. },
  76. setLZValue(key, value) {
  77. return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
  78. }
  79. };
  80. // eslint-disable-next-line no-var
  81. var dbExec = dbExecIndexedDB;
  82. dbExec.initialized = false;
  83. // we use chrome.storage.local fallback if IndexedDB doesn't save data,
  84. // which, once detected on the first run, is remembered in chrome.storage.local
  85. // for reliablility and in localStorage for fast synchronous access
  86. // (FF may block localStorage depending on its privacy options)
  87. do {
  88. const done = () => {
  89. cachedStyles.mutex.inProgress = false;
  90. getStyles().then(() => {
  91. dbExec.initialized = true;
  92. window.dispatchEvent(new Event('storageReady'));
  93. });
  94. };
  95. const fallback = () => {
  96. dbExec = dbExecChromeStorage;
  97. chromeLocal.set({dbInChromeStorage: true});
  98. localStorage.dbInChromeStorage = 'true';
  99. ignoreChromeError();
  100. done();
  101. };
  102. const fallbackSet = localStorage.dbInChromeStorage;
  103. if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
  104. fallback();
  105. break;
  106. } else if (fallbackSet === 'false') {
  107. done();
  108. break;
  109. }
  110. chromeLocal.get('dbInChromeStorage')
  111. .then(data =>
  112. data && data.dbInChromeStorage && Promise.reject())
  113. .then(() =>
  114. tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
  115. Promise.reject())
  116. .then(({target}) => (
  117. (target.result || [])[0] ?
  118. Promise.reject('ok') :
  119. dbExecIndexedDB('put', {id: -1})))
  120. .then(() =>
  121. dbExecIndexedDB('get', -1))
  122. .then(({target}) => (
  123. (target.result || {}).id === -1 ?
  124. dbExecIndexedDB('delete', -1) :
  125. Promise.reject()))
  126. .then(() =>
  127. Promise.reject('ok'))
  128. .catch(result => {
  129. if (result === 'ok') {
  130. chromeLocal.set({dbInChromeStorage: false});
  131. localStorage.dbInChromeStorage = 'false';
  132. done();
  133. } else {
  134. fallback();
  135. }
  136. });
  137. } while (0);
  138. function dbExecIndexedDB(method, ...args) {
  139. return new Promise((resolve, reject) => {
  140. Object.assign(indexedDB.open('stylish', 2), {
  141. onsuccess(event) {
  142. const database = event.target.result;
  143. if (!method) {
  144. resolve(database);
  145. } else {
  146. const transaction = database.transaction(['styles'], 'readwrite');
  147. const store = transaction.objectStore('styles');
  148. Object.assign(store[method](...args), {
  149. onsuccess: event => resolve(event, store, transaction, database),
  150. onerror: reject,
  151. });
  152. }
  153. },
  154. onerror(event) {
  155. console.warn(event.target.error || event.target.errorCode);
  156. reject(event);
  157. },
  158. onupgradeneeded(event) {
  159. if (event.oldVersion === 0) {
  160. event.target.result.createObjectStore('styles', {
  161. keyPath: 'id',
  162. autoIncrement: true,
  163. });
  164. }
  165. },
  166. });
  167. });
  168. }
  169. function dbExecChromeStorage(method, data) {
  170. const STYLE_KEY_PREFIX = 'style-';
  171. switch (method) {
  172. case 'get':
  173. return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
  174. .then(result => ({target: {result}}));
  175. case 'put':
  176. if (!data.id) {
  177. return getStyles().then(() => {
  178. data.id = 1;
  179. for (const style of cachedStyles.list) {
  180. data.id = Math.max(data.id, style.id + 1);
  181. }
  182. return dbExecChromeStorage('put', data);
  183. });
  184. }
  185. return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
  186. .then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
  187. case 'delete':
  188. return chromeLocal.remove(STYLE_KEY_PREFIX + data);
  189. case 'getAll':
  190. return chromeLocal.get(null).then(storage => {
  191. const styles = [];
  192. const leftovers = [];
  193. for (const key in storage) {
  194. if (key.startsWith(STYLE_KEY_PREFIX) &&
  195. Number(key.substr(STYLE_KEY_PREFIX.length))) {
  196. styles.push(storage[key]);
  197. } else if (key.startsWith('tempUsercssCode')) {
  198. leftovers.push(key);
  199. }
  200. }
  201. if (leftovers.length) {
  202. chromeLocal.remove(leftovers);
  203. }
  204. return {target: {result: styles}};
  205. });
  206. }
  207. return Promise.reject();
  208. }
  209. function getStyles(options) {
  210. if (cachedStyles.list) {
  211. return Promise.resolve(filterStyles(options));
  212. }
  213. if (cachedStyles.mutex.inProgress) {
  214. return new Promise(resolve => {
  215. cachedStyles.mutex.onDone.push({options, resolve});
  216. });
  217. }
  218. cachedStyles.mutex.inProgress = true;
  219. return dbExec('getAll').then(event => {
  220. cachedStyles.list = event.target.result || [];
  221. cachedStyles.byId.clear();
  222. for (const style of cachedStyles.list) {
  223. cachedStyles.byId.set(style.id, style);
  224. }
  225. cachedStyles.mutex.inProgress = false;
  226. for (const {options, resolve} of cachedStyles.mutex.onDone) {
  227. resolve(filterStyles(options));
  228. }
  229. cachedStyles.mutex.onDone = [];
  230. return filterStyles(options);
  231. });
  232. }
  233. function filterStyles({
  234. enabled = null,
  235. url = null,
  236. id = null,
  237. matchUrl = null,
  238. asHash = null,
  239. strictRegexp = true, // used by the popup to detect bad regexps
  240. } = {}) {
  241. enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
  242. typeof enabled === 'string' ? enabled === 'true' : null;
  243. id = id === null ? null : Number(id);
  244. if (
  245. enabled === null &&
  246. url === null &&
  247. id === null &&
  248. matchUrl === null &&
  249. asHash !== true
  250. ) {
  251. return cachedStyles.list;
  252. }
  253. if (matchUrl && !URLS.supported(matchUrl)) {
  254. return asHash ? {} : [];
  255. }
  256. const blankHash = asHash && {
  257. disableAll: prefs.get('disableAll'),
  258. exposeIframes: prefs.get('exposeIframes'),
  259. };
  260. // add \t after url to prevent collisions (not sure it can actually happen though)
  261. const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp;
  262. const cached = cachedStyles.filters.get(cacheKey);
  263. if (cached) {
  264. cached.hits++;
  265. cached.lastHit = Date.now();
  266. return asHash
  267. ? Object.assign(blankHash, cached.styles)
  268. : cached.styles;
  269. }
  270. return filterStylesInternal({
  271. enabled,
  272. url,
  273. id,
  274. matchUrl,
  275. asHash,
  276. strictRegexp,
  277. blankHash,
  278. cacheKey,
  279. });
  280. }
  281. function filterStylesInternal({
  282. // js engines don't like big functions (V8 often deoptimized the original filterStyles)
  283. // it also makes sense to extract the less frequently executed code
  284. enabled,
  285. url,
  286. id,
  287. matchUrl,
  288. asHash,
  289. strictRegexp,
  290. blankHash,
  291. cacheKey,
  292. }) {
  293. if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
  294. cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
  295. for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
  296. const firstKey = cachedStyles.urlDomains.keys().next().value;
  297. cachedStyles.urlDomains.delete(firstKey);
  298. }
  299. }
  300. const styles = id === null
  301. ? cachedStyles.list
  302. : [cachedStyles.byId.get(id)];
  303. if (!styles[0]) {
  304. // may happen when users [accidentally] reopen an old URL
  305. // of edit.html with a non-existent style id parameter
  306. return asHash ? blankHash : [];
  307. }
  308. const filtered = asHash ? {} : [];
  309. const needSections = asHash || matchUrl !== null;
  310. const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
  311. let style;
  312. for (let i = 0; (style = styles[i]); i++) {
  313. if ((enabled === null || style.enabled === enabled)
  314. && (url === null || style.url === url)
  315. && (id === null || style.id === id)) {
  316. const sections = needSections &&
  317. getApplicableSections({
  318. style,
  319. matchUrl,
  320. strictRegexp,
  321. stopOnFirst: !asHash,
  322. skipUrlCheck: true,
  323. matchUrlBase,
  324. });
  325. if (asHash) {
  326. if (sections.length) {
  327. filtered[style.id] = sections;
  328. }
  329. } else if (matchUrl === null || sections.length) {
  330. filtered.push(style);
  331. }
  332. }
  333. }
  334. cachedStyles.filters.set(cacheKey, {
  335. styles: filtered,
  336. lastHit: Date.now(),
  337. hits: 1,
  338. });
  339. if (cachedStyles.filters.size > 10000) {
  340. cleanupCachedFilters();
  341. }
  342. // a shallow copy is needed because the cache doesn't store options like disableAll
  343. return asHash
  344. ? Object.assign(blankHash, filtered)
  345. : filtered;
  346. }
  347. function saveStyle(style) {
  348. const id = Number(style.id) || null;
  349. const reason = style.reason;
  350. const notify = style.notify !== false;
  351. delete style.method;
  352. delete style.reason;
  353. delete style.notify;
  354. if (!style.name) {
  355. delete style.name;
  356. }
  357. let existed;
  358. let codeIsUpdated;
  359. return maybeCalcDigest()
  360. .then(maybeImportFix)
  361. .then(decide);
  362. function maybeCalcDigest() {
  363. if (reason === 'update' || reason === 'update-digest') {
  364. return calcStyleDigest(style).then(digest => {
  365. style.originalDigest = digest;
  366. });
  367. }
  368. return Promise.resolve();
  369. }
  370. function maybeImportFix() {
  371. if (reason === 'import') {
  372. style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
  373. delete style.styleDigest; // TODO: remove in the future
  374. if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
  375. delete style.originalDigest;
  376. }
  377. }
  378. }
  379. function decide() {
  380. if (id !== null) {
  381. // Update or create
  382. style.id = id;
  383. return dbExec('get', id).then((event, store) => {
  384. const oldStyle = event.target.result;
  385. existed = Boolean(oldStyle);
  386. if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
  387. return style;
  388. }
  389. codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
  390. style = Object.assign({}, oldStyle, style);
  391. return write(style, store);
  392. });
  393. } else {
  394. // Create
  395. delete style.id;
  396. style = Object.assign({
  397. // Set optional things if they're undefined
  398. enabled: true,
  399. updateUrl: null,
  400. md5Url: null,
  401. url: null,
  402. originalMd5: null,
  403. }, style);
  404. return write(style);
  405. }
  406. }
  407. function write(style, store) {
  408. style.sections = normalizeStyleSections(style);
  409. if (store) {
  410. return new Promise(resolve => {
  411. store.put(style).onsuccess = event => resolve(done(event));
  412. });
  413. } else {
  414. return dbExec('put', style).then(done);
  415. }
  416. }
  417. function done(event) {
  418. if (reason === 'update-digest') {
  419. return style;
  420. }
  421. style.id = style.id || event.target.result;
  422. invalidateCache(existed ? {updated: style} : {added: style});
  423. if (notify) {
  424. notifyAllTabs({
  425. method: existed ? 'styleUpdated' : 'styleAdded',
  426. style, codeIsUpdated, reason,
  427. });
  428. }
  429. return style;
  430. }
  431. }
  432. function deleteStyle({id, notify = true}) {
  433. id = Number(id);
  434. return dbExec('delete', id).then(() => {
  435. invalidateCache({deletedId: id});
  436. if (notify) {
  437. notifyAllTabs({method: 'styleDeleted', id});
  438. }
  439. return id;
  440. });
  441. }
  442. function getApplicableSections({
  443. style,
  444. matchUrl,
  445. strictRegexp = true,
  446. // filterStylesInternal() sets the following to avoid recalc on each style:
  447. stopOnFirst,
  448. skipUrlCheck,
  449. matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
  450. // as per spec the fragment portion is ignored in @-moz-document:
  451. // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
  452. // but the spec is outdated and doesn't account for SPA sites
  453. // so we only respect it in case of url("http://exact.url/without/hash")
  454. }) {
  455. if (!skipUrlCheck && !URLS.supported(matchUrl)) {
  456. return [];
  457. }
  458. const sections = [];
  459. for (const section of style.sections) {
  460. const {urls, domains, urlPrefixes, regexps, code} = section;
  461. const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
  462. const isMatching = !isGlobal && (
  463. urls.length
  464. && (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
  465. || urlPrefixes.length
  466. && arraySomeIsPrefix(urlPrefixes, matchUrl)
  467. || domains.length
  468. && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
  469. || regexps.length
  470. && arraySomeMatches(regexps, matchUrl, strictRegexp));
  471. if (isGlobal && !styleCodeEmpty(code) || isMatching) {
  472. sections.push(section);
  473. if (stopOnFirst) {
  474. break;
  475. }
  476. }
  477. }
  478. return sections;
  479. function arraySomeIsPrefix(array, string) {
  480. for (const prefix of array) {
  481. if (string.startsWith(prefix)) {
  482. return true;
  483. }
  484. }
  485. return false;
  486. }
  487. function arraySomeIn(array, haystack) {
  488. for (const el of array) {
  489. if (haystack.indexOf(el) >= 0) {
  490. return true;
  491. }
  492. }
  493. return false;
  494. }
  495. function arraySomeMatches(array, matchUrl, strictRegexp) {
  496. for (const regexp of array) {
  497. for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
  498. const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
  499. let rx = cachedStyles.regexps.get(cacheKey);
  500. if (rx === false) {
  501. // invalid regexp
  502. break;
  503. }
  504. if (!rx) {
  505. const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
  506. rx = tryRegExp(anchored);
  507. cachedStyles.regexps.set(cacheKey, rx || false);
  508. if (!rx) {
  509. // invalid regexp
  510. break;
  511. }
  512. }
  513. if (rx.test(matchUrl)) {
  514. return true;
  515. }
  516. }
  517. }
  518. return false;
  519. }
  520. }
  521. function styleCodeEmpty(code) {
  522. // Collect the global section if it's not empty, not comment-only, not namespace-only.
  523. const cmtOpen = code && code.indexOf('/*');
  524. if (cmtOpen >= 0) {
  525. const cmtCloseLast = code.lastIndexOf('*/');
  526. if (cmtCloseLast < 0) {
  527. code = code.substr(0, cmtOpen);
  528. } else {
  529. code = code.substr(0, cmtOpen) +
  530. code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
  531. code.substr(cmtCloseLast + 2);
  532. }
  533. }
  534. return !code
  535. || !code.trim()
  536. || code.includes('@namespace') && !code.replace(RX_NAMESPACE, '').trim();
  537. }
  538. function styleSectionsEqual({sections: a}, {sections: b}) {
  539. if (!a || !b) {
  540. return undefined;
  541. }
  542. if (a.length !== b.length) {
  543. return false;
  544. }
  545. // order of sections should be identical to account for the case of multiple
  546. // sections matching the same URL because the order of rules is part of cascading
  547. return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
  548. function propertiesEqual(secA, secB) {
  549. for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
  550. if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
  551. return false;
  552. }
  553. }
  554. return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
  555. }
  556. function equalOrEmpty(a, b, telltale, comparator) {
  557. const typeA = a && typeof a[telltale] === 'function';
  558. const typeB = b && typeof b[telltale] === 'function';
  559. return (
  560. (a === null || a === undefined || (typeA && !a.length)) &&
  561. (b === null || b === undefined || (typeB && !b.length))
  562. ) || typeA && typeB && a.length === b.length && comparator(a, b);
  563. }
  564. function arrayMirrors(array1, array2) {
  565. return (
  566. array1.every(el => array2.includes(el)) &&
  567. array2.every(el => array1.includes(el))
  568. );
  569. }
  570. }
  571. function invalidateCache({added, updated, deletedId} = {}) {
  572. if (!cachedStyles.list) {
  573. return;
  574. }
  575. const id = added ? added.id : updated ? updated.id : deletedId;
  576. const cached = cachedStyles.byId.get(id);
  577. if (updated) {
  578. if (cached) {
  579. Object.assign(cached, updated);
  580. cachedStyles.filters.clear();
  581. cachedStyles.needTransitionPatch.delete(id);
  582. return;
  583. } else {
  584. added = updated;
  585. }
  586. }
  587. if (added) {
  588. if (!cached) {
  589. cachedStyles.list.push(added);
  590. cachedStyles.byId.set(added.id, added);
  591. cachedStyles.filters.clear();
  592. cachedStyles.needTransitionPatch.delete(id);
  593. }
  594. return;
  595. }
  596. if (deletedId !== undefined) {
  597. if (cached) {
  598. const cachedIndex = cachedStyles.list.indexOf(cached);
  599. cachedStyles.list.splice(cachedIndex, 1);
  600. cachedStyles.byId.delete(deletedId);
  601. cachedStyles.filters.clear();
  602. cachedStyles.needTransitionPatch.delete(id);
  603. return;
  604. }
  605. }
  606. cachedStyles.list = null;
  607. cachedStyles.filters.clear();
  608. cachedStyles.needTransitionPatch.clear(id);
  609. }
  610. function cleanupCachedFilters({force = false} = {}) {
  611. if (!force) {
  612. debounce(cleanupCachedFilters, 1000, {force: true});
  613. return;
  614. }
  615. const size = cachedStyles.filters.size;
  616. const oldestHit = cachedStyles.filters.values().next().value.lastHit;
  617. const now = Date.now();
  618. const timeSpan = now - oldestHit;
  619. const recencyWeight = 5 / size;
  620. const hitWeight = 1 / 4; // we make ~4 hits per URL
  621. const lastHitWeight = 10;
  622. // delete the oldest 10%
  623. [...cachedStyles.filters.entries()]
  624. .map(([id, v], index) => ({
  625. id,
  626. weight:
  627. index * recencyWeight +
  628. v.hits * hitWeight +
  629. (v.lastHit - oldestHit) / timeSpan * lastHitWeight,
  630. }))
  631. .sort((a, b) => a.weight - b.weight)
  632. .slice(0, size / 10 + 1)
  633. .forEach(({id}) => cachedStyles.filters.delete(id));
  634. }
  635. function getDomains(url) {
  636. let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
  637. if (!d || url.startsWith('file:')) {
  638. return [];
  639. }
  640. const domains = [d];
  641. while (d.indexOf('.') !== -1) {
  642. d = d.substring(d.indexOf('.') + 1);
  643. domains.push(d);
  644. }
  645. return domains;
  646. }
  647. function normalizeStyleSections({sections}) {
  648. // retain known properties in an arbitrarily predefined order
  649. return (sections || []).map(section => ({
  650. code: section.code || '',
  651. urls: section.urls || [],
  652. urlPrefixes: section.urlPrefixes || [],
  653. domains: section.domains || [],
  654. regexps: section.regexps || [],
  655. }));
  656. }
  657. function calcStyleDigest(style) {
  658. const jsonString = style.usercssData ?
  659. style.sourceCode : JSON.stringify(normalizeStyleSections(style));
  660. const text = new TextEncoder('utf-8').encode(jsonString);
  661. return crypto.subtle.digest('SHA-1', text).then(hex);
  662. function hex(buffer) {
  663. const parts = [];
  664. const PAD8 = '00000000';
  665. const view = new DataView(buffer);
  666. for (let i = 0; i < view.byteLength; i += 4) {
  667. parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
  668. }
  669. return parts.join('');
  670. }
  671. }
  672. function handleCssTransitionBug({tabId, frameId, url, styles}) {
  673. for (let id in styles) {
  674. id |= 0;
  675. if (!id) {
  676. continue;
  677. }
  678. let need = cachedStyles.needTransitionPatch.get(id);
  679. if (need === false) {
  680. continue;
  681. }
  682. if (need !== true) {
  683. need = styles[id].some(sectionContainsTransitions);
  684. cachedStyles.needTransitionPatch.set(id, need);
  685. if (!need) {
  686. continue;
  687. }
  688. }
  689. if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
  690. patchFirefox();
  691. } else {
  692. styles.needTransitionPatch = true;
  693. }
  694. break;
  695. }
  696. function patchFirefox() {
  697. const options = {
  698. frameId,
  699. code: CSS_TRANSITION_SUPPRESSOR,
  700. matchAboutBlank: true,
  701. };
  702. if (FIREFOX >= 53) {
  703. options.cssOrigin = 'user';
  704. }
  705. browser.tabs.insertCSS(tabId, Object.assign(options, {
  706. runAt: 'document_start',
  707. })).then(() => setTimeout(() => {
  708. browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
  709. })).catch(ignoreChromeError);
  710. }
  711. function sectionContainsTransitions(section) {
  712. let code = section.code;
  713. const firstTransition = code.indexOf('transition');
  714. if (firstTransition < 0) {
  715. return false;
  716. }
  717. const firstCmt = code.indexOf('/*');
  718. // check the part before the first comment
  719. if (firstCmt < 0 || firstTransition < firstCmt) {
  720. if (quickCheckAround(code, firstTransition)) {
  721. return true;
  722. } else if (firstCmt < 0) {
  723. return false;
  724. }
  725. }
  726. // check the rest
  727. const lastCmt = code.lastIndexOf('*/');
  728. if (lastCmt < firstCmt) {
  729. // the comment is unclosed and we already checked the preceding part
  730. return false;
  731. }
  732. let mid = code.slice(firstCmt, lastCmt + 2);
  733. mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
  734. code = mid + code.slice(lastCmt + 2);
  735. return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
  736. }
  737. function quickCheckAround(code, pos = code.indexOf('transition')) {
  738. return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
  739. }
  740. }