nodebb-plugin-pdf-secure 1.2.11 → 1.2.12
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 -4
- package/library.js +5 -5
- package/package.json +1 -1
- package/static/viewer.html +92 -38
package/lib/controllers.js
CHANGED
|
@@ -37,11 +37,9 @@ Controllers.servePdfBinary = async function (req, res) {
|
|
|
37
37
|
return res.status(400).json({ error: 'Missing nonce' });
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
return res.status(401).json({ error: 'Not authenticated' });
|
|
42
|
-
}
|
|
40
|
+
const uid = req.uid || 0; // Guest uid = 0
|
|
43
41
|
|
|
44
|
-
const data = nonceStore.validate(nonce,
|
|
42
|
+
const data = nonceStore.validate(nonce, uid);
|
|
45
43
|
if (!data) {
|
|
46
44
|
return res.status(403).json({ error: 'Invalid or expired nonce' });
|
|
47
45
|
}
|
package/library.js
CHANGED
|
@@ -54,14 +54,14 @@ plugin.init = async (params) => {
|
|
|
54
54
|
next();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
// PDF binary endpoint (nonce-validated)
|
|
58
|
-
router.get('/api/v3/plugins/pdf-secure/pdf-data',
|
|
57
|
+
// PDF binary endpoint (nonce-validated, guests allowed)
|
|
58
|
+
router.get('/api/v3/plugins/pdf-secure/pdf-data', controllers.servePdfBinary);
|
|
59
59
|
|
|
60
60
|
// Admin page route
|
|
61
61
|
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
|
|
62
62
|
|
|
63
|
-
// Viewer page route (fullscreen Mozilla PDF.js viewer)
|
|
64
|
-
router.get('/plugins/pdf-secure/viewer',
|
|
63
|
+
// Viewer page route (fullscreen Mozilla PDF.js viewer, guests allowed)
|
|
64
|
+
router.get('/plugins/pdf-secure/viewer', (req, res) => {
|
|
65
65
|
const { file } = req.query;
|
|
66
66
|
if (!file) {
|
|
67
67
|
return res.status(400).send('Missing file parameter');
|
|
@@ -81,7 +81,7 @@ plugin.init = async (params) => {
|
|
|
81
81
|
// Generate nonce + key HERE (in viewer route)
|
|
82
82
|
// This way the key is ONLY embedded in HTML, never in a separate API response
|
|
83
83
|
const isPremium = true;
|
|
84
|
-
const nonceData = nonceStore.generate(req.uid, safeName, isPremium);
|
|
84
|
+
const nonceData = nonceStore.generate(req.uid || 0, safeName, isPremium);
|
|
85
85
|
|
|
86
86
|
// Serve the viewer template with comprehensive security headers
|
|
87
87
|
res.set({
|
package/package.json
CHANGED
package/static/viewer.html
CHANGED
|
@@ -1287,10 +1287,12 @@
|
|
|
1287
1287
|
display: block !important;
|
|
1288
1288
|
min-width: unset;
|
|
1289
1289
|
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
|
|
1290
|
+
pointer-events: none;
|
|
1290
1291
|
}
|
|
1291
1292
|
|
|
1292
1293
|
.toolDropdown.visible {
|
|
1293
1294
|
transform: translateY(0);
|
|
1295
|
+
pointer-events: auto;
|
|
1294
1296
|
}
|
|
1295
1297
|
|
|
1296
1298
|
/* Responsive dropzone */
|
|
@@ -1454,10 +1456,12 @@
|
|
|
1454
1456
|
display: block !important;
|
|
1455
1457
|
min-width: unset;
|
|
1456
1458
|
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
|
|
1459
|
+
pointer-events: none;
|
|
1457
1460
|
}
|
|
1458
1461
|
|
|
1459
1462
|
.toolDropdown.visible {
|
|
1460
1463
|
transform: translateY(0);
|
|
1464
|
+
pointer-events: auto;
|
|
1461
1465
|
}
|
|
1462
1466
|
|
|
1463
1467
|
/* Selection toolbar & toast above bottom bar */
|
|
@@ -2267,7 +2271,7 @@
|
|
|
2267
2271
|
});
|
|
2268
2272
|
|
|
2269
2273
|
eventBus.on('pagerendered', (evt) => {
|
|
2270
|
-
|
|
2274
|
+
injectAnnotationLayer(evt.pageNumber);
|
|
2271
2275
|
|
|
2272
2276
|
// Rotation is handled natively by PDF.js via pagesRotation
|
|
2273
2277
|
});
|
|
@@ -2350,7 +2354,17 @@
|
|
|
2350
2354
|
const dropdownBackdrop = document.getElementById('dropdownBackdrop');
|
|
2351
2355
|
const overflowDropdown = document.getElementById('overflowDropdown');
|
|
2352
2356
|
|
|
2357
|
+
// Swipe-to-dismiss controller (declared before closeAllDropdowns which references it)
|
|
2358
|
+
let swipeAbortController = null;
|
|
2359
|
+
|
|
2353
2360
|
function closeAllDropdowns() {
|
|
2361
|
+
// Clean up swipe listeners
|
|
2362
|
+
if (swipeAbortController) { swipeAbortController.abort(); swipeAbortController = null; }
|
|
2363
|
+
// Reset inline transform from swipe gesture
|
|
2364
|
+
[highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown].forEach(dd => {
|
|
2365
|
+
dd.style.transform = '';
|
|
2366
|
+
dd.style.transition = '';
|
|
2367
|
+
});
|
|
2354
2368
|
highlightDropdown.classList.remove('visible');
|
|
2355
2369
|
drawDropdown.classList.remove('visible');
|
|
2356
2370
|
shapesDropdown.classList.remove('visible');
|
|
@@ -2374,10 +2388,52 @@
|
|
|
2374
2388
|
// Show backdrop on mobile/tablet portrait
|
|
2375
2389
|
if (useBottomSheet) {
|
|
2376
2390
|
dropdownBackdrop.classList.add('visible');
|
|
2391
|
+
setupBottomSheetSwipe(dropdown);
|
|
2377
2392
|
}
|
|
2378
2393
|
}
|
|
2379
2394
|
}
|
|
2380
2395
|
|
|
2396
|
+
// Bottom sheet swipe-to-dismiss (uses AbortController to prevent listener accumulation)
|
|
2397
|
+
function setupBottomSheetSwipe(dropdown) {
|
|
2398
|
+
// Abort previous swipe listeners if any
|
|
2399
|
+
if (swipeAbortController) swipeAbortController.abort();
|
|
2400
|
+
swipeAbortController = new AbortController();
|
|
2401
|
+
const signal = swipeAbortController.signal;
|
|
2402
|
+
|
|
2403
|
+
let startY = 0, currentY = 0, isDragging = false;
|
|
2404
|
+
|
|
2405
|
+
dropdown.addEventListener('touchstart', (e) => {
|
|
2406
|
+
const rect = dropdown.getBoundingClientRect();
|
|
2407
|
+
const touchY = e.touches[0].clientY;
|
|
2408
|
+
if (touchY - rect.top > 40 && dropdown.scrollTop > 0) return;
|
|
2409
|
+
startY = touchY;
|
|
2410
|
+
currentY = startY;
|
|
2411
|
+
isDragging = true;
|
|
2412
|
+
dropdown.style.transition = 'none';
|
|
2413
|
+
}, { signal });
|
|
2414
|
+
|
|
2415
|
+
dropdown.addEventListener('touchmove', (e) => {
|
|
2416
|
+
if (!isDragging) return;
|
|
2417
|
+
currentY = e.touches[0].clientY;
|
|
2418
|
+
const dy = currentY - startY;
|
|
2419
|
+
if (dy > 0) {
|
|
2420
|
+
dropdown.style.transform = `translateY(${dy}px)`;
|
|
2421
|
+
e.preventDefault();
|
|
2422
|
+
}
|
|
2423
|
+
}, { passive: false, signal });
|
|
2424
|
+
|
|
2425
|
+
dropdown.addEventListener('touchend', () => {
|
|
2426
|
+
if (!isDragging) return;
|
|
2427
|
+
isDragging = false;
|
|
2428
|
+
const dy = currentY - startY;
|
|
2429
|
+
dropdown.style.transition = '';
|
|
2430
|
+
if (dy > 80) {
|
|
2431
|
+
closeAllDropdowns();
|
|
2432
|
+
}
|
|
2433
|
+
dropdown.style.transform = '';
|
|
2434
|
+
}, { signal });
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2381
2437
|
// Backdrop click closes dropdowns
|
|
2382
2438
|
dropdownBackdrop.addEventListener('click', () => {
|
|
2383
2439
|
closeAllDropdowns();
|
|
@@ -2881,7 +2937,7 @@
|
|
|
2881
2937
|
const scaleY = vb.scaleY;
|
|
2882
2938
|
|
|
2883
2939
|
if (currentTool === 'eraser') {
|
|
2884
|
-
eraseAt(svg, x, y, scaleX);
|
|
2940
|
+
eraseAt(svg, x, y, scaleX, coords.clientX, coords.clientY);
|
|
2885
2941
|
// Performance: Don't save here - isDrawing is true, stopDraw will save on mouseup
|
|
2886
2942
|
return;
|
|
2887
2943
|
}
|
|
@@ -2982,7 +3038,7 @@
|
|
|
2982
3038
|
const scaleX = vb.scaleX;
|
|
2983
3039
|
|
|
2984
3040
|
if (currentTool === 'eraser') {
|
|
2985
|
-
eraseAt(svg, x, y, scaleX);
|
|
3041
|
+
eraseAt(svg, x, y, scaleX, coords.clientX, coords.clientY);
|
|
2986
3042
|
// Performance: Don't save on every mousemove - let mouseup handle it
|
|
2987
3043
|
return;
|
|
2988
3044
|
}
|
|
@@ -3390,13 +3446,12 @@
|
|
|
3390
3446
|
});
|
|
3391
3447
|
}
|
|
3392
3448
|
|
|
3393
|
-
function eraseAt(svg, x, y, scale = 1) {
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
if (
|
|
3399
|
-
y >= bbox.y - hitRadius && y <= bbox.y + bbox.height + hitRadius) {
|
|
3449
|
+
function eraseAt(svg, x, y, scale = 1, clientX, clientY) {
|
|
3450
|
+
// Use elementsFromPoint for rotation-aware hit testing
|
|
3451
|
+
const annotationTags = new Set(['path', 'text', 'rect', 'ellipse', 'line']);
|
|
3452
|
+
const elements = document.elementsFromPoint(clientX, clientY);
|
|
3453
|
+
elements.forEach(el => {
|
|
3454
|
+
if (el.closest('.annotationLayer') === svg && annotationTags.has(el.tagName)) {
|
|
3400
3455
|
el.remove();
|
|
3401
3456
|
}
|
|
3402
3457
|
});
|
|
@@ -4322,12 +4377,16 @@
|
|
|
4322
4377
|
// ERGONOMIC FEATURES
|
|
4323
4378
|
// ==========================================
|
|
4324
4379
|
|
|
4325
|
-
// Fullscreen toggle function
|
|
4380
|
+
// Fullscreen toggle function (with webkit fallback for iOS Safari)
|
|
4326
4381
|
function toggleFullscreen() {
|
|
4327
|
-
|
|
4328
|
-
|
|
4382
|
+
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
|
|
4383
|
+
if (fsEl) {
|
|
4384
|
+
if (document.exitFullscreen) document.exitFullscreen();
|
|
4385
|
+
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
|
4329
4386
|
} else {
|
|
4330
|
-
document.documentElement
|
|
4387
|
+
const el = document.documentElement;
|
|
4388
|
+
if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
|
|
4389
|
+
else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
|
|
4331
4390
|
}
|
|
4332
4391
|
}
|
|
4333
4392
|
|
|
@@ -4335,7 +4394,8 @@
|
|
|
4335
4394
|
function updateFullscreenIcon() {
|
|
4336
4395
|
const icon = document.getElementById('fullscreenIcon');
|
|
4337
4396
|
const btn = document.getElementById('fullscreenBtn');
|
|
4338
|
-
|
|
4397
|
+
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
|
|
4398
|
+
if (fsEl) {
|
|
4339
4399
|
icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
|
|
4340
4400
|
btn.classList.add('active');
|
|
4341
4401
|
} else {
|
|
@@ -4345,31 +4405,12 @@
|
|
|
4345
4405
|
}
|
|
4346
4406
|
|
|
4347
4407
|
document.addEventListener('fullscreenchange', updateFullscreenIcon);
|
|
4408
|
+
document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
|
|
4348
4409
|
|
|
4349
4410
|
// Fullscreen button click
|
|
4350
4411
|
document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
|
|
4351
4412
|
|
|
4352
|
-
// Double-click on page for fullscreen
|
|
4353
|
-
let lastClickTime = 0;
|
|
4354
|
-
container.addEventListener('click', (e) => {
|
|
4355
|
-
const now = Date.now();
|
|
4356
|
-
if (now - lastClickTime < 300) {
|
|
4357
|
-
toggleFullscreen();
|
|
4358
|
-
}
|
|
4359
|
-
lastClickTime = now;
|
|
4360
|
-
});
|
|
4361
4413
|
|
|
4362
|
-
// Auto-fullscreen when viewer loads inside iframe
|
|
4363
|
-
if (window.self !== window.top && window.PDF_SECURE_CONFIG) {
|
|
4364
|
-
// We're inside an iframe - request fullscreen on first user interaction
|
|
4365
|
-
const autoFullscreen = () => {
|
|
4366
|
-
document.documentElement.requestFullscreen().catch(() => { });
|
|
4367
|
-
container.removeEventListener('click', autoFullscreen);
|
|
4368
|
-
container.removeEventListener('touchstart', autoFullscreen);
|
|
4369
|
-
};
|
|
4370
|
-
container.addEventListener('click', autoFullscreen, { once: true });
|
|
4371
|
-
container.addEventListener('touchstart', autoFullscreen, { once: true });
|
|
4372
|
-
}
|
|
4373
4414
|
|
|
4374
4415
|
// Mouse wheel zoom with Ctrl
|
|
4375
4416
|
container.addEventListener('wheel', (e) => {
|
|
@@ -4453,9 +4494,11 @@
|
|
|
4453
4494
|
setupResponsiveToolbar();
|
|
4454
4495
|
});
|
|
4455
4496
|
|
|
4456
|
-
// Also handle resize for orientation changes
|
|
4497
|
+
// Also handle resize for orientation changes (debounced)
|
|
4498
|
+
let resizeTimer;
|
|
4457
4499
|
window.addEventListener('resize', () => {
|
|
4458
|
-
|
|
4500
|
+
clearTimeout(resizeTimer);
|
|
4501
|
+
resizeTimer = setTimeout(setupResponsiveToolbar, 150);
|
|
4459
4502
|
});
|
|
4460
4503
|
|
|
4461
4504
|
// ==========================================
|
|
@@ -4472,7 +4515,18 @@
|
|
|
4472
4515
|
}
|
|
4473
4516
|
|
|
4474
4517
|
container.addEventListener('touchstart', (e) => {
|
|
4475
|
-
if (e.touches.length === 2
|
|
4518
|
+
if (e.touches.length === 2) {
|
|
4519
|
+
// Cancel any active drawing and clean up
|
|
4520
|
+
if (isDrawing && currentDrawingPage) {
|
|
4521
|
+
// Remove incomplete path
|
|
4522
|
+
if (currentPath && currentPath.parentNode) currentPath.remove();
|
|
4523
|
+
currentPath = null;
|
|
4524
|
+
pathSegments = [];
|
|
4525
|
+
if (drawRAF) { cancelAnimationFrame(drawRAF); drawRAF = null; }
|
|
4526
|
+
isDrawing = false;
|
|
4527
|
+
currentSvg = null;
|
|
4528
|
+
currentDrawingPage = null;
|
|
4529
|
+
}
|
|
4476
4530
|
isPinching = true;
|
|
4477
4531
|
pinchStartDistance = getTouchDistance(e.touches);
|
|
4478
4532
|
pinchStartScale = pdfViewer.currentScale;
|