diff --git a/CHANGELOG.md b/CHANGELOG.md index 9110eb824c..8b494cd652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,227 @@ +#### v3.11.0 (2024-11-27) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-reputation, nodebb.error (985e5e3d) + * fallback strings for new resources: nodebb.admin-manage-user-custom-fields, nodebb.error (174be398) + * fallback strings for new resources: nodebb.error (b676c220) + * fallback strings for new resources: nodebb.admin-manage-user-custom-fields (b89036cd) + * fallback strings for new resources: nodebb.admin-settings-uploads (e3ef858b) + * fallback strings for new resources: nodebb.admin-manage-users, nodebb.error (15e16704) + * fallback strings for new resources: nodebb.topic (9fd5ca27) + * fallback strings for new resources: nodebb.pages, nodebb.user (1c26048f) +* migrate admin/settings/social.json into admin/settings/general.json (206613dd) +* migrate admin/settings/languages.json into admin/settings/general.json (ab143b1d) +* migrate admin/settings/homepage.json into admin/settings/general.json (a7678185) +* combine admin/settings/guest.json language file into admin/settings/user.json (3f14373c) +* up themes (388a156d) +* up harmony (2324a5bf) +* up harmony (03cea237) +* up themes (2ec3d1af) +* up themes (965ca636) +* up persona (1a4a9d08) +* up peace (acd42c23) +* up peace (7179a280) +* up peace (bcf8dee2) +* up peace (524b2b76) +* up themes (4ad082b4) +* up peace (953ea609) +* up widgets (6a71fd24) +* up harmony (f13b7f8d) +* up persona (6f89bf6b) +* up persona (79c93c85) +* up themes (d63a828d) +* up themes (6aa8f1b6) +* up peace (a4d8baf3) +* up themes (24e9adf6) +* up persona (baff68b4) +* up peace (c4b50607) +* up lavender (c32c63aa) +* up lavender (66214bea) +* update tx config (4ce387b9) +* up themes (65f64eba) +* up themes (280e7613) +* up widgets (b13bd803) +* incrementing version number - v3.10.3 (57d54224) +* update changelog for v3.10.3 (83965385) +* up harmony (f6f1d906) +* incrementing version number - v3.10.2 (2f15f464) +* incrementing version number - v3.10.1 (cca3a644) +* up harmony (9426fd1a) +* 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) +* **deps:** + * update commitlint monorepo to v19.6.0 (#12920) (151e0164) + * update postgres docker tag to v17.2 (#12931) (036f4564) + * update postgres docker tag to v17.1 (#12915) (0d0f9144) + * update dependency sass-embedded to v1.81.0 (#12914) (ff258028) + * update dependency sass-embedded to v1.80.6 (#12894) (37f877b3) + * update dependency sass-embedded to v1.80.5 (#12889) (383f332c) + * update dependency mocha to v10.8.2 (#12884) (d86bedf8) + * update coverallsapp/github-action action to v2.3.4 (#12867) (351bcdbf) + * update dependency sass-embedded to v1.80.4 (#12861) (be0c92ec) + * update dependency smtp-server to v3.13.6 (#12859) (a0804485) + * update coverallsapp/github-action action to v2.3.3 (#12854) (48b09f1d) + * update coverallsapp/github-action action to v2.3.2 (#12846) (c16192dc) + * update dependency sass-embedded to v1.79.5 (#12850) (6f227264) + * update dependency eslint-plugin-import to v2.31.0 (#12832) (cc2e0e9f) + * update redis docker tag to v7.4.1 (#12836) (e7cba341) + * update mongo docker tag to v8 (#12810) (2fd89558) + * update dependency sass-embedded to v1.79.4 (#12825) (24928b17) + * update postgres docker tag to v17 (#12829) (d7299f90) + +##### New Features + +* use displayname in chat system messages, closes #12937 (e6f78d25) +* move website/location fields into custom user fields (669c9c50) +* add pagination to groups page, api routes (49e0e1ab) +* add date and multiselect custom fields (9cf85ced) +* new language file for user custom fields (05fb8aa5) +* closes #12902, allow adding users as post editors (bc00df3c) +* added nn_NO, takk\! (b9eff72d) +* add read topics route (757d7101) + +##### Bug Fixes + +* update acp template to use new lang keys (bc5b91aa) +* spec, dont show registered-users, verified-users, unverified-users in manage groups (8605584f) +* #12927, give more right padding on FF (5b3b003d) +* dont show chat upload btn if maxFileSize is 0, #12926 (14125858) +* dont allow core user fields to be used as custom fields (836e8458) +* button to match category dropdown (5e9b7b44) +* filter undefined posts/topics (db291ecd) +* don't crash if post is undefined (d4770908) +* spec (d5c9c0ba) +* block search showing old matches (a9fc13dd) +* missing template (008ad1e3) +* naive checking to handle uploaded images whose filenames end with '-resized' (1268ed50) +* update copy to reflect real default value of 2000px for `resizeImageWidthThreshold` (4614958b) +* editor removal (977d80c1) +* add warning txt file for nn-NO (23b3a64a) +* incorrect folder syntax for nn_NO (396c7766) +* use postData.topic.title instead of topicData.title (d24bc5c5) +* another missing await (10a85e94) +* **deps:** + * update dependency sortablejs to v1.15.4 (#12934) (2db5b815) + * update dependency nodebb-plugin-2factor to v7.5.7 (#12930) (f9c4815a) + * update dependency @fortawesome/fontawesome-free to v6.7.1 (#12929) (80b4d10b) + * update dependency mongodb to v6.11.0 (#12932) (c3b42c85) + * update dependency @fortawesome/fontawesome-free to v6.7.0 (#12921) (a3042479) + * update dependency ace-builds to v1.36.5 (#12913) (68637633) + * update dependency sass to v1.81.0 (#12916) (ee91e516) + * update dependency cron to v3.2.1 (#12911) (42791287) + * update dependency postcss to v8.4.49 (#12910) (85f55695) + * update dependency winston to v3.17.0 (#12906) (87c276dd) + * update dependency postcss to v8.4.48 (#12907) (aa57b248) + * update dependency nodebb-theme-peace to v2.2.8 (#12901) (8af37a7d) + * update dependency cron to v3.1.9 (#12899) (87ec8637) + * update dependency ace-builds to v1.36.4 (#12898) (3aa0fa61) + * update dependency winston to v3.16.0 (#12896) (a4f75ca1) + * update dependency compression to v1.7.5 (#12890) (f5439d95) + * update dependency webpack to v5.96.1 (#12895) (3d847a5a) + * update dependency sass to v1.80.6 (#12892) (c5dd8b67) + * update dependency jquery-ui to v1.14.1 (#12891) (3fb6587a) + * update dependency nodebb-plugin-composer-default to v10.2.42 (#12888) (2cb2ac3a) + * update dependency cron to v3.1.8 (#12881) (8ae2f503) + * update dependency chart.js to v4.4.6 (#12876) (89bd1a24) + * update dependency nodemailer to v6.9.16 (#12874) (51cb249e) + * update dependency pg-cursor to v2.12.1 (#12870) (e3203b66) + * update dependency pg to v8.13.1 (#12869) (b628013d) + * update socket.io packages to v4.8.1 (#12871) (cdd9caa7) + * update dependency nodebb-theme-harmony to v1.2.77 (#12868) (d30e1f91) + * update dependency mongodb to v6.10.0 (#12862) (6da24583) + * update dependency ace-builds to v1.36.3 (#12860) (8d8243c4) + * update dependency sass to v1.80.4 (#12855) (80034ae8) + * update socket.io packages to v4.8.0 (#12815) (4788e2a3) + * update dependency chart.js to v4.4.5 (#12852) (c504b49f) + * update dependency workerpool to v9.2.0 (#12851) (8dfb2237) + * update dependency sass to v1.79.5 (#12847) (ab859aec) + * update dependency cookie-parser to v1.4.7 (#12842) (526faf07) + * update dependency express to v4.21.1 (#12843) (0e7f11bd) + * update dependency express-session to v1.18.1 (#12844) (38c1072f) + * update dependency sanitize-html to v2.13.1 (#12839) (5159552d) + * update dependency winston to v3.15.0 (#12840) (29826cf2) + * update dependency webpack to v5.95.0 (#12828) (55faa8a2) + * update dependency sass to v1.79.4 (#12826) (376ac905) + * update dependency helmet to v7.2.0 (#12827) (5acf0398) + * update dependency nodebb-theme-harmony to v1.2.71 (#12820) (b1993ff0) + +##### Other Changes + +* remove unused utils (0e4fc531) +* remove unused utils (b6a79365) +* //github.com/NodeBB/NodeBB/issues/12824 (c4a60dbb) + +##### Refactors + +* shorter check for showfullname (6ebff2e1) +* remove unused uids (20da7148) +* no category sharing for a long time (6af5cef7) +* move flags into core (3dff083d) +* add some margin bottom to bs tags (40758b32) +* post queue btn and dropdown (038fb71e) +* search logic to use switch..case (bd76ccf0) + +##### Tests + +* fix tests (173c604a) +* fix crash due to excludeGroups (61d43ee1) +* fix spec (70d99501) +* fix spec (c586854a) +* update spec on plugins (f9178a39) + #### v3.10.3 (2024-10-23) ##### Chores diff --git a/docker-compose.yml b/docker-compose.yml index f12392abd1..709cd69d03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: nodebb: build: . diff --git a/install/package.json b/install/package.json index 5fe15b3916..1ca1c335f5 100644 --- a/install/package.json +++ b/install/package.json @@ -50,7 +50,7 @@ "bootstrap": "5.3.3", "bootswatch": "5.3.3", "chalk": "4.1.2", - "chart.js": "4.4.6", + "chart.js": "4.4.7", "cli-graph": "3.2.2", "clipboard": "2.0.11", "colors": "1.4.0", @@ -69,7 +69,7 @@ "daemon": "1.1.0", "diff": "7.0.0", "esbuild": "0.24.0", - "express": "4.21.1", + "express": "4.21.2", "express-session": "1.18.1", "express-useragent": "1.0.15", "fetch-cookie": "3.0.1", @@ -105,12 +105,12 @@ "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.0.0-pre.10", "nodebb-plugin-mentions": "4.6.10", - "nodebb-plugin-spam-be-gone": "2.2.2", + "nodebb-plugin-spam-be-gone": "2.3.0", "nodebb-plugin-web-push": "0.7.0", "nodebb-rewards-essentials": "1.0.0", "nodebb-theme-harmony": "2.0.0-pre.43", - "nodebb-theme-lavender": "7.1.16", - "nodebb-theme-peace": "2.2.23", + "nodebb-theme-lavender": "7.1.17", + "nodebb-theme-peace": "2.2.28", "nodebb-theme-persona": "14.0.0-pre.5", "nodebb-widget-essentials": "7.0.31", "nodemailer": "6.9.16", @@ -129,7 +129,7 @@ "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.13.1", - "sass": "1.81.0", + "sass": "1.82.0", "satori": "^0.11.1", "semver": "7.6.3", "serve-favicon": "2.5.0", @@ -148,7 +148,7 @@ "toobusy-js": "0.5.1", "tough-cookie": "5.0.0", "validator": "13.12.0", - "webpack": "5.96.1", + "webpack": "5.97.1", "webpack-merge": "6.0.1", "winston": "3.17.0", "workerpool": "9.2.0", @@ -169,15 +169,15 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "25.0.1", - "lint-staged": "15.2.10", - "mocha": "10.8.2", + "lint-staged": "15.2.11", + "mocha": "11.0.1", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", "smtp-server": "3.13.6" }, "optionalDependencies": { - "sass-embedded": "1.81.0" + "sass-embedded": "1.82.0" }, "resolutions": { "*/jquery": "3.7.1" diff --git a/install/web.js b/install/web.js index 92fe675c22..cd38c3be6e 100644 --- a/install/web.js +++ b/install/web.js @@ -2,6 +2,7 @@ const winston = require('winston'); const express = require('express'); +const session = require('express-session'); const bodyParser = require('body-parser'); const fs = require('fs'); const path = require('path'); @@ -13,7 +14,10 @@ const nconf = require('nconf'); const Benchpress = require('benchpressjs'); const { mkdirp } = require('mkdirp'); const { paths } = require('../src/constants'); -const sass = require('../src/utils').getSass(); +const utils = require('../src/utils'); + +const sass = utils.getSass(); +const { generateToken, csrfSynchronisedProtection } = require('../src/middleware/csrf'); const app = express(); let server; @@ -73,6 +77,13 @@ web.install = async function (port) { app.use(bodyParser.urlencoded({ extended: true, })); + + app.use(session({ + secret: utils.generateUUID(), + resave: false, + saveUninitialized: false, + })); + try { await Promise.all([ compileTemplate(), @@ -103,8 +114,8 @@ function launchExpress(port) { } function setupRoutes() { - app.get('/', welcome); - app.post('/', install); + app.get('/', csrfSynchronisedProtection, welcome); + app.post('/', csrfSynchronisedProtection, install); app.get('/testdb', testDatabase); app.get('/ping', ping); app.get('/sping', ping); @@ -160,6 +171,7 @@ function welcome(req, res) { minimumPasswordStrength: defaults.minimumPasswordStrength, installing: installing, percentInstalled: installing ? ((Date.now() - timeStart) / totalTime * 100).toFixed(2) : 0, + csrf_token: generateToken(req), }); } diff --git a/public/language/zh-CN/aria.json b/public/language/zh-CN/aria.json index c69f1844a9..5bfde2aa6a 100644 --- a/public/language/zh-CN/aria.json +++ b/public/language/zh-CN/aria.json @@ -1,5 +1,5 @@ { - "post-sort-option": "帖子分类选项,1%", + "post-sort-option": "帖子分类选项,%1", "topic-sort-option": "主题分类选项,%1", "user-avatar-for": "用户头像%1", "user-watched-tags": "用户关注标签", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index a20595f84f..f7e63925ff 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -38,8 +38,8 @@ "chat.no-pinned-messages": "这里没有已置顶消息", "chat.pin-message": "置顶消息", "chat.unpin-message": "取消置顶消息", - "chat.public-rooms": "公开房间(1%)", - "chat.private-rooms": "私有房间(1%)", + "chat.public-rooms": "公开房间(%1)", + "chat.private-rooms": "私有房间(%1)", "chat.create-room": "创建聊天室", "chat.private.option": "私有(仅已加入房间用户可见)", "chat.public.option": "公开(对选中组里的所有用户可见)", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index 33e2575729..515fe112dc 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -117,9 +117,9 @@ "thread-tools.purge-confirm": "确认清除此主题吗?", "thread-tools.merge-topics": "合并主题", "thread-tools.merge": "合并主题", - "topic-move-success": "注意:此主题将会被移动到“1%”。点击此处可取消。", + "topic-move-success": "注意:此主题将会被移动到“%1”。点击此处可取消。", "topic-move-multiple-success": "注意:以下主题将会被移动到“%1”,点击此处可取消。", - "topic-move-all-success": "注意 :全部主题都将被移动到“1%”,点击此处可取消。", + "topic-move-all-success": "注意 :全部主题都将被移动到“%1”,点击此处可取消。", "topic-move-undone": "撤销主题移动", "topic-move-posts-success": "此帖子将马上移动。点击此处撤销", "topic-move-posts-undone": "撤销帖子移动", @@ -170,7 +170,7 @@ "composer.schedule": "定时", "composer.replying-to": "正在回复 %1", "composer.new-topic": "新主题", - "composer.editing-in": "在 1% 中编辑帖子", + "composer.editing-in": "在 %1 中编辑帖子", "composer.uploading": "正在上传...", "composer.thumb-url-label": "粘贴主题缩略图网址", "composer.thumb-title": "给此主题添加缩略图", diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 721ba26aab..9c710781d0 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -80,20 +80,38 @@ paths: $ref: 'read/admin/dashboard/topics.yaml' /api/admin/dashboard/searches: $ref: 'read/admin/dashboard/searches.yaml' - "/api/admin/settings/{term}": - $ref: 'read/admin/settings/term.yaml' + "/api/admin/settings/general": + $ref: 'read/admin/settings/general.yaml' /api/admin/settings/navigation: $ref: 'read/admin/settings/navigation.yaml' - /api/admin/settings/api: - $ref: 'read/admin/settings/api.yaml' - /api/admin/settings/email: - $ref: 'read/admin/settings/email.yaml' /api/admin/settings/user: $ref: 'read/admin/settings/user.yaml' + /api/admin/settings/reputation: + $ref: 'read/admin/settings/reputation.yaml' + /api/admin/settings/group: + $ref: 'read/admin/settings/group.yaml' + /api/admin/settings/tags: + $ref: 'read/admin/settings/tags.yaml' /api/admin/settings/post: $ref: 'read/admin/settings/post.yaml' /api/admin/settings/activitypub: $ref: 'read/admin/settings/activitypub.yaml' + /api/admin/settings/uploads: + $ref: 'read/admin/settings/uploads.yaml' + /api/admin/settings/email: + $ref: 'read/admin/settings/email.yaml' + /api/admin/settings/chat: + $ref: 'read/admin/settings/chat.yaml' + /api/admin/settings/pagination: + $ref: 'read/admin/settings/pagination.yaml' + /api/admin/settings/notifications: + $ref: 'read/admin/settings/notifications.yaml' + /api/admin/settings/api: + $ref: 'read/admin/settings/api.yaml' + /api/admin/settings/cookies: + $ref: 'read/admin/settings/cookies.yaml' + /api/admin/settings/web-crawler: + $ref: 'read/admin/settings/web-crawler.yaml' /api/admin/settings/advanced: $ref: 'read/admin/settings/advanced.yaml' /api/admin/manage/categories: @@ -124,8 +142,12 @@ paths: $ref: 'read/admin/manage/uploads.yaml' /api/admin/manage/digest: $ref: 'read/admin/manage/digest.yaml' - "/api/admin/appearance/{term}": - $ref: 'read/admin/appearance/term.yaml' + "/api/admin/appearance/themes": + $ref: 'read/admin/appearance/themes.yaml' + "/api/admin/appearance/skins": + $ref: 'read/admin/appearance/skins.yaml' + "/api/admin/appearance/customise": + $ref: 'read/admin/appearance/customise.yaml' /api/admin/extend/plugins: $ref: 'read/admin/extend/plugins.yaml' /api/admin/extend/widgets: diff --git a/public/openapi/read/admin/appearance/term.yaml b/public/openapi/read/admin/appearance/customise.yaml similarity index 65% rename from public/openapi/read/admin/appearance/term.yaml rename to public/openapi/read/admin/appearance/customise.yaml index 5c8364fa31..d2b2eabb68 100644 --- a/public/openapi/read/admin/appearance/term.yaml +++ b/public/openapi/read/admin/appearance/customise.yaml @@ -2,13 +2,6 @@ get: tags: - admin summary: Get appearance settings - parameters: - - name: term - in: path - required: true - schema: - type: string - example: themes responses: "200": description: "" diff --git a/public/openapi/read/admin/appearance/skins.yaml b/public/openapi/read/admin/appearance/skins.yaml new file mode 100644 index 0000000000..d2b2eabb68 --- /dev/null +++ b/public/openapi/read/admin/appearance/skins.yaml @@ -0,0 +1,11 @@ +get: + tags: + - admin + summary: Get appearance settings + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/appearance/themes.yaml b/public/openapi/read/admin/appearance/themes.yaml new file mode 100644 index 0000000000..d2b2eabb68 --- /dev/null +++ b/public/openapi/read/admin/appearance/themes.yaml @@ -0,0 +1,11 @@ +get: + tags: + - admin + summary: Get appearance settings + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/chat.yaml b/public/openapi/read/admin/settings/chat.yaml new file mode 100644 index 0000000000..402f9b173c --- /dev/null +++ b/public/openapi/read/admin/settings/chat.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get chat settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/cookies.yaml b/public/openapi/read/admin/settings/cookies.yaml new file mode 100644 index 0000000000..8f454839e5 --- /dev/null +++ b/public/openapi/read/admin/settings/cookies.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get cookie settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/term.yaml b/public/openapi/read/admin/settings/general.yaml similarity index 87% rename from public/openapi/read/admin/settings/term.yaml rename to public/openapi/read/admin/settings/general.yaml index 86eaf50b4e..d19ff555ee 100644 --- a/public/openapi/read/admin/settings/term.yaml +++ b/public/openapi/read/admin/settings/general.yaml @@ -2,13 +2,6 @@ get: tags: - admin summary: Get system settings - parameters: - - name: term - in: path - required: true - schema: - type: string - example: general responses: "200": description: "" diff --git a/public/openapi/read/admin/settings/group.yaml b/public/openapi/read/admin/settings/group.yaml new file mode 100644 index 0000000000..e4b0157a2f --- /dev/null +++ b/public/openapi/read/admin/settings/group.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get group settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/notifications.yaml b/public/openapi/read/admin/settings/notifications.yaml new file mode 100644 index 0000000000..3ef8e368ab --- /dev/null +++ b/public/openapi/read/admin/settings/notifications.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get notification settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/pagination.yaml b/public/openapi/read/admin/settings/pagination.yaml new file mode 100644 index 0000000000..cb598ef643 --- /dev/null +++ b/public/openapi/read/admin/settings/pagination.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get pagination settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/reputation.yaml b/public/openapi/read/admin/settings/reputation.yaml new file mode 100644 index 0000000000..91f6ea0561 --- /dev/null +++ b/public/openapi/read/admin/settings/reputation.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get reputation settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/tags.yaml b/public/openapi/read/admin/settings/tags.yaml new file mode 100644 index 0000000000..53d82a4355 --- /dev/null +++ b/public/openapi/read/admin/settings/tags.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get tag settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/uploads.yaml b/public/openapi/read/admin/settings/uploads.yaml new file mode 100644 index 0000000000..f181df0792 --- /dev/null +++ b/public/openapi/read/admin/settings/uploads.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get upload settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/web-crawler.yaml b/public/openapi/read/admin/settings/web-crawler.yaml new file mode 100644 index 0000000000..57963c5901 --- /dev/null +++ b/public/openapi/read/admin/settings/web-crawler.yaml @@ -0,0 +1,16 @@ +get: + tags: + - admin + summary: Get web crawler settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/src/client/users.js b/public/src/client/users.js index 47c16f9387..45ea32ea40 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -32,13 +32,16 @@ define('forum/users', [ // Populate box with query if present const searchEl = document.getElementById('search-user'); - const search = new URLSearchParams(document.location.search); - const query = search.get('query'); - if (query) { - searchEl.value = query; + if (searchEl) { + const search = new URLSearchParams(document.location.search); + const query = search.get('query'); + if (query) { + searchEl.value = query; + } + if (!utils.isMobile()) { + searchEl.focus(); + } } - - searchEl.focus(); }; function doSearch() { diff --git a/src/api/users.js b/src/api/users.js index 6450885bbf..d3f9d8f7a3 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -67,8 +67,8 @@ usersAPI.update = async function (caller, data) { privileges.users.canEdit(caller.uid, data.uid), ]); - // Changing own email/username requires password confirmation - if (data.hasOwnProperty('email') || data.hasOwnProperty('username')) { + const isChangingEmailOrUsername = data.hasOwnProperty('email') || data.hasOwnProperty('username'); + if (isChangingEmailOrUsername) { await isPrivilegedOrSelfAndPasswordMatch(caller, data); } @@ -531,7 +531,6 @@ async function processDeletion({ uid, method, password, caller }) { throw new Error('[[error:no-privileges]]'); } - // Self-deletions require a password const hasPassword = await user.hasPassword(uid); if (isSelf && hasPassword) { const ok = await user.isPasswordCorrect(uid, password, caller.ip); diff --git a/src/cache/lru.js b/src/cache/lru.js index e4fb790c93..094d3a3e93 100644 --- a/src/cache/lru.js +++ b/src/cache/lru.js @@ -2,6 +2,8 @@ module.exports = function (opts) { const { LRUCache } = require('lru-cache'); + const os = require('os'); + const pubsub = require('../pubsub'); // lru-cache@7 deprecations @@ -99,7 +101,9 @@ module.exports = function (opts) { cache.delete = cache.del; cache.reset = function () { - pubsub.publish(`${cache.name}:lruCache:reset`); + pubsub.publish(`${cache.name}:lruCache:reset`, { + id: `${os.hostname()}:${process.pid}`, + }); localReset(); }; cache.clear = cache.reset; @@ -110,8 +114,10 @@ module.exports = function (opts) { cache.misses = 0; } - pubsub.on(`${cache.name}:lruCache:reset`, () => { - localReset(); + pubsub.on(`${cache.name}:lruCache:reset`, ({ id }) => { + if (id !== `${os.hostname()}:${process.pid}`) { + localReset(); + } }); pubsub.on(`${cache.name}:lruCache:del`, (keys) => { diff --git a/src/cache/ttl.js b/src/cache/ttl.js index 5e1bc2d5cd..8647d0b9ac 100644 --- a/src/cache/ttl.js +++ b/src/cache/ttl.js @@ -2,6 +2,8 @@ module.exports = function (opts) { const TTLCache = require('@isaacs/ttlcache'); + const os = require('os'); + const pubsub = require('../pubsub'); const ttlCache = new TTLCache(opts); @@ -72,7 +74,9 @@ module.exports = function (opts) { cache.delete = cache.del; cache.reset = function () { - pubsub.publish(`${cache.name}:ttlCache:reset`); + pubsub.publish(`${cache.name}:ttlCache:reset`, { + id: `${os.hostname()}:${process.pid}`, + }); localReset(); }; cache.clear = cache.reset; @@ -83,8 +87,10 @@ module.exports = function (opts) { cache.misses = 0; } - pubsub.on(`${cache.name}:ttlCache:reset`, () => { - localReset(); + pubsub.on(`${cache.name}:ttlCache:reset`, ({ id }) => { + if (id !== `${os.hostname()}:${process.pid}`) { + localReset(); + } }); pubsub.on(`${cache.name}:ttlCache:del`, (keys) => { diff --git a/src/categories/create.js b/src/categories/create.js index 745c8b9d73..60923d28b6 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -165,7 +165,7 @@ module.exports = function (Categories) { Categories.assignColours = function () { const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946']; const text = ['#ffffff', '#ffffff', '#333333', '#ffffff', '#333333', '#ffffff', '#ffffff', '#ffffff']; - const index = Math.floor(Math.random() * backgrounds.length); + const index = utils.secureRandom(0, backgrounds.length - 1); return [backgrounds[index], text[index]]; }; diff --git a/src/cli/setup.js b/src/cli/setup.js index 859f674a9c..511dc81e73 100644 --- a/src/cli/setup.js +++ b/src/cli/setup.js @@ -51,8 +51,8 @@ async function setup(initConfig) { } console.log('NodeBB Setup Completed. Run "./nodebb start" to manually start your NodeBB server.'); - // If I am a child process, notify the parent of the returned data before exiting (useful for notifying - // hosts of auto-generated username/password during headless setups) + // If I am a child process, notify the parent of the returned data before exiting + // (useful for notifying hosts during headless setups) if (process.send) { process.send(data); } diff --git a/src/controllers/404.js b/src/controllers/404.js index 33f04f142e..becc206e76 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -8,6 +8,7 @@ const meta = require('../meta'); const plugins = require('../plugins'); const middleware = require('../middleware'); const helpers = require('../middleware/helpers'); +const { secureRandom } = require('../utils'); exports.handle404 = helpers.try(async (req, res) => { const relativePath = nconf.get('relative_path'); @@ -64,6 +65,6 @@ exports.send404 = helpers.try(async (req, res) => { path: validator.escape(path), title: '[[global:404.title]]', bodyClass: helpers.buildBodyClass(req, res), - icon: icons[Math.floor(Math.random() * icons.length)], + icon: icons[secureRandom(0, icons.length - 1)], }); }); diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js index f0659bcbe9..4e42a0bd55 100644 --- a/src/controllers/accounts/follow.js +++ b/src/controllers/accounts/follow.js @@ -1,11 +1,15 @@ 'use strict'; +const nconf = require('nconf'); + const user = require('../../user'); const helpers = require('../helpers'); const pagination = require('../../pagination'); const followController = module.exports; +const url = nconf.get('url'); + followController.getFollowing = async function (req, res, next) { await getFollow('account/following', 'following', req, res, next); }; @@ -39,5 +43,12 @@ async function getFollow(tpl, name, req, res, next) { payload.breadcrumbs = helpers.buildBreadcrumbs([{ text: username, url: `/user/${userslug}` }, { text: `[[user:${name}]]` }]); + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}${req.url.replace(/^\/api/, '')}`, + }, + ]; + res.render(tpl, payload); } diff --git a/src/controllers/accounts/groups.js b/src/controllers/accounts/groups.js index 3a9d66243e..fbe9a39407 100644 --- a/src/controllers/accounts/groups.js +++ b/src/controllers/accounts/groups.js @@ -1,11 +1,15 @@ 'use strict'; +const nconf = require('nconf'); + const user = require('../../user'); const groups = require('../../groups'); const helpers = require('../helpers'); const groupsController = module.exports; +const url = nconf.get('url'); + groupsController.get = async function (req, res) { const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']); @@ -21,5 +25,11 @@ groupsController.get = async function (req, res) { payload.groups = groupsData; payload.title = `[[pages:account/groups, ${username}]]`; payload.breadcrumbs = helpers.buildBreadcrumbs([{ text: username, url: `/user/${userslug}` }, { text: '[[global:header.groups]]' }]); + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}${req.url.replace(/^\/api/, '')}`, + }, + ]; res.render('account/groups', payload); }; diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 34da1e8473..fa8bc1ac24 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -1,5 +1,7 @@ 'use strict'; +const nconf = require('nconf'); + const db = require('../../database'); const user = require('../../user'); const posts = require('../../posts'); @@ -13,6 +15,8 @@ const utils = require('../../utils'); const postsController = module.exports; +const url = nconf.get('url'); + const templateToData = { 'account/bookmarks': { type: 'posts', @@ -247,6 +251,13 @@ async function getPostsFromUserSet(template, req, res) { option.selected = option.url.includes(`sort=${req.query.sort}`); }); + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}${req.url.replace(/^\/api/, '')}`, + }, + ]; + res.render(template, payload); } diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index 288faa8a3e..b22e6eb73c 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -16,6 +16,8 @@ const utils = require('../../utils'); const profileController = module.exports; +const url = nconf.get('url'); + profileController.get = async function (req, res, next) { const { userData } = res.locals; if (!userData) { @@ -159,11 +161,18 @@ function addTags(res, userData) { ); } + res.locals.linkTags = []; + + res.locals.linkTags.push({ + rel: 'canonical', + href: `${url}/user/${userData.userslug}`, + }); + if (meta.config.activitypubEnabled) { - res.locals.linkTags = [{ + res.locals.linkTags.push({ rel: 'alternate', type: 'application/activity+json', href: `${nconf.get('url')}/uid/${userData.uid}`, - }]; + }); } } diff --git a/src/controllers/admin/appearance.js b/src/controllers/admin/appearance.js index 5bcfa5321e..a1384a4185 100644 --- a/src/controllers/admin/appearance.js +++ b/src/controllers/admin/appearance.js @@ -2,8 +2,14 @@ const appearanceController = module.exports; -appearanceController.get = function (req, res) { - const term = req.params.term ? req.params.term : 'themes'; - - res.render(`admin/appearance/${term}`, {}); +appearanceController.themes = function (req, res) { + res.render(`admin/appearance/themes`, {}); +}; + +appearanceController.skins = function (req, res) { + res.render(`admin/appearance/skins`, {}); +}; + +appearanceController.customise = function (req, res) { + res.render(`admin/appearance/customise`, {}); }; diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js index 2faad03fc2..f46d7fd41e 100644 --- a/src/controllers/admin/cache.js +++ b/src/controllers/admin/cache.js @@ -52,7 +52,7 @@ cacheController.dump = async function (req, res, next) { local: require('../../cache'), }; caches = await plugins.hooks.fire('filter:admin.cache.get', caches); - if (!caches[req.query.name]) { + if (!caches.hasOwnProperty(req.query.name)) { return next(); } diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index a8930e35f4..405a239aaf 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -275,7 +275,7 @@ dashboardController.getLogins = async (req, res) => { res.render('admin/dashboard/logins', { set: 'logins', - query: req.query, + query: _.pick(req.query, ['units', 'until', 'count']), stats, summary, sessions, @@ -303,7 +303,7 @@ dashboardController.getUsers = async (req, res) => { res.render('admin/dashboard/users', { set: 'registrations', - query: req.query, + query: _.pick(req.query, ['units', 'until', 'count']), stats, summary, users, @@ -330,7 +330,7 @@ dashboardController.getTopics = async (req, res) => { res.render('admin/dashboard/topics', { set: 'topics', - query: req.query, + query: _.pick(req.query, ['units', 'until', 'count']), stats, summary, topics: topicData, diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index efd854a598..69d74ddbae 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -17,64 +17,20 @@ const translator = require('../../translator'); const settingsController = module.exports; -settingsController.get = async function (req, res) { - const term = req.params.term || 'general'; - const payload = { - title: `[[admin/menu:settings/${term}]]`, - }; - if (term === 'general') { - payload.routes = await helpers.getHomePageRoutes(req.uid); - payload.postSharing = await social.getPostSharing(); - const languageData = await languages.list(); - languageData.forEach((language) => { - language.selected = language.code === meta.config.defaultLang; - }); - payload.languages = languageData; - payload.autoDetectLang = meta.config.autoDetectLang; - } - res.render(`admin/settings/${term}`, payload); -}; - -settingsController.email = async (req, res) => { - const emails = await emailer.getTemplates(meta.config); - - res.render('admin/settings/email', { - title: '[[admin/menu:settings/email]]', - emails: emails, - sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path), - services: emailer.listServices(), +settingsController.general = async (req, res) => { + const routes = await helpers.getHomePageRoutes(req.uid); + const postSharing = await social.getPostSharing(); + const languageData = await languages.list(); + languageData.forEach((language) => { + language.selected = language.code === meta.config.defaultLang; }); -}; -settingsController.user = async (req, res) => { - const [notificationTypes, groupData] = await Promise.all([ - notifications.getAllNotificationTypes(), - groups.getNonPrivilegeGroups('groups:createtime', 0, -1), - ]); - const notificationSettings = notificationTypes.map(type => ({ - name: type, - label: `[[notifications:${type.replace(/_/g, '-')}]]`, - })); - res.render('admin/settings/user', { - title: '[[admin/menu:settings/user]]', - notificationSettings: notificationSettings, - groupsExemptFromNewUserRestrictions: groupData, - }); -}; - -settingsController.post = async (req, res) => { - const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); - res.render('admin/settings/post', { - title: '[[admin/menu:settings/post]]', - groupsExemptFromPostQueue: groupData, - }); -}; - -settingsController.advanced = async (req, res) => { - const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); - res.render('admin/settings/advanced', { - title: '[[admin/menu:settings/advanced]]', - groupsExemptFromMaintenanceMode: groupData, + res.render('admin/settings/general', { + title: `[[admin/menu:settings/general]]`, + routes, + postSharing, + languages: languageData, + autoDetectLang: meta.config.autoDetectLang, }); }; @@ -108,6 +64,83 @@ settingsController.navigation = async function (req, res) { res.render('admin/settings/navigation', admin); }; +settingsController.user = async (req, res) => { + const [notificationTypes, groupData] = await Promise.all([ + notifications.getAllNotificationTypes(), + groups.getNonPrivilegeGroups('groups:createtime', 0, -1), + ]); + const notificationSettings = notificationTypes.map(type => ({ + name: type, + label: `[[notifications:${type.replace(/_/g, '-')}]]`, + })); + res.render('admin/settings/user', { + title: '[[admin/menu:settings/user]]', + notificationSettings: notificationSettings, + groupsExemptFromNewUserRestrictions: groupData, + }); +}; + +settingsController.reputation = async (req, res) => { + res.render(`admin/settings/reputation`, { + title: `[[admin/menu:settings/reputation]]`, + }); +}; + +settingsController.group = async (req, res) => { + res.render(`admin/settings/group`, { + title: `[[admin/menu:settings/group]]`, + }); +}; + +settingsController.tags = async (req, res) => { + res.render(`admin/settings/tags`, { + title: `[[admin/menu:settings/tags]]`, + }); +}; + +settingsController.post = async (req, res) => { + const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); + res.render('admin/settings/post', { + title: '[[admin/menu:settings/post]]', + groupsExemptFromPostQueue: groupData, + }); +}; + +settingsController.uploads = async (req, res) => { + res.render(`admin/settings/uploads`, { + title: `[[admin/menu:settings/uploads]]`, + }); +}; + +settingsController.email = async (req, res) => { + const emails = await emailer.getTemplates(meta.config); + + res.render('admin/settings/email', { + title: '[[admin/menu:settings/email]]', + emails: emails, + sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path), + services: emailer.listServices(), + }); +}; + +settingsController.chat = async (req, res) => { + res.render(`admin/settings/chat`, { + title: `[[admin/menu:settings/chat]]`, + }); +}; + +settingsController.pagination = async (req, res) => { + res.render(`admin/settings/pagination`, { + title: `[[admin/menu:settings/pagination]]`, + }); +}; + +settingsController.notifications = async (req, res) => { + res.render(`admin/settings/notifications`, { + title: `[[admin/menu:settings/notifications]]`, + }); +}; + settingsController.api = async (req, res) => { const page = parseInt(req.query.page, 10) || 1; const resultsPerPage = 50; @@ -133,3 +166,27 @@ settingsController.activitypub = async (req, res) => { instanceCount, }); }; + +settingsController.cookies = async (req, res) => { + res.render(`admin/settings/cookies`, { + title: `[[admin/menu:settings/cookies]]`, + }); +}; + +settingsController.webCrawler = async (req, res) => { + res.render(`admin/settings/web-crawler`, { + title: `[[admin/menu:settings/web-crawler]]`, + }); +}; + +settingsController.advanced = async (req, res) => { + const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); + res.render('admin/settings/advanced', { + title: '[[admin/menu:settings/advanced]]', + groupsExemptFromMaintenanceMode: groupData, + }); +}; + + + + diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js index db08d08f84..339884c95d 100644 --- a/src/controllers/admin/themes.js +++ b/src/controllers/admin/themes.js @@ -20,12 +20,18 @@ themesController.get = async function (req, res, next) { themeConfig = JSON.parse(themeConfig); } catch (err) { if (err.code === 'ENOENT') { - return next(Error('invalid-data')); + return next(Error('[[error:invalid-data]]')); } return next(err); } - const screenshotPath = themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath; - const exists = await file.exists(screenshotPath); + const screenshotPath = themeConfig.screenshot ? + path.join(themeDir, themeConfig.screenshot) : + ''; + + if (screenshotPath && !screenshotPath.startsWith(themeDir)) { + throw new Error('[[error:invalid-path]]'); + } + const exists = screenshotPath ? await file.exists(screenshotPath) : false; res.sendFile(exists ? screenshotPath : defaultScreenshotPath); }; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 76c1b00f97..4638384b6e 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -12,6 +12,8 @@ const privileges = require('../privileges'); const groupsController = module.exports; +const url = nconf.get('url'); + groupsController.list = async function (req, res) { const sort = req.query.sort || 'alpha'; const page = parseInt(req.query.page, 10) || 1; @@ -20,6 +22,13 @@ groupsController.list = async function (req, res) { getGroups(req, sort, page), ]); + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}${req.url.replace(/^\/api/, '')}`, + }, + ]; + res.render('groups/list', { groups: groupData, allowGroupCreation: allowGroupCreation, @@ -101,6 +110,13 @@ groupsController.details = async function (req, res, next) { return next(); } + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}/groups/${lowercaseSlug}`, + }, + ]; + res.render('groups/details', { title: `[[pages:group, ${groupData.displayName}]]`, group: groupData, diff --git a/src/controllers/tags.js b/src/controllers/tags.js index 4b8385b177..d2a0ee5e6b 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -14,6 +14,8 @@ const helpers = require('./helpers'); const tagsController = module.exports; +const url = nconf.get('url'); + tagsController.getTag = async function (req, res) { const tag = validator.escape(utils.cleanUpTag(req.params.tag, meta.config.maximumTagLength)); const page = parseInt(req.query.page, 10) || 1; @@ -98,6 +100,13 @@ tagsController.getTags = async function (req, res) { topics.getCategoryTagsData(cids, 0, 99), ]); + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}/tags`, + }, + ]; + res.render('tags', { tags: tags.filter(Boolean), displayTagSearch: canSearch, diff --git a/src/controllers/users.js b/src/controllers/users.js index ce45c709f9..5f290bdc9a 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -1,5 +1,7 @@ 'use strict'; +const nconf = require('nconf'); + const user = require('../user'); const meta = require('../meta'); @@ -12,6 +14,8 @@ const utils = require('../utils'); const usersController = module.exports; +const url = nconf.get('url'); + usersController.index = async function (req, res, next) { const section = req.query.section || 'joindate'; const sectionToController = { @@ -206,6 +210,13 @@ async function render(req, res, data) { data['reputation:disabled'] = meta.config['reputation:disabled']; + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}${req.url.replace(/^\/api/, '')}`, + }, + ]; + res.append('X-Total-Count', data.userCount); res.render('users', data); } diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js index 3f110b79f9..0c84a0f210 100644 --- a/src/database/mongo/sets.js +++ b/src/database/mongo/sets.js @@ -3,6 +3,7 @@ module.exports = function (module) { const _ = require('lodash'); const helpers = require('./helpers'); + const { secureRandom } = require('../../utils'); module.setAdd = async function (key, value) { if (!Array.isArray(value)) { @@ -200,7 +201,7 @@ module.exports = function (module) { return; } - const randomIndex = Math.floor(Math.random() * data.members.length); + const randomIndex = secureRandom(0, data.members.length - 1); const value = data.members[randomIndex]; await module.setRemove(data._key, value); return value; diff --git a/src/flags.js b/src/flags.js index 5d14319f76..d19da08d95 100644 --- a/src/flags.js +++ b/src/flags.js @@ -687,6 +687,7 @@ Flags.canFlag = async function (type, id, uid, skipLimitCheck = false) { Flags.getTarget = async function (type, id, uid) { if (type === 'user') { const userData = await user.getUserData(id); + userData.aboutme = validator.escape(String(userData.aboutme)); return userData && userData.uid ? userData : {}; } if (type === 'post') { diff --git a/src/install.js b/src/install.js index 04c8ed569a..90977b955d 100644 --- a/src/install.js +++ b/src/install.js @@ -370,26 +370,21 @@ async function createAdmin() { } async function retryPassword(originalResults) { - // Ask only the password questions const results = await prompt.get(passwordQuestions); - // Update the original data with newly collected password originalResults.password = results.password; originalResults['password:confirm'] = results['password:confirm']; - // Send back to success to handle return await success(originalResults); } - // Add the password questions questions = questions.concat(passwordQuestions); if (!install.values) { const results = await prompt.get(questions); return await success(results); } - // If automated setup did not provide a user password, generate one, - // it will be shown to the user upon setup completion + if (!install.values.hasOwnProperty('admin:password') && !nconf.get('admin:password')) { console.log('Password was not provided during automated setup, generating one...'); password = utils.generateUUID().slice(0, 8); diff --git a/src/meta/cacheBuster.js b/src/meta/cacheBuster.js index 58129ff2f8..d1e4d2d239 100644 --- a/src/meta/cacheBuster.js +++ b/src/meta/cacheBuster.js @@ -11,7 +11,11 @@ let cached; // cache buster is an 11-character, lowercase, alphanumeric string function generate() { - return (Math.random() * 1e18).toString(32).slice(0, 11); + const crypto = require('crypto'); + const length = 11; + const generated = crypto.randomBytes(Math.ceil(length / 2)) + .toString('hex').slice(0, length); + return generated; } exports.write = async function write() { diff --git a/src/middleware/admin.js b/src/middleware/admin.js index bf89079103..84d34dfbd0 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -47,7 +47,6 @@ middleware.checkPrivileges = helpers.try(async (req, res, next) => { } } - // If user does not have password const hasPassword = await user.hasPassword(req.uid); if (!hasPassword) { return next(); diff --git a/src/password.js b/src/password.js index 7c0d01931c..6f9617f6ab 100644 --- a/src/password.js +++ b/src/password.js @@ -29,7 +29,9 @@ async function getFakeHash() { if (fakeHashCache) { return fakeHashCache; } - fakeHashCache = await exports.hash(12, Math.random().toString()); + const length = 18; + fakeHashCache = crypto.randomBytes(Math.ceil(length / 2)) + .toString('hex').slice(0, length); return fakeHashCache; } diff --git a/src/routes/admin.js b/src/routes/admin.js index 30b8f7b947..21bfdd5f31 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -33,16 +33,28 @@ module.exports = function (app, name, middleware, controllers) { helpers.setupAdminPageRoute(app, `/${name}/manage/uploads`, middlewares, controllers.admin.uploads.get); helpers.setupAdminPageRoute(app, `/${name}/manage/digest`, middlewares, controllers.admin.digest.get); - helpers.setupAdminPageRoute(app, `/${name}/settings/email`, middlewares, controllers.admin.settings.email); - helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user); - helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post); - helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced); + helpers.setupAdminPageRoute(app, `/${name}/settings/general`, middlewares, controllers.admin.settings.general); helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation); + helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user); + helpers.setupAdminPageRoute(app, `/${name}/settings/reputation`, middlewares, controllers.admin.settings.reputation); + helpers.setupAdminPageRoute(app, `/${name}/settings/group`, middlewares, controllers.admin.settings.group); + helpers.setupAdminPageRoute(app, `/${name}/settings/tags`, middlewares, controllers.admin.settings.tags); + helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post); + helpers.setupAdminPageRoute(app, `/${name}/settings/uploads`, middlewares, controllers.admin.settings.uploads); + helpers.setupAdminPageRoute(app, `/${name}/settings/email`, middlewares, controllers.admin.settings.email); + helpers.setupAdminPageRoute(app, `/${name}/settings/chat`, middlewares, controllers.admin.settings.chat); + helpers.setupAdminPageRoute(app, `/${name}/settings/pagination`, middlewares, controllers.admin.settings.pagination); + helpers.setupAdminPageRoute(app, `/${name}/settings/notifications`, middlewares, controllers.admin.settings.notifications); helpers.setupAdminPageRoute(app, `/${name}/settings/api`, middlewares, controllers.admin.settings.api); helpers.setupAdminPageRoute(app, `/${name}/settings/activitypub`, middlewares, controllers.admin.settings.activitypub); - helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get); + helpers.setupAdminPageRoute(app, `/${name}/settings/cookies`, middlewares, controllers.admin.settings.cookies); + helpers.setupAdminPageRoute(app, `/${name}/settings/web-crawler`, middlewares, controllers.admin.settings.webCrawler); + helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced); + + helpers.setupAdminPageRoute(app, `/${name}/appearance/themes`, middlewares, controllers.admin.appearance.themes); + helpers.setupAdminPageRoute(app, `/${name}/appearance/skins`, middlewares, controllers.admin.appearance.skins); + helpers.setupAdminPageRoute(app, `/${name}/appearance/customise`, middlewares, controllers.admin.appearance.customise); - helpers.setupAdminPageRoute(app, `/${name}/appearance/:term?`, middlewares, controllers.admin.appearance.get); helpers.setupAdminPageRoute(app, `/${name}/extend/plugins`, middlewares, controllers.admin.plugins.get); helpers.setupAdminPageRoute(app, `/${name}/extend/widgets`, middlewares, controllers.admin.extend.widgets.get); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 51e5dc9f71..48b491ab43 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -24,7 +24,6 @@ require('./user/status')(SocketUser); require('./user/picture')(SocketUser); require('./user/registration')(SocketUser); -// Password Reset SocketUser.reset = {}; SocketUser.reset.send = async function (socket, email) { @@ -47,10 +46,10 @@ SocketUser.reset.send = async function (socket, email) { try { await user.reset.send(email); await logEvent('[[success:success]]'); - await sleep(2500 + ((Math.random() * 500) - 250)); + await sleep(2500 + (utils.secureRandom(0, 500) - 250)); } catch (err) { await logEvent(err.message); - await sleep(2500 + ((Math.random() * 500) - 250)); + await sleep(2500 + (utils.secureRandom(0, 500) - 250)); const internalErrors = ['[[error:invalid-email]]']; if (!internalErrors.includes(err.message)) { throw err; diff --git a/src/user/data.js b/src/user/data.js index 85b1df2c9d..ec643f729b 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -93,7 +93,6 @@ module.exports = function (User) { if (!fields.length) { fields = results.whitelist; } else { - // Never allow password retrieval via this method fields = fields.filter(value => value !== 'password'); } diff --git a/src/user/index.js b/src/user/index.js index 1afb94c778..35c3e67f3e 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -282,7 +282,7 @@ User.addInterstitials = function (callback) { plugins.hooks.register('core', { hook: 'filter:register.interstitial', method: [ - User.interstitials.email, // Email address (for password reset + digest) + User.interstitials.email, // Email address User.interstitials.gdpr, // GDPR information collection/processing consent + email consent User.interstitials.tou, // Forum Terms of Use ], diff --git a/src/user/reset.js b/src/user/reset.js index 0ef7ec4f40..5db6355e29 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -108,7 +108,6 @@ UserReset.commit = async function (code, password) { 'password:shaWrapped': 1, }; - // don't verify email if password reset is due to expiry const isPasswordExpired = userData.passwordExpiry && userData.passwordExpiry < Date.now(); if (!isPasswordExpired) { data['email:confirmed'] = 1; diff --git a/src/utils.js b/src/utils.js index fb59865f69..6600b855ce 100644 --- a/src/utils.js +++ b/src/utils.js @@ -31,6 +31,16 @@ utils.generateUUID = function () { return rnd.join('-'); }; +utils.secureRandom = function (low, high) { + if (low > high) { + throw new Error("The 'low' parameter must be less than or equal to the 'high' parameter."); + } + const randomBuffer = crypto.randomBytes(4); + const randomInt = randomBuffer.readUInt32BE(0); + const range = high - low + 1; + return low + (randomInt % range); +}; + utils.getSass = function () { try { const sass = require('sass-embedded'); diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 549b23a234..a89fb850a8 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -103,7 +103,7 @@
[[admin/settings/social:info-plugins-additional]]
+[[admin/settings/general:info-plugins-additional]]