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 +34 -26
- package/package.json +1 -1
- package/static/lib/main.js +138 -26
- package/static/viewer.html +7 -3
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
|
-
//
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
package/static/lib/main.js
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
138
|
+
// Fullscreen handler
|
|
98
139
|
fullscreenBtn.addEventListener('click', function () {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
})();
|
package/static/viewer.html
CHANGED
|
@@ -1506,11 +1506,15 @@
|
|
|
1506
1506
|
// Step 4: Load into viewer
|
|
1507
1507
|
await loadPDFFromBuffer(pdfBuffer);
|
|
1508
1508
|
|
|
1509
|
-
// Step 5:
|
|
1510
|
-
|
|
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]
|
|
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);
|