nodebb-plugin-pdf-secure 1.2.19 → 1.2.21
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/docs/plans/2026-02-24-premium-gate-design.md +52 -0
- package/docs/plans/2026-02-24-premium-gate.md +323 -0
- package/lib/pdf-handler.js +33 -0
- package/library.js +17 -6
- package/package.json +1 -1
- package/static/image.png +0 -0
- package/static/viewer-app.js +160 -15
- package/static/viewer.html +237 -13
|
@@ -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
|
+
```
|
package/lib/pdf-handler.js
CHANGED
|
@@ -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
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
package/static/image.png
ADDED
|
Binary file
|
package/static/viewer-app.js
CHANGED
|
@@ -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
|
-
|
|
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 (!
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
435
|
+
if (isLocked) {
|
|
436
|
+
thumb.innerHTML = `<div class="thumbnailNum">${i}</div><div class="thumbnail-lock">🔒</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
|
-
|
|
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
|
}
|
package/static/viewer.html
CHANGED
|
@@ -1572,6 +1572,107 @@
|
|
|
1572
1572
|
touch-action: manipulation;
|
|
1573
1573
|
}
|
|
1574
1574
|
}
|
|
1575
|
+
|
|
1576
|
+
/* Per-page lock overlay - her kilitli sayfa icin */
|
|
1577
|
+
.page-lock-overlay {
|
|
1578
|
+
position: absolute;
|
|
1579
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
1580
|
+
display: flex;
|
|
1581
|
+
flex-direction: column;
|
|
1582
|
+
align-items: center;
|
|
1583
|
+
justify-content: center;
|
|
1584
|
+
background: linear-gradient(180deg, rgba(31,31,31,0.95) 0%, rgba(20,20,20,0.98) 100%);
|
|
1585
|
+
z-index: 5;
|
|
1586
|
+
text-align: center;
|
|
1587
|
+
padding: 20px;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.page-lock-icon {
|
|
1591
|
+
margin-bottom: 16px;
|
|
1592
|
+
opacity: 0.9;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.page-lock-title {
|
|
1596
|
+
font-size: 20px;
|
|
1597
|
+
font-weight: 600;
|
|
1598
|
+
color: #ffd700;
|
|
1599
|
+
margin-bottom: 10px;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.page-lock-message {
|
|
1603
|
+
font-size: 14px;
|
|
1604
|
+
color: #a0a0a0;
|
|
1605
|
+
margin-bottom: 24px;
|
|
1606
|
+
max-width: 320px;
|
|
1607
|
+
line-height: 1.5;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.page-lock-button {
|
|
1611
|
+
display: inline-block;
|
|
1612
|
+
padding: 12px 36px;
|
|
1613
|
+
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
|
1614
|
+
color: #1a1a1a;
|
|
1615
|
+
font-size: 15px;
|
|
1616
|
+
font-weight: 700;
|
|
1617
|
+
border-radius: 8px;
|
|
1618
|
+
text-decoration: none;
|
|
1619
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
1620
|
+
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.page-lock-button:hover {
|
|
1624
|
+
transform: translateY(-2px);
|
|
1625
|
+
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.page-lock-secondary {
|
|
1629
|
+
margin-top: 16px;
|
|
1630
|
+
font-size: 13px;
|
|
1631
|
+
color: #888;
|
|
1632
|
+
font-style: italic;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/* Blur locked page content - even if overlay removed, content unreadable */
|
|
1636
|
+
.page-locked-blur canvas,
|
|
1637
|
+
.page-locked-blur .textLayer,
|
|
1638
|
+
.page-locked-blur .annotationLayer {
|
|
1639
|
+
filter: blur(8px) !important;
|
|
1640
|
+
-webkit-filter: blur(8px) !important;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/* Locked Thumbnails */
|
|
1644
|
+
.thumbnail.locked {
|
|
1645
|
+
opacity: 0.5;
|
|
1646
|
+
position: relative;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.thumbnail.locked:hover {
|
|
1650
|
+
border-color: rgba(255, 215, 0, 0.4);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.thumbnail-lock {
|
|
1654
|
+
position: absolute;
|
|
1655
|
+
top: 50%;
|
|
1656
|
+
left: 50%;
|
|
1657
|
+
transform: translate(-50%, -50%);
|
|
1658
|
+
font-size: 20px;
|
|
1659
|
+
z-index: 2;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
@media (max-width: 768px) {
|
|
1663
|
+
.page-lock-title {
|
|
1664
|
+
font-size: 17px;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.page-lock-message {
|
|
1668
|
+
font-size: 13px;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
.page-lock-button {
|
|
1672
|
+
padding: 10px 28px;
|
|
1673
|
+
font-size: 13px;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1575
1676
|
</style>
|
|
1576
1677
|
</head>
|
|
1577
1678
|
|
|
@@ -1954,6 +2055,10 @@
|
|
|
1954
2055
|
(function () {
|
|
1955
2056
|
'use strict';
|
|
1956
2057
|
|
|
2058
|
+
// Security: Capture config early and delete from window immediately
|
|
2059
|
+
const _cfg = window.PDF_SECURE_CONFIG ? Object.assign({}, window.PDF_SECURE_CONFIG) : null;
|
|
2060
|
+
delete window.PDF_SECURE_CONFIG;
|
|
2061
|
+
|
|
1957
2062
|
// ============================================
|
|
1958
2063
|
// CANVAS EXPORT PROTECTION
|
|
1959
2064
|
// Block toDataURL/toBlob for PDF render canvas only
|
|
@@ -1995,6 +2100,9 @@
|
|
|
1995
2100
|
let currentPath = null;
|
|
1996
2101
|
let currentDrawingPage = null;
|
|
1997
2102
|
|
|
2103
|
+
// Premium info (saved before config deletion for UI use)
|
|
2104
|
+
let premiumInfo = null;
|
|
2105
|
+
|
|
1998
2106
|
// RAF throttle for smooth drawing performance
|
|
1999
2107
|
let pathSegments = []; // Buffer path segments
|
|
2000
2108
|
let drawRAF = null; // requestAnimationFrame ID
|
|
@@ -2044,8 +2152,7 @@
|
|
|
2044
2152
|
firstPageRendered = true;
|
|
2045
2153
|
// Notify parent that PDF is fully rendered (for queue system)
|
|
2046
2154
|
if (window.parent && window.parent !== window) {
|
|
2047
|
-
|
|
2048
|
-
window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, window.location.origin);
|
|
2155
|
+
window.parent.postMessage({ type: 'pdf-secure-ready', filename: (_cfg || {}).filename }, window.location.origin);
|
|
2049
2156
|
console.log('[PDF-Secure] First page rendered, notifying parent');
|
|
2050
2157
|
}
|
|
2051
2158
|
}
|
|
@@ -2118,14 +2225,117 @@
|
|
|
2118
2225
|
return data.buffer;
|
|
2119
2226
|
}
|
|
2120
2227
|
|
|
2228
|
+
function injectPageLock(pageEl) {
|
|
2229
|
+
if (!pageEl) return;
|
|
2230
|
+
// Add blur class to page content
|
|
2231
|
+
if (!pageEl.classList.contains('page-locked-blur')) {
|
|
2232
|
+
pageEl.classList.add('page-locked-blur');
|
|
2233
|
+
}
|
|
2234
|
+
// Don't duplicate overlay
|
|
2235
|
+
if (pageEl.querySelector('.page-lock-overlay')) return;
|
|
2236
|
+
var overlay = document.createElement('div');
|
|
2237
|
+
overlay.className = 'page-lock-overlay';
|
|
2238
|
+
overlay.innerHTML = '\
|
|
2239
|
+
<div class="page-lock-icon">\
|
|
2240
|
+
<svg viewBox="0 0 24 24" width="48" height="48" fill="#ffd700">\
|
|
2241
|
+
<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"/>\
|
|
2242
|
+
</svg>\
|
|
2243
|
+
</div>\
|
|
2244
|
+
<div class="page-lock-title">Kilitli</div>\
|
|
2245
|
+
<div class="page-lock-message">Bu sayfayi goruntulemeye devam etmek icin Premium uyelik gereklidir.</div>\
|
|
2246
|
+
<a href="https://forumtest.ieu.app/premium" target="_blank" class="page-lock-button">Premium Satin Al</a>\
|
|
2247
|
+
<div class="page-lock-secondary">Materyal yukleyerek de Premium olabilirsiniz!</div>';
|
|
2248
|
+
pageEl.style.position = 'relative';
|
|
2249
|
+
pageEl.appendChild(overlay);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function resetOverlayCSS(overlay) {
|
|
2253
|
+
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;z-index:5;opacity:1;visibility:visible;pointer-events:auto;';
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function applyPageLocks() {
|
|
2257
|
+
var pages = document.querySelectorAll('#viewer .page');
|
|
2258
|
+
pages.forEach(function (page) {
|
|
2259
|
+
var pageNum = parseInt(page.dataset.pageNumber || '0', 10);
|
|
2260
|
+
if (pageNum > 1) {
|
|
2261
|
+
// Ensure page is visible (undo any old display:none)
|
|
2262
|
+
if (page.style.display === 'none') {
|
|
2263
|
+
page.style.display = '';
|
|
2264
|
+
}
|
|
2265
|
+
injectPageLock(page);
|
|
2266
|
+
// CSS integrity: reset overlay styles in case of tampering
|
|
2267
|
+
var existing = page.querySelector('.page-lock-overlay');
|
|
2268
|
+
if (existing) resetOverlayCSS(existing);
|
|
2269
|
+
// Ensure blur class is present
|
|
2270
|
+
if (!page.classList.contains('page-locked-blur')) {
|
|
2271
|
+
page.classList.add('page-locked-blur');
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// ============================================
|
|
2278
|
+
// PREMIUM INTEGRITY: Periodic Check (2s interval)
|
|
2279
|
+
// ============================================
|
|
2280
|
+
function startPeriodicCheck() {
|
|
2281
|
+
setInterval(function () {
|
|
2282
|
+
if (!premiumInfo || premiumInfo.isPremium) return;
|
|
2283
|
+
applyPageLocks();
|
|
2284
|
+
}, 2000);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// ============================================
|
|
2288
|
+
// PREMIUM INTEGRITY: MutationObserver Anti-Tampering
|
|
2289
|
+
// ============================================
|
|
2290
|
+
function setupAntiTampering() {
|
|
2291
|
+
var viewerEl = document.getElementById('viewer');
|
|
2292
|
+
if (!viewerEl) return;
|
|
2293
|
+
|
|
2294
|
+
// Observer 1: Watch for lock overlay removal from any page
|
|
2295
|
+
new MutationObserver(function (mutations) {
|
|
2296
|
+
if (!premiumInfo || premiumInfo.isPremium) return;
|
|
2297
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
2298
|
+
var removed = mutations[i].removedNodes;
|
|
2299
|
+
for (var j = 0; j < removed.length; j++) {
|
|
2300
|
+
if (removed[j].classList && removed[j].classList.contains('page-lock-overlay')) {
|
|
2301
|
+
var pageEl = mutations[i].target;
|
|
2302
|
+
if (pageEl && pageEl.classList && pageEl.classList.contains('page')) {
|
|
2303
|
+
injectPageLock(pageEl);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}).observe(viewerEl, { childList: true, subtree: true });
|
|
2309
|
+
|
|
2310
|
+
// Observer 2: Watch for style/class tampering on overlays and pages
|
|
2311
|
+
new MutationObserver(function (mutations) {
|
|
2312
|
+
if (!premiumInfo || premiumInfo.isPremium) return;
|
|
2313
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
2314
|
+
var target = mutations[i].target;
|
|
2315
|
+
if (!target || !target.classList) continue;
|
|
2316
|
+
// Overlay CSS tampered - reset it
|
|
2317
|
+
if (target.classList.contains('page-lock-overlay')) {
|
|
2318
|
+
resetOverlayCSS(target);
|
|
2319
|
+
}
|
|
2320
|
+
// Blur class removed from locked page - re-add it
|
|
2321
|
+
if (target.classList.contains('page') && !target.classList.contains('page-locked-blur')) {
|
|
2322
|
+
var pageNum = parseInt(target.dataset.pageNumber || '0', 10);
|
|
2323
|
+
if (pageNum > 1) {
|
|
2324
|
+
target.classList.add('page-locked-blur');
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}).observe(viewerEl, { subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2121
2331
|
// Auto-load PDF if config is present (injected by NodeBB plugin)
|
|
2122
2332
|
async function autoLoadSecurePDF() {
|
|
2123
|
-
if (!
|
|
2333
|
+
if (!_cfg || !_cfg.filename) {
|
|
2124
2334
|
console.log('[PDF-Secure] No config found, showing file picker');
|
|
2125
2335
|
return;
|
|
2126
2336
|
}
|
|
2127
2337
|
|
|
2128
|
-
const config =
|
|
2338
|
+
const config = _cfg;
|
|
2129
2339
|
console.log('[PDF-Secure] Auto-loading:', config.filename);
|
|
2130
2340
|
|
|
2131
2341
|
// Show loading state
|
|
@@ -2197,8 +2407,8 @@
|
|
|
2197
2407
|
pdfBuffer = encodedBuffer;
|
|
2198
2408
|
}
|
|
2199
2409
|
|
|
2200
|
-
// Send buffer to parent for caching
|
|
2201
|
-
if (window.parent && window.parent !== window) {
|
|
2410
|
+
// Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
|
|
2411
|
+
if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
|
|
2202
2412
|
// Clone buffer for parent (we keep original)
|
|
2203
2413
|
const bufferCopy = pdfBuffer.slice(0);
|
|
2204
2414
|
window.parent.postMessage({
|
|
@@ -2214,14 +2424,21 @@
|
|
|
2214
2424
|
// Step 4: Load into viewer
|
|
2215
2425
|
await loadPDFFromBuffer(pdfBuffer);
|
|
2216
2426
|
|
|
2427
|
+
// Premium Gate: Client-side page restriction for non-premium users
|
|
2428
|
+
if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
|
|
2429
|
+
premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
|
|
2430
|
+
applyPageLocks();
|
|
2431
|
+
startPeriodicCheck();
|
|
2432
|
+
setupAntiTampering();
|
|
2433
|
+
} else {
|
|
2434
|
+
premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2217
2437
|
// Step 5: Moved to pagerendered event for proper timing
|
|
2218
2438
|
|
|
2219
2439
|
// Step 6: Security - clear references to prevent extraction
|
|
2220
2440
|
pdfBuffer = null;
|
|
2221
2441
|
|
|
2222
|
-
// Security: Delete config containing sensitive data (nonce, key)
|
|
2223
|
-
delete window.PDF_SECURE_CONFIG;
|
|
2224
|
-
|
|
2225
2442
|
// Security: Remove PDF.js globals to prevent console manipulation
|
|
2226
2443
|
delete window.pdfjsLib;
|
|
2227
2444
|
delete window.pdfjsViewer;
|
|
@@ -2245,10 +2462,9 @@
|
|
|
2245
2462
|
|
|
2246
2463
|
// Notify parent of error (prevents 60s queue hang)
|
|
2247
2464
|
if (window.parent && window.parent !== window) {
|
|
2248
|
-
const config = window.PDF_SECURE_CONFIG || {};
|
|
2249
2465
|
window.parent.postMessage({
|
|
2250
2466
|
type: 'pdf-secure-ready',
|
|
2251
|
-
filename:
|
|
2467
|
+
filename: (_cfg || {}).filename,
|
|
2252
2468
|
error: err.message
|
|
2253
2469
|
}, window.location.origin);
|
|
2254
2470
|
}
|
|
@@ -2289,9 +2505,11 @@
|
|
|
2289
2505
|
}).promise;
|
|
2290
2506
|
|
|
2291
2507
|
const thumb = document.createElement('div');
|
|
2292
|
-
|
|
2508
|
+
const isLocked = premiumInfo && !premiumInfo.isPremium && i > 1;
|
|
2509
|
+
thumb.className = 'thumbnail' + (i === 1 ? ' active' : '') + (isLocked ? ' locked' : '');
|
|
2293
2510
|
thumb.dataset.page = i;
|
|
2294
|
-
thumb.innerHTML = `<div class="thumbnailNum">${i}</div
|
|
2511
|
+
thumb.innerHTML = `<div class="thumbnailNum">${i}</div>` +
|
|
2512
|
+
(isLocked ? '<div class="thumbnail-lock">\u{1F512}</div>' : '');
|
|
2295
2513
|
thumb.insertBefore(canvas, thumb.firstChild);
|
|
2296
2514
|
|
|
2297
2515
|
thumb.onclick = () => {
|
|
@@ -2337,6 +2555,12 @@
|
|
|
2337
2555
|
eventBus.on('pagerendered', (evt) => {
|
|
2338
2556
|
injectAnnotationLayer(evt.pageNumber);
|
|
2339
2557
|
|
|
2558
|
+
// Re-inject lock overlay if this is a locked page (PDF.js re-render removes it)
|
|
2559
|
+
if (premiumInfo && !premiumInfo.isPremium && evt.pageNumber > 1) {
|
|
2560
|
+
var pageEl = document.querySelector('#viewer .page[data-page-number="' + evt.pageNumber + '"]');
|
|
2561
|
+
if (pageEl) injectPageLock(pageEl);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2340
2564
|
// Rotation is handled natively by PDF.js via pagesRotation
|
|
2341
2565
|
});
|
|
2342
2566
|
|