-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
184 lines (169 loc) · 6.32 KB
/
Copy pathbackground.js
File metadata and controls
184 lines (169 loc) · 6.32 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
* 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;
}
/**
* Helper function to parse target PDF URL and construct the viewer path.
* Splits on the FIRST '#' character only, preserving any subsequent
* fragments (e.g., #page=2#anchor) and passing them outside the file param
* so PDF.js can parse them correctly for its own in-document navigation.
*/
function buildViewerUrl(url) {
const hashIdx = url.indexOf('#');
const baseUrl = hashIdx === -1 ? url : url.slice(0, hashIdx);
const fragment = hashIdx === -1 ? '' : url.slice(hashIdx + 1);
return `${VIEWER}?file=${encodeURIComponent(baseUrl)}${fragment ? '#' + fragment : ''}`;
}
/**
* 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) {
chrome.tabs.update(tabId, { url: buildViewerUrl(url) });
}
// ── 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(() => {
// NOTE: Chrome context menu targetUrlPatterns are glob-only and case-sensitive.
// We can't match case-insensitive extensions (.PDF, .Pdf) without regex, which is
// not supported here. The webNavigation filter handles all intercept cases anyway.
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 viewerUrl = buildViewerUrl(info.linkUrl);
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.
// Uses a chunk size of 8192 to prevent hitting V8 stack limits in
// String.fromCharCode.apply, which would throw a RangeError on large buffers.
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}`);
}
// Enforce a size limit to prevent background service worker OOM crashes.
// Chrome limits service worker heap size to ~100-200MB.
const len = parseInt(response.headers.get('content-length') || '0', 10);
if (len > 50 * 1024 * 1024) { // 50 MB cap
throw new Error('PDF file is too large to proxy (>50 MB). Please open the URL directly or download it.');
}
return response.arrayBuffer();
})
.then(buffer => {
// Double check buffer size in case content-length header was missing
if (buffer.byteLength > 50 * 1024 * 1024) {
throw new Error('PDF file is too large to proxy (>50 MB). Please open the URL directly or download it.');
}
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
}
});