diff --git a/install/package.json b/install/package.json index a477f60e06..76954ea502 100644 --- a/install/package.json +++ b/install/package.json @@ -107,10 +107,10 @@ "nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.87", + "nodebb-theme-harmony": "1.2.88", "nodebb-theme-lavender": "7.1.16", - "nodebb-theme-peace": "2.2.21", - "nodebb-theme-persona": "13.3.53", + "nodebb-theme-peace": "2.2.22", + "nodebb-theme-persona": "13.3.54", "nodebb-widget-essentials": "7.0.31", "nodemailer": "6.9.16", "nprogress": "0.2.0", diff --git a/public/language/en-GB/admin/settings/reputation.json b/public/language/en-GB/admin/settings/reputation.json index 479069e3a4..a4fde0d0af 100644 --- a/public/language/en-GB/admin/settings/reputation.json +++ b/public/language/en-GB/admin/settings/reputation.json @@ -20,7 +20,6 @@ "min-rep-chat": "Minimum reputation to send chat messages", "min-rep-post-links": "Minimum reputation to post links", "min-rep-flag": "Minimum reputation to flag posts", - "min-rep-website": "Minimum reputation to add \"Website\" to user profile", "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 5ddc42d596..5f69e9ffc5 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -211,6 +211,7 @@ "not-enough-reputation-custom-field": "You need %1 reputation for %2", "custom-user-field-value-too-long": "Custom field value too long, %1", "custom-user-field-select-value-invalid": "Custom field selected option is invalid, %1", + "custom-user-field-invalid-text": "Custom field text is invalid, %1", "custom-user-field-invalid-link": "Custom field link is invalid, %1", "custom-user-field-invalid-number": "Custom field number is invalid, %1", "custom-user-field-invalid-date": "Custom field date is invalid, %1", diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml index d86cecbaca..1d0dee6081 100644 --- a/public/openapi/components/schemas/UserObject.yaml +++ b/public/openapi/components/schemas/UserObject.yaml @@ -41,19 +41,11 @@ UserObject: type: string description: This is either username or fullname depending on forum and user settings example: Dragon Fruit - location: - type: string - example: 'Toronto, Canada' - nullable: true birthday: type: string description: A birthdate given in an ISO format parseable by the Date object example: 03/27/2020 nullable: true - website: - type: string - example: 'https://example.org' - nullable: true aboutme: type: string example: | @@ -172,9 +164,7 @@ UserObject: - joindate - lastonline - picture - - location - birthday - - website - aboutme - signature - uploadedpicture @@ -245,16 +235,10 @@ UserObjectFull: type: string description: This is either username or fullname depending on forum and user settings example: Dragon Fruit - location: - type: string - example: 'Toronto, Canada' birthday: type: string description: A birthdate given in an ISO format parseable by the Date object example: 03/27/2020 - website: - type: string - example: 'https://example.org' aboutme: type: string example: | @@ -508,10 +492,6 @@ UserObjectFull: - name - visibility - public - websiteLink: - type: string - websiteName: - type: string username:disableEdit: type: number email:disableEdit: diff --git a/public/openapi/read/admin/manage/users.yaml b/public/openapi/read/admin/manage/users.yaml index 80dbed1728..148a44f56c 100644 --- a/public/openapi/read/admin/manage/users.yaml +++ b/public/openapi/read/admin/manage/users.yaml @@ -15,6 +15,9 @@ get: type: array items: $ref: ../../../components/schemas/UserObject.yaml#/UserObjectACP + customUserFields: + type: array + description: array of custom user fields page: type: number pageCount: diff --git a/public/openapi/read/user/userslug/edit.yaml b/public/openapi/read/user/userslug/edit.yaml index e7fdb560a0..32c476c99f 100644 --- a/public/openapi/read/user/userslug/edit.yaml +++ b/public/openapi/read/user/userslug/edit.yaml @@ -37,8 +37,6 @@ get: type: boolean allowAccountDelete: type: boolean - allowWebsite: - type: boolean allowAboutMe: type: boolean allowSignature: diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 0f8dffaa29..a3bca173b0 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -43,12 +43,15 @@ define('admin/manage/users', [ { label: '[[admin/manage/users:export-field-followercount]]', field: 'followerCount', selected: false }, { label: '[[admin/manage/users:export-field-followingcount]]', field: 'followingCount', selected: false }, { label: '[[admin/manage/users:export-field-fullname]]', field: 'fullname', selected: false }, - { label: '[[admin/manage/users:export-field-website]]', field: 'website', selected: false }, - { label: '[[admin/manage/users:export-field-location]]', field: 'location', selected: false }, { label: '[[admin/manage/users:export-field-birthday]]', field: 'birthday', selected: false }, { label: '[[admin/manage/users:export-field-signature]]', field: 'signature', selected: false }, { label: '[[admin/manage/users:export-field-aboutme]]', field: 'aboutme', selected: false }, - ]; + ].concat(ajaxify.data.customUserFields.map(field => ({ + label: field.name, + field: field.key, + selected: false, + }))); + const options = defaultFields.map((field, i) => (`
diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index c687279698..2326361fdc 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -38,7 +38,6 @@ editController.get = async function (req, res, next) { userData.maximumProfileImageSize = meta.config.maximumProfileImageSize; userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; userData.allowAccountDelete = meta.config.allowAccountDelete === 1; - userData.allowWebsite = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:website']; userData.allowAboutMe = !isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:aboutme']; userData.allowSignature = canUseSignature && (!isSelf || !!meta.config['reputation:disabled'] || reputation >= meta.config['min:rep:signature']); userData.profileImageDimension = meta.config.profileImageDimension; diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 89e7013411..72ef70b9bd 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -104,12 +104,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {}) userData.banned = Boolean(userData.banned); userData.muted = parseInt(userData.mutedUntil, 10) > Date.now(); - userData.website = escape(userData.website); - userData.websiteLink = !userData.website.startsWith('http') ? `http://${userData.website}` : userData.website; - userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), ''); - userData.fullname = escape(userData.fullname); - userData.location = escape(userData.location); userData.signature = escape(userData.signature); userData.birthday = validator.escape(String(userData.birthday || '')); userData.moderationNote = validator.escape(String(userData.moderationNote || '')); @@ -148,6 +143,9 @@ helpers.getCustomUserFields = async function (userData) { if (f.type === 'select-multi' && userValue) { userValue = JSON.parse(userValue || '[]'); } + 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( opt => ({ value: opt, diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index a372342640..db1be70157 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -94,9 +94,10 @@ async function getUsers(req, res) { const set = buildSet(); const uids = await getUids(set); - const [count, users] = await Promise.all([ + const [count, users, customUserFields] = await Promise.all([ getCount(set), loadUserInfo(req.uid, uids), + getCustomUserFields(), ]); await render(req, res, { @@ -106,9 +107,15 @@ async function getUsers(req, res) { resultsPerPage: resultsPerPage, reverse: reverse, sortBy: sortBy, + customUserFields, }); } +async function getCustomUserFields() { + const keys = await db.getSortedSetRange('user-custom-fields', 0, -1); + return (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean); +} + usersController.search = async function (req, res) { const sortDirection = req.query.sortDirection || 'desc'; const reverse = sortDirection === 'desc'; diff --git a/src/upgrades/3.11.0/default-custom-profile-fields.js b/src/upgrades/3.11.0/default-custom-profile-fields.js new file mode 100644 index 0000000000..1739586f94 --- /dev/null +++ b/src/upgrades/3.11.0/default-custom-profile-fields.js @@ -0,0 +1,44 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Add website and location as custom profile fields', + timestamp: Date.UTC(2024, 10, 25), + method: async function () { + const minRepWebsite = parseInt(await db.getObjectField('config', 'min:rep:website'), 10) || 0; + + const website = { + icon: 'fa-solid fa-globe', + key: 'website', + 'min:rep': minRepWebsite, + name: '[[user:website]]', + 'select-options': '', + type: 'input-link', + }; + + const location = { + icon: 'fa-solid fa-map-pin', + key: 'location', + 'min:rep': 0, + name: '[[user:location]]', + 'select-options': '', + type: 'input-text', + }; + + await db.sortedSetAdd( + `user-custom-fields`, + [0, 1], + ['website', 'location'] + ); + + await db.setObjectBulk([ + [`user-custom-field:website`, website], + [`user-custom-field:location`, location], + ]); + + await db.deleteObjectField('config', 'min:rep:website'); + }, +}; diff --git a/src/user/data.js b/src/user/data.js index 037b13f887..491501a6d6 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -21,7 +21,7 @@ const intFields = [ module.exports = function (User) { const fieldWhitelist = [ 'uid', 'username', 'userslug', 'email', 'email:confirmed', 'joindate', - 'lastonline', 'picture', 'icon:bgColor', 'fullname', 'location', 'birthday', 'website', + 'lastonline', 'picture', 'icon:bgColor', 'fullname', 'birthday', 'aboutme', 'signature', 'uploadedpicture', 'profileviews', 'reputation', 'postcount', 'topiccount', 'lastposttime', 'banned', 'banned:expire', 'status', 'flags', 'followerCount', 'followingCount', 'cover:url', diff --git a/src/user/profile.js b/src/user/profile.js index ebf17f354e..7869f14410 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -77,11 +77,9 @@ module.exports = function (User) { async function validateData(callerUid, data) { await isEmailValid(data); await isUsernameAvailable(data, data.uid); - await isWebsiteValid(callerUid, data); await isAboutMeValid(callerUid, data); await isSignatureValid(callerUid, data); isFullnameValid(data); - isLocationValid(data); isBirthdayValid(data); isGroupTitleValid(data); await validateCustomFields(data); @@ -113,6 +111,10 @@ module.exports = function (User) { throw new Error(tx.compile( 'error:custom-user-field-invalid-number', field.name )); + } else if (value && type === 'input-text' && validator.isURL(value)) { + throw new Error(tx.compile( + 'error:custom-user-field-invalid-text', field.name + )); } else if (value && type === 'input-date' && !validator.isDate(value)) { throw new Error(tx.compile( 'error:custom-user-field-invalid-date', field.name @@ -197,16 +199,6 @@ module.exports = function (User) { } User.checkUsername = async username => isUsernameAvailable({ username }); - async function isWebsiteValid(callerUid, data) { - if (!data.website) { - return; - } - if (data.website.length > 255) { - throw new Error('[[error:invalid-website]]'); - } - await User.checkMinReputation(callerUid, data.uid, 'min:rep:website'); - } - async function isAboutMeValid(callerUid, data) { if (!data.aboutme) { return; @@ -235,12 +227,6 @@ module.exports = function (User) { } } - function isLocationValid(data) { - if (data.location && (validator.isURL(data.location) || data.location.length > 255)) { - throw new Error('[[error:invalid-location]]'); - } - } - function isBirthdayValid(data) { if (!data.birthday) { return; diff --git a/src/views/admin/settings/reputation.tpl b/src/views/admin/settings/reputation.tpl index daedbd76cb..10e72b6f47 100644 --- a/src/views/admin/settings/reputation.tpl +++ b/src/views/admin/settings/reputation.tpl @@ -73,10 +73,6 @@
-
- - -