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 ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.2.34",
3
+ "version": "1.2.36",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  --accent: #0078d4;
24
24
  --accent-hover: #1a86d9;
25
25
  --border-color: #404040;
26
- --toolbar-height: 34px;
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
- padding: 0 6px;
113
- gap: 1px;
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: 24px;
125
- height: 24px;
126
- min-width: 24px;
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: 4px;
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: 14px;
149
- height: 14px;
149
+ width: 15px;
150
+ height: 15px;
150
151
  fill: currentColor;
151
152
  }
152
153
 
153
154
  .separator {
154
155
  width: 1px;
155
- height: 16px;
156
+ height: 18px;
156
157
  background: var(--border-color);
157
- margin: 0 2px;
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: 24px;
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: 4px;
546
- margin-left: auto;
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
- annotationRotations.set(pageNum, pdfViewer.pagesRotation || 0);
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
- const key = `textHighlight_${pageNum}`;
4331
- localStorage.setItem(key, container.innerHTML);
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
- const key = `textHighlight_${pageNum}`;
4337
- const saved = localStorage.getItem(key);
4338
- if (saved) {
4339
- let container = pageDiv.querySelector('.textHighlightContainer');
4340
- if (!container) {
4341
- container = document.createElement('div');
4342
- container.className = 'textHighlightContainer';
4343
- container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
4344
- pageDiv.insertBefore(container, pageDiv.firstChild);
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
- container.innerHTML = saved;
4347
- }
4559
+ } catch (e) { /* localStorage unavailable */ }
4348
4560
  }
4349
4561
 
4350
4562
  function showHighlightPopup(x, y, pageDiv, rects) {