nodebb-plugin-pdf-secure 1.2.19 → 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.
@@ -0,0 +1,52 @@
1
+ # Premium Gate Design
2
+
3
+ ## Problem
4
+ All PDFs are currently fully viewable by all users. We need to restrict non-Premium users to viewing only the first page, with a lock overlay prompting them to upgrade.
5
+
6
+ ## Requirements
7
+ - Users in the "Premium" NodeBB group see all PDF pages
8
+ - Users NOT in "Premium" see only the first page
9
+ - Locked pages show a warning overlay with:
10
+ - Lock icon + "Premium required" message
11
+ - Link to forumtest.ieu.app/premium
12
+ - Secondary message about uploading materials to become Premium
13
+ - Admins and Global Moderators always get full access
14
+
15
+ ## Approach: Server-Side Control
16
+
17
+ ### Server Changes
18
+
19
+ **library.js (viewer route, line ~83):**
20
+ - Replace `const isPremium = true;` with actual group membership check
21
+ - Use `groups.isMember(req.uid, 'Premium')`
22
+ - Admin + Global Mod bypass (already exists for direct access, reuse pattern)
23
+ - Inject `isPremium` and `totalPages` into `window.PDF_SECURE_CONFIG`
24
+
25
+ **lib/pdf-handler.js:**
26
+ - Add `getTotalPages(filename)` function
27
+ - Loads PDF, returns `srcDoc.getPageCount()`
28
+ - Used by viewer route to tell client how many pages exist
29
+
30
+ **lib/controllers.js:**
31
+ - No changes needed - already handles `isPremium` flag from nonce data
32
+
33
+ ### Client Changes
34
+
35
+ **static/viewer.html / viewer-app.js:**
36
+ - After PDF loads, if `isPremium === false`:
37
+ - Show only 1 page (server already sends only 1 page)
38
+ - Below the first page, render a lock overlay
39
+ - Overlay content:
40
+ - Lock icon (SVG)
41
+ - "Bu icerigi goruntulemek icin Premium uyelik gereklidir"
42
+ - "Premium Satin Al" button → forumtest.ieu.app/premium
43
+ - "Materyal yukleyerek de Premium olabilirsiniz!" secondary text
44
+
45
+ ### Flow
46
+ 1. User clicks PDF → viewer route called
47
+ 2. Server checks Premium group membership
48
+ 3. isPremium flag set in nonce + injected to viewer HTML
49
+ 4. If not premium: server sends single-page PDF via getSinglePagePdf()
50
+ 5. Client shows the page + lock overlay with upgrade prompt
51
+ 6. If premium: server sends full PDF via getFullPdf()
52
+ 7. Client shows all pages normally
@@ -0,0 +1,323 @@
1
+ # Premium Gate Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Restrict non-Premium users to viewing only the first PDF page, with a lock overlay prompting upgrade.
6
+
7
+ **Architecture:** Server checks user's "Premium" group membership, sends single-page PDF for non-premium users. Client renders a lock overlay below the first page showing upgrade prompts. Admin/Global Moderators always get full access.
8
+
9
+ **Tech Stack:** NodeBB plugin (Node.js), pdf-lib, PDF.js viewer, LESS/CSS
10
+
11
+ ---
12
+
13
+ ### Task 1: Add getTotalPages to pdf-handler
14
+
15
+ **Files:**
16
+ - Modify: `lib/pdf-handler.js`
17
+
18
+ **Step 1: Add getTotalPages function**
19
+
20
+ Add after `getSinglePagePdf` (after line 89):
21
+
22
+ ```javascript
23
+ PdfHandler.getTotalPages = async function (filename) {
24
+ const filePath = PdfHandler.resolveFilePath(filename);
25
+ if (!filePath) {
26
+ throw new Error('Invalid filename');
27
+ }
28
+
29
+ if (!fs.existsSync(filePath)) {
30
+ throw new Error('File not found');
31
+ }
32
+
33
+ const pdfBytes = await fs.promises.readFile(filePath);
34
+ const srcDoc = await PDFDocument.load(pdfBytes);
35
+ return srcDoc.getPageCount();
36
+ };
37
+ ```
38
+
39
+ **Step 2: Commit**
40
+
41
+ ```bash
42
+ git add lib/pdf-handler.js
43
+ git commit -m "feat: add getTotalPages to pdf-handler"
44
+ ```
45
+
46
+ ---
47
+
48
+ ### Task 2: Add Premium group check to viewer route
49
+
50
+ **Files:**
51
+ - Modify: `library.js` (lines 63-119, the viewer route)
52
+
53
+ **Step 1: Replace hardcoded isPremium with group check**
54
+
55
+ In `library.js`, replace line 83:
56
+ ```javascript
57
+ const isPremium = true;
58
+ ```
59
+
60
+ With:
61
+ ```javascript
62
+ // Check if user is Premium (admins/global mods always premium)
63
+ let isPremium = false;
64
+ if (req.uid) {
65
+ const [isAdmin, isGlobalMod, isPremiumMember] = await Promise.all([
66
+ groups.isMember(req.uid, 'administrators'),
67
+ groups.isMember(req.uid, 'Global Moderators'),
68
+ groups.isMember(req.uid, 'Premium'),
69
+ ]);
70
+ isPremium = isAdmin || isGlobalMod || isPremiumMember;
71
+ }
72
+ ```
73
+
74
+ **Step 2: Add totalPages to config injection**
75
+
76
+ In `library.js`, inside the viewer route handler, before the nonce generation, add:
77
+ ```javascript
78
+ // Get total page count for non-premium lock overlay
79
+ const pdfHandler = require('./lib/pdf-handler');
80
+ let totalPages = 1;
81
+ try {
82
+ totalPages = await pdfHandler.getTotalPages(safeName);
83
+ } catch (e) {
84
+ console.error('[PDF-Secure] Failed to get page count:', e.message);
85
+ }
86
+ ```
87
+
88
+ Note: Move the `pdfHandler` require to the top of the file (line 10 area) instead of inline.
89
+
90
+ **Step 3: Inject isPremium and totalPages into viewer config**
91
+
92
+ In the `window.PDF_SECURE_CONFIG` object (around line 108-114), add two new fields:
93
+ ```javascript
94
+ window.PDF_SECURE_CONFIG = {
95
+ filename: ${JSON.stringify(safeName)},
96
+ relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
97
+ csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
98
+ nonce: ${JSON.stringify(nonceData.nonce)},
99
+ dk: ${JSON.stringify(nonceData.xorKey)},
100
+ isPremium: ${JSON.stringify(isPremium)},
101
+ totalPages: ${JSON.stringify(totalPages)}
102
+ };
103
+ ```
104
+
105
+ **Step 4: Make the viewer route handler async**
106
+
107
+ The current route handler at line 64 is a sync callback: `router.get('/plugins/pdf-secure/viewer', (req, res) => {`
108
+
109
+ Change it to async: `router.get('/plugins/pdf-secure/viewer', async (req, res) => {`
110
+
111
+ **Step 5: Commit**
112
+
113
+ ```bash
114
+ git add library.js
115
+ git commit -m "feat: check Premium group membership in viewer route"
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Task 3: Add lock overlay to viewer-app.js
121
+
122
+ **Files:**
123
+ - Modify: `static/viewer-app.js`
124
+
125
+ **Step 1: Add premium lock overlay after PDF loads**
126
+
127
+ In `autoLoadSecurePDF()`, after `await loadPDFFromBuffer(pdfBuffer);` (line 262), add the lock overlay logic. The config is still available at this point (it gets deleted on line 270).
128
+
129
+ Insert before `pdfBuffer = null;` (line 267):
130
+
131
+ ```javascript
132
+ // Premium Gate: Show lock overlay for non-premium users
133
+ if (config.isPremium === false && config.totalPages > 1) {
134
+ showPremiumLockOverlay(config.totalPages);
135
+ }
136
+ ```
137
+
138
+ **Step 2: Add showPremiumLockOverlay function**
139
+
140
+ Add this function inside the IIFE, before `autoLoadSecurePDF()`:
141
+
142
+ ```javascript
143
+ function showPremiumLockOverlay(totalPages) {
144
+ const viewerEl = document.getElementById('viewer');
145
+ if (!viewerEl) return;
146
+
147
+ const overlay = document.createElement('div');
148
+ overlay.id = 'premiumLockOverlay';
149
+ overlay.innerHTML = `
150
+ <div class="premium-lock-icon">
151
+ <svg viewBox="0 0 24 24" width="64" height="64" fill="#ffd700">
152
+ <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"/>
153
+ </svg>
154
+ </div>
155
+ <div class="premium-lock-pages">
156
+ ${totalPages - 1} sayfa daha kilitli
157
+ </div>
158
+ <div class="premium-lock-message">
159
+ Bu icerigi goruntuleye devam etmek icin Premium uyelik gereklidir.
160
+ </div>
161
+ <a href="https://forumtest.ieu.app/premium" target="_blank" class="premium-lock-button">
162
+ Premium Satin Al
163
+ </a>
164
+ <div class="premium-lock-secondary">
165
+ Materyal yukleyerek de Premium olabilirsiniz!
166
+ </div>
167
+ `;
168
+
169
+ viewerEl.appendChild(overlay);
170
+ }
171
+ ```
172
+
173
+ **Step 3: Commit**
174
+
175
+ ```bash
176
+ git add static/viewer-app.js
177
+ git commit -m "feat: add premium lock overlay for non-premium users"
178
+ ```
179
+
180
+ ---
181
+
182
+ ### Task 4: Add lock overlay CSS styles
183
+
184
+ **Files:**
185
+ - Modify: `static/viewer.html` (inline styles section)
186
+
187
+ **Step 1: Add premium lock overlay styles**
188
+
189
+ Add these styles inside the `<style>` tag in viewer.html, before the closing `</style>`:
190
+
191
+ ```css
192
+ /* Premium Lock Overlay */
193
+ #premiumLockOverlay {
194
+ display: flex;
195
+ flex-direction: column;
196
+ align-items: center;
197
+ justify-content: center;
198
+ padding: 60px 20px;
199
+ text-align: center;
200
+ background: linear-gradient(180deg, rgba(31,31,31,0.95) 0%, rgba(20,20,20,0.98) 100%);
201
+ border-top: 2px solid rgba(255, 215, 0, 0.3);
202
+ min-height: 400px;
203
+ margin: 0 auto;
204
+ max-width: 100%;
205
+ }
206
+
207
+ .premium-lock-icon {
208
+ margin-bottom: 20px;
209
+ opacity: 0.9;
210
+ }
211
+
212
+ .premium-lock-pages {
213
+ font-size: 22px;
214
+ font-weight: 600;
215
+ color: #ffd700;
216
+ margin-bottom: 12px;
217
+ }
218
+
219
+ .premium-lock-message {
220
+ font-size: 16px;
221
+ color: #a0a0a0;
222
+ margin-bottom: 28px;
223
+ max-width: 400px;
224
+ line-height: 1.5;
225
+ }
226
+
227
+ .premium-lock-button {
228
+ display: inline-block;
229
+ padding: 14px 40px;
230
+ background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
231
+ color: #1a1a1a;
232
+ font-size: 16px;
233
+ font-weight: 700;
234
+ border-radius: 8px;
235
+ text-decoration: none;
236
+ transition: transform 0.2s, box-shadow 0.2s;
237
+ box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
238
+ }
239
+
240
+ .premium-lock-button:hover {
241
+ transform: translateY(-2px);
242
+ box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
243
+ }
244
+
245
+ .premium-lock-secondary {
246
+ margin-top: 20px;
247
+ font-size: 14px;
248
+ color: #888;
249
+ font-style: italic;
250
+ }
251
+
252
+ @media (max-width: 768px) {
253
+ #premiumLockOverlay {
254
+ padding: 40px 16px;
255
+ min-height: 300px;
256
+ }
257
+
258
+ .premium-lock-pages {
259
+ font-size: 18px;
260
+ }
261
+
262
+ .premium-lock-message {
263
+ font-size: 14px;
264
+ }
265
+
266
+ .premium-lock-button {
267
+ padding: 12px 32px;
268
+ font-size: 14px;
269
+ }
270
+ }
271
+ ```
272
+
273
+ **Step 2: Commit**
274
+
275
+ ```bash
276
+ git add static/viewer.html
277
+ git commit -m "feat: add premium lock overlay CSS styles"
278
+ ```
279
+
280
+ ---
281
+
282
+ ### Task 5: Hide sidebar and page count for non-premium users
283
+
284
+ **Files:**
285
+ - Modify: `static/viewer-app.js`
286
+
287
+ **Step 1: Update page count display for non-premium**
288
+
289
+ In the `pagesinit` event handler (around line 360-362), update the page count to show total pages from config instead of the actual loaded pages:
290
+
291
+ Find the `eventBus.on('pagesinit', ...)` handler and wrap the pageCount update:
292
+
293
+ ```javascript
294
+ eventBus.on('pagesinit', () => {
295
+ pdfViewer.currentScaleValue = 'page-width';
296
+ // For non-premium, show "1 / totalPages" instead of "1 / 1"
297
+ const savedConfig = window._pdfSecurePremiumInfo;
298
+ if (savedConfig && !savedConfig.isPremium && savedConfig.totalPages > 1) {
299
+ document.getElementById('pageCount').textContent = `/ ${savedConfig.totalPages}`;
300
+ } else {
301
+ document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
302
+ }
303
+ });
304
+ ```
305
+
306
+ **Step 2: Save premium info before config is deleted**
307
+
308
+ In `autoLoadSecurePDF()`, right before the config delete line (`delete window.PDF_SECURE_CONFIG`), save the premium info:
309
+
310
+ ```javascript
311
+ // Save premium info for UI (page count display)
312
+ window._pdfSecurePremiumInfo = {
313
+ isPremium: config.isPremium,
314
+ totalPages: config.totalPages
315
+ };
316
+ ```
317
+
318
+ **Step 3: Commit**
319
+
320
+ ```bash
321
+ git add static/viewer-app.js
322
+ git commit -m "feat: show real total page count for non-premium users"
323
+ ```
@@ -6,6 +6,7 @@ const { PDFDocument } = require('pdf-lib');
6
6
  const nconf = require.main.require('nconf');
7
7
 
8
8
  const singlePageCache = new Map();
9
+ const pageCountCache = new Map();
9
10
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour
10
11
 
11
12
  // Periodic cleanup of expired cache entries
@@ -16,6 +17,11 @@ setInterval(() => {
16
17
  singlePageCache.delete(key);
17
18
  }
18
19
  }
20
+ for (const [key, entry] of pageCountCache.entries()) {
21
+ if (now - entry.createdAt > CACHE_TTL) {
22
+ pageCountCache.delete(key);
23
+ }
24
+ }
19
25
  }, 10 * 60 * 1000).unref(); // cleanup every 10 minutes
20
26
 
21
27
  const PdfHandler = module.exports;
@@ -72,6 +78,10 @@ PdfHandler.getSinglePagePdf = async function (filename) {
72
78
  const existingPdfBytes = await fs.promises.readFile(filePath);
73
79
  const srcDoc = await PDFDocument.load(existingPdfBytes);
74
80
 
81
+ // Cache page count while we have the document loaded (avoids double read)
82
+ const totalPages = srcDoc.getPageCount();
83
+ pageCountCache.set(filename, { count: totalPages, createdAt: Date.now() });
84
+
75
85
  const newDoc = await PDFDocument.create();
76
86
  const [copiedPage] = await newDoc.copyPages(srcDoc, [0]);
77
87
  newDoc.addPage(copiedPage);
@@ -87,3 +97,26 @@ PdfHandler.getSinglePagePdf = async function (filename) {
87
97
 
88
98
  return buffer;
89
99
  };
100
+
101
+ PdfHandler.getTotalPages = async function (filename) {
102
+ const cached = pageCountCache.get(filename);
103
+ if (cached && (Date.now() - cached.createdAt < CACHE_TTL)) {
104
+ return cached.count;
105
+ }
106
+
107
+ const filePath = PdfHandler.resolveFilePath(filename);
108
+ if (!filePath) {
109
+ throw new Error('Invalid filename');
110
+ }
111
+
112
+ if (!fs.existsSync(filePath)) {
113
+ throw new Error('File not found');
114
+ }
115
+
116
+ const pdfBytes = await fs.promises.readFile(filePath);
117
+ const srcDoc = await PDFDocument.load(pdfBytes);
118
+ const count = srcDoc.getPageCount();
119
+
120
+ pageCountCache.set(filename, { count, createdAt: Date.now() });
121
+ return count;
122
+ };
package/library.js CHANGED
@@ -61,7 +61,7 @@ plugin.init = async (params) => {
61
61
  routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
62
62
 
63
63
  // Viewer page route (fullscreen Mozilla PDF.js viewer, guests allowed)
64
- router.get('/plugins/pdf-secure/viewer', (req, res) => {
64
+ router.get('/plugins/pdf-secure/viewer', async (req, res) => {
65
65
  const { file } = req.query;
66
66
  if (!file) {
67
67
  return res.status(400).send('Missing file parameter');
@@ -78,10 +78,20 @@ plugin.init = async (params) => {
78
78
  return res.status(500).send('Viewer not available');
79
79
  }
80
80
 
81
- // Generate nonce + key HERE (in viewer route)
82
- // This way the key is ONLY embedded in HTML, never in a separate API response
83
- const isPremium = true;
84
- const nonceData = nonceStore.generate(req.uid || 0, safeName, isPremium);
81
+ // Check if user is Premium (admins/global mods always premium)
82
+ let isPremium = false;
83
+ if (req.uid) {
84
+ const [isAdmin, isGlobalMod, isPremiumMember] = await Promise.all([
85
+ groups.isMember(req.uid, 'administrators'),
86
+ groups.isMember(req.uid, 'Global Moderators'),
87
+ groups.isMember(req.uid, 'Premium'),
88
+ ]);
89
+ isPremium = isAdmin || isGlobalMod || isPremiumMember;
90
+ }
91
+
92
+ // Always send full PDF (client-side premium gating)
93
+ // isPremium flag is sent to client for UI control only
94
+ const nonceData = nonceStore.generate(req.uid || 0, safeName, true);
85
95
 
86
96
  // Serve the viewer template with comprehensive security headers
87
97
  res.set({
@@ -110,7 +120,8 @@ plugin.init = async (params) => {
110
120
  relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
111
121
  csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
112
122
  nonce: ${JSON.stringify(nonceData.nonce)},
113
- dk: ${JSON.stringify(nonceData.xorKey)}
123
+ dk: ${JSON.stringify(nonceData.xorKey)},
124
+ isPremium: ${JSON.stringify(isPremium)}
114
125
  };
115
126
  </script>
116
127
  </head>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.19",
3
+ "version": "1.2.20",
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": {
@@ -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
  }
@@ -1572,6 +1572,106 @@
1572
1572
  touch-action: manipulation;
1573
1573
  }
1574
1574
  }
1575
+
1576
+ /* Premium Lock Overlay */
1577
+ #premiumLockOverlay {
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ align-items: center;
1581
+ justify-content: center;
1582
+ padding: 60px 20px;
1583
+ text-align: center;
1584
+ background: linear-gradient(180deg, rgba(31,31,31,0.95) 0%, rgba(20,20,20,0.98) 100%);
1585
+ border-top: 2px solid rgba(255, 215, 0, 0.3);
1586
+ min-height: 400px;
1587
+ margin: 0 auto;
1588
+ max-width: 100%;
1589
+ }
1590
+
1591
+ .premium-lock-icon {
1592
+ margin-bottom: 20px;
1593
+ opacity: 0.9;
1594
+ }
1595
+
1596
+ .premium-lock-pages {
1597
+ font-size: 22px;
1598
+ font-weight: 600;
1599
+ color: #ffd700;
1600
+ margin-bottom: 12px;
1601
+ }
1602
+
1603
+ .premium-lock-message {
1604
+ font-size: 16px;
1605
+ color: #a0a0a0;
1606
+ margin-bottom: 28px;
1607
+ max-width: 400px;
1608
+ line-height: 1.5;
1609
+ }
1610
+
1611
+ .premium-lock-button {
1612
+ display: inline-block;
1613
+ padding: 14px 40px;
1614
+ background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
1615
+ color: #1a1a1a;
1616
+ font-size: 16px;
1617
+ font-weight: 700;
1618
+ border-radius: 8px;
1619
+ text-decoration: none;
1620
+ transition: transform 0.2s, box-shadow 0.2s;
1621
+ box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
1622
+ }
1623
+
1624
+ .premium-lock-button:hover {
1625
+ transform: translateY(-2px);
1626
+ box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
1627
+ }
1628
+
1629
+ .premium-lock-secondary {
1630
+ margin-top: 20px;
1631
+ font-size: 14px;
1632
+ color: #888;
1633
+ font-style: italic;
1634
+ }
1635
+
1636
+ /* Locked Thumbnails */
1637
+ .thumbnail.locked {
1638
+ opacity: 0.4;
1639
+ cursor: not-allowed;
1640
+ position: relative;
1641
+ }
1642
+
1643
+ .thumbnail.locked:hover {
1644
+ border-color: rgba(255, 215, 0, 0.4);
1645
+ }
1646
+
1647
+ .thumbnail-lock {
1648
+ position: absolute;
1649
+ top: 50%;
1650
+ left: 50%;
1651
+ transform: translate(-50%, -50%);
1652
+ font-size: 20px;
1653
+ z-index: 2;
1654
+ }
1655
+
1656
+ @media (max-width: 768px) {
1657
+ #premiumLockOverlay {
1658
+ padding: 40px 16px;
1659
+ min-height: 300px;
1660
+ }
1661
+
1662
+ .premium-lock-pages {
1663
+ font-size: 18px;
1664
+ }
1665
+
1666
+ .premium-lock-message {
1667
+ font-size: 14px;
1668
+ }
1669
+
1670
+ .premium-lock-button {
1671
+ padding: 12px 32px;
1672
+ font-size: 14px;
1673
+ }
1674
+ }
1575
1675
  </style>
1576
1676
  </head>
1577
1677
 
@@ -1954,6 +2054,10 @@
1954
2054
  (function () {
1955
2055
  'use strict';
1956
2056
 
2057
+ // Security: Capture config early and delete from window immediately
2058
+ const _cfg = window.PDF_SECURE_CONFIG ? Object.assign({}, window.PDF_SECURE_CONFIG) : null;
2059
+ delete window.PDF_SECURE_CONFIG;
2060
+
1957
2061
  // ============================================
1958
2062
  // CANVAS EXPORT PROTECTION
1959
2063
  // Block toDataURL/toBlob for PDF render canvas only
@@ -1995,6 +2099,9 @@
1995
2099
  let currentPath = null;
1996
2100
  let currentDrawingPage = null;
1997
2101
 
2102
+ // Premium info (saved before config deletion for UI use)
2103
+ let premiumInfo = null;
2104
+
1998
2105
  // RAF throttle for smooth drawing performance
1999
2106
  let pathSegments = []; // Buffer path segments
2000
2107
  let drawRAF = null; // requestAnimationFrame ID
@@ -2044,8 +2151,7 @@
2044
2151
  firstPageRendered = true;
2045
2152
  // Notify parent that PDF is fully rendered (for queue system)
2046
2153
  if (window.parent && window.parent !== window) {
2047
- const config = window.PDF_SECURE_CONFIG || {};
2048
- window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, window.location.origin);
2154
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: (_cfg || {}).filename }, window.location.origin);
2049
2155
  console.log('[PDF-Secure] First page rendered, notifying parent');
2050
2156
  }
2051
2157
  }
@@ -2118,14 +2224,90 @@
2118
2224
  return data.buffer;
2119
2225
  }
2120
2226
 
2227
+ function showPremiumLockOverlay(totalPages) {
2228
+ var viewerEl = document.getElementById('viewer');
2229
+ if (!viewerEl) return;
2230
+
2231
+ var overlay = document.createElement('div');
2232
+ overlay.id = 'premiumLockOverlay';
2233
+ overlay.innerHTML = '\
2234
+ <div class="premium-lock-icon">\
2235
+ <svg viewBox="0 0 24 24" width="64" height="64" fill="#ffd700">\
2236
+ <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"/>\
2237
+ </svg>\
2238
+ </div>\
2239
+ <div class="premium-lock-pages">' + (totalPages - 1) + ' sayfa daha kilitli</div>\
2240
+ <div class="premium-lock-message">Bu icerigi goruntulemeye devam etmek icin Premium uyelik gereklidir.</div>\
2241
+ <a href="https://forumtest.ieu.app/premium" target="_blank" class="premium-lock-button">Premium Satin Al</a>\
2242
+ <div class="premium-lock-secondary">Materyal yukleyerek de Premium olabilirsiniz!</div>';
2243
+
2244
+ viewerEl.appendChild(overlay);
2245
+ }
2246
+
2247
+ // ============================================
2248
+ // PREMIUM INTEGRITY: Periodic Check (2s interval)
2249
+ // ============================================
2250
+ function startPeriodicCheck() {
2251
+ setInterval(function () {
2252
+ if (!premiumInfo || premiumInfo.isPremium) return;
2253
+ var pages = document.querySelectorAll('#viewer .page');
2254
+ pages.forEach(function (page, idx) {
2255
+ if (idx > 0 && page.style.display !== 'none') {
2256
+ page.style.display = 'none';
2257
+ }
2258
+ });
2259
+ if (!document.getElementById('premiumLockOverlay')) {
2260
+ showPremiumLockOverlay(premiumInfo.totalPages);
2261
+ }
2262
+ if (pdfViewer && pdfViewer.currentPageNumber > 1) {
2263
+ pdfViewer.currentPageNumber = 1;
2264
+ }
2265
+ }, 2000);
2266
+ }
2267
+
2268
+ // ============================================
2269
+ // PREMIUM INTEGRITY: MutationObserver Anti-Tampering
2270
+ // ============================================
2271
+ function setupAntiTampering() {
2272
+ var viewerEl = document.getElementById('viewer');
2273
+ if (!viewerEl) return;
2274
+
2275
+ new MutationObserver(function (mutations) {
2276
+ for (var i = 0; i < mutations.length; i++) {
2277
+ var removed = mutations[i].removedNodes;
2278
+ for (var j = 0; j < removed.length; j++) {
2279
+ if (removed[j].id === 'premiumLockOverlay') {
2280
+ showPremiumLockOverlay(premiumInfo.totalPages);
2281
+ return;
2282
+ }
2283
+ }
2284
+ }
2285
+ }).observe(viewerEl, { childList: true });
2286
+
2287
+ new MutationObserver(function (mutations) {
2288
+ for (var i = 0; i < mutations.length; i++) {
2289
+ var m = mutations[i];
2290
+ if (m.type === 'attributes' && m.attributeName === 'style') {
2291
+ var target = m.target;
2292
+ if (target.classList && target.classList.contains('page')) {
2293
+ var pageNum = parseInt(target.dataset.pageNumber || '0', 10);
2294
+ if (pageNum > 1 && target.style.display !== 'none') {
2295
+ target.style.display = 'none';
2296
+ }
2297
+ }
2298
+ }
2299
+ }
2300
+ }).observe(viewerEl, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
2301
+ }
2302
+
2121
2303
  // Auto-load PDF if config is present (injected by NodeBB plugin)
2122
2304
  async function autoLoadSecurePDF() {
2123
- if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
2305
+ if (!_cfg || !_cfg.filename) {
2124
2306
  console.log('[PDF-Secure] No config found, showing file picker');
2125
2307
  return;
2126
2308
  }
2127
2309
 
2128
- const config = window.PDF_SECURE_CONFIG;
2310
+ const config = _cfg;
2129
2311
  console.log('[PDF-Secure] Auto-loading:', config.filename);
2130
2312
 
2131
2313
  // Show loading state
@@ -2197,8 +2379,8 @@
2197
2379
  pdfBuffer = encodedBuffer;
2198
2380
  }
2199
2381
 
2200
- // Send buffer to parent for caching
2201
- if (window.parent && window.parent !== window) {
2382
+ // Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
2383
+ if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
2202
2384
  // Clone buffer for parent (we keep original)
2203
2385
  const bufferCopy = pdfBuffer.slice(0);
2204
2386
  window.parent.postMessage({
@@ -2214,14 +2396,21 @@
2214
2396
  // Step 4: Load into viewer
2215
2397
  await loadPDFFromBuffer(pdfBuffer);
2216
2398
 
2399
+ // Premium Gate: Client-side page restriction for non-premium users
2400
+ if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
2401
+ premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
2402
+ showPremiumLockOverlay(pdfDoc.numPages);
2403
+ startPeriodicCheck();
2404
+ setupAntiTampering();
2405
+ } else {
2406
+ premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
2407
+ }
2408
+
2217
2409
  // Step 5: Moved to pagerendered event for proper timing
2218
2410
 
2219
2411
  // Step 6: Security - clear references to prevent extraction
2220
2412
  pdfBuffer = null;
2221
2413
 
2222
- // Security: Delete config containing sensitive data (nonce, key)
2223
- delete window.PDF_SECURE_CONFIG;
2224
-
2225
2414
  // Security: Remove PDF.js globals to prevent console manipulation
2226
2415
  delete window.pdfjsLib;
2227
2416
  delete window.pdfjsViewer;
@@ -2245,10 +2434,9 @@
2245
2434
 
2246
2435
  // Notify parent of error (prevents 60s queue hang)
2247
2436
  if (window.parent && window.parent !== window) {
2248
- const config = window.PDF_SECURE_CONFIG || {};
2249
2437
  window.parent.postMessage({
2250
2438
  type: 'pdf-secure-ready',
2251
- filename: config.filename,
2439
+ filename: (_cfg || {}).filename,
2252
2440
  error: err.message
2253
2441
  }, window.location.origin);
2254
2442
  }