nodebb-plugin-pdf-secure 1.2.2 → 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.2",
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,15 +1,54 @@
1
1
  'use strict';
2
2
 
3
- console.log('[PDF-Secure] main.js loaded');
4
-
5
- // Main plugin logic - PDF links become inline embedded viewers
3
+ // Main plugin logic - PDF links become inline embedded viewers with lazy loading + queue
6
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
+
7
44
  try {
8
45
  var hooks = await app.require('hooks');
9
- console.log('[PDF-Secure] Hooks loaded');
10
46
 
11
47
  hooks.on('action:ajaxify.end', function () {
12
- console.log('[PDF-Secure] ajaxify.end');
48
+ // Clear queue on page change
49
+ loadQueue.length = 0;
50
+ isLoading = false;
51
+ currentResolver = null;
13
52
  interceptPdfLinks();
14
53
  });
15
54
  } catch (err) {
@@ -29,14 +68,13 @@ console.log('[PDF-Secure] main.js loaded');
29
68
  var href = link.getAttribute('href');
30
69
  var parts = href.split('/');
31
70
  var filename = parts[parts.length - 1];
32
- console.log('[PDF-Secure] Embedding:', filename);
33
71
 
34
- // Create inline viewer container with INLINE STYLES to ensure they work
72
+ // Create container
35
73
  var container = document.createElement('div');
36
74
  container.className = 'pdf-secure-embed';
37
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);';
38
76
 
39
- // Header with filename - ALL INLINE STYLES
77
+ // Header
40
78
  var header = document.createElement('div');
41
79
  header.className = 'pdf-secure-embed-header';
42
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);';
@@ -45,7 +83,6 @@ console.log('[PDF-Secure] main.js loaded');
45
83
  title.className = 'pdf-secure-embed-title';
46
84
  title.style.cssText = 'display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;font-weight:500;';
47
85
 
48
- // PDF icon with INLINE SIZE
49
86
  var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
50
87
  icon.setAttribute('viewBox', '0 0 24 24');
51
88
  icon.style.cssText = 'width:20px;height:20px;min-width:20px;max-width:20px;fill:#e81224;flex-shrink:0;';
@@ -58,7 +95,7 @@ console.log('[PDF-Secure] main.js loaded');
58
95
  title.appendChild(icon);
59
96
  title.appendChild(nameSpan);
60
97
 
61
- // Actions (fullscreen button)
98
+ // Actions
62
99
  var actions = document.createElement('div');
63
100
  actions.style.cssText = 'display:flex;gap:8px;';
64
101
 
@@ -78,35 +115,110 @@ console.log('[PDF-Secure] main.js loaded');
78
115
  header.appendChild(actions);
79
116
  container.appendChild(header);
80
117
 
81
- // Iframe wrapper
118
+ // Body with loading placeholder
82
119
  var iframeWrapper = document.createElement('div');
83
120
  iframeWrapper.className = 'pdf-secure-embed-body';
84
121
  iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
85
122
 
86
- var iframe = document.createElement('iframe');
87
- iframe.className = 'pdf-secure-iframe';
88
- iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
89
- iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
90
- iframe.setAttribute('frameborder', '0');
91
- iframe.setAttribute('allowfullscreen', 'true');
92
- iframe.setAttribute('loading', 'lazy');
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 = `
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"/>
130
+ </svg>
131
+ <div class="pdf-loading-text" style="font-size:14px;color:#a0a0a0;">Sırada bekliyor...</div>
132
+ <style>@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>
133
+ `;
134
+ iframeWrapper.appendChild(loadingPlaceholder);
93
135
 
94
- iframeWrapper.appendChild(iframe);
95
136
  container.appendChild(iframeWrapper);
96
137
 
97
- // Fullscreen button handler
138
+ // Fullscreen handler
98
139
  fullscreenBtn.addEventListener('click', function () {
99
- if (iframe.requestFullscreen) {
100
- iframe.requestFullscreen();
101
- } else if (iframe.webkitRequestFullscreen) {
102
- iframe.webkitRequestFullscreen();
103
- } else if (iframe.msRequestFullscreen) {
104
- iframe.msRequestFullscreen();
140
+ var iframe = iframeWrapper.querySelector('iframe');
141
+ if (iframe) {
142
+ if (iframe.requestFullscreen) iframe.requestFullscreen();
143
+ else if (iframe.webkitRequestFullscreen) iframe.webkitRequestFullscreen();
105
144
  }
106
145
  });
107
146
 
108
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
+
174
+ observer.observe(container);
109
175
  });
110
176
  });
111
177
  }
178
+
179
+ function loadPdfIframe(wrapper, filename, placeholder) {
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
+ });
223
+ }
112
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);