mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-23 17:00:24 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9bda2af2 | ||
|
|
51e660d5ae | ||
|
|
1cdf37a218 | ||
|
|
0298a3af0d | ||
|
|
d77d2055c3 | ||
|
|
6672de00cb | ||
|
|
be62ae24ad | ||
|
|
ef5ae00652 | ||
|
|
d4a1b4dad9 | ||
|
|
47734d4cd3 | ||
|
|
4d7335903a | ||
|
|
0b92d52593 | ||
|
|
933c18f4ac | ||
|
|
3dbd2b308f |
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,3 +1,52 @@
|
||||
#### v4.0.1 (2025-01-29)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up dbsearch (88fa4553)
|
||||
* up benchpress (c9584800)
|
||||
* up harmony (10409e0e)
|
||||
* up themes (6918c3f3)
|
||||
* up themes (050effe2)
|
||||
* up harmony (90e0a2d6)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
* update changelog for v4.0.0 (ae8f58d6)
|
||||
|
||||
##### New Features
|
||||
|
||||
* use text-danger if chat over limit (2f5b4b29)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* #13087, disallow following cid -1 (ddb6e0f3)
|
||||
* encoding of pid in notifyCategoryFollowers, #13087 (6d88dcb2)
|
||||
* #13084 bump persona (4feda224)
|
||||
* closes #13091, dont show world category (4c66eed9)
|
||||
* #13088, up dbsearch (8644565a)
|
||||
* #13090, update themes fix selector (822bff62)
|
||||
* #13086 move rateLimit check (487d9f73)
|
||||
* null checks for category sync and actor assertions (b3b8b9e9)
|
||||
* #13067, add sourceContent to teasers (679fcb71)
|
||||
* #13065, send missing `actor` property when 1b12 announcing local posts (e61df4de)
|
||||
* closes #13068, encodeURIComponent X-Redirect (f3b8ed27)
|
||||
* #13062 add displayname to email tpl data (f0c2090d)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* missing ; (8b38cb3a)
|
||||
* reduce image size (#12702) (a95a51c6)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* 🤡 (4ba01d18)
|
||||
|
||||
##### Tests
|
||||
|
||||
* adjust webfinger test for updated 404 status code (4a827b7e)
|
||||
* fix x-redirect tests (b80440aa)
|
||||
* add sourceContent to spec (526a9521)
|
||||
* change test to 404 (52f7f0a7)
|
||||
* remove only (0ba4ba65)
|
||||
|
||||
#### v4.0.0 (2025-01-20)
|
||||
|
||||
##### Breaking Changes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "nodebb",
|
||||
"license": "GPL-3.0",
|
||||
"description": "NodeBB Forum",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"homepage": "https://www.nodebb.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -100,18 +100,18 @@
|
||||
"nconf": "0.12.1",
|
||||
"nodebb-plugin-2factor": "7.5.8",
|
||||
"nodebb-plugin-composer-default": "10.2.44",
|
||||
"nodebb-plugin-dbsearch": "6.2.7",
|
||||
"nodebb-plugin-emoji": "6.0.1",
|
||||
"nodebb-plugin-dbsearch": "6.2.8",
|
||||
"nodebb-plugin-emoji": "6.0.2",
|
||||
"nodebb-plugin-emoji-android": "4.1.1",
|
||||
"nodebb-plugin-markdown": "13.0.0",
|
||||
"nodebb-plugin-mentions": "4.6.10",
|
||||
"nodebb-plugin-spam-be-gone": "2.3.0",
|
||||
"nodebb-plugin-web-push": "0.7.2",
|
||||
"nodebb-rewards-essentials": "1.0.0",
|
||||
"nodebb-theme-harmony": "2.0.5",
|
||||
"nodebb-theme-harmony": "2.0.7",
|
||||
"nodebb-theme-lavender": "7.1.17",
|
||||
"nodebb-theme-peace": "2.2.35",
|
||||
"nodebb-theme-persona": "14.0.5",
|
||||
"nodebb-theme-peace": "2.2.36",
|
||||
"nodebb-theme-persona": "14.0.8",
|
||||
"nodebb-widget-essentials": "7.0.32",
|
||||
"nodemailer": "6.9.16",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"title-layout": "Title Layout",
|
||||
"title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}",
|
||||
"description.placeholder": "A short description about your community",
|
||||
"description": "Choose what page is shown when users navigate to the root URL of your forum.",
|
||||
"description": "Site Description",
|
||||
"keywords": "Site Keywords",
|
||||
"keywords-placeholder": "Keywords describing your community, comma-separated",
|
||||
"logo-and-icons": "Site Logo & Icons",
|
||||
@@ -51,6 +51,7 @@
|
||||
"topic-tools": "Topic Tools",
|
||||
"home-page": "Home Page",
|
||||
"home-page-route": "Home Page Route",
|
||||
"home-page-description": "Choose what page is shown when users navigate to the root URL of your forum.",
|
||||
"custom-route": "Custom Route",
|
||||
"allow-user-home-pages": "Allow User Home Pages",
|
||||
"home-page-title": "Title of the home page (default \"Home\")",
|
||||
|
||||
@@ -23,6 +23,10 @@ get:
|
||||
type: string
|
||||
browserTitle:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
keywords:
|
||||
type: string
|
||||
titleLayout:
|
||||
type: string
|
||||
showSiteTitle:
|
||||
|
||||
@@ -23,6 +23,10 @@ get:
|
||||
type: string
|
||||
browserTitle:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
keywords:
|
||||
type: string
|
||||
titleLayout:
|
||||
type: string
|
||||
showSiteTitle:
|
||||
|
||||
@@ -38,7 +38,7 @@ function enabledCheck(next) {
|
||||
activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) => {
|
||||
// Privilege checks should be done upstream
|
||||
const assertion = await activitypub.actors.assert(actor);
|
||||
if (!assertion || !assertion.length) {
|
||||
if (!assertion || (Array.isArray(assertion) && assertion.length)) {
|
||||
throw new Error('[[error:activitypub.invalid-id]]');
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ topicsAPI.reply = async function (caller, data) {
|
||||
return await posts.addToQueue(payload);
|
||||
}
|
||||
|
||||
const postData = await topics.reply(payload); // postData seems to be a subset of postObj, refactor?
|
||||
const postData = await topics.reply(payload);
|
||||
|
||||
const result = {
|
||||
posts: [postData],
|
||||
|
||||
@@ -194,7 +194,11 @@ 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);
|
||||
if (f.type === 'select') {
|
||||
f['select-options'].unshift('');
|
||||
}
|
||||
f['select-options'] = f['select-options'].map(
|
||||
opt => ({
|
||||
value: opt,
|
||||
selected: Array.isArray(userValue) ?
|
||||
|
||||
@@ -32,6 +32,8 @@ apiController.loadConfig = async function (req) {
|
||||
assetBaseUrl: asset_base_url, // deprecate in 1.20.x
|
||||
siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')),
|
||||
browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')),
|
||||
description: validator.escape(String(meta.config.description || '')),
|
||||
keywords: validator.escape(String(meta.config.keywords || '')),
|
||||
titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'),
|
||||
showSiteTitle: meta.config.showSiteTitle === 1,
|
||||
maintenanceMode: meta.config.maintenanceMode === 1,
|
||||
|
||||
@@ -188,7 +188,7 @@ module.exports = function (module) {
|
||||
};
|
||||
|
||||
module.deleteObjectField = async function (key, field) {
|
||||
await module.deleteObjectFields(key, [field]);
|
||||
await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]);
|
||||
};
|
||||
|
||||
module.deleteObjectFields = async function (key, fields) {
|
||||
|
||||
@@ -295,7 +295,7 @@ SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b
|
||||
};
|
||||
|
||||
module.deleteObjectField = async function (key, field) {
|
||||
await module.deleteObjectFields(key, [field]);
|
||||
await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]);
|
||||
};
|
||||
|
||||
module.deleteObjectFields = async function (key, fields) {
|
||||
@@ -377,12 +377,34 @@ RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
|
||||
if (!Array.isArray(data) || !data.length) {
|
||||
return;
|
||||
}
|
||||
// TODO: perf?
|
||||
await Promise.all(data.map(async (item) => {
|
||||
for (const [field, value] of Object.entries(item[1])) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await module.incrObjectFieldBy(item[0], field, value);
|
||||
}
|
||||
}));
|
||||
|
||||
await module.transaction(async (client) => {
|
||||
await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'hash');
|
||||
|
||||
const keys = data.map(item => item[0]);
|
||||
const dataStrings = data.map(item => JSON.stringify(item[1]));
|
||||
|
||||
await client.query({
|
||||
name: 'incrObjectFieldByBulk',
|
||||
text: `
|
||||
INSERT INTO "legacy_hash" ("_key", "data")
|
||||
SELECT k, d
|
||||
FROM UNNEST($1::TEXT[], $2::JSONB[]) vs(k, d)
|
||||
ON CONFLICT ("_key")
|
||||
DO UPDATE SET "data" = (
|
||||
SELECT jsonb_object_agg(
|
||||
key,
|
||||
CASE
|
||||
WHEN jsonb_typeof(legacy_hash.data -> key) = 'number'
|
||||
AND jsonb_typeof(EXCLUDED.data -> key) = 'number'
|
||||
THEN to_jsonb((legacy_hash.data ->> key)::NUMERIC + (EXCLUDED.data ->> key)::NUMERIC)
|
||||
ELSE COALESCE(EXCLUDED.data -> key, legacy_hash.data -> key)
|
||||
END
|
||||
)
|
||||
FROM jsonb_each(legacy_hash.data || EXCLUDED.data) AS merged(key, value)
|
||||
);`,
|
||||
values: [keys, dataStrings],
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -75,9 +75,26 @@ RETURNING A."array"[array_length(A."array", 1)] v`,
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
// TODO: remove all values with one query
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
await Promise.all(value.map(v => module.listRemoveAll(key, v)));
|
||||
await module.pool.query({
|
||||
name: 'listRemoveAllMultiple',
|
||||
text: `
|
||||
UPDATE "legacy_list" l
|
||||
SET "array" = (
|
||||
SELECT ARRAY(
|
||||
SELECT elem
|
||||
FROM unnest(l."array") WITH ORDINALITY AS u(elem, ord)
|
||||
WHERE elem NOT IN (SELECT unnest($2::TEXT[]))
|
||||
ORDER BY ord
|
||||
)
|
||||
)
|
||||
FROM "legacy_object_live" o
|
||||
WHERE o."_key" = l."_key"
|
||||
AND o."type" = l."type"
|
||||
AND o."_key" = $1::TEXT;`,
|
||||
values: [key, value],
|
||||
});
|
||||
return;
|
||||
}
|
||||
await module.pool.query({
|
||||
|
||||
@@ -547,8 +547,38 @@ RETURNING "score" s`,
|
||||
};
|
||||
|
||||
module.sortedSetIncrByBulk = async function (data) {
|
||||
// TODO: perf single query?
|
||||
return await Promise.all(data.map(item => module.sortedSetIncrBy(item[0], item[1], item[2])));
|
||||
if (!data.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await module.transaction(async (client) => {
|
||||
await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset');
|
||||
|
||||
const values = [];
|
||||
const queryParams = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
data.forEach(([key, increment, value]) => {
|
||||
value = helpers.valueToString(value);
|
||||
increment = parseFloat(increment);
|
||||
values.push(key, value, increment);
|
||||
queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`);
|
||||
paramIndex += 3;
|
||||
});
|
||||
|
||||
const query = `
|
||||
INSERT INTO "legacy_zset" ("_key", "value", "score")
|
||||
VALUES ${queryParams.join(', ')}
|
||||
ON CONFLICT ("_key", "value")
|
||||
DO UPDATE SET "score" = "legacy_zset"."score" + EXCLUDED."score"
|
||||
RETURNING "value", "score"`;
|
||||
|
||||
const res = await client.query({
|
||||
text: query,
|
||||
values,
|
||||
});
|
||||
return res.rows.map(row => parseFloat(row.score));
|
||||
});
|
||||
};
|
||||
|
||||
module.getSortedSetRangeByLex = async function (key, min, max, start, count) {
|
||||
|
||||
@@ -196,6 +196,7 @@ module.exports = function (Topics) {
|
||||
parentPosts.forEach((post, i) => {
|
||||
if (usersMap[post.uid]) {
|
||||
parents[parentPids[i]] = {
|
||||
uid: post.uid,
|
||||
username: usersMap[post.uid].username,
|
||||
displayname: usersMap[post.uid].displayname,
|
||||
};
|
||||
|
||||
@@ -127,7 +127,7 @@ module.exports = function (User) {
|
||||
));
|
||||
} else if (field.type === 'select') {
|
||||
const opts = field['select-options'].split('\n').filter(Boolean);
|
||||
if (!opts.includes(value)) {
|
||||
if (!opts.includes(value) && value !== '') {
|
||||
throw new Error(tx.compile(
|
||||
'error:custom-user-field-select-value-invalid', field.name
|
||||
));
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
<div class="">
|
||||
<p>
|
||||
[[admin/settings/general:description]]
|
||||
[[admin/settings/general:home-page-description]]
|
||||
</p>
|
||||
<form class="row">
|
||||
<div class="col-sm-12">
|
||||
|
||||
Reference in New Issue
Block a user