Repository: darkreader/darkreader
Pull request #6888 (2021-09-24 01:21:04)

Fix for bankofamerica.com
The first fix (.fsdnav-sub-nav-left) fixes the hover submenus (Accounts, Bill Pay...) which are otherwise unreadable over a transparent background.
The second section is cosmetic, as the background images of some elements get badly inverted, but that's IMHO. Feel free to just push the first modification if you don't agree.

If needed, I can provide before/after photos from the user dashboard (which is accessible only with an account)


Patch:
diff --git a/src/background/extension.ts b/src/background/extension.ts
index 7380bcbd..9bfbfc36 100644
--- a/src/background/extension.ts
+++ b/src/background/extension.ts
@@ -61,6 +61,7 @@ export class Extension {
                 }
                 return this.getConnectionMessage(url, frameURL);
             },
+            getTabMessage: this.getTabMessage,
             onColorSchemeChange: this.onColorSchemeChange,
         });
         this.user = new UserStorage({onRemoteSettingsChange: () => this.onRemoteSettingsChange()});
@@ -349,7 +350,7 @@ export class Extension {
         if (this.wasEnabledOnLastCheck === null || this.wasEnabledOnLastCheck !== isEnabled) {
             this.wasEnabledOnLastCheck = isEnabled;
             this.onAppToggle();
-            this.tabs.sendMessage(this.getTabMessage);
+            this.tabs.sendMessage();
             this.reportChanges();
             this.stateManager.saveState();
         }
@@ -468,7 +469,7 @@ export class Extension {
         }
         await this.stateManager.loadState();
         this.wasEnabledOnLastCheck = this.isEnabled;
-        this.tabs.sendMessage(this.getTabMessage);
+        this.tabs.sendMessage();
         this.saveUserSettings();
         this.reportChanges();
         this.stateManager.saveState();
diff --git a/src/background/messenger.ts b/src/background/messenger.ts
index 88288216..d5dae38a 100644
--- a/src/background/messenger.ts
+++ b/src/background/messenger.ts
@@ -27,7 +27,7 @@ export default class Messenger {
     constructor(adapter: ExtensionAdapter) {
         this.adapter = adapter;
         this.changeListenerCount = 0;
-        const allowedSenderURL = [chrome.runtime.getURL('/ui/popup/index.html'), chrome.runtime.getURL('/ui/devtools/index.html')];
+        const allowedSenderURL = [chrome.runtime.getURL('/ui/popup/index.html'), chrome.runtime.getURL('/ui/devtools/index.html'), chrome.runtime.getURL('/ui/stylesheet-editor/index.html')];
         chrome.runtime.onMessage.addListener((message: Message, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) => {
             if (allowedSenderURL.includes(sender.url)) {
                 this.onUIMessage(message, sendResponse);
diff --git a/src/background/tab-manager.ts b/src/background/tab-manager.ts
index 73cc5c8a..cab721df 100644
--- a/src/background/tab-manager.ts
+++ b/src/background/tab-manager.ts
@@ -3,7 +3,7 @@ import {createFileLoader} from './utils/network';
 import type {Message} from '../definitions';
 import {isFirefox, isMV3, isThunderbird} from '../utils/platform';
 import {MessageType} from '../utils/message';
-import {logInfo, logWarn} from '../utils/log';
+import {logWarn} from '../utils/log';
 import {StateManager} from './utils/state-manager';
 
 async function queryTabs(query: chrome.tabs.QueryInfo) {
@@ -20,16 +20,19 @@ interface ConnectionMessageOptions {
 
 interface TabManagerOptions {
     getConnectionMessage: (options: ConnectionMessageOptions) => Message | Promise<Message>;
+    getTabMessage: (url: string, frameUrl: string) => Message;
     onColorSchemeChange: ({isDark}: {isDark: boolean}) => void;
 }
 
 interface FrameInfo {
-    url: string;
+    url?: string;
     state: DocumentState;
+    timestamp: number;
 }
 
 interface TabManagerState {
     tabs: {[tabId: number]: {[frameId: number]: FrameInfo}};
+    timestamp: number;
 }
 
 /*
@@ -50,14 +53,17 @@ export default class TabManager {
     private tabs: {[tabId: number]: {[frameId: number]: FrameInfo}};
     private stateManager: StateManager<TabManagerState>;
     private fileLoader: any = null;
+    private getTabMessage: (url: string, frameUrl: string) => Message;
+    private timestamp: number = null;
     static LOCAL_STORAGE_KEY = 'TabManager-state';
 
-    constructor({getConnectionMessage, onColorSchemeChange}: TabManagerOptions) {
-        this.stateManager = new StateManager<TabManagerState>(TabManager.LOCAL_STORAGE_KEY, this, {tabs: {}});
+    constructor({getConnectionMessage, onColorSchemeChange, getTabMessage}: TabManagerOptions) {
+        this.stateManager = new StateManager<TabManagerState>(TabManager.LOCAL_STORAGE_KEY, this, {tabs: {}, timestamp: 0});
         this.tabs = {};
+        this.getTabMessage = getTabMessage;
 
         chrome.runtime.onMessage.addListener(async (message: Message, sender, sendResponse) => {
-            function addFrame(tabs: {[tabId: number]: {[frameId: number]: FrameInfo}}, tabId: number, frameId: number, senderURL: string) {
+            function addFrame(tabs: {[tabId: number]: {[frameId: number]: FrameInfo}}, tabId: number, frameId: number, senderURL: string, timestamp: number) {
                 let frames: {[frameId: number]: FrameInfo};
                 if (tabs[tabId]) {
                     frames = tabs[tabId];
@@ -65,7 +71,11 @@ export default class TabManager {
                     frames = {};
                     tabs[tabId] = frames;
                 }
-                frames[frameId] = {url: senderURL, state: DocumentState.ACTIVE};
+                frames[frameId] = {
+                    url: senderURL,
+                    state: DocumentState.ACTIVE,
+                    timestamp,
+                };
             }
 
             switch (message.type) {
@@ -92,7 +102,7 @@ export default class TabManager {
                         } else {
                             sendResponse('unsupportedSender');
                         }
-                        return;
+                        return true;
                     }
 
                     const tabId = sender.tab.id;
@@ -100,7 +110,7 @@ export default class TabManager {
                     const senderURL = sender.url;
                     const tabURL = sender.tab.url;
 
-                    addFrame(this.tabs, tabId, frameId, senderURL);
+                    addFrame(this.tabs, tabId, frameId, senderURL, this.timestamp);
 
                     reply({
                         url: tabURL,
@@ -132,14 +142,28 @@ export default class TabManager {
                 }
                 case MessageType.CS_FRAME_FREEZE:
                     await this.stateManager.loadState();
-                    this.tabs[sender.tab.id][sender.frameId].state = DocumentState.FROZEN;
+                    const info = this.tabs[sender.tab.id][sender.frameId];
+                    info.state = DocumentState.FROZEN;
+                    info.url = null;
                     this.stateManager.saveState();
                     break;
-                case MessageType.CS_FRAME_RESUME:
+                case MessageType.CS_FRAME_RESUME: {
                     await this.stateManager.loadState();
-                    addFrame(this.tabs, sender.tab.id, sender.frameId, sender.url);
+                    const tabId = sender.tab.id;
+                    const frameId = sender.frameId;
+                    const frameURL = sender.url;
+                    if (this.tabs[tabId][frameId].timestamp < this.timestamp) {
+                        const message = this.getTabMessage(this.getTabURL(sender.tab), frameURL);
+                        chrome.tabs.sendMessage<Message>(tabId, message, {frameId});
+                    }
+                    this.tabs[sender.tab.id][sender.frameId] = {
+                        url: sender.url,
+                        state: DocumentState.ACTIVE,
+                        timestamp: this.timestamp,
+                    };
                     this.stateManager.saveState();
                     break;
+                }
 
                 case MessageType.CS_FETCH: {
                     // Using custom response due to Chrome and Firefox incompatibility
@@ -187,6 +211,8 @@ export default class TabManager {
                     break;
                 }
             }
+            sendResponse({type: '¯\\_(ツ)_/¯'});
+            return true;
         });
     }
 
@@ -232,23 +258,24 @@ export default class TabManager {
         });
     }
 
-    async sendMessage(getMessage: (url: string, frameUrl: string) => Message) {
+    async sendMessage() {
+        this.timestamp++;
+
         (await queryTabs({}))
             .filter((tab) => Boolean(this.tabs[tab.id]))
             .forEach((tab) => {
                 const frames = this.tabs[tab.id];
-                Object.entries(frames).forEach(([, {url, state}], frameId) => {
-                    if (state !== DocumentState.ACTIVE && state !== DocumentState.PASSIVE) {
-                        // TODO: avoid sending messages to frozen tabs for performance reasons.
-                        logInfo('Sending message to a frozen tab.');
-                    }
-                    const message = getMessage(this.getTabURL(tab), frameId === 0 ? null : url);
-                    if (tab.active && frameId === 0) {
-                        chrome.tabs.sendMessage<Message>(tab.id, message, {frameId});
-                    } else {
-                        setTimeout(() => chrome.tabs.sendMessage<Message>(tab.id, message, {frameId}));
-                    }
-                });
+                Object.entries(frames)
+                    .filter(([, {state}]) => state === DocumentState.ACTIVE || state === DocumentState.PASSIVE)
+                    .forEach(([, {url}], frameId) => {
+                        const message = this.getTabMessage(this.getTabURL(tab), frameId === 0 ? null : url);
+                        if (tab.active && frameId === 0) {
+                            chrome.tabs.sendMessage<Message>(tab.id, message, {frameId});
+                        } else {
+                            setTimeout(() => chrome.tabs.sendMessage<Message>(tab.id, message, {frameId}));
+                        }
+                        this.tabs[tab.id][frameId].timestamp = this.timestamp;
+                    });
             });
     }
 
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index e8cb2260..3c909d18 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -299,6 +299,9 @@ body {
 
 INVERT
 #map
+path[d^="M34.8"]
+._oow7vi
+path[d^="M15.759"]
 
 CSS
 a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, html, i, iframe, img, ins, kbd, label, legend, li, main, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video {
@@ -1514,6 +1517,20 @@ img[alt="Bankier.TV"]
 
 ================================
 
+bankofamerica.com
+
+CSS
+.fsdnav-sub-nav-left {
+    background-color: var(--darkreader-neutral-background) !important;
+}
+.ad-acct-layout,.ad-acct-detail-card-layout,
+.thrfwl-body,
+.ad-summary-container,.olb-summary-widget-container {
+    background-image: none !important;
+}
+
+================================
+
 basecamp.com
 
 INVERT
@@ -2003,6 +2020,18 @@ INVERT
 
 ================================
 
+boxberry.ru
+
+INVERT
+.slider__image
+img[src$="cancel.png"]
+.box_img_block
+img[src$="Vector.png"]
+ymaps[class$="ground-pane"]
+ymaps[class$="places-pane"] > ymaps > ymaps > ymaps
+
+================================
+
 bpmn.io
 
 IGNORE INLINE STYLE
@@ -2285,6 +2314,13 @@ INVERT
 
 ================================
 
+cbpp.org
+
+INVERT
+.navbar-brand
+
+================================
+
 cdc.gov
 
 INVERT
@@ -2605,7 +2641,7 @@ CSS
 cnn.com
 
 INVERT
-[data-test="section-link"]
+[data-test="section-link"] > svg
 img.metadata-header__logo
 
 IGNORE INLINE STYLE
@@ -3912,9 +3948,6 @@ CSS
 .docs-text-ui-cursor-blink {
     fill: ${black} !important;
 }
-.ndfHFb-c4YZDc.ndfHFb-c4YZDc-i5oIFb .ndfHFb-c4YZDc-Bz112c:not([onclick]):not(:link):not(:visited) {
-    background-image: url('//ssl.gstatic.com/docs/common/viewer/v3/v-sprite26.svg') !important;
-}
 .docs-title-input-label:not([style*="pointer-events: auto"]) > #docs-title-input-label-inner {
     visibility: hidden !important;
 }
@@ -4022,16 +4055,6 @@ INVERT
 div[role="menu"] > div[role="menuitem"] > div > div > div
 div[role="menu"] > div[role="menuitem"] > div > div
 
-CSS
-div[role="toolbar"] div[role="button"] > div[class*='-']:not([onclick]):not(:link):not(:visited):not([style*="background-image"]):first-child,
-div[role="toolbar"] div[role="button"] > div[class*='-']:not([onclick]):not(:link):not(:visited) > :nth-child(2) > div,
-div[role="toolbar"] + div > div > div[role="button"] > div[class*='-']:not([onclick]):not(:link):not(:visited),
-div[style^="bottom: 0px;"] > div[style^="opacity:"] div[role="button"] > div:not([onclick]):not(:link):not(:visited),
-div[role="menu"] > div[role="menuitem"] > div > div:not([style*="background-image"]),
-div[role="region"] > div[role="button"] + div div[tabindex="0"] div:not([aria-label*="@"]) > div[role="img"]:not([onclick]):not(:link):not(:visited) {
-    background-image: url('//ssl.gstatic.com/docs/common/viewer/v3/v-sprite38.svg') !important;
-}
-
 ================================
 
 drive.google.com/picker
@@ -4248,6 +4271,10 @@ economist.com
 INVERT
 #ds-wordmark-1843 path:nth-child(5)
 
+IGNORE INLINE STYLE
+#ds-economist-logo > path
+.ds-share-link > svg > g > circle
+
 ================================
 
 edstem.org
@@ -4559,6 +4586,13 @@ INVERT
 
 ================================
 
+f-droid.org
+
+INVERT
+.anti-feature-icon
+
+================================
+
 facebook.com
 
 INVERT
@@ -4999,6 +5033,13 @@ CSS
 
 ================================
 
+fiverr.com
+
+IGNORE INLINE STYLE
+.site-logo *
+
+================================
+
 fivethirtyeight.com
 
 INVERT
@@ -5821,6 +5862,13 @@ CSS
 
 ================================
 
+gizmodo.com
+
+INVERT
+[aria-label="Gizmodo logo"]
+
+================================
+
 glasswire.com
 
 INVERT
@@ -8004,6 +8052,7 @@ img[src$="profile_mask2.png"]
 .rY>.sa
 .buh
 .n3 .qr
+.aii + .qj
 .aih + .qj
 .asor
 .aiC
@@ -13343,6 +13392,14 @@ CSS
 
 ================================
 
+urbandecay.com
+
+IGNORE INLINE STYLE
+.c-select__icon
+.c-swatch
+
+================================
+
 urbandictionary.com
 
 INVERT
diff --git a/src/inject/dynamic-theme/css-rules.ts b/src/inject/dynamic-theme/css-rules.ts
index cd003f12..be5b823e 100644
--- a/src/inject/dynamic-theme/css-rules.ts
+++ b/src/inject/dynamic-theme/css-rules.ts
@@ -92,7 +92,7 @@ export const cssURLRegex = /url\((('.+?')|(".+?")|([^\)]*?))\)/g;
 export const cssImportRegex = /@import\s*(url\()?(('.+?')|(".+?")|([^\)]*?))\)?;?/g;
 
 export function getCSSURLValue(cssURL: string) {
-    return cssURL.replace(/^url\((.*)\)$/, '$1').replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
+    return cssURL.replace(/^url\((.*)\)$/, '$1').trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
 }
 
 export function getCSSBaseBath(url: string) {
diff --git a/src/utils/url.ts b/src/utils/url.ts
index 8d1410e4..d09b4589 100644
--- a/src/utils/url.ts
+++ b/src/utils/url.ts
@@ -33,7 +33,11 @@ export function getAbsoluteURL($base: string, $relative: string) {
     if ($relative.match(/^data\\?\:/)) {
         return $relative;
     }
-
+    // Check if relative starts with `//hostname...`.
+    // We have to add a protocol to make it absolute.
+    if (/^\/\//.test($relative)) {
+        return `${location.protocol}${$relative}`;
+    }
     const b = parseURL($base);
     const a = parseURL($relative, b.href);
     return a.href;


Test patch:
diff --git a/tests/utils/url.tests.ts b/tests/utils/url.tests.ts
index 6ddd5fe6..7ebdcad1 100644
--- a/tests/utils/url.tests.ts
+++ b/tests/utils/url.tests.ts
@@ -270,8 +270,7 @@ test('Absolute URL', () => {
     expect(getAbsoluteURL('https://www.google.com/long/path/', '../another/image.jpg')).toBe('https://www.google.com/long/another/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path/page.html', '/image.jpg')).toBe('https://www.google.com/image.jpg');
-    expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/image.jpg')).toBe('https://www.google.com/path/image.jpg');
-    expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/../another/image.jpg')).toBe('https://www.google.com/another/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/image.jpg')).toBe(`${location.protocol}//www.google.com/path/image.jpg`);
     expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/image.jpg')).toBe('https://www.google.com/path/image.jpg');
     expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/../another/image.jpg')).toBe('https://www.google.com/another/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');