| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- /* global API msg */// msg.js
- /* global StyleInjector */
- /* global prefs */
- 'use strict';
- (() => {
- if (window.INJECTED === 1) return;
- /** true -> when the page styles are received,
- * false -> when disableAll mode is on at start, the styles won't be sent
- * so while disableAll lasts we can ignore messages about style updates because
- * the tab will explicitly ask for all styles in bulk when disableAll mode ends */
- let hasStyles = false;
- let isDisabled = false;
- let isTab = !chrome.tabs || location.pathname !== '/popup.html';
- const isFrame = window !== parent;
- const isFrameAboutBlank = isFrame && location.href === 'about:blank';
- const isUnstylable = !chrome.app && document instanceof XMLDocument;
- const styleInjector = StyleInjector({
- compare: (a, b) => a.id - b.id,
- onUpdate: onInjectorUpdate,
- });
- // dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
- let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
- location.href;
- // save it now because chrome.runtime will be unavailable in the orphaned script
- const orphanEventId = chrome.runtime.id;
- let isOrphaned;
- // firefox doesn't orphanize content scripts so the old elements stay
- if (!chrome.app) styleInjector.clearOrphans();
- /** @type chrome.runtime.Port */
- let port;
- let lazyBadge = isFrame;
- let parentDomain;
- /* about:blank iframes are often used by sites for file upload or background tasks
- * and they may break if unexpected DOM stuff is present at `load` event
- * so we'll add the styles only if the iframe becomes visible */
- const {IntersectionObserver} = window;
- const xoEventId = `${Math.random()}`;
- /** @type IntersectionObserver */
- let xo;
- if (IntersectionObserver) {
- window[Symbol.for('xo')] = (el, cb) => {
- if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
- el.addEventListener(xoEventId, cb, {once: true});
- xo.observe(el);
- };
- }
- // Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
- const ready = init();
- // the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
- if (!isTab) {
- chrome.tabs.getCurrent(tab => {
- isTab = Boolean(tab);
- if (tab && styleInjector.list.length) updateCount();
- });
- }
- msg.onTab(applyOnMessage);
- if (!chrome.tabs) {
- window.dispatchEvent(new CustomEvent(orphanEventId));
- window.addEventListener(orphanEventId, orphanCheck, true);
- }
- // detect media change in content script
- // FIXME: move this to background page when following bugs are fixed:
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1561546
- // https://bugs.chromium.org/p/chromium/issues/detail?id=968651
- const media = window.matchMedia('(prefers-color-scheme: dark)');
- media.addListener(() => API.colorScheme.updateSystemPreferDark().catch(console.error));
- function onInjectorUpdate() {
- if (!isOrphaned) {
- updateCount();
- const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
- onOff('disableAll', updateDisableAll);
- if (isFrame) {
- updateExposeIframes();
- onOff('exposeIframes', updateExposeIframes);
- }
- }
- }
- async function init() {
- if (isUnstylable) {
- await API.styleViaAPI({method: 'styleApply'});
- } else {
- const SYM_ID = 'styles';
- const SYM = Symbol.for(SYM_ID);
- const parentStyles = isFrameAboutBlank &&
- tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
- const styles =
- window[SYM] ||
- parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
- !isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
- await API.styles.getSectionsByUrl(matchUrl, null, true);
- isDisabled = styles.disableAll;
- hasStyles = !isDisabled;
- if (hasStyles) {
- window[SYM] = styles;
- await styleInjector.apply(styles);
- } else {
- delete window[SYM];
- prefs.subscribe('disableAll', updateDisableAll);
- }
- styleInjector.toggle(hasStyles);
- }
- }
- /** Must be executed inside try/catch */
- function getStylesViaXhr() {
- const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
- const url = 'blob:' + chrome.runtime.getURL(blobId);
- document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, false); // synchronous
- xhr.send();
- URL.revokeObjectURL(url);
- return JSON.parse(xhr.response);
- }
- function applyOnMessage(request) {
- const {method} = request;
- if (isUnstylable) {
- if (method === 'urlChanged') {
- request.method = 'styleReplaceAll';
- }
- if (/^(style|updateCount)/.test(method)) {
- API.styleViaAPI(request);
- return;
- }
- }
- const {style} = request;
- switch (method) {
- case 'ping':
- return true;
- case 'styleDeleted':
- styleInjector.remove(style.id);
- break;
- case 'styleUpdated':
- if (!hasStyles && isDisabled) break;
- if (style.enabled) {
- API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
- sections[style.id]
- ? styleInjector.apply(sections)
- : styleInjector.remove(style.id));
- } else {
- styleInjector.remove(style.id);
- }
- break;
- case 'styleAdded':
- if (!hasStyles && isDisabled) break;
- if (style.enabled) {
- API.styles.getSectionsByUrl(matchUrl, style.id)
- .then(styleInjector.apply);
- }
- break;
- case 'urlChanged':
- if (!hasStyles && isDisabled || matchUrl === request.url) break;
- matchUrl = request.url;
- API.styles.getSectionsByUrl(matchUrl).then(sections => {
- hasStyles = true;
- styleInjector.replace(sections);
- });
- break;
- case 'backgroundReady':
- ready.catch(err =>
- msg.isIgnorableError(err)
- ? init()
- : console.error(err));
- break;
- case 'updateCount':
- updateCount();
- break;
- }
- }
- function updateDisableAll(key, disableAll) {
- isDisabled = disableAll;
- if (isUnstylable) {
- API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
- } else if (!hasStyles && !disableAll) {
- init();
- } else {
- styleInjector.toggle(!disableAll);
- }
- }
- async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
- const attr = 'stylus-iframe';
- const el = document.documentElement;
- if (!el) return; // got no styles so styleInjector didn't wait for <html>
- if (!value || !styleInjector.list.length) {
- el.removeAttribute(attr);
- } else {
- if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
- // Check first to avoid triggering DOM mutation
- if (el.getAttribute(attr) !== parentDomain) {
- el.setAttribute(attr, parentDomain);
- }
- }
- }
- function updateCount() {
- if (!isTab) return;
- if (isFrame) {
- if (!port && styleInjector.list.length) {
- port = chrome.runtime.connect({name: 'iframe'});
- } else if (port && !styleInjector.list.length) {
- port.disconnect();
- }
- if (lazyBadge && performance.now() > 1000) lazyBadge = false;
- }
- (isUnstylable ?
- API.styleViaAPI({method: 'updateCount'}) :
- API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
- ).catch(msg.ignoreError);
- }
- function onFrameElementInView(cb) {
- if (IntersectionObserver) {
- parent[parent.Symbol.for('xo')](frameElement, cb);
- } else {
- requestAnimationFrame(cb);
- }
- }
- /** @param {IntersectionObserverEntry[]} entries */
- function onIntersect(entries) {
- for (const e of entries) {
- if (e.isIntersecting) {
- xo.unobserve(e.target);
- e.target.dispatchEvent(new Event(xoEventId));
- }
- }
- }
- function tryCatch(func, ...args) {
- try {
- return func(...args);
- } catch (e) {}
- }
- function orphanCheck() {
- if (tryCatch(() => chrome.i18n.getUILanguage())) return;
- // In Chrome content script is orphaned on an extension update/reload
- // so we need to detach event listeners
- window.removeEventListener(orphanEventId, orphanCheck, true);
- isOrphaned = true;
- setTimeout(styleInjector.clear, 1000); // avoiding FOUC
- tryCatch(msg.off, applyOnMessage);
- }
- })();
|