diff --git a/eslint.config.mjs b/eslint.config.mjs index 47cfa158f5..5b3c417f7d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -60,6 +60,11 @@ export default defineConfig([ } }, ...publicConfig, - ...serverConfig + ...serverConfig, + { + rules: { + 'preserve-caught-error': 'off' + } + } ]); diff --git a/install/package.json b/install/package.json index 6524111dad..cbf138083b 100644 --- a/install/package.json +++ b/install/package.json @@ -164,10 +164,10 @@ "@commitlint/cli": "20.4.1", "@commitlint/config-angular": "20.4.1", "coveralls": "3.1.1", - "@eslint/js": "9.39.2", + "@eslint/js": "10.0.1", "@stylistic/eslint-plugin": "5.8.0", - "eslint-config-nodebb": "1.1.11", - "eslint-plugin-import": "2.32.0", + "eslint-config-nodebb": "2.0.1", + "globals": "17.3.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 7e68c65b8c..c9bbd2bc5b 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -117,23 +117,17 @@ app.onDomReady(); fallback = $(this).text(); }); - let mainTitle; let pageTitle; if (/admin\/plugins\//.test(url)) { - mainTitle = fallback; - pageTitle = '[[admin/menu:section-plugins]] > ' + mainTitle; + pageTitle = '[[admin/menu:section-plugins]] > ' + fallback; } else { const matches = url.match(/admin\/(.+?)\/(.+?)$/); if (matches) { - mainTitle = '[[admin/menu:' + matches[1] + '/' + matches[2] + ']]'; + const mainTitle = '[[admin/menu:' + matches[1] + '/' + matches[2] + ']]'; pageTitle = '[[admin/menu:section-' + (matches[1] === 'development' ? 'advanced' : matches[1]) + ']]' + (matches[2] ? (' > ' + mainTitle) : ''); - if (matches[2] === 'settings') { - mainTitle = translator.compile('admin/menu:settings.page-title', mainTitle); - } } else { - mainTitle = '[[admin/menu:section-dashboard]]'; pageTitle = '[[admin/menu:section-dashboard]]'; } } diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 1a8532c207..24ca324392 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -138,7 +138,7 @@ define('autocomplete', [ if (!targetEl) { return; } - var editor; + let editor; if (targetEl.nodeName === 'TEXTAREA' || targetEl.nodeName === 'INPUT') { editor = new TextareaEditor(targetEl); } else if (targetEl.nodeName === 'DIV' && targetEl.getAttribute('contenteditable') === 'true') { @@ -150,7 +150,7 @@ define('autocomplete', [ // yuku-t/textcomplete inherits directionality from target element itself targetEl.setAttribute('dir', document.querySelector('html').getAttribute('data-dir')); - var textcomplete = new Textcomplete(editor, strategies, { + const textcomplete = new Textcomplete(editor, strategies, { dropdown: options, }); textcomplete.on('rendered', function () { diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index 0532beb3b4..766bc02d9c 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -727,7 +727,7 @@ define('navigator', [ } } - let scrollTop = 0; + let scrollTop; if (postHeight < viewportHeight - navbarHeight - topicHeaderHeight) { scrollTop = scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2); } else { diff --git a/public/src/modules/uploadHelpers.js b/public/src/modules/uploadHelpers.js index 3e56550315..0fbcd9a9f6 100644 --- a/public/src/modules/uploadHelpers.js +++ b/public/src/modules/uploadHelpers.js @@ -88,7 +88,7 @@ define('uploadHelpers', ['alerts'], function (alerts) { let formData; if (window.FormData) { formData = new FormData(); - for (var i = 0; i < files.length; ++i) { + for (let i = 0; i < files.length; ++i) { formData.append('files[]', files[i], files[i].name); } } @@ -215,7 +215,7 @@ define('uploadHelpers', ['alerts'], function (alerts) { success: function (res) { const uploads = res.response.images; if (uploads && uploads.length) { - for (var i = 0; i < uploads.length; ++i) { + for (let i = 0; i < uploads.length; ++i) { uploads[i].filename = files[i].name; uploads[i].isImage = /image./.test(files[i].type); } diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 32efdb5319..85491b27b1 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -344,14 +344,13 @@ ActivityPub.get = async (type, id, uri, options) => { requestCache.set(cacheKey, body); return body; - } catch (e) { - if (String(e.code).startsWith('ap_get_')) { - throw e; + } catch (err) { + if (String(err.code).startsWith('ap_get_')) { + throw err; } // Handle things like non-json body, etc. - const { cause } = e; - throw new Error(`[[error:activitypub.get-failed]]`, { cause }); + throw new Error(`[[error:activitypub.get-failed]]`, { cause: err }); } }; diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 7aad23fb6f..3e6e31d5df 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -621,7 +621,7 @@ Mocks.notes.public = async (post) => { let tag = null; let followersUrl; - let name = null; + let name; ({ titleRaw: name } = await topics.getTopicFields(post.tid, ['title'])); if (post.toPid) { // direct reply diff --git a/src/api/search.js b/src/api/search.js index 070dc9810b..ce1d1e2282 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -17,8 +17,6 @@ const searchApi = module.exports; searchApi.categories = async (caller, data) => { // used by categorySearch module - - let cids = []; let matchedCids = []; const privilege = data.privilege || 'topics:read'; data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map( @@ -26,6 +24,7 @@ searchApi.categories = async (caller, data) => { ); data.parentCid = parseInt(data.parentCid || 0, 10); + let cids; if (data.search) { ({ cids, matchedCids } = await findMatchedCids(caller.uid, data)); } else { diff --git a/src/api/users.js b/src/api/users.js index 4fb8155734..7a311bbb12 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -615,15 +615,14 @@ usersAPI.changePicture = async (caller, data) => { throw new Error('[[error:invalid-data]]'); } - const { type, url } = data; - let picture = ''; - await user.checkMinReputation(caller.uid, data.uid, 'min:rep:profile-picture'); const canEdit = await privileges.users.canEdit(caller.uid, data.uid); if (!canEdit) { throw new Error('[[error:no-privileges]]'); } + const { type, url } = data; + let picture; if (type === 'default') { picture = ''; } else if (type === 'uploaded') { diff --git a/src/categories/index.js b/src/categories/index.js index ed0b60b08f..948ff86226 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -268,7 +268,6 @@ async function getChildrenTree(category, uid) { } let childrenData = await Categories.getCategoriesData(childrenCids); childrenData = childrenData.filter(Boolean); - childrenCids = childrenData.map(child => child.cid); Categories.getTree([category].concat(childrenData), category.parentCid); } diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 1021181e22..d5f4c99764 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -72,7 +72,7 @@ module.exports = function (Categories) { return; } const categoriesToLoad = categoryData.filter(c => c && c.numRecentReplies && parseInt(c.numRecentReplies, 10) > 0); - let keys = []; + let keys; if (plugins.hooks.hasListeners('filter:categories.getRecentTopicReplies')) { const result = await plugins.hooks.fire('filter:categories.getRecentTopicReplies', { categories: categoriesToLoad, diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index f8c8bdf3f6..cd9463fce2 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -25,7 +25,7 @@ uploadsController.get = async function (req, res, next) { } const itemsPerPage = 20; const page = parseInt(req.query.page, 10) || 1; - let files = []; + let files; try { await checkSymLinks(req.query.dir); files = await getFilesInFolder(currentFolder); @@ -149,7 +149,7 @@ async function getFileData(currentDir, file) { uploadsController.uploadCategoryPicture = async function (req, res, next) { const uploadedFile = req.files[0]; - let params = null; + let params; try { params = JSON.parse(req.body.params); diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 6a07823cd8..a1f48f35b1 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -77,7 +77,7 @@ async function getUsers(req, res) { } async function getUids(set) { - let uids = []; + let uids; if (Array.isArray(set)) { const weights = set.map((s, index) => (index ? 0 : 1)); uids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ diff --git a/src/database/helpers.js b/src/database/helpers.js index 2717428e2c..598c1d8812 100644 --- a/src/database/helpers.js +++ b/src/database/helpers.js @@ -16,7 +16,7 @@ helpers.mergeBatch = function (batchData, start, stop, sort) { } return selectedArray.length ? selectedArray.shift() : null; } - let item = null; + let item; const result = []; do { item = getFirst(batchData); diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 08869d5b5f..b24007e98a 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -82,7 +82,7 @@ module.exports = function (module) { limit = 0; } - let result = []; + let result; async function doQuery(_key, fields, skip, limit) { return await module.client.collection('objects').find({ ...query, ...{ _key: _key }, diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 9f59224497..351fe3e059 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -228,7 +228,7 @@ SELECT o."_key" k, if (!Array.isArray(keys)) { keys = [keys]; } - let counts = []; + let counts; if (min !== '-inf' || max !== '+inf') { if (min === '-inf') { min = null; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index d8613a3a68..5b9652e6e0 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -300,7 +300,7 @@ module.exports = function (module) { let cursor = '0'; const returnData = []; - let done = false; + let done; const seen = Object.create(null); do { /* eslint-disable no-await-in-loop */ diff --git a/src/events.js b/src/events.js index 6bd65c1b21..f189a4e377 100644 --- a/src/events.js +++ b/src/events.js @@ -119,7 +119,7 @@ events.getEvents = async function (options) { const from = options.hasOwnProperty('from') ? options.from : '-inf'; const to = options.hasOwnProperty('to') ? options.to : '+inf'; const { filter, start, stop, uids } = options; - let eids = []; + let eids; if (Array.isArray(uids)) { if (filter === '') { diff --git a/src/flags.js b/src/flags.js index b2d15e887f..98949b6b5a 100644 --- a/src/flags.js +++ b/src/flags.js @@ -755,8 +755,7 @@ Flags.update = async function (flagId, uid, changeset) { await notifications.push(notifObj, [assigneeId]); }; const isAssignable = async function (assigneeId) { - let allowed = false; - allowed = await user.isAdminOrGlobalMod(assigneeId); + let allowed = await user.isAdminOrGlobalMod(assigneeId); // Mods are also allowed to be assigned, if flag target is post in uid's moderated cid if (!allowed && current.type === 'post') { @@ -918,7 +917,7 @@ Flags.notify = async function (flagObj, uid, notifySelf = false) { groups.getMembers('Global Moderators', 0, -1), ]); let uids = admins.concat(globalMods); - let notifObj = null; + let notifObj; const { displayname } = flagObj.reports[flagObj.reports.length - 1].reporter; diff --git a/src/messaging/index.js b/src/messaging/index.js index 263fd53543..8881d41b2a 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -250,7 +250,7 @@ Messaging.generateChatWithMessage = async function (room, callerUid, userLang) { const usernames = users.map(u => (utils.isNumber(u.uid) ? `${u.displayname}` : `${u.displayname}`)); - let compiled = ''; + let compiled; if (!users.length) { return '[[modules:chat.no-users-in-room]]'; } diff --git a/src/middleware/index.js b/src/middleware/index.js index 0e0fbcde3e..aa4c456c91 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -150,7 +150,7 @@ middleware.routeTouchIcon = function routeTouchIcon(req, res) { return res.redirect(brandTouchIcon); } - let iconPath = ''; + let iconPath; if (brandTouchIcon) { const uploadPath = nconf.get('upload_path'); iconPath = path.join(uploadPath, brandTouchIcon.replace(/assets\/uploads/, '')); @@ -242,11 +242,11 @@ middleware.delayLoading = function delayLoading(req, res, next) { // Introduces an artificial delay during load so that brute force attacks are effectively mitigated // Add IP to cache so if too many requests are made, subsequent requests are blocked for a minute - let timesSeen = delayCache.get(req.ip) || 0; + const timesSeen = delayCache.get(req.ip) || 0; if (timesSeen > 10) { return res.sendStatus(429); } - delayCache.set(req.ip, timesSeen += 1); + delayCache.set(req.ip, timesSeen + 1); setTimeout(next, 1000); }; diff --git a/src/search.js b/src/search.js index b8909b1b41..e1a7fb4287 100644 --- a/src/search.js +++ b/src/search.js @@ -76,7 +76,7 @@ async function searchInContent(data) { } return []; } - let pids = []; + let pids; let tids = []; const inTopic = String(data.query || '').match(/^in:topic-([\d]+) /); if (inTopic) { diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js index e0cf1a1075..05e5293ae4 100644 --- a/src/socket.io/topics/tags.js +++ b/src/socket.io/topics/tags.js @@ -74,7 +74,7 @@ module.exports = function (SocketTopics) { // used by tag filter search SocketTopics.tagFilterSearch = async function (socket, data) { - let cids = []; + let cids; if (Array.isArray(data.cids)) { cids = await privileges.categories.filterCids('topics:read', data.cids, socket.uid); } else { // if no cids passed in get all cids we can read @@ -82,7 +82,7 @@ module.exports = function (SocketTopics) { cids = cids.filter(cid => cid !== -1); } - let tags = []; + let tags; if (data.query) { const allowed = await privileges.global.can('search:tags', socket.uid); if (!allowed) { diff --git a/src/topics/events.js b/src/topics/events.js index 87d81b3100..59284dc84a 100644 --- a/src/topics/events.js +++ b/src/topics/events.js @@ -134,10 +134,10 @@ Events.get = async (tid, uid, reverse = false) => { return []; } - let eventIds = await db.getSortedSetRangeWithScores(`topic:${tid}:events`, 0, -1); + const eventIds = await db.getSortedSetRangeWithScores(`topic:${tid}:events`, 0, -1); const keys = eventIds.map(obj => `topicEvent:${obj.value}`); const timestamps = eventIds.map(obj => obj.score); - eventIds = eventIds.map(obj => obj.value); + let events = await db.getObjects(keys); events.forEach((e, idx) => { e.timestamp = timestamps[idx]; diff --git a/src/topics/posts.js b/src/topics/posts.js index 67d9d4ed62..535d53ff1d 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -252,7 +252,7 @@ module.exports = function (Topics) { }; Topics.getLatestUndeletedReply = async function (tid) { - let isDeleted = false; + let isDeleted; let index = 0; do { /* eslint-disable no-await-in-loop */ diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 3fc3ce5fd3..f0bb2ac731 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -43,7 +43,7 @@ module.exports = function (Topics) { const result = await plugins.hooks.fire('filter:topics.getSortedTids', { params: params, tids: [] }); return result.tids; } - let tids = []; + let tids; if (params.term !== 'alltime') { if (params.sort === 'posts') { tids = await getTidsWithMostPostsInTerm(params.cids, params.uid, params.term); diff --git a/src/topics/tags.js b/src/topics/tags.js index 0df806c34d..3cbc29a801 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -205,7 +205,7 @@ module.exports = function (Topics) { }; Topics.getTagTopicCount = async function (tag, cids = []) { - let count = 0; + let count; if (cids.length) { count = await db.sortedSetsCardSum( cids.map(cid => `cid:${cid}:tag:${tag}:topics`) @@ -476,7 +476,7 @@ module.exports = function (Topics) { if (parseInt(data.cid, 10)) { tagWhitelist = await categories.getTagWhitelist([data.cid]); } - let tags = []; + let tags; if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) { const scores = await db.sortedSetScores(`cid:${data.cid}:tags`, tagWhitelist[0]); tags = tagWhitelist[0].map((tag, index) => ({ value: tag, score: scores[index] })); diff --git a/src/topics/teaser.js b/src/topics/teaser.js index 477d2891b8..f729afb4ec 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -110,7 +110,7 @@ module.exports = function (Topics) { } async function getPreviousNonBlockedPost(postData, blockedUids) { - let isBlocked = false; + let isBlocked; let prevPost = postData; const postsPerIteration = 5; let start = 0; diff --git a/src/user/approval.js b/src/user/approval.js index 15729be36c..c0e654438c 100644 --- a/src/user/approval.js +++ b/src/user/approval.js @@ -110,9 +110,8 @@ module.exports = function (User) { }; function parseCreateOptions(userData) { - let opts = {}; try { - opts = JSON.parse(userData._opts || '{}'); + const opts = JSON.parse(userData._opts || '{}'); delete userData._opts; return opts; } catch (err) { diff --git a/src/user/profile.js b/src/user/profile.js index 8602461629..2a0c187ebc 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -257,7 +257,7 @@ module.exports = function (User) { if (!data.groupTitle) { return; } - let groupTitles = []; + let groupTitles; if (validator.isJSON(data.groupTitle)) { groupTitles = JSON.parse(data.groupTitle); if (!Array.isArray(groupTitles)) { diff --git a/test/topics.js b/test/topics.js index fb3f9cb835..3aad58afe6 100644 --- a/test/topics.js +++ b/test/topics.js @@ -888,15 +888,10 @@ describe('Topic\'s', () => { }); const { topics: topicsData } = results; - let topic; - let i; - for (i = 0; i < topicsData.length; i += 1) { - if (topicsData[i].tid === parseInt(newTid, 10)) { - assert.equal(false, topicsData[i].unread, 'ignored topic was marked as unread in recent list'); - return; - } - } - assert.ok(topic, 'topic didn\'t appear in the recent list'); + + const topic = topicsData.find(topic => topic.tid === parseInt(newTid, 10)); + assert(topic, 'ignored topic didn\'t appear in the recent list'); + assert.strictEqual(topic.unread, false, 'ignored topic was marked as unread in recent list'); }); it('should appear as unread again when marked as following', async () => {