nodebb-plugin-pdf-secure 1.2.29 → 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.
- package/image.png +0 -0
- package/lib/controllers.js +21 -24
- package/lib/nonce-store.js +12 -16
- package/library.js +56 -47
- package/package.json +1 -1
- package/static/lib/pdf-secure (1).pdf +0 -0
- package/static/viewer-app.js +32 -25
- package/static/viewer.html +127 -28
- package/static/lib/viewer.js +0 -184
- package/static/viewer-yedek.html +0 -4548
package/image.png
ADDED
|
Binary file
|
package/lib/controllers.js
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const nonceStore = require('./nonce-store');
|
|
4
5
|
const pdfHandler = require('./pdf-handler');
|
|
5
6
|
|
|
6
7
|
const Controllers = module.exports;
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const fullEncryptLen = Math.min(10240, data.length);
|
|
16
|
-
for (let i = 0; i < fullEncryptLen; i++) {
|
|
17
|
-
data[i] = data[i] ^ xorKey[i % keyLen];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Encrypt every 50th byte after that
|
|
21
|
-
for (let i = fullEncryptLen; i < data.length; i += 50) {
|
|
22
|
-
data[i] = data[i] ^ xorKey[i % keyLen];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return data;
|
|
9
|
+
// AES-256-GCM encryption - replaces weak XOR obfuscation
|
|
10
|
+
function aesGcmEncrypt(buffer, key, iv) {
|
|
11
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
12
|
+
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
13
|
+
const authTag = cipher.getAuthTag(); // 16 bytes
|
|
14
|
+
// Format: [authTag (16 bytes)] [ciphertext]
|
|
15
|
+
return Buffer.concat([authTag, encrypted]);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
Controllers.renderAdminPage = function (req, res) {
|
|
@@ -32,12 +22,17 @@ Controllers.renderAdminPage = function (req, res) {
|
|
|
32
22
|
};
|
|
33
23
|
|
|
34
24
|
Controllers.servePdfBinary = async function (req, res) {
|
|
25
|
+
// Authentication gate - require logged-in user
|
|
26
|
+
if (!req.uid) {
|
|
27
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
35
30
|
const { nonce } = req.query;
|
|
36
31
|
if (!nonce) {
|
|
37
32
|
return res.status(400).json({ error: 'Missing nonce' });
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
const uid = req.uid
|
|
35
|
+
const uid = req.uid;
|
|
41
36
|
|
|
42
37
|
const data = nonceStore.validate(nonce, uid);
|
|
43
38
|
if (!data) {
|
|
@@ -45,14 +40,16 @@ Controllers.servePdfBinary = async function (req, res) {
|
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
try {
|
|
48
|
-
//
|
|
49
|
-
const pdfBuffer =
|
|
43
|
+
// Server-side premium gate: non-premium users only get first page
|
|
44
|
+
const pdfBuffer = data.isPremium
|
|
45
|
+
? await pdfHandler.getFullPdf(data.file)
|
|
46
|
+
: await pdfHandler.getSinglePagePdf(data.file);
|
|
50
47
|
|
|
51
|
-
// Apply
|
|
52
|
-
const encodedBuffer =
|
|
48
|
+
// Apply AES-256-GCM encryption
|
|
49
|
+
const encodedBuffer = aesGcmEncrypt(pdfBuffer, data.encKey, data.encIv);
|
|
53
50
|
|
|
54
51
|
res.set({
|
|
55
|
-
'Content-Type': '
|
|
52
|
+
'Content-Type': 'application/octet-stream',
|
|
56
53
|
'Cache-Control': 'no-store, no-cache, must-revalidate, private',
|
|
57
54
|
'X-Content-Type-Options': 'nosniff',
|
|
58
55
|
'Content-Disposition': 'inline',
|
package/lib/nonce-store.js
CHANGED
|
@@ -17,40 +17,36 @@ setInterval(() => {
|
|
|
17
17
|
}
|
|
18
18
|
}, CLEANUP_INTERVAL).unref();
|
|
19
19
|
|
|
20
|
-
// Generate
|
|
21
|
-
function
|
|
22
|
-
return
|
|
20
|
+
// Generate AES-256-GCM key (32 bytes) and IV (12 bytes)
|
|
21
|
+
function generateEncryptionParams() {
|
|
22
|
+
return {
|
|
23
|
+
key: crypto.randomBytes(32),
|
|
24
|
+
iv: crypto.randomBytes(12),
|
|
25
|
+
};
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const NonceStore = module.exports;
|
|
26
29
|
|
|
27
30
|
NonceStore.generate = function (uid, file, isPremium) {
|
|
28
31
|
const nonce = uuidv4();
|
|
29
|
-
const
|
|
32
|
+
const { key, iv } = generateEncryptionParams();
|
|
30
33
|
|
|
31
34
|
store.set(nonce, {
|
|
32
35
|
uid: uid,
|
|
33
36
|
file: file,
|
|
34
37
|
isPremium: isPremium,
|
|
35
|
-
|
|
38
|
+
encKey: key, // AES-256-GCM key
|
|
39
|
+
encIv: iv, // GCM initialization vector
|
|
36
40
|
createdAt: Date.now(),
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
return {
|
|
40
44
|
nonce: nonce,
|
|
41
|
-
|
|
45
|
+
dk: key.toString('base64'), // decryption key for viewer
|
|
46
|
+
iv: iv.toString('base64'), // IV for viewer
|
|
42
47
|
};
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
// Get key without consuming nonce (for viewer injection)
|
|
46
|
-
NonceStore.getKey = function (nonce) {
|
|
47
|
-
const data = store.get(nonce);
|
|
48
|
-
if (!data) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
return data.xorKey.toString('base64');
|
|
52
|
-
};
|
|
53
|
-
|
|
54
50
|
NonceStore.validate = function (nonce, uid) {
|
|
55
51
|
const data = store.get(nonce);
|
|
56
52
|
if (!data) {
|
|
@@ -70,5 +66,5 @@ NonceStore.validate = function (nonce, uid) {
|
|
|
70
66
|
return null;
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
return data; //
|
|
69
|
+
return data; // Includes encKey and encIv for AES-256-GCM
|
|
74
70
|
};
|
package/library.js
CHANGED
|
@@ -55,14 +55,19 @@ plugin.init = async (params) => {
|
|
|
55
55
|
next();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
// PDF binary endpoint (nonce-validated,
|
|
58
|
+
// PDF binary endpoint (nonce-validated, authentication required)
|
|
59
59
|
router.get('/api/v3/plugins/pdf-secure/pdf-data', controllers.servePdfBinary);
|
|
60
60
|
|
|
61
61
|
// Admin page route
|
|
62
62
|
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
|
|
63
63
|
|
|
64
|
-
// Viewer page route (fullscreen Mozilla PDF.js viewer,
|
|
64
|
+
// Viewer page route (fullscreen Mozilla PDF.js viewer, authentication required)
|
|
65
65
|
router.get('/plugins/pdf-secure/viewer', async (req, res) => {
|
|
66
|
+
// Authentication gate - require logged-in user
|
|
67
|
+
if (!req.uid) {
|
|
68
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
const { file } = req.query;
|
|
67
72
|
if (!file) {
|
|
68
73
|
return res.status(400).send('Missing file parameter');
|
|
@@ -79,10 +84,10 @@ plugin.init = async (params) => {
|
|
|
79
84
|
return res.status(500).send('Viewer not available');
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
try {
|
|
88
|
+
// Check if user is Premium or Lite (admins/global mods always premium)
|
|
89
|
+
let isPremium = false;
|
|
90
|
+
let isLite = false;
|
|
86
91
|
const [isAdmin, isGlobalMod, isPremiumMember, isVipMember, isLiteMember] = await Promise.all([
|
|
87
92
|
groups.isMember(req.uid, 'administrators'),
|
|
88
93
|
groups.isMember(req.uid, 'Global Moderators'),
|
|
@@ -93,48 +98,52 @@ plugin.init = async (params) => {
|
|
|
93
98
|
isPremium = isAdmin || isGlobalMod || isPremiumMember || isVipMember;
|
|
94
99
|
// Lite: full PDF access but restricted UI (no annotations, sidebar, etc.)
|
|
95
100
|
isLite = !isPremium && isLiteMember;
|
|
96
|
-
}
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
102
|
+
// Lite users get full PDF like premium (for nonce/server-side PDF data)
|
|
103
|
+
const hasFullAccess = isPremium || isLite;
|
|
104
|
+
const nonceData = nonceStore.generate(req.uid, safeName, hasFullAccess);
|
|
105
|
+
|
|
106
|
+
// Serve the viewer template with comprehensive security headers
|
|
107
|
+
res.set({
|
|
108
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
109
|
+
'X-Content-Type-Options': 'nosniff',
|
|
110
|
+
'X-XSS-Protection': '1; mode=block',
|
|
111
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, private, max-age=0',
|
|
112
|
+
'Pragma': 'no-cache',
|
|
113
|
+
'Expires': '0',
|
|
114
|
+
'Referrer-Policy': 'no-referrer',
|
|
115
|
+
'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
|
|
116
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'self'",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Inject the filename, nonce, and key into the cached viewer
|
|
120
|
+
// Key is embedded in HTML - NOT visible in any network API response!
|
|
121
|
+
const injectedHtml = viewerHtmlCache
|
|
122
|
+
.replace('</head>', `
|
|
123
|
+
<style>
|
|
124
|
+
/* Hide upload overlay since PDF will auto-load */
|
|
125
|
+
#uploadOverlay { display: none !important; }
|
|
126
|
+
</style>
|
|
127
|
+
<script>
|
|
128
|
+
window.PDF_SECURE_CONFIG = {
|
|
129
|
+
filename: ${JSON.stringify(safeName)},
|
|
130
|
+
relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
|
|
131
|
+
csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
|
|
132
|
+
nonce: ${JSON.stringify(nonceData.nonce)},
|
|
133
|
+
dk: ${JSON.stringify(nonceData.dk)},
|
|
134
|
+
iv: ${JSON.stringify(nonceData.iv)},
|
|
135
|
+
isPremium: ${JSON.stringify(isPremium)},
|
|
136
|
+
isLite: ${JSON.stringify(isLite)},
|
|
137
|
+
uid: ${JSON.stringify(req.uid)}
|
|
138
|
+
};
|
|
139
|
+
</script>
|
|
140
|
+
</head>`);
|
|
141
|
+
|
|
142
|
+
res.type('html').send(injectedHtml);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('[PDF-Secure] Viewer route error:', err.message);
|
|
145
|
+
return res.status(500).send('Internal error');
|
|
146
|
+
}
|
|
138
147
|
});
|
|
139
148
|
};
|
|
140
149
|
|
package/package.json
CHANGED
|
Binary file
|
package/static/viewer-app.js
CHANGED
|
@@ -151,24 +151,30 @@
|
|
|
151
151
|
// Thumbnails will be generated on-demand when sidebar opens
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
154
|
+
// AES-256-GCM decoder using Web Crypto API
|
|
155
|
+
async function aesGcmDecode(encodedData, keyBase64, ivBase64) {
|
|
156
|
+
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
|
157
|
+
const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
|
|
158
|
+
const encData = new Uint8Array(encodedData);
|
|
159
|
+
|
|
160
|
+
// Format: [authTag (16 bytes)] [ciphertext]
|
|
161
|
+
const authTag = encData.slice(0, 16);
|
|
162
|
+
const ciphertext = encData.slice(16);
|
|
163
|
+
|
|
164
|
+
// Web Crypto expects ciphertext + authTag concatenated
|
|
165
|
+
const combined = new Uint8Array(ciphertext.length + authTag.length);
|
|
166
|
+
combined.set(ciphertext);
|
|
167
|
+
combined.set(authTag, ciphertext.length);
|
|
168
|
+
|
|
169
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
170
|
+
'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return crypto.subtle.decrypt(
|
|
174
|
+
{ name: 'AES-GCM', iv: iv },
|
|
175
|
+
cryptoKey,
|
|
176
|
+
combined
|
|
177
|
+
);
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
function showPremiumLockOverlay(totalPages) {
|
|
@@ -373,7 +379,8 @@
|
|
|
373
379
|
if (!pdfBuffer) {
|
|
374
380
|
// Nonce and key are embedded in HTML config (not fetched from API)
|
|
375
381
|
const nonce = config.nonce;
|
|
376
|
-
const
|
|
382
|
+
const decryptKey = config.dk;
|
|
383
|
+
const decryptIv = config.iv;
|
|
377
384
|
|
|
378
385
|
// Fetch encrypted PDF binary
|
|
379
386
|
const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
|
|
@@ -386,10 +393,10 @@
|
|
|
386
393
|
const encodedBuffer = await pdfRes.arrayBuffer();
|
|
387
394
|
console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
|
|
388
395
|
|
|
389
|
-
//
|
|
390
|
-
if (
|
|
391
|
-
console.log('[PDF-Secure]
|
|
392
|
-
pdfBuffer =
|
|
396
|
+
// Decrypt AES-256-GCM encrypted data
|
|
397
|
+
if (decryptKey && decryptIv) {
|
|
398
|
+
console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
|
|
399
|
+
pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
|
|
393
400
|
} else {
|
|
394
401
|
pdfBuffer = encodedBuffer;
|
|
395
402
|
}
|
|
@@ -467,7 +474,7 @@
|
|
|
467
474
|
<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"/>
|
|
468
475
|
</svg>
|
|
469
476
|
<h2>Hata</h2>
|
|
470
|
-
<p>${err.message}</p>
|
|
477
|
+
<p>${err.message.replace(/[<>&"']/g, c => ({'<':'<','>':'>','&':'&','"':'"',"'":'''}[c]))}</p>
|
|
471
478
|
`;
|
|
472
479
|
}
|
|
473
480
|
}
|
|
@@ -2658,7 +2665,7 @@
|
|
|
2658
2665
|
function toggleFullscreen() {
|
|
2659
2666
|
if (window.self !== window.top) {
|
|
2660
2667
|
// Inside iframe: ask parent to handle fullscreen
|
|
2661
|
-
window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' },
|
|
2668
|
+
window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' }, window.location.origin);
|
|
2662
2669
|
} else {
|
|
2663
2670
|
// Standalone mode: use native fullscreen
|
|
2664
2671
|
if (document.fullscreenElement) {
|
package/static/viewer.html
CHANGED
|
@@ -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
|
-
//
|
|
2283
|
-
function
|
|
2284
|
-
const
|
|
2285
|
-
const
|
|
2286
|
-
const
|
|
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
|
-
//
|
|
2289
|
-
const
|
|
2290
|
-
|
|
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
|
-
//
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -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="
|
|
2325
|
-
|
|
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
|
|
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
|
-
//
|
|
2534
|
-
if (
|
|
2535
|
-
console.log('[PDF-Secure]
|
|
2536
|
-
pdfBuffer =
|
|
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 => ({'<':'<','>':'>','&':'&','"':'"',"'":'''}[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>
|