nodebb-plugin-pdf-secure 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library.js +46 -2
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/main.js +161 -95
- package/static/viewer-app.js +2641 -0
- package/static/viewer.css +1484 -0
- package/static/viewer.html +78 -2995
package/library.js
CHANGED
|
@@ -129,10 +129,21 @@ plugin.filterMetaTags = async (hookData) => {
|
|
|
129
129
|
return hookData;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Admin/Global Moderator bypass - no filtering for privileged users
|
|
133
|
+
if (hookData.req && hookData.req.uid) {
|
|
134
|
+
const [isAdmin, isGlobalMod] = await Promise.all([
|
|
135
|
+
groups.isMember(hookData.req.uid, 'administrators'),
|
|
136
|
+
groups.isMember(hookData.req.uid, 'Global Moderators'),
|
|
137
|
+
]);
|
|
138
|
+
if (isAdmin || isGlobalMod) {
|
|
139
|
+
return hookData;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
// Filter out PDF-related meta tags
|
|
133
144
|
hookData.tags = hookData.tags.filter(tag => {
|
|
134
|
-
// Remove og:image if it contains .pdf
|
|
135
|
-
if (tag.property === 'og:image' && tag.content && tag.content.toLowerCase().includes('.pdf')) {
|
|
145
|
+
// Remove og:image and og:image:url if it contains .pdf
|
|
146
|
+
if ((tag.property === 'og:image' || tag.property === 'og:image:url') && tag.content && tag.content.toLowerCase().includes('.pdf')) {
|
|
136
147
|
return false;
|
|
137
148
|
}
|
|
138
149
|
// Remove twitter:image if it contains .pdf
|
|
@@ -154,4 +165,37 @@ plugin.filterMetaTags = async (hookData) => {
|
|
|
154
165
|
return hookData;
|
|
155
166
|
};
|
|
156
167
|
|
|
168
|
+
// Transform PDF links to secure placeholders (server-side)
|
|
169
|
+
// This hides PDF URLs from: page source, API, RSS, ActivityPub
|
|
170
|
+
plugin.transformPdfLinks = async (data) => {
|
|
171
|
+
if (!data || !data.postData || !data.postData.content) {
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Regex to match PDF links: <a href="...xxx.pdf">text</a>
|
|
176
|
+
// Captures: full URL path, filename, link text
|
|
177
|
+
const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
|
|
178
|
+
|
|
179
|
+
data.postData.content = data.postData.content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
|
|
180
|
+
// Decode filename to prevent double encoding (URL may already be encoded)
|
|
181
|
+
let decodedFilename;
|
|
182
|
+
try { decodedFilename = decodeURIComponent(filename); }
|
|
183
|
+
catch (e) { decodedFilename = filename; }
|
|
184
|
+
|
|
185
|
+
// Sanitize for HTML attribute
|
|
186
|
+
const safeFilename = decodedFilename.replace(/[<>"'&]/g, '');
|
|
187
|
+
const displayName = linkText.trim() || safeFilename;
|
|
188
|
+
|
|
189
|
+
// Return secure placeholder div instead of actual link
|
|
190
|
+
return `<div class="pdf-secure-placeholder" data-filename="${safeFilename}">
|
|
191
|
+
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:#e81224;vertical-align:middle;margin-right:8px;">
|
|
192
|
+
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
|
|
193
|
+
</svg>
|
|
194
|
+
<span>${displayName}</span>
|
|
195
|
+
</div>`;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return data;
|
|
199
|
+
};
|
|
200
|
+
|
|
157
201
|
module.exports = plugin;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -2,13 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
// Main plugin logic - PDF links become inline embedded viewers with lazy loading + queue
|
|
4
4
|
(async function () {
|
|
5
|
+
// ============================================
|
|
6
|
+
// PDF.js PRELOAD - Cache CDN assets before iframe loads
|
|
7
|
+
// ============================================
|
|
8
|
+
(function preloadPdfJs() {
|
|
9
|
+
const preloads = [
|
|
10
|
+
{ href: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js', as: 'script' },
|
|
11
|
+
{ href: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css', as: 'style' }
|
|
12
|
+
];
|
|
13
|
+
preloads.forEach(({ href, as }) => {
|
|
14
|
+
if (!document.querySelector(`link[href="${href}"]`)) {
|
|
15
|
+
const link = document.createElement('link');
|
|
16
|
+
link.rel = 'preload';
|
|
17
|
+
link.href = href;
|
|
18
|
+
link.as = as;
|
|
19
|
+
link.crossOrigin = 'anonymous';
|
|
20
|
+
document.head.appendChild(link);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
|
|
5
25
|
// Loading queue - only load one PDF at a time
|
|
6
26
|
const loadQueue = [];
|
|
7
27
|
let isLoading = false;
|
|
8
28
|
let currentResolver = null;
|
|
9
29
|
|
|
10
|
-
//
|
|
30
|
+
// ============================================
|
|
31
|
+
// SPA MEMORY CACHE - Cache decoded PDF buffers
|
|
32
|
+
// ============================================
|
|
33
|
+
const pdfBufferCache = new Map(); // filename -> ArrayBuffer
|
|
34
|
+
const CACHE_MAX_SIZE = 5; // ~50MB limit (avg 10MB per PDF)
|
|
35
|
+
let currentLoadingFilename = null;
|
|
36
|
+
|
|
37
|
+
function setCachedBuffer(filename, buffer) {
|
|
38
|
+
// Evict oldest if cache is full
|
|
39
|
+
if (pdfBufferCache.size >= CACHE_MAX_SIZE) {
|
|
40
|
+
const firstKey = pdfBufferCache.keys().next().value;
|
|
41
|
+
pdfBufferCache.delete(firstKey);
|
|
42
|
+
console.log('[PDF-Secure] Cache: Evicted', firstKey);
|
|
43
|
+
}
|
|
44
|
+
pdfBufferCache.set(filename, buffer);
|
|
45
|
+
console.log('[PDF-Secure] Cache: Stored', filename, '(', (buffer.byteLength / 1024 / 1024).toFixed(2), 'MB)');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Listen for postMessage from iframe
|
|
11
49
|
window.addEventListener('message', function (event) {
|
|
50
|
+
// Security: Only accept messages from same origin
|
|
51
|
+
if (event.origin !== window.location.origin) return;
|
|
52
|
+
|
|
53
|
+
// PDF ready - resolve queue
|
|
12
54
|
if (event.data && event.data.type === 'pdf-secure-ready') {
|
|
13
55
|
console.log('[PDF-Secure] Queue: PDF ready -', event.data.filename);
|
|
14
56
|
if (currentResolver) {
|
|
@@ -16,6 +58,37 @@
|
|
|
16
58
|
currentResolver = null;
|
|
17
59
|
}
|
|
18
60
|
}
|
|
61
|
+
|
|
62
|
+
// PDF buffer from viewer - cache it
|
|
63
|
+
if (event.data && event.data.type === 'pdf-secure-buffer') {
|
|
64
|
+
const { filename, buffer } = event.data;
|
|
65
|
+
if (filename && buffer) {
|
|
66
|
+
setCachedBuffer(filename, buffer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Viewer asking for cached buffer
|
|
71
|
+
if (event.data && event.data.type === 'pdf-secure-cache-request') {
|
|
72
|
+
const { filename } = event.data;
|
|
73
|
+
const cached = pdfBufferCache.get(filename);
|
|
74
|
+
if (cached && event.source) {
|
|
75
|
+
// Send cached buffer to viewer (transferable for 0-copy)
|
|
76
|
+
event.source.postMessage({
|
|
77
|
+
type: 'pdf-secure-cache-response',
|
|
78
|
+
filename: filename,
|
|
79
|
+
buffer: cached
|
|
80
|
+
}, event.origin, [cached.slice(0)]); // Clone buffer since we keep original
|
|
81
|
+
console.log('[PDF-Secure] Cache: Hit -', filename);
|
|
82
|
+
} else if (event.source) {
|
|
83
|
+
// No cache, viewer will fetch normally
|
|
84
|
+
event.source.postMessage({
|
|
85
|
+
type: 'pdf-secure-cache-response',
|
|
86
|
+
filename: filename,
|
|
87
|
+
buffer: null
|
|
88
|
+
}, event.origin);
|
|
89
|
+
console.log('[PDF-Secure] Cache: Miss -', filename);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
19
92
|
});
|
|
20
93
|
|
|
21
94
|
async function processQueue() {
|
|
@@ -59,8 +132,20 @@
|
|
|
59
132
|
var postContents = document.querySelectorAll('[component="post/content"]');
|
|
60
133
|
|
|
61
134
|
postContents.forEach(function (content) {
|
|
62
|
-
|
|
135
|
+
// NEW: Detect server-rendered secure placeholders (hides URL from source)
|
|
136
|
+
var placeholders = content.querySelectorAll('.pdf-secure-placeholder');
|
|
137
|
+
placeholders.forEach(function (placeholder) {
|
|
138
|
+
if (placeholder.dataset.pdfSecureProcessed) return;
|
|
139
|
+
placeholder.dataset.pdfSecureProcessed = 'true';
|
|
63
140
|
|
|
141
|
+
var filename = placeholder.dataset.filename;
|
|
142
|
+
var displayName = placeholder.querySelector('span')?.textContent || filename;
|
|
143
|
+
|
|
144
|
+
createPdfViewer(placeholder, filename, displayName);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// FALLBACK: Detect old-style PDF links (for backwards compatibility)
|
|
148
|
+
var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
|
|
64
149
|
pdfLinks.forEach(function (link) {
|
|
65
150
|
if (link.dataset.pdfSecure) return;
|
|
66
151
|
link.dataset.pdfSecure = 'true';
|
|
@@ -68,112 +153,93 @@
|
|
|
68
153
|
var href = link.getAttribute('href');
|
|
69
154
|
var parts = href.split('/');
|
|
70
155
|
var filename = parts[parts.length - 1];
|
|
156
|
+
var displayName = link.textContent || filename;
|
|
71
157
|
|
|
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
|
-
var iframeWrapper = document.createElement('div');
|
|
120
|
-
iframeWrapper.className = 'pdf-secure-embed-body';
|
|
121
|
-
iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
|
|
122
|
-
|
|
123
|
-
// Loading placeholder - ALWAYS VISIBLE until PDF ready (z-index: 10)
|
|
124
|
-
var loadingPlaceholder = document.createElement('div');
|
|
125
|
-
loadingPlaceholder.className = 'pdf-loading-placeholder';
|
|
126
|
-
loadingPlaceholder.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#2d2d2d;color:#fff;gap:16px;z-index:10;transition:opacity 0.3s;';
|
|
127
|
-
loadingPlaceholder.innerHTML = `
|
|
158
|
+
createPdfViewer(link, filename, displayName);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createPdfViewer(targetElement, filename, displayName) {
|
|
164
|
+
|
|
165
|
+
// Create container
|
|
166
|
+
var container = document.createElement('div');
|
|
167
|
+
container.className = 'pdf-secure-embed';
|
|
168
|
+
container.style.cssText = 'margin:16px 0;border-radius:12px;overflow:hidden;background:#1f1f1f;border:1px solid rgba(255,255,255,0.1);box-shadow:0 4px 20px rgba(0,0,0,0.25);';
|
|
169
|
+
|
|
170
|
+
// Header
|
|
171
|
+
var header = document.createElement('div');
|
|
172
|
+
header.className = 'pdf-secure-embed-header';
|
|
173
|
+
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:linear-gradient(135deg,#2d2d2d 0%,#252525 100%);border-bottom:1px solid rgba(255,255,255,0.08);';
|
|
174
|
+
|
|
175
|
+
var title = document.createElement('div');
|
|
176
|
+
title.className = 'pdf-secure-embed-title';
|
|
177
|
+
title.style.cssText = 'display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;font-weight:500;';
|
|
178
|
+
|
|
179
|
+
var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
180
|
+
icon.setAttribute('viewBox', '0 0 24 24');
|
|
181
|
+
icon.style.cssText = 'width:20px;height:20px;min-width:20px;max-width:20px;fill:#e81224;flex-shrink:0;';
|
|
182
|
+
icon.innerHTML = '<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>';
|
|
183
|
+
|
|
184
|
+
var nameSpan = document.createElement('span');
|
|
185
|
+
nameSpan.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px;';
|
|
186
|
+
try { nameSpan.textContent = decodeURIComponent(displayName); }
|
|
187
|
+
catch (e) { nameSpan.textContent = displayName; }
|
|
188
|
+
|
|
189
|
+
title.appendChild(icon);
|
|
190
|
+
title.appendChild(nameSpan);
|
|
191
|
+
|
|
192
|
+
header.appendChild(title);
|
|
193
|
+
container.appendChild(header);
|
|
194
|
+
|
|
195
|
+
// Body with loading placeholder
|
|
196
|
+
var iframeWrapper = document.createElement('div');
|
|
197
|
+
iframeWrapper.className = 'pdf-secure-embed-body';
|
|
198
|
+
iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
|
|
199
|
+
|
|
200
|
+
// Loading placeholder - ALWAYS VISIBLE until PDF ready (z-index: 10)
|
|
201
|
+
var loadingPlaceholder = document.createElement('div');
|
|
202
|
+
loadingPlaceholder.className = 'pdf-loading-placeholder';
|
|
203
|
+
loadingPlaceholder.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#2d2d2d;color:#fff;gap:16px;z-index:10;transition:opacity 0.3s;';
|
|
204
|
+
loadingPlaceholder.innerHTML = `
|
|
128
205
|
<svg viewBox="0 0 24 24" style="width:48px;height:48px;fill:#555;">
|
|
129
206
|
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
|
|
130
207
|
</svg>
|
|
131
208
|
<div class="pdf-loading-text" style="font-size:14px;color:#a0a0a0;">Sırada bekliyor...</div>
|
|
132
209
|
<style>@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>
|
|
133
210
|
`;
|
|
134
|
-
|
|
211
|
+
iframeWrapper.appendChild(loadingPlaceholder);
|
|
212
|
+
|
|
213
|
+
container.appendChild(iframeWrapper);
|
|
135
214
|
|
|
136
|
-
|
|
215
|
+
targetElement.replaceWith(container);
|
|
137
216
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
217
|
+
// LAZY LOADING with Intersection Observer + Queue
|
|
218
|
+
var observer = new IntersectionObserver(function (entries) {
|
|
219
|
+
entries.forEach(function (entry) {
|
|
220
|
+
if (entry.isIntersecting) {
|
|
221
|
+
// Update placeholder to show loading state
|
|
222
|
+
var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
|
|
223
|
+
if (textEl) textEl.textContent = 'PDF Yükleniyor...';
|
|
224
|
+
|
|
225
|
+
var svgEl = loadingPlaceholder.querySelector('svg');
|
|
226
|
+
if (svgEl) {
|
|
227
|
+
svgEl.style.fill = '#0078d4';
|
|
228
|
+
svgEl.style.animation = 'spin 1s linear infinite';
|
|
229
|
+
svgEl.innerHTML = '<path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>';
|
|
144
230
|
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
link.replaceWith(container);
|
|
148
|
-
|
|
149
|
-
// LAZY LOADING with Intersection Observer + Queue
|
|
150
|
-
var observer = new IntersectionObserver(function (entries) {
|
|
151
|
-
entries.forEach(function (entry) {
|
|
152
|
-
if (entry.isIntersecting) {
|
|
153
|
-
// Update placeholder to show loading state
|
|
154
|
-
var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
|
|
155
|
-
if (textEl) textEl.textContent = 'PDF Yükleniyor...';
|
|
156
|
-
|
|
157
|
-
var svgEl = loadingPlaceholder.querySelector('svg');
|
|
158
|
-
if (svgEl) {
|
|
159
|
-
svgEl.style.fill = '#0078d4';
|
|
160
|
-
svgEl.style.animation = 'spin 1s linear infinite';
|
|
161
|
-
svgEl.innerHTML = '<path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Add to queue
|
|
165
|
-
queuePdfLoad(iframeWrapper, filename, loadingPlaceholder);
|
|
166
|
-
observer.disconnect();
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}, {
|
|
170
|
-
rootMargin: '200px',
|
|
171
|
-
threshold: 0
|
|
172
|
-
});
|
|
173
231
|
|
|
174
|
-
|
|
232
|
+
// Add to queue
|
|
233
|
+
queuePdfLoad(iframeWrapper, filename, loadingPlaceholder);
|
|
234
|
+
observer.disconnect();
|
|
235
|
+
}
|
|
175
236
|
});
|
|
237
|
+
}, {
|
|
238
|
+
rootMargin: '200px',
|
|
239
|
+
threshold: 0
|
|
176
240
|
});
|
|
241
|
+
|
|
242
|
+
observer.observe(container);
|
|
177
243
|
}
|
|
178
244
|
|
|
179
245
|
function loadPdfIframe(wrapper, filename, placeholder) {
|