Merge commit '72f48fd9c47eaddfacd85d5ad963236190270278' into v4.x

This commit is contained in:
Misty Release Bot
2026-03-25 15:03:11 +00:00
21 changed files with 338 additions and 46 deletions

View File

@@ -1,3 +1,234 @@
#### v4.10.0 (2026-03-19)
##### Chores
* **deps:**
* update dependency lint-staged to v16.4.0 (#14099) (bea46026)
* update commitlint monorepo to v20.5.0 (#14098) (06c3b88b)
* update dependency jsdom to v29 (#14100) (3825c755)
* update commitlint monorepo to v20.4.4 (#14088) (02f8ea2c)
* update dependency lint-staged to v16.3.3 (#14075) (ac45b719)
* update dependency @stylistic/eslint-plugin to v5.10.0 (#14065) (9e2c6b67)
* update docker/build-push-action action to v7 (#14066) (6d8c4493)
* update docker/metadata-action action to v6 (#14067) (73b5bce5)
* update docker/setup-buildx-action action to v4 (#14060) (d7de8cf6)
* update docker/login-action action to v4 (#14054) (8c15096f)
* update dependency lint-staged to v16.3.2 (#14048) (ddd6db0f)
* update commitlint monorepo to v20.4.3 (#14047) (07881cbf)
* update dependency lint-staged to v16.3.1 (#14029) (2e4ee9f1)
* update dependency globals to v17.4.0 (#14035) (32864460)
* update github artifact actions (#14027) (aec68c6b)
* update postgres docker tag to v18.3 (#14023) (b69dbc38)
* update dependency nyc to v18 (#14011) (dc1ce5e1)
* up develop (4a01d55f)
* up mentions (168b17e8)
* up composer (73b023b4)
* up harmony (ec4e87ff)
* incrementing version number - v4.9.2 (e6846052)
* update changelog for v4.9.2 (2c00b137)
* incrementing version number - v4.9.1 (72e44c86)
* incrementing version number - v4.9.0 (3fdd1bef)
* incrementing version number - v4.8.1 (713ae0c0)
* incrementing version number - v4.8.0 (3fac737a)
* incrementing version number - v4.7.2 (cd419d8a)
* incrementing version number - v4.7.1 (afb88805)
* incrementing version number - v4.7.0 (e82d40f8)
* incrementing version number - v4.6.3 (9fc5b0f3)
* incrementing version number - v4.6.2 (f98747db)
* incrementing version number - v4.6.1 (f47aa678)
* incrementing version number - v4.6.0 (ee395bc5)
* incrementing version number - v4.5.2 (ad2da639)
* incrementing version number - v4.5.1 (69f4b61f)
* incrementing version number - v4.5.0 (f05c5d06)
* incrementing version number - v4.4.6 (074043ad)
* incrementing version number - v4.4.5 (6f106923)
* incrementing version number - v4.4.4 (d323af44)
* 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)
* **i18n:**
* fallback strings for new resources: nodebb.admin-settings-activitypub (aeb53043)
* fallback strings for new resources: nodebb.world (e9063a63)
* fallback strings for new resources: nodebb.world (61414fa4)
* fallback strings for new resources: nodebb.world (702f7c62)
* fallback strings for new resources: nodebb.admin-settings-general (e3ba38f2)
* fallback strings for new resources: nodebb.admin-manage-users (a46f0136)
* fallback strings for new resources: nodebb.admin-advanced-jobs (8e050353)
* fallback strings for new resources: nodebb.admin-menu (5a8a1661)
* fallback strings for new resources: nodebb.error (a17cd6c7)
* fallback strings for new resources: nodebb.world (4d44e913)
* fallback strings for new resources: nodebb.admin-dashboard (49a21a1f)
* fallback strings for new resources: nodebb.admin-dashboard (858d84ff)
##### Documentation Changes
* wrong type for worldDefaultCid (895997b2)
##### New Features
* add /world as a potential home page route (58d3aa77)
* add category selector to /world quick composer (2f5021e5)
* ability to show only local posts in /world (44e65b8d)
* #14094, notification drawer UX improvements (6c01a5d8)
* allow 3 profile pics (#14092) (533ae69c)
* screenshot upload in ACP, send fallback brand icons in manifest, serve assets for richer PWA install UI (75a6dfff)
* category group actor outbox, #14083 (b317cdd3)
* new ap mocks, now publishing user outboxes (f848393e)
* show cronjobs in acp (#14068) (3c0a6540)
* redirect cold requests to remote resources to their canonical source, #14043 (2b12f8b5)
* include alt text in image/attachment property federating out (ca5aee10)
##### Bug Fixes
* improve idempotency of ap test (8ca34e74)
* call syncfollowcounts on unfollow as well (ebe709da)
* sync follow counts on local and remote follows, #14105 (44e78e47)
* cold load redirect should only affect guests (cc606677)
* schema fix for new api config value (7e2c7db3)
* close notif drawer on item click, fix crash in module (7c65471b)
* schema fix for new api config value (efaf8eb9)
* issue where initial quickcreate post wouldn't go to the right cid (35c03e5c)
* only show category selector on quickreply on /world (27b0fbe6)
* delete cid:<cid>:privilegeMask on category.purge (902533db)
* bump themes (3aa8d5ba)
* removing topic tools/checkbox from /world for guests, reword guest CTA in /world (53286625)
* add back 'after' query param handling in /world that was removed accidentally (67a93da5)
* restrict contextmenu preventDefault to the checkbox only (2eb0964d)
* long-press support for topicSelect, #14045 (d1e1a008)
* debug log (58da9036)
* restore guest access to /world, default to latest(all) (1aa5ca88)
* bump harmony (10859455)
* restored popular calculation behaviour that was broken by e2131d1d2e1c6f14cb8867ac7e22840da3f4c63f, removed followingOnly arg passing for popular (1af83564)
* imagesLoaded integration for handleBack in world.js (38a1da46)
* merged chat notifications if all the messages are from the same user (26bb60ef)
* type (59dd22ca)
* type (40fecd01)
* merged chat notifications if all the messages are from the same user (6147a4d0)
* screenshot fallback (09c54127)
* buildRecipients to handle if local uids are passed in followers (a8f081c0)
* update Like/Dislike to have addressees in activity (c8e349ca)
* accidental hardcoded cid (464bc275)
* skip AP cache on context processing methods (a3ee7447)
* skip AP cache on context processing methods (10e4d579)
* cache key (9eea12ec)
* cache key (74fa77dd)
* missing orderedItems on category outbox index retrieval (8496e1ef)
* #14084, fix tags not getting properly removed from topics (0a94cecb)
* group badge on group details page (52e8ede8)
* return digest header only if it is set to something (aka not null) (1ad9ce5f)
* missing page parseInt (9978af59)
* dont add self username when clicking reply (9fcaad38)
* notifs (ed3a3672)
* also use tx.compile in chat notif merge (6d22e33a)
* publish id with user outbox, fixes #13478 (c08a45a5)
* merge with txArgs (e1d0e2a0)
* merged notification translations (34b68109)
* derped handleBack in world.js (ac483152)
* syntax error on undefined value (6b3b3e7e)
* filter out image attachments from remote data if they are already embedded in content (40b8544f)
* update thumbs loading logic to always include post attachments as part of thumbs (prior: was controlled by thumbsOnly flag orshowPostUploadsAsThumbnail setting) (c2d190e1)
* #14072, world to call thumbs with thumbsOnly filter (f1976168)
* promises in groups.leave (f826e629)
* #14071, duplicate items loaded via IS on /world (d29f1fbd)
* #14043, cold-load redirect should only affect guests (5a7316b1)
* hacking handleBack module to work with world page (971c8603)
* bump web-push (27e12a28)
* patch translateKey to wrap arguments in first string isolate (FSI) and Pop Directional Isolate (PDI) characters (59f19ba4)
* add missing db call (4d1d1c86)
* #14061, world.js show more buttons on infinite scroll (0aead782)
* update clamp-fade to use mask-image, add background to btn-link on Brite skin (9bc1b400)
* #14046, sneak in a mention to the community in mocked replies (b8ef027c)
* world page 'see more' bugs (cc733631)
* #13239, unescape custom user field values (9e69b9ad)
* restore `preview` as it is now supported by BridyFed (8a371d23)
* missing done (9604a0cd)
* bump harmony, #14042 (b02cdaa9)
* #14042, adopt plugin-feed's show-more/less logic/scss (43f2951a)
* update minimum title length default to zero to allow title-less topics via composer (6bfe3cd0)
* parent cid (f567d970)
* skip parsing of duplicate emoji tags (363cad29)
* **deps:**
* update dependency cronstrue to v3.14.0 (#14107) (f51e1b2a)
* update dependency nodemailer to v8.0.3 (#14104) (fa7c1a52)
* update dependency esbuild to v0.27.4 (#14090) (c26bfddf)
* update dependency lru-cache to v11.2.7 (#14096) (3765fb37)
* update dependency tough-cookie to v6.0.1 (#14097) (d5f4a370)
* update dependency nodebb-theme-peace to v2.2.57 (#14076) (cd08a5e4)
* update dependency nodemailer to v8.0.2 (#14077) (add3c651)
* update dependency satori to v0.25.0 (#14037) (817c38b9)
* update dependency postcss to v8.5.8 (#14051) (942619db)
* update dependency pg to v8.20.0 (#14058) (9cc2c2f9)
* update dependency pg-cursor to v2.19.0 (#14059) (045a7073)
* update dependency terser-webpack-plugin to v5.3.17 (#14052) (4410d884)
* update dependency multer to v2.1.1 (#14050) (de22b7a9)
* update dependency webpack to v5.105.4 (#14053) (3dc3b2e2)
* update dependency fs-extra to v11.3.4 (#14049) (cfb6145e)
* update dependency satori to v0.19.3 (#14036) (250911b7)
* update dependency nodebb-plugin-emoji to v6.0.6 (#14034) (7434103c)
* update dependency sitemap to v9.0.1 (#14028) (f97484c2)
* update dependency webpack to v5.105.3 (#14022) (38787a2d)
* update dependency multer to v2.1.0 (#14024) (9b65e316)
* update dependency pg-cursor to v2.18.0 (#14026) (54810cfd)
* update dependency pg to v8.19.0 (#14025) (badb57f2)
* update dependency autoprefixer to v10.4.27 (#14021) (054b4aa6)
##### Other Changes
* remove unused (19bb37ca)
* jobs.json (e1b6e617)
* remove unused (d6d3116e)
* remove unused (b50a10df)
##### Performance Improvements
* switch to set, remove parseFloat in redis (09de6fb9)
* move out nconf.get and isClientScript regex (4d55ee0a)
* move out nconf.get and isClientScript regex (f2bca332)
* make a single round trip for set(s)Remove (bcbb7bc4)
* cache groups:createtime (380d9895)
##### Refactors
* /world sorting logic to always use topics/sorted logic (e2131d1d)
* move to data (36bf3f16)
* get rid of cleanupUids use missing set (6569ea51)
* remove async.series, use batch.processSortedSet (894248e6)
* get rid of helper function (1cc77343)
* switch to cursor (1c7daf0d)
* use set (d9344140)
* pass in cid to rename/remove (fe4a22fb)
* remove admin.themes.getInstalled (92d72f67)
##### Tests
* add missing selectedCategory to world.yaml (779a372f)
* exclude uploadScreenshot from routeMap parsing test (ff1e1b92)
* make tests happy (08bed89b)
* fix test maybe (25f6088f)
* add one more topic to tag test (e01cb104)
* cleaner user.delete test (215d6440)
* set minimumtitlelength for test (7429b5d4)
* added test to ensure that Likes do not get processed when privilege is rescinded (8cba65cd)
* break apart inbox handling tests to its own file in test/activitypub (06e0bd6a)
* add debug test to see if failing test is due to race condition (e3119c76)
* fix spec (6dd9f734)
#### v4.9.2 (2026-03-11)
##### Chores

View File

@@ -108,7 +108,7 @@
"nodebb-plugin-spam-be-gone": "2.3.2",
"nodebb-plugin-web-push": "0.7.7",
"nodebb-rewards-essentials": "1.0.2",
"nodebb-theme-harmony": "2.2.61",
"nodebb-theme-harmony": "2.2.62",
"nodebb-theme-lavender": "7.1.21",
"nodebb-theme-peace": "2.2.57",
"nodebb-theme-persona": "14.2.33",

View File

@@ -104,6 +104,8 @@
"watch.title": "Be notified of new replies in this topic",
"unwatch.title": "Stop watching this topic",
"share-this-post": "Share this Post",
"share-mail-subject": "Check out this post on \"%1\"",
"share-mail-body": "I thought you might be interested in this post: %1",
"watching": "Watching",
"not-watching": "Not Watching",
"ignoring": "Ignoring",

View File

@@ -115,14 +115,20 @@ define('forum/category', [
}
function handleDescription() {
const fadeEl = document.querySelector(`.description.clamp-fade-sm-4`);
if (!fadeEl) {
return;
}
fadeEl.addEventListener('click', () => {
const state = fadeEl.classList.contains('line-clamp-4');
fadeEl.classList.toggle('line-clamp-4', !state);
const fadeEl = $(`.description[class*="clamp-fade-"]`);
fadeEl.on('click', function () {
const $this = $(this);
let clampClass = $this.data('clampClass');
if (!clampClass) {
const match = $this.attr('class').match(/line-clamp-(\S+)/);
if (match && match[1]) {
clampClass = `line-clamp-${match[1]}`;
fadeEl.data('clampClass', clampClass);
}
}
if (clampClass) {
fadeEl.toggleClass(clampClass);
}
});
}

View File

@@ -9,6 +9,9 @@ define('forum/header/notifications', function () {
notifTrigger.on('show.bs.dropdown', async (ev) => {
const notifications = await app.require('notifications');
const triggerEl = $(ev.target);
const dropdownEl = triggerEl.parent().find('.dropdown-menu');
dropdownEl.find('[data-filter="all"]').addClass('active');
dropdownEl.find('[data-filter="unread"]').removeClass('active');
notifications.loadNotifications(triggerEl, triggerEl.parent().find('[component="notifications/list"]'));
});

View File

@@ -1,7 +1,7 @@
'use strict';
define('share', ['hooks'], function (hooks) {
define('share', ['hooks', 'translator'], function (hooks, translator) {
const share = {};
const baseUrl = window.location.protocol + '//' + window.location.host;
@@ -17,7 +17,7 @@ define('share', ['hooks'], function (hooks) {
$('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () {
const postLink = $(this).find('.post-link');
postLink.val(baseUrl + getPostUrl($(this)));
postLink.val(getPostUrl($(this)));
// without the setTimeout can't select the text in the input
setTimeout(function () {
@@ -69,6 +69,16 @@ define('share', ['hooks'], function (hooks) {
return openShare(mastodon_url, postUrl, 626, 760);
});
addHandler('[component="share/email"]', async function () {
const postUrl = getPostUrl($(this));
const [subject, body] = await translator.translateKeys([
translator.compile('topic:share-mail-subject', config.siteTitle),
translator.compile('topic:share-mail-body', postUrl),
]);
const mailtoUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
window.location.href = mailtoUrl;
});
hooks.fire('action:share.addHandlers', { openShare: openShare });
};
@@ -77,9 +87,10 @@ define('share', ['hooks'], function (hooks) {
}
function getPostUrl(clickedElement) {
const pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10);
const path = '/post' + (pid ? '/' + (pid) : '');
return baseUrl + config.relative_path + path;
const pid = clickedElement.parents('[data-pid]').attr('data-pid');
return pid ?
`${baseUrl + config.relative_path}/post/${pid}` :
window.location.href;
}
return share;

View File

@@ -409,21 +409,20 @@ authenticationController.localLogin = async function (req, username, password, n
userData.isAdminOrGlobalMod = isAdminOrGlobalMod;
if (!canLoginIfBanned) {
return next(await getBanError(uid));
}
// Doing this after the ban check, because user's privileges might change after a ban expires
const hasLoginPrivilege = await privileges.global.can('local:login', uid);
if (parseInt(uid, 10) && !hasLoginPrivilege) {
return next(new Error('[[error:local-login-disabled]]'));
}
try {
const passwordMatch = await user.isPasswordCorrect(uid, password, req.ip);
if (!passwordMatch) {
return next(new Error('[[error:invalid-login-credentials]]'));
}
if (!canLoginIfBanned) {
return next(await getBanError(uid));
}
// Doing this after the ban check, because user's privileges might change after a ban expires
const hasLoginPrivilege = await privileges.global.can('local:login', uid);
if (parseInt(uid, 10) && !hasLoginPrivilege) {
return next(new Error('[[error:local-login-disabled]]'));
}
} catch (e) {
if (req.loggedIn) {
await logoutAsync(req);

View File

@@ -45,6 +45,11 @@ social.getPostSharing = async function () {
name: 'Mastodon',
class: 'fa-brands fa-mastodon',
},
{
id: 'email',
name: 'Email',
class: 'fa-regular fa-envelope',
},
];
networks = await plugins.hooks.fire('filter:social.posts', networks);
networks.forEach((network) => {

View File

@@ -134,7 +134,7 @@ module.exports = function (User) {
`uid:${uid}:flag:pids`,
`uid:${uid}:sessions`,
`uid:${uid}:shares`,
`uid:${uid}:profile:images`,
`uid:${uid}:profile:pictures`,
`invitation:uid:${uid}`,
];

View File

@@ -143,7 +143,7 @@ module.exports = function (User) {
const filename = generateProfileImageFilename(updateUid, extension);
const uploadedImage = await image.uploadImage(filename, `profile/uid-${updateUid}`, {
uid: updateUid,
path: picture.path,
path: normalizedPath,
name: 'profileAvatar',
});

View File

@@ -125,7 +125,7 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">[[admin/extend/plugins:order-active]]</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<p>

View File

@@ -6,7 +6,7 @@
<div class="">
<div component="toaster/tray" class="alert-window fixed-bottom mb-5 mb-md-2 me-2 me-md-5 ms-auto" style="width:300px; z-index: 1090;">
<div id="reconnect-alert" class="alert alert-dismissible alert-warning fade hide" component="toaster/toast">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="[[global:buttons.close]]"></button>
<p class="mb-0">[[global:reconnecting-message, {config.siteTitle}]]</p>
</div>
</div>

View File

@@ -45,12 +45,12 @@
</div>
</div>
<div class="modal fade" id="create-modal">
<div class="modal fade" id="create-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">[[admin/manage/tags:create]]</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<form>

View File

@@ -1,9 +1,9 @@
<div class="modal fade" id="create-modal">
<div class="modal fade" id="create-modal" tabindex="-1" aria-label="[[admin/manage/groups:create]]">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">[[admin/manage/groups:create]]</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<form>

View File

@@ -1,8 +1,8 @@
<div id="chat-modal" class="chat-modal d-flex flex-nowrap modal hide overflow-visible" tabindex="-1" role="dialog" aria-labelledby="Chat" aria-hidden="true" data-center="false">
<div id="chat-modal" class="chat-modal d-flex flex-nowrap modal overflow-visible" tabindex="-1" role="dialog" aria-labelledby="chat-room-title-{roomId}" data-center="false">
<div class="modal-dialog">
<div class="modal-content" component="chat/message/window">
<div class="modal-header d-flex gap-4 justify-content-between">
<div class="fs-6 flex-grow-1 fw-semibold tracking-tight text-truncate text-nowrap" component="chat/room/name" data-icon="{icon}">{{{ if ./roomName }}}<i class="fa {icon} text-muted"></i> {roomName}{{{ else }}}{./chatWithMessage}{{{ end}}}</div>
<div id="chat-room-title-{roomId}" class="fs-6 flex-grow-1 fw-semibold tracking-tight text-truncate text-nowrap" component="chat/room/name" data-icon="{icon}">{{{ if ./roomName }}}<i class="fa {icon} text-muted"></i> {roomName}{{{ else }}}{./chatWithMessage}{{{ end}}}</div>
<div class="d-flex gap-1 align-items-center">
<button type="button" class="btn btn-ghost btn-sm d-none d-md-flex align-self-stretch align-items-center" data-action="maximize" title="[[modules:chat.maximize]]" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="fa fa-fw fa-expand text-muted"></i>

View File

@@ -1,9 +1,9 @@
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-hidden="true">
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-labelledby="crop-picture">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 id="crop-picture">[[user:crop-picture]]</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<div id="upload-progress-box" class="progress hide">
@@ -30,7 +30,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" aria-hidden="true">Close</button>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button class="btn btn-primary upload-btn {{{ if !allowSkippingCrop }}}hidden{{{ end }}}">[[user:upload-picture]]</button>
<button class="btn btn-primary crop-btn">[[user:upload-cropped-picture]]</button>
</div>

View File

@@ -1,9 +1,9 @@
<div class="modal" tabindex="-1" role="dialog" aria-labelledby="[[flags:modal-title]]" aria-hidden="true">
<div class="modal" tabindex="-1" role="dialog" aria-label="[[flags:modal-title]]">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">[[flags:modal-title]]</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<p class="lead">

View File

@@ -1,9 +1,9 @@
<div class="modal" tabindex="-1" role="dialog" aria-labelledby="upload-file" aria-hidden="true">
<div class="modal" tabindex="-1" role="dialog" aria-label="{title}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<form class="mb-3" id="uploadForm" action="" method="post" enctype="multipart/form-data">
@@ -36,7 +36,7 @@
<div id="alert-error" class="alert alert-danger hide"></div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" aria-hidden="true">[[global:close]]</button>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">[[global:close]]</button>
<button id="fileUploadSubmitBtn" class="btn btn-primary">{button}</button>
</div>
</div>

View File

@@ -1,15 +1,15 @@
<div id="upload-picture-from-url-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="upload-picture-url" aria-hidden="true">
<div id="upload-picture-from-url-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="upload-picture-url" aria-label="[[user:upload-picture]]">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="upload-picture-url">[[user:upload-picture]]</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="[[global:buttons.close]]"></button>
</div>
<div class="modal-body">
<input id="uploadFromUrl" class="form-control" type="text"/>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" aria-hidden="true">[[global:close]]</button>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">[[global:close]]</button>
<button class="btn btn-primary upload-btn">[[user:upload-picture]]</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<div id="reconnect-alert" class="alert alert-dismissible alert-warning fade hide" component="toaster/toast" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-hidden="true"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="[[global:buttons.close]]"></button>
<p class="mb-0">[[global:reconnecting-message, {config.siteTitle}]]</p>
</div>

View File

@@ -1144,6 +1144,30 @@ describe('User', () => {
});
});
it('should normalize uploaded image to png', async () => {
const oldValue = meta.config['profile:convertProfileImageToPNG'];
meta.config['profile:convertProfileImageToPNG'] = 1;
const uid = await User.create({ username: 'pngnormalize', password: '123456' });
const { jar, csrf_token } = await helpers.loginUser('pngnormalize', '123456');
const pathToJpeg = path.join(__dirname, '../test/files/normalise.jpg');
const { response } = await helpers.uploadFile(
`${nconf.get('url')}/api/user/pngnormalize/uploadpicture`,
pathToJpeg, { }, jar, csrf_token
);
assert.strictEqual(response.statusCode, 200);
const picture = await db.getObjectField(`user:${uid}`, 'picture');
const uploadedPath = path.join(
nconf.get('upload_path'), `${picture.replace(nconf.get('upload_url'), '')}`
);
const sharp = require('sharp');
const metadata = await sharp(uploadedPath).metadata();
assert.strictEqual(metadata.format, 'png');
meta.config['profile:convertProfileImageToPNG'] = oldValue;
});
it('should not allow image data with bad MIME type to be passed in', (done) => {
User.uploadCroppedPicture({
callerUid: uid,
@@ -1394,6 +1418,17 @@ describe('User', () => {
assert.strictEqual(await db.isSortedSetMember('users:banned', testUid), false);
});
it('should not return ban reason if login is incorrect', async () => {
const testUid = await User.create({ username: 'bannedUser4', password: '654321' });
await User.bans.ban(testUid, 0, 'testing bans');
let { response, body } = await helpers.loginUser('bannedUser4', '5555555');
assert.strictEqual(response.status, 403);
assert.strictEqual(body, '[[error:invalid-login-credentials]]');
({ response, body } = await helpers.loginUser('bannedUser4', '654321'));
assert.strictEqual(response.status, 403);
assert.strictEqual(body.reason, 'testing bans');
});
});
describe('Digest.getSubscribers', () => {