mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 08:37:05 +02:00
Merge commit 'fc9cc8d6d7f66275e91be129f0bd59db79672af1' into v4.x
This commit is contained in:
130
CHANGELOG.md
130
CHANGELOG.md
@@ -1,3 +1,133 @@
|
||||
#### v4.1.0 (2025-02-27)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v4.0.6 (4a52fb2e)
|
||||
* update changelog for v4.0.6 (78bbea30)
|
||||
* comment out testing helper call (bad0a4c2)
|
||||
* incrementing version number - v4.0.5 (1792a62b)
|
||||
* incrementing version number - v4.0.4 (b1125cce)
|
||||
* incrementing version number - v4.0.3 (2b65c735)
|
||||
* up harmony (ea110a0e)
|
||||
* incrementing version number - v4.0.2 (73fe5fcf)
|
||||
* incrementing version number - v4.0.1 (a461b758)
|
||||
* bump emoji for #13077 as well (ff0de097)
|
||||
* fix ap dev helper (b14494b0)
|
||||
* add helper method to ease in dev (d7d64a14)
|
||||
* add helper method to ease in dev (7d5482b2)
|
||||
* incrementing version number - v4.0.0 (c1eaee45)
|
||||
* **deps:**
|
||||
* update dependency sass-embedded to v1.85.1 (#13208) (3907e6c8)
|
||||
* update postgres docker tag to v17.4 (#13196) (cba2bc5e)
|
||||
* update postgres docker tag to v17.3 (#13162) (47e28a0e)
|
||||
* update dependency sass-embedded to v1.85.0 (#13161) (2258e145)
|
||||
* update commitlint monorepo to v19.7.1 (#13123) (ca6734b3)
|
||||
* update coverallsapp/github-action action to v2.3.6 (#13089) (84b28fae)
|
||||
* update dependency lint-staged to v15.4.3 (#13079) (1d846134)
|
||||
* update dependency mocha to v11.1.0 (#13069) (8e99c97a)
|
||||
* update dependency lint-staged to v15.4.1 (#13060) (153e65bc)
|
||||
* update dependency lint-staged to v15.4.1 (#13060) (37b2b83d)
|
||||
* **i18n:**
|
||||
* fallback strings for new resources: nodebb.category (00253821)
|
||||
* fallback strings for new resources: nodebb.error (589be143)
|
||||
* fallback strings for new resources: nodebb.themes-harmony (25049714)
|
||||
* fallback strings for new resources: nodebb.admin-settings-advanced (ad6b6132)
|
||||
* fallback strings for new resources: nodebb.themes-harmony (fc063bb0)
|
||||
* fallback strings for new resources: nodebb.admin-settings-general (d41109a0)
|
||||
|
||||
##### New Features
|
||||
|
||||
* support remote "Video" type objects in note assertion, #13120 (95f2c4ed)
|
||||
* 1b12 compatibility (7dc1e8ab)
|
||||
* remove activities older than a week (d9e86c7b)
|
||||
* federate out Announce of a tid's mainPid if the tid is moved out of cid -1 (b7f9983a)
|
||||
* syncUserInboxes to take into account remote topic tags, closes #13074 (637addc4)
|
||||
* allow search bar to load remote 7888 Conversations, aka nodebb topics (7687da00)
|
||||
* introduce new 'markdown' post parsing type, closes #13077 (b386e4a6)
|
||||
* #13066, report canonical URL in user agent for outgoing requests (c3e9cb68)
|
||||
* changes to how a topic is presented via ActivityPub; conformance with upcoming changes to 7888 (4fd7a9dc)
|
||||
* changes to how a topic is presented via ActivityPub; conformance with upcoming changes to 7888 (adeaff4b)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* scheduled topics and posts should return 404 on AP request (428300de)
|
||||
* tag handling when remote objects contain tags without leading # symbol (5c3f1cfe)
|
||||
* handle multiple types in remote actor payload (65895651)
|
||||
* missing db (058befb3)
|
||||
* remove handle on category purge (adb430f2)
|
||||
* restrict feps methods to real cids (8b717d54)
|
||||
* restore old behaviour of 1b12 federating both object and activity (f0ee43dc)
|
||||
* send `actor` with 1b12 announce, fixes #13072 again (86b0e591)
|
||||
* isArray check (5f3ed76d)
|
||||
* delete from payload instead of setting null (489c5ce2)
|
||||
* send `actor` with 1b12 announce, fixes #13072 (3cd87f1b)
|
||||
* #13139, payload.version can be null (be1598d1)
|
||||
* tidChanged (bfd4e68b)
|
||||
* bad logic that invisibly broke outgoing user follows completely (334be721)
|
||||
* #13076, allow pulling in of topics by their topic URL fix: reapply fixes that were part of since-reverted 4fd7a9dc59b65e8654d704c493f2254793e8d6a9 (c6e6ab43)
|
||||
* call relativeToAbsolute helper when generating markdown source content in mocks.notes.public/private (02fb99eb)
|
||||
* extend remoteAnchorToLocalProfile ap helper to handle markdown content (db1f8959)
|
||||
* incorrect `posts` url in topic posts collection (812ec73e)
|
||||
* incorrect `posts` url in topic posts collection (b2530e61)
|
||||
* **deps:**
|
||||
* update dependency sass to v1.85.1 (#13209) (386ab89f)
|
||||
* update dependency bcryptjs to v3 (#13160) (6ea65678)
|
||||
* update dependency cron to v4 (#13184) (41eec8d7)
|
||||
* update dependency xregexp to v5.1.2 (#13195) (23621eca)
|
||||
* update dependency ace-builds to v1.39.0 (#13197) (a3f5721a)
|
||||
* update dependency chart.js to v4.4.8 (#13182) (474d267e)
|
||||
* update dependency postcss to v8.5.3 (#13183) (5fc4c806)
|
||||
* update dependency mongodb to v6.13.1 (#13187) (77b0160c)
|
||||
* update dependency nodebb-plugin-web-push to v0.7.3 (#13178) (000ceee4)
|
||||
* update dependency sass to v1.85.0 (#13163) (75a7188a)
|
||||
* update dependency pg to v8.13.3 (#13157) (f3c156e9)
|
||||
* update dependency pg-cursor to v2.12.3 (#13158) (6b8e4b39)
|
||||
* update dependency webpack to v5.98.0 (#13159) (db74c1e8)
|
||||
* update dependency nodebb-widget-essentials to v7.0.33 (#13156) (af7f4242)
|
||||
* update dependency pg-cursor to v2.12.2 (#13150) (b5ce9e14)
|
||||
* update dependency compression to v1.8.0 (#13152) (1e52cf34)
|
||||
* update dependency ace-builds to v1.38.0 (#13151) (db0b816c)
|
||||
* update dependency pg to v8.13.2 (#13149) (bea1367d)
|
||||
* update dependency postcss to v8.5.2 (#13144) (3449e76d)
|
||||
* update dependency benchpressjs to v2.5.3 (#13098) (6688edde)
|
||||
* update dependency esbuild to v0.25.0 (#13141) (d7fdd80c)
|
||||
* update dependency tough-cookie to v5.1.1 (#13140) (33ce7239)
|
||||
* update dependency ioredis to v5.5.0 (#13138) (b337e999)
|
||||
* update dependency sass to v1.84.0 (#13128) (f872a768)
|
||||
* update dependency semver to v7.7.1 (#13122) (5f3c5a55)
|
||||
* update dependency mongodb to v6.13.0 (#13106) (31ff6c2e)
|
||||
* update dependency semver to v7.7.0 (#13099) (a348e808)
|
||||
* update dependency nodemailer to v6.10.0 (#13073) (8ab71e4f)
|
||||
* update dependency nodebb-theme-persona to v14.0.2 (#13064) (8ec3ceae)
|
||||
* update dependency nodebb-theme-harmony to v2.0.3 (#13063) (b98d047a)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* remove unused db (06b3d9ad)
|
||||
* remove tab (54bc54e1)
|
||||
* fix tab (397d28e3)
|
||||
|
||||
##### Performance Improvements
|
||||
|
||||
* closes #13145, reduce calls in actors.prune (676acb7e)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* remove cid:-1:tids (and variants) from intersection in /world, fixes #13125 (d0561a60)
|
||||
* single remove (0784e11b)
|
||||
* move 1b12 announce logic out of inbox and into separate feps module (9fd6ac6b)
|
||||
* acceptable types in context.js to index.js, allow searching for remote topis by topic url (d644c0f4)
|
||||
* Posts.relativeToAbsolute so that the regexes passed to it no longer need a pre-defined length, it is now calculated from the match result, added new regex for markdown image/anchors (f64e6f0f)
|
||||
|
||||
##### Tests
|
||||
|
||||
* moved AP actor tests to separate actors.js file, added failing test for scheduled topics (01be4d79)
|
||||
* update test to assert the note assertion itself (c6ba56a5)
|
||||
* update bcrypt hash for 3.x (bfffbfbe)
|
||||
* update pwd test for bcrypt3.x (ca0fa1d3)
|
||||
* add sourceContent to spec (d1d55461)
|
||||
* adjust webfinger test for updated 404 status code (59afd193)
|
||||
|
||||
#### v4.0.6 (2025-02-27)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"username:disableEdit": 0,
|
||||
"email:disableEdit": 0,
|
||||
"email:smtpTransport:pool": 0,
|
||||
"email:smtpTransport:allow-self-signed": 0,
|
||||
"hideFullname": 0,
|
||||
"hideEmail": 0,
|
||||
"showFullnameAsDisplayName": 0,
|
||||
|
||||
@@ -103,15 +103,15 @@
|
||||
"nodebb-plugin-dbsearch": "6.2.13",
|
||||
"nodebb-plugin-emoji": "6.0.2",
|
||||
"nodebb-plugin-emoji-android": "4.1.1",
|
||||
"nodebb-plugin-markdown": "13.1.0",
|
||||
"nodebb-plugin-mentions": "4.7.0",
|
||||
"nodebb-plugin-markdown": "13.1.1",
|
||||
"nodebb-plugin-mentions": "4.7.1",
|
||||
"nodebb-plugin-spam-be-gone": "2.3.1",
|
||||
"nodebb-plugin-web-push": "0.7.3",
|
||||
"nodebb-rewards-essentials": "1.0.1",
|
||||
"nodebb-theme-harmony": "2.0.37",
|
||||
"nodebb-theme-lavender": "7.1.17",
|
||||
"nodebb-theme-harmony": "2.0.39",
|
||||
"nodebb-theme-lavender": "7.1.18",
|
||||
"nodebb-theme-peace": "2.2.39",
|
||||
"nodebb-theme-persona": "14.0.15",
|
||||
"nodebb-theme-persona": "14.0.16",
|
||||
"nodebb-widget-essentials": "7.0.35",
|
||||
"nodemailer": "6.10.0",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"federation.followers-handle": "Handle",
|
||||
"federation.followers-id": "ID",
|
||||
"federation.followers-none": "No followers.",
|
||||
"federation.followers-autofill": "Autofill",
|
||||
|
||||
"alert.created": "Created",
|
||||
"alert.create-success": "Category successfully created!",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"smtp-transport.password": "Password",
|
||||
"smtp-transport.pool": "Enable pooled connections",
|
||||
"smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.",
|
||||
"smtp-transport.allow-self-signed": "Allow self-signed certificates",
|
||||
"smtp-transport.allow-self-signed-help": "Enabling this setting will allow you to use self-signed or invalid TLS certificates.",
|
||||
|
||||
"template": "Edit Email Template",
|
||||
"template.select": "Select Email Template",
|
||||
|
||||
@@ -297,7 +297,6 @@ inbox.announce = async (req) => {
|
||||
}
|
||||
|
||||
({ tid } = assertion);
|
||||
await topics.updateLastPostTime(tid, timestamp);
|
||||
await activitypub.notes.updateLocalRecipients(pid, { to, cc });
|
||||
await activitypub.notes.syncUserInboxes(tid);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
const path = require('path');
|
||||
const nconf = require('nconf');
|
||||
const fs = require('fs');
|
||||
const winston = require('winston');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
const meta = require('../../meta');
|
||||
const posts = require('../../posts');
|
||||
@@ -22,9 +24,15 @@ uploadsController.get = async function (req, res, next) {
|
||||
}
|
||||
const itemsPerPage = 20;
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
let files = [];
|
||||
try {
|
||||
await checkSymLinks(req.query.dir);
|
||||
files = await getFilesInFolder(currentFolder);
|
||||
} catch (err) {
|
||||
winston.error(err.stack);
|
||||
return next(new Error('[[error:invalid-path]]'));
|
||||
}
|
||||
try {
|
||||
let files = await fs.promises.readdir(currentFolder);
|
||||
files = files.filter(filename => filename !== '.gitignore');
|
||||
const itemCount = files.length;
|
||||
const start = Math.max(0, (page - 1) * itemsPerPage);
|
||||
const stop = start + itemsPerPage;
|
||||
@@ -64,6 +72,34 @@ uploadsController.get = async function (req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
async function checkSymLinks(folder) {
|
||||
let dir = path.normalize(folder || '');
|
||||
while (dir.length && dir !== '.') {
|
||||
const nextPath = path.join(nconf.get('upload_path'), dir);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const stat = await fs.promises.lstat(nextPath);
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new Error('[[invalid-path]]');
|
||||
}
|
||||
const newDir = path.dirname(dir);
|
||||
if (newDir === dir) {
|
||||
break;
|
||||
}
|
||||
dir = newDir;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFilesInFolder(folder) {
|
||||
const dirents = await fs.promises.readdir(folder, { withFileTypes: true });
|
||||
const files = [];
|
||||
for await (const dirent of dirents) {
|
||||
if (!dirent.isSymbolicLink() && dirent.name !== '.gitignore') {
|
||||
files.push(dirent.name);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function buildBreadcrumbs(currentFolder) {
|
||||
const crumbs = [];
|
||||
const parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep);
|
||||
@@ -94,14 +130,14 @@ async function getFileData(currentDir, file) {
|
||||
const stat = await fs.promises.stat(pathToFile);
|
||||
let filesInDir = [];
|
||||
if (stat.isDirectory()) {
|
||||
filesInDir = await fs.promises.readdir(pathToFile);
|
||||
filesInDir = await getFilesInFolder(pathToFile);
|
||||
}
|
||||
const url = `${nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '')}/${file}`;
|
||||
return {
|
||||
name: file,
|
||||
path: pathToFile.replace(path.join(nconf.get('upload_path'), '/'), ''),
|
||||
url: url,
|
||||
fileCount: Math.max(0, filesInDir.length - 1), // ignore .gitignore
|
||||
fileCount: filesInDir.length,
|
||||
size: stat.size,
|
||||
sizeHumanReadable: `${(stat.size / 1024).toFixed(1)}KiB`,
|
||||
isDirectory: stat.isDirectory(),
|
||||
@@ -121,11 +157,50 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) {
|
||||
return next(new Error('[[error:invalid-json]]'));
|
||||
}
|
||||
|
||||
if (uploadedFile.path.endsWith('.svg')) {
|
||||
await sanitizeSvg(uploadedFile.path);
|
||||
}
|
||||
|
||||
await validateUpload(uploadedFile, allowedImageTypes);
|
||||
const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`;
|
||||
await uploadImage(filename, 'category', uploadedFile, req, res, next);
|
||||
};
|
||||
|
||||
async function sanitizeSvg(filePath) {
|
||||
const dirty = await fs.promises.readFile(filePath, 'utf8');
|
||||
const clean = sanitizeHtml(dirty, {
|
||||
allowedTags: [
|
||||
'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop',
|
||||
'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect',
|
||||
'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern',
|
||||
'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode',
|
||||
],
|
||||
allowedAttributes: {
|
||||
'*': [
|
||||
// Geometry
|
||||
'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
|
||||
'width', 'height', 'd', 'points', 'viewBox', 'transform',
|
||||
|
||||
// Presentation
|
||||
'fill', 'stroke', 'stroke-width', 'opacity',
|
||||
'stop-color', 'stop-opacity', 'offset', 'style', 'class',
|
||||
|
||||
// Text
|
||||
'text-anchor', 'font-size', 'font-family',
|
||||
|
||||
// Misc
|
||||
'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform',
|
||||
'xmlns', 'preserveAspectRatio',
|
||||
],
|
||||
},
|
||||
parser: {
|
||||
lowerCaseTags: false,
|
||||
lowerCaseAttributeNames: false,
|
||||
},
|
||||
});
|
||||
await fs.promises.writeFile(filePath, clean);
|
||||
}
|
||||
|
||||
uploadsController.uploadFavicon = async function (req, res, next) {
|
||||
const uploadedFile = req.files.files[0];
|
||||
const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
|
||||
@@ -197,6 +272,9 @@ uploadsController.uploadFile = async function (req, res, next) {
|
||||
return next(new Error('[[error:invalid-json]]'));
|
||||
}
|
||||
|
||||
if (!await file.exists(path.join(nconf.get('upload_path'), params.folder))) {
|
||||
return next(new Error('[[error:invalid-path]]'));
|
||||
}
|
||||
try {
|
||||
const data = await file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path);
|
||||
res.json([{ url: data.url }]);
|
||||
|
||||
@@ -153,7 +153,11 @@ Emailer.setupFallbackTransport = (config) => {
|
||||
} else {
|
||||
smtpOptions.service = String(config['email:smtpTransport:service']);
|
||||
}
|
||||
|
||||
if (config['email:smtpTransport:allow-self-signed']) {
|
||||
smtpOptions.tls = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
}
|
||||
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
|
||||
Emailer.fallbackTransport = Emailer.transports.smtp;
|
||||
} else {
|
||||
|
||||
@@ -18,9 +18,6 @@ module.exports = function (Messaging) {
|
||||
uids = [uids];
|
||||
}
|
||||
uids = uids.filter(uid => parseInt(uid, 10) > 0);
|
||||
if (!uids.length) {
|
||||
return;
|
||||
}
|
||||
uids.forEach((uid) => {
|
||||
io.in(`uid_${uid}`).emit('event:unread.updateChatCount', data);
|
||||
});
|
||||
|
||||
@@ -132,6 +132,7 @@ Configs.setMultiple = async function (data) {
|
||||
await processConfig(data);
|
||||
data = serialize(data);
|
||||
await db.setObject('config', data);
|
||||
await updateNavItems(data);
|
||||
updateConfig(deserialize(data));
|
||||
};
|
||||
|
||||
@@ -228,6 +229,13 @@ async function getLogoSize(data) {
|
||||
data['brand:emailLogo:width'] = size.width;
|
||||
}
|
||||
|
||||
async function updateNavItems(data) {
|
||||
if (data.hasOwnProperty('activitypubEnabled')) {
|
||||
const navAdmin = require('../navigation/admin');
|
||||
await navAdmin.update('/world', { enabled: data.activitypubEnabled ? 'on' : '' });
|
||||
}
|
||||
}
|
||||
|
||||
function updateConfig(config) {
|
||||
updateLocalConfig(config);
|
||||
pubsub.publish('config:update', config);
|
||||
|
||||
@@ -85,6 +85,19 @@ admin.get = async function () {
|
||||
return cache.map(item => ({ ...item }));
|
||||
};
|
||||
|
||||
admin.update = async function (route, data) {
|
||||
const ids = await db.getSortedSetRange('navigation:enabled', 0, -1);
|
||||
const navItems = await db.getObjects(ids.map(id => `navigation:enabled:${id}`));
|
||||
const matchedRoutes = navItems.filter(item => item && item.route === route);
|
||||
if (matchedRoutes.length) {
|
||||
await db.setObjectBulk(
|
||||
matchedRoutes.map(item => [`navigation:enabled:${item.order}`, data])
|
||||
);
|
||||
cache = null;
|
||||
pubsub.publish('admin:navigation:save');
|
||||
}
|
||||
};
|
||||
|
||||
async function getAvailable() {
|
||||
const core = require('../../install/data/navigation.json').map((item) => {
|
||||
item.core = true;
|
||||
|
||||
@@ -275,8 +275,12 @@ async function fireActionHook(hook, hookList, params) {
|
||||
winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
await hookObj.method(params);
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
await hookObj.method(params);
|
||||
} catch (err) {
|
||||
winston.error(`[plugins] Error in hook ${hookObj.id}@${hookObj.hook} \n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ Plugins.toggleActive = async function (socket, plugin_id) {
|
||||
|
||||
Plugins.toggleInstall = async function (socket, data) {
|
||||
const isInstalled = await plugins.isInstalled(data.id);
|
||||
if (nconf.get('acpPluginInstallDisabled') && !isInstalled) {
|
||||
const isStarterPlan = nconf.get('saas_plan') === 'starter';
|
||||
if ((isStarterPlan || nconf.get('acpPluginInstallDisabled')) && !isInstalled) {
|
||||
throw new Error('[[error:plugin-installation-via-acp-disabled]]');
|
||||
}
|
||||
postsCache.reset();
|
||||
|
||||
@@ -6,20 +6,23 @@ const user = require('../user');
|
||||
const meta = require('../meta');
|
||||
const topics = require('../topics');
|
||||
const privileges = require('../privileges');
|
||||
const messaging = require('../messaging');
|
||||
|
||||
const SocketMeta = module.exports;
|
||||
SocketMeta.rooms = {};
|
||||
|
||||
SocketMeta.reconnected = function (socket, data, callback) {
|
||||
callback = callback || function () {};
|
||||
if (socket.uid) {
|
||||
topics.pushUnreadCount(socket.uid);
|
||||
user.notifications.pushCount(socket.uid);
|
||||
SocketMeta.reconnected = async function (socket) {
|
||||
if (socket.uid > 0) {
|
||||
await Promise.all([
|
||||
topics.pushUnreadCount(socket.uid),
|
||||
user.notifications.pushCount(socket.uid),
|
||||
messaging.pushUnreadCount(socket.uid),
|
||||
]);
|
||||
}
|
||||
callback(null, {
|
||||
return {
|
||||
'cache-buster': meta.config['cache-buster'],
|
||||
hostname: os.hostname(),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/* Rooms */
|
||||
|
||||
@@ -38,25 +38,29 @@ module.exports = function (Topics) {
|
||||
cid = await posts.getCidByPid(mainPid);
|
||||
}
|
||||
|
||||
const [postData, isAdminOrMod] = await Promise.all([
|
||||
const [mainPost, isAdminOrMod] = await Promise.all([
|
||||
posts.getPostData(mainPid),
|
||||
privileges.categories.isAdminOrMod(cid, uid),
|
||||
]);
|
||||
let lastPost = mainPost;
|
||||
if (pids.length > 1) {
|
||||
lastPost = await posts.getPostData(pids[pids.length - 1]);
|
||||
}
|
||||
|
||||
if (!isAdminOrMod) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const scheduled = postData.timestamp > Date.now();
|
||||
const now = Date.now();
|
||||
const scheduled = mainPost.timestamp > now;
|
||||
const params = {
|
||||
uid: postData.uid,
|
||||
uid: mainPost.uid,
|
||||
title: title,
|
||||
cid: cid,
|
||||
timestamp: scheduled && postData.timestamp,
|
||||
timestamp: mainPost.timestamp,
|
||||
};
|
||||
const result = await plugins.hooks.fire('filter:topic.fork', {
|
||||
params: params,
|
||||
tid: postData.tid,
|
||||
tid: mainPost.tid,
|
||||
});
|
||||
|
||||
const tid = await Topics.create(result.params);
|
||||
@@ -71,21 +75,21 @@ module.exports = function (Topics) {
|
||||
await Topics.movePostToTopic(uid, pid, tid, scheduled);
|
||||
}
|
||||
|
||||
await Topics.updateLastPostTime(tid, scheduled ? (postData.timestamp + 1) : Date.now());
|
||||
await Topics.updateLastPostTime(tid, scheduled ? (mainPost.timestamp + 1) : lastPost.timestamp);
|
||||
|
||||
await Promise.all([
|
||||
Topics.setTopicFields(tid, {
|
||||
upvotes: postData.upvotes,
|
||||
downvotes: postData.downvotes,
|
||||
upvotes: mainPost.upvotes,
|
||||
downvotes: mainPost.downvotes,
|
||||
forkedFromTid: fromTid,
|
||||
forkerUid: uid,
|
||||
forkTimestamp: Date.now(),
|
||||
forkTimestamp: now,
|
||||
}),
|
||||
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], postData.votes, tid),
|
||||
db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], mainPost.votes, tid),
|
||||
Topics.events.log(fromTid, { type: 'fork', uid, href: `/topic/${tid}` }),
|
||||
]);
|
||||
|
||||
plugins.hooks.fire('action:topic.fork', { tid: tid, fromTid: fromTid, uid: uid });
|
||||
plugins.hooks.fire('action:topic.fork', { tid, fromTid, uid });
|
||||
|
||||
return await Topics.getTopicData(tid);
|
||||
};
|
||||
|
||||
@@ -234,7 +234,9 @@ module.exports = function (User) {
|
||||
}
|
||||
|
||||
async function deleteImages(uid) {
|
||||
const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`);
|
||||
await rimraf(folder);
|
||||
if (utils.isNumber(uid)) {
|
||||
const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`);
|
||||
await rimraf(folder);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,23 +124,22 @@ UserEmail.sendValidationEmail = async function (uid, options) {
|
||||
};
|
||||
}
|
||||
|
||||
const confirm_code = utils.generateUUID();
|
||||
const confirm_link = `${nconf.get('url')}/confirm/${confirm_code}`;
|
||||
|
||||
const { emailConfirmInterval, emailConfirmExpiry } = meta.config;
|
||||
|
||||
// If no email passed in (default), retrieve email from uid
|
||||
if (!options.email || !options.email.length) {
|
||||
options.email = await user.getUserField(uid, 'email');
|
||||
}
|
||||
if (!options.email) {
|
||||
winston.warn(`[user/email] No email found for uid ${uid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { emailConfirmInterval, emailConfirmExpiry } = meta.config;
|
||||
if (!options.force && !await UserEmail.canSendValidation(uid, options.email)) {
|
||||
throw new Error(`[[error:confirm-email-already-sent, ${emailConfirmInterval}]]`);
|
||||
}
|
||||
|
||||
const confirm_code = utils.generateUUID();
|
||||
const confirm_link = `${nconf.get('url')}/confirm/${confirm_code}`;
|
||||
const username = await user.getUserField(uid, 'username');
|
||||
const data = await plugins.hooks.fire('filter:user.verify', {
|
||||
uid,
|
||||
|
||||
@@ -14,85 +14,85 @@
|
||||
<a class="btn btn-primary" href="{config.relative_path}/admin/settings/activitypub">[[admin/manage/categories:federation.disabled-cta]]</a>
|
||||
</div>
|
||||
{{{ else }}}
|
||||
<div class="acp-page-container">
|
||||
<div class="row settings m-0">
|
||||
<div class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
||||
<div id="site-settings" class="mb-4">
|
||||
<form role="form">
|
||||
<h5 class="fw-bold settings-header">[[admin/manage/categories:federation.syncing-header]]</h5>
|
||||
<p>[[admin/manage/categories:federation.syncing-intro]]</p>
|
||||
<p class="form-text">[[admin/manage/categories:federation.syncing-caveat]]</p>
|
||||
<div class="row settings m-0">
|
||||
<div class="col-12 px-0 mb-4" tabindex="0">
|
||||
<div id="site-settings" class="mb-4">
|
||||
<form role="form">
|
||||
<h5 class="fw-bold settings-header">[[admin/manage/categories:federation.syncing-header]]</h5>
|
||||
<p>[[admin/manage/categories:federation.syncing-intro]]</p>
|
||||
<p class="form-text">[[admin/manage/categories:federation.syncing-caveat]]</p>
|
||||
|
||||
{{{ if !following.length }}}
|
||||
<div class="alert alert-info">[[admin/manage/categories:federation.syncing-none]]</div>
|
||||
{{{ else }}}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[[admin/manage/categories:federation.syncing-actorUri]]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each following }}}
|
||||
<tr>
|
||||
<td>
|
||||
<pre class="mb-0 mt-1">{./id}</pre>
|
||||
{{{ if !./approved }}}
|
||||
<span class="form-text text-warning">Pending</span>
|
||||
{{{ end }}}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" data-action="unfollow" data-actor="{./id}" class="btn btn-sm btn-danger">[[admin/manage/categories:federation.syncing-unfollow]]</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ end }}}
|
||||
{{{ if !following.length }}}
|
||||
<div class="alert alert-info">[[admin/manage/categories:federation.syncing-none]]</div>
|
||||
{{{ else }}}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[[admin/manage/categories:federation.syncing-actorUri]]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each following }}}
|
||||
<tr>
|
||||
<td>
|
||||
<pre class="mb-0 mt-1">{./id}</pre>
|
||||
{{{ if !./approved }}}
|
||||
<span class="form-text text-warning">Pending</span>
|
||||
{{{ end }}}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" data-action="unfollow" data-actor="{./id}" class="btn btn-sm btn-danger">[[admin/manage/categories:federation.syncing-unfollow]]</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ end }}}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="syncing-add">[[admin/manage/categories:federation.syncing-add]]</label>
|
||||
<div class="input-group">
|
||||
<input id="syncing-add" type="url" class="form-control" />
|
||||
<button data-action="follow" type="button" class="btn btn-primary">[[admin/manage/categories:federation.syncing-follow]]</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="syncing-add">[[admin/manage/categories:federation.syncing-add]]</label>
|
||||
<div class="input-group">
|
||||
<input id="syncing-add" type="url" class="form-control" />
|
||||
<button data-action="follow" type="button" class="btn btn-primary">[[admin/manage/categories:federation.syncing-follow]]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<div class="mb-3">
|
||||
<p>[[admin/manage/categories:federation.followers]]</p>
|
||||
<table class="table small">
|
||||
<tr>
|
||||
<th>[[admin/manage/categories:federation.followers-handle]]</th>
|
||||
<th>[[admin/manage/categories:federation.followers-id]]</th>
|
||||
</tr>
|
||||
{{{ if !followers.length}}}
|
||||
<tr>
|
||||
<td class="text-center border-0" colspan="2">
|
||||
<em>[[admin/manage/categories:federation.followers-none]]</em>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
{{{ each followers }}}
|
||||
<tr data-uid="{./uid}">
|
||||
<td>
|
||||
{buildAvatar(followers, "24px", true)}
|
||||
{./userslug}
|
||||
</td>
|
||||
<td>
|
||||
<code>{./uid}</code>
|
||||
<button type="button" class="btn btn-link" data-action="autofill">
|
||||
<i class="fa fa-exchange-alt"></i>
|
||||
<div class="mb-3">
|
||||
<p>[[admin/manage/categories:federation.followers]]</p>
|
||||
<table class="table small">
|
||||
<tr>
|
||||
<th>[[admin/manage/categories:federation.followers-handle]]</th>
|
||||
<th>[[admin/manage/categories:federation.followers-id]]</th>
|
||||
</tr>
|
||||
{{{ if !followers.length}}}
|
||||
<tr>
|
||||
<td class="text-center border-0" colspan="2">
|
||||
<em>[[admin/manage/categories:federation.followers-none]]</em>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
{{{ each followers }}}
|
||||
<tr data-uid="{./uid}">
|
||||
<td class="w-100 text-truncate" style="max-width: 1px;">
|
||||
{buildAvatar(followers, "24px", true)}
|
||||
{./userslug}
|
||||
</td>
|
||||
<td class="w-0">
|
||||
<div class="d-flex gap-2 flex-nowrap align-items-center">
|
||||
<button type="button" class="btn btn-ghost btn-sm border" data-action="autofill" title="[[admin/manage/categories:federation.followers-autofill]]">
|
||||
<i class="fa fa-exchange-alt text-primary"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<code>{./uid}</code>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,11 @@
|
||||
<label for="email:smtpTransport:pool" class="form-check-label">[[admin/settings/email:smtp-transport.pool]]</label>
|
||||
<p class="form-text">[[admin/settings/email:smtp-transport.pool-help]]</p>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="email:smtpTransport:allow-self-signed" data-field="email:smtpTransport:allow-self-signed" name="email:smtpTransport:allow-self-signed" />
|
||||
<label for="email:smtpTransport:allow-self-signed" class="form-check-label">[[admin/settings/email:smtp-transport.allow-self-signed]]</label>
|
||||
<p class="form-text">[[admin/settings/email:smtp-transport.allow-self-signed-help]]</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email:smtpTransport:service">[[admin/settings/email:smtp-transport.service]]</label>
|
||||
<select class="form-select" id="email:smtpTransport:service" data-field="email:smtpTransport:service">
|
||||
|
||||
24
test/navigation.js
Normal file
24
test/navigation.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const db = require('./mocks/databasemock');
|
||||
const meta = require('../src/meta');
|
||||
const navAdmin = require('../src/navigation/admin');
|
||||
|
||||
describe('Navigation', () => {
|
||||
before(async () => {
|
||||
const data = require('../install/data/navigation.json');
|
||||
await navAdmin.save(data);
|
||||
});
|
||||
|
||||
it('should toggle /world route when ap is toggled', async () => {
|
||||
let nav = await navAdmin.get();
|
||||
let world = nav.find(item => item.route === '/world');
|
||||
assert.strictEqual(!!world.enabled, true);
|
||||
await meta.configs.setMultiple({ activitypubEnabled: 0 });
|
||||
nav = await navAdmin.get();
|
||||
world = nav.find(item => item.route === '/world');
|
||||
assert.strictEqual(!!world.enabled, false);
|
||||
});
|
||||
});
|
||||
@@ -269,12 +269,9 @@ describe('socket.io', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should push unread notifications on reconnect', (done) => {
|
||||
it('should push unread notifications/chats on reconnect', async () => {
|
||||
const socketMeta = require('../src/socket.io/meta');
|
||||
socketMeta.reconnected({ uid: 1 }, {}, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
await socketMeta.reconnected({ uid: 1 }, {});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -400,6 +400,17 @@ describe('Upload Controllers', () => {
|
||||
assert.strictEqual(body.error, '[[error:invalid-path]]');
|
||||
});
|
||||
|
||||
it('should fail to upload regular file if directory does not exist', async () => {
|
||||
const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), {
|
||||
params: JSON.stringify({
|
||||
folder: 'does-not-exist',
|
||||
}),
|
||||
}, jar, csrf_token);
|
||||
|
||||
assert.equal(response.statusCode, 500);
|
||||
assert.strictEqual(body.error, '[[error:invalid-path]]');
|
||||
});
|
||||
|
||||
describe('ACP uploads screen', () => {
|
||||
it('should create a folder', async () => {
|
||||
const { response } = await helpers.createFolder('', 'myfolder', jar, csrf_token);
|
||||
|
||||
Reference in New Issue
Block a user