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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -18,6 +18,10 @@
18
18
  {
19
19
  "hook": "filter:meta.getMetaTags",
20
20
  "method": "filterMetaTags"
21
+ },
22
+ {
23
+ "hook": "filter:parse.post",
24
+ "method": "transformPdfLinks"
21
25
  }
22
26
  ],
23
27
  "staticDirs": {
@@ -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
- // Listen for postMessage from iframe when PDF is fully rendered
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
- var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
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
- // Create container
73
- var container = document.createElement('div');
74
- container.className = 'pdf-secure-embed';
75
- 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);';
76
-
77
- // Header
78
- var header = document.createElement('div');
79
- header.className = 'pdf-secure-embed-header';
80
- 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);';
81
-
82
- var title = document.createElement('div');
83
- title.className = 'pdf-secure-embed-title';
84
- title.style.cssText = 'display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;font-weight:500;';
85
-
86
- var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
87
- icon.setAttribute('viewBox', '0 0 24 24');
88
- icon.style.cssText = 'width:20px;height:20px;min-width:20px;max-width:20px;fill:#e81224;flex-shrink:0;';
89
- 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"/>';
90
-
91
- var nameSpan = document.createElement('span');
92
- nameSpan.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px;';
93
- nameSpan.textContent = decodeURIComponent(link.textContent || filename);
94
-
95
- title.appendChild(icon);
96
- title.appendChild(nameSpan);
97
-
98
- // Actions
99
- var actions = document.createElement('div');
100
- actions.style.cssText = 'display:flex;gap:8px;';
101
-
102
- var fullscreenBtn = document.createElement('button');
103
- fullscreenBtn.className = 'pdf-secure-fullscreen-btn';
104
- fullscreenBtn.title = 'Tam Ekran';
105
- fullscreenBtn.style.cssText = 'display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:rgba(255,255,255,0.08);border:none;border-radius:6px;cursor:pointer;';
106
-
107
- var fullscreenIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
108
- fullscreenIcon.setAttribute('viewBox', '0 0 24 24');
109
- fullscreenIcon.style.cssText = 'width:18px;height:18px;min-width:18px;max-width:18px;fill:#fff;';
110
- fullscreenIcon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
111
- fullscreenBtn.appendChild(fullscreenIcon);
112
- actions.appendChild(fullscreenBtn);
113
-
114
- header.appendChild(title);
115
- header.appendChild(actions);
116
- container.appendChild(header);
117
-
118
- // Body with loading placeholder
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
- iframeWrapper.appendChild(loadingPlaceholder);
211
+ iframeWrapper.appendChild(loadingPlaceholder);
212
+
213
+ container.appendChild(iframeWrapper);
135
214
 
136
- container.appendChild(iframeWrapper);
215
+ targetElement.replaceWith(container);
137
216
 
138
- // Fullscreen handler
139
- fullscreenBtn.addEventListener('click', function () {
140
- var iframe = iframeWrapper.querySelector('iframe');
141
- if (iframe) {
142
- if (iframe.requestFullscreen) iframe.requestFullscreen();
143
- else if (iframe.webkitRequestFullscreen) iframe.webkitRequestFullscreen();
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
- observer.observe(container);
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) {