nodebb-plugin-pdf-secure 1.0.1
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 +8 -0
- package/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/commitlint.config.js +26 -0
- package/eslint.config.mjs +10 -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 +51 -0
- package/lib/nonce-store.js +52 -0
- package/lib/pdf-handler.js +89 -0
- package/library.js +72 -0
- package/package.json +53 -0
- package/plugin.json +20 -0
- package/renovate.json +5 -0
- package/static/lib/main.js +108 -0
- package/static/lib/pdf.min.mjs +21 -0
- package/static/lib/pdf.worker.min.mjs +21 -0
- package/static/lib/viewer.css +158 -0
- package/static/lib/viewer.html +29 -0
- package/static/lib/viewer.js +181 -0
- package/static/style.less +42 -0
- package/static/templates/admin/plugins/pdf-secure.tpl +31 -0
- package/test/.eslintrc +9 -0
- package/test/index.js +41 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Auto detect text files and perform LF normalization
|
|
2
|
+
* text=auto
|
|
3
|
+
|
|
4
|
+
# Custom for Visual Studio
|
|
5
|
+
*.cs diff=csharp
|
|
6
|
+
*.sln merge=union
|
|
7
|
+
*.csproj merge=union
|
|
8
|
+
*.vbproj merge=union
|
|
9
|
+
*.fsproj merge=union
|
|
10
|
+
*.dbproj merge=union
|
|
11
|
+
|
|
12
|
+
# Standard to msysgit
|
|
13
|
+
*.doc diff=astextplain
|
|
14
|
+
*.DOC diff=astextplain
|
|
15
|
+
*.docx diff=astextplain
|
|
16
|
+
*.DOCX diff=astextplain
|
|
17
|
+
*.dot diff=astextplain
|
|
18
|
+
*.DOT diff=astextplain
|
|
19
|
+
*.pdf diff=astextplain
|
|
20
|
+
*.PDF diff=astextplain
|
|
21
|
+
*.rtf diff=astextplain
|
|
22
|
+
*.RTF diff=astextplain
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 NodeBB Inc. <sales@nodebb.org>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Quickstart Plugin for NodeBB
|
|
2
|
+
|
|
3
|
+
A starter kit for quickly creating NodeBB plugins. Comes with a pre-setup SCSS file, server side JS script with an `static:app.load` hook, and a client-side script. Most plugins need at least one of the above, so this ought to save you some time. For a full list of hooks have a look at our [wiki page](https://github.com/NodeBB/NodeBB/wiki/Hooks), and for more information about creating plugins please visit our [documentation portal](https://docs.nodebb.org/).
|
|
4
|
+
|
|
5
|
+
Fork this or copy it, and using your favourite text editor find and replace all instances of `nodebb-plugin-quickstart` with `nodebb-plugin-your-plugins-name`. Change the author's name in the LICENSE and package.json files.
|
|
6
|
+
|
|
7
|
+
## Hello World
|
|
8
|
+
|
|
9
|
+
Really simple, just edit `public/lib/main.js` and paste in `console.log('hello world');`, and that's it!
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
npm install nodebb-plugin-quickstart
|
|
14
|
+
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
17
|
+
Don't forget to add screenshots!
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
extends: ['@commitlint/config-angular'],
|
|
5
|
+
rules: {
|
|
6
|
+
'header-max-length': [1, 'always', 72],
|
|
7
|
+
'type-enum': [
|
|
8
|
+
2,
|
|
9
|
+
'always',
|
|
10
|
+
[
|
|
11
|
+
'breaking',
|
|
12
|
+
'build',
|
|
13
|
+
'chore',
|
|
14
|
+
'ci',
|
|
15
|
+
'docs',
|
|
16
|
+
'feat',
|
|
17
|
+
'fix',
|
|
18
|
+
'perf',
|
|
19
|
+
'refactor',
|
|
20
|
+
'revert',
|
|
21
|
+
'style',
|
|
22
|
+
'test',
|
|
23
|
+
],
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"view-pdf": "PDF anzeigen",
|
|
3
|
+
"premium-upgrade": "Upgrade auf Premium, um das vollständige Dokument anzuzeigen.",
|
|
4
|
+
"loading": "PDF wird geladen...",
|
|
5
|
+
"access-denied": "Zugriff verweigert",
|
|
6
|
+
"login-required": "Sie müssen angemeldet sein, um diese PDF anzuzeigen."
|
|
7
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const nonceStore = require('./nonce-store');
|
|
4
|
+
const pdfHandler = require('./pdf-handler');
|
|
5
|
+
|
|
6
|
+
const Controllers = module.exports;
|
|
7
|
+
|
|
8
|
+
Controllers.renderAdminPage = function (req, res) {
|
|
9
|
+
res.render('admin/plugins/pdf-secure', {
|
|
10
|
+
title: 'PDF Secure Viewer',
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
Controllers.servePdfBinary = async function (req, res) {
|
|
15
|
+
const { nonce } = req.query;
|
|
16
|
+
if (!nonce) {
|
|
17
|
+
return res.status(400).json({ error: 'Missing nonce' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!req.uid) {
|
|
21
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = nonceStore.validate(nonce, req.uid);
|
|
25
|
+
if (!data) {
|
|
26
|
+
return res.status(403).json({ error: 'Invalid or expired nonce' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let pdfBuffer;
|
|
31
|
+
if (data.isPremium) {
|
|
32
|
+
pdfBuffer = await pdfHandler.getFullPdf(data.file);
|
|
33
|
+
} else {
|
|
34
|
+
pdfBuffer = await pdfHandler.getSinglePagePdf(data.file);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.set({
|
|
38
|
+
'Content-Type': 'application/octet-stream',
|
|
39
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, private',
|
|
40
|
+
'X-Content-Type-Options': 'nosniff',
|
|
41
|
+
'Content-Disposition': 'inline',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return res.send(pdfBuffer);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.message === 'File not found') {
|
|
47
|
+
return res.status(404).json({ error: 'PDF not found' });
|
|
48
|
+
}
|
|
49
|
+
return res.status(500).json({ error: 'Internal error' });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
|
|
5
|
+
const store = new Map();
|
|
6
|
+
const NONCE_TTL = 30 * 1000; // 30 seconds
|
|
7
|
+
const CLEANUP_INTERVAL = 60 * 1000; // 60 seconds
|
|
8
|
+
|
|
9
|
+
// Periodic cleanup of expired nonces
|
|
10
|
+
setInterval(() => {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
for (const [nonce, data] of store.entries()) {
|
|
13
|
+
if (now - data.createdAt > NONCE_TTL) {
|
|
14
|
+
store.delete(nonce);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}, CLEANUP_INTERVAL).unref();
|
|
18
|
+
|
|
19
|
+
const NonceStore = module.exports;
|
|
20
|
+
|
|
21
|
+
NonceStore.generate = function (uid, file, isPremium) {
|
|
22
|
+
const nonce = uuidv4();
|
|
23
|
+
store.set(nonce, {
|
|
24
|
+
uid: uid,
|
|
25
|
+
file: file,
|
|
26
|
+
isPremium: isPremium,
|
|
27
|
+
createdAt: Date.now(),
|
|
28
|
+
});
|
|
29
|
+
return nonce;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
NonceStore.validate = function (nonce, uid) {
|
|
33
|
+
const data = store.get(nonce);
|
|
34
|
+
if (!data) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Delete immediately (single-use)
|
|
39
|
+
store.delete(nonce);
|
|
40
|
+
|
|
41
|
+
// Check UID match
|
|
42
|
+
if (data.uid !== uid) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check TTL
|
|
47
|
+
if (Date.now() - data.createdAt > NONCE_TTL) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return data;
|
|
52
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
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 CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
|
|
11
|
+
// Periodic cleanup of expired cache entries
|
|
12
|
+
setInterval(() => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
for (const [key, entry] of singlePageCache.entries()) {
|
|
15
|
+
if (now - entry.createdAt > CACHE_TTL) {
|
|
16
|
+
singlePageCache.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}, 10 * 60 * 1000).unref(); // cleanup every 10 minutes
|
|
20
|
+
|
|
21
|
+
const PdfHandler = module.exports;
|
|
22
|
+
|
|
23
|
+
PdfHandler.resolveFilePath = function (filename) {
|
|
24
|
+
// Sanitize: only allow basename (prevent directory traversal)
|
|
25
|
+
const safeName = path.basename(filename);
|
|
26
|
+
if (!safeName || safeName !== filename || safeName.includes('..')) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const uploadPath = nconf.get('upload_path') || path.join(nconf.get('base_dir'), 'public', 'uploads');
|
|
31
|
+
const filePath = path.join(uploadPath, 'files', safeName);
|
|
32
|
+
|
|
33
|
+
// Verify the resolved path is still within the upload directory
|
|
34
|
+
const resolvedPath = path.resolve(filePath);
|
|
35
|
+
const resolvedUploadDir = path.resolve(path.join(uploadPath, 'files'));
|
|
36
|
+
if (!resolvedPath.startsWith(resolvedUploadDir)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return filePath;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
PdfHandler.getFullPdf = async function (filename) {
|
|
44
|
+
const filePath = PdfHandler.resolveFilePath(filename);
|
|
45
|
+
if (!filePath) {
|
|
46
|
+
throw new Error('Invalid filename');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
throw new Error('File not found');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return fs.promises.readFile(filePath);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
PdfHandler.getSinglePagePdf = async function (filename) {
|
|
57
|
+
// Check cache first
|
|
58
|
+
const cached = singlePageCache.get(filename);
|
|
59
|
+
if (cached && (Date.now() - cached.createdAt < CACHE_TTL)) {
|
|
60
|
+
return cached.buffer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = PdfHandler.resolveFilePath(filename);
|
|
64
|
+
if (!filePath) {
|
|
65
|
+
throw new Error('Invalid filename');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(filePath)) {
|
|
69
|
+
throw new Error('File not found');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const existingPdfBytes = await fs.promises.readFile(filePath);
|
|
73
|
+
const srcDoc = await PDFDocument.load(existingPdfBytes);
|
|
74
|
+
|
|
75
|
+
const newDoc = await PDFDocument.create();
|
|
76
|
+
const [copiedPage] = await newDoc.copyPages(srcDoc, [0]);
|
|
77
|
+
newDoc.addPage(copiedPage);
|
|
78
|
+
|
|
79
|
+
const pdfBytes = await newDoc.save();
|
|
80
|
+
const buffer = Buffer.from(pdfBytes);
|
|
81
|
+
|
|
82
|
+
// Cache the result
|
|
83
|
+
singlePageCache.set(filename, {
|
|
84
|
+
buffer: buffer,
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return buffer;
|
|
89
|
+
};
|
package/library.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const meta = require.main.require('./src/meta');
|
|
4
|
+
const groups = require.main.require('./src/groups');
|
|
5
|
+
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
6
|
+
|
|
7
|
+
const controllers = require('./lib/controllers');
|
|
8
|
+
const nonceStore = require('./lib/nonce-store');
|
|
9
|
+
|
|
10
|
+
const plugin = {};
|
|
11
|
+
|
|
12
|
+
plugin.init = async (params) => {
|
|
13
|
+
const { router, middleware } = params;
|
|
14
|
+
|
|
15
|
+
// PDF direct access blocker middleware
|
|
16
|
+
// Intercepts requests to uploaded PDF files and returns 403
|
|
17
|
+
router.get('/assets/uploads/files/:filename', (req, res, next) => {
|
|
18
|
+
if (req.params.filename && req.params.filename.toLowerCase().endsWith('.pdf')) {
|
|
19
|
+
return res.status(403).json({ error: 'Direct PDF access is not allowed. Use the secure viewer.' });
|
|
20
|
+
}
|
|
21
|
+
next();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// PDF binary endpoint (nonce-validated)
|
|
25
|
+
router.get('/api/v3/plugins/pdf-secure/pdf-data', middleware.ensureLoggedIn, controllers.servePdfBinary);
|
|
26
|
+
|
|
27
|
+
// Admin page route
|
|
28
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
plugin.addRoutes = async ({ router, middleware, helpers }) => {
|
|
32
|
+
// Nonce generation endpoint
|
|
33
|
+
routeHelpers.setupApiRoute(router, 'get', '/pdf-secure/nonce', [middleware.ensureLoggedIn], async (req, res) => {
|
|
34
|
+
const { file } = req.query;
|
|
35
|
+
if (!file) {
|
|
36
|
+
return helpers.formatApiResponse(400, res, new Error('Missing file parameter'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sanitize filename
|
|
40
|
+
const path = require('path');
|
|
41
|
+
const safeName = path.basename(file);
|
|
42
|
+
if (!safeName || !safeName.toLowerCase().endsWith('.pdf')) {
|
|
43
|
+
return helpers.formatApiResponse(400, res, new Error('Invalid file'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get premium group name from settings (default: 'Premium')
|
|
47
|
+
const settings = await meta.settings.get('pdf-secure');
|
|
48
|
+
const premiumGroup = settings.premiumGroup || 'Premium';
|
|
49
|
+
|
|
50
|
+
// Check if user is in premium group
|
|
51
|
+
const isPremium = await groups.isMember(req.uid, premiumGroup);
|
|
52
|
+
|
|
53
|
+
const nonce = nonceStore.generate(req.uid, safeName, isPremium);
|
|
54
|
+
|
|
55
|
+
helpers.formatApiResponse(200, res, {
|
|
56
|
+
nonce: nonce,
|
|
57
|
+
isPremium: isPremium,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
plugin.addAdminNavigation = (header) => {
|
|
63
|
+
header.plugins.push({
|
|
64
|
+
route: '/plugins/pdf-secure',
|
|
65
|
+
icon: 'fa-file-pdf-o',
|
|
66
|
+
name: 'PDF Secure Viewer',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return header;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-pdf-secure",
|
|
3
|
+
"version": "1.0.1",
|
|
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,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-pdf-secure",
|
|
3
|
+
"url": "https://github.com/NodeBB/nodebb-plugin-pdf-secure",
|
|
4
|
+
"library": "./library.js",
|
|
5
|
+
"hooks": [
|
|
6
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
7
|
+
{ "hook": "static:api.routes", "method": "addRoutes" },
|
|
8
|
+
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" }
|
|
9
|
+
],
|
|
10
|
+
"staticDirs": {
|
|
11
|
+
"static": "./static"
|
|
12
|
+
},
|
|
13
|
+
"less": [
|
|
14
|
+
"static/style.less"
|
|
15
|
+
],
|
|
16
|
+
"scripts": [
|
|
17
|
+
"static/lib/main.js"
|
|
18
|
+
],
|
|
19
|
+
"templates": "./static/templates"
|
|
20
|
+
}
|
package/renovate.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
(async () => {
|
|
4
|
+
const hooks = await app.require('hooks');
|
|
5
|
+
|
|
6
|
+
hooks.on('action:ajaxify.end', () => {
|
|
7
|
+
interceptPdfLinks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function interceptPdfLinks() {
|
|
11
|
+
const postContents = document.querySelectorAll('[component="post/content"]');
|
|
12
|
+
postContents.forEach((content) => {
|
|
13
|
+
const pdfLinks = content.querySelectorAll('a[href$=".pdf"], a[href$=".PDF"]');
|
|
14
|
+
pdfLinks.forEach((link) => {
|
|
15
|
+
// Skip already processed links
|
|
16
|
+
if (link.dataset.pdfSecure) return;
|
|
17
|
+
link.dataset.pdfSecure = 'true';
|
|
18
|
+
|
|
19
|
+
// Extract filename from href
|
|
20
|
+
const href = link.getAttribute('href');
|
|
21
|
+
const parts = href.split('/');
|
|
22
|
+
const filename = parts[parts.length - 1];
|
|
23
|
+
|
|
24
|
+
// Create button replacement
|
|
25
|
+
const btn = document.createElement('button');
|
|
26
|
+
btn.type = 'button';
|
|
27
|
+
btn.className = 'btn btn-sm btn-outline-primary pdf-secure-btn';
|
|
28
|
+
btn.innerHTML = '<i class="fa fa-file-pdf-o"></i> ' + (link.textContent || filename);
|
|
29
|
+
btn.title = 'View PDF securely';
|
|
30
|
+
btn.dataset.filename = filename;
|
|
31
|
+
|
|
32
|
+
btn.addEventListener('click', (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
openSecureViewer(filename);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
link.replaceWith(btn);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function openSecureViewer(filename) {
|
|
44
|
+
try {
|
|
45
|
+
// Request nonce from server
|
|
46
|
+
const response = await fetch(
|
|
47
|
+
`${config.relative_path}/api/v3/plugins/pdf-secure/nonce?file=${encodeURIComponent(filename)}`,
|
|
48
|
+
{
|
|
49
|
+
credentials: 'same-origin',
|
|
50
|
+
headers: {
|
|
51
|
+
'x-csrf-token': config.csrf_token,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
if (response.status === 401) {
|
|
58
|
+
app.alertError('You must be logged in to view PDFs.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
app.alertError('Failed to load PDF viewer.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await response.json();
|
|
66
|
+
const { nonce, isPremium } = result.response;
|
|
67
|
+
|
|
68
|
+
// Create overlay with iframe
|
|
69
|
+
const overlay = document.createElement('div');
|
|
70
|
+
overlay.className = 'pdf-secure-overlay';
|
|
71
|
+
overlay.id = 'pdf-secure-overlay';
|
|
72
|
+
|
|
73
|
+
const iframe = document.createElement('iframe');
|
|
74
|
+
const viewerUrl = `${config.relative_path}/plugins/nodebb-plugin-pdf-secure/static/lib/viewer.html?nonce=${encodeURIComponent(nonce)}&premium=${isPremium}&apiBase=${encodeURIComponent(config.relative_path)}`;
|
|
75
|
+
iframe.src = viewerUrl;
|
|
76
|
+
iframe.className = 'pdf-secure-iframe';
|
|
77
|
+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
|
78
|
+
|
|
79
|
+
overlay.appendChild(iframe);
|
|
80
|
+
document.body.appendChild(overlay);
|
|
81
|
+
|
|
82
|
+
// Listen for close message from viewer
|
|
83
|
+
function onMessage(e) {
|
|
84
|
+
if (e.data && e.data.type === 'pdf-viewer-close') {
|
|
85
|
+
closeOverlay();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
window.addEventListener('message', onMessage);
|
|
89
|
+
|
|
90
|
+
// Close on Escape key
|
|
91
|
+
function onKeydown(e) {
|
|
92
|
+
if (e.key === 'Escape') {
|
|
93
|
+
closeOverlay();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
document.addEventListener('keydown', onKeydown);
|
|
97
|
+
|
|
98
|
+
function closeOverlay() {
|
|
99
|
+
window.removeEventListener('message', onMessage);
|
|
100
|
+
document.removeEventListener('keydown', onKeydown);
|
|
101
|
+
const el = document.getElementById('pdf-secure-overlay');
|
|
102
|
+
if (el) el.remove();
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
app.alertError('Failed to open PDF viewer.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})();
|