nodebb-plugin-pdf-secure 1.2.3 → 1.2.4

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
@@ -11,9 +11,21 @@ const nonceStore = require('./lib/nonce-store');
11
11
 
12
12
  const plugin = {};
13
13
 
14
+ // Memory cache for viewer.html
15
+ let viewerHtmlCache = null;
16
+
14
17
  plugin.init = async (params) => {
15
18
  const { router, middleware } = params;
16
19
 
20
+ // Pre-load viewer.html into memory cache
21
+ const viewerPath = path.join(__dirname, 'static', 'viewer.html');
22
+ try {
23
+ viewerHtmlCache = fs.readFileSync(viewerPath, 'utf8');
24
+ console.log('[PDF-Secure] Viewer template cached in memory');
25
+ } catch (err) {
26
+ console.error('[PDF-Secure] Failed to cache viewer template:', err.message);
27
+ }
28
+
17
29
  // PDF direct access blocker middleware
18
30
  // Intercepts requests to uploaded PDF files and returns 403
19
31
  router.get('/assets/uploads/files/:filename', (req, res, next) => {
@@ -42,6 +54,11 @@ plugin.init = async (params) => {
42
54
  return res.status(400).send('Invalid file');
43
55
  }
44
56
 
57
+ // Check cache
58
+ if (!viewerHtmlCache) {
59
+ return res.status(500).send('Viewer not available');
60
+ }
61
+
45
62
  // Serve the viewer template with comprehensive security headers
46
63
  res.set({
47
64
  'X-Frame-Options': 'SAMEORIGIN',
@@ -55,32 +72,23 @@ plugin.init = async (params) => {
55
72
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'self'",
56
73
  });
57
74
 
58
- // Read and send the viewer HTML
59
- const viewerPath = path.join(__dirname, 'static', 'viewer.html');
60
- fs.readFile(viewerPath, 'utf8', (err, html) => {
61
- if (err) {
62
- return res.status(500).send('Viewer not found');
63
- }
64
-
65
- // Inject the filename and config into the viewer
66
- // Also inject CSS to hide uploadOverlay immediately
67
- const injectedHtml = html
68
- .replace('</head>', `
69
- <style>
70
- /* Hide upload overlay since PDF will auto-load */
71
- #uploadOverlay { display: none !important; }
72
- </style>
73
- <script>
74
- window.PDF_SECURE_CONFIG = {
75
- filename: ${JSON.stringify(safeName)},
76
- relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
77
- csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')}
78
- };
79
- </script>
80
- </head>`);
81
-
82
- res.type('html').send(injectedHtml);
83
- });
75
+ // Inject the filename and config into the cached viewer
76
+ const injectedHtml = viewerHtmlCache
77
+ .replace('</head>', `
78
+ <style>
79
+ /* Hide upload overlay since PDF will auto-load */
80
+ #uploadOverlay { display: none !important; }
81
+ </style>
82
+ <script>
83
+ window.PDF_SECURE_CONFIG = {
84
+ filename: ${JSON.stringify(safeName)},
85
+ relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
86
+ csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')}
87
+ };
88
+ </script>
89
+ </head>`);
90
+
91
+ res.type('html').send(injectedHtml);
84
92
  });
85
93
  };
86
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
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": {
@@ -1,11 +1,54 @@
1
1
  'use strict';
2
2
 
3
- // Main plugin logic - PDF links become inline embedded viewers with lazy loading
3
+ // Main plugin logic - PDF links become inline embedded viewers with lazy loading + queue
4
4
  (async function () {
5
+ // Loading queue - only load one PDF at a time
6
+ const loadQueue = [];
7
+ let isLoading = false;
8
+ let currentResolver = null;
9
+
10
+ // Listen for postMessage from iframe when PDF is fully rendered
11
+ window.addEventListener('message', function (event) {
12
+ if (event.data && event.data.type === 'pdf-secure-ready') {
13
+ console.log('[PDF-Secure] Queue: PDF ready -', event.data.filename);
14
+ if (currentResolver) {
15
+ currentResolver();
16
+ currentResolver = null;
17
+ }
18
+ }
19
+ });
20
+
21
+ async function processQueue() {
22
+ if (isLoading || loadQueue.length === 0) return;
23
+
24
+ isLoading = true;
25
+ const { wrapper, filename, placeholder } = loadQueue.shift();
26
+
27
+ try {
28
+ await loadPdfIframe(wrapper, filename, placeholder);
29
+ } catch (err) {
30
+ console.error('[PDF-Secure] Load error:', err);
31
+ }
32
+
33
+ isLoading = false;
34
+
35
+ // Small delay between loads
36
+ setTimeout(processQueue, 200);
37
+ }
38
+
39
+ function queuePdfLoad(wrapper, filename, placeholder) {
40
+ loadQueue.push({ wrapper, filename, placeholder });
41
+ processQueue();
42
+ }
43
+
5
44
  try {
6
45
  var hooks = await app.require('hooks');
7
46
 
8
47
  hooks.on('action:ajaxify.end', function () {
48
+ // Clear queue on page change
49
+ loadQueue.length = 0;
50
+ isLoading = false;
51
+ currentResolver = null;
9
52
  interceptPdfLinks();
10
53
  });
11
54
  } catch (err) {
@@ -26,7 +69,7 @@
26
69
  var parts = href.split('/');
27
70
  var filename = parts[parts.length - 1];
28
71
 
29
- // Create container with loading placeholder
72
+ // Create container
30
73
  var container = document.createElement('div');
31
74
  container.className = 'pdf-secure-embed';
32
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);';
@@ -77,15 +120,15 @@
77
120
  iframeWrapper.className = 'pdf-secure-embed-body';
78
121
  iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
79
122
 
80
- // Loading placeholder
123
+ // Loading placeholder - ALWAYS VISIBLE until PDF ready (z-index: 10)
81
124
  var loadingPlaceholder = document.createElement('div');
82
125
  loadingPlaceholder.className = 'pdf-loading-placeholder';
83
- 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;';
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;';
84
127
  loadingPlaceholder.innerHTML = `
85
- <svg viewBox="0 0 24 24" style="width:48px;height:48px;fill:#0078d4;animation:spin 1s linear infinite;">
86
- <path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>
128
+ <svg viewBox="0 0 24 24" style="width:48px;height:48px;fill:#555;">
129
+ <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"/>
87
130
  </svg>
88
- <div style="font-size:14px;color:#a0a0a0;">PDF Yükleniyor...</div>
131
+ <div class="pdf-loading-text" style="font-size:14px;color:#a0a0a0;">Sırada bekliyor...</div>
89
132
  <style>@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>
90
133
  `;
91
134
  iframeWrapper.appendChild(loadingPlaceholder);
@@ -103,17 +146,28 @@
103
146
 
104
147
  link.replaceWith(container);
105
148
 
106
- // LAZY LOADING with Intersection Observer
149
+ // LAZY LOADING with Intersection Observer + Queue
107
150
  var observer = new IntersectionObserver(function (entries) {
108
151
  entries.forEach(function (entry) {
109
152
  if (entry.isIntersecting) {
110
- // Load iframe only when visible
111
- loadPdfIframe(iframeWrapper, filename, loadingPlaceholder);
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);
112
166
  observer.disconnect();
113
167
  }
114
168
  });
115
169
  }, {
116
- rootMargin: '200px', // Start loading 200px before visible
170
+ rootMargin: '200px',
117
171
  threshold: 0
118
172
  });
119
173
 
@@ -123,21 +177,48 @@
123
177
  }
124
178
 
125
179
  function loadPdfIframe(wrapper, filename, placeholder) {
126
- var iframe = document.createElement('iframe');
127
- iframe.className = 'pdf-secure-iframe';
128
- iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;opacity:0;transition:opacity 0.3s;';
129
- iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
130
- iframe.setAttribute('frameborder', '0');
131
- iframe.setAttribute('allowfullscreen', 'true');
132
-
133
- // When iframe loads, fade in and remove placeholder
134
- iframe.onload = function () {
135
- iframe.style.opacity = '1';
136
- if (placeholder && placeholder.parentNode) {
137
- placeholder.remove();
138
- }
139
- };
140
-
141
- wrapper.appendChild(iframe);
180
+ return new Promise((resolve, reject) => {
181
+ // Create iframe HIDDEN (z-index: 1, under placeholder)
182
+ var iframe = document.createElement('iframe');
183
+ iframe.className = 'pdf-secure-iframe';
184
+ iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:1;';
185
+ iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
186
+ iframe.setAttribute('frameborder', '0');
187
+ iframe.setAttribute('allowfullscreen', 'true');
188
+
189
+ // Store resolver for postMessage callback
190
+ currentResolver = function () {
191
+ // Fade out placeholder, show iframe
192
+ if (placeholder) {
193
+ placeholder.style.opacity = '0';
194
+ setTimeout(function () {
195
+ if (placeholder.parentNode) {
196
+ placeholder.remove();
197
+ }
198
+ }, 300);
199
+ }
200
+ resolve();
201
+ };
202
+
203
+ iframe.onerror = function () {
204
+ currentResolver = null;
205
+ if (placeholder) {
206
+ var textEl = placeholder.querySelector('.pdf-loading-text');
207
+ if (textEl) textEl.textContent = 'Yükleme hatası!';
208
+ }
209
+ reject(new Error('Failed to load iframe'));
210
+ };
211
+
212
+ wrapper.appendChild(iframe);
213
+
214
+ // Timeout fallback (60 seconds for large PDFs)
215
+ setTimeout(function () {
216
+ if (currentResolver) {
217
+ console.log('[PDF-Secure] Queue: Timeout, forcing next');
218
+ currentResolver();
219
+ currentResolver = null;
220
+ }
221
+ }, 60000);
222
+ });
142
223
  }
143
224
  })();
@@ -1506,11 +1506,15 @@
1506
1506
  // Step 4: Load into viewer
1507
1507
  await loadPDFFromBuffer(pdfBuffer);
1508
1508
 
1509
- // Step 5: Security - clear references to prevent extraction
1510
- // Nullify the buffer after loading
1509
+ // Step 5: Notify parent that PDF is fully loaded (for queue system)
1510
+ if (window.parent && window.parent !== window) {
1511
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, '*');
1512
+ }
1513
+
1514
+ // Step 6: Security - clear references to prevent extraction
1511
1515
  pdfBuffer = null;
1512
1516
 
1513
- console.log('[PDF-Secure] Buffer references cleared');
1517
+ console.log('[PDF-Secure] PDF fully loaded and ready');
1514
1518
 
1515
1519
  } catch (err) {
1516
1520
  console.error('[PDF-Secure] Auto-load error:', err);