nodebb-plugin-pdf-secure 1.2.28 → 1.2.30

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.
@@ -1749,6 +1749,56 @@
1749
1749
  font-size: 12px;
1750
1750
  }
1751
1751
  }
1752
+
1753
+ /* ── Unicourse Page Watermark ── */
1754
+ .unicourse-page-wm {
1755
+ position: absolute;
1756
+ bottom: calc(10px * var(--wm-scale, 1));
1757
+ right: calc(12px * var(--wm-scale, 1));
1758
+ z-index: 10;
1759
+ display: flex;
1760
+ align-items: center;
1761
+ gap: calc(5px * var(--wm-scale, 1));
1762
+ background: none;
1763
+ border: none;
1764
+ padding: 0;
1765
+ text-decoration: none;
1766
+ opacity: 0.7;
1767
+ transition: opacity 0.2s ease;
1768
+ cursor: pointer;
1769
+ transform-origin: bottom right;
1770
+ }
1771
+
1772
+ .unicourse-page-wm:hover {
1773
+ opacity: 1;
1774
+ }
1775
+
1776
+ .unicourse-page-wm img {
1777
+ height: calc(26px * var(--wm-scale, 1));
1778
+ width: auto;
1779
+ display: block;
1780
+ pointer-events: none;
1781
+ flex-shrink: 0;
1782
+ }
1783
+
1784
+ .unicourse-page-wm-text {
1785
+ display: flex;
1786
+ flex-direction: column;
1787
+ align-items: flex-start;
1788
+ gap: calc(1px * var(--wm-scale, 1));
1789
+ }
1790
+
1791
+ .unicourse-page-wm-text span {
1792
+ font-size: calc(7.5px * var(--wm-scale, 1));
1793
+ color: #888;
1794
+ white-space: nowrap;
1795
+ line-height: 1.2;
1796
+ font-weight: 600;
1797
+ }
1798
+
1799
+ @media print {
1800
+ .unicourse-page-wm { display: none !important; }
1801
+ }
1752
1802
  </style>
1753
1803
  </head>
1754
1804
 
@@ -2279,24 +2329,30 @@
2279
2329
  // Thumbnails will be generated on-demand when sidebar opens
2280
2330
  }
2281
2331
 
2282
- // Partial XOR decoder - must match backend encoding
2283
- function partialXorDecode(encodedData, keyBase64) {
2284
- const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
2285
- const data = new Uint8Array(encodedData);
2286
- const keyLen = key.length;
2332
+ // AES-256-GCM decoder using Web Crypto API
2333
+ async function aesGcmDecode(encodedData, keyBase64, ivBase64) {
2334
+ const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
2335
+ const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
2336
+ const encData = new Uint8Array(encodedData);
2287
2337
 
2288
- // Decrypt first 10KB fully
2289
- const fullDecryptLen = Math.min(10240, data.length);
2290
- for (let i = 0; i < fullDecryptLen; i++) {
2291
- data[i] = data[i] ^ key[i % keyLen];
2292
- }
2338
+ // Format: [authTag (16 bytes)] [ciphertext]
2339
+ const authTag = encData.slice(0, 16);
2340
+ const ciphertext = encData.slice(16);
2293
2341
 
2294
- // Decrypt every 50th byte after that
2295
- for (let i = fullDecryptLen; i < data.length; i += 50) {
2296
- data[i] = data[i] ^ key[i % keyLen];
2297
- }
2342
+ // Web Crypto expects ciphertext + authTag concatenated
2343
+ const combined = new Uint8Array(ciphertext.length + authTag.length);
2344
+ combined.set(ciphertext);
2345
+ combined.set(authTag, ciphertext.length);
2298
2346
 
2299
- return data.buffer;
2347
+ const cryptoKey = await crypto.subtle.importKey(
2348
+ 'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']
2349
+ );
2350
+
2351
+ return crypto.subtle.decrypt(
2352
+ { name: 'AES-GCM', iv: iv },
2353
+ cryptoKey,
2354
+ combined
2355
+ );
2300
2356
  }
2301
2357
 
2302
2358
  function injectPageLock(pageEl) {
@@ -2307,7 +2363,7 @@
2307
2363
  }
2308
2364
  // Don't duplicate overlay
2309
2365
  if (pageEl.querySelector('.page-lock-overlay')) return;
2310
- var uid = (window.PDF_SECURE_CONFIG && window.PDF_SECURE_CONFIG.uid) || 0;
2366
+ var uid = (_cfg && _cfg.uid) || 0;
2311
2367
  var checkoutUrl = 'https://forum.ieu.app/pay/checkout?uid=' + uid;
2312
2368
  var overlay = document.createElement('div');
2313
2369
  overlay.className = 'page-lock-overlay';
@@ -2321,8 +2377,8 @@
2321
2377
  <div class="page-lock-message">Tum sayfalara erisim icin Premium satin alabilir veya materyal yukleyerek erisim kazanabilirsiniz.</div>\
2322
2378
  <div class="page-lock-actions">\
2323
2379
  <a href="' + checkoutUrl + '" target="_blank" class="page-lock-button">\
2324
- <svg viewBox="0 0 24 24" width="18" height="18" fill="#1a1a1a"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V6h16v12zm-8-1h2v-4h4v-2h-4V7h-2v4H8v2h4v4z"/></svg>\
2325
- Premium Satin Al\
2380
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="#1a1a1a"><path d="M16 18v2H8v-2h8zM11 7.99V16h2V7.99h3L12 4l-4 3.99h3z"/></svg>\
2381
+ Hesabini Yukselt\
2326
2382
  </a>\
2327
2383
  <div class="page-lock-divider">ya da</div>\
2328
2384
  <a href="https://forum.ieu.app/material-info" target="_blank" class="page-lock-button-secondary">\
@@ -2517,7 +2573,8 @@
2517
2573
  if (!pdfBuffer) {
2518
2574
  // Nonce and key are embedded in HTML config (not fetched from API)
2519
2575
  const nonce = config.nonce;
2520
- const xorKey = config.dk;
2576
+ const decryptKey = config.dk;
2577
+ const decryptIv = config.iv;
2521
2578
 
2522
2579
  // Fetch encrypted PDF binary
2523
2580
  const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
@@ -2530,10 +2587,10 @@
2530
2587
  const encodedBuffer = await pdfRes.arrayBuffer();
2531
2588
  console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
2532
2589
 
2533
- // Decode XOR encrypted data
2534
- if (xorKey) {
2535
- console.log('[PDF-Secure] Decoding XOR encrypted data...');
2536
- pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
2590
+ // Decrypt AES-256-GCM encrypted data
2591
+ if (decryptKey && decryptIv) {
2592
+ console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
2593
+ pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
2537
2594
  } else {
2538
2595
  pdfBuffer = encodedBuffer;
2539
2596
  }
@@ -2611,7 +2668,7 @@
2611
2668
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
2612
2669
  </svg>
2613
2670
  <h2>Hata</h2>
2614
- <p>${err.message}</p>
2671
+ <p>${err.message.replace(/[<>&"']/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]))}</p>
2615
2672
  `;
2616
2673
  }
2617
2674
  }
@@ -2688,9 +2745,45 @@
2688
2745
  currentDrawingPage = null;
2689
2746
  });
2690
2747
 
2748
+ // ── Unicourse page watermark injection ──
2749
+ function injectPageWatermark(pageEl) {
2750
+ if (!pageEl) return;
2751
+ var scale = pdfViewer ? pdfViewer.currentScale : 1;
2752
+ var existing = pageEl.querySelector('.unicourse-page-wm');
2753
+ if (existing) {
2754
+ existing.style.setProperty('--wm-scale', scale);
2755
+ return;
2756
+ }
2757
+ pageEl.style.position = 'relative';
2758
+ var a = document.createElement('a');
2759
+ a.href = 'https://unicourse.co/dersler/izmir-ekonomi-universitesi';
2760
+ a.target = '_blank';
2761
+ a.rel = 'noopener';
2762
+ a.className = 'unicourse-page-wm';
2763
+ a.style.setProperty('--wm-scale', scale);
2764
+ a.innerHTML = '<img src="https://cdn.prod.website-files.com/66c986ba4ac79c4fbbd5f45e/66c9958f0188acfb12a2d2fb_64a464c6b3b7df67c22a255b_logo_full.png" alt="unicourse">' +
2765
+ '<div class="unicourse-page-wm-text">' +
2766
+ '<span>Çıkmış sorular ve</span>' +
2767
+ '<span>ders notları için tıkla</span>' +
2768
+ '</div>';
2769
+ pageEl.appendChild(a);
2770
+ }
2771
+
2772
+ // Update all watermarks when zoom changes
2773
+ function updateAllWatermarkScales() {
2774
+ var scale = pdfViewer ? pdfViewer.currentScale : 1;
2775
+ document.querySelectorAll('.unicourse-page-wm').forEach(function(wm) {
2776
+ wm.style.setProperty('--wm-scale', scale);
2777
+ });
2778
+ }
2779
+
2691
2780
  eventBus.on('pagerendered', (evt) => {
2692
2781
  injectAnnotationLayer(evt.pageNumber);
2693
2782
 
2783
+ // Inject unicourse watermark on every page
2784
+ var wmPageEl = document.querySelector('#viewer .page[data-page-number="' + evt.pageNumber + '"]');
2785
+ if (wmPageEl) injectPageWatermark(wmPageEl);
2786
+
2694
2787
  // Re-inject lock overlay if this is a locked page (PDF.js re-render removes it)
2695
2788
  if (premiumInfo && !premiumInfo.isPremium && evt.pageNumber > 1) {
2696
2789
  var pageEl = document.querySelector('#viewer .page[data-page-number="' + evt.pageNumber + '"]');
@@ -2709,8 +2802,8 @@
2709
2802
  };
2710
2803
 
2711
2804
  // Zoom
2712
- document.getElementById('zoomIn').onclick = () => pdfViewer.currentScale += 0.25;
2713
- document.getElementById('zoomOut').onclick = () => pdfViewer.currentScale -= 0.25;
2805
+ document.getElementById('zoomIn').onclick = () => { pdfViewer.currentScale += 0.25; updateAllWatermarkScales(); };
2806
+ document.getElementById('zoomOut').onclick = () => { pdfViewer.currentScale -= 0.25; updateAllWatermarkScales(); };
2714
2807
 
2715
2808
  // Sidebar toggle (deferred thumbnail generation)
2716
2809
  const sidebarEl = document.getElementById('sidebar');
@@ -4794,18 +4887,21 @@
4794
4887
  e.preventDefault();
4795
4888
  e.stopPropagation();
4796
4889
  pdfViewer.currentScale += 0.25;
4890
+ updateAllWatermarkScales();
4797
4891
  return;
4798
4892
  }
4799
4893
  if ((e.ctrlKey || e.metaKey) && (key === '-' || e.code === 'Minus')) {
4800
4894
  e.preventDefault();
4801
4895
  e.stopPropagation();
4802
4896
  pdfViewer.currentScale -= 0.25;
4897
+ updateAllWatermarkScales();
4803
4898
  return;
4804
4899
  }
4805
4900
  if ((e.ctrlKey || e.metaKey) && (key === '0' || e.code === 'Digit0')) {
4806
4901
  e.preventDefault();
4807
4902
  e.stopPropagation();
4808
4903
  pdfViewer.currentScaleValue = 'page-width';
4904
+ updateAllWatermarkScales();
4809
4905
  return;
4810
4906
  }
4811
4907
 
@@ -4889,8 +4985,8 @@
4889
4985
  case 'highlight': setTool('highlight'); break;
4890
4986
  case 'pen': setTool('pen'); break;
4891
4987
  case 'text': setTool('text'); break;
4892
- case 'zoomIn': pdfViewer.currentScale += 0.25; break;
4893
- case 'zoomOut': pdfViewer.currentScale -= 0.25; break;
4988
+ case 'zoomIn': pdfViewer.currentScale += 0.25; updateAllWatermarkScales(); break;
4989
+ case 'zoomOut': pdfViewer.currentScale -= 0.25; updateAllWatermarkScales(); break;
4894
4990
  case 'sepia': document.getElementById('sepiaBtn').click(); break;
4895
4991
  }
4896
4992
  contextMenu.classList.remove('visible');
@@ -4908,7 +5004,7 @@
4908
5004
  if (inIframe) {
4909
5005
  // In iframe: ask parent to fullscreen the iframe element
4910
5006
  // Parent manages fullscreen → more stable on tablets
4911
- window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' }, '*');
5007
+ window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' }, window.location.origin);
4912
5008
  } else {
4913
5009
  // Standalone: use local fullscreen
4914
5010
  const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
@@ -4995,6 +5091,7 @@
4995
5091
  } else {
4996
5092
  pdfViewer.currentScale -= 0.1;
4997
5093
  }
5094
+ updateAllWatermarkScales();
4998
5095
  }
4999
5096
  }, { passive: false });
5000
5097
 
@@ -5181,6 +5278,7 @@
5181
5278
  const oldScrollTop = container.scrollTop;
5182
5279
 
5183
5280
  pdfViewer.currentScale = finalScale;
5281
+ updateAllWatermarkScales();
5184
5282
 
5185
5283
  // Adjust scroll after PDF.js re-render to keep view centered
5186
5284
  requestAnimationFrame(() => {
@@ -5459,6 +5557,7 @@
5459
5557
  // End of main IIFE - pdfDoc, pdfViewer not accessible from console
5460
5558
  })();
5461
5559
  </script>
5560
+
5462
5561
  </body>
5463
5562
 
5464
5563
  </html>
@@ -1,184 +0,0 @@
1
- import { getDocument, GlobalWorkerOptions } from './pdf.min.mjs';
2
-
3
- GlobalWorkerOptions.workerSrc = new URL('./pdf.worker.min.mjs', import.meta.url).href;
4
- console.log('[PDF-Secure][Viewer] ES module loaded, worker:', GlobalWorkerOptions.workerSrc);
5
-
6
- // Listen for NodeBB SPA navigations (jQuery event)
7
- $(window).on('action:ajaxify.end', function () {
8
- console.log('[PDF-Secure][Viewer] action:ajaxify.end fired');
9
- interceptPdfLinks();
10
- });
11
-
12
- // Also run immediately for the current page
13
- console.log('[PDF-Secure][Viewer] Running initial interceptPdfLinks...');
14
- interceptPdfLinks();
15
-
16
- function interceptPdfLinks() {
17
- var postContents = document.querySelectorAll('[component="post/content"]');
18
- console.log('[PDF-Secure][Viewer] Found ' + postContents.length + ' post areas');
19
-
20
- postContents.forEach(function (content, idx) {
21
- var pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
22
- console.log('[PDF-Secure][Viewer] Post #' + idx + ': ' + pdfLinks.length + ' PDF links');
23
-
24
- pdfLinks.forEach(function (link) {
25
- if (link.dataset.pdfSecure) return;
26
- link.dataset.pdfSecure = 'true';
27
-
28
- var href = link.getAttribute('href');
29
- var parts = href.split('/');
30
- var filename = parts[parts.length - 1];
31
- console.log('[PDF-Secure][Viewer] Processing:', filename);
32
-
33
- var container = document.createElement('div');
34
- container.className = 'pdf-secure-inline';
35
- container.innerHTML =
36
- '<div class="pdf-secure-inline-header">' +
37
- '<i class="fa fa-file-pdf-o"></i> ' +
38
- '<span class="pdf-secure-filename">' + escapeHtml(link.textContent || filename) + '</span>' +
39
- '</div>' +
40
- '<div class="pdf-secure-inline-body">' +
41
- '<div class="pdf-secure-loading">Loading PDF...</div>' +
42
- '<div class="pdf-secure-error"></div>' +
43
- '<canvas class="pdf-secure-canvas"></canvas>' +
44
- '</div>' +
45
- '<div class="pdf-secure-inline-footer">' +
46
- '<button class="pdf-secure-prev" disabled>&#8249; Prev</button>' +
47
- '<span class="pdf-secure-page-info"></span>' +
48
- '<button class="pdf-secure-next" disabled>Next &#8250;</button>' +
49
- '</div>';
50
-
51
- link.replaceWith(container);
52
- console.log('[PDF-Secure][Viewer] Container created for:', filename);
53
-
54
- loadPdf(container, filename);
55
- });
56
- });
57
- }
58
-
59
- async function loadPdf(container, filename) {
60
- var loadingEl = container.querySelector('.pdf-secure-loading');
61
- var errorEl = container.querySelector('.pdf-secure-error');
62
- var canvas = container.querySelector('.pdf-secure-canvas');
63
- var footer = container.querySelector('.pdf-secure-inline-footer');
64
- var prevBtn = container.querySelector('.pdf-secure-prev');
65
- var nextBtn = container.querySelector('.pdf-secure-next');
66
- var pageInfo = container.querySelector('.pdf-secure-page-info');
67
- var bodyEl = container.querySelector('.pdf-secure-inline-body');
68
-
69
- function showError(msg) {
70
- console.error('[PDF-Secure][Viewer] ERROR ' + filename + ':', msg);
71
- loadingEl.style.display = 'none';
72
- canvas.style.display = 'none';
73
- errorEl.style.display = 'flex';
74
- errorEl.textContent = msg;
75
- }
76
-
77
- try {
78
- // Step 1: Fetch nonce
79
- var nonceUrl = config.relative_path + '/api/v3/plugins/pdf-secure/nonce?file=' + encodeURIComponent(filename);
80
- console.log('[PDF-Secure][Viewer] Step 1 - Nonce request:', nonceUrl);
81
-
82
- var nonceRes = await fetch(nonceUrl, {
83
- credentials: 'same-origin',
84
- headers: { 'x-csrf-token': config.csrf_token },
85
- });
86
- console.log('[PDF-Secure][Viewer] Step 1 - Status:', nonceRes.status);
87
-
88
- if (!nonceRes.ok) {
89
- showError(nonceRes.status === 401 ? 'Log in to view this PDF.' : 'Failed to load PDF (' + nonceRes.status + ')');
90
- return;
91
- }
92
-
93
- var result = await nonceRes.json();
94
- var nonce = result.response.nonce;
95
- console.log('[PDF-Secure][Viewer] Step 1 - Nonce:', nonce);
96
-
97
- // Step 2: Fetch PDF binary
98
- var pdfUrl = config.relative_path + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
99
- console.log('[PDF-Secure][Viewer] Step 2 - PDF request:', pdfUrl);
100
-
101
- var pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
102
- console.log('[PDF-Secure][Viewer] Step 2 - Status:', pdfRes.status);
103
-
104
- if (!pdfRes.ok) {
105
- showError('Failed to load PDF data (' + pdfRes.status + ')');
106
- return;
107
- }
108
-
109
- var pdfArrayBuffer = await pdfRes.arrayBuffer();
110
- console.log('[PDF-Secure][Viewer] Step 2 - PDF loaded:', pdfArrayBuffer.byteLength, 'bytes');
111
-
112
- // Step 3: Render PDF
113
- console.log('[PDF-Secure][Viewer] Step 3 - Rendering...');
114
- var pdfDoc = await getDocument({ data: new Uint8Array(pdfArrayBuffer) }).promise;
115
- var totalPages = pdfDoc.numPages;
116
- console.log('[PDF-Secure][Viewer] Step 3 - Pages:', totalPages);
117
-
118
- loadingEl.style.display = 'none';
119
- canvas.style.display = 'block';
120
-
121
- // Security: scoped to container
122
- container.addEventListener('contextmenu', function (e) { e.preventDefault(); });
123
- container.addEventListener('dragstart', function (e) { e.preventDefault(); });
124
- container.addEventListener('selectstart', function (e) { e.preventDefault(); });
125
-
126
- var ctx = canvas.getContext('2d');
127
- var currentPage = 1;
128
- var rendering = false;
129
-
130
- async function renderPage(pageNum) {
131
- if (rendering) return;
132
- rendering = true;
133
- console.log('[PDF-Secure][Viewer] renderPage(' + pageNum + ')');
134
-
135
- try {
136
- var page = await pdfDoc.getPage(pageNum);
137
- var containerWidth = bodyEl.clientWidth - 20;
138
- var vp = page.getViewport({ scale: 1 });
139
- var scale = Math.min(containerWidth / vp.width, 2.0);
140
- var scaled = page.getViewport({ scale: scale });
141
-
142
- canvas.width = scaled.width;
143
- canvas.height = scaled.height;
144
-
145
- await page.render({ canvasContext: ctx, viewport: scaled }).promise;
146
-
147
- currentPage = pageNum;
148
- pageInfo.textContent = currentPage + ' / ' + totalPages;
149
- prevBtn.disabled = currentPage <= 1;
150
- nextBtn.disabled = currentPage >= totalPages;
151
- console.log('[PDF-Secure][Viewer] renderPage(' + pageNum + ') done, canvas:', scaled.width + 'x' + scaled.height);
152
- } catch (err) {
153
- console.error('[PDF-Secure][Viewer] Render error:', err);
154
- showError('Error rendering page.');
155
- }
156
- rendering = false;
157
- }
158
-
159
- await renderPage(1);
160
-
161
- if (totalPages > 1) {
162
- footer.style.display = 'flex';
163
- prevBtn.addEventListener('click', function () {
164
- if (currentPage > 1) renderPage(currentPage - 1);
165
- });
166
- nextBtn.addEventListener('click', function () {
167
- if (currentPage < totalPages) renderPage(currentPage + 1);
168
- });
169
- console.log('[PDF-Secure][Viewer] Navigation enabled (' + totalPages + ' pages)');
170
- }
171
-
172
- console.log('[PDF-Secure][Viewer] DONE for:', filename);
173
- } catch (err) {
174
- console.error('[PDF-Secure][Viewer] CATCH:', err);
175
- console.error('[PDF-Secure][Viewer] Stack:', err.stack);
176
- showError('Failed to load PDF.');
177
- }
178
- }
179
-
180
- function escapeHtml(str) {
181
- var d = document.createElement('div');
182
- d.textContent = str;
183
- return d.innerHTML;
184
- }