nodebb-plugin-pdf-secure2 1.4.2 → 1.5.0
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/.claude/settings.local.json +6 -1
- package/lib/controllers.js +33 -8
- package/lib/gemini-chat.js +452 -409
- package/lib/nonce-store.js +4 -4
- package/lib/pdf-handler.js +0 -1
- package/lib/topic-access.js +96 -0
- package/library.js +65 -5
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/main.js +3 -74
- package/static/templates/admin/plugins/pdf-secure.tpl +21 -0
- package/static/viewer-app.js +18 -62
- package/static/viewer.html +696 -70
package/lib/nonce-store.js
CHANGED
|
@@ -53,18 +53,18 @@ NonceStore.validate = function (nonce, uid) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
store.delete(nonce);
|
|
58
|
-
|
|
59
|
-
// Check UID match
|
|
56
|
+
// Validate BEFORE deleting — prevents DoS via nonce consumption with wrong UID
|
|
60
57
|
if (data.uid !== uid) {
|
|
61
58
|
return null;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
// Check TTL
|
|
65
62
|
if (Date.now() - data.createdAt > NONCE_TTL) {
|
|
63
|
+
store.delete(nonce); // Expired, clean up
|
|
66
64
|
return null;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
// All checks passed — delete now (single-use)
|
|
68
|
+
store.delete(nonce);
|
|
69
69
|
return data; // Includes encKey and encIv for AES-256-GCM
|
|
70
70
|
};
|
package/lib/pdf-handler.js
CHANGED
|
@@ -36,7 +36,6 @@ PdfHandler.resolveFilePath = function (filename) {
|
|
|
36
36
|
const uploadPath = nconf.get('upload_path') || path.join(nconf.get('base_dir'), 'public', 'uploads');
|
|
37
37
|
const filePath = path.join(uploadPath, 'files', safeName);
|
|
38
38
|
|
|
39
|
-
// Verify the resolved path is still within the upload directory
|
|
40
39
|
const resolvedPath = path.resolve(filePath);
|
|
41
40
|
const resolvedUploadDir = path.resolve(path.join(uploadPath, 'files'));
|
|
42
41
|
if (!resolvedPath.startsWith(resolvedUploadDir)) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const privileges = require.main.require('./src/privileges');
|
|
4
|
+
const topics = require.main.require('./src/topics');
|
|
5
|
+
const posts = require.main.require('./src/posts');
|
|
6
|
+
const groups = require.main.require('./src/groups');
|
|
7
|
+
const db = require.main.require('./src/database');
|
|
8
|
+
|
|
9
|
+
const TopicAccess = module.exports;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate that a user has access to a PDF through a specific topic.
|
|
13
|
+
* Checks: 1) User can read the topic, 2) The PDF filename exists in the topic's posts.
|
|
14
|
+
* Admin/Global Moderators bypass all checks.
|
|
15
|
+
*
|
|
16
|
+
* @param {number} uid - User ID
|
|
17
|
+
* @param {number|string} tid - Topic ID
|
|
18
|
+
* @param {string} filename - Sanitized PDF filename (basename only)
|
|
19
|
+
* @returns {Promise<{allowed: boolean, reason?: string}>}
|
|
20
|
+
*/
|
|
21
|
+
TopicAccess.validate = async function (uid, tid, filename) {
|
|
22
|
+
// Require valid tid
|
|
23
|
+
tid = parseInt(tid, 10);
|
|
24
|
+
if (!tid || isNaN(tid) || tid <= 0) {
|
|
25
|
+
return { allowed: false, reason: 'Missing or invalid topic ID' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Admin/Global Moderator bypass
|
|
30
|
+
const [isAdmin, isGlobalMod] = await Promise.all([
|
|
31
|
+
groups.isMember(uid, 'administrators'),
|
|
32
|
+
groups.isMember(uid, 'Global Moderators'),
|
|
33
|
+
]);
|
|
34
|
+
if (isAdmin || isGlobalMod) {
|
|
35
|
+
return { allowed: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if user can read the topic (NodeBB privilege system)
|
|
39
|
+
const canRead = await privileges.topics.can('topics:read', tid, uid);
|
|
40
|
+
if (!canRead) {
|
|
41
|
+
return { allowed: false, reason: 'Access denied' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verify the PDF filename exists in one of the topic's posts
|
|
45
|
+
const exists = await TopicAccess.pdfExistsInTopic(tid, filename);
|
|
46
|
+
if (!exists) {
|
|
47
|
+
return { allowed: false, reason: 'Access denied' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { allowed: true };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// DB error, topic not found, etc. — deny by default
|
|
53
|
+
return { allowed: false, reason: 'Access check failed' };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a PDF filename is referenced in any post of a topic.
|
|
59
|
+
* Searches both the main post and all reply posts.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} tid - Topic ID
|
|
62
|
+
* @param {string} filename - PDF filename to search for
|
|
63
|
+
* @returns {Promise<boolean>}
|
|
64
|
+
*/
|
|
65
|
+
TopicAccess.pdfExistsInTopic = async function (tid, filename) {
|
|
66
|
+
// Get the topic's main post ID
|
|
67
|
+
const topicData = await topics.getTopicFields(tid, ['mainPid']);
|
|
68
|
+
if (!topicData || !topicData.mainPid) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get all post IDs in this topic (replies)
|
|
73
|
+
const replyPids = await db.getSortedSetRange('tid:' + tid + ':posts', 0, -1);
|
|
74
|
+
const allPids = [topicData.mainPid, ...replyPids].filter(Boolean);
|
|
75
|
+
|
|
76
|
+
if (allPids.length === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get raw content of all posts
|
|
81
|
+
const postsData = await posts.getPostsFields(allPids, ['content']);
|
|
82
|
+
|
|
83
|
+
// Escape filename for regex safety, also match URL-encoded variant
|
|
84
|
+
// (post content may store "Özel Döküman.pdf" as "%C3%96zel%20D%C3%B6k%C3%BCman.pdf")
|
|
85
|
+
const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
const encodedEscaped = encodeURIComponent(filename).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
const pattern = new RegExp('(' + escaped + '|' + encodedEscaped + ')', 'i');
|
|
88
|
+
|
|
89
|
+
for (const post of postsData) {
|
|
90
|
+
if (post && post.content && pattern.test(post.content)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
};
|
package/library.js
CHANGED
|
@@ -11,6 +11,7 @@ const controllers = require('./lib/controllers');
|
|
|
11
11
|
const nonceStore = require('./lib/nonce-store');
|
|
12
12
|
const pdfHandler = require('./lib/pdf-handler');
|
|
13
13
|
const geminiChat = require('./lib/gemini-chat');
|
|
14
|
+
const topicAccess = require('./lib/topic-access');
|
|
14
15
|
|
|
15
16
|
const plugin = {};
|
|
16
17
|
|
|
@@ -18,6 +19,16 @@ const plugin = {};
|
|
|
18
19
|
let viewerHtmlCache = null;
|
|
19
20
|
let pluginSettings = {};
|
|
20
21
|
|
|
22
|
+
// Rate limit for viewer endpoint — prevents brute-force topic ID enumeration
|
|
23
|
+
const viewerRateLimit = new Map(); // uid -> { count, windowStart }
|
|
24
|
+
const VIEWER_RATE_LIMIT = { max: 15, window: 60 * 1000 }; // 15 requests per 60 seconds
|
|
25
|
+
setInterval(() => {
|
|
26
|
+
const cutoff = Date.now() - VIEWER_RATE_LIMIT.window;
|
|
27
|
+
for (const [uid, data] of viewerRateLimit.entries()) {
|
|
28
|
+
if (data.windowStart < cutoff) viewerRateLimit.delete(uid);
|
|
29
|
+
}
|
|
30
|
+
}, 60 * 1000).unref();
|
|
31
|
+
|
|
21
32
|
plugin.init = async (params) => {
|
|
22
33
|
const { router, middleware } = params;
|
|
23
34
|
|
|
@@ -26,6 +37,7 @@ plugin.init = async (params) => {
|
|
|
26
37
|
try {
|
|
27
38
|
viewerHtmlCache = fs.readFileSync(viewerPath, 'utf8');
|
|
28
39
|
} catch (err) {
|
|
40
|
+
console.error('[PDF-Secure] Failed to read viewer.html:', err.message);
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
// Double slash bypass protection - catches /uploads//files/ attempts
|
|
@@ -76,6 +88,12 @@ plugin.init = async (params) => {
|
|
|
76
88
|
// Apply admin-configured quota settings
|
|
77
89
|
controllers.initQuotaSettings(pluginSettings);
|
|
78
90
|
|
|
91
|
+
// Apply admin-configured custom prompts
|
|
92
|
+
geminiChat.setCustomPrompts({
|
|
93
|
+
premium: pluginSettings.promptPremium || '',
|
|
94
|
+
vip: pluginSettings.promptVip || '',
|
|
95
|
+
});
|
|
96
|
+
|
|
79
97
|
const watermarkEnabled = pluginSettings.watermarkEnabled === 'on';
|
|
80
98
|
|
|
81
99
|
// Admin page route
|
|
@@ -88,6 +106,19 @@ plugin.init = async (params) => {
|
|
|
88
106
|
return res.status(401).json({ error: 'Authentication required' });
|
|
89
107
|
}
|
|
90
108
|
|
|
109
|
+
// Rate limit — prevent brute-force topic ID enumeration
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const rateData = viewerRateLimit.get(req.uid) || { count: 0, windowStart: now };
|
|
112
|
+
if (now - rateData.windowStart > VIEWER_RATE_LIMIT.window) {
|
|
113
|
+
rateData.count = 0;
|
|
114
|
+
rateData.windowStart = now;
|
|
115
|
+
}
|
|
116
|
+
rateData.count++;
|
|
117
|
+
viewerRateLimit.set(req.uid, rateData);
|
|
118
|
+
if (rateData.count > VIEWER_RATE_LIMIT.max) {
|
|
119
|
+
return res.status(429).json({ error: 'Too many requests. Please slow down.' });
|
|
120
|
+
}
|
|
121
|
+
|
|
91
122
|
const { file } = req.query;
|
|
92
123
|
if (!file) {
|
|
93
124
|
return res.status(400).send('Missing file parameter');
|
|
@@ -99,6 +130,13 @@ plugin.init = async (params) => {
|
|
|
99
130
|
return res.status(400).send('Invalid file');
|
|
100
131
|
}
|
|
101
132
|
|
|
133
|
+
// Topic-level access control: require tid and validate access
|
|
134
|
+
const { tid } = req.query;
|
|
135
|
+
const accessResult = await topicAccess.validate(req.uid, tid, safeName);
|
|
136
|
+
if (!accessResult.allowed) {
|
|
137
|
+
return res.status(403).json({ error: accessResult.reason || 'Access denied' });
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
// Check cache
|
|
103
141
|
if (!viewerHtmlCache) {
|
|
104
142
|
return res.status(500).send('Viewer not available');
|
|
@@ -132,6 +170,7 @@ plugin.init = async (params) => {
|
|
|
132
170
|
try {
|
|
133
171
|
totalPages = await pdfHandler.getTotalPages(safeName);
|
|
134
172
|
} catch (err) {
|
|
173
|
+
console.error('[PDF-Secure] Failed to get total pages for', safeName, ':', err.message);
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
|
|
@@ -152,6 +191,7 @@ plugin.init = async (params) => {
|
|
|
152
191
|
// Key is embedded in HTML - NOT visible in any network API response!
|
|
153
192
|
const configObj = {
|
|
154
193
|
filename: safeName,
|
|
194
|
+
tid: parseInt(tid, 10) || 0,
|
|
155
195
|
relativePath: req.app.get('relative_path') || '',
|
|
156
196
|
csrfToken: req.csrfToken ? req.csrfToken() : '',
|
|
157
197
|
nonce: nonceData.nonce,
|
|
@@ -187,7 +227,7 @@ plugin.init = async (params) => {
|
|
|
187
227
|
.replace(/<script>(\r?\n\s*\/\/ IIFE to prevent global access)/, `<script nonce="${cspNonce}">$1`);
|
|
188
228
|
|
|
189
229
|
// Update CSP header with the nonce for the inline viewer script
|
|
190
|
-
res.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-${cspNonce}' https://cdnjs.cloudflare.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: blob: https://cdnjs.cloudflare.com https://i.ibb.co; connect-src 'self'; frame-ancestors 'self'; form-action 'none'; base-uri 'self'`);
|
|
230
|
+
res.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-${cspNonce}' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://cdnjs.cloudflare.com https://i.ibb.co; connect-src 'self'; frame-ancestors 'self'; form-action 'none'; base-uri 'self'`);
|
|
191
231
|
|
|
192
232
|
res.type('html').send(injectedHtml);
|
|
193
233
|
});
|
|
@@ -257,19 +297,32 @@ plugin.filterConfig = async function (data) {
|
|
|
257
297
|
|
|
258
298
|
// Transform PDF links to secure placeholders (server-side)
|
|
259
299
|
// This hides PDF URLs from: page source, API, RSS, ActivityPub
|
|
300
|
+
// Supports both filter:parse.post (data.postData.content) and filter:parse.raw (string)
|
|
260
301
|
plugin.transformPdfLinks = async (data) => {
|
|
261
|
-
|
|
302
|
+
// Support multiple hook data formats
|
|
303
|
+
let content = null;
|
|
304
|
+
let contentPath = null;
|
|
305
|
+
|
|
306
|
+
if (data && data.postData && data.postData.content) {
|
|
307
|
+
content = data.postData.content;
|
|
308
|
+
contentPath = 'postData';
|
|
309
|
+
} else if (data && typeof data === 'string') {
|
|
310
|
+
content = data;
|
|
311
|
+
contentPath = 'raw';
|
|
312
|
+
} else {
|
|
262
313
|
return data;
|
|
263
314
|
}
|
|
264
315
|
|
|
265
|
-
|
|
266
316
|
// Regex to match PDF links: <a href="...xxx.pdf">text</a>
|
|
267
317
|
// Captures: full URL path, filename, link text
|
|
268
318
|
const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
|
|
269
319
|
|
|
270
|
-
const matchCount = (
|
|
320
|
+
const matchCount = (content.match(pdfLinkRegex) || []).length;
|
|
321
|
+
if (matchCount === 0) {
|
|
322
|
+
return data;
|
|
323
|
+
}
|
|
271
324
|
|
|
272
|
-
|
|
325
|
+
content = content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
|
|
273
326
|
// Decode filename to prevent double encoding (URL may already be encoded)
|
|
274
327
|
let decodedFilename;
|
|
275
328
|
try { decodedFilename = decodeURIComponent(filename); }
|
|
@@ -288,6 +341,13 @@ plugin.transformPdfLinks = async (data) => {
|
|
|
288
341
|
</div>`;
|
|
289
342
|
});
|
|
290
343
|
|
|
344
|
+
// Write back to the correct location
|
|
345
|
+
if (contentPath === 'postData') {
|
|
346
|
+
data.postData.content = content;
|
|
347
|
+
} else if (contentPath === 'raw') {
|
|
348
|
+
data = content;
|
|
349
|
+
}
|
|
350
|
+
|
|
291
351
|
return data;
|
|
292
352
|
};
|
|
293
353
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -40,32 +40,6 @@
|
|
|
40
40
|
let isLoading = false;
|
|
41
41
|
let currentResolver = null;
|
|
42
42
|
|
|
43
|
-
// ============================================
|
|
44
|
-
// SPA MEMORY CACHE - Cache decoded PDF buffers
|
|
45
|
-
// ============================================
|
|
46
|
-
const pdfBufferCache = new Map(); // filename -> { buffer: ArrayBuffer, cachedAt: number }
|
|
47
|
-
const CACHE_MAX_SIZE = 5; // ~50MB limit (avg 10MB per PDF)
|
|
48
|
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
49
|
-
|
|
50
|
-
function setCachedBuffer(filename, buffer) {
|
|
51
|
-
// Evict oldest if cache is full
|
|
52
|
-
if (pdfBufferCache.size >= CACHE_MAX_SIZE) {
|
|
53
|
-
const firstKey = pdfBufferCache.keys().next().value;
|
|
54
|
-
pdfBufferCache.delete(firstKey);
|
|
55
|
-
}
|
|
56
|
-
pdfBufferCache.set(filename, { buffer: buffer, cachedAt: Date.now() });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getCachedBuffer(filename) {
|
|
60
|
-
const entry = pdfBufferCache.get(filename);
|
|
61
|
-
if (!entry) return null;
|
|
62
|
-
if (Date.now() - entry.cachedAt > CACHE_TTL) {
|
|
63
|
-
pdfBufferCache.delete(filename);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
return entry.buffer;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
43
|
// Listen for postMessage from iframe
|
|
70
44
|
window.addEventListener('message', function (event) {
|
|
71
45
|
// Security: Only accept messages from same origin
|
|
@@ -79,21 +53,6 @@
|
|
|
79
53
|
}
|
|
80
54
|
}
|
|
81
55
|
|
|
82
|
-
// PDF buffer from viewer - cache it
|
|
83
|
-
if (event.data && event.data.type === 'pdf-secure-buffer') {
|
|
84
|
-
// Source verification: only accept buffers from pdf-secure iframes
|
|
85
|
-
var isFromSecureIframe = false;
|
|
86
|
-
document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
|
|
87
|
-
if (f.contentWindow === event.source) isFromSecureIframe = true;
|
|
88
|
-
});
|
|
89
|
-
if (!isFromSecureIframe) return;
|
|
90
|
-
|
|
91
|
-
const { filename, buffer } = event.data;
|
|
92
|
-
if (filename && buffer) {
|
|
93
|
-
setCachedBuffer(filename, buffer);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
56
|
// Fullscreen toggle request from iframe viewer
|
|
98
57
|
if (event.data && event.data.type === 'pdf-secure-fullscreen-toggle') {
|
|
99
58
|
var sourceIframe = document.querySelector('.pdf-secure-iframe');
|
|
@@ -135,35 +94,6 @@
|
|
|
135
94
|
}
|
|
136
95
|
}
|
|
137
96
|
|
|
138
|
-
// Viewer asking for cached buffer
|
|
139
|
-
if (event.data && event.data.type === 'pdf-secure-cache-request') {
|
|
140
|
-
// Source verification: only respond to pdf-secure iframes
|
|
141
|
-
var isFromSecureIframe = false;
|
|
142
|
-
document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
|
|
143
|
-
if (f.contentWindow === event.source) isFromSecureIframe = true;
|
|
144
|
-
});
|
|
145
|
-
if (!isFromSecureIframe) return;
|
|
146
|
-
|
|
147
|
-
const { filename } = event.data;
|
|
148
|
-
const cached = getCachedBuffer(filename);
|
|
149
|
-
if (cached && event.source) {
|
|
150
|
-
// Send cached buffer to viewer (transferable for 0-copy)
|
|
151
|
-
// Clone once: keep original in cache, transfer the copy
|
|
152
|
-
const copy = cached.slice(0);
|
|
153
|
-
event.source.postMessage({
|
|
154
|
-
type: 'pdf-secure-cache-response',
|
|
155
|
-
filename: filename,
|
|
156
|
-
buffer: copy
|
|
157
|
-
}, event.origin, [copy]);
|
|
158
|
-
} else if (event.source) {
|
|
159
|
-
// No cache, viewer will fetch normally
|
|
160
|
-
event.source.postMessage({
|
|
161
|
-
type: 'pdf-secure-cache-response',
|
|
162
|
-
filename: filename,
|
|
163
|
-
buffer: null
|
|
164
|
-
}, event.origin);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
97
|
});
|
|
168
98
|
|
|
169
99
|
// Forward fullscreen state changes to all viewer iframes
|
|
@@ -300,8 +230,6 @@
|
|
|
300
230
|
loadQueue.length = 0;
|
|
301
231
|
isLoading = false;
|
|
302
232
|
currentResolver = null;
|
|
303
|
-
// Clear decrypted PDF buffer cache on navigation
|
|
304
|
-
pdfBufferCache.clear();
|
|
305
233
|
// Exit simulated fullscreen on SPA navigation
|
|
306
234
|
exitSimulatedFullscreen();
|
|
307
235
|
interceptPdfLinks();
|
|
@@ -453,10 +381,11 @@
|
|
|
453
381
|
var iframe = document.createElement('iframe');
|
|
454
382
|
iframe.className = 'pdf-secure-iframe';
|
|
455
383
|
iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:1;';
|
|
456
|
-
|
|
384
|
+
var tidParam = (ajaxify && ajaxify.data && ajaxify.data.tid) ? '&tid=' + encodeURIComponent(ajaxify.data.tid) : '';
|
|
385
|
+
iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename) + tidParam;
|
|
457
386
|
iframe.setAttribute('frameborder', '0');
|
|
458
387
|
iframe.setAttribute('allowfullscreen', 'true');
|
|
459
|
-
iframe.setAttribute('allow', 'fullscreen');
|
|
388
|
+
iframe.setAttribute('allow', 'fullscreen; clipboard-write');
|
|
460
389
|
|
|
461
390
|
// Store resolver for postMessage callback
|
|
462
391
|
currentResolver = function () {
|
|
@@ -96,6 +96,27 @@
|
|
|
96
96
|
</div>
|
|
97
97
|
</div>
|
|
98
98
|
|
|
99
|
+
<hr class="my-3">
|
|
100
|
+
<h6 class="fw-semibold mb-3" style="font-size:13px;">AI Sistem Prompt'lari</h6>
|
|
101
|
+
|
|
102
|
+
<div class="mb-3">
|
|
103
|
+
<label class="form-label fw-medium" for="promptPremium" style="font-size:13px;">
|
|
104
|
+
<span class="badge text-bg-primary me-1" style="font-size:9px;">PREMIUM</span>
|
|
105
|
+
Sistem Prompt
|
|
106
|
+
</label>
|
|
107
|
+
<textarea id="promptPremium" name="promptPremium" data-key="promptPremium" class="form-control" rows="6" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
|
|
108
|
+
<div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="mb-3">
|
|
112
|
+
<label class="form-label fw-medium" for="promptVip" style="font-size:13px;">
|
|
113
|
+
<span class="badge me-1" style="font-size:9px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;">VIP</span>
|
|
114
|
+
Sistem Prompt
|
|
115
|
+
</label>
|
|
116
|
+
<textarea id="promptVip" name="promptVip" data-key="promptVip" class="form-control" rows="8" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
|
|
117
|
+
<div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
99
120
|
<hr class="my-3">
|
|
100
121
|
<h6 class="fw-semibold mb-3" style="font-size:13px;">Token Kota Ayarlari</h6>
|
|
101
122
|
|
package/static/viewer-app.js
CHANGED
|
@@ -353,73 +353,29 @@
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
try {
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
356
|
+
// Nonce and key are embedded in HTML config (not fetched from API)
|
|
357
|
+
const nonce = config.nonce;
|
|
358
|
+
const decryptKey = config.dk;
|
|
359
|
+
const decryptIv = config.iv;
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const handler = (event) => {
|
|
365
|
-
if (event.data && event.data.type === 'pdf-secure-cache-response' && event.data.filename === config.filename) {
|
|
366
|
-
window.removeEventListener('message', handler);
|
|
367
|
-
resolve(event.data.buffer);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
window.addEventListener('message', handler);
|
|
371
|
-
|
|
372
|
-
// Timeout after 100ms
|
|
373
|
-
setTimeout(() => {
|
|
374
|
-
window.removeEventListener('message', handler);
|
|
375
|
-
resolve(null);
|
|
376
|
-
}, 100);
|
|
361
|
+
// Fetch encrypted PDF binary
|
|
362
|
+
const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
|
|
363
|
+
const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
|
|
377
364
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
pdfBuffer = await cachePromise;
|
|
382
|
-
if (pdfBuffer) {
|
|
383
|
-
console.log('[PDF-Secure] Using cached buffer');
|
|
384
|
-
}
|
|
365
|
+
if (!pdfRes.ok) {
|
|
366
|
+
throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
|
|
385
367
|
}
|
|
386
368
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Nonce and key are embedded in HTML config (not fetched from API)
|
|
390
|
-
const nonce = config.nonce;
|
|
391
|
-
const decryptKey = config.dk;
|
|
392
|
-
const decryptIv = config.iv;
|
|
393
|
-
|
|
394
|
-
// Fetch encrypted PDF binary
|
|
395
|
-
const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
|
|
396
|
-
const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
|
|
369
|
+
const encodedBuffer = await pdfRes.arrayBuffer();
|
|
370
|
+
console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
|
|
397
371
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Decrypt AES-256-GCM encrypted data
|
|
406
|
-
if (decryptKey && decryptIv) {
|
|
407
|
-
console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
|
|
408
|
-
pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
|
|
409
|
-
} else {
|
|
410
|
-
pdfBuffer = encodedBuffer;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Send buffer to parent for caching (premium/lite only - non-premium must not leak decoded buffer)
|
|
414
|
-
if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
|
|
415
|
-
// Clone buffer for parent (we keep original)
|
|
416
|
-
const bufferCopy = pdfBuffer.slice(0);
|
|
417
|
-
window.parent.postMessage({
|
|
418
|
-
type: 'pdf-secure-buffer',
|
|
419
|
-
filename: config.filename,
|
|
420
|
-
buffer: bufferCopy
|
|
421
|
-
}, window.location.origin, [bufferCopy]); // Transferable
|
|
422
|
-
}
|
|
372
|
+
// Decrypt AES-256-GCM encrypted data
|
|
373
|
+
let pdfBuffer;
|
|
374
|
+
if (decryptKey && decryptIv) {
|
|
375
|
+
console.log('[PDF-Secure] Decrypting AES-256-GCM data...');
|
|
376
|
+
pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
|
|
377
|
+
} else {
|
|
378
|
+
pdfBuffer = encodedBuffer;
|
|
423
379
|
}
|
|
424
380
|
|
|
425
381
|
console.log('[PDF-Secure] PDF decoded successfully');
|