nodebb-plugin-pdf-secure 1.2.6 → 1.2.7

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
@@ -131,8 +131,8 @@ plugin.filterMetaTags = async (hookData) => {
131
131
 
132
132
  // Filter out PDF-related meta tags
133
133
  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')) {
134
+ // Remove og:image and og:image:url if it contains .pdf
135
+ if ((tag.property === 'og:image' || tag.property === 'og:image:url') && tag.content && tag.content.toLowerCase().includes('.pdf')) {
136
136
  return false;
137
137
  }
138
138
  // Remove twitter:image if it contains .pdf
@@ -154,4 +154,37 @@ plugin.filterMetaTags = async (hookData) => {
154
154
  return hookData;
155
155
  };
156
156
 
157
+ // Transform PDF links to secure placeholders (server-side)
158
+ // This hides PDF URLs from: page source, API, RSS, ActivityPub
159
+ plugin.transformPdfLinks = async (data) => {
160
+ if (!data || !data.postData || !data.postData.content) {
161
+ return data;
162
+ }
163
+
164
+ // Regex to match PDF links: <a href="...xxx.pdf">text</a>
165
+ // Captures: full URL path, filename, link text
166
+ const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
167
+
168
+ data.postData.content = data.postData.content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
169
+ // Decode filename to prevent double encoding (URL may already be encoded)
170
+ let decodedFilename;
171
+ try { decodedFilename = decodeURIComponent(filename); }
172
+ catch (e) { decodedFilename = filename; }
173
+
174
+ // Sanitize for HTML attribute
175
+ const safeFilename = decodedFilename.replace(/[<>"'&]/g, '');
176
+ const displayName = linkText.trim() || safeFilename;
177
+
178
+ // Return secure placeholder div instead of actual link
179
+ return `<div class="pdf-secure-placeholder" data-filename="${safeFilename}">
180
+ <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:#e81224;vertical-align:middle;margin-right:8px;">
181
+ <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"/>
182
+ </svg>
183
+ <span>${displayName}</span>
184
+ </div>`;
185
+ });
186
+
187
+ return data;
188
+ };
189
+
157
190
  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.7",
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,6 +2,26 @@
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;
@@ -9,6 +29,9 @@
9
29
 
10
30
  // Listen for postMessage from iframe when PDF is fully rendered
11
31
  window.addEventListener('message', function (event) {
32
+ // Security: Only accept messages from same origin
33
+ if (event.origin !== window.location.origin) return;
34
+
12
35
  if (event.data && event.data.type === 'pdf-secure-ready') {
13
36
  console.log('[PDF-Secure] Queue: PDF ready -', event.data.filename);
14
37
  if (currentResolver) {
@@ -59,8 +82,20 @@
59
82
  var postContents = document.querySelectorAll('[component="post/content"]');
60
83
 
61
84
  postContents.forEach(function (content) {
62
- var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
85
+ // NEW: Detect server-rendered secure placeholders (hides URL from source)
86
+ var placeholders = content.querySelectorAll('.pdf-secure-placeholder');
87
+ placeholders.forEach(function (placeholder) {
88
+ if (placeholder.dataset.pdfSecureProcessed) return;
89
+ placeholder.dataset.pdfSecureProcessed = 'true';
63
90
 
91
+ var filename = placeholder.dataset.filename;
92
+ var displayName = placeholder.querySelector('span')?.textContent || filename;
93
+
94
+ createPdfViewer(placeholder, filename, displayName);
95
+ });
96
+
97
+ // FALLBACK: Detect old-style PDF links (for backwards compatibility)
98
+ var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
64
99
  pdfLinks.forEach(function (link) {
65
100
  if (link.dataset.pdfSecure) return;
66
101
  link.dataset.pdfSecure = 'true';
@@ -68,112 +103,119 @@
68
103
  var href = link.getAttribute('href');
69
104
  var parts = href.split('/');
70
105
  var filename = parts[parts.length - 1];
106
+ var displayName = link.textContent || filename;
71
107
 
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 = `
108
+ createPdfViewer(link, filename, displayName);
109
+ });
110
+ });
111
+ }
112
+
113
+ function createPdfViewer(targetElement, filename, displayName) {
114
+
115
+ // Create container
116
+ var container = document.createElement('div');
117
+ container.className = 'pdf-secure-embed';
118
+ 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);';
119
+
120
+ // Header
121
+ var header = document.createElement('div');
122
+ header.className = 'pdf-secure-embed-header';
123
+ 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);';
124
+
125
+ var title = document.createElement('div');
126
+ title.className = 'pdf-secure-embed-title';
127
+ title.style.cssText = 'display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;font-weight:500;';
128
+
129
+ var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
130
+ icon.setAttribute('viewBox', '0 0 24 24');
131
+ icon.style.cssText = 'width:20px;height:20px;min-width:20px;max-width:20px;fill:#e81224;flex-shrink:0;';
132
+ 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"/>';
133
+
134
+ var nameSpan = document.createElement('span');
135
+ nameSpan.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px;';
136
+ try { nameSpan.textContent = decodeURIComponent(displayName); }
137
+ catch (e) { nameSpan.textContent = displayName; }
138
+
139
+ title.appendChild(icon);
140
+ title.appendChild(nameSpan);
141
+
142
+ // Actions
143
+ var actions = document.createElement('div');
144
+ actions.style.cssText = 'display:flex;gap:8px;';
145
+
146
+ var fullscreenBtn = document.createElement('button');
147
+ fullscreenBtn.className = 'pdf-secure-fullscreen-btn';
148
+ fullscreenBtn.title = 'Tam Ekran';
149
+ 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;';
150
+
151
+ var fullscreenIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
152
+ fullscreenIcon.setAttribute('viewBox', '0 0 24 24');
153
+ fullscreenIcon.style.cssText = 'width:18px;height:18px;min-width:18px;max-width:18px;fill:#fff;';
154
+ fullscreenIcon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
155
+ fullscreenBtn.appendChild(fullscreenIcon);
156
+ actions.appendChild(fullscreenBtn);
157
+
158
+ header.appendChild(title);
159
+ header.appendChild(actions);
160
+ container.appendChild(header);
161
+
162
+ // Body with loading placeholder
163
+ var iframeWrapper = document.createElement('div');
164
+ iframeWrapper.className = 'pdf-secure-embed-body';
165
+ iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
166
+
167
+ // Loading placeholder - ALWAYS VISIBLE until PDF ready (z-index: 10)
168
+ var loadingPlaceholder = document.createElement('div');
169
+ loadingPlaceholder.className = 'pdf-loading-placeholder';
170
+ 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;';
171
+ loadingPlaceholder.innerHTML = `
128
172
  <svg viewBox="0 0 24 24" style="width:48px;height:48px;fill:#555;">
129
173
  <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
174
  </svg>
131
175
  <div class="pdf-loading-text" style="font-size:14px;color:#a0a0a0;">Sırada bekliyor...</div>
132
176
  <style>@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>
133
177
  `;
134
- iframeWrapper.appendChild(loadingPlaceholder);
178
+ iframeWrapper.appendChild(loadingPlaceholder);
135
179
 
136
- container.appendChild(iframeWrapper);
180
+ container.appendChild(iframeWrapper);
137
181
 
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();
182
+ // Fullscreen handler
183
+ fullscreenBtn.addEventListener('click', function () {
184
+ var iframe = iframeWrapper.querySelector('iframe');
185
+ if (iframe) {
186
+ if (iframe.requestFullscreen) iframe.requestFullscreen();
187
+ else if (iframe.webkitRequestFullscreen) iframe.webkitRequestFullscreen();
188
+ }
189
+ });
190
+
191
+ targetElement.replaceWith(container);
192
+
193
+ // LAZY LOADING with Intersection Observer + Queue
194
+ var observer = new IntersectionObserver(function (entries) {
195
+ entries.forEach(function (entry) {
196
+ if (entry.isIntersecting) {
197
+ // Update placeholder to show loading state
198
+ var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
199
+ if (textEl) textEl.textContent = 'PDF Yükleniyor...';
200
+
201
+ var svgEl = loadingPlaceholder.querySelector('svg');
202
+ if (svgEl) {
203
+ svgEl.style.fill = '#0078d4';
204
+ svgEl.style.animation = 'spin 1s linear infinite';
205
+ svgEl.innerHTML = '<path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>';
144
206
  }
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
207
 
174
- observer.observe(container);
208
+ // Add to queue
209
+ queuePdfLoad(iframeWrapper, filename, loadingPlaceholder);
210
+ observer.disconnect();
211
+ }
175
212
  });
213
+ }, {
214
+ rootMargin: '200px',
215
+ threshold: 0
176
216
  });
217
+
218
+ observer.observe(container);
177
219
  }
178
220
 
179
221
  function loadPdfIframe(wrapper, filename, placeholder) {
@@ -1568,6 +1568,17 @@
1568
1568
 
1569
1569
  } catch (err) {
1570
1570
  console.error('[PDF-Secure] Auto-load error:', err);
1571
+
1572
+ // Notify parent of error (prevents 60s queue hang)
1573
+ if (window.parent && window.parent !== window) {
1574
+ const config = window.PDF_SECURE_CONFIG || {};
1575
+ window.parent.postMessage({
1576
+ type: 'pdf-secure-ready',
1577
+ filename: config.filename,
1578
+ error: err.message
1579
+ }, '*');
1580
+ }
1581
+
1571
1582
  if (dropzone) {
1572
1583
  dropzone.innerHTML = `
1573
1584
  <svg viewBox="0 0 24 24" style="fill: #e81224;">