Merge commit 'bffb830d8754344f7bf8cc819e7985cd3318d49b' into v1.14.x

This commit is contained in:
Misty (Bot)
2020-06-24 20:37:03 +00:00
15 changed files with 177 additions and 54 deletions

View File

@@ -80,7 +80,7 @@
"@nodebb/mubsub": "^1.6.0",
"@nodebb/socket.io-adapter-mongo": "3.0.0",
"nconf": "^0.10.0",
"nodebb-plugin-composer-default": "6.3.43",
"nodebb-plugin-composer-default": "6.3.44",
"nodebb-plugin-dbsearch": "4.0.7",
"nodebb-plugin-emoji": "^3.3.0",
"nodebb-plugin-emoji-android": "2.0.0",
@@ -90,9 +90,9 @@
"nodebb-plugin-spam-be-gone": "0.7.2",
"nodebb-rewards-essentials": "0.1.3",
"nodebb-theme-lavender": "5.0.11",
"nodebb-theme-persona": "10.1.54",
"nodebb-theme-persona": "10.1.55",
"nodebb-theme-slick": "1.2.29",
"nodebb-theme-vanilla": "11.1.29",
"nodebb-theme-vanilla": "11.1.30",
"nodebb-widget-essentials": "4.1.0",
"nodemailer": "^6.4.6",
"passport": "^0.4.1",
@@ -126,7 +126,7 @@
"toobusy-js": "^0.5.1",
"uglify-es": "^3.3.9",
"validator": "13.1.1",
"winston": "3.3.2",
"winston": "3.3.3",
"xml": "^1.0.1",
"xregexp": "^4.3.0",
"zxcvbn": "^4.4.2"
@@ -136,7 +136,7 @@
"@commitlint/cli": "9.0.1",
"@commitlint/config-angular": "9.0.1",
"coveralls": "3.1.0",
"eslint": "7.2.0",
"eslint": "7.3.1",
"eslint-config-airbnb-base": "14.1.0",
"eslint-plugin-import": "2.21.1",
"grunt": "1.1.0",

View File

@@ -11,6 +11,7 @@
"num-recent-replies": "# of Recent Replies",
"ext-link": "External Link",
"is-section": "Treat this category as a section",
"tag-whitelist": "Tag Whitelist",
"upload-image": "Upload Image",
"delete-image": "Remove",
"category-image": "Category Image",

View File

@@ -62,4 +62,10 @@ CategoryObject:
description: The number of posts in the category
totalTopicCount:
type: number
description: The number of topics in the category
description: The number of topics in the category
minTags:
type: number
description: Minimum tags per topic in this category
maxTags:
type: number
description: Maximum tags per topic in this category

View File

@@ -3130,6 +3130,10 @@ paths:
type: number
totalTopicCount:
type: number
minTags:
type: number
maxTags:
type: number
/api/categories:
get:
tags:
@@ -3655,6 +3659,10 @@ paths:
type: array
items:
type: string
minTags:
type: number
maxTags:
type: number
thread_tools:
type: array
items:

View File

@@ -124,49 +124,55 @@ define('admin/manage/category', [
});
$('.copy-settings').on('click', function () {
Benchpress.parse('admin/partials/categories/copy-settings', {
categories: ajaxify.data.allCategories,
}, function (html) {
var selectedCid;
var modal = bootbox.dialog({
title: '[[modules:composer.select_category]]',
message: html,
buttons: {
save: {
label: '[[modules:bootbox.confirm]]',
className: 'btn-primary',
callback: function () {
if (!selectedCid) {
return;
}
socket.emit('categories.getSelectCategories', {}, function (err, allCategories) {
if (err) {
return app.alertError(err.message);
}
socket.emit('admin.categories.copySettingsFrom', {
fromCid: selectedCid,
toCid: ajaxify.data.category.cid,
copyParent: modal.find('#copyParent').prop('checked'),
}, function (err) {
if (err) {
return app.alertError(err.message);
Benchpress.parse('admin/partials/categories/copy-settings', {
categories: allCategories,
}, function (html) {
var selectedCid;
var modal = bootbox.dialog({
title: '[[modules:composer.select_category]]',
message: html,
buttons: {
save: {
label: '[[modules:bootbox.confirm]]',
className: 'btn-primary',
callback: function () {
if (!selectedCid || parseInt(selectedCid, 10) === parseInt(ajaxify.data.category.cid, 10)) {
return;
}
modal.modal('hide');
app.alertSuccess('[[admin/manage/categories:alert.copy-success]]');
ajaxify.refresh();
});
return false;
socket.emit('admin.categories.copySettingsFrom', {
fromCid: selectedCid,
toCid: ajaxify.data.category.cid,
copyParent: modal.find('#copyParent').prop('checked'),
}, function (err) {
if (err) {
return app.alertError(err.message);
}
modal.modal('hide');
app.alertSuccess('[[admin/manage/categories:alert.copy-success]]');
ajaxify.refresh();
});
return false;
},
},
},
},
});
modal.find('.modal-footer button').prop('disabled', true);
categorySelector.init(modal.find('[component="category-selector"]'), function (selectedCategory) {
selectedCid = selectedCategory && selectedCategory.cid;
if (selectedCid) {
modal.find('.modal-footer button').prop('disabled', false);
}
});
modal.find('.modal-footer button').prop('disabled', true);
categorySelector.init(modal.find('[component="category-selector"]'), function (selectedCategory) {
selectedCid = selectedCategory && selectedCategory.cid;
if (selectedCid) {
modal.find('.modal-footer button').prop('disabled', false);
}
});
});
return false;
});
return false;
});
$('.upload-button').on('click', function () {

View File

@@ -149,7 +149,7 @@ define('forum/topic/events', [
});
if (data.topic.tags && tagsUpdated(data.topic.tags)) {
Benchpress.parse('partials/post_bar', 'tags', { tags: data.topic.tags }, function (html) {
Benchpress.parse('partials/topic/tags', { tags: data.topic.tags }, function (html) {
var tags = $('.tags');
tags.fadeOut(250, function () {

View File

@@ -143,6 +143,8 @@ module.exports = function (Categories) {
destination.class = source.class;
destination.image = source.image;
destination.imageClass = source.imageClass;
destination.minTags = source.minTags;
destination.maxTags = source.maxTags;
if (copyParent) {
destination.parentCid = source.parentCid || 0;

View File

@@ -1,12 +1,14 @@
'use strict';
var validator = require('validator');
const validator = require('validator');
var db = require('../database');
const db = require('../database');
const meta = require('../meta');
const intFields = [
'cid', 'parentCid', 'disabled', 'isSection', 'order',
'topic_count', 'post_count', 'numRecentReplies',
'minTags', 'maxTags',
];
module.exports = function (Categories) {
@@ -54,11 +56,24 @@ module.exports = function (Categories) {
};
};
function defaultMinMaxTags(category, fields, fieldName, defaultField) {
if (!fields.length || fields.includes(fieldName)) {
const useDefault = !category.hasOwnProperty(fieldName) ||
category[fieldName] === null ||
category[fieldName] === '' ||
!parseInt(category[fieldName], 10);
category[fieldName] = useDefault ? meta.config[defaultField] : category[fieldName];
}
}
function modifyCategory(category, fields) {
if (!category) {
return;
}
defaultMinMaxTags(category, fields, 'minTags', 'minimumTagsPerTopic');
defaultMinMaxTags(category, fields, 'maxTags', 'maximumTagsPerTopic');
db.parseIntFields(category, intFields, fields);
if (category.hasOwnProperty('name')) {

View File

@@ -117,6 +117,8 @@ module.exports = function (Posts) {
throw new Error('[[error:no-privileges]]');
}
}
await topics.validateTags(data.tags, topicData.cid);
const results = await plugins.fireHook('filter:topic.edit', { req: data.req, topic: newTopicData, data: data });
await db.setObject('topic:' + tid, results.topic);
await topics.updateTopicTags(tid, data.tags);

View File

@@ -25,10 +25,6 @@ module.exports = function (SocketPosts) {
throw new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]');
} else if (data.title && data.title.length > meta.config.maximumTitleLength) {
throw new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]');
} else if (data.tags && data.tags.length < meta.config.minimumTagsPerTopic) {
throw new Error('[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]');
} else if (data.tags && data.tags.length > meta.config.maximumTagsPerTopic) {
throw new Error('[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]');
} else if (meta.config.minimumPostLength !== 0 && contentLen < meta.config.minimumPostLength) {
throw new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]');
} else if (contentLen > meta.config.maximumPostLength) {

View File

@@ -68,7 +68,7 @@ module.exports = function (Topics) {
data.content = utils.rtrim(data.content);
}
check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long');
check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags');
await Topics.validateTags(data.tags, data.cid);
check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long');
const [categoryExists, canCreate, canTag] = await Promise.all([

View File

@@ -160,6 +160,8 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev
topicData.posts = posts;
topicData.category = category;
topicData.tagWhitelist = tagWhitelist[0];
topicData.minTags = category.minTags;
topicData.maxTags = category.maxTags;
topicData.thread_tools = threadTools.tools;
topicData.isFollowing = followData[0].following;
topicData.isNotFollowing = !followData[0].following && !followData[0].ignoring;

View File

@@ -19,7 +19,7 @@ module.exports = function (Topics) {
return;
}
const result = await plugins.fireHook('filter:tags.filter', { tags: tags, tid: tid });
tags = _.uniq(result.tags).slice(0, meta.config.maximumTagsPerTopic || 5)
tags = _.uniq(result.tags)
.map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength))
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
@@ -32,6 +32,19 @@ module.exports = function (Topics) {
await Promise.all(tags.map(tag => updateTagCount(tag)));
};
Topics.validateTags = async function (tags, cid) {
if (!Array.isArray(tags)) {
throw new Error('[[error:invalid-data]]');
}
tags = _.uniq(tags);
const categoryData = await categories.getCategoryFields(cid, ['minTags', 'maxTags']);
if (tags.length < parseInt(categoryData.minTags, 10)) {
throw new Error('[[error:not-enough-tags, ' + categoryData.minTags + ']]');
} else if (tags.length > parseInt(categoryData.maxTags, 10)) {
throw new Error('[[error:too-many-tags, ' + categoryData.maxTags + ']]');
}
};
async function filterCategoryTags(tags, tid) {
const cid = await Topics.getTopicField(tid, 'cid');
const tagWhitelist = await categories.getTagWhitelist([cid]);

View File

@@ -95,9 +95,29 @@
</div>
</div>
</fieldset>
<fieldset>
<label for="tag-whitelist">Tag Whitelist</label><br />
<input id="tag-whitelist" type="text" class="form-control" placeholder="Enter category tags here" data-name="tagWhitelist" value="" />
<fieldset class="row">
<div class="col-sm-6 col-xs-12">
<div class="form-group">
<label for="cid-min-tags">
[[admin/settings/tags:min-per-topic]]
</label>
<input id="cid-min-tags" type="text" class="form-control" data-name="minTags" value="{category.minTags}" />
</div>
</div>
<div class="col-sm-6 col-xs-12">
<div class="form-group">
<label for="cid-max-tags">
[[admin/settings/tags:max-per-topic]]
</label>
<input id="cid-max-tags" type="text" class="form-control" data-name="maxTags" value="{category.maxTags}" />
</div>
</div>
</fieldset>
<fieldset class="row">
<div class="col-lg-12">
<label for="tag-whitelist">[[admin/manage/categories:tag-whitelist]]</label><br />
<input id="tag-whitelist" type="text" class="form-control" data-name="tagWhitelist" value="" />
</div>
</fieldset>
</div>
</div>

View File

@@ -1782,6 +1782,58 @@ describe('Topic\'s', function () {
tags = await topics.getTopicTags(tid);
assert.deepStrictEqual(tags, ['tag2', 'tag4', 'tag6']);
});
it('should respect minTags', async () => {
const oldValue = meta.config.minimumTagsPerTopic;
meta.config.minimumTagsPerTopic = 2;
let err;
try {
await topics.post({ uid: adminUid, tags: ['tag4'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId });
} catch (_err) {
err = _err;
}
assert.equal(err.message, '[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]');
meta.config.minimumTagsPerTopic = oldValue;
});
it('should respect maxTags', async () => {
const oldValue = meta.config.maximumTagsPerTopic;
meta.config.maximumTagsPerTopic = 2;
let err;
try {
await topics.post({ uid: adminUid, tags: ['tag1', 'tag2', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId });
} catch (_err) {
err = _err;
}
assert.equal(err.message, '[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]');
meta.config.maximumTagsPerTopic = oldValue;
});
it('should respect minTags per category', async () => {
const minTags = 2;
await categories.setCategoryField(topic.categoryId, 'minTags', minTags);
let err;
try {
await topics.post({ uid: adminUid, tags: ['tag4'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId });
} catch (_err) {
err = _err;
}
assert.equal(err.message, '[[error:not-enough-tags, ' + minTags + ']]');
await db.deleteObjectField('category:' + topic.categoryId, 'minTags');
});
it('should respect maxTags per category', async () => {
const maxTags = 2;
await categories.setCategoryField(topic.categoryId, 'maxTags', maxTags);
let err;
try {
await topics.post({ uid: adminUid, tags: ['tag1', 'tag2', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId });
} catch (_err) {
err = _err;
}
assert.equal(err.message, '[[error:too-many-tags, ' + maxTags + ']]');
await db.deleteObjectField('category:' + topic.categoryId, 'maxTags');
});
});
describe('follow/unfollow', function () {