diff --git a/CHANGELOG.md b/CHANGELOG.md index e8883eacb3..ec4faa3658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +#### 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 diff --git a/Gruntfile.js b/Gruntfile.js index dcfa831cd6..60d8f8b23e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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 + }); } }); }); diff --git a/install/docker/entrypoint.sh b/install/docker/entrypoint.sh index dd17d707e7..db2a637ee0 100755 --- a/install/docker/entrypoint.sh +++ b/install/docker/entrypoint.sh @@ -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..." diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 1919638f01..43a64a9fa3 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -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() { diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 56d64674cf..ccd4261b36 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -258,10 +258,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 +281,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); }; @@ -296,6 +296,10 @@ uploadsController.uploadOgImage = async function (req, res, next) { async function upload(name, req, res, next) { const uploadedFile = req.files.files[0]; + if (uploadedFile.path.endsWith('.svg')) { + await sanitizeSvg(uploadedFile.path); + } + await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); await uploadImage(filename, 'system', uploadedFile, req, res, next); diff --git a/src/start.js b/src/start.js index 99f3b662c5..c4dd925aad 100644 --- a/src/start.js +++ b/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'); + } } } }); diff --git a/src/views/modals/temporary-ban.tpl b/src/views/modals/temporary-ban.tpl index d84bc28331..33bd5c8b47 100644 --- a/src/views/modals/temporary-ban.tpl +++ b/src/views/modals/temporary-ban.tpl @@ -12,7 +12,7 @@