'use strict'; const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium const FIREFOX = /Firefox/.test(navigator.userAgent); const VIVALDI = /Vivaldi/.test(navigator.userAgent); const OPERA = /OPR/.test(navigator.userAgent); document.addEventListener('stylishUpdate', onUpdateClicked); document.addEventListener('stylishUpdateChrome', onUpdateClicked); document.addEventListener('stylishUpdateOpera', onUpdateClicked); document.addEventListener('stylishInstall', onInstallClicked); document.addEventListener('stylishInstallChrome', onInstallClicked); document.addEventListener('stylishInstallOpera', onInstallClicked); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // orphaned content script check if (msg.method === 'ping') { sendResponse(true); } }); // TODO: remove the following statement when USO is fixed document.documentElement.appendChild(document.createElement('script')).text = '(' + function () { let settings; document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { document.removeEventListener('stylusFixBuggyUSOsettings', _); settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search); }); const originalResponseJson = Response.prototype.json; Response.prototype.json = function (...args) { return originalResponseJson.call(this, ...args).then(json => { Response.prototype.json = originalResponseJson; if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { return json; } const images = new Map(); for (const jsonSetting of json.style_settings) { let value = settings.get('ik-' + jsonSetting.install_key); if (!value || !jsonSetting.style_setting_options || !jsonSetting.style_setting_options[0]) { continue; } if (value.startsWith('ik-')) { value = value.replace(/^ik-/, ''); const defaultItem = jsonSetting.style_setting_options.find(item => item.default); if (!defaultItem || defaultItem.install_key !== value) { if (defaultItem) { defaultItem.default = false; } jsonSetting.style_setting_options.some(item => { if (item.install_key === value) { item.default = true; return true; } }); } } else if (jsonSetting.setting_type === 'image') { jsonSetting.style_setting_options.some(item => { if (item.default) { item.default = false; return true; } }); images.set(jsonSetting.install_key, value); } else { const item = jsonSetting.style_setting_options[0]; if (item.value !== value && item.install_key === 'placeholder') { item.value = value; } } } if (images.size) { new MutationObserver((_, observer) => { if (!document.getElementById('style-settings')) { return; } observer.disconnect(); for (const [name, url] of images.entries()) { const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); if (elUrl) { elUrl.value = url; } } }).observe(document, {childList: true, subtree: true}); } return json; }); }; } + ')()'; // TODO: remove the following statement when USO pagination is fixed if (location.search.includes('category=')) { document.addEventListener('DOMContentLoaded', function _() { document.removeEventListener('DOMContentLoaded', _); new MutationObserver((_, observer) => { if (!document.getElementById('pagination')) { return; } observer.disconnect(); const category = '&' + location.search.match(/category=[^&]+/)[0]; const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); for (let i = 0; i < links.length; i++) { links[i].href += category; } }).observe(document, {childList: true, subtree: true}); }); } new MutationObserver((mutations, observer) => { if (document.body) { observer.disconnect(); // TODO: remove the following statement when USO pagination title is fixed document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: '); chrome.runtime.sendMessage({ method: 'getStyles', url: getMeta('stylish-id-url') || location.href }, checkUpdatability); } }).observe(document.documentElement, {childList: true}); /* since we are using "stylish-code-chrome" meta key on all browsers and US.o does not provide "advanced settings" on this url if browser is not Chrome, we need to fix this URL using "stylish-update-url" meta key */ function getStyleURL() { const url = getMeta('stylish-code-chrome'); // TODO: remove when USO is fixed const directUrl = getMeta('stylish-update-url'); if (directUrl.includes('?') && !url.includes('?')) { /* get custom settings from the update url */ return Object.assign(new URL(url), { search: (new URL(directUrl)).search }).href; } return url; } function checkUpdatability([installedStyle]) { // TODO: remove the following statement when USO is fixed document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { detail: installedStyle && installedStyle.updateUrl, })); 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(getStyleURL()).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) { if (FIREFOX) { type = type.replace('Chrome', ''); } else if (OPERA || VIVALDI) { type = type.replace('Chrome', 'Opera'); } 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; } enableUpdateButton(false); getResource(getStyleURL()).then(code => { chrome.runtime.sendMessage( Object.assign(JSON.parse(code), addProps, { method: 'saveStyle', reason: 'update', }), style => { if (message === 'styleUpdate' && style.updateUrl.includes('?')) { enableUpdateButton(true); } else { sendEvent('styleInstalledChrome'); } } ); resolve(); }); }); function enableUpdateButton(state) { const button = document.getElementById('update_style_button'); if (button) { button.style.cssText = state ? '' : 'pointer-events: none !important; opacity: .25 !important;'; } } } 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('stylishUpdate', onUpdateClicked); document.removeEventListener('stylishUpdateChrome', onUpdateClicked); document.removeEventListener('stylishUpdateOpera', onUpdateClicked); document.removeEventListener('stylishInstall', onInstallClicked); document.removeEventListener('stylishInstallChrome', onInstallClicked); document.removeEventListener('stylishInstallOpera', 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)); }