-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
161 lines (147 loc) · 5.1 KB
/
Copy pathbackground.js
File metadata and controls
161 lines (147 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Freedom PDF Viewer — Background Service Worker
*
* Intercepts navigations to PDF files and redirects them to the
* built-in PDF.js viewer. This covers:
* • Clicking a PDF link (http / https)
* • Typing / pasting a PDF URL into the address bar
* • Opening a local PDF via File → Open (file://)
* • Dragging and dropping a PDF file onto a Chrome tab (file://)
*
* Embedded PDFs (lazy-loaded iframes, <object>, etc.) must NOT be
* redirected — only full-tab navigations to a .pdf URL.
*/
const VIEWER = chrome.runtime.getURL('pdfjs/web/viewer.html');
/**
* Returns true if the URL looks like a PDF and is not already
* being handled by our viewer.
* Uses path/extension only (not Content-Type). URLs like `/download?id=1`
* that serve `application/pdf` are not intercepted unless they end in `.pdf`.
*/
function isPdfNavigation(url) {
if (!url || typeof url !== 'string' || url.startsWith(VIEWER)) return false;
try {
const { pathname } = new URL(url);
return pathname.toLowerCase().endsWith('.pdf');
} catch {
// Fallback for malformed URLs
return /\.pdf($|\?|#)/i.test(url);
}
}
/**
* True only for a user-visible tab navigation to a PDF — not an iframe,
* fenced frame, or prerendered document.
* Chrome 106+ exposes `frameType`; older builds fall back to frameId/parentFrameId.
*/
function isTopLevelUserTabNavigation(details) {
if (details.documentLifecycle === 'prerender') {
return false;
}
const { frameType, frameId, parentFrameId } = details;
if (frameType === 'sub_frame' || frameType === 'fenced_frame') {
return false;
}
if (frameType === 'outermost_frame') {
return true;
}
// Chrome < 106: frameType may be undefined — use frame hierarchy only
if (frameType === undefined) {
if (frameId !== 0) {
return false;
}
if (parentFrameId === undefined || parentFrameId === null) {
return true;
}
return parentFrameId === -1;
}
return false;
}
/**
* Redirect the tab to our PDF viewer with the original URL
* encoded as the `file` query parameter that PDF.js expects.
*/
function openInViewer(tabId, url) {
// If the URL has a fragment (e.g. #page=2), keep it outside the 'file' param
// so that PDF.js can parse it correctly for its own navigation.
const [baseUrl, fragment] = url.split('#');
const redirectUrl = `${VIEWER}?file=${encodeURIComponent(baseUrl)}${fragment ? '#' + fragment : ''}`;
chrome.tabs.update(tabId, { url: redirectUrl });
}
// ── Intercept navigations ────────────────────────────────────────────────────
// onBeforeNavigate fires early — before Chrome's built-in PDF renderer
// has a chance to claim the navigation — making it ideal for interception.
chrome.webNavigation.onBeforeNavigate.addListener(
(details) => {
if (!isPdfNavigation(details.url)) {
return;
}
if (!isTopLevelUserTabNavigation(details)) {
return;
}
openInViewer(details.tabId, details.url);
},
{
url: [
{ schemes: ['http', 'https', 'file'], urlMatches: '\\.[pP][dD][fF]($|\\?|#)' }
],
}
);
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'open-with-freedom-pdf',
title: 'Open with Freedom PDF Viewer',
contexts: ['link'],
targetUrlPatterns: [
'*://*/**.pdf',
'*://*/**.pdf?*',
'*://*/**.pdf#*',
'file:///*.pdf',
'file:///*.pdf?*',
'file:///*.pdf#*'
]
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'open-with-freedom-pdf') {
const [baseUrl, fragment] = info.linkUrl.split('#');
const viewerUrl = `${VIEWER}?file=${encodeURIComponent(baseUrl)}${fragment ? '#' + fragment : ''}`;
if (tab && tab.id) {
chrome.tabs.update(tab.id, { url: viewerUrl });
} else {
chrome.tabs.create({ url: viewerUrl });
}
}
});
// Optimized chunk-based base64 encoder that avoids stack size limits
function bufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
const len = bytes.byteLength;
const chunkSize = 8192;
for (let i = 0; i < len; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
// Bypass CORS restrictions for remote PDF files by fetching them via the background service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'fetchPdf') {
fetch(message.url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.arrayBuffer();
})
.then(buffer => {
const base64 = bufferToBase64(buffer);
sendResponse({ success: true, data: base64 });
})
.catch(error => {
console.error('Freedom PDF Viewer: Error fetching remote PDF:', error);
sendResponse({ success: false, error: error.message });
});
return true; // Keep the message channel open for async response
}
});