mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-25 01:40:27 +01:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb913c152b | ||
|
|
32de562e70 | ||
|
|
955b27debc | ||
|
|
567ed8755b | ||
|
|
d5f57af342 | ||
|
|
de05dad251 | ||
|
|
af95cde187 | ||
|
|
5f5a697253 | ||
|
|
3b60931604 | ||
|
|
c7c83e0e4b | ||
|
|
7c00e814b7 | ||
|
|
bbb9a46019 | ||
|
|
5f696176b4 | ||
|
|
fe9b49e3d5 | ||
|
|
b74c789849 | ||
|
|
5a86415092 | ||
|
|
5bcf078a75 | ||
|
|
a8f4c5e63a | ||
|
|
3e961257ec | ||
|
|
7b14e26775 | ||
|
|
2490c312c9 | ||
|
|
a3fed408e5 | ||
|
|
8c69c6a0c4 | ||
|
|
da2597f81c | ||
|
|
dc37789b5d | ||
|
|
84d99a0fc7 | ||
|
|
6c5b22684b | ||
|
|
32faaba0e5 | ||
|
|
0ebb31fe87 | ||
|
|
8ab034d8f0 | ||
|
|
14e30c4bf8 | ||
|
|
0c9297f81c |
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,3 +1,128 @@
|
||||
#### v4.4.5 (2025-07-31)
|
||||
|
||||
##### Chores
|
||||
|
||||
* **config:** migrate config renovate.json (#13565) (5a864150)
|
||||
* incrementing version number - v4.4.4 (d323af44)
|
||||
* update changelog for v4.4.4 (7b14e267)
|
||||
* incrementing version number - v4.4.3 (d354c2eb)
|
||||
* incrementing version number - v4.4.2 (55c510ae)
|
||||
* incrementing version number - v4.4.1 (5ae79b4e)
|
||||
* incrementing version number - v4.4.0 (0a75eee3)
|
||||
* incrementing version number - v4.3.2 (b92b5d80)
|
||||
* incrementing version number - v4.3.1 (308e6b9f)
|
||||
* incrementing version number - v4.3.0 (bff291db)
|
||||
* incrementing version number - v4.2.2 (17fecc24)
|
||||
* incrementing version number - v4.2.1 (852a270c)
|
||||
* incrementing version number - v4.2.0 (87581958)
|
||||
* incrementing version number - v4.1.1 (b2afbb16)
|
||||
* incrementing version number - v4.1.0 (36c80850)
|
||||
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||
* incrementing version number - v4.0.5 (1792a62b)
|
||||
* incrementing version number - v4.0.4 (b1125cce)
|
||||
* incrementing version number - v4.0.3 (2b65c735)
|
||||
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||
* incrementing version number - v4.0.1 (a461b758)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
|
||||
##### New Features
|
||||
|
||||
* add filter:post.getDiffs (bbb9a460)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* clearTimeout if item is evicted from cache (5f696176)
|
||||
* use sharp to convert svg to png, closes #13534 (b74c7898)
|
||||
* use filename to check for svg, tempPath doesn't always have extension (5bcf078a)
|
||||
* apply sanitizeSvg to regular uploads and uploads from manage uploads acp page (a8f4c5e6)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* use promise.all (7c00e814)
|
||||
|
||||
##### Tests
|
||||
|
||||
* one more fix (5f5a6972)
|
||||
* fix spec (3b609316)
|
||||
* fix openapi (c7c83e0e)
|
||||
* increase timeout of failing test (fe9b49e3)
|
||||
|
||||
#### v4.4.4 (2025-06-18)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v4.4.3 (d354c2eb)
|
||||
* update changelog for v4.4.3 (0c9297f8)
|
||||
* incrementing version number - v4.4.2 (55c510ae)
|
||||
* incrementing version number - v4.4.1 (5ae79b4e)
|
||||
* incrementing version number - v4.4.0 (0a75eee3)
|
||||
* incrementing version number - v4.3.2 (b92b5d80)
|
||||
* incrementing version number - v4.3.1 (308e6b9f)
|
||||
* incrementing version number - v4.3.0 (bff291db)
|
||||
* incrementing version number - v4.2.2 (17fecc24)
|
||||
* incrementing version number - v4.2.1 (852a270c)
|
||||
* incrementing version number - v4.2.0 (87581958)
|
||||
* incrementing version number - v4.1.1 (b2afbb16)
|
||||
* incrementing version number - v4.1.0 (36c80850)
|
||||
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||
* incrementing version number - v4.0.5 (1792a62b)
|
||||
* incrementing version number - v4.0.4 (b1125cce)
|
||||
* incrementing version number - v4.0.3 (2b65c735)
|
||||
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||
* incrementing version number - v4.0.1 (a461b758)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
|
||||
##### New Features
|
||||
|
||||
* link to post in preview timestamp (8c69c6a0)
|
||||
* Add live reload functionality with Grunt watch and Socket.IO (#13489) (84d99a0f)
|
||||
* closes #13484, post preview changes (14e30c4b)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* sanitize svg when uploading site-logo, default avatar and og:image (da2597f8)
|
||||
* Revise package hash check in Docker entrypoint.sh (#13483) (6c5b2268)
|
||||
* more edge cases (32faaba0)
|
||||
* #13484, clear tooltip if cursor leaves link (0ebb31fe)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* fix lint (8ab034d8)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* send single message (dc37789b)
|
||||
|
||||
#### v4.4.3 (2025-06-09)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up composer (5f51dfc4)
|
||||
* incrementing version number - v4.4.2 (55c510ae)
|
||||
* update changelog for v4.4.2 (6d40a211)
|
||||
* incrementing version number - v4.4.1 (5ae79b4e)
|
||||
* incrementing version number - v4.4.0 (0a75eee3)
|
||||
* incrementing version number - v4.3.2 (b92b5d80)
|
||||
* incrementing version number - v4.3.1 (308e6b9f)
|
||||
* incrementing version number - v4.3.0 (bff291db)
|
||||
* incrementing version number - v4.2.2 (17fecc24)
|
||||
* incrementing version number - v4.2.1 (852a270c)
|
||||
* incrementing version number - v4.2.0 (87581958)
|
||||
* incrementing version number - v4.1.1 (b2afbb16)
|
||||
* incrementing version number - v4.1.0 (36c80850)
|
||||
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||
* incrementing version number - v4.0.5 (1792a62b)
|
||||
* incrementing version number - v4.0.4 (b1125cce)
|
||||
* incrementing version number - v4.0.3 (2b65c735)
|
||||
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||
* incrementing version number - v4.0.1 (a461b758)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* escape, query params (b02eb57d)
|
||||
* closes #13475, don't store escaped username (806e54bf)
|
||||
|
||||
#### v4.4.2 (2025-06-02)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -173,7 +173,10 @@ module.exports = function (grunt) {
|
||||
winston.error(err.stack);
|
||||
}
|
||||
if (worker) {
|
||||
worker.send({ compiling: compiling });
|
||||
worker.send({
|
||||
compiling: compiling,
|
||||
livereload: true, // Send livereload event via Socket.IO for instant browser refresh
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ Our minimalist "Harmony" theme gets you going right away, no coding experience r
|
||||
NodeBB requires the following software to be installed:
|
||||
|
||||
* A version of Node.js at least 20 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions))
|
||||
* MongoDB, version 3.6 or greater **or** Redis, version 2.8.9 or greater
|
||||
* MongoDB, version 5 or greater **or** Redis, version 7.2 or greater
|
||||
* If you are using [clustering](https://docs.nodebb.org/configuring/scaling/) you need Redis installed and configured.
|
||||
* nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB)
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ build_forum() {
|
||||
local config="$1"
|
||||
local start_build="$2"
|
||||
local package_hash=$(md5sum install/package.json | head -c 32)
|
||||
if [ "$package_hash" = "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then
|
||||
if [ "$package_hash" != "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then
|
||||
echo "package.json was updated. Upgrading..."
|
||||
/usr/src/app/nodebb upgrade --config="$config" || {
|
||||
echo "Failed to build NodeBB. Exiting..."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "nodebb",
|
||||
"license": "GPL-3.0",
|
||||
"description": "NodeBB Forum",
|
||||
"version": "4.4.3",
|
||||
"version": "4.4.6",
|
||||
"homepage": "https://www.nodebb.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -35,7 +35,6 @@
|
||||
"@isaacs/ttlcache": "1.4.1",
|
||||
"@nodebb/spider-detector": "2.0.3",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@resvg/resvg-js": "2.6.2",
|
||||
"@textcomplete/contenteditable": "0.1.13",
|
||||
"@textcomplete/core": "0.1.13",
|
||||
"@textcomplete/textarea": "0.1.13",
|
||||
|
||||
@@ -99,8 +99,12 @@ Loader.start = function () {
|
||||
function forkWorker(index, isPrimary) {
|
||||
const ports = getPorts();
|
||||
const args = [];
|
||||
const execArgv = [];
|
||||
if (nconf.get('max-memory')) {
|
||||
args.push(`--max-old-space-size=${nconf.get('max-memory')}`);
|
||||
execArgv.push(`--max-old-space-size=${nconf.get('max-memory')}`);
|
||||
}
|
||||
if (nconf.get('expose-gc')) {
|
||||
execArgv.push('--expose-gc');
|
||||
}
|
||||
if (!ports[index]) {
|
||||
return console.log(`[cluster] invalid port for worker : ${index} ports: ${ports.length}`);
|
||||
@@ -109,10 +113,10 @@ function forkWorker(index, isPrimary) {
|
||||
process.env.isPrimary = isPrimary;
|
||||
process.env.isCluster = nconf.get('isCluster') || ports.length > 1;
|
||||
process.env.port = ports[index];
|
||||
|
||||
const worker = fork(appPath, args, {
|
||||
silent: silent,
|
||||
env: process.env,
|
||||
execArgv: execArgv,
|
||||
});
|
||||
|
||||
worker.index = index;
|
||||
|
||||
@@ -24,6 +24,10 @@ get:
|
||||
response:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: number
|
||||
pid:
|
||||
type: number
|
||||
timestamps:
|
||||
type: array
|
||||
items:
|
||||
@@ -37,6 +41,8 @@ get:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
uid:
|
||||
type: number
|
||||
editable:
|
||||
type: boolean
|
||||
deletable:
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// fixes for global skin issues
|
||||
|
||||
// brite text-secondary is white :/
|
||||
.skin-brite .text-secondary {
|
||||
color: var(--bs-secondary-color) !important;
|
||||
}
|
||||
|
||||
// fix minty buttons
|
||||
.skin-minty .btn{
|
||||
.skin-minty .btn {
|
||||
color: initial!important;
|
||||
}
|
||||
@@ -307,66 +307,93 @@ define('forum/topic', [
|
||||
if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
let timeoutId = 0;
|
||||
let renderTimeout = 0;
|
||||
let destroyed = false;
|
||||
let link = null;
|
||||
|
||||
const postCache = {};
|
||||
function destroyTooltip() {
|
||||
clearTimeout(timeoutId);
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = 0;
|
||||
$('#post-tooltip').remove();
|
||||
destroyed = true;
|
||||
}
|
||||
|
||||
function onClickOutside(ev) {
|
||||
// If the click is outside the tooltip, destroy it
|
||||
if (!$(ev.target).closest('#post-tooltip').length) {
|
||||
destroyTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
$(window).one('action:ajaxify.start', destroyTooltip);
|
||||
$('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () {
|
||||
const link = $(this);
|
||||
|
||||
$('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () {
|
||||
link = $(this);
|
||||
link.removeAttr('over-tooltip');
|
||||
link.one('mouseleave', function () {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = 0;
|
||||
setTimeout(() => {
|
||||
if (!link.attr('over-tooltip') && !renderTimeout) {
|
||||
destroyTooltip();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
clearTimeout(renderTimeout);
|
||||
destroyed = false;
|
||||
|
||||
async function renderPost(pid) {
|
||||
const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`);
|
||||
$('#post-tooltip').remove();
|
||||
if (postData && ajaxify.data.template.topic) {
|
||||
postCache[pid] = postData;
|
||||
const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData });
|
||||
if (destroyed) {
|
||||
return;
|
||||
renderTimeout = setTimeout(async () => {
|
||||
async function renderPost(pid) {
|
||||
const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`);
|
||||
$('#post-tooltip').remove();
|
||||
if (postData && ajaxify.data.template.topic) {
|
||||
postCache[pid] = postData;
|
||||
const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData });
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
tooltip.hide().find('.timeago').timeago();
|
||||
tooltip.appendTo($('body')).fadeIn(300);
|
||||
const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first();
|
||||
const postRect = postContent.offset();
|
||||
const postWidth = postContent.width();
|
||||
const linkRect = link.offset();
|
||||
const { top } = link.get(0).getBoundingClientRect();
|
||||
const dropup = top > window.innerHeight / 2;
|
||||
tooltip.on('mouseenter', function () {
|
||||
link.attr('over-tooltip', 1);
|
||||
});
|
||||
tooltip.one('mouseleave', destroyTooltip);
|
||||
$(window).off('click', onClickOutside).one('click', onClickOutside);
|
||||
tooltip.css({
|
||||
top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30,
|
||||
left: postRect.left,
|
||||
width: postWidth,
|
||||
});
|
||||
}
|
||||
tooltip.hide().find('.timeago').timeago();
|
||||
tooltip.appendTo($('body')).fadeIn(300);
|
||||
const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first();
|
||||
const postRect = postContent.offset();
|
||||
const postWidth = postContent.width();
|
||||
const linkRect = link.offset();
|
||||
tooltip.css({
|
||||
top: linkRect.top + 30,
|
||||
left: postRect.left,
|
||||
width: postWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const href = link.attr('href');
|
||||
const location = utils.urlToLocation(href);
|
||||
const pathname = location.pathname;
|
||||
const validHref = href && href !== '#' && window.location.hostname === location.hostname;
|
||||
$('#post-tooltip').remove();
|
||||
const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/);
|
||||
const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/);
|
||||
if (postMatch) {
|
||||
const pid = postMatch[1];
|
||||
if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) {
|
||||
return; // dont render self post
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(async () => {
|
||||
const href = link.attr('href');
|
||||
const location = utils.urlToLocation(href);
|
||||
const pathname = location.pathname;
|
||||
const validHref = href && href !== '#' && window.location.hostname === location.hostname;
|
||||
$('#post-tooltip').remove();
|
||||
const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/);
|
||||
const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/);
|
||||
if (postMatch) {
|
||||
const pid = postMatch[1];
|
||||
if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) {
|
||||
return; // dont render self post
|
||||
}
|
||||
renderPost(pid);
|
||||
}, 300);
|
||||
} else if (topicMatch) {
|
||||
timeoutId = setTimeout(async () => {
|
||||
} else if (topicMatch) {
|
||||
const tid = topicMatch[1];
|
||||
const topicData = await api.get('/topics/' + tid, {});
|
||||
renderPost(topicData.mainPid);
|
||||
}, 300);
|
||||
}
|
||||
}).on('mouseleave', '[component="post"] a, [component="topic/event"] a', destroyTooltip);
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
function setupQuickReply() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"baseBranches": [
|
||||
"baseBranchPatterns": [
|
||||
"develop"
|
||||
],
|
||||
"labels": [
|
||||
@@ -14,8 +14,7 @@
|
||||
"dependencies"
|
||||
],
|
||||
"rangeStrategy": "pin",
|
||||
"matchPackageNames": [
|
||||
]
|
||||
"matchPackageNames": []
|
||||
},
|
||||
{
|
||||
"matchDepTypes": [
|
||||
|
||||
@@ -346,7 +346,16 @@ ActivityPub.get = async (type, id, uri, options) => {
|
||||
}
|
||||
};
|
||||
|
||||
ActivityPub.retryQueue = lru({ name: 'activitypub-retry-queue', max: 4000, ttl: 1000 * 60 * 60 * 24 * 60 });
|
||||
ActivityPub.retryQueue = lru({
|
||||
name: 'activitypub-retry-queue',
|
||||
max: 4000,
|
||||
ttl: 1000 * 60 * 60 * 24 * 60,
|
||||
dispose: (value) => {
|
||||
if (value) {
|
||||
clearTimeout(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// handle clearing retry queue from another member of the cluster
|
||||
pubsub.on(`activitypub-retry-queue:lruCache:del`, (keys) => {
|
||||
|
||||
@@ -491,10 +491,12 @@ async function diffsPrivilegeCheck(pid, uid) {
|
||||
|
||||
postsAPI.getDiffs = async (caller, data) => {
|
||||
await diffsPrivilegeCheck(data.pid, caller.uid);
|
||||
const timestamps = await posts.diffs.list(data.pid);
|
||||
const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']);
|
||||
const [timestamps, post, diffs] = await Promise.all([
|
||||
posts.diffs.list(data.pid),
|
||||
posts.getPostFields(data.pid, ['timestamp', 'uid']),
|
||||
posts.diffs.get(data.pid),
|
||||
]);
|
||||
|
||||
const diffs = await posts.diffs.get(data.pid);
|
||||
const uids = diffs.map(diff => diff.uid || null);
|
||||
uids.push(post.uid);
|
||||
let usernames = await user.getUsersFields(uids, ['username']);
|
||||
@@ -508,18 +510,21 @@ postsAPI.getDiffs = async (caller, data) => {
|
||||
|
||||
// timestamps returned by posts.diffs.list are strings
|
||||
timestamps.push(String(post.timestamp));
|
||||
|
||||
return {
|
||||
const result = await plugins.hooks.fire('filter:post.getDiffs', {
|
||||
uid: caller.uid,
|
||||
pid: data.pid,
|
||||
timestamps: timestamps,
|
||||
revisions: timestamps.map((timestamp, idx) => ({
|
||||
timestamp: timestamp,
|
||||
username: usernames[idx],
|
||||
uid: uids[idx],
|
||||
})),
|
||||
// Only admins, global mods and moderator of that cid can delete a diff
|
||||
deletable: isAdmin || isModerator,
|
||||
// These and post owners can restore to a different post version
|
||||
editable: isAdmin || isModerator || parseInt(caller.uid, 10) === parseInt(post.uid, 10),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
postsAPI.loadDiff = async (caller, data) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ const fs = require('fs/promises');
|
||||
const nconf = require('nconf');
|
||||
const winston = require('winston');
|
||||
const { default: satori } = require('satori');
|
||||
const { Resvg } = require('@resvg/resvg-js');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const utils = require('../utils');
|
||||
|
||||
@@ -96,9 +96,9 @@ Icons.regenerate = async (cid) => {
|
||||
await fs.writeFile(path.resolve(nconf.get('upload_path'), 'category', `category-${cid}-icon.svg`), svg);
|
||||
|
||||
// Generate and save PNG
|
||||
const resvg = new Resvg(Buffer.from(svg));
|
||||
const pngData = resvg.render();
|
||||
const pngBuffer = pngData.asPng();
|
||||
const pngBuffer = await sharp(Buffer.from(svg))
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
await fs.writeFile(path.resolve(nconf.get('upload_path'), 'category', `category-${cid}-icon.png`), pngBuffer);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const path = require('path');
|
||||
const nconf = require('nconf');
|
||||
const fs = require('fs');
|
||||
const winston = require('winston');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
const meta = require('../../meta');
|
||||
const posts = require('../../posts');
|
||||
@@ -157,50 +156,11 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) {
|
||||
return next(new Error('[[error:invalid-json]]'));
|
||||
}
|
||||
|
||||
if (uploadedFile.path.endsWith('.svg')) {
|
||||
await sanitizeSvg(uploadedFile.path);
|
||||
}
|
||||
|
||||
await validateUpload(uploadedFile, allowedImageTypes);
|
||||
const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`;
|
||||
await uploadImage(filename, 'category', uploadedFile, req, res, next);
|
||||
};
|
||||
|
||||
async function sanitizeSvg(filePath) {
|
||||
const dirty = await fs.promises.readFile(filePath, 'utf8');
|
||||
const clean = sanitizeHtml(dirty, {
|
||||
allowedTags: [
|
||||
'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop',
|
||||
'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect',
|
||||
'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern',
|
||||
'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode',
|
||||
],
|
||||
allowedAttributes: {
|
||||
'*': [
|
||||
// Geometry
|
||||
'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
|
||||
'width', 'height', 'd', 'points', 'viewBox', 'transform',
|
||||
|
||||
// Presentation
|
||||
'fill', 'stroke', 'stroke-width', 'opacity',
|
||||
'stop-color', 'stop-opacity', 'offset', 'style', 'class',
|
||||
|
||||
// Text
|
||||
'text-anchor', 'font-size', 'font-family',
|
||||
|
||||
// Misc
|
||||
'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform',
|
||||
'xmlns', 'preserveAspectRatio',
|
||||
],
|
||||
},
|
||||
parser: {
|
||||
lowerCaseTags: false,
|
||||
lowerCaseAttributeNames: false,
|
||||
},
|
||||
});
|
||||
await fs.promises.writeFile(filePath, clean);
|
||||
}
|
||||
|
||||
uploadsController.uploadFavicon = async function (req, res, next) {
|
||||
const uploadedFile = req.files.files[0];
|
||||
const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
|
||||
@@ -258,10 +218,6 @@ uploadsController.uploadMaskableIcon = async function (req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
uploadsController.uploadLogo = async function (req, res, next) {
|
||||
await upload('site-logo', req, res, next);
|
||||
};
|
||||
|
||||
uploadsController.uploadFile = async function (req, res, next) {
|
||||
const uploadedFile = req.files.files[0];
|
||||
let params;
|
||||
@@ -285,6 +241,10 @@ uploadsController.uploadFile = async function (req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
uploadsController.uploadLogo = async function (req, res, next) {
|
||||
await upload('site-logo', req, res, next);
|
||||
};
|
||||
|
||||
uploadsController.uploadDefaultAvatar = async function (req, res, next) {
|
||||
await upload('avatar-default', req, res, next);
|
||||
};
|
||||
|
||||
40
src/file.js
40
src/file.js
@@ -7,6 +7,7 @@ const winston = require('winston');
|
||||
const { mkdirp } = require('mkdirp');
|
||||
const mime = require('mime');
|
||||
const graceful = require('graceful-fs');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
const slugify = require('./slugify');
|
||||
|
||||
@@ -27,6 +28,10 @@ file.saveFileToLocal = async function (filename, folder, tempPath) {
|
||||
|
||||
winston.verbose(`Saving file ${filename} to : ${uploadPath}`);
|
||||
await mkdirp(path.dirname(uploadPath));
|
||||
if (filename.endsWith('.svg')) {
|
||||
await sanitizeSvg(tempPath);
|
||||
}
|
||||
|
||||
await fs.promises.copyFile(tempPath, uploadPath);
|
||||
return {
|
||||
url: `/assets/uploads/${folder ? `${folder}/` : ''}${filename}`,
|
||||
@@ -155,4 +160,39 @@ file.walk = async function (dir) {
|
||||
return files.reduce((a, f) => a.concat(f), []);
|
||||
};
|
||||
|
||||
async function sanitizeSvg(filePath) {
|
||||
const dirty = await fs.promises.readFile(filePath, 'utf8');
|
||||
const clean = sanitizeHtml(dirty, {
|
||||
allowedTags: [
|
||||
'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop',
|
||||
'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect',
|
||||
'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern',
|
||||
'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode',
|
||||
],
|
||||
allowedAttributes: {
|
||||
'*': [
|
||||
// Geometry
|
||||
'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
|
||||
'width', 'height', 'd', 'points', 'viewBox', 'transform',
|
||||
|
||||
// Presentation
|
||||
'fill', 'stroke', 'stroke-width', 'opacity',
|
||||
'stop-color', 'stop-opacity', 'offset', 'style', 'class',
|
||||
|
||||
// Text
|
||||
'text-anchor', 'font-size', 'font-family',
|
||||
|
||||
// Misc
|
||||
'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform',
|
||||
'xmlns', 'preserveAspectRatio',
|
||||
],
|
||||
},
|
||||
parser: {
|
||||
lowerCaseTags: false,
|
||||
lowerCaseAttributeNames: false,
|
||||
},
|
||||
});
|
||||
await fs.promises.writeFile(filePath, clean);
|
||||
}
|
||||
|
||||
require('./promisify')(file);
|
||||
|
||||
@@ -16,7 +16,7 @@ const utils = require('../utils');
|
||||
const CSS = module.exports;
|
||||
|
||||
CSS.supportedSkins = [
|
||||
'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', 'litera',
|
||||
'brite', 'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', 'litera',
|
||||
'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone',
|
||||
'simplex', 'sketchy', 'slate', 'solar', 'spacelab', 'superhero', 'united',
|
||||
'vapor', 'yeti', 'zephyr',
|
||||
@@ -270,7 +270,7 @@ CSS.getSkinSwitcherOptions = async function (uid) {
|
||||
{ name: '[[user:no-skin]]', value: 'noskin', selected: userSettings.bootswatchSkin === 'noskin' },
|
||||
];
|
||||
const lightSkins = [
|
||||
'cerulean', 'cosmo', 'flatly', 'journal', 'litera',
|
||||
'brite', 'cerulean', 'cosmo', 'flatly', 'journal', 'litera',
|
||||
'lumen', 'lux', 'materia', 'minty', 'morph', 'pulse', 'sandstone',
|
||||
'simplex', 'sketchy', 'spacelab', 'united', 'yeti', 'zephyr',
|
||||
];
|
||||
|
||||
25
src/start.js
25
src/start.js
@@ -107,13 +107,24 @@ function addProcessHandlers() {
|
||||
shutdown(1);
|
||||
});
|
||||
process.on('message', (msg) => {
|
||||
if (msg && Array.isArray(msg.compiling)) {
|
||||
if (msg.compiling.includes('tpl')) {
|
||||
const benchpressjs = require('benchpressjs');
|
||||
benchpressjs.flush();
|
||||
} else if (msg.compiling.includes('lang')) {
|
||||
const translator = require('./translator');
|
||||
translator.flush();
|
||||
if (msg) {
|
||||
if (Array.isArray(msg.compiling)) {
|
||||
if (msg.compiling.includes('tpl')) {
|
||||
const benchpressjs = require('benchpressjs');
|
||||
benchpressjs.flush();
|
||||
} else if (msg.compiling.includes('lang')) {
|
||||
const translator = require('./translator');
|
||||
translator.flush();
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.livereload) {
|
||||
// Send livereload event to all connected clients via Socket.IO
|
||||
const websockets = require('./socket.io');
|
||||
if (websockets.server) {
|
||||
websockets.server.emit('event:livereload');
|
||||
winston.info('[livereload] Sent reload event to all clients');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="col-5">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="1" />
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<div id="post-tooltip" class="card card-body shadow bg-body text-body z-1 position-absolute">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex gap-1 align-items-center">
|
||||
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">
|
||||
{buildAvatar(post.user, "24px", true, "", "user/picture")} {post.user.username}
|
||||
</a>
|
||||
<span class="timeago text-xs" title="{post.timestampISO}"></span>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div>
|
||||
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">{buildAvatar(post.user, "20px", true, "", "user/picture")}</a>
|
||||
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">{post.user.username}</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{config.relative_path}/post/{post.pid}" class="timeago text-xs text-secondary lh-1" style="vertical-align: middle;" title="{post.timestampISO}"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">{post.content}</div>
|
||||
<div class="content ghost-scrollbar" style="max-height: 300px; overflow-y:auto;">{post.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -466,7 +466,8 @@ describe('Notes', () => {
|
||||
assert.strictEqual(cid, remoteCid);
|
||||
});
|
||||
|
||||
it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async () => {
|
||||
it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async function () {
|
||||
this.timeout(60000);
|
||||
const { id: remoteCid } = helpers.mocks.group({
|
||||
id: `https://example.com/${utils.generateUUID()}`,
|
||||
});
|
||||
|
||||
4
test/files/dirty.svg
Normal file
4
test/files/dirty.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="10" width="80" height="80" fill="red" stroke="black" stroke-width="4"/>
|
||||
</svg>
|
||||
<script>alert('foo');</script>
|
||||
|
After Width: | Height: | Size: 192 B |
@@ -338,6 +338,15 @@ describe('Upload Controllers', () => {
|
||||
assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.png`);
|
||||
});
|
||||
|
||||
it('should upload svg as category image after cleaning it up', async () => {
|
||||
const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/dirty.svg'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token);
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert(Array.isArray(body));
|
||||
assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.svg`);
|
||||
const svgContents = await fs.readFile(path.join(__dirname, '../test/uploads/category/category-1.svg'), 'utf-8');
|
||||
assert.strictEqual(svgContents.includes('<script>'), false);
|
||||
});
|
||||
|
||||
it('should upload default avatar', async () => {
|
||||
const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadDefaultAvatar`, path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token);
|
||||
assert.equal(response.statusCode, 200);
|
||||
|
||||
Reference in New Issue
Block a user