nodebb-plugin-pdf-secure2 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/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/commitlint.config.js +26 -0
- package/docs/plans/2026-02-24-premium-gate-design.md +52 -0
- package/docs/plans/2026-02-24-premium-gate.md +323 -0
- package/eslint.config.mjs +10 -0
- package/image.png +0 -0
- package/languages/de/pdf-secure.json +7 -0
- package/languages/en-GB/pdf-secure.json +7 -0
- package/languages/en-US/pdf-secure.json +7 -0
- package/lib/controllers.js +65 -0
- package/lib/nonce-store.js +70 -0
- package/lib/pdf-handler.js +122 -0
- package/library.js +249 -0
- package/package.json +53 -0
- package/plugin.json +37 -0
- package/renovate.json +5 -0
- package/static/image.png +0 -0
- package/static/lib/main.js +477 -0
- package/static/lib/pdf-secure (1).pdf +0 -0
- package/static/lib/pdf.min.mjs +21 -0
- package/static/lib/pdf.worker.min.mjs +21 -0
- package/static/lib/viewer.js +184 -0
- package/static/style.less +142 -0
- package/static/templates/admin/plugins/pdf-secure.tpl +43 -0
- package/static/viewer-app.js +2906 -0
- package/static/viewer-yedek.html +4548 -0
- package/static/viewer.css +1500 -0
- package/static/viewer.html +5632 -0
- package/test/.eslintrc +9 -0
- package/test/index.js +41 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { PDFDocument } = require('pdf-lib');
|
|
6
|
+
const nconf = require.main.require('nconf');
|
|
7
|
+
|
|
8
|
+
const singlePageCache = new Map();
|
|
9
|
+
const pageCountCache = new Map();
|
|
10
|
+
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
11
|
+
|
|
12
|
+
// Periodic cleanup of expired cache entries
|
|
13
|
+
setInterval(() => {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
for (const [key, entry] of singlePageCache.entries()) {
|
|
16
|
+
if (now - entry.createdAt > CACHE_TTL) {
|
|
17
|
+
singlePageCache.delete(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const [key, entry] of pageCountCache.entries()) {
|
|
21
|
+
if (now - entry.createdAt > CACHE_TTL) {
|
|
22
|
+
pageCountCache.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, 10 * 60 * 1000).unref(); // cleanup every 10 minutes
|
|
26
|
+
|
|
27
|
+
const PdfHandler = module.exports;
|
|
28
|
+
|
|
29
|
+
PdfHandler.resolveFilePath = function (filename) {
|
|
30
|
+
// Sanitize: only allow basename (prevent directory traversal)
|
|
31
|
+
const safeName = path.basename(filename);
|
|
32
|
+
if (!safeName || safeName !== filename || safeName.includes('..')) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const uploadPath = nconf.get('upload_path') || path.join(nconf.get('base_dir'), 'public', 'uploads');
|
|
37
|
+
const filePath = path.join(uploadPath, 'files', safeName);
|
|
38
|
+
|
|
39
|
+
// Verify the resolved path is still within the upload directory
|
|
40
|
+
const resolvedPath = path.resolve(filePath);
|
|
41
|
+
const resolvedUploadDir = path.resolve(path.join(uploadPath, 'files'));
|
|
42
|
+
if (!resolvedPath.startsWith(resolvedUploadDir)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return filePath;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
PdfHandler.getFullPdf = async function (filename) {
|
|
50
|
+
const filePath = PdfHandler.resolveFilePath(filename);
|
|
51
|
+
if (!filePath) {
|
|
52
|
+
throw new Error('Invalid filename');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(filePath)) {
|
|
56
|
+
throw new Error('File not found');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return fs.promises.readFile(filePath);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
PdfHandler.getSinglePagePdf = async function (filename) {
|
|
63
|
+
// Check cache first
|
|
64
|
+
const cached = singlePageCache.get(filename);
|
|
65
|
+
if (cached && (Date.now() - cached.createdAt < CACHE_TTL)) {
|
|
66
|
+
return cached.buffer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const filePath = PdfHandler.resolveFilePath(filename);
|
|
70
|
+
if (!filePath) {
|
|
71
|
+
throw new Error('Invalid filename');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync(filePath)) {
|
|
75
|
+
throw new Error('File not found');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const existingPdfBytes = await fs.promises.readFile(filePath);
|
|
79
|
+
const srcDoc = await PDFDocument.load(existingPdfBytes);
|
|
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
|
+
|
|
85
|
+
const newDoc = await PDFDocument.create();
|
|
86
|
+
const [copiedPage] = await newDoc.copyPages(srcDoc, [0]);
|
|
87
|
+
newDoc.addPage(copiedPage);
|
|
88
|
+
|
|
89
|
+
const pdfBytes = await newDoc.save();
|
|
90
|
+
const buffer = Buffer.from(pdfBytes);
|
|
91
|
+
|
|
92
|
+
// Cache the result
|
|
93
|
+
singlePageCache.set(filename, {
|
|
94
|
+
buffer: buffer,
|
|
95
|
+
createdAt: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return buffer;
|
|
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
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const meta = require.main.require('./src/meta');
|
|
6
|
+
const groups = require.main.require('./src/groups');
|
|
7
|
+
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
8
|
+
|
|
9
|
+
const controllers = require('./lib/controllers');
|
|
10
|
+
const nonceStore = require('./lib/nonce-store');
|
|
11
|
+
const pdfHandler = require('./lib/pdf-handler');
|
|
12
|
+
|
|
13
|
+
const plugin = {};
|
|
14
|
+
|
|
15
|
+
// Memory cache for viewer.html
|
|
16
|
+
let viewerHtmlCache = null;
|
|
17
|
+
|
|
18
|
+
plugin.init = async (params) => {
|
|
19
|
+
const { router, middleware } = params;
|
|
20
|
+
|
|
21
|
+
// Pre-load viewer.html into memory cache
|
|
22
|
+
const viewerPath = path.join(__dirname, 'static', 'viewer.html');
|
|
23
|
+
try {
|
|
24
|
+
viewerHtmlCache = fs.readFileSync(viewerPath, 'utf8');
|
|
25
|
+
console.log('[PDF-Secure] Viewer template cached in memory');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('[PDF-Secure] Failed to cache viewer template:', err.message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Double slash bypass protection - catches /uploads//files/ attempts
|
|
31
|
+
router.use((req, res, next) => {
|
|
32
|
+
if (req.path.includes('//') && req.path.toLowerCase().includes('.pdf')) {
|
|
33
|
+
return res.status(403).json({ error: 'Invalid path' });
|
|
34
|
+
}
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// PDF direct access blocker middleware
|
|
39
|
+
// Intercepts requests to uploaded PDF files and returns 403
|
|
40
|
+
// Admin and Global Moderators can bypass this restriction
|
|
41
|
+
router.get('/assets/uploads/files/:filename', async (req, res, next) => {
|
|
42
|
+
if (req.params.filename && req.params.filename.toLowerCase().endsWith('.pdf')) {
|
|
43
|
+
// Admin ve Global Mod'lar direkt erişebilsin
|
|
44
|
+
if (req.uid) {
|
|
45
|
+
const [isAdmin, isGlobalMod, isVip] = await Promise.all([
|
|
46
|
+
groups.isMember(req.uid, 'administrators'),
|
|
47
|
+
groups.isMember(req.uid, 'Global Moderators'),
|
|
48
|
+
groups.isMember(req.uid, 'VIP'),
|
|
49
|
+
]);
|
|
50
|
+
if (isAdmin || isGlobalMod || isVip) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return res.status(403).json({ error: 'Direct PDF access is not allowed. Use the secure viewer.' });
|
|
55
|
+
}
|
|
56
|
+
next();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// PDF binary endpoint (nonce-validated, authentication required)
|
|
60
|
+
router.get('/api/v3/plugins/pdf-secure/pdf-data', controllers.servePdfBinary);
|
|
61
|
+
|
|
62
|
+
// Admin page route
|
|
63
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
|
|
64
|
+
|
|
65
|
+
// Viewer page route (fullscreen Mozilla PDF.js viewer, authentication required)
|
|
66
|
+
router.get('/plugins/pdf-secure/viewer', async (req, res) => {
|
|
67
|
+
// Authentication gate - require logged-in user
|
|
68
|
+
if (!req.uid) {
|
|
69
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { file } = req.query;
|
|
73
|
+
if (!file) {
|
|
74
|
+
return res.status(400).send('Missing file parameter');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Sanitize filename
|
|
78
|
+
const safeName = path.basename(file);
|
|
79
|
+
if (!safeName || !safeName.toLowerCase().endsWith('.pdf')) {
|
|
80
|
+
return res.status(400).send('Invalid file');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check cache
|
|
84
|
+
if (!viewerHtmlCache) {
|
|
85
|
+
return res.status(500).send('Viewer not available');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if user is Premium or Lite (admins/global mods always premium)
|
|
89
|
+
let isPremium = false;
|
|
90
|
+
let isLite = false;
|
|
91
|
+
if (req.uid) {
|
|
92
|
+
const [isAdmin, isGlobalMod, isPremiumMember, isVipMember, isLiteMember] = await Promise.all([
|
|
93
|
+
groups.isMember(req.uid, 'administrators'),
|
|
94
|
+
groups.isMember(req.uid, 'Global Moderators'),
|
|
95
|
+
groups.isMember(req.uid, 'Premium'),
|
|
96
|
+
groups.isMember(req.uid, 'VIP'),
|
|
97
|
+
groups.isMember(req.uid, 'Lite'),
|
|
98
|
+
]);
|
|
99
|
+
isPremium = isAdmin || isGlobalMod || isPremiumMember || isVipMember;
|
|
100
|
+
// Lite: full PDF access but restricted UI (no annotations, sidebar, etc.)
|
|
101
|
+
isLite = !isPremium && isLiteMember;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Lite users get full PDF like premium (for nonce/server-side PDF data)
|
|
105
|
+
const hasFullAccess = isPremium || isLite;
|
|
106
|
+
const nonceData = nonceStore.generate(req.uid, safeName, hasFullAccess);
|
|
107
|
+
|
|
108
|
+
// For non-premium users, get total page count so client can show lock overlay
|
|
109
|
+
let totalPages = 1;
|
|
110
|
+
if (!hasFullAccess) {
|
|
111
|
+
try {
|
|
112
|
+
totalPages = await pdfHandler.getTotalPages(safeName);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('[PDF-Secure] Failed to get page count:', err.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Serve the viewer template with comprehensive security headers
|
|
119
|
+
res.set({
|
|
120
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
121
|
+
'X-Content-Type-Options': 'nosniff',
|
|
122
|
+
'X-XSS-Protection': '1; mode=block',
|
|
123
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, private, max-age=0',
|
|
124
|
+
'Pragma': 'no-cache',
|
|
125
|
+
'Expires': '0',
|
|
126
|
+
'Referrer-Policy': 'no-referrer',
|
|
127
|
+
'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()',
|
|
128
|
+
'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'",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Inject the filename, nonce, and key into the cached viewer
|
|
132
|
+
// Key is embedded in HTML - NOT visible in any network API response!
|
|
133
|
+
const injectedHtml = viewerHtmlCache
|
|
134
|
+
.replace('</head>', `
|
|
135
|
+
<style>
|
|
136
|
+
/* Hide upload overlay since PDF will auto-load */
|
|
137
|
+
#uploadOverlay { display: none !important; }
|
|
138
|
+
</style>
|
|
139
|
+
<script>
|
|
140
|
+
window.PDF_SECURE_CONFIG = {
|
|
141
|
+
filename: ${JSON.stringify(safeName)},
|
|
142
|
+
relativePath: ${JSON.stringify(req.app.get('relative_path') || '')},
|
|
143
|
+
csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
|
|
144
|
+
nonce: ${JSON.stringify(nonceData.nonce)},
|
|
145
|
+
dk: ${JSON.stringify(nonceData.dk)},
|
|
146
|
+
iv: ${JSON.stringify(nonceData.iv)},
|
|
147
|
+
isPremium: ${JSON.stringify(isPremium)},
|
|
148
|
+
isLite: ${JSON.stringify(isLite)},
|
|
149
|
+
uid: ${JSON.stringify(req.uid)},
|
|
150
|
+
totalPages: ${JSON.stringify(totalPages)}
|
|
151
|
+
};
|
|
152
|
+
</script>
|
|
153
|
+
</head>`);
|
|
154
|
+
|
|
155
|
+
res.type('html').send(injectedHtml);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
plugin.addRoutes = async ({ router, middleware, helpers }) => {
|
|
160
|
+
// Nonce endpoint removed - nonce is now generated in viewer route
|
|
161
|
+
// This improves security by not exposing any key-related data in API responses
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
plugin.addAdminNavigation = (header) => {
|
|
165
|
+
header.plugins.push({
|
|
166
|
+
route: '/plugins/pdf-secure',
|
|
167
|
+
icon: 'fa-file-pdf-o',
|
|
168
|
+
name: 'PDF Secure Viewer',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return header;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Filter meta tags to hide PDF URLs and filenames
|
|
175
|
+
plugin.filterMetaTags = async (hookData) => {
|
|
176
|
+
if (!hookData || !hookData.tags) {
|
|
177
|
+
return hookData;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Admin/Global Moderator bypass - no filtering for privileged users
|
|
181
|
+
if (hookData.req && hookData.req.uid) {
|
|
182
|
+
const [isAdmin, isGlobalMod] = await Promise.all([
|
|
183
|
+
groups.isMember(hookData.req.uid, 'administrators'),
|
|
184
|
+
groups.isMember(hookData.req.uid, 'Global Moderators'),
|
|
185
|
+
]);
|
|
186
|
+
if (isAdmin || isGlobalMod) {
|
|
187
|
+
return hookData;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Filter out PDF-related meta tags
|
|
192
|
+
hookData.tags = hookData.tags.filter(tag => {
|
|
193
|
+
// Remove og:image and og:image:url if it contains .pdf
|
|
194
|
+
if ((tag.property === 'og:image' || tag.property === 'og:image:url') && tag.content && tag.content.toLowerCase().includes('.pdf')) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Remove twitter:image if it contains .pdf
|
|
198
|
+
if (tag.name === 'twitter:image' && tag.content && tag.content.toLowerCase().includes('.pdf')) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Sanitize description to hide .pdf extensions
|
|
205
|
+
hookData.tags = hookData.tags.map(tag => {
|
|
206
|
+
if ((tag.name === 'description' || tag.property === 'og:description') && tag.content) {
|
|
207
|
+
// Replace .pdf extension with empty string in description
|
|
208
|
+
tag.content = tag.content.replace(/\.pdf/gi, '');
|
|
209
|
+
}
|
|
210
|
+
return tag;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return hookData;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Transform PDF links to secure placeholders (server-side)
|
|
217
|
+
// This hides PDF URLs from: page source, API, RSS, ActivityPub
|
|
218
|
+
plugin.transformPdfLinks = async (data) => {
|
|
219
|
+
if (!data || !data.postData || !data.postData.content) {
|
|
220
|
+
return data;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Regex to match PDF links: <a href="...xxx.pdf">text</a>
|
|
224
|
+
// Captures: full URL path, filename, link text
|
|
225
|
+
const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
|
|
226
|
+
|
|
227
|
+
data.postData.content = data.postData.content.replace(pdfLinkRegex, (match, fullPath, filename, linkText) => {
|
|
228
|
+
// Decode filename to prevent double encoding (URL may already be encoded)
|
|
229
|
+
let decodedFilename;
|
|
230
|
+
try { decodedFilename = decodeURIComponent(filename); }
|
|
231
|
+
catch (e) { decodedFilename = filename; }
|
|
232
|
+
|
|
233
|
+
// Sanitize for HTML attribute
|
|
234
|
+
const safeFilename = decodedFilename.replace(/[<>"'&]/g, '');
|
|
235
|
+
const displayName = linkText.trim() || safeFilename;
|
|
236
|
+
|
|
237
|
+
// Return secure placeholder div instead of actual link
|
|
238
|
+
return `<div class="pdf-secure-placeholder" data-filename="${safeFilename}">
|
|
239
|
+
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:#e81224;vertical-align:middle;margin-right:8px;">
|
|
240
|
+
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
|
|
241
|
+
</svg>
|
|
242
|
+
<span>${displayName}</span>
|
|
243
|
+
</div>`;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return data;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
module.exports = plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-pdf-secure2",
|
|
3
|
+
"version": "1.2.30",
|
|
4
|
+
"description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/nodebb/nodebb-plugin-pdf-secure"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"lint": "eslint ."
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nodebb",
|
|
15
|
+
"plugin",
|
|
16
|
+
"pdf",
|
|
17
|
+
"secure",
|
|
18
|
+
"viewer"
|
|
19
|
+
],
|
|
20
|
+
"husky": {
|
|
21
|
+
"hooks": {
|
|
22
|
+
"pre-commit": "lint-staged",
|
|
23
|
+
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"lint-staged": {
|
|
27
|
+
"*.js": [
|
|
28
|
+
"eslint --fix",
|
|
29
|
+
"git add"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/nodebb/nodebb-plugin-pdf-secure/issues"
|
|
35
|
+
},
|
|
36
|
+
"readmeFilename": "README.md",
|
|
37
|
+
"nbbpm": {
|
|
38
|
+
"compatibility": "^3.2.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"pdf-lib": "^1.17.1",
|
|
42
|
+
"uuid": "^11.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@commitlint/cli": "20.4.1",
|
|
46
|
+
"@commitlint/config-angular": "20.4.1",
|
|
47
|
+
"eslint": "9.39.2",
|
|
48
|
+
"eslint-config-nodebb": "1.1.11",
|
|
49
|
+
"husky": "9.1.7",
|
|
50
|
+
"lint-staged": "16.2.7",
|
|
51
|
+
"pdfjs-dist": "^4.9.155"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-pdf-secure",
|
|
3
|
+
"url": "https://github.com/NodeBB/nodebb-plugin-pdf-secure",
|
|
4
|
+
"library": "./library.js",
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"hook": "static:app.load",
|
|
8
|
+
"method": "init"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"hook": "static:api.routes",
|
|
12
|
+
"method": "addRoutes"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"hook": "filter:admin.header.build",
|
|
16
|
+
"method": "addAdminNavigation"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"hook": "filter:meta.getMetaTags",
|
|
20
|
+
"method": "filterMetaTags"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"hook": "filter:parse.post",
|
|
24
|
+
"method": "transformPdfLinks"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"staticDirs": {
|
|
28
|
+
"static": "./static"
|
|
29
|
+
},
|
|
30
|
+
"less": [
|
|
31
|
+
"static/style.less"
|
|
32
|
+
],
|
|
33
|
+
"scripts": [
|
|
34
|
+
"static/lib/main.js"
|
|
35
|
+
],
|
|
36
|
+
"templates": "./static/templates"
|
|
37
|
+
}
|
package/renovate.json
ADDED
package/static/image.png
ADDED
|
Binary file
|