update.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
  2. /* global getStyleDigests, updateStyleDigest */
  3. 'use strict';
  4. // eslint-disable-next-line no-var
  5. var updater = {
  6. COUNT: 'count',
  7. UPDATED: 'updated',
  8. SKIPPED: 'skipped',
  9. DONE: 'done',
  10. // details for SKIPPED status
  11. EDITED: 'locally edited',
  12. MAYBE_EDITED: 'may be locally edited',
  13. SAME_MD5: 'up-to-date: MD5 is unchanged',
  14. SAME_CODE: 'up-to-date: code sections are unchanged',
  15. ERROR_MD5: 'error: MD5 is invalid',
  16. ERROR_JSON: 'error: JSON is invalid',
  17. lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
  18. checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
  19. updater.resetInterval();
  20. updater.checkAllStyles.running = true;
  21. return getStyles({}).then(styles => {
  22. styles = styles.filter(style => style.updateUrl);
  23. observer(updater.COUNT, styles.length);
  24. updater.log('');
  25. updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
  26. return Promise.all(
  27. styles.map(style =>
  28. updater.checkStyle({style, observer, save, ignoreDigest})));
  29. }).then(() => {
  30. observer(updater.DONE);
  31. updater.log('');
  32. updater.checkAllStyles.running = false;
  33. });
  34. },
  35. checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
  36. let hasDigest;
  37. /*
  38. Original style digests are calculated in these cases:
  39. * style is installed or updated from server
  40. * style is checked for an update and its code is equal to the server code
  41. Update check proceeds in these cases:
  42. * style has the original digest and it's equal to the current digest
  43. * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
  44. * [ignoreDigest: none/false] style doesn't yet have the original digest
  45. so we compare the code to the server code and if it's the same we save the digest,
  46. otherwise we skip the style and report MAYBE_EDITED status
  47. 'ignoreDigest' option is set on the second manual individual update check on the manage page.
  48. */
  49. return getStyleDigests(style)
  50. .then(maybeFetchMd5)
  51. .then(maybeFetchCode)
  52. .then(maybeSave)
  53. .then(saved => {
  54. observer(updater.UPDATED, saved);
  55. updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
  56. })
  57. .catch(err => {
  58. observer(updater.SKIPPED, style, err);
  59. err = err === 0 ? 'server unreachable' : err;
  60. updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
  61. });
  62. function maybeFetchMd5([originalDigest, current]) {
  63. hasDigest = Boolean(originalDigest);
  64. if (hasDigest && !ignoreDigest && originalDigest != current) {
  65. return Promise.reject(updater.EDITED);
  66. }
  67. return download(style.md5Url);
  68. }
  69. function maybeFetchCode(md5) {
  70. if (!md5 || md5.length != 32) {
  71. return Promise.reject(updater.ERROR_MD5);
  72. }
  73. if (md5 == style.originalMd5 && hasDigest && !ignoreDigest) {
  74. return Promise.reject(updater.SAME_MD5);
  75. }
  76. return download(style.updateUrl);
  77. }
  78. function maybeSave(text) {
  79. const json = tryJSONparse(text);
  80. if (!styleJSONseemsValid(json)) {
  81. return Promise.reject(updater.ERROR_JSON);
  82. }
  83. json.id = style.id;
  84. if (styleSectionsEqual(json, style)) {
  85. // JSONs may have different order of items even if sections are effectively equal
  86. // so we'll update the digest anyway
  87. updateStyleDigest(json);
  88. return Promise.reject(updater.SAME_CODE);
  89. } else if (!hasDigest && !ignoreDigest) {
  90. return Promise.reject(updater.MAYBE_EDITED);
  91. }
  92. return !save ? json :
  93. saveStyle(Object.assign(json, {
  94. name: null, // keep local name customizations
  95. reason: 'update',
  96. }));
  97. }
  98. function styleJSONseemsValid(json) {
  99. return json
  100. && json.sections
  101. && json.sections.length
  102. && typeof json.sections.every == 'function'
  103. && typeof json.sections[0].code == 'string';
  104. }
  105. },
  106. schedule() {
  107. const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
  108. if (interval) {
  109. const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
  110. debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
  111. } else {
  112. debounce.unregister(updater.checkAllStyles);
  113. }
  114. },
  115. resetInterval() {
  116. localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
  117. updater.schedule();
  118. },
  119. log: (() => {
  120. let queue = [];
  121. let lastWriteTime = 0;
  122. return text => {
  123. queue.push({text, time: new Date().toLocaleString()});
  124. debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
  125. };
  126. function flushQueue() {
  127. chromeLocal.getValue('updateLog').then((lines = []) => {
  128. const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
  129. if (!queue[0].text) {
  130. queue.shift();
  131. if (lines[lines.length - 1]) {
  132. lines.push('');
  133. }
  134. }
  135. lines.splice(0, lines.length - 1000);
  136. lines.push(time + queue[0].text);
  137. lines.push(...queue.slice(1).map(item => item.text));
  138. chromeLocal.setValue('updateLog', lines);
  139. lastWriteTime = Date.now();
  140. queue = [];
  141. });
  142. }
  143. })(),
  144. };
  145. updater.schedule();
  146. prefs.subscribe(updater.schedule, ['updateInterval']);