nodebb-plugin-pdf-secure2 1.2.30

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.
@@ -0,0 +1,477 @@
1
+ 'use strict';
2
+
3
+ // Main plugin logic - PDF links become inline embedded viewers with lazy loading + queue
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
+
25
+ // Fullscreen API support detection
26
+ // On touch devices (tablets, mobiles), always use CSS simulated fullscreen.
27
+ // Native fullscreen has an unblockable browser "scroll-to-exit" gesture on touch devices.
28
+ // Desktop keeps native fullscreen for the proper OS-level experience.
29
+ var isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
30
+ var fullscreenApiSupported = !isTouchDevice && !!(
31
+ document.documentElement.requestFullscreen ||
32
+ document.documentElement.webkitRequestFullscreen
33
+ );
34
+ var simulatedFullscreenIframe = null;
35
+ var savedBodyOverflow = '';
36
+
37
+ // Loading queue - only load one PDF at a time
38
+ const loadQueue = [];
39
+ let isLoading = false;
40
+ let currentResolver = null;
41
+
42
+ // ============================================
43
+ // SPA MEMORY CACHE - Cache decoded PDF buffers
44
+ // ============================================
45
+ const pdfBufferCache = new Map(); // filename -> ArrayBuffer
46
+ const CACHE_MAX_SIZE = 5; // ~50MB limit (avg 10MB per PDF)
47
+ let currentLoadingFilename = null;
48
+
49
+ function setCachedBuffer(filename, buffer) {
50
+ // Evict oldest if cache is full
51
+ if (pdfBufferCache.size >= CACHE_MAX_SIZE) {
52
+ const firstKey = pdfBufferCache.keys().next().value;
53
+ pdfBufferCache.delete(firstKey);
54
+ console.log('[PDF-Secure] Cache: Evicted', firstKey);
55
+ }
56
+ pdfBufferCache.set(filename, buffer);
57
+ console.log('[PDF-Secure] Cache: Stored', filename, '(', (buffer.byteLength / 1024 / 1024).toFixed(2), 'MB)');
58
+ }
59
+
60
+ // Listen for postMessage from iframe
61
+ window.addEventListener('message', function (event) {
62
+ // Security: Only accept messages from same origin
63
+ if (event.origin !== window.location.origin) return;
64
+
65
+ // PDF ready - resolve queue
66
+ if (event.data && event.data.type === 'pdf-secure-ready') {
67
+ console.log('[PDF-Secure] Queue: PDF ready -', event.data.filename);
68
+ if (currentResolver) {
69
+ currentResolver();
70
+ currentResolver = null;
71
+ }
72
+ }
73
+
74
+ // PDF buffer from viewer - cache it
75
+ if (event.data && event.data.type === 'pdf-secure-buffer') {
76
+ const { filename, buffer } = event.data;
77
+ if (filename && buffer) {
78
+ setCachedBuffer(filename, buffer);
79
+ }
80
+ }
81
+
82
+ // Fullscreen toggle request from iframe viewer
83
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-toggle') {
84
+ var sourceIframe = document.querySelector('.pdf-secure-iframe');
85
+ // Find the specific iframe that sent the message
86
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
87
+ if (f.contentWindow === event.source) sourceIframe = f;
88
+ });
89
+ if (!sourceIframe) return;
90
+
91
+ if (fullscreenApiSupported) {
92
+ // Native fullscreen path
93
+ var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
94
+ if (fsEl) {
95
+ if (document.exitFullscreen) document.exitFullscreen();
96
+ else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
97
+ } else {
98
+ if (sourceIframe.requestFullscreen) sourceIframe.requestFullscreen().catch(function () { });
99
+ else if (sourceIframe.webkitRequestFullscreen) sourceIframe.webkitRequestFullscreen();
100
+ }
101
+ } else {
102
+ // Simulated fullscreen path (iOS Safari / Chrome)
103
+ if (simulatedFullscreenIframe) {
104
+ exitSimulatedFullscreen();
105
+ } else {
106
+ enterSimulatedFullscreen(sourceIframe);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Fullscreen state query from iframe
112
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-query') {
113
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement) ||
114
+ (simulatedFullscreenIframe !== null);
115
+ if (event.source) {
116
+ event.source.postMessage({
117
+ type: 'pdf-secure-fullscreen-state',
118
+ isFullscreen: fsActive
119
+ }, event.origin);
120
+ }
121
+ }
122
+
123
+ // Viewer asking for cached buffer
124
+ if (event.data && event.data.type === 'pdf-secure-cache-request') {
125
+ const { filename } = event.data;
126
+ const cached = pdfBufferCache.get(filename);
127
+ if (cached && event.source) {
128
+ // Send cached buffer to viewer (transferable for 0-copy)
129
+ // Clone once: keep original in cache, transfer the copy
130
+ const copy = cached.slice(0);
131
+ event.source.postMessage({
132
+ type: 'pdf-secure-cache-response',
133
+ filename: filename,
134
+ buffer: copy
135
+ }, event.origin, [copy]);
136
+ console.log('[PDF-Secure] Cache: Hit -', filename);
137
+ } else if (event.source) {
138
+ // No cache, viewer will fetch normally
139
+ event.source.postMessage({
140
+ type: 'pdf-secure-cache-response',
141
+ filename: filename,
142
+ buffer: null
143
+ }, event.origin);
144
+ console.log('[PDF-Secure] Cache: Miss -', filename);
145
+ }
146
+ }
147
+ });
148
+
149
+ // Forward fullscreen state changes to all viewer iframes
150
+ function notifyFullscreenChange() {
151
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement);
152
+ if (fsActive) {
153
+ document.body.style.overscrollBehavior = 'none';
154
+ document.body.style.overflow = 'hidden';
155
+ } else {
156
+ document.body.style.overscrollBehavior = '';
157
+ document.body.style.overflow = '';
158
+ }
159
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
160
+ if (f.contentWindow) {
161
+ f.style.overscrollBehavior = 'none';
162
+ f.contentWindow.postMessage({
163
+ type: 'pdf-secure-fullscreen-state',
164
+ isFullscreen: fsActive
165
+ }, window.location.origin);
166
+ }
167
+ });
168
+ }
169
+ document.addEventListener('fullscreenchange', notifyFullscreenChange);
170
+ document.addEventListener('webkitfullscreenchange', notifyFullscreenChange);
171
+
172
+ // Touch handler to block parent scroll during simulated fullscreen
173
+ function parentFullscreenTouchHandler(e) {
174
+ e.preventDefault();
175
+ }
176
+
177
+ // Enter CSS simulated fullscreen (for iOS Safari/Chrome)
178
+ function enterSimulatedFullscreen(iframe) {
179
+ if (simulatedFullscreenIframe) return; // already in simulated fullscreen
180
+ simulatedFullscreenIframe = iframe;
181
+
182
+ // Save original iframe styles
183
+ iframe._savedStyle = {
184
+ position: iframe.style.position,
185
+ top: iframe.style.top,
186
+ left: iframe.style.left,
187
+ width: iframe.style.width,
188
+ height: iframe.style.height,
189
+ zIndex: iframe.style.zIndex
190
+ };
191
+
192
+ // Apply fullscreen styles
193
+ iframe.style.position = 'fixed';
194
+ iframe.style.top = '0';
195
+ iframe.style.left = '0';
196
+ iframe.style.width = '100vw';
197
+ iframe.style.width = '100dvw'; // Override: dynamic viewport (excludes browser chrome)
198
+ iframe.style.height = '100vh';
199
+ iframe.style.height = '100dvh'; // Override: dynamic viewport (excludes address bar on mobile)
200
+ iframe.style.zIndex = '2147483647';
201
+
202
+ // Lock body scroll
203
+ savedBodyOverflow = document.body.style.overflow;
204
+ document.body.style.overflow = 'hidden';
205
+ document.body.style.overscrollBehavior = 'none';
206
+
207
+ // Block touch scroll on parent
208
+ document.addEventListener('touchmove', parentFullscreenTouchHandler, { passive: false });
209
+
210
+ // Notify iframe it is now fullscreen
211
+ if (iframe.contentWindow) {
212
+ iframe.contentWindow.postMessage({
213
+ type: 'pdf-secure-fullscreen-state',
214
+ isFullscreen: true
215
+ }, window.location.origin);
216
+ }
217
+ }
218
+
219
+ // Exit CSS simulated fullscreen
220
+ function exitSimulatedFullscreen() {
221
+ if (!simulatedFullscreenIframe) return;
222
+ var iframe = simulatedFullscreenIframe;
223
+ simulatedFullscreenIframe = null;
224
+
225
+ // Restore original iframe styles
226
+ if (iframe._savedStyle) {
227
+ iframe.style.position = iframe._savedStyle.position;
228
+ iframe.style.top = iframe._savedStyle.top;
229
+ iframe.style.left = iframe._savedStyle.left;
230
+ iframe.style.width = iframe._savedStyle.width;
231
+ iframe.style.height = iframe._savedStyle.height;
232
+ iframe.style.zIndex = iframe._savedStyle.zIndex;
233
+ delete iframe._savedStyle;
234
+ }
235
+
236
+ // Restore body scroll
237
+ document.body.style.overflow = savedBodyOverflow;
238
+ document.body.style.overscrollBehavior = '';
239
+ savedBodyOverflow = '';
240
+
241
+ // Remove parent touch block
242
+ document.removeEventListener('touchmove', parentFullscreenTouchHandler);
243
+
244
+ // Notify iframe it is no longer fullscreen
245
+ if (iframe.contentWindow) {
246
+ iframe.contentWindow.postMessage({
247
+ type: 'pdf-secure-fullscreen-state',
248
+ isFullscreen: false
249
+ }, window.location.origin);
250
+ }
251
+ }
252
+
253
+ async function processQueue() {
254
+ if (isLoading || loadQueue.length === 0) return;
255
+
256
+ isLoading = true;
257
+ const { wrapper, filename, placeholder } = loadQueue.shift();
258
+
259
+ try {
260
+ await loadPdfIframe(wrapper, filename, placeholder);
261
+ } catch (err) {
262
+ console.error('[PDF-Secure] Load error:', err);
263
+ }
264
+
265
+ isLoading = false;
266
+
267
+ // Small delay between loads
268
+ setTimeout(processQueue, 200);
269
+ }
270
+
271
+ function queuePdfLoad(wrapper, filename, placeholder) {
272
+ loadQueue.push({ wrapper, filename, placeholder });
273
+ processQueue();
274
+ }
275
+
276
+ try {
277
+ var hooks = await app.require('hooks');
278
+
279
+ hooks.on('action:ajaxify.end', function () {
280
+ // Clear queue on page change
281
+ loadQueue.length = 0;
282
+ isLoading = false;
283
+ currentResolver = null;
284
+ // Exit simulated fullscreen on SPA navigation
285
+ exitSimulatedFullscreen();
286
+ interceptPdfLinks();
287
+ });
288
+ } catch (err) {
289
+ console.error('[PDF-Secure] Init error:', err);
290
+ }
291
+
292
+ function interceptPdfLinks() {
293
+ var postContents = document.querySelectorAll('[component="post/content"]');
294
+
295
+ postContents.forEach(function (content) {
296
+ // NEW: Detect server-rendered secure placeholders (hides URL from source)
297
+ var placeholders = content.querySelectorAll('.pdf-secure-placeholder');
298
+ placeholders.forEach(function (placeholder) {
299
+ if (placeholder.dataset.pdfSecureProcessed) return;
300
+ placeholder.dataset.pdfSecureProcessed = 'true';
301
+
302
+ var filename = placeholder.dataset.filename;
303
+ var displayName = placeholder.querySelector('span')?.textContent || filename;
304
+
305
+ createPdfViewer(placeholder, filename, displayName);
306
+ });
307
+
308
+ // FALLBACK: Detect old-style PDF links (for backwards compatibility)
309
+ var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
310
+ pdfLinks.forEach(function (link) {
311
+ if (link.dataset.pdfSecure) return;
312
+ link.dataset.pdfSecure = 'true';
313
+
314
+ var href = link.getAttribute('href');
315
+ var parts = href.split('/');
316
+ var filename = parts[parts.length - 1];
317
+ var displayName = link.textContent || filename;
318
+
319
+ createPdfViewer(link, filename, displayName);
320
+ });
321
+ });
322
+ }
323
+
324
+ function createPdfViewer(targetElement, filename, displayName) {
325
+
326
+ // Create container
327
+ var container = document.createElement('div');
328
+ container.className = 'pdf-secure-embed';
329
+ 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);';
330
+
331
+ // Header
332
+ var header = document.createElement('div');
333
+ header.className = 'pdf-secure-embed-header';
334
+ 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);';
335
+
336
+ var title = document.createElement('div');
337
+ title.className = 'pdf-secure-embed-title';
338
+ title.style.cssText = 'display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;font-weight:500;';
339
+
340
+ var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
341
+ icon.setAttribute('viewBox', '0 0 24 24');
342
+ icon.style.cssText = 'width:20px;height:20px;min-width:20px;max-width:20px;fill:#e81224;flex-shrink:0;';
343
+ 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"/>';
344
+
345
+ var nameSpan = document.createElement('span');
346
+ nameSpan.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px;';
347
+ try { nameSpan.textContent = decodeURIComponent(displayName); }
348
+ catch (e) { nameSpan.textContent = displayName; }
349
+
350
+ title.appendChild(icon);
351
+ title.appendChild(nameSpan);
352
+
353
+ header.appendChild(title);
354
+ container.appendChild(header);
355
+
356
+ // Body with loading placeholder
357
+ var iframeWrapper = document.createElement('div');
358
+ iframeWrapper.className = 'pdf-secure-embed-body';
359
+ iframeWrapper.style.cssText = 'position:relative;width:100%;height:600px;background:#525659;';
360
+
361
+ // Loading placeholder - ALWAYS VISIBLE until PDF ready (z-index: 10)
362
+ var loadingPlaceholder = document.createElement('div');
363
+ loadingPlaceholder.className = 'pdf-loading-placeholder';
364
+ 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;';
365
+ loadingPlaceholder.innerHTML = `
366
+ <svg viewBox="0 0 24 24" style="width:48px;height:48px;fill:#555;">
367
+ <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"/>
368
+ </svg>
369
+ <div class="pdf-loading-text" style="font-size:14px;color:#a0a0a0;">Sırada bekliyor...</div>
370
+ <style>@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}</style>
371
+ `;
372
+ iframeWrapper.appendChild(loadingPlaceholder);
373
+
374
+ container.appendChild(iframeWrapper);
375
+
376
+ targetElement.replaceWith(container);
377
+
378
+ // LAZY LOADING with Intersection Observer + Queue
379
+ // Smart loading: only loads PDFs that are actually visible
380
+ var queueEntry = null; // Track if this PDF is in queue
381
+ var observer = new IntersectionObserver(function (entries) {
382
+ entries.forEach(function (entry) {
383
+ if (entry.isIntersecting) {
384
+ // Update placeholder to show loading state
385
+ var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
386
+ if (textEl) textEl.textContent = 'PDF Yükleniyor...';
387
+
388
+ var svgEl = loadingPlaceholder.querySelector('svg');
389
+ if (svgEl) {
390
+ svgEl.style.fill = '#0078d4';
391
+ svgEl.style.animation = 'spin 1s linear infinite';
392
+ svgEl.innerHTML = '<path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>';
393
+ }
394
+
395
+ // Add to queue (if not already)
396
+ if (!queueEntry) {
397
+ queueEntry = { wrapper: iframeWrapper, filename, placeholder: loadingPlaceholder };
398
+ loadQueue.push(queueEntry);
399
+ processQueue();
400
+ }
401
+ } else {
402
+ // LEFT viewport - remove from queue if waiting
403
+ if (queueEntry && loadQueue.includes(queueEntry)) {
404
+ var idx = loadQueue.indexOf(queueEntry);
405
+ if (idx > -1) {
406
+ loadQueue.splice(idx, 1);
407
+ console.log('[PDF-Secure] Queue: Removed (left viewport) -', filename);
408
+
409
+ // Reset placeholder to waiting state
410
+ var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
411
+ if (textEl) textEl.textContent = 'Sırada bekliyor...';
412
+ var svgEl = loadingPlaceholder.querySelector('svg');
413
+ if (svgEl) {
414
+ svgEl.style.fill = '#555';
415
+ svgEl.style.animation = 'none';
416
+ svgEl.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"/>';
417
+ }
418
+ }
419
+ queueEntry = null;
420
+ }
421
+ }
422
+ });
423
+ }, {
424
+ rootMargin: '0px', // Only trigger when actually visible
425
+ threshold: 0
426
+ });
427
+
428
+ observer.observe(container);
429
+ }
430
+
431
+ function loadPdfIframe(wrapper, filename, placeholder) {
432
+ return new Promise((resolve, reject) => {
433
+ // Create iframe HIDDEN (z-index: 1, under placeholder)
434
+ var iframe = document.createElement('iframe');
435
+ iframe.className = 'pdf-secure-iframe';
436
+ iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:1;';
437
+ iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
438
+ iframe.setAttribute('frameborder', '0');
439
+ iframe.setAttribute('allowfullscreen', 'true');
440
+ iframe.setAttribute('allow', 'fullscreen');
441
+
442
+ // Store resolver for postMessage callback
443
+ currentResolver = function () {
444
+ // Fade out placeholder, show iframe
445
+ if (placeholder) {
446
+ placeholder.style.opacity = '0';
447
+ setTimeout(function () {
448
+ if (placeholder.parentNode) {
449
+ placeholder.remove();
450
+ }
451
+ }, 300);
452
+ }
453
+ resolve();
454
+ };
455
+
456
+ iframe.onerror = function () {
457
+ currentResolver = null;
458
+ if (placeholder) {
459
+ var textEl = placeholder.querySelector('.pdf-loading-text');
460
+ if (textEl) textEl.textContent = 'Yükleme hatası!';
461
+ }
462
+ reject(new Error('Failed to load iframe'));
463
+ };
464
+
465
+ wrapper.appendChild(iframe);
466
+
467
+ // Timeout fallback (60 seconds for large PDFs)
468
+ setTimeout(function () {
469
+ if (currentResolver) {
470
+ console.log('[PDF-Secure] Queue: Timeout, forcing next');
471
+ currentResolver();
472
+ currentResolver = null;
473
+ }
474
+ }, 60000);
475
+ });
476
+ }
477
+ })();
Binary file