update.js 6.7 KB

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