nodebb-plugin-pdf-secure2 1.2.35 → 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/package.json +1 -1
- package/static/viewer.html +247 -36
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,25 +109,26 @@
|
|
|
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
|
|
|
117
118
|
.toolbarGroup {
|
|
118
119
|
display: flex;
|
|
119
120
|
align-items: center;
|
|
120
|
-
gap:
|
|
121
|
+
gap: 0px;
|
|
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 */
|
|
@@ -416,8 +417,8 @@
|
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
.dropdownArrow {
|
|
419
|
-
width:
|
|
420
|
-
height:
|
|
420
|
+
width: 14px;
|
|
421
|
+
height: 26px;
|
|
421
422
|
border: none;
|
|
422
423
|
background: transparent;
|
|
423
424
|
color: var(--text-primary);
|
|
@@ -542,24 +543,25 @@
|
|
|
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 {
|
|
550
|
-
width:
|
|
551
|
-
height:
|
|
551
|
+
width: 28px;
|
|
552
|
+
height: 22px;
|
|
552
553
|
background: var(--bg-tertiary);
|
|
553
554
|
border: 1px solid var(--border-color);
|
|
554
|
-
border-radius:
|
|
555
|
+
border-radius: 3px;
|
|
555
556
|
color: var(--text-primary);
|
|
556
557
|
text-align: center;
|
|
557
|
-
font-size:
|
|
558
|
+
font-size: 11px;
|
|
559
|
+
padding: 0;
|
|
558
560
|
}
|
|
559
561
|
|
|
560
562
|
#pageCount {
|
|
561
563
|
color: var(--text-secondary);
|
|
562
|
-
font-size:
|
|
564
|
+
font-size: 11px;
|
|
563
565
|
}
|
|
564
566
|
|
|
565
567
|
/* Sidebar - Thumbnails */
|
|
@@ -2304,6 +2306,181 @@
|
|
|
2304
2306
|
const annotationsStore = new Map();
|
|
2305
2307
|
const annotationRotations = new Map(); // tracks rotation when annotations were saved
|
|
2306
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
|
+
|
|
2307
2484
|
// AbortControllers for annotation layer event listeners (cleanup on re-inject)
|
|
2308
2485
|
const annotationAbortControllers = new Map(); // pageNum -> AbortController
|
|
2309
2486
|
|
|
@@ -2651,6 +2828,7 @@
|
|
|
2651
2828
|
console.log('[PDF-Secure] No config found, showing file picker');
|
|
2652
2829
|
return;
|
|
2653
2830
|
}
|
|
2831
|
+
touchManifest();
|
|
2654
2832
|
|
|
2655
2833
|
const config = _cfg;
|
|
2656
2834
|
console.log('[PDF-Secure] Auto-loading:', config.filename);
|
|
@@ -3350,6 +3528,15 @@
|
|
|
3350
3528
|
|
|
3351
3529
|
|
|
3352
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
|
+
|
|
3353
3540
|
// Restore saved annotations for this page (with rotation transform if needed)
|
|
3354
3541
|
if (annotationsStore.has(pageNum)) {
|
|
3355
3542
|
const savedRot = annotationRotations.get(pageNum) || 0;
|
|
@@ -3538,10 +3725,13 @@
|
|
|
3538
3725
|
|
|
3539
3726
|
if (newState) {
|
|
3540
3727
|
annotationsStore.set(pageNum, newState);
|
|
3541
|
-
|
|
3728
|
+
var rot = pdfViewer.pagesRotation || 0;
|
|
3729
|
+
annotationRotations.set(pageNum, rot);
|
|
3730
|
+
debouncedPersist(pageNum, newState, rot);
|
|
3542
3731
|
} else {
|
|
3543
3732
|
annotationsStore.delete(pageNum);
|
|
3544
3733
|
annotationRotations.delete(pageNum);
|
|
3734
|
+
persistToStorage(pageNum, null, 0);
|
|
3545
3735
|
}
|
|
3546
3736
|
|
|
3547
3737
|
updateUndoRedoButtons();
|
|
@@ -3584,9 +3774,11 @@
|
|
|
3584
3774
|
if (previousHtml.trim()) {
|
|
3585
3775
|
annotationsStore.set(pageNum, svg.innerHTML);
|
|
3586
3776
|
annotationRotations.set(pageNum, curRot);
|
|
3777
|
+
persistToStorage(pageNum, svg.innerHTML, curRot);
|
|
3587
3778
|
} else {
|
|
3588
3779
|
annotationsStore.delete(pageNum);
|
|
3589
3780
|
annotationRotations.delete(pageNum);
|
|
3781
|
+
persistToStorage(pageNum, null, 0);
|
|
3590
3782
|
}
|
|
3591
3783
|
|
|
3592
3784
|
clearAnnotationSelection();
|
|
@@ -3620,9 +3812,11 @@
|
|
|
3620
3812
|
if (redoHtml.trim()) {
|
|
3621
3813
|
annotationsStore.set(pageNum, svg.innerHTML);
|
|
3622
3814
|
annotationRotations.set(pageNum, curRot);
|
|
3815
|
+
persistToStorage(pageNum, svg.innerHTML, curRot);
|
|
3623
3816
|
} else {
|
|
3624
3817
|
annotationsStore.delete(pageNum);
|
|
3625
3818
|
annotationRotations.delete(pageNum);
|
|
3819
|
+
persistToStorage(pageNum, null, 0);
|
|
3626
3820
|
}
|
|
3627
3821
|
|
|
3628
3822
|
clearAnnotationSelection();
|
|
@@ -3648,6 +3842,7 @@
|
|
|
3648
3842
|
svg.innerHTML = '';
|
|
3649
3843
|
annotationsStore.delete(pageNum);
|
|
3650
3844
|
annotationRotations.delete(pageNum);
|
|
3845
|
+
persistToStorage(pageNum, null, 0);
|
|
3651
3846
|
|
|
3652
3847
|
clearAnnotationSelection();
|
|
3653
3848
|
updateUndoRedoButtons();
|
|
@@ -4328,24 +4523,40 @@
|
|
|
4328
4523
|
function saveTextHighlights(pageNum, pageDiv) {
|
|
4329
4524
|
const container = pageDiv.querySelector('.textHighlightContainer');
|
|
4330
4525
|
if (container) {
|
|
4331
|
-
|
|
4332
|
-
|
|
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
|
+
}
|
|
4333
4543
|
}
|
|
4334
4544
|
}
|
|
4335
4545
|
|
|
4336
4546
|
function loadTextHighlights(pageNum, pageDiv) {
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
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);
|
|
4346
4558
|
}
|
|
4347
|
-
|
|
4348
|
-
}
|
|
4559
|
+
} catch (e) { /* localStorage unavailable */ }
|
|
4349
4560
|
}
|
|
4350
4561
|
|
|
4351
4562
|
function showHighlightPopup(x, y, pageDiv, rects) {
|