nodebb-plugin-pdf-secure2 1.2.34 → 1.2.36
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/image.png +0 -0
- package/package.json +1 -1
- package/static/viewer.html +241 -29
package/image.png
ADDED
|
Binary file
|
package/package.json
CHANGED
package/static/viewer.html
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
--accent: #0078d4;
|
|
24
24
|
--accent-hover: #1a86d9;
|
|
25
25
|
--border-color: #404040;
|
|
26
|
-
--toolbar-height:
|
|
26
|
+
--toolbar-height: 36px;
|
|
27
27
|
--sidebar-width: 200px;
|
|
28
28
|
--toolbar-height-mobile: 44px;
|
|
29
29
|
--bottom-bar-height: 52px;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
fill: var(--accent);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/* Toolbar - Edge Style */
|
|
101
|
+
/* Toolbar - Edge Style (compact for iframe embed) */
|
|
102
102
|
#toolbar {
|
|
103
103
|
position: fixed;
|
|
104
104
|
top: 0;
|
|
@@ -109,8 +109,9 @@
|
|
|
109
109
|
border-bottom: 1px solid var(--border-color);
|
|
110
110
|
display: flex;
|
|
111
111
|
align-items: center;
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
justify-content: flex-start;
|
|
113
|
+
padding: 0 4px;
|
|
114
|
+
gap: 0px;
|
|
114
115
|
z-index: 100;
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -121,13 +122,13 @@
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
.toolbarBtn {
|
|
124
|
-
width:
|
|
125
|
-
height:
|
|
126
|
-
min-width:
|
|
125
|
+
width: 26px;
|
|
126
|
+
height: 26px;
|
|
127
|
+
min-width: 26px;
|
|
127
128
|
border: none;
|
|
128
129
|
background: transparent;
|
|
129
130
|
color: var(--text-primary);
|
|
130
|
-
border-radius:
|
|
131
|
+
border-radius: 3px;
|
|
131
132
|
cursor: pointer;
|
|
132
133
|
display: flex;
|
|
133
134
|
align-items: center;
|
|
@@ -145,16 +146,16 @@
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
.toolbarBtn svg {
|
|
148
|
-
width:
|
|
149
|
-
height:
|
|
149
|
+
width: 15px;
|
|
150
|
+
height: 15px;
|
|
150
151
|
fill: currentColor;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.separator {
|
|
154
155
|
width: 1px;
|
|
155
|
-
height:
|
|
156
|
+
height: 18px;
|
|
156
157
|
background: var(--border-color);
|
|
157
|
-
margin: 0
|
|
158
|
+
margin: 0 3px;
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/* Enhanced Tooltips */
|
|
@@ -417,7 +418,7 @@
|
|
|
417
418
|
|
|
418
419
|
.dropdownArrow {
|
|
419
420
|
width: 14px;
|
|
420
|
-
height:
|
|
421
|
+
height: 26px;
|
|
421
422
|
border: none;
|
|
422
423
|
background: transparent;
|
|
423
424
|
color: var(--text-primary);
|
|
@@ -542,8 +543,8 @@
|
|
|
542
543
|
.pageInfo {
|
|
543
544
|
display: flex;
|
|
544
545
|
align-items: center;
|
|
545
|
-
gap:
|
|
546
|
-
margin-left:
|
|
546
|
+
gap: 3px;
|
|
547
|
+
margin-left: 3px;
|
|
547
548
|
}
|
|
548
549
|
|
|
549
550
|
#pageInput {
|
|
@@ -555,6 +556,7 @@
|
|
|
555
556
|
color: var(--text-primary);
|
|
556
557
|
text-align: center;
|
|
557
558
|
font-size: 11px;
|
|
559
|
+
padding: 0;
|
|
558
560
|
}
|
|
559
561
|
|
|
560
562
|
#pageCount {
|
|
@@ -1450,6 +1452,7 @@
|
|
|
1450
1452
|
|
|
1451
1453
|
body.viewer-fullscreen .pageInfo {
|
|
1452
1454
|
gap: 8px;
|
|
1455
|
+
margin-left: auto;
|
|
1453
1456
|
}
|
|
1454
1457
|
|
|
1455
1458
|
body.viewer-fullscreen #viewerContainer {
|
|
@@ -2303,6 +2306,181 @@
|
|
|
2303
2306
|
const annotationsStore = new Map();
|
|
2304
2307
|
const annotationRotations = new Map(); // tracks rotation when annotations were saved
|
|
2305
2308
|
|
|
2309
|
+
// localStorage persistence helpers
|
|
2310
|
+
var _storagePrefix = (function () {
|
|
2311
|
+
var filename = (_cfg && _cfg.filename) || 'local';
|
|
2312
|
+
// FNV-1a hash to avoid key collisions from filenames containing delimiters
|
|
2313
|
+
var hash = 0x811c9dc5;
|
|
2314
|
+
for (var i = 0; i < filename.length; i++) {
|
|
2315
|
+
hash ^= filename.charCodeAt(i);
|
|
2316
|
+
hash = (hash * 0x01000193) >>> 0;
|
|
2317
|
+
}
|
|
2318
|
+
return hash.toString(36);
|
|
2319
|
+
})();
|
|
2320
|
+
|
|
2321
|
+
function getStorageKey(pageNum) {
|
|
2322
|
+
return 'pdfA::' + _storagePrefix + '::p' + pageNum;
|
|
2323
|
+
}
|
|
2324
|
+
function getRotStorageKey(pageNum) {
|
|
2325
|
+
return 'pdfR::' + _storagePrefix + '::p' + pageNum;
|
|
2326
|
+
}
|
|
2327
|
+
function getTextHighlightKey(pageNum) {
|
|
2328
|
+
return 'pdfH::' + _storagePrefix + '::p' + pageNum;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// Touch manifest - tracks which files have annotations and when last accessed
|
|
2332
|
+
function touchManifest() {
|
|
2333
|
+
try {
|
|
2334
|
+
var manifest = JSON.parse(localStorage.getItem('pdfAnnot_manifest') || '{}');
|
|
2335
|
+
manifest[_storagePrefix] = Date.now();
|
|
2336
|
+
localStorage.setItem('pdfAnnot_manifest', JSON.stringify(manifest));
|
|
2337
|
+
} catch (e) { /* ignore */ }
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// Evict oldest file's annotations when quota is exceeded
|
|
2341
|
+
function evictOldest(excludePrefix) {
|
|
2342
|
+
try {
|
|
2343
|
+
var manifest = JSON.parse(localStorage.getItem('pdfAnnot_manifest') || '{}');
|
|
2344
|
+
var entries = Object.entries(manifest).sort(function (a, b) { return a[1] - b[1]; });
|
|
2345
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2346
|
+
var oldPrefix = entries[i][0];
|
|
2347
|
+
if (oldPrefix === excludePrefix) continue;
|
|
2348
|
+
// Remove all keys for this old file
|
|
2349
|
+
var keysToRemove = [];
|
|
2350
|
+
for (var j = 0; j < localStorage.length; j++) {
|
|
2351
|
+
var key = localStorage.key(j);
|
|
2352
|
+
if (key && key.indexOf('::' + oldPrefix + '::') !== -1) {
|
|
2353
|
+
keysToRemove.push(key);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
keysToRemove.forEach(function (k) { localStorage.removeItem(k); });
|
|
2357
|
+
delete manifest[oldPrefix];
|
|
2358
|
+
localStorage.setItem('pdfAnnot_manifest', JSON.stringify(manifest));
|
|
2359
|
+
return keysToRemove.length > 0;
|
|
2360
|
+
}
|
|
2361
|
+
return false;
|
|
2362
|
+
} catch (e) { return false; }
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// Debounced persistence - avoids blocking main thread on every stroke
|
|
2366
|
+
var _persistTimers = {};
|
|
2367
|
+
function debouncedPersist(pageNum, svgHtml, rotation) {
|
|
2368
|
+
if (_persistTimers[pageNum]) clearTimeout(_persistTimers[pageNum]);
|
|
2369
|
+
_persistTimers[pageNum] = setTimeout(function () {
|
|
2370
|
+
persistToStorage(pageNum, svgHtml, rotation);
|
|
2371
|
+
delete _persistTimers[pageNum];
|
|
2372
|
+
}, 400);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// Flush all pending debounced writes (called on page unload)
|
|
2376
|
+
function flushPendingPersist() {
|
|
2377
|
+
Object.keys(_persistTimers).forEach(function (pn) {
|
|
2378
|
+
clearTimeout(_persistTimers[pn]);
|
|
2379
|
+
var data = annotationsStore.get(parseInt(pn, 10));
|
|
2380
|
+
var rot = annotationRotations.get(parseInt(pn, 10)) || 0;
|
|
2381
|
+
if (data) persistToStorage(parseInt(pn, 10), data, rot);
|
|
2382
|
+
});
|
|
2383
|
+
_persistTimers = {};
|
|
2384
|
+
}
|
|
2385
|
+
window.addEventListener('beforeunload', flushPendingPersist);
|
|
2386
|
+
window.addEventListener('pagehide', flushPendingPersist);
|
|
2387
|
+
document.addEventListener('visibilitychange', function () {
|
|
2388
|
+
if (document.hidden) flushPendingPersist();
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
function persistToStorage(pageNum, svgHtml, rotation) {
|
|
2392
|
+
try {
|
|
2393
|
+
if (svgHtml) {
|
|
2394
|
+
localStorage.setItem(getStorageKey(pageNum), svgHtml);
|
|
2395
|
+
localStorage.setItem(getRotStorageKey(pageNum), String(rotation || 0));
|
|
2396
|
+
} else {
|
|
2397
|
+
localStorage.removeItem(getStorageKey(pageNum));
|
|
2398
|
+
localStorage.removeItem(getRotStorageKey(pageNum));
|
|
2399
|
+
}
|
|
2400
|
+
} catch (e) {
|
|
2401
|
+
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
|
2402
|
+
if (evictOldest(_storagePrefix)) {
|
|
2403
|
+
try {
|
|
2404
|
+
localStorage.setItem(getStorageKey(pageNum), svgHtml);
|
|
2405
|
+
localStorage.setItem(getRotStorageKey(pageNum), String(rotation || 0));
|
|
2406
|
+
return;
|
|
2407
|
+
} catch (e2) { /* still full */ }
|
|
2408
|
+
}
|
|
2409
|
+
if (typeof showToast === 'function') {
|
|
2410
|
+
showToast('Depolama alani dolu. Bazi eski notlar silindi.', 'warning');
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function loadFromStorage(pageNum) {
|
|
2416
|
+
try {
|
|
2417
|
+
var html = localStorage.getItem(getStorageKey(pageNum));
|
|
2418
|
+
var rot = parseInt(localStorage.getItem(getRotStorageKey(pageNum)) || '0', 10);
|
|
2419
|
+
return html ? { html: html, rotation: rot } : null;
|
|
2420
|
+
} catch (e) { return null; }
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// Sanitize SVG/HTML from localStorage to prevent XSS
|
|
2424
|
+
var _allowedSvgTags = ['path','rect','ellipse','line','circle','text','tspan','g','defs','marker','polyline','polygon'];
|
|
2425
|
+
var _allowedSvgAttrs = ['d','x','y','x1','y1','x2','y2','cx','cy','r','rx','ry','width','height','viewBox',
|
|
2426
|
+
'fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','stroke-dasharray','opacity',
|
|
2427
|
+
'transform','class','data-id','data-type','font-size','font-family','text-anchor','points',
|
|
2428
|
+
'marker-start','marker-end','marker-mid','id','refX','refY','markerWidth','markerHeight',
|
|
2429
|
+
'orient','markerUnits','style'];
|
|
2430
|
+
function sanitizeSvgHtml(html) {
|
|
2431
|
+
if (!html) return '';
|
|
2432
|
+
var temp = document.createElement('div');
|
|
2433
|
+
temp.innerHTML = html;
|
|
2434
|
+
// Remove all non-allowed elements
|
|
2435
|
+
temp.querySelectorAll('*').forEach(function (el) {
|
|
2436
|
+
var tag = el.tagName.toLowerCase();
|
|
2437
|
+
if (_allowedSvgTags.indexOf(tag) === -1) {
|
|
2438
|
+
el.remove();
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
// Remove non-allowed attributes
|
|
2442
|
+
var attrs = Array.from(el.attributes);
|
|
2443
|
+
attrs.forEach(function (attr) {
|
|
2444
|
+
if (_allowedSvgAttrs.indexOf(attr.name) === -1) {
|
|
2445
|
+
el.removeAttribute(attr.name);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
// Sanitize style attribute - only allow safe CSS properties
|
|
2449
|
+
if (el.hasAttribute('style')) {
|
|
2450
|
+
var style = el.getAttribute('style');
|
|
2451
|
+
if (/expression|javascript|url\s*\(/i.test(style)) {
|
|
2452
|
+
el.removeAttribute('style');
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
return temp.innerHTML;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Sanitize text highlight HTML from localStorage
|
|
2460
|
+
function sanitizeHighlightHtml(html) {
|
|
2461
|
+
if (!html) return '';
|
|
2462
|
+
var temp = document.createElement('div');
|
|
2463
|
+
temp.innerHTML = html;
|
|
2464
|
+
temp.querySelectorAll('*').forEach(function (el) {
|
|
2465
|
+
var tag = el.tagName.toLowerCase();
|
|
2466
|
+
if (tag !== 'div') { el.remove(); return; }
|
|
2467
|
+
// Only allow style and class attributes on divs
|
|
2468
|
+
var attrs = Array.from(el.attributes);
|
|
2469
|
+
attrs.forEach(function (attr) {
|
|
2470
|
+
if (attr.name !== 'style' && attr.name !== 'class') {
|
|
2471
|
+
el.removeAttribute(attr.name);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
if (el.hasAttribute('style')) {
|
|
2475
|
+
var style = el.getAttribute('style');
|
|
2476
|
+
if (/expression|javascript|url\s*\(/i.test(style)) {
|
|
2477
|
+
el.removeAttribute('style');
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
return temp.innerHTML;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2306
2484
|
// AbortControllers for annotation layer event listeners (cleanup on re-inject)
|
|
2307
2485
|
const annotationAbortControllers = new Map(); // pageNum -> AbortController
|
|
2308
2486
|
|
|
@@ -2650,6 +2828,7 @@
|
|
|
2650
2828
|
console.log('[PDF-Secure] No config found, showing file picker');
|
|
2651
2829
|
return;
|
|
2652
2830
|
}
|
|
2831
|
+
touchManifest();
|
|
2653
2832
|
|
|
2654
2833
|
const config = _cfg;
|
|
2655
2834
|
console.log('[PDF-Secure] Auto-loading:', config.filename);
|
|
@@ -3349,6 +3528,15 @@
|
|
|
3349
3528
|
|
|
3350
3529
|
|
|
3351
3530
|
|
|
3531
|
+
// Restore from localStorage if not already in memory
|
|
3532
|
+
if (!annotationsStore.has(pageNum)) {
|
|
3533
|
+
var stored = loadFromStorage(pageNum);
|
|
3534
|
+
if (stored) {
|
|
3535
|
+
annotationsStore.set(pageNum, sanitizeSvgHtml(stored.html));
|
|
3536
|
+
annotationRotations.set(pageNum, stored.rotation);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3352
3540
|
// Restore saved annotations for this page (with rotation transform if needed)
|
|
3353
3541
|
if (annotationsStore.has(pageNum)) {
|
|
3354
3542
|
const savedRot = annotationRotations.get(pageNum) || 0;
|
|
@@ -3537,10 +3725,13 @@
|
|
|
3537
3725
|
|
|
3538
3726
|
if (newState) {
|
|
3539
3727
|
annotationsStore.set(pageNum, newState);
|
|
3540
|
-
|
|
3728
|
+
var rot = pdfViewer.pagesRotation || 0;
|
|
3729
|
+
annotationRotations.set(pageNum, rot);
|
|
3730
|
+
debouncedPersist(pageNum, newState, rot);
|
|
3541
3731
|
} else {
|
|
3542
3732
|
annotationsStore.delete(pageNum);
|
|
3543
3733
|
annotationRotations.delete(pageNum);
|
|
3734
|
+
persistToStorage(pageNum, null, 0);
|
|
3544
3735
|
}
|
|
3545
3736
|
|
|
3546
3737
|
updateUndoRedoButtons();
|
|
@@ -3583,9 +3774,11 @@
|
|
|
3583
3774
|
if (previousHtml.trim()) {
|
|
3584
3775
|
annotationsStore.set(pageNum, svg.innerHTML);
|
|
3585
3776
|
annotationRotations.set(pageNum, curRot);
|
|
3777
|
+
persistToStorage(pageNum, svg.innerHTML, curRot);
|
|
3586
3778
|
} else {
|
|
3587
3779
|
annotationsStore.delete(pageNum);
|
|
3588
3780
|
annotationRotations.delete(pageNum);
|
|
3781
|
+
persistToStorage(pageNum, null, 0);
|
|
3589
3782
|
}
|
|
3590
3783
|
|
|
3591
3784
|
clearAnnotationSelection();
|
|
@@ -3619,9 +3812,11 @@
|
|
|
3619
3812
|
if (redoHtml.trim()) {
|
|
3620
3813
|
annotationsStore.set(pageNum, svg.innerHTML);
|
|
3621
3814
|
annotationRotations.set(pageNum, curRot);
|
|
3815
|
+
persistToStorage(pageNum, svg.innerHTML, curRot);
|
|
3622
3816
|
} else {
|
|
3623
3817
|
annotationsStore.delete(pageNum);
|
|
3624
3818
|
annotationRotations.delete(pageNum);
|
|
3819
|
+
persistToStorage(pageNum, null, 0);
|
|
3625
3820
|
}
|
|
3626
3821
|
|
|
3627
3822
|
clearAnnotationSelection();
|
|
@@ -3647,6 +3842,7 @@
|
|
|
3647
3842
|
svg.innerHTML = '';
|
|
3648
3843
|
annotationsStore.delete(pageNum);
|
|
3649
3844
|
annotationRotations.delete(pageNum);
|
|
3845
|
+
persistToStorage(pageNum, null, 0);
|
|
3650
3846
|
|
|
3651
3847
|
clearAnnotationSelection();
|
|
3652
3848
|
updateUndoRedoButtons();
|
|
@@ -4327,24 +4523,40 @@
|
|
|
4327
4523
|
function saveTextHighlights(pageNum, pageDiv) {
|
|
4328
4524
|
const container = pageDiv.querySelector('.textHighlightContainer');
|
|
4329
4525
|
if (container) {
|
|
4330
|
-
|
|
4331
|
-
|
|
4526
|
+
try {
|
|
4527
|
+
localStorage.setItem(getTextHighlightKey(pageNum), container.innerHTML);
|
|
4528
|
+
touchManifest();
|
|
4529
|
+
} catch (e) {
|
|
4530
|
+
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
|
4531
|
+
if (evictOldest(_storagePrefix)) {
|
|
4532
|
+
try {
|
|
4533
|
+
localStorage.setItem(getTextHighlightKey(pageNum), container.innerHTML);
|
|
4534
|
+
touchManifest();
|
|
4535
|
+
return;
|
|
4536
|
+
} catch (e2) { /* still full */ }
|
|
4537
|
+
}
|
|
4538
|
+
if (typeof showToast === 'function') {
|
|
4539
|
+
showToast('Depolama alani dolu. Bazi eski notlar silindi.', 'warning');
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4332
4543
|
}
|
|
4333
4544
|
}
|
|
4334
4545
|
|
|
4335
4546
|
function loadTextHighlights(pageNum, pageDiv) {
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4547
|
+
try {
|
|
4548
|
+
const saved = localStorage.getItem(getTextHighlightKey(pageNum));
|
|
4549
|
+
if (saved) {
|
|
4550
|
+
let container = pageDiv.querySelector('.textHighlightContainer');
|
|
4551
|
+
if (!container) {
|
|
4552
|
+
container = document.createElement('div');
|
|
4553
|
+
container.className = 'textHighlightContainer';
|
|
4554
|
+
container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
|
|
4555
|
+
pageDiv.insertBefore(container, pageDiv.firstChild);
|
|
4556
|
+
}
|
|
4557
|
+
container.innerHTML = sanitizeHighlightHtml(saved);
|
|
4345
4558
|
}
|
|
4346
|
-
|
|
4347
|
-
}
|
|
4559
|
+
} catch (e) { /* localStorage unavailable */ }
|
|
4348
4560
|
}
|
|
4349
4561
|
|
|
4350
4562
|
function showHighlightPopup(x, y, pageDiv, rects) {
|