install.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. 'use strict';
  2. document.addEventListener('stylishUpdateChrome', onUpdateClicked);
  3. document.addEventListener('stylishInstallChrome', onInstallClicked);
  4. chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  5. // orphaned content script check
  6. if (msg.method == 'ping') {
  7. sendResponse(true);
  8. }
  9. });
  10. new MutationObserver((mutations, observer) => {
  11. if (document.body) {
  12. observer.disconnect();
  13. chrome.runtime.sendMessage({
  14. method: 'getStyles',
  15. url: getMeta('stylish-id-url') || location.href
  16. }, checkUpdatability);
  17. }
  18. }).observe(document.documentElement, {childList: true});
  19. function checkUpdatability([installedStyle]) {
  20. if (!installedStyle) {
  21. sendEvent('styleCanBeInstalledChrome');
  22. return;
  23. }
  24. const md5Url = getMeta('stylish-md5-url');
  25. if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
  26. getResource(md5Url).then(md5 => {
  27. reportUpdatable(md5 != installedStyle.originalMd5);
  28. });
  29. } else {
  30. getResource(getMeta('stylish-code-chrome')).then(code => {
  31. reportUpdatable(code === null ||
  32. !styleSectionsEqual(JSON.parse(code), installedStyle));
  33. });
  34. }
  35. function reportUpdatable(isUpdatable) {
  36. sendEvent(
  37. isUpdatable
  38. ? 'styleCanBeUpdatedChrome'
  39. : 'styleAlreadyInstalledChrome',
  40. {
  41. updateUrl: installedStyle.updateUrl
  42. }
  43. );
  44. }
  45. }
  46. function sendEvent(type, detail = null) {
  47. detail = {detail};
  48. if (typeof cloneInto != 'undefined') {
  49. // Firefox requires explicit cloning, however USO can't process our messages anyway
  50. // because USO tries to use a global "event" variable deprecated in Firefox
  51. detail = cloneInto(detail, document); // eslint-disable-line no-undef
  52. }
  53. onDOMready().then(() => {
  54. document.dispatchEvent(new CustomEvent(type, detail));
  55. });
  56. }
  57. function onInstallClicked() {
  58. if (!orphanCheck || !orphanCheck()) {
  59. return;
  60. }
  61. getResource(getMeta('stylish-description'))
  62. .then(name => saveStyleCode('styleInstall', name))
  63. .then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
  64. }
  65. function onUpdateClicked() {
  66. if (!orphanCheck || !orphanCheck()) {
  67. return;
  68. }
  69. chrome.runtime.sendMessage({
  70. method: 'getStyles',
  71. url: getMeta('stylish-id-url') || location.href,
  72. }, ([style]) => {
  73. saveStyleCode('styleUpdate', style.name, {id: style.id});
  74. });
  75. }
  76. function saveStyleCode(message, name, addProps) {
  77. return new Promise(resolve => {
  78. if (!confirm(chrome.i18n.getMessage(message, [name]))) {
  79. return;
  80. }
  81. getResource(getMeta('stylish-code-chrome')).then(code => {
  82. chrome.runtime.sendMessage(
  83. Object.assign(JSON.parse(code), addProps, {
  84. method: 'saveStyle',
  85. reason: 'update',
  86. }),
  87. () => sendEvent('styleInstalledChrome')
  88. );
  89. resolve();
  90. });
  91. });
  92. }
  93. function getMeta(name) {
  94. const e = document.querySelector(`link[rel="${name}"]`);
  95. return e ? e.getAttribute('href') : null;
  96. }
  97. function getResource(url) {
  98. return new Promise(resolve => {
  99. if (url.startsWith('#')) {
  100. resolve(document.getElementById(url.slice(1)).textContent);
  101. } else {
  102. chrome.runtime.sendMessage({method: 'download', url}, resolve);
  103. }
  104. });
  105. }
  106. function styleSectionsEqual({sections: a}, {sections: b}) {
  107. if (!a || !b) {
  108. return undefined;
  109. }
  110. if (a.length != b.length) {
  111. return false;
  112. }
  113. const checkedInB = [];
  114. return a.every(sectionA => b.some(sectionB => {
  115. if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
  116. checkedInB.push(sectionB);
  117. return true;
  118. }
  119. }));
  120. function propertiesEqual(secA, secB) {
  121. for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
  122. if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
  123. return false;
  124. }
  125. }
  126. return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
  127. }
  128. function equalOrEmpty(a, b, telltale, comparator) {
  129. const typeA = a && typeof a[telltale] == 'function';
  130. const typeB = b && typeof b[telltale] == 'function';
  131. return (
  132. (a === null || a === undefined || (typeA && !a.length)) &&
  133. (b === null || b === undefined || (typeB && !b.length))
  134. ) || typeA && typeB && a.length == b.length && comparator(a, b);
  135. }
  136. function arrayMirrors(array1, array2) {
  137. for (const el of array1) {
  138. if (array2.indexOf(el) < 0) {
  139. return false;
  140. }
  141. }
  142. for (const el of array2) {
  143. if (array1.indexOf(el) < 0) {
  144. return false;
  145. }
  146. }
  147. return true;
  148. }
  149. }
  150. function onDOMready() {
  151. if (document.readyState != 'loading') {
  152. return Promise.resolve();
  153. }
  154. return new Promise(resolve => {
  155. document.addEventListener('DOMContentLoaded', function _() {
  156. document.removeEventListener('DOMContentLoaded', _);
  157. resolve();
  158. });
  159. });
  160. }
  161. function orphanCheck() {
  162. const port = chrome.runtime.connect();
  163. if (port) {
  164. port.disconnect();
  165. return true;
  166. }
  167. // we're orphaned due to an extension update
  168. // we can detach event listeners
  169. document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
  170. document.removeEventListener('stylishInstallChrome', onInstallClicked);
  171. // we can't detach chrome.runtime.onMessage because it's no longer connected internally
  172. // we can destroy global functions in this context to free up memory
  173. [
  174. 'checkUpdatability',
  175. 'getMeta',
  176. 'getResource',
  177. 'onDOMready',
  178. 'onInstallClicked',
  179. 'onUpdateClicked',
  180. 'orphanCheck',
  181. 'saveStyleCode',
  182. 'sendEvent',
  183. 'styleSectionsEqual',
  184. ].forEach(fn => (window[fn] = null));
  185. }