nodebb-plugin-pdf-secure 1.2.18 → 1.2.20

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.
@@ -2,6 +2,10 @@
2
2
  (function () {
3
3
  'use strict';
4
4
 
5
+ // Security: Capture config early and delete from window immediately
6
+ const _cfg = window.PDF_SECURE_CONFIG ? Object.assign({}, window.PDF_SECURE_CONFIG) : null;
7
+ delete window.PDF_SECURE_CONFIG;
8
+
5
9
  // ============================================
6
10
  // CANVAS EXPORT PROTECTION
7
11
  // Block toDataURL/toBlob for PDF render canvas only
@@ -45,6 +49,9 @@
45
49
  let pathSegments = [];
46
50
  let drawRAF = null;
47
51
 
52
+ // Premium info (saved before config deletion for UI use)
53
+ let premiumInfo = null;
54
+
48
55
  // Annotation persistence - stores SVG innerHTML per page
49
56
  const annotationsStore = new Map();
50
57
  const annotationRotations = new Map(); // tracks rotation when annotations were saved
@@ -91,8 +98,7 @@
91
98
  firstPageRendered = true;
92
99
  // Notify parent that PDF is fully rendered (for queue system)
93
100
  if (window.parent && window.parent !== window) {
94
- const config = window.PDF_SECURE_CONFIG || {};
95
- window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, window.location.origin);
101
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: (_cfg || {}).filename }, window.location.origin);
96
102
  console.log('[PDF-Secure] First page rendered, notifying parent');
97
103
  }
98
104
  }
@@ -165,14 +171,106 @@
165
171
  return data.buffer;
166
172
  }
167
173
 
174
+ function showPremiumLockOverlay(totalPages) {
175
+ const viewerEl = document.getElementById('viewer');
176
+ if (!viewerEl) return;
177
+
178
+ const overlay = document.createElement('div');
179
+ overlay.id = 'premiumLockOverlay';
180
+ overlay.innerHTML = `
181
+ <div class="premium-lock-icon">
182
+ <svg viewBox="0 0 24 24" width="64" height="64" fill="#ffd700">
183
+ <path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/>
184
+ </svg>
185
+ </div>
186
+ <div class="premium-lock-pages">
187
+ ${totalPages - 1} sayfa daha kilitli
188
+ </div>
189
+ <div class="premium-lock-message">
190
+ Bu icerigi goruntulemeye devam etmek icin Premium uyelik gereklidir.
191
+ </div>
192
+ <a href="https://forumtest.ieu.app/premium" target="_blank" class="premium-lock-button">
193
+ Premium Satin Al
194
+ </a>
195
+ <div class="premium-lock-secondary">
196
+ Materyal yukleyerek de Premium olabilirsiniz!
197
+ </div>
198
+ `;
199
+
200
+ viewerEl.appendChild(overlay);
201
+ }
202
+
203
+ // ============================================
204
+ // PREMIUM INTEGRITY: Periodic Check (2s interval)
205
+ // Hides pages 2+, recreates overlay if removed, forces page 1
206
+ // ============================================
207
+ function startPeriodicCheck() {
208
+ setInterval(function () {
209
+ if (!premiumInfo || premiumInfo.isPremium) return;
210
+ // Hide all pages beyond page 1
211
+ var pages = document.querySelectorAll('#viewer .page');
212
+ pages.forEach(function (page, idx) {
213
+ if (idx > 0 && page.style.display !== 'none') {
214
+ page.style.display = 'none';
215
+ }
216
+ });
217
+ // Ensure overlay exists
218
+ if (!document.getElementById('premiumLockOverlay')) {
219
+ showPremiumLockOverlay(premiumInfo.totalPages);
220
+ }
221
+ // Force page 1 if somehow on another page
222
+ if (pdfViewer && pdfViewer.currentPageNumber > 1) {
223
+ pdfViewer.currentPageNumber = 1;
224
+ }
225
+ }, 2000);
226
+ }
227
+
228
+ // ============================================
229
+ // PREMIUM INTEGRITY: MutationObserver Anti-Tampering
230
+ // Recreates overlay on removal, re-hides pages on style change
231
+ // ============================================
232
+ function setupAntiTampering() {
233
+ var viewerEl = document.getElementById('viewer');
234
+ if (!viewerEl) return;
235
+
236
+ // Observer 1: Watch for overlay removal (childList)
237
+ new MutationObserver(function (mutations) {
238
+ for (var i = 0; i < mutations.length; i++) {
239
+ var removed = mutations[i].removedNodes;
240
+ for (var j = 0; j < removed.length; j++) {
241
+ if (removed[j].id === 'premiumLockOverlay') {
242
+ showPremiumLockOverlay(premiumInfo.totalPages);
243
+ return;
244
+ }
245
+ }
246
+ }
247
+ }).observe(viewerEl, { childList: true });
248
+
249
+ // Observer 2: Watch for page visibility tampering (attributes)
250
+ new MutationObserver(function (mutations) {
251
+ for (var i = 0; i < mutations.length; i++) {
252
+ var m = mutations[i];
253
+ if (m.type === 'attributes' && m.attributeName === 'style') {
254
+ var target = m.target;
255
+ if (target.classList && target.classList.contains('page')) {
256
+ var pageNum = parseInt(target.dataset.pageNumber || '0', 10);
257
+ if (pageNum > 1 && target.style.display !== 'none') {
258
+ target.style.display = 'none';
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }).observe(viewerEl, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
264
+ }
265
+
168
266
  // Auto-load PDF if config is present (injected by NodeBB plugin)
169
267
  async function autoLoadSecurePDF() {
170
- if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
268
+ if (!_cfg || !_cfg.filename) {
171
269
  console.log('[PDF-Secure] No config found, showing file picker');
172
270
  return;
173
271
  }
174
272
 
175
- const config = window.PDF_SECURE_CONFIG;
273
+ const config = _cfg;
176
274
  console.log('[PDF-Secure] Auto-loading:', config.filename);
177
275
 
178
276
  // Show loading state
@@ -244,8 +342,8 @@
244
342
  pdfBuffer = encodedBuffer;
245
343
  }
246
344
 
247
- // Send buffer to parent for caching
248
- if (window.parent && window.parent !== window) {
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) {
249
347
  // Clone buffer for parent (we keep original)
250
348
  const bufferCopy = pdfBuffer.slice(0);
251
349
  window.parent.postMessage({
@@ -261,14 +359,21 @@
261
359
  // Step 4: Load into viewer
262
360
  await loadPDFFromBuffer(pdfBuffer);
263
361
 
362
+ // Premium Gate: Client-side page restriction for non-premium users
363
+ if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
364
+ premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
365
+ showPremiumLockOverlay(pdfDoc.numPages);
366
+ startPeriodicCheck();
367
+ setupAntiTampering();
368
+ } else {
369
+ premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
370
+ }
371
+
264
372
  // Step 5: Moved to pagerendered event for proper timing
265
373
 
266
374
  // Step 6: Security - clear references to prevent extraction
267
375
  pdfBuffer = null;
268
376
 
269
- // Security: Delete config containing sensitive data (nonce, key)
270
- delete window.PDF_SECURE_CONFIG;
271
-
272
377
  // Security: Remove PDF.js globals to prevent console manipulation
273
378
  delete window.pdfjsLib;
274
379
  delete window.pdfjsViewer;
@@ -292,10 +397,9 @@
292
397
 
293
398
  // Notify parent of error (prevents 60s queue hang)
294
399
  if (window.parent && window.parent !== window) {
295
- const config = window.PDF_SECURE_CONFIG || {};
296
400
  window.parent.postMessage({
297
401
  type: 'pdf-secure-ready',
298
- filename: config.filename,
402
+ filename: (_cfg || {}).filename,
299
403
  error: err.message
300
404
  }, window.location.origin);
301
405
  }
@@ -325,10 +429,16 @@
325
429
  // Create placeholder thumbnails for all pages
326
430
  for (let i = 1; i <= pdfDoc.numPages; i++) {
327
431
  const thumb = document.createElement('div');
328
- thumb.className = 'thumbnail' + (i === 1 ? ' active' : '');
432
+ const isLocked = premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1 && i > 1;
433
+ thumb.className = 'thumbnail' + (i === 1 ? ' active' : '') + (isLocked ? ' locked' : '');
329
434
  thumb.dataset.page = i;
330
- thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
435
+ if (isLocked) {
436
+ thumb.innerHTML = `<div class="thumbnailNum">${i}</div><div class="thumbnail-lock">&#128274;</div>`;
437
+ } else {
438
+ thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
439
+ }
331
440
  thumb.onclick = () => {
441
+ if (isLocked) return;
332
442
  pdfViewer.currentPageNumber = i;
333
443
  document.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
334
444
  thumb.classList.add('active');
@@ -339,7 +449,7 @@
339
449
  // Lazy render thumbnails with IntersectionObserver
340
450
  const thumbObserver = new IntersectionObserver((entries) => {
341
451
  entries.forEach(async (entry) => {
342
- if (entry.isIntersecting && !entry.target.dataset.rendered) {
452
+ if (entry.isIntersecting && !entry.target.dataset.rendered && !entry.target.classList.contains('locked')) {
343
453
  entry.target.dataset.rendered = 'true';
344
454
  const pageNum = parseInt(entry.target.dataset.page);
345
455
  const page = await pdfDoc.getPage(pageNum);
@@ -359,10 +469,31 @@
359
469
  // Events
360
470
  eventBus.on('pagesinit', () => {
361
471
  pdfViewer.currentScaleValue = 'page-width';
362
- document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
472
+
473
+ // Premium Gate: Hide all pages beyond page 1 immediately
474
+ if (premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1) {
475
+ for (let i = 1; i < pdfViewer.pagesCount; i++) {
476
+ const pageView = pdfViewer.getPageView(i); // 0-indexed
477
+ if (pageView && pageView.div) {
478
+ pageView.div.style.display = 'none';
479
+ }
480
+ }
481
+ }
482
+
483
+ if (premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1) {
484
+ document.getElementById('pageCount').textContent = `/ ${premiumInfo.totalPages}`;
485
+ } else {
486
+ document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
487
+ }
363
488
  });
364
489
 
365
490
  eventBus.on('pagechanging', (evt) => {
491
+ // Premium Gate: Block navigation beyond page 1 for non-premium users
492
+ if (premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1 && evt.pageNumber > 1) {
493
+ // Force back to page 1
494
+ setTimeout(() => { pdfViewer.currentPageNumber = 1; }, 0);
495
+ return;
496
+ }
366
497
  document.getElementById('pageInput').value = evt.pageNumber;
367
498
  // Update active thumbnail
368
499
  document.querySelectorAll('.thumbnail').forEach(t => {
@@ -385,6 +516,15 @@
385
516
  });
386
517
 
387
518
  eventBus.on('pagerendered', (evt) => {
519
+ // Premium Gate: Hide pages beyond page 1 for non-premium users
520
+ if (premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1 && evt.pageNumber > 1) {
521
+ const pageView = pdfViewer.getPageView(evt.pageNumber - 1);
522
+ if (pageView && pageView.div) {
523
+ pageView.div.style.display = 'none';
524
+ }
525
+ return;
526
+ }
527
+
388
528
  injectAnnotationLayer(evt.pageNumber);
389
529
 
390
530
  // Rotation is handled natively by PDF.js via pagesRotation
@@ -393,6 +533,11 @@
393
533
  // Page Navigation
394
534
  document.getElementById('pageInput').onchange = (e) => {
395
535
  const num = parseInt(e.target.value);
536
+ // Premium Gate: Block manual page input beyond page 1
537
+ if (premiumInfo && !premiumInfo.isPremium && premiumInfo.totalPages > 1 && num > 1) {
538
+ e.target.value = 1;
539
+ return;
540
+ }
396
541
  if (num >= 1 && num <= pdfViewer.pagesCount) {
397
542
  pdfViewer.currentPageNumber = num;
398
543
  }
@@ -525,6 +670,10 @@
525
670
  document.getElementById('sepiaBtn').classList.contains('active'));
526
671
  closeAllDropdowns();
527
672
  };
673
+ document.getElementById('overflowFullscreen').onclick = () => {
674
+ toggleFullscreen();
675
+ closeAllDropdowns();
676
+ };
528
677
 
529
678
  // Close dropdowns when clicking outside
530
679
  document.addEventListener('click', (e) => {
@@ -613,15 +762,18 @@
613
762
  highlightColor = c;
614
763
  if (currentTool === 'highlight') currentColor = c;
615
764
  document.getElementById('highlightWave').setAttribute('stroke', c);
765
+ document.getElementById('highlightColorIndicator').style.background = c;
616
766
  });
617
767
  setupColorPicker('drawColors', c => {
618
768
  drawColor = c;
619
769
  if (currentTool === 'pen') currentColor = c;
620
770
  document.getElementById('drawWave').setAttribute('stroke', c);
771
+ document.getElementById('drawColorIndicator').style.background = c;
621
772
  });
622
773
  setupColorPicker('shapeColors', c => {
623
774
  shapeColor = c;
624
775
  if (currentTool === 'shape') currentColor = c;
776
+ document.getElementById('shapeColorIndicator').style.background = c;
625
777
  });
626
778
 
627
779
  // Highlighter Thickness Slider
@@ -745,13 +897,9 @@
745
897
  e.preventDefault();
746
898
  draw(e);
747
899
  };
748
- if (annotationMode) {
749
- svg.addEventListener('touchstart', touchStartHandler, { passive: false, signal });
750
- svg.addEventListener('touchmove', touchMoveHandler, { passive: false, signal });
751
- } else {
752
- svg.addEventListener('touchstart', touchStartHandler, { signal });
753
- svg.addEventListener('touchmove', touchMoveHandler, { signal });
754
- }
900
+ // Always use passive:false so preventDefault() works when a tool is later activated
901
+ svg.addEventListener('touchstart', touchStartHandler, { passive: false, signal });
902
+ svg.addEventListener('touchmove', touchMoveHandler, { passive: false, signal });
755
903
  svg.addEventListener('touchend', () => stopDraw(pageNum), { signal });
756
904
  svg.addEventListener('touchcancel', () => stopDraw(pageNum), { signal });
757
905
 
@@ -965,7 +1113,8 @@
965
1113
  // Text tool - create/edit/drag text
966
1114
  if (currentTool === 'text') {
967
1115
  // Check if clicked on existing text element
968
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
1116
+ // Use coords (touch-safe) instead of e.clientX which is undefined on TouchEvent
1117
+ const elementsUnderClick = document.elementsFromPoint(coords.clientX, coords.clientY);
969
1118
  const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
970
1119
 
971
1120
  if (existingText) {
@@ -973,7 +1122,7 @@
973
1122
  startTextDrag(e, existingText, svg, scaleX, pageNum);
974
1123
  } else {
975
1124
  // Create new text
976
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
1125
+ showTextEditor(coords.clientX, coords.clientY, svg, x, y, scaleX, pageNum);
977
1126
  }
978
1127
  return;
979
1128
  }
@@ -1235,14 +1384,18 @@
1235
1384
  textEl.classList.add('dragging');
1236
1385
  hasDragged = false;
1237
1386
 
1238
- dragStartX = e.clientX;
1239
- dragStartY = e.clientY;
1387
+ // Touch-safe coordinate extraction
1388
+ const startCoords = getEventCoords(e);
1389
+ dragStartX = startCoords.clientX;
1390
+ dragStartY = startCoords.clientY;
1240
1391
  textOriginalX = parseFloat(textEl.getAttribute('x'));
1241
1392
  textOriginalY = parseFloat(textEl.getAttribute('y'));
1242
1393
 
1243
- function onMouseMove(ev) {
1244
- const dxScreen = ev.clientX - dragStartX;
1245
- const dyScreen = ev.clientY - dragStartY;
1394
+ function onMove(ev) {
1395
+ ev.preventDefault();
1396
+ const moveCoords = getEventCoords(ev);
1397
+ const dxScreen = moveCoords.clientX - dragStartX;
1398
+ const dyScreen = moveCoords.clientY - dragStartY;
1246
1399
  // Convert screen delta to viewBox delta (rotation-aware)
1247
1400
  const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen, textEl);
1248
1401
 
@@ -1254,29 +1407,31 @@
1254
1407
  textEl.setAttribute('y', (textOriginalY + vbDelta.dy).toFixed(2));
1255
1408
  }
1256
1409
 
1257
- function onMouseUp(ev) {
1258
- document.removeEventListener('mousemove', onMouseMove);
1259
- document.removeEventListener('mouseup', onMouseUp);
1410
+ function onEnd(ev) {
1411
+ document.removeEventListener('mousemove', onMove);
1412
+ document.removeEventListener('mouseup', onEnd);
1413
+ document.removeEventListener('touchmove', onMove);
1414
+ document.removeEventListener('touchend', onEnd);
1260
1415
  textEl.classList.remove('dragging');
1261
1416
 
1262
1417
  if (hasDragged) {
1263
1418
  // Moved - save position
1264
1419
  saveAnnotations(pageNum);
1265
1420
  } else {
1266
- // Not moved - short click = edit
1267
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
1268
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
1421
+ // Not moved - short click/tap = edit
1269
1422
  const svgX = parseFloat(textEl.getAttribute('x'));
1270
1423
  const svgY = parseFloat(textEl.getAttribute('y'));
1271
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
1272
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
1424
+ const endCoords = getEventCoords(ev);
1425
+ showTextEditor(endCoords.clientX, endCoords.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
1273
1426
  }
1274
1427
 
1275
1428
  draggedText = null;
1276
1429
  }
1277
1430
 
1278
- document.addEventListener('mousemove', onMouseMove);
1279
- document.addEventListener('mouseup', onMouseUp);
1431
+ document.addEventListener('mousemove', onMove);
1432
+ document.addEventListener('mouseup', onEnd);
1433
+ document.addEventListener('touchmove', onMove, { passive: false });
1434
+ document.addEventListener('touchend', onEnd);
1280
1435
  }
1281
1436
 
1282
1437
  // Inline Text Editor