'use strict'; document.addEventListener('stylishUpdateChrome', onUpdateClicked); document.addEventListener('stylishInstallChrome', onInstallClicked); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // orphaned content script check if (msg.method == 'ping') { sendResponse(true); } }); new MutationObserver((mutations, observer) => { if (document.body) { observer.disconnect(); chrome.runtime.sendMessage({ method: 'getStyles', url: getMeta('stylish-id-url') || location.href }, checkUpdatability); } }).observe(document.documentElement, {childList: true}); function checkUpdatability([installedStyle]) { if (!installedStyle) { sendEvent('styleCanBeInstalledChrome'); return; } const md5Url = getMeta('stylish-md5-url'); if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { getResource(md5Url).then(md5 => { reportUpdatable(md5 != installedStyle.originalMd5); }); } else { getResource(getMeta('stylish-code-chrome')).then(code => { reportUpdatable(code === null || !styleSectionsEqual(JSON.parse(code), installedStyle)); }); } function reportUpdatable(isUpdatable) { sendEvent( isUpdatable ? 'styleCanBeUpdatedChrome' : 'styleAlreadyInstalledChrome', { updateUrl: installedStyle.updateUrl } ); } } function sendEvent(type, detail = null) { detail = {detail}; if (typeof cloneInto != 'undefined') { // Firefox requires explicit cloning, however USO can't process our messages anyway // because USO tries to use a global "event" variable deprecated in Firefox detail = cloneInto(detail, document); // eslint-disable-line no-undef } onDOMready().then(() => { document.dispatchEvent(new CustomEvent(type, detail)); }); } function onInstallClicked() { if (!orphanCheck || !orphanCheck()) { return; } getResource(getMeta('stylish-description')) .then(name => saveStyleCode('styleInstall', name)) .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); } function onUpdateClicked() { if (!orphanCheck || !orphanCheck()) { return; } chrome.runtime.sendMessage({ method: 'getStyles', url: getMeta('stylish-id-url') || location.href, }, ([style]) => { saveStyleCode('styleUpdate', style.name, {id: style.id}); }); } function saveStyleCode(message, name, addProps) { return new Promise(resolve => { if (!confirm(chrome.i18n.getMessage(message, [name]))) { return; } getResource(getMeta('stylish-code-chrome')).then(code => { chrome.runtime.sendMessage( Object.assign(JSON.parse(code), addProps, { method: 'saveStyle', reason: 'update', }), () => sendEvent('styleInstalledChrome') ); resolve(); }); }); } function getMeta(name) { const e = document.querySelector(`link[rel="${name}"]`); return e ? e.getAttribute('href') : null; } function getResource(url) { return new Promise(resolve => { if (url.startsWith('#')) { resolve(document.getElementById(url.slice(1)).textContent); } else { chrome.runtime.sendMessage({method: 'download', url}, resolve); } }); } function styleSectionsEqual({sections: a}, {sections: b}) { if (!a || !b) { return undefined; } if (a.length != b.length) { return false; } const checkedInB = []; return a.every(sectionA => b.some(sectionB => { if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { checkedInB.push(sectionB); return true; } })); function propertiesEqual(secA, secB) { for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { return false; } } return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); } function equalOrEmpty(a, b, telltale, comparator) { const typeA = a && typeof a[telltale] == 'function'; const typeB = b && typeof b[telltale] == 'function'; return ( (a === null || a === undefined || (typeA && !a.length)) && (b === null || b === undefined || (typeB && !b.length)) ) || typeA && typeB && a.length == b.length && comparator(a, b); } function arrayMirrors(array1, array2) { for (const el of array1) { if (array2.indexOf(el) < 0) { return false; } } for (const el of array2) { if (array1.indexOf(el) < 0) { return false; } } return true; } } function onDOMready() { if (document.readyState != 'loading') { return Promise.resolve(); } return new Promise(resolve => { document.addEventListener('DOMContentLoaded', function _() { document.removeEventListener('DOMContentLoaded', _); resolve(); }); }); } function orphanCheck() { const port = chrome.runtime.connect(); if (port) { port.disconnect(); return true; } // we're orphaned due to an extension update // we can detach event listeners document.removeEventListener('stylishUpdateChrome', onUpdateClicked); document.removeEventListener('stylishInstallChrome', onInstallClicked); // we can't detach chrome.runtime.onMessage because it's no longer connected internally // we can destroy global functions in this context to free up memory [ 'checkUpdatability', 'getMeta', 'getResource', 'onDOMready', 'onInstallClicked', 'onUpdateClicked', 'orphanCheck', 'saveStyleCode', 'sendEvent', 'styleSectionsEqual', ].forEach(fn => (window[fn] = null)); }