nodebb-plugin-pdf-secure 1.2.23 → 1.2.26
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/lib/controllers.js +2 -6
- package/library.js +11 -6
- package/package.json +1 -1
- package/static/templates/admin/plugins/pdf-secure.tpl +7 -1
- package/static/viewer-app.js +100 -30
- package/static/viewer.css +8 -4
- package/static/viewer.html +108 -34
package/lib/controllers.js
CHANGED
|
@@ -45,12 +45,8 @@ Controllers.servePdfBinary = async function (req, res) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
pdfBuffer = await pdfHandler.getFullPdf(data.file);
|
|
51
|
-
} else {
|
|
52
|
-
pdfBuffer = await pdfHandler.getSinglePagePdf(data.file);
|
|
53
|
-
}
|
|
48
|
+
// Always send full PDF - page restriction is handled client-side
|
|
49
|
+
const pdfBuffer = await pdfHandler.getFullPdf(data.file);
|
|
54
50
|
|
|
55
51
|
// Apply partial XOR encryption with dynamic key from nonce
|
|
56
52
|
const encodedBuffer = partialXorEncode(pdfBuffer, data.xorKey);
|
package/library.js
CHANGED
|
@@ -78,20 +78,24 @@ plugin.init = async (params) => {
|
|
|
78
78
|
return res.status(500).send('Viewer not available');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// Check if user is Premium (admins/global mods always premium)
|
|
81
|
+
// Check if user is Premium or Lite (admins/global mods always premium)
|
|
82
82
|
let isPremium = false;
|
|
83
|
+
let isLite = false;
|
|
83
84
|
if (req.uid) {
|
|
84
|
-
const [isAdmin, isGlobalMod, isPremiumMember] = await Promise.all([
|
|
85
|
+
const [isAdmin, isGlobalMod, isPremiumMember, isLiteMember] = await Promise.all([
|
|
85
86
|
groups.isMember(req.uid, 'administrators'),
|
|
86
87
|
groups.isMember(req.uid, 'Global Moderators'),
|
|
87
88
|
groups.isMember(req.uid, 'Premium'),
|
|
89
|
+
groups.isMember(req.uid, 'Lite'),
|
|
88
90
|
]);
|
|
89
91
|
isPremium = isAdmin || isGlobalMod || isPremiumMember;
|
|
92
|
+
// Lite: full PDF access but restricted UI (no annotations, sidebar, etc.)
|
|
93
|
+
isLite = !isPremium && isLiteMember;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const nonceData = nonceStore.generate(req.uid || 0, safeName,
|
|
96
|
+
// Lite users get full PDF like premium (for nonce/server-side PDF data)
|
|
97
|
+
const hasFullAccess = isPremium || isLite;
|
|
98
|
+
const nonceData = nonceStore.generate(req.uid || 0, safeName, hasFullAccess);
|
|
95
99
|
|
|
96
100
|
// Serve the viewer template with comprehensive security headers
|
|
97
101
|
res.set({
|
|
@@ -121,7 +125,8 @@ plugin.init = async (params) => {
|
|
|
121
125
|
csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
|
|
122
126
|
nonce: ${JSON.stringify(nonceData.nonce)},
|
|
123
127
|
dk: ${JSON.stringify(nonceData.xorKey)},
|
|
124
|
-
isPremium: ${JSON.stringify(isPremium)}
|
|
128
|
+
isPremium: ${JSON.stringify(isPremium)},
|
|
129
|
+
isLite: ${JSON.stringify(isLite)}
|
|
125
130
|
};
|
|
126
131
|
</script>
|
|
127
132
|
</head>`);
|
package/package.json
CHANGED
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
<div class="mb-3">
|
|
15
15
|
<label class="form-label" for="premiumGroup">Premium Group Name</label>
|
|
16
16
|
<input type="text" id="premiumGroup" name="premiumGroup" title="Premium Group Name" class="form-control" placeholder="Premium" value="Premium">
|
|
17
|
-
<div class="form-text">Users in this group can view full PDFs. Others can only see the first page.</div>
|
|
17
|
+
<div class="form-text">Users in this group can view full PDFs with all tools. Others can only see the first page.</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="mb-3">
|
|
21
|
+
<label class="form-label" for="liteGroup">Lite Group Name</label>
|
|
22
|
+
<input type="text" id="liteGroup" name="liteGroup" title="Lite Group Name" class="form-control" placeholder="Lite" value="Lite">
|
|
23
|
+
<div class="form-text">Users in this group can view full PDFs but only with zoom and fullscreen. No annotations, sidebar, or other tools.</div>
|
|
18
24
|
</div>
|
|
19
25
|
|
|
20
26
|
<div class="form-check form-switch mb-3">
|
package/static/viewer-app.js
CHANGED
|
@@ -200,6 +200,50 @@
|
|
|
200
200
|
viewerEl.appendChild(overlay);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// ============================================
|
|
204
|
+
// LITE MODE: Hide all tools except fullscreen and zoom
|
|
205
|
+
// Lite users can view full PDF but cannot use annotations,
|
|
206
|
+
// sidebar, rotate, sepia, overflow menu, etc.
|
|
207
|
+
// ============================================
|
|
208
|
+
function applyLiteMode() {
|
|
209
|
+
// Hide sidebar button (İçindekiler)
|
|
210
|
+
const sidebarBtn = document.getElementById('sidebarBtn');
|
|
211
|
+
if (sidebarBtn) sidebarBtn.style.display = 'none';
|
|
212
|
+
|
|
213
|
+
// Close sidebar if open
|
|
214
|
+
const sidebarEl = document.getElementById('sidebar');
|
|
215
|
+
if (sidebarEl) sidebarEl.classList.remove('open');
|
|
216
|
+
|
|
217
|
+
// Hide entire annotation tools group (highlight, draw, eraser, select, undo/redo, text, shapes)
|
|
218
|
+
const annotationGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(3)');
|
|
219
|
+
if (annotationGroup) annotationGroup.style.display = 'none';
|
|
220
|
+
|
|
221
|
+
// In the zoom/utility group, hide everything except zoomIn and zoomOut
|
|
222
|
+
const keepIds = new Set(['zoomIn', 'zoomOut']);
|
|
223
|
+
const utilityGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(5)');
|
|
224
|
+
if (utilityGroup) {
|
|
225
|
+
Array.from(utilityGroup.children).forEach(function (child) {
|
|
226
|
+
if (!keepIds.has(child.id)) {
|
|
227
|
+
child.style.display = 'none';
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Hide bottom toolbar annotation tools (mobile), keep only fullscreen button
|
|
233
|
+
const bottomToolbarInner = document.getElementById('bottomToolbarInner');
|
|
234
|
+
if (bottomToolbarInner) bottomToolbarInner.style.display = 'none';
|
|
235
|
+
|
|
236
|
+
// Hide all top-level separators between groups
|
|
237
|
+
document.querySelectorAll('#toolbar > .separator').forEach(function (sep) {
|
|
238
|
+
sep.style.display = 'none';
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Hide page info (Lite users don't need page input)
|
|
242
|
+
// Actually keep page info for navigation awareness
|
|
243
|
+
|
|
244
|
+
console.log('[PDF-Secure] Lite mode applied - restricted toolbar');
|
|
245
|
+
}
|
|
246
|
+
|
|
203
247
|
// ============================================
|
|
204
248
|
// PREMIUM INTEGRITY: Periodic Check (2s interval)
|
|
205
249
|
// Hides pages 2+, recreates overlay if removed, forces page 1
|
|
@@ -342,8 +386,8 @@
|
|
|
342
386
|
pdfBuffer = encodedBuffer;
|
|
343
387
|
}
|
|
344
388
|
|
|
345
|
-
// Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
|
|
346
|
-
if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
|
|
389
|
+
// Send buffer to parent for caching (premium/lite only - non-premium must not leak decoded buffer)
|
|
390
|
+
if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
|
|
347
391
|
// Clone buffer for parent (we keep original)
|
|
348
392
|
const bufferCopy = pdfBuffer.slice(0);
|
|
349
393
|
window.parent.postMessage({
|
|
@@ -360,7 +404,7 @@
|
|
|
360
404
|
await loadPDFFromBuffer(pdfBuffer);
|
|
361
405
|
|
|
362
406
|
// Premium Gate: Client-side page restriction for non-premium users
|
|
363
|
-
if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
|
|
407
|
+
if (config.isPremium === false && !config.isLite && pdfDoc && pdfDoc.numPages > 1) {
|
|
364
408
|
premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
|
|
365
409
|
showPremiumLockOverlay(pdfDoc.numPages);
|
|
366
410
|
startPeriodicCheck();
|
|
@@ -369,6 +413,11 @@
|
|
|
369
413
|
premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
|
|
370
414
|
}
|
|
371
415
|
|
|
416
|
+
// Lite Mode: Hide all tools except fullscreen and zoom
|
|
417
|
+
if (config.isLite) {
|
|
418
|
+
applyLiteMode();
|
|
419
|
+
}
|
|
420
|
+
|
|
372
421
|
// Step 5: Moved to pagerendered event for proper timing
|
|
373
422
|
|
|
374
423
|
// Step 6: Security - clear references to prevent extraction
|
|
@@ -653,27 +702,43 @@
|
|
|
653
702
|
|
|
654
703
|
// Overflow menu toggle
|
|
655
704
|
document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
|
|
656
|
-
overflowDropdown.
|
|
705
|
+
overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
|
|
706
|
+
overflowDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
707
|
+
|
|
708
|
+
// Overflow menu actions — use both click + pointerup for reliable tablet/touch support
|
|
709
|
+
function attachOverflowAction(id, action) {
|
|
710
|
+
const el = document.getElementById(id);
|
|
711
|
+
let fired = false;
|
|
712
|
+
function run() {
|
|
713
|
+
if (fired) return;
|
|
714
|
+
fired = true;
|
|
715
|
+
requestAnimationFrame(() => { fired = false; });
|
|
716
|
+
action();
|
|
717
|
+
}
|
|
718
|
+
el.addEventListener('click', run);
|
|
719
|
+
el.addEventListener('pointerup', (e) => {
|
|
720
|
+
if (e.pointerType === 'touch' || e.pointerType === 'pen') run();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
657
723
|
|
|
658
|
-
|
|
659
|
-
document.getElementById('overflowRotateLeft').onclick = () => {
|
|
724
|
+
attachOverflowAction('overflowRotateLeft', () => {
|
|
660
725
|
rotatePage(-90);
|
|
661
726
|
closeAllDropdowns();
|
|
662
|
-
};
|
|
663
|
-
|
|
727
|
+
});
|
|
728
|
+
attachOverflowAction('overflowRotateRight', () => {
|
|
664
729
|
rotatePage(90);
|
|
665
730
|
closeAllDropdowns();
|
|
666
|
-
};
|
|
667
|
-
|
|
731
|
+
});
|
|
732
|
+
attachOverflowAction('overflowSepia', () => {
|
|
668
733
|
document.getElementById('sepiaBtn').click();
|
|
669
734
|
document.getElementById('overflowSepia').classList.toggle('active',
|
|
670
735
|
document.getElementById('sepiaBtn').classList.contains('active'));
|
|
671
736
|
closeAllDropdowns();
|
|
672
|
-
};
|
|
673
|
-
|
|
737
|
+
});
|
|
738
|
+
attachOverflowAction('overflowFullscreen', () => {
|
|
674
739
|
toggleFullscreen();
|
|
675
740
|
closeAllDropdowns();
|
|
676
|
-
};
|
|
741
|
+
});
|
|
677
742
|
|
|
678
743
|
// Close dropdowns when clicking outside
|
|
679
744
|
document.addEventListener('click', (e) => {
|
|
@@ -2393,46 +2458,51 @@
|
|
|
2393
2458
|
if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
|
|
2394
2459
|
|
|
2395
2460
|
const key = e.key.toLowerCase();
|
|
2461
|
+
const isLiteMode = _cfg && _cfg.isLite;
|
|
2396
2462
|
|
|
2397
|
-
//
|
|
2398
|
-
if (key === 'h') { setTool('highlight'); e.preventDefault(); }
|
|
2399
|
-
if (key === 'p') { setTool('pen'); e.preventDefault(); }
|
|
2400
|
-
if (key === 'e') { setTool('eraser'); e.preventDefault(); }
|
|
2401
|
-
if (key === 't') { setTool('text'); e.preventDefault(); }
|
|
2402
|
-
if (key === 'r') { setTool('shape'); e.preventDefault(); }
|
|
2403
|
-
if (key === 'v') { setTool('select'); e.preventDefault(); }
|
|
2463
|
+
// Fullscreen shortcut (always allowed)
|
|
2404
2464
|
if (key === 'f') { toggleFullscreen(); e.preventDefault(); }
|
|
2405
2465
|
|
|
2406
|
-
//
|
|
2407
|
-
if (
|
|
2466
|
+
// Tool shortcuts (blocked in Lite mode)
|
|
2467
|
+
if (!isLiteMode) {
|
|
2468
|
+
if (key === 'h') { setTool('highlight'); e.preventDefault(); }
|
|
2469
|
+
if (key === 'p') { setTool('pen'); e.preventDefault(); }
|
|
2470
|
+
if (key === 'e') { setTool('eraser'); e.preventDefault(); }
|
|
2471
|
+
if (key === 't') { setTool('text'); e.preventDefault(); }
|
|
2472
|
+
if (key === 'r') { setTool('shape'); e.preventDefault(); }
|
|
2473
|
+
if (key === 'v') { setTool('select'); e.preventDefault(); }
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Delete selected annotation(s) (blocked in Lite mode)
|
|
2477
|
+
if (!isLiteMode && (key === 'delete' || key === 'backspace') && (selectedAnnotation || multiSelectedAnnotations.length > 0)) {
|
|
2408
2478
|
deleteSelectedAnnotation();
|
|
2409
2479
|
e.preventDefault();
|
|
2410
2480
|
}
|
|
2411
2481
|
|
|
2412
|
-
// Undo/Redo
|
|
2413
|
-
if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {
|
|
2482
|
+
// Undo/Redo (blocked in Lite mode)
|
|
2483
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {
|
|
2414
2484
|
performUndo();
|
|
2415
2485
|
e.preventDefault();
|
|
2416
2486
|
return;
|
|
2417
2487
|
}
|
|
2418
|
-
if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {
|
|
2488
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {
|
|
2419
2489
|
performRedo();
|
|
2420
2490
|
e.preventDefault();
|
|
2421
2491
|
return;
|
|
2422
2492
|
}
|
|
2423
2493
|
|
|
2424
|
-
// Copy/Paste annotations
|
|
2425
|
-
if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
|
|
2494
|
+
// Copy/Paste annotations (blocked in Lite mode)
|
|
2495
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
|
|
2426
2496
|
copySelectedAnnotation();
|
|
2427
2497
|
e.preventDefault();
|
|
2428
2498
|
}
|
|
2429
|
-
if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
|
|
2499
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
|
|
2430
2500
|
pasteAnnotation();
|
|
2431
2501
|
e.preventDefault();
|
|
2432
2502
|
}
|
|
2433
2503
|
|
|
2434
|
-
//
|
|
2435
|
-
if (key === 's') {
|
|
2504
|
+
// Sidebar toggle (blocked in Lite mode)
|
|
2505
|
+
if (!isLiteMode && key === 's') {
|
|
2436
2506
|
document.getElementById('sidebarBtn').click();
|
|
2437
2507
|
e.preventDefault();
|
|
2438
2508
|
}
|
package/static/viewer.css
CHANGED
|
@@ -115,6 +115,7 @@ body {
|
|
|
115
115
|
align-items: center;
|
|
116
116
|
justify-content: center;
|
|
117
117
|
transition: background 0.1s;
|
|
118
|
+
touch-action: manipulation;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
.toolbarBtn:hover {
|
|
@@ -426,8 +427,12 @@ body {
|
|
|
426
427
|
border-radius: 6px;
|
|
427
428
|
font-size: 14px;
|
|
428
429
|
white-space: nowrap;
|
|
430
|
+
touch-action: manipulation;
|
|
431
|
+
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
|
432
|
+
user-select: none;
|
|
433
|
+
-webkit-user-select: none;
|
|
429
434
|
}
|
|
430
|
-
.overflowItem:hover { background: var(--bg-tertiary); }
|
|
435
|
+
.overflowItem:hover, .overflowItem:active { background: var(--bg-tertiary); }
|
|
431
436
|
.overflowItem svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; }
|
|
432
437
|
.overflowItem.active { color: var(--accent); }
|
|
433
438
|
.overflowDivider { height: 1px; background: var(--border-color); margin: 6px 0; }
|
|
@@ -436,13 +441,12 @@ body {
|
|
|
436
441
|
#overflowWrapper { display: flex; }
|
|
437
442
|
.overflowSep { display: block; }
|
|
438
443
|
|
|
439
|
-
/* Hide rotate, sepia and their separators (children 3-
|
|
444
|
+
/* Hide rotate, sepia and their separators (children 3-7 of view group) */
|
|
440
445
|
.toolbarGroup:nth-child(5) > :nth-child(3),
|
|
441
446
|
.toolbarGroup:nth-child(5) > :nth-child(4),
|
|
442
447
|
.toolbarGroup:nth-child(5) > :nth-child(5),
|
|
443
448
|
.toolbarGroup:nth-child(5) > :nth-child(6),
|
|
444
|
-
.toolbarGroup:nth-child(5) > :nth-child(7)
|
|
445
|
-
.toolbarGroup:nth-child(5) > :nth-child(8) { display: none !important; }
|
|
449
|
+
.toolbarGroup:nth-child(5) > :nth-child(7) { display: none !important; }
|
|
446
450
|
|
|
447
451
|
/* Shape Grid */
|
|
448
452
|
.shapeGrid {
|
package/static/viewer.html
CHANGED
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
align-items: center;
|
|
133
133
|
justify-content: center;
|
|
134
134
|
transition: background 0.1s;
|
|
135
|
+
touch-action: manipulation;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
.toolbarBtn:hover {
|
|
@@ -455,9 +456,14 @@
|
|
|
455
456
|
border-radius: 6px;
|
|
456
457
|
font-size: 14px;
|
|
457
458
|
white-space: nowrap;
|
|
459
|
+
touch-action: manipulation;
|
|
460
|
+
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
|
461
|
+
user-select: none;
|
|
462
|
+
-webkit-user-select: none;
|
|
458
463
|
}
|
|
459
464
|
|
|
460
|
-
.overflowItem:hover
|
|
465
|
+
.overflowItem:hover,
|
|
466
|
+
.overflowItem:active {
|
|
461
467
|
background: var(--bg-tertiary);
|
|
462
468
|
}
|
|
463
469
|
|
|
@@ -487,13 +493,12 @@
|
|
|
487
493
|
display: block;
|
|
488
494
|
}
|
|
489
495
|
|
|
490
|
-
/* Hide rotate, sepia and their separators (children 3-
|
|
496
|
+
/* Hide rotate, sepia and their separators (children 3-7 of view group) */
|
|
491
497
|
.toolbarGroup:nth-child(5)> :nth-child(3),
|
|
492
498
|
.toolbarGroup:nth-child(5)> :nth-child(4),
|
|
493
499
|
.toolbarGroup:nth-child(5)> :nth-child(5),
|
|
494
500
|
.toolbarGroup:nth-child(5)> :nth-child(6),
|
|
495
|
-
.toolbarGroup:nth-child(5)> :nth-child(7)
|
|
496
|
-
.toolbarGroup:nth-child(5)> :nth-child(8) {
|
|
501
|
+
.toolbarGroup:nth-child(5)> :nth-child(7) {
|
|
497
502
|
display: none !important;
|
|
498
503
|
}
|
|
499
504
|
|
|
@@ -1687,6 +1692,13 @@
|
|
|
1687
1692
|
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" />
|
|
1688
1693
|
</svg>
|
|
1689
1694
|
</button>
|
|
1695
|
+
|
|
1696
|
+
<!-- Fullscreen Toggle -->
|
|
1697
|
+
<button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
|
|
1698
|
+
<svg viewBox="0 0 24 24" id="fullscreenIcon">
|
|
1699
|
+
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
|
1700
|
+
</svg>
|
|
1701
|
+
</button>
|
|
1690
1702
|
</div>
|
|
1691
1703
|
|
|
1692
1704
|
|
|
@@ -1952,15 +1964,6 @@
|
|
|
1952
1964
|
</svg>
|
|
1953
1965
|
</button>
|
|
1954
1966
|
|
|
1955
|
-
<div class="separator"></div>
|
|
1956
|
-
|
|
1957
|
-
<!-- Fullscreen Toggle -->
|
|
1958
|
-
<button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
|
|
1959
|
-
<svg viewBox="0 0 24 24" id="fullscreenIcon">
|
|
1960
|
-
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
|
1961
|
-
</svg>
|
|
1962
|
-
</button>
|
|
1963
|
-
|
|
1964
1967
|
<div class="separator overflowSep"></div>
|
|
1965
1968
|
|
|
1966
1969
|
<div class="toolbarBtnWithDropdown" id="overflowWrapper">
|
|
@@ -2259,7 +2262,7 @@
|
|
|
2259
2262
|
}
|
|
2260
2263
|
|
|
2261
2264
|
function resetOverlayCSS(overlay) {
|
|
2262
|
-
if (!isOverlayTampered(overlay)) return;
|
|
2265
|
+
if (!isOverlayTampered(overlay)) return;
|
|
2263
2266
|
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;z-index:15;opacity:1;visibility:visible;pointer-events:auto;';
|
|
2264
2267
|
}
|
|
2265
2268
|
|
|
@@ -2268,15 +2271,12 @@
|
|
|
2268
2271
|
pages.forEach(function (page) {
|
|
2269
2272
|
var pageNum = parseInt(page.dataset.pageNumber || '0', 10);
|
|
2270
2273
|
if (pageNum > 1) {
|
|
2271
|
-
// Ensure page is visible (undo any old display:none)
|
|
2272
2274
|
if (page.style.display === 'none') {
|
|
2273
2275
|
page.style.display = '';
|
|
2274
2276
|
}
|
|
2275
2277
|
injectPageLock(page);
|
|
2276
|
-
// CSS integrity: reset overlay styles in case of tampering
|
|
2277
2278
|
var existing = page.querySelector('.page-lock-overlay');
|
|
2278
2279
|
if (existing) resetOverlayCSS(existing);
|
|
2279
|
-
// Ensure blur class is present
|
|
2280
2280
|
if (!page.classList.contains('page-locked-blur')) {
|
|
2281
2281
|
page.classList.add('page-locked-blur');
|
|
2282
2282
|
}
|
|
@@ -2323,13 +2323,11 @@
|
|
|
2323
2323
|
for (var i = 0; i < mutations.length; i++) {
|
|
2324
2324
|
var target = mutations[i].target;
|
|
2325
2325
|
if (!target || !target.classList) continue;
|
|
2326
|
-
// Overlay CSS tampered - reset only if values actually differ
|
|
2327
2326
|
if (target.classList.contains('page-lock-overlay')) {
|
|
2328
2327
|
if (isOverlayTampered(target)) {
|
|
2329
2328
|
resetOverlayCSS(target);
|
|
2330
2329
|
}
|
|
2331
2330
|
}
|
|
2332
|
-
// Blur class removed from locked page - re-add it
|
|
2333
2331
|
if (target.classList.contains('page') && !target.classList.contains('page-locked-blur')) {
|
|
2334
2332
|
var pageNum = parseInt(target.dataset.pageNumber || '0', 10);
|
|
2335
2333
|
if (pageNum > 1) {
|
|
@@ -2340,6 +2338,47 @@
|
|
|
2340
2338
|
}).observe(viewerEl, { subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
|
|
2341
2339
|
}
|
|
2342
2340
|
|
|
2341
|
+
// ============================================
|
|
2342
|
+
// LITE MODE: Hide all tools except fullscreen and zoom
|
|
2343
|
+
// Lite users can view full PDF but cannot use annotations,
|
|
2344
|
+
// sidebar, rotate, sepia, overflow menu, etc.
|
|
2345
|
+
// ============================================
|
|
2346
|
+
function applyLiteMode() {
|
|
2347
|
+
// Hide sidebar button
|
|
2348
|
+
var sidebarBtn = document.getElementById('sidebarBtn');
|
|
2349
|
+
if (sidebarBtn) sidebarBtn.style.display = 'none';
|
|
2350
|
+
|
|
2351
|
+
// Close sidebar if open
|
|
2352
|
+
var sidebarEl = document.getElementById('sidebar');
|
|
2353
|
+
if (sidebarEl) sidebarEl.classList.remove('open');
|
|
2354
|
+
|
|
2355
|
+
// Hide entire annotation tools group (highlight, draw, eraser, select, undo/redo, text, shapes)
|
|
2356
|
+
var annotationGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(3)');
|
|
2357
|
+
if (annotationGroup) annotationGroup.style.display = 'none';
|
|
2358
|
+
|
|
2359
|
+
// In the zoom/utility group, hide everything except zoomIn and zoomOut
|
|
2360
|
+
var keepIds = new Set(['zoomIn', 'zoomOut']);
|
|
2361
|
+
var utilityGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(5)');
|
|
2362
|
+
if (utilityGroup) {
|
|
2363
|
+
Array.from(utilityGroup.children).forEach(function (child) {
|
|
2364
|
+
if (!keepIds.has(child.id)) {
|
|
2365
|
+
child.style.display = 'none';
|
|
2366
|
+
}
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// Hide bottom toolbar annotation tools (mobile), keep only fullscreen button
|
|
2371
|
+
var bottomToolbarInner = document.getElementById('bottomToolbarInner');
|
|
2372
|
+
if (bottomToolbarInner) bottomToolbarInner.style.display = 'none';
|
|
2373
|
+
|
|
2374
|
+
// Hide all top-level separators between groups
|
|
2375
|
+
document.querySelectorAll('#toolbar > .separator').forEach(function (sep) {
|
|
2376
|
+
sep.style.display = 'none';
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
console.log('[PDF-Secure] Lite mode applied - restricted toolbar');
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2343
2382
|
// Auto-load PDF if config is present (injected by NodeBB plugin)
|
|
2344
2383
|
async function autoLoadSecurePDF() {
|
|
2345
2384
|
if (!_cfg || !_cfg.filename) {
|
|
@@ -2420,7 +2459,7 @@
|
|
|
2420
2459
|
}
|
|
2421
2460
|
|
|
2422
2461
|
// Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
|
|
2423
|
-
if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
|
|
2462
|
+
if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
|
|
2424
2463
|
// Clone buffer for parent (we keep original)
|
|
2425
2464
|
const bufferCopy = pdfBuffer.slice(0);
|
|
2426
2465
|
window.parent.postMessage({
|
|
@@ -2436,8 +2475,8 @@
|
|
|
2436
2475
|
// Step 4: Load into viewer
|
|
2437
2476
|
await loadPDFFromBuffer(pdfBuffer);
|
|
2438
2477
|
|
|
2439
|
-
// Premium Gate: Client-side page restriction for non-premium users
|
|
2440
|
-
if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
|
|
2478
|
+
// Premium Gate: Client-side page restriction for non-premium, non-lite users
|
|
2479
|
+
if (config.isPremium === false && !config.isLite && pdfDoc && pdfDoc.numPages > 1) {
|
|
2441
2480
|
premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
|
|
2442
2481
|
applyPageLocks();
|
|
2443
2482
|
startPeriodicCheck();
|
|
@@ -2446,6 +2485,11 @@
|
|
|
2446
2485
|
premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
|
|
2447
2486
|
}
|
|
2448
2487
|
|
|
2488
|
+
// Lite Mode: Full PDF access but restricted toolbar
|
|
2489
|
+
if (config.isLite) {
|
|
2490
|
+
applyLiteMode();
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2449
2493
|
// Step 5: Moved to pagerendered event for proper timing
|
|
2450
2494
|
|
|
2451
2495
|
// Step 6: Security - clear references to prevent extraction
|
|
@@ -2657,16 +2701,25 @@
|
|
|
2657
2701
|
// Swipe-to-dismiss controller (declared before closeAllDropdowns which references it)
|
|
2658
2702
|
let swipeAbortController = null;
|
|
2659
2703
|
|
|
2704
|
+
// Track dropdown's original parent so we can return it after closing
|
|
2705
|
+
const dropdownOriginalParents = new Map();
|
|
2706
|
+
|
|
2660
2707
|
function closeAllDropdowns() {
|
|
2661
2708
|
// Clean up swipe listeners
|
|
2662
2709
|
if (swipeAbortController) { swipeAbortController.abort(); swipeAbortController = null; }
|
|
2663
|
-
// Reset inline
|
|
2710
|
+
// Reset inline styles and move dropdowns back to original parents
|
|
2664
2711
|
[highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown].forEach(dd => {
|
|
2665
2712
|
dd.style.transform = '';
|
|
2666
2713
|
dd.style.transition = '';
|
|
2667
2714
|
dd.style.position = '';
|
|
2668
2715
|
dd.style.top = '';
|
|
2669
2716
|
dd.style.left = '';
|
|
2717
|
+
// Return dropdown to its original parent if it was moved to body
|
|
2718
|
+
const origParent = dropdownOriginalParents.get(dd);
|
|
2719
|
+
if (origParent && dd.parentNode === document.body) {
|
|
2720
|
+
origParent.appendChild(dd);
|
|
2721
|
+
}
|
|
2722
|
+
dropdownOriginalParents.delete(dd);
|
|
2670
2723
|
});
|
|
2671
2724
|
highlightDropdown.classList.remove('visible');
|
|
2672
2725
|
drawDropdown.classList.remove('visible');
|
|
@@ -2687,13 +2740,16 @@
|
|
|
2687
2740
|
handle.className = 'bottomSheetHandle';
|
|
2688
2741
|
dropdown.insertBefore(handle, dropdown.firstChild);
|
|
2689
2742
|
}
|
|
2743
|
+
// Move dropdown to <body> to escape any parent stacking context / overflow clipping
|
|
2744
|
+
dropdownOriginalParents.set(dropdown, dropdown.parentNode);
|
|
2745
|
+
document.body.appendChild(dropdown);
|
|
2690
2746
|
dropdown.classList.add('visible');
|
|
2691
2747
|
if (useBottomSheet) {
|
|
2692
2748
|
// Show backdrop on mobile/tablet portrait
|
|
2693
2749
|
dropdownBackdrop.classList.add('visible');
|
|
2694
2750
|
setupBottomSheetSwipe(dropdown);
|
|
2695
2751
|
} else {
|
|
2696
|
-
// Desktop/tablet landscape:
|
|
2752
|
+
// Desktop/tablet landscape: position below the button
|
|
2697
2753
|
const wrapper = e.target.closest('.toolbarBtnWithDropdown');
|
|
2698
2754
|
if (wrapper) {
|
|
2699
2755
|
const rect = wrapper.getBoundingClientRect();
|
|
@@ -2761,27 +2817,45 @@
|
|
|
2761
2817
|
|
|
2762
2818
|
// Overflow menu toggle
|
|
2763
2819
|
document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
|
|
2764
|
-
overflowDropdown.
|
|
2820
|
+
overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
|
|
2821
|
+
overflowDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
2822
|
+
|
|
2823
|
+
// Overflow menu actions — use both click + pointerup for reliable tablet/touch support
|
|
2824
|
+
// pointerup fires immediately on touch release (no 300ms delay).
|
|
2825
|
+
// A guard flag prevents double-firing when both events reach the handler.
|
|
2826
|
+
function attachOverflowAction(id, action) {
|
|
2827
|
+
const el = document.getElementById(id);
|
|
2828
|
+
let fired = false;
|
|
2829
|
+
function run() {
|
|
2830
|
+
if (fired) return;
|
|
2831
|
+
fired = true;
|
|
2832
|
+
requestAnimationFrame(() => { fired = false; });
|
|
2833
|
+
action();
|
|
2834
|
+
}
|
|
2835
|
+
el.addEventListener('click', run);
|
|
2836
|
+
el.addEventListener('pointerup', (e) => {
|
|
2837
|
+
if (e.pointerType === 'touch' || e.pointerType === 'pen') run();
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2765
2840
|
|
|
2766
|
-
|
|
2767
|
-
document.getElementById('overflowRotateLeft').onclick = () => {
|
|
2841
|
+
attachOverflowAction('overflowRotateLeft', () => {
|
|
2768
2842
|
rotatePage(-90);
|
|
2769
2843
|
closeAllDropdowns();
|
|
2770
|
-
};
|
|
2771
|
-
|
|
2844
|
+
});
|
|
2845
|
+
attachOverflowAction('overflowRotateRight', () => {
|
|
2772
2846
|
rotatePage(90);
|
|
2773
2847
|
closeAllDropdowns();
|
|
2774
|
-
};
|
|
2775
|
-
|
|
2848
|
+
});
|
|
2849
|
+
attachOverflowAction('overflowSepia', () => {
|
|
2776
2850
|
document.getElementById('sepiaBtn').click();
|
|
2777
2851
|
document.getElementById('overflowSepia').classList.toggle('active',
|
|
2778
2852
|
document.getElementById('sepiaBtn').classList.contains('active'));
|
|
2779
2853
|
closeAllDropdowns();
|
|
2780
|
-
};
|
|
2781
|
-
|
|
2854
|
+
});
|
|
2855
|
+
attachOverflowAction('overflowFullscreen', () => {
|
|
2782
2856
|
toggleFullscreen();
|
|
2783
2857
|
closeAllDropdowns();
|
|
2784
|
-
};
|
|
2858
|
+
});
|
|
2785
2859
|
|
|
2786
2860
|
// Close dropdowns when clicking outside
|
|
2787
2861
|
document.addEventListener('click', (e) => {
|