Skip to content

Commit dbe1818

Browse files
fix: Enhance Firefox compatibility with messaging listener adjustments, Drive download workarounds, and new debugging utilities.
1 parent 07311cf commit dbe1818

File tree

8 files changed

+180
-23
lines changed

8 files changed

+180
-23
lines changed

extension/entrypoints/background.ts

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ const cancelledByUs = new Set<number>();
5555
const AUTHUSER_CANDIDATES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
5656
const CLASSROOM_URL_PATTERN = /^https:\/\/classroom\.google\.com\//;
5757

58+
/**
59+
* Detect if running in Firefox.
60+
* Firefox's downloads API doesn't pass auth cookies for cross-origin requests,
61+
* so we need to use bypass tab method instead.
62+
*/
63+
function isFirefox(): boolean {
64+
if (typeof navigator === 'undefined') {
65+
console.log('[CQD:BG] isFirefox check: navigator is undefined');
66+
return false;
67+
}
68+
const isFF = /Firefox/i.test(navigator.userAgent);
69+
console.log('[CQD:BG] isFirefox check:', { isFF, userAgent: navigator.userAgent });
70+
return isFF;
71+
}
72+
5873
/* ---------------------------------------------
5974
* Icon / tab context helpers
6075
* -------------------------------------------*/
@@ -81,10 +96,18 @@ const GRAY_ICON_PATHS: Record<number, string> = {
8196
};
8297

8398
function setActionIcon(tabId: number, classroom: boolean) {
84-
if (typeof chrome === 'undefined' || !chrome.action?.setIcon) return;
99+
if (typeof chrome === 'undefined') return;
100+
85101
const path = classroom ? COLOR_ICON_PATHS : GRAY_ICON_PATHS;
102+
103+
// Firefox MV2 uses browserAction, Chrome MV3 uses action
104+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105+
const actionApi = (chrome as any).action || (chrome as any).browserAction;
106+
107+
if (!actionApi?.setIcon) return;
108+
86109
try {
87-
chrome.action.setIcon({ tabId, path });
110+
actionApi.setIcon({ tabId, path });
88111
} catch {
89112
/* ignore */
90113
}
@@ -170,7 +193,9 @@ export default defineBackground(() => {
170193
});
171194

172195
// --- Icon Logic ---
173-
if (typeof chrome !== 'undefined' && chrome.tabs && chrome.action) {
196+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
197+
const hasActionApi = (chrome as any).action || (chrome as any).browserAction;
198+
if (typeof chrome !== 'undefined' && chrome.tabs && hasActionApi) {
174199
try {
175200
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
176201
if (tabs[0]?.id != null) updateIconForTab(tabs[0].id, tabs[0].url);
@@ -205,14 +230,15 @@ export default defineBackground(() => {
205230
* 1) Messages from drive_bypass.content.ts (Drive tab)
206231
* -----------------------------------------------------*/
207232
chrome.runtime.onMessage.addListener((message, sender) => {
208-
if (!message || !sender.tab || sender.tab.id == null) return;
233+
// Firefox fix: explicitly return false if we won't handle this message
234+
if (!message || !sender.tab || sender.tab.id == null) return false;
209235

210236
const tabId = sender.tab.id;
211237
const pending = pendingByBypassTabId.get(tabId);
212238

213239
// Not related to any bypass tab we're tracking
214240
if (!pending && typeof message.type === 'string' && message.type.startsWith('CQD_')) {
215-
return;
241+
return false;
216242
}
217243

218244
// A) SUCCESS: Drive tab clicked Download / Download anyway
@@ -274,7 +300,9 @@ export default defineBackground(() => {
274300
/* -------------------------------------------------------
275301
* 2) onDeterminingFilename - SELF HEALING LOGIC ADDED
276302
* -----------------------------------------------------*/
277-
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
303+
// Firefox does not support onDeterminingFilename, so we guard this.
304+
if (chrome.downloads && chrome.downloads.onDeterminingFilename) {
305+
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
278306
// 1. Try finding by ID first
279307
let pending = pendingByDownloadId.get(item.id);
280308

@@ -355,6 +383,7 @@ export default defineBackground(() => {
355383
suggest({ conflictAction: 'uniquify' });
356384
}
357385
});
386+
}
358387

359388
/* -------------------------------------------------------
360389
* 3) onChanged: ANALYTICS TRIGGER
@@ -409,20 +438,26 @@ export default defineBackground(() => {
409438
* 4) CQD_DOWNLOAD HANDLER
410439
* -----------------------------------------------------*/
411440
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
412-
if (!message || message.type !== 'CQD_DOWNLOAD') return;
441+
// Firefox fix: Only handle CQD_DOWNLOAD messages in this listener
442+
if (!message || message.type !== 'CQD_DOWNLOAD') return false;
443+
444+
console.log('[CQD:BG] Received CQD_DOWNLOAD message:', { url: message.url, requestId: message.requestId });
413445

414446
const rawUrl = message.url as string | undefined;
415447
const fileMeta = message.fileMeta as FileMetaMsg | undefined;
416448
const requestId = message.requestId || `req-${Date.now()}`;
417449

418450
if (!rawUrl) {
451+
console.log('[CQD:BG] No rawUrl provided, sending error response');
419452
sendResponse?.({
420453
started: false,
421454
userMessage: 'No valid link found.',
422455
});
423-
return;
456+
return true; // Firefox: must return true even for sync responses
424457
}
425458

459+
console.log('[CQD:BG] Processing URL:', rawUrl);
460+
426461
const { baseUrl, isDrive } = normalizeUrl(rawUrl);
427462
const initialAuthUser = isDrive
428463
? extractAuthUserFromUrl(rawUrl)
@@ -459,20 +494,40 @@ export default defineBackground(() => {
459494
};
460495

461496
if (isDrive) {
497+
// FIREFOX WORKAROUND: Firefox's downloads API doesn't pass authentication
498+
// cookies for cross-origin requests (like Drive). Skip directly to bypass tab.
499+
if (isFirefox()) {
500+
console.log('[CQD:BG] Firefox detected - using bypass tab for Drive download');
501+
pending.fallbackStarted = true;
502+
openDriveBypassTab(pending, pending.baseUrl);
503+
respondOnce({
504+
started: true,
505+
requestId,
506+
userMessage: 'Opening Drive tab for download…',
507+
});
508+
return true;
509+
}
510+
462511
const firstUrl =
463512
typeof pending.currentAuthUser === 'number'
464513
? buildUrlWithAuthUser(pending.baseUrl, pending.currentAuthUser)
465514
: pending.baseUrl;
466515

516+
console.log('[CQD:BG] Starting Drive download:', { firstUrl, isDrive: true });
517+
467518
chrome.downloads.download(
468519
{
469520
url: firstUrl,
470521
saveAs: false,
471522
conflictAction: 'uniquify',
472523
},
473524
(id) => {
525+
console.log('[CQD:BG] Download callback:', { id, lastError: chrome.runtime.lastError?.message });
526+
474527
if (chrome.runtime.lastError || !id) {
475528
// download could not even start
529+
console.log('[CQD:BG] Download failed to start:', chrome.runtime.lastError?.message);
530+
476531
recordDownloadEvent({
477532
type: pending.fileMeta?.ext || 'unknown',
478533
status: 'fail',
@@ -498,6 +553,7 @@ export default defineBackground(() => {
498553
return;
499554
}
500555

556+
console.log('[CQD:BG] Download started successfully:', { id, requestId });
501557
pending.currentDownloadId = id;
502558
pendingByDownloadId.set(id, pending);
503559
respondOnce({ started: true, requestId, downloadId: id });
@@ -519,15 +575,21 @@ function startSingleAttempt(
519575
pending: PendingDownload,
520576
respondOnce?: (payload: any) => void,
521577
) {
578+
console.log('[CQD:BG] startSingleAttempt (non-Drive):', { url: pending.baseUrl });
579+
522580
chrome.downloads.download(
523581
{
524582
url: pending.baseUrl,
525583
saveAs: false,
526584
conflictAction: 'uniquify',
527585
},
528586
(downloadId) => {
587+
console.log('[CQD:BG] Non-Drive download callback:', { downloadId, lastError: chrome.runtime.lastError?.message });
588+
529589
if (chrome.runtime.lastError || !downloadId) {
530590
// could not start a direct download at all
591+
console.log('[CQD:BG] Non-Drive download failed:', chrome.runtime.lastError?.message);
592+
531593
recordDownloadEvent({
532594
type: pending.fileMeta?.ext || 'unknown',
533595
status: 'fail',

extension/entrypoints/comment_frame.content.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ function stopCommentsFeature(): void {
139139
* ---------------------------------------------------*/
140140
if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
141141
chrome.runtime.onMessage.addListener((message) => {
142-
if (!message) return;
143-
if (message.type !== 'CQD_POPUP_SET_DESIRED_STATE') return;
142+
// Firefox fix: return false for messages we don't handle
143+
if (!message) return false;
144+
if (message.type !== 'CQD_POPUP_SET_DESIRED_STATE') return false;
144145
tabEnabled = !!message.enabled;
145146
if (tabEnabled) {
146147
whenExtensionEnabled(() => {
@@ -149,6 +150,7 @@ if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
149150
} else {
150151
stopCommentsFeature();
151152
}
153+
return false; // No async response needed
152154
});
153155
}
154156

extension/entrypoints/content/both-badge.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// filepath: entrypoints/content/both-badge.ts
2-
import { COMMENT_ICON_URL, EDIT_ICON_SVG_RAW } from './icons';
2+
import { COMMENT_ICON_URL, EDIT_ICON_SVG_RAW, appendSvgFromString } from './icons';
33

44
const INJECTED_ATTR = 'data-cqd-injected';
55

@@ -128,7 +128,7 @@ export function upgradeCombinedBadge(post: HTMLElement): void {
128128

129129
const editedIcon = document.createElement('div');
130130
editedIcon.className = 'cqd-both-icon cqd-both-icon-edited';
131-
editedIcon.innerHTML = EDIT_ICON_SVG_RAW;
131+
appendSvgFromString(editedIcon, EDIT_ICON_SVG_RAW);
132132

133133
const diffValue = document.createElement('span');
134134
diffValue.className = 'cqd-both-value cqd-both-value-edited';

extension/entrypoints/content/icons.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,29 @@ export const EDIT_ICON_URL = `data:image/svg+xml;utf8,${encodeURIComponent(
5050
)}`;
5151
export const COMMENT_ICON_URL = `data:image/svg+xml;utf8,${encodeURIComponent(
5252
COMMENT_ICON_SVG_RAW
53-
)}`;
53+
)}`;
54+
55+
/**
56+
* CSP-safe: Parse SVG string and append to container using DOMParser.
57+
* Avoids innerHTML which can be blocked by strict CSP in Firefox.
58+
*/
59+
export function appendSvgFromString(container: HTMLElement, svgString: string): void {
60+
try {
61+
const parser = new DOMParser();
62+
const doc = parser.parseFromString(svgString, 'image/svg+xml');
63+
const svg = doc.documentElement;
64+
65+
// Check for parse errors
66+
const parseError = doc.querySelector('parsererror');
67+
if (parseError) {
68+
console.warn('[CQD] SVG parse error:', parseError.textContent);
69+
return;
70+
}
71+
72+
// Import the node into the current document and append
73+
const importedSvg = document.importNode(svg, true);
74+
container.appendChild(importedSvg);
75+
} catch (err) {
76+
console.warn('[CQD] Failed to append SVG:', err);
77+
}
78+
}

extension/entrypoints/content/index.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -688,19 +688,25 @@ function startBackgroundDownload(
688688
return;
689689
}
690690
try {
691+
console.log('[CQD:Content] Sending download message:', { type: 'CQD_DOWNLOAD', url: finalUrl, requestId });
691692
chrome.runtime.sendMessage(
692693
{ type: 'CQD_DOWNLOAD', url: finalUrl, requestId, fileMeta },
693694
(response) => {
695+
console.log('[CQD:Content] Got response:', response);
696+
console.log('[CQD:Content] Last error:', chrome.runtime.lastError?.message);
697+
694698
if (
695699
chrome.runtime.lastError ||
696700
!response ||
697701
response.started === false
698702
) {
703+
console.log('[CQD:Content] Download failed to start');
699704
resolve({
700705
ok: false,
701706
userMessage: response?.userMessage || 'Could not start.',
702707
});
703708
} else {
709+
console.log('[CQD:Content] Download started successfully');
704710
resolve({ ok: true });
705711
}
706712
},
@@ -748,8 +754,9 @@ function delay(ms: number): Promise<void> {
748754
* ---------------------------------------------------*/
749755
if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
750756
chrome.runtime.onMessage.addListener(
751-
(message, _sender, sendResponse): void | true => {
752-
if (!message) return;
757+
(message, _sender, sendResponse): boolean => {
758+
// Firefox fix: return false for messages we don't handle
759+
if (!message) return false;
753760

754761
// Popup asks for this tab's state
755762
if (message.type === 'CQD_POPUP_QUERY_STATE') {
@@ -776,10 +783,10 @@ if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
776783

777784
if (message.type === 'CQD_DOWNLOAD_STATUS') {
778785
const requestId = message.requestId as string | undefined;
779-
if (!requestId) return;
786+
if (!requestId) return false;
780787

781788
const pending = pendingButtons.get(requestId);
782-
if (!pending) return;
789+
if (!pending) return false;
783790

784791
const { button, startedAt } = pending;
785792
(async () => {
@@ -824,8 +831,10 @@ if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
824831
await showErrorState(button, userMessage);
825832
}
826833
})();
827-
return;
834+
return false; // Firefox fix: no async response needed for status updates
828835
}
836+
837+
return false; // Firefox fix: unhandled message types
829838
},
830839
);
831840
}

extension/entrypoints/edited_frame.content.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// filepath: entrypoints/edited_frame.content.ts
2-
import { EDIT_ICON_SVG_RAW } from './content/icons';
2+
import { EDIT_ICON_SVG_RAW, appendSvgFromString } from './content/icons';
33
import { injectStyles } from './content/styles';
44
import { isPageDark } from './content/theme';
55
import { t } from './content/i18n';
@@ -132,8 +132,9 @@ function stopEditedFeature(): void {
132132
* ---------------------------------------------------*/
133133
if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
134134
chrome.runtime.onMessage.addListener((message) => {
135-
if (!message) return;
136-
if (message.type !== 'CQD_POPUP_SET_DESIRED_STATE') return;
135+
// Firefox fix: return false for messages we don't handle
136+
if (!message) return false;
137+
if (message.type !== 'CQD_POPUP_SET_DESIRED_STATE') return false;
137138
tabEnabled = !!message.enabled;
138139
if (tabEnabled) {
139140
whenExtensionEnabled(() => {
@@ -142,6 +143,7 @@ if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) {
142143
} else {
143144
stopEditedFeature();
144145
}
146+
return false; // No async response needed
145147
});
146148
}
147149

@@ -360,7 +362,7 @@ function createEditedOverlay(post: HTMLElement, diffText: string) {
360362

361363
const iconWrapper = document.createElement('div');
362364
iconWrapper.className = 'cqd-edited-icon';
363-
iconWrapper.innerHTML = EDIT_ICON_SVG_RAW;
365+
appendSvgFromString(iconWrapper, EDIT_ICON_SVG_RAW);
364366
pill.appendChild(iconWrapper);
365367

366368
const content = document.createElement('div');

0 commit comments

Comments
 (0)