diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f13cce4d0..dfda132a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,74 @@ +#### v3.12.1 (2024-12-20) + +##### Chores + +* up harmony (18904bbb) +* up persona (b4ec3a6a) +* incrementing version number - v3.12.0 (052c195e) +* update changelog for v3.12.0 (5395062d) +* incrementing version number - v3.11.1 (0c0dd480) +* incrementing version number - v3.11.0 (acf27e85) +* incrementing version number - v3.10.3 (57d54224) +* incrementing version number - v3.10.2 (2f15f464) +* incrementing version number - v3.10.1 (cca3a644) +* incrementing version number - v3.10.0 (b60a9b4e) +* incrementing version number - v3.9.1 (f120c91c) +* incrementing version number - v3.9.0 (4880f32d) +* incrementing version number - v3.8.4 (4833f9a6) +* incrementing version number - v3.8.3 (97ce2c44) +* incrementing version number - v3.8.2 (72d91251) +* incrementing version number - v3.8.1 (527326f7) +* incrementing version number - v3.8.0 (e228a6eb) +* incrementing version number - v3.7.5 (6882894d) +* incrementing version number - v3.7.4 (6678744c) +* incrementing version number - v3.7.3 (2d62b6f6) +* incrementing version number - v3.7.2 (cc257e7e) +* incrementing version number - v3.7.1 (712365a5) +* incrementing version number - v3.7.0 (9a6153d7) +* incrementing version number - v3.6.7 (86a17e38) +* incrementing version number - v3.6.6 (6604bf37) +* incrementing version number - v3.6.5 (6c653625) +* incrementing version number - v3.6.4 (83d131b4) +* incrementing version number - v3.6.3 (fc7d2bfd) +* incrementing version number - v3.6.2 (0f577a57) +* incrementing version number - v3.6.1 (f1a69468) +* incrementing version number - v3.6.0 (4cdf85f8) +* incrementing version number - v3.5.3 (ed0e8783) +* incrementing version number - v3.5.2 (52fbb2da) +* incrementing version number - v3.5.1 (4c543488) +* incrementing version number - v3.5.0 (d06fb4f0) +* incrementing version number - v3.4.3 (5c984250) +* incrementing version number - v3.4.2 (3f0dac38) +* incrementing version number - v3.4.1 (01e69574) +* incrementing version number - v3.4.0 (fd9247c5) +* incrementing version number - v3.3.9 (5805e770) +* incrementing version number - v3.3.8 (a5603565) +* incrementing version number - v3.3.7 (b26f1744) +* incrementing version number - v3.3.6 (7fb38792) +* incrementing version number - v3.3.4 (a67f84ea) +* incrementing version number - v3.3.3 (f94d239b) +* incrementing version number - v3.3.2 (ec9dac97) +* incrementing version number - v3.3.1 (151cc68f) +* incrementing version number - v3.3.0 (fc1ad70f) +* incrementing version number - v3.2.3 (b06d3e63) +* incrementing version number - v3.2.2 (758ecfcd) +* incrementing version number - v3.2.1 (20145074) +* incrementing version number - v3.2.0 (9ecac38e) +* incrementing version number - v3.1.7 (0b4e81ab) +* incrementing version number - v3.1.6 (b3a3b130) +* incrementing version number - v3.1.5 (ec19343a) +* incrementing version number - v3.1.4 (2452783c) +* incrementing version number - v3.1.3 (3b4e9d3f) +* incrementing version number - v3.1.2 (40fa3489) +* incrementing version number - v3.1.1 (40250733) +* incrementing version number - v3.1.0 (0cb386bd) +* incrementing version number - v3.0.1 (26f6ea49) +* incrementing version number - v3.0.0 (224e08cd) + +##### Bug Fixes + +* check install.values, it can be undefined (9bb8002a) + #### v3.12.0 (2024-12-18) ##### Chores diff --git a/install/data/defaults.json b/install/data/defaults.json index 8b6f7a2b03..e49c8282a9 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -107,11 +107,14 @@ "flags:actionOnReject": "rescind", "notificationType_upvote": "notification", "notificationType_new-topic": "notification", + "notificationType_new-topic-with-tag": "notification", + "notificationType_new-topic-in-category": "notification", "notificationType_new-reply": "notification", "notificationType_post-edit": "notification", "notificationType_follow": "notification", "notificationType_new-chat": "notification", "notificationType_new-group-chat": "notification", + "notificationType_new-public-chat": "none", "notificationType_group-invite": "notification", "notificationType_group-leave": "notification", "notificationType_group-request-membership": "notification", diff --git a/install/package.json b/install/package.json index 000bf1ad43..9cfb3f094a 100644 --- a/install/package.json +++ b/install/package.json @@ -98,20 +98,20 @@ "multiparty": "4.2.3", "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.7", - "nodebb-plugin-composer-default": "10.2.43", + "nodebb-plugin-composer-default": "10.2.44", "nodebb-plugin-dbsearch": "6.2.5", "nodebb-plugin-emoji": "5.1.15", "nodebb-plugin-emoji-android": "4.0.0", - "nodebb-plugin-markdown": "12.2.8", + "nodebb-plugin-markdown": "12.2.9", "nodebb-plugin-mentions": "4.4.5", "nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-spam-be-gone": "2.3.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.92", + "nodebb-theme-harmony": "1.2.95", "nodebb-theme-lavender": "7.1.17", - "nodebb-theme-peace": "2.2.32", - "nodebb-theme-persona": "13.3.61", - "nodebb-widget-essentials": "7.0.31", + "nodebb-theme-peace": "2.2.33", + "nodebb-theme-persona": "13.3.63", + "nodebb-widget-essentials": "7.0.32", "nodemailer": "6.9.16", "nprogress": "0.2.0", "passport": "0.7.0", diff --git a/public/language/en-GB/aria.json b/public/language/en-GB/aria.json index 6f023dcae0..8e2c565c82 100644 --- a/public/language/en-GB/aria.json +++ b/public/language/en-GB/aria.json @@ -2,6 +2,7 @@ "post-sort-option": "Post sort option, %1", "topic-sort-option": "Topic sort option, %1", "user-avatar-for": "User avatar for %1", + "profile-page-for": "Profile page for user %1", "user-watched-tags": "User watched tags", "delete-upload-button": "Delete upload button", "group-page-link-for": "Group page link for %1" diff --git a/public/openapi/read/admin/config.yaml b/public/openapi/read/admin/config.yaml index f79df9eca6..73105169dc 100644 --- a/public/openapi/read/admin/config.yaml +++ b/public/openapi/read/admin/config.yaml @@ -122,8 +122,6 @@ get: type: array items: type: string - resizeImageWidth: - type: number cookies: type: object properties: diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml index 3b38168c2e..5e6f05d91c 100644 --- a/public/openapi/read/config.yaml +++ b/public/openapi/read/config.yaml @@ -122,8 +122,6 @@ get: type: array items: type: string - resizeImageWidth: - type: number cookies: type: object properties: diff --git a/public/scss/generics.scss b/public/scss/generics.scss index baa5a01087..a95d7b1c25 100644 --- a/public/scss/generics.scss +++ b/public/scss/generics.scss @@ -117,16 +117,21 @@ blockquote { .hover-visible { visibility: hidden; } + .hover-opacity-75 { + opacity: 0; + &:focus { opacity: 0.75 } + } + .hover-opacity-100 { + opacity: 0; + &:focus {opacity: 1; } + } + &:hover { - .hover-d-block { - display: block!important; - } - .hover-d-flex { - display: flex!important; - } - .hover-visible { - visibility: visible; - } + .hover-d-block { display: block!important; } + .hover-d-flex { display: flex!important; } + .hover-visible { visibility: visible; } + .hover-opacity-100 { opacity: 1; } + .hover-opacity-75 { opacity: 0.75; } } } diff --git a/public/src/app.js b/public/src/app.js index 599792838f..c0012de205 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -220,7 +220,7 @@ if (document.readyState === 'loading') { if (!isTouchDevice) { els = els || $('body'); els.tooltip({ - selector: '.avatar.avatar-tooltip', + selector: '.avatar-tooltip', placement: placement || 'top', container: '#content', animation: false, diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js index bff84bd849..fc402b8d96 100644 --- a/public/src/client/header/notifications.js +++ b/public/src/client/header/notifications.js @@ -6,14 +6,19 @@ define('forum/header/notifications', function () { notifications.prepareDOM = function () { const notifTrigger = $('[component="notifications"] [data-bs-toggle="dropdown"]'); - notifTrigger.on('show.bs.dropdown', (ev) => { - requireAndCall('loadNotifications', $(ev.target).parent().find('[component="notifications/list"]')); + notifTrigger.on('show.bs.dropdown', async (ev) => { + const notifications = await app.require('notifications'); + const triggerEl = $(ev.target); + notifications.loadNotifications(triggerEl, triggerEl.parent().find('[component="notifications/list"]')); }); notifTrigger.each((index, el) => { - const dropdownEl = $(el).parent().find('.dropdown-menu'); + const triggerEl = $(el); + const dropdownEl = triggerEl.parent().find('.dropdown-menu'); if (dropdownEl.hasClass('show')) { - requireAndCall('loadNotifications', dropdownEl.find('[component="notifications/list"]')); + app.require('notifications').then((notifications) => { + notifications.loadNotifications(triggerEl, dropdownEl.find('[component="notifications/list"]')); + }); } }); @@ -24,18 +29,14 @@ define('forum/header/notifications', function () { socket.on('event:notifications.updateCount', onUpdateCount); }; - function onNewNotification(data) { - requireAndCall('onNewNotification', data); + async function onNewNotification(data) { + const notifications = await app.require('notifications'); + notifications.onNewNotification(data); } - function onUpdateCount(data) { - requireAndCall('updateNotifCount', data); - } - - function requireAndCall(method, param) { - require(['notifications'], function (notifications) { - notifications[method](param); - }); + async function onUpdateCount(data) { + const notifications = await app.require('notifications'); + notifications.updateNotifCount(data); } return notifications; diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js index a3667c8ece..b420acc5c7 100644 --- a/public/src/client/topic/fork.js +++ b/public/src/client/topic/fork.js @@ -29,7 +29,10 @@ define('forum/topic/fork', [ $('body').append(forkModal); - categorySelector.init(forkModal.find('[component="category-selector"]'), { + const dropdownEl = forkModal.find('[component="category-selector"]'); + dropdownEl.addClass('dropup'); + + categorySelector.init(dropdownEl, { onSelect: function (category) { selectedCategory = category; }, diff --git a/public/src/client/topic/images.js b/public/src/client/topic/images.js index 5fc2b34f66..c4c30319d8 100644 --- a/public/src/client/topic/images.js +++ b/public/src/client/topic/images.js @@ -19,7 +19,7 @@ define('forum/topic/images', [], function () { } if (!imageEl.parent().is('a')) { - if (utils.isRelativeUrl(src) && suffixRegex.test(src) && imageEl.get(0).naturalWidth >= config.resizeImageWidth) { + if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { src = src.replace(suffixRegex, '$1'); } const alt = imageEl.attr('alt') || ''; diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js index dd9ca76fca..ba4f055e68 100644 --- a/public/src/client/topic/move.js +++ b/public/src/client/topic/move.js @@ -28,8 +28,10 @@ define('forum/topic/move', [ if (Move.moveAll || (Move.tids && Move.tids.length > 1)) { modal.find('.card-header').translateText('[[topic:move-topics]]'); } + const dropdownEl = modal.find('[component="category-selector"]'); + dropdownEl.addClass('dropup'); - categorySelector.init(modal.find('[component="category-selector"]'), { + categorySelector.init(dropdownEl, { onSelect: onCategorySelected, privilege: 'moderate', }); diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 5638f29928..ae7493aedc 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -214,9 +214,9 @@ module.exports = function (utils, Benchpress, relative_path) { function renderTopicImage(topicObj) { if (topicObj.thumb) { - return ''; + return ''; } - return ''; + return ''; } function renderDigestAvatar(block) { @@ -300,7 +300,7 @@ module.exports = function (utils, Benchpress, relative_path) { } classNames = classNames || ''; const attributes = new Map([ - ['title', userObj.username], + ['title', userObj.displayname], ['data-uid', userObj.uid], ['class', `avatar ${classNames}${rounded ? ' avatar-rounded' : ''}`], ]); @@ -313,7 +313,7 @@ module.exports = function (utils, Benchpress, relative_path) { let output = ''; if (userObj.picture) { - output += ``; + output += ``; } output += `${userObj['icon:text']}`; return output; @@ -383,7 +383,7 @@ module.exports = function (utils, Benchpress, relative_path) { `; }); - return html; + return html.join(''); } function register() { diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index a983519183..0fef33f650 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -28,7 +28,7 @@ define('notifications', [ }); hooks.on('filter:notifications.load', _addTimeagoString); - Notifications.loadNotifications = function (notifList, callback) { + Notifications.loadNotifications = function (triggerEl, notifList, callback) { callback = callback || function () {}; socket.emit('notifications.get', null, function (err, data) { if (err) { @@ -47,7 +47,7 @@ define('notifications', [ if (scrollToPostIndexIfOnPage(notifEl)) { ev.stopPropagation(); ev.preventDefault(); - components.get('notifications/list').dropdown('toggle'); + triggerEl.dropdown('toggle'); } const unread = notifEl.hasClass('unread'); diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 0d87e398f4..a4757cbd73 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -28,9 +28,9 @@ define('search', [ const toggleVisibility = searchFields.hasClass('hidden'); if (toggleVisibility) { - searchInput.off('blur').on('blur', function dismissSearch() { + searchFields.off('focusout').on('focusout', function dismissSearch() { setTimeout(function () { - if (!searchInput.is(':focus')) { + if (!searchFields.find(':focus').length) { searchFields.addClass('hidden'); searchButton.removeClass('hidden'); } @@ -177,30 +177,33 @@ define('search', [ doSearch(); }, 500)); - let mousedownOnResults = false; quickSearchResults.on('mousedown', '.quick-search-results > *', function () { $(window).one('mouseup', function () { quickSearchResults.addClass('hidden'); }); - mousedownOnResults = true; }); - inputEl.on('blur', function () { + + const inputParent = inputEl.parent(); + const resultParent = quickSearchResults.parent(); + inputParent.on('focusout', hideResults); + resultParent.on('focusout', hideResults); + function hideResults() { setTimeout(function () { - if (!inputEl.is(':focus') && !mousedownOnResults && !quickSearchResults.hasClass('hidden')) { + if (!inputParent.find(':focus').length && !resultParent.find(':focus').length && !quickSearchResults.hasClass('hidden')) { quickSearchResults.addClass('hidden'); } }, 200); - }); + } let ajaxified = false; hooks.on('action:ajaxify.end', function () { if (!ajaxify.isCold()) { ajaxified = true; } + quickSearchResults.addClass('hidden'); }); inputEl.on('focus', function () { - mousedownOnResults = false; const query = inputEl.val(); oldValue = query; if (query && quickSearchResults.find('#quick-search-results').children().length) { diff --git a/public/vendor/jquery/draggable-background/backgroundDraggable.js b/public/vendor/jquery/draggable-background/backgroundDraggable.js index 8453e5aeb0..6785d32e6a 100644 --- a/public/vendor/jquery/draggable-background/backgroundDraggable.js +++ b/public/vendor/jquery/draggable-background/backgroundDraggable.js @@ -79,6 +79,39 @@ imageDimensions = getBackgroundImageDimensions($el); } + $(window).on('keydown.dbg', (e) => { + var pos = $el.css('background-position').match(/(-?\d+).*?\s(-?\d+)/) || []; + var xPos = parseInt(pos[1]) || 0; + var yPos = parseInt(pos[2]) || 0; + // We must convert percentage back to pixels + if (options.units == 'percent') { + xPos = Math.round(xPos / -200 * imageDimensions.width); + yPos = Math.round(yPos / -200 * imageDimensions.height); + } + + var x = 0, y = 0; + if (e.which === 37) { // left + x = -5 + } else if (e.which === 39) { // right + x = 5 + } else if (e.which === 38) { // up + y = -5 + } else if (e.which === 40) { // down + y = +5 + } + if (options.units === 'percent') { + xPos = options.axis === 'y' ? xPos : limit(-imageDimensions.width/2, 0, xPos+x, options.bound); + yPos = options.axis === 'x' ? yPos : limit(-imageDimensions.height/2, 0, yPos+y, options.bound); + + // Convert pixels to percentage + $el.css('background-position', xPos / imageDimensions.width * -200 + '% ' + yPos / imageDimensions.height * -200 + '%'); + } else { + xPos = options.axis === 'y' ? xPos : limit($el.innerWidth()-imageDimensions.width, 0, xPos+x, options.bound); + yPos = options.axis === 'x' ? yPos : limit($el.innerHeight()-imageDimensions.height, 0, yPos+y, options.bound); + } + return [37, 38, 39, 40].includes(e.which) ? false : undefined; + }); + $el.on('mousedown.dbg touchstart.dbg', function(e) { if (e.target !== $el[0]) { return; @@ -145,7 +178,7 @@ Plugin.prototype.disable = function() { var $el = $(this.element); $el.off('mousedown.dbg touchstart.dbg'); - $window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg'); + $window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg keydown.dbg'); } $.fn.backgroundDraggable = function(options) { diff --git a/src/api/posts.js b/src/api/posts.js index 4e3917a008..efeec43ace 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -370,10 +370,10 @@ postsAPI.getUpvoters = async function (caller, data) { upvotedUids = upvotedUids.slice(0, cutoff - 1); } - const usernames = await user.getUsernamesByUids(upvotedUids); + const users = await user.getUsersFields(upvotedUids, ['username']); return { otherCount, - usernames, + usernames: users.map(user => user.displayname), cutoff, }; }; diff --git a/src/cli/index.js b/src/cli/index.js index e6f0485585..7bd1c37b87 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -87,7 +87,8 @@ program .option('--log-level ', 'Default logging level to use', 'info') .option('--config ', 'Specify a config file', 'config.json') .option('-d, --dev', 'Development mode, including verbose logging', false) - .option('-l, --log', 'Log subprocess output to console', false); + .option('-l, --log', 'Log subprocess output to console', false) + .option('-y, --unattended', 'Answer yes to any prompts, like plugin upgrades', false); // provide a yargs object ourselves // otherwise yargs will consume `--help` or `help` @@ -294,6 +295,7 @@ program ].join('\n')}`); }) .action((scripts, options) => { + options.unattended = program.opts().unattended; if (program.opts().dev) { process.env.NODE_ENV = 'development'; global.env = 'development'; @@ -308,7 +310,8 @@ program .alias('upgradePlugins') .description('Upgrade plugins') .action(() => { - require('./upgrade-plugins').upgradePlugins((err) => { + const { unattended } = program.opts(); + require('./upgrade-plugins').upgradePlugins(unattended, (err) => { if (err) { throw err; } diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index b68db55eac..92cc980fa2 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -120,7 +120,7 @@ async function checkPlugins() { return upgradable; } -async function upgradePlugins() { +async function upgradePlugins(unattended = false) { try { const found = await checkPlugins(); if (found && found.length) { @@ -132,16 +132,18 @@ async function upgradePlugins() { console.log(chalk.green('\nAll packages up-to-date!')); return; } + let result = { upgrade: 'y' }; + if (!unattended) { + prompt.message = ''; + prompt.delimiter = ''; - prompt.message = ''; - prompt.delimiter = ''; - - prompt.start(); - const result = await prompt.get({ - name: 'upgrade', - description: '\nProceed with upgrade (y|n)?', - type: 'string', - }); + prompt.start(); + result = await prompt.get({ + name: 'upgrade', + description: '\nProceed with upgrade (y|n)?', + type: 'string', + }); + } if (['y', 'Y', 'yes', 'YES'].includes(result.upgrade)) { console.log('\nUpgrading packages...'); diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index ff3487388c..e879239b7a 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -24,9 +24,9 @@ const steps = { }, plugins: { message: 'Checking installed plugins for updates...', - handler: async function () { + handler: async function (options) { await require('../database').init(); - await upgradePlugins(); + await upgradePlugins(options.unattended); }, }, schema: { @@ -45,14 +45,14 @@ const steps = { }, }; -async function runSteps(tasks) { +async function runSteps(tasks, options) { try { for (let i = 0; i < tasks.length; i++) { const step = steps[tasks[i]]; if (step && step.message && step.handler) { process.stdout.write(`\n${chalk.bold(`${i + 1}. `)}${chalk.yellow(step.message)}`); /* eslint-disable-next-line */ - await step.handler(); + await step.handler(options); } } const message = 'NodeBB Upgrade Complete!'; @@ -95,7 +95,7 @@ async function runUpgrade(upgrades, options) { options.plugins || options.schema || options.build) { tasks = tasks.filter(key => options[key]); } - await runSteps(tasks); + await runSteps(tasks, options); return; } diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 5b30cd0d7a..f2e0fbc5a7 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -160,7 +160,7 @@ helpers.getCustomUserFields = async function (callerUID, userData) { if (f.type === 'input-link' && userValue) { f.linkValue = validator.escape(String(userValue.replace('http://', '').replace('https://', ''))); } - f['select-options'] = f['select-options'].split('\n').filter(Boolean).map( + f['select-options'] = (f['select-options'] || '').split('\n').filter(Boolean).map( opt => ({ value: opt, selected: Array.isArray(userValue) ? diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index 6248bc5ddd..8530486d7c 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -113,8 +113,14 @@ const doUnsubscribe = async (payload) => { user.updateDigestSetting(payload.uid, 'off'), ]); } else if (payload.template === 'notification') { + const currentToNewSetting = { + notificationemail: 'notification', + email: 'none', + }; const current = await db.getObjectField(`user:${payload.uid}:settings`, `notificationType_${payload.type}`); - await user.setSetting(payload.uid, `notificationType_${payload.type}`, (current === 'notificationemail' ? 'notification' : 'none')); + if (currentToNewSetting.hasOwnProperty(current)) { + await user.setSetting(payload.uid, `notificationType_${payload.type}`, currentToNewSetting[current]); + } } return true; }; diff --git a/src/controllers/api.js b/src/controllers/api.js index c988f501a3..95cbf1c377 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -78,7 +78,6 @@ apiController.loadConfig = async function (req) { enablePostHistory: meta.config.enablePostHistory === 1, timeagoCutoff: meta.config.timeagoCutoff !== '' ? Math.max(0, parseInt(meta.config.timeagoCutoff, 10)) : meta.config.timeagoCutoff, timeagoCodes: languages.timeagoCodes, - resizeImageWidth: meta.config.resizeImageWidth, cookies: { enabled: meta.config.cookieConsentEnabled === 1, message: translator.escape(validator.escape(meta.config.cookieConsentMessage || '[[global:cookies.message]]')).replace(/\\/g, '\\\\'), @@ -91,7 +90,7 @@ apiController.loadConfig = async function (req) { }, emailPrompt: meta.config.emailPrompt, useragent: { - isSafari: req.useragent.isSafari, + isSafari: req.useragent && req.useragent.isSafari, }, fontawesome: { pro: fontawesome_pro, diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 6591459cf2..299bfa571b 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -49,7 +49,10 @@ async function registerAndLoginUser(req, res, userData) { const uid = await user.create(userData); if (res.locals.processLogin) { - await authenticationController.doLogin(req, uid); + const hasLoginPrivilege = await privileges.global.can('local:login', uid); + if (hasLoginPrivilege) { + await authenticationController.doLogin(req, uid); + } } // Distinguish registrations through invites from direct ones diff --git a/src/controllers/index.js b/src/controllers/index.js index 0e5dde32b6..3812d0bc55 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -6,7 +6,7 @@ const validator = require('validator'); const meta = require('../meta'); const user = require('../user'); const plugins = require('../plugins'); -const privileges = require('../privileges'); +const privilegesHelpers = require('../privileges/helpers'); const helpers = require('./helpers'); const Controllers = module.exports; @@ -124,7 +124,8 @@ Controllers.login = async function (req, res) { data.title = '[[pages:login]]'; data.allowPasswordReset = !meta.config['password:disableEdit']; - const hasLoginPrivilege = await privileges.global.canGroup('local:login', 'registered-users'); + const loginPrivileges = await privilegesHelpers.getGroupPrivileges(0, ['groups:local:login']); + const hasLoginPrivilege = !!loginPrivileges.find(privilege => privilege.privileges['groups:local:login']); data.allowLocalLogin = hasLoginPrivilege || parseInt(req.query.local, 10) === 1; if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index b428d9926b..5e42a4f26d 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -174,11 +174,12 @@ module.exports = function (module) { } const data = {}; - fields.forEach((field) => { + fields = fields.map((field) => { field = helpers.fieldToString(field); if (field) { data[field] = 1; } + return field; }); const item = await module.client.collection('objects').findOne({ _key: key }, { projection: data }); @@ -194,14 +195,13 @@ module.exports = function (module) { if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) { return; } - fields = fields.filter(Boolean); + fields = fields.map(helpers.fieldToString).filter(Boolean); if (!fields.length) { return; } const data = {}; fields.forEach((field) => { - field = helpers.fieldToString(field); data[field] = ''; }); if (Array.isArray(key)) { diff --git a/src/database/mongo/helpers.js b/src/database/mongo/helpers.js index 4259771b2e..46ef8b39cb 100644 --- a/src/database/mongo/helpers.js +++ b/src/database/mongo/helpers.js @@ -23,7 +23,9 @@ helpers.fieldToString = function (field) { field = field.toString(); } // if there is a '.' in the field name it inserts subdocument in mongo, replace '.'s with \uff0E - return field.replace(/\./g, '\uff0E'); + // replace $ with \uff04 so we can use $ in document fields + return field.replace(/\./g, '\uff0E') + .replace(/\$/g, '\uFF04'); }; helpers.serializeData = function (data) { diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index 45e80cf532..4c6e7b374f 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -172,8 +172,11 @@ module.exports = function (module) { if (key === undefined || key === null || field === undefined || field === null) { return; } - await module.client.hdel(key, field); - cache.del(key); + field = field.toString(); + if (field) { + await module.client.hdel(key, field); + cache.del(key); + } }; module.deleteObjectFields = async function (key, fields) { diff --git a/src/emailer.js b/src/emailer.js index 486729eaae..5defe52617 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -354,8 +354,11 @@ Emailer.sendViaFallback = async (data) => { data.text = data.plaintext; delete data.plaintext; - // NodeMailer uses a combined "from" - data.from = `${data.from_name}<${data.from}>`; + // use an address object https://nodemailer.com/message/addresses/ + data.from = { + name: data.from_name, + address: data.from, + }; delete data.from_name; await Emailer.fallbackTransport.sendMail(data); }; diff --git a/src/image.js b/src/image.js index 6c0ae9e68a..06e3f2d76d 100644 --- a/src/image.js +++ b/src/image.js @@ -103,7 +103,7 @@ image.size = async function (path) { }; image.stripEXIF = async function (path) { - if (!meta.config.stripEXIFData || path.endsWith('.svg')) { + if (!meta.config.stripEXIFData || path.endsWith('.gif') || path.endsWith('.svg')) { return; } try { diff --git a/src/meta/minifier.js b/src/meta/minifier.js index 6aa975b334..6a617cc45d 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -164,7 +164,10 @@ actions.buildCSS = async function buildCSS(data) { loadPaths: data.paths, }; if (data.minify) { - opts.silenceDeprecations = ['mixed-decls', 'color-functions']; + opts.silenceDeprecations = [ + 'legacy-js-api', 'mixed-decls', 'color-functions', + 'global-builtin', 'import', + ]; } const scssOutput = await sass.compileStringAsync(data.source, opts); css = scssOutput.css.toString(); diff --git a/src/middleware/user.js b/src/middleware/user.js index ca6afcaf9b..261aa9c738 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -254,7 +254,7 @@ module.exports = function (middleware) { if (res.locals.isAPI) { req.params.userslug = lowercaseSlug; } else { - const newPath = req.path.replace(new RegExp(`/${req.params.userslug}`), () => `/${lowercaseSlug}`); + const newPath = req.path.replace(`/${req.params.userslug}`, () => `/${lowercaseSlug}`); return res.redirect(`${nconf.get('relative_path')}${newPath}`); } } diff --git a/src/routes/index.js b/src/routes/index.js index 4008f1565a..02d7a5259a 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -66,8 +66,8 @@ _mounts.post = (app, name, middleware, controllers) => { middleware.registrationComplete, middleware.pluginHooks, ]; - app.get(`/${name}/:pid`, middleware.busyCheck, middlewares, controllers.posts.redirectToPost); - app.get(`/api/${name}/:pid`, middlewares, controllers.posts.redirectToPost); + app.get(`/${name}/:pid`, middleware.busyCheck, middlewares, helpers.tryRoute(controllers.posts.redirectToPost)); + app.get(`/api/${name}/:pid`, middlewares, helpers.tryRoute(controllers.posts.redirectToPost)); }; _mounts.tags = (app, name, middleware, controllers) => { diff --git a/src/user/profile.js b/src/user/profile.js index 7869f14410..14942f98aa 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -16,7 +16,7 @@ const tx = require('../translator'); module.exports = function (User) { User.updateProfile = async function (uid, data, extraFields) { let fields = [ - 'username', 'email', 'fullname', 'website', 'location', + 'username', 'email', 'fullname', 'groupTitle', 'birthday', 'signature', 'aboutme', ...await db.getSortedSetRange('user-custom-fields', 0, -1), ]; diff --git a/src/views/admin/dashboard.tpl b/src/views/admin/dashboard.tpl index 355aedaba3..4f0f41c496 100644 --- a/src/views/admin/dashboard.tpl +++ b/src/views/admin/dashboard.tpl @@ -140,12 +140,12 @@
[[admin/dashboard:popular-searches]] [[admin/dashboard:view-all]]
- +
{{{ each popularSearches }}} - - + + {{{ end }}} diff --git a/src/views/admin/dashboard/searches.tpl b/src/views/admin/dashboard/searches.tpl index 97c02cc863..a5f34852b3 100644 --- a/src/views/admin/dashboard/searches.tpl +++ b/src/views/admin/dashboard/searches.tpl @@ -17,7 +17,7 @@ -
{popularSearches.value}{formattedNumber(popularSearches.score)}{popularSearches.value}{formattedNumber(popularSearches.score)}
+
@@ -30,8 +30,8 @@ {{{ end }}} {{{ each searches }}} - - + + {{{ end }}} diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index fb80f94b0f..0a3346e6dc 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -39,7 +39,7 @@
-
[[admin/dashboard:search-term]] [[admin/dashboard:search-count]]
{searches.value}{formattedNumber(searches.score)}{searches.value}{formattedNumber(searches.score)}