'use strict'; document.addEventListener('stylishUpdateChrome', onUpdateClicked); document.addEventListener('stylishInstallChrome', onInstallClicked); new MutationObserver(waitForBody) .observe(document.documentElement, {childList: true}); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // orphaned content script check if (msg.method == 'ping') { sendResponse(true); } }); function waitForBody() { if (!document.body) { return; } this.disconnect(); rebrand([{addedNodes: [document.body]}]); const rebrandObserver = new MutationObserver(rebrand); rebrandObserver.observe(document.body, {childList: true, subtree: true}); document.addEventListener('DOMContentLoaded', function _() { document.removeEventListener('DOMContentLoaded', _); rebrandObserver.disconnect(); chrome.runtime.sendMessage({ method: 'getStyles', url: getMeta('stylish-id-url') || location.href }, checkUpdatability); }); } 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 } document.dispatchEvent(new CustomEvent(type, detail)); } function onInstallClicked() { if (!orphanCheck()) { return; } getResource(getMeta('stylish-description')) .then(name => saveStyleCode('styleInstall', name)) .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); } function onUpdateClicked() { if (!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'}), () => sendEvent('styleInstalledChrome') ); resolve(); }); }); } function getMeta(name) { const e = document.querySelector(`link[rel="${name}"]`); return e ? e.getAttribute('href') : null; } function getResource(url) { if (url.startsWith('#')) { return Promise.resolve(document.getElementById(url.slice(1)).textContent); } return new Promise(resolve => { const xhr = new XMLHttpRequest(); xhr.onloadend = () => resolve(xhr.status < 400 ? xhr.responseText : null); if (url.length > 2000) { const [mainUrl, query] = url.split('?'); xhr.open('POST', mainUrl, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.send(query); } else { xhr.open('GET', url); xhr.send(); } }); } function rebrand(mutations) { /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ for (let m = mutations.length; --m >= 0;) { const added = mutations[m].addedNodes; for (let n = added.length; --n >= 0;) { const addedNode = added[n]; if (addedNode.nodeType != Node.ELEMENT_NODE) { continue; } const elementsToCheck = addedNode.matches('.install-status') ? [addedNode] : addedNode.getElementsByClassName('install-status'); for (let i = elementsToCheck.length; --i >= 0;) { const el = elementsToCheck[i]; if (!el.textContent.includes('Stylish')) { continue; } const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; const text = node.nodeValue; if (text.includes('Stylish') && node.parentNode.localName != 'a') { node.nodeValue = text.replace(/Stylish/g, 'Stylus'); } } } } } } 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 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', 'onInstallClicked', 'onUpdateClicked', 'orphanCheck', 'rebrand', 'saveStyleCode', 'sendEvent', 'styleSectionsEqual', 'waitForBody', ].forEach(fn => (window[fn] = null)); }