mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-26 00:21:16 +01:00
Merge remote-tracking branch 'origin/master' into singleton-flags
This commit is contained in:
@@ -48,7 +48,7 @@ Our minimalist "Persona" theme gets you going right away, no coding experience r
|
||||
|
||||
NodeBB requires the following software to be installed:
|
||||
|
||||
* A version of Node.js at least 8 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions))
|
||||
* A version of Node.js at least 10 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions))
|
||||
* Redis, version 2.8.9 or greater **or** MongoDB, version 2.6 or greater
|
||||
* nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "nodebb",
|
||||
"license": "GPL-3.0",
|
||||
"description": "NodeBB Forum",
|
||||
"version": "1.14.3-beta.0",
|
||||
"version": "1.14.3-beta.4",
|
||||
"homepage": "http://www.nodebb.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.4.9",
|
||||
"archiver": "^4.0.0",
|
||||
"archiver": "^5.0.0",
|
||||
"async": "^3.2.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"bcryptjs": "2.4.3",
|
||||
@@ -42,14 +42,14 @@
|
||||
"cli-graph": "^3.2.2",
|
||||
"clipboard": "^2.0.6",
|
||||
"colors": "^1.4.0",
|
||||
"commander": "^5.0.0",
|
||||
"commander": "^6.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-flash": "^0.1.1",
|
||||
"connect-mongo": "3.2.0",
|
||||
"connect-multiparty": "^2.2.0",
|
||||
"connect-pg-simple": "^6.1.0",
|
||||
"connect-redis": "4.0.4",
|
||||
"connect-redis": "5.0.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cron": "^1.8.2",
|
||||
"cropperjs": "^1.5.6",
|
||||
@@ -70,7 +70,7 @@
|
||||
"less": "^3.11.1",
|
||||
"lodash": "^4.17.15",
|
||||
"logrotate-stream": "^0.2.6",
|
||||
"lru-cache": "5.1.1",
|
||||
"lru-cache": "6.0.0",
|
||||
"material-design-lite": "^1.3.0",
|
||||
"mime": "^2.4.4",
|
||||
"mkdirp": "^1.0.4",
|
||||
@@ -80,17 +80,17 @@
|
||||
"@nodebb/mubsub": "^1.6.0",
|
||||
"@nodebb/socket.io-adapter-mongo": "3.0.1",
|
||||
"nconf": "^0.10.0",
|
||||
"nodebb-plugin-composer-default": "6.3.51",
|
||||
"nodebb-plugin-dbsearch": "4.1.1",
|
||||
"nodebb-plugin-composer-default": "6.3.52",
|
||||
"nodebb-plugin-dbsearch": "4.1.2",
|
||||
"nodebb-plugin-emoji": "^3.3.0",
|
||||
"nodebb-plugin-emoji-android": "2.0.0",
|
||||
"nodebb-plugin-markdown": "8.11.2",
|
||||
"nodebb-plugin-mentions": "2.9.1",
|
||||
"nodebb-plugin-mentions": "2.9.3",
|
||||
"nodebb-plugin-soundpack-default": "1.0.0",
|
||||
"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.67",
|
||||
"nodebb-theme-persona": "10.1.68",
|
||||
"nodebb-theme-slick": "1.2.29",
|
||||
"nodebb-theme-vanilla": "11.1.35",
|
||||
"nodebb-widget-essentials": "4.1.1",
|
||||
@@ -132,28 +132,28 @@
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "9.0.1",
|
||||
"@apidevtools/swagger-parser": "10.0.1",
|
||||
"@commitlint/cli": "9.1.1",
|
||||
"@commitlint/config-angular": "9.1.1",
|
||||
"coveralls": "3.1.0",
|
||||
"eslint": "7.3.1",
|
||||
"eslint": "7.5.0",
|
||||
"eslint-config-airbnb-base": "14.1.0",
|
||||
"eslint-plugin-import": "2.21.1",
|
||||
"grunt": "1.1.0",
|
||||
"grunt": "1.2.1",
|
||||
"grunt-contrib-watch": "1.1.0",
|
||||
"husky": "4.2.5",
|
||||
"jsdom": "16.2.2",
|
||||
"jsdom": "16.3.0",
|
||||
"lint-staged": "10.2.11",
|
||||
"mocha": "8.0.1",
|
||||
"mocha-lcov-reporter": "1.3.0",
|
||||
"nyc": "15.0.1",
|
||||
"nyc": "15.1.0",
|
||||
"smtp-server": "3.7.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NodeBB/NodeBB/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=10"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/*
|
||||
The following stylesheet is only included on pages that can execute javascript
|
||||
*/
|
||||
@@ -3,6 +3,7 @@
|
||||
"ip": "IP <strong>%1</strong>",
|
||||
"nodes-responded": "%1 nodes responded within %2ms!",
|
||||
"host": "host",
|
||||
"primary": "primary / run jobs",
|
||||
"pid": "pid",
|
||||
"nodejs": "nodejs",
|
||||
"online": "online",
|
||||
|
||||
@@ -79,5 +79,6 @@
|
||||
"alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!",
|
||||
"collapse-all": "Collapse All",
|
||||
"expand-all": "Expand All",
|
||||
"disable-on-create": "Disable on create"
|
||||
"disable-on-create": "Disable on create",
|
||||
"no-matches": "No matches"
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"admin": "Admin",
|
||||
"group-privileges": "Group Privileges",
|
||||
"user-privileges": "User Privileges",
|
||||
"edit-privileges": "Edit Privileges",
|
||||
"chat": "Chat",
|
||||
"upload-images": "Upload Images",
|
||||
"upload-files": "Upload Files",
|
||||
@@ -33,7 +34,6 @@
|
||||
"delete-topics": "Delete Topics",
|
||||
"purge": "Purge",
|
||||
"moderate": "Moderate",
|
||||
|
||||
"admin-dashboard": "Dashboard",
|
||||
"admin-categories": "Categories",
|
||||
"admin-privileges": "Privileges",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"chat.chatting_with": "Chat with",
|
||||
"chat.placeholder": "Type chat message here, press enter to send",
|
||||
"chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.",
|
||||
"chat.send": "Send",
|
||||
"chat.no_active": "You have no active chats.",
|
||||
"chat.user_typing": "%1 is typing ...",
|
||||
|
||||
@@ -27,3 +27,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-admin-groups {
|
||||
[component="category/list"] li {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,18 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.page-admin-privileges {
|
||||
@keyframes fadeOut {
|
||||
0% {background-color: @brand-primary;}
|
||||
100% {background-color: white;}
|
||||
}
|
||||
|
||||
[data-group-name].selected {
|
||||
animation-name: fadeOut;
|
||||
animation-duration: 5s;
|
||||
animation-fill-mode: both;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
@@ -1317,6 +1317,36 @@ paths:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
level:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
required:
|
||||
- cid
|
||||
- name
|
||||
- icon
|
||||
allowPrivateGroups:
|
||||
type: number
|
||||
maximumGroupNameLength:
|
||||
@@ -2195,11 +2225,24 @@ paths:
|
||||
load:
|
||||
type: string
|
||||
description: CPU load
|
||||
nodebb:
|
||||
type: object
|
||||
properties:
|
||||
isPrimary:
|
||||
type: boolean
|
||||
isCluster:
|
||||
type: boolean
|
||||
runJobs:
|
||||
type: boolean
|
||||
jobsDisabled:
|
||||
type: boolean
|
||||
git:
|
||||
type: object
|
||||
properties:
|
||||
hash:
|
||||
type: string
|
||||
hashShort:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
stats:
|
||||
@@ -5032,6 +5075,35 @@ paths:
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
allCategories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
bgColor:
|
||||
type: string
|
||||
cid:
|
||||
type: number
|
||||
color:
|
||||
type: string
|
||||
disabled:
|
||||
type: number
|
||||
disabledClass:
|
||||
type: boolean
|
||||
icon:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
slug:
|
||||
type: string
|
||||
posts:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -11,9 +11,9 @@ define('admin/manage/categories', [
|
||||
var sortables;
|
||||
|
||||
Categories.init = function () {
|
||||
socket.emit('admin.categories.getAll', function (error, payload) {
|
||||
if (error) {
|
||||
return app.alertError(error.message);
|
||||
socket.emit('admin.categories.getAll', function (err, payload) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
Categories.render(payload);
|
||||
@@ -63,8 +63,55 @@ define('admin/manage/categories', [
|
||||
el.find('i').toggleClass('fa-minus', expand).toggleClass('fa-plus', !expand);
|
||||
el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand);
|
||||
}
|
||||
|
||||
$('#category-search').on('keyup', function () {
|
||||
searchCategory();
|
||||
});
|
||||
};
|
||||
|
||||
function searchCategory() {
|
||||
var container = $('#content .categories');
|
||||
function revealParents(cid) {
|
||||
var parentCid = container.find('li[data-cid="' + cid + '"]').attr('data-parent-cid');
|
||||
if (parentCid) {
|
||||
container.find('li[data-cid="' + parentCid + '"]').removeClass('hidden');
|
||||
revealParents(parentCid);
|
||||
}
|
||||
}
|
||||
|
||||
function revealChildren(cid) {
|
||||
var els = container.find('li[data-parent-cid="' + cid + '"]');
|
||||
els.each(function (index, el) {
|
||||
var $el = $(el);
|
||||
$el.removeClass('hidden');
|
||||
revealChildren($el.attr('data-cid'));
|
||||
});
|
||||
}
|
||||
|
||||
var categoryEls = container.find('li[data-cid]');
|
||||
var val = $('#category-search').val().toLowerCase();
|
||||
var noMatch = true;
|
||||
var cids = [];
|
||||
categoryEls.each(function () {
|
||||
var liEl = $(this);
|
||||
var isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1;
|
||||
if (noMatch && isMatch) {
|
||||
noMatch = false;
|
||||
}
|
||||
if (isMatch && val) {
|
||||
cids.push(liEl.attr('data-cid'));
|
||||
}
|
||||
liEl.toggleClass('hidden', !isMatch);
|
||||
});
|
||||
|
||||
cids.forEach(function (cid) {
|
||||
revealParents(cid);
|
||||
revealChildren(cid);
|
||||
});
|
||||
|
||||
$('[component="category/no-matches"]').toggleClass('hidden', !noMatch);
|
||||
}
|
||||
|
||||
Categories.throwCreateModal = function () {
|
||||
socket.emit('categories.getSelectCategories', {}, function (err, categories) {
|
||||
if (err) {
|
||||
|
||||
@@ -5,7 +5,8 @@ define('admin/manage/group', [
|
||||
'forum/groups/memberlist',
|
||||
'iconSelect',
|
||||
'admin/modules/colorpicker',
|
||||
], function (memberList, iconSelect, colorpicker) {
|
||||
'translator',
|
||||
], function (memberList, iconSelect, colorpicker, translator) {
|
||||
var Groups = {};
|
||||
|
||||
Groups.init = function () {
|
||||
@@ -36,6 +37,68 @@ define('admin/manage/group', [
|
||||
groupLabelPreview.css('color', changeGroupTextColor.val() || '#ffffff');
|
||||
});
|
||||
|
||||
setupGroupMembersMenu(groupName);
|
||||
|
||||
$('#group-icon, #group-icon-label').on('click', function () {
|
||||
iconSelect.init(groupIcon, function () {
|
||||
var newIcon = groupIcon.attr('value');
|
||||
if (newIcon === 'fa-nbb-none') {
|
||||
newIcon = 'hidden';
|
||||
}
|
||||
$('#group-icon-preview').attr('class', 'fa fa-fw ' + (newIcon || 'hidden'));
|
||||
});
|
||||
});
|
||||
|
||||
$('[component="category/list"] [data-cid]').on('click', navigateToCategory);
|
||||
|
||||
colorpicker.enable(changeGroupLabelColor, function (hsb, hex) {
|
||||
groupLabelPreview.css('background-color', '#' + hex);
|
||||
});
|
||||
|
||||
colorpicker.enable(changeGroupTextColor, function (hsb, hex) {
|
||||
groupLabelPreview.css('color', '#' + hex);
|
||||
});
|
||||
|
||||
$('form').on('change', 'input, select, textarea', function () {
|
||||
app.flags = app.flags || {};
|
||||
app.flags._unsaved = true;
|
||||
});
|
||||
|
||||
$('#save').on('click', function () {
|
||||
socket.emit('admin.groups.update', {
|
||||
groupName: groupName,
|
||||
values: {
|
||||
name: $('#change-group-name').val(),
|
||||
userTitle: changeGroupUserTitle.val(),
|
||||
description: $('#change-group-desc').val(),
|
||||
icon: groupIcon.attr('value'),
|
||||
labelColor: changeGroupLabelColor.val(),
|
||||
textColor: changeGroupTextColor.val(),
|
||||
userTitleEnabled: $('#group-userTitleEnabled').is(':checked'),
|
||||
private: $('#group-private').is(':checked'),
|
||||
hidden: $('#group-hidden').is(':checked'),
|
||||
disableJoinRequests: $('#group-disableJoinRequests').is(':checked'),
|
||||
disableLeave: $('#group-disableLeave').is(':checked'),
|
||||
},
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
var newName = $('#change-group-name').val();
|
||||
|
||||
// If the group name changed, change url
|
||||
if (groupName !== newName) {
|
||||
ajaxify.go('admin/manage/groups/' + encodeURIComponent(newName), undefined, true);
|
||||
}
|
||||
|
||||
app.alertSuccess('[[admin/manage/groups:edit.save-success]]');
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
function setupGroupMembersMenu(groupName) {
|
||||
$('[component="groups/members"]').on('click', '[data-action]', function () {
|
||||
var btnEl = $(this);
|
||||
var userRow = btnEl.parents('[data-uid]');
|
||||
@@ -77,58 +140,27 @@ define('admin/manage/group', [
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#group-icon, #group-icon-label').on('click', function () {
|
||||
iconSelect.init(groupIcon, function () {
|
||||
var newIcon = groupIcon.attr('value');
|
||||
if (newIcon === 'fa-nbb-none') {
|
||||
newIcon = 'hidden';
|
||||
}
|
||||
$('#group-icon-preview').attr('class', 'fa fa-fw ' + (newIcon || 'hidden'));
|
||||
});
|
||||
});
|
||||
function navigateToCategory() {
|
||||
var cid = $(this).attr('data-cid');
|
||||
|
||||
colorpicker.enable(changeGroupLabelColor, function (hsb, hex) {
|
||||
groupLabelPreview.css('background-color', '#' + hex);
|
||||
});
|
||||
|
||||
colorpicker.enable(changeGroupTextColor, function (hsb, hex) {
|
||||
groupLabelPreview.css('color', '#' + hex);
|
||||
});
|
||||
|
||||
$('#save').on('click', function () {
|
||||
socket.emit('admin.groups.update', {
|
||||
groupName: groupName,
|
||||
values: {
|
||||
name: $('#change-group-name').val(),
|
||||
userTitle: changeGroupUserTitle.val(),
|
||||
description: $('#change-group-desc').val(),
|
||||
icon: groupIcon.attr('value'),
|
||||
labelColor: changeGroupLabelColor.val(),
|
||||
textColor: changeGroupTextColor.val(),
|
||||
userTitleEnabled: $('#group-userTitleEnabled').is(':checked'),
|
||||
private: $('#group-private').is(':checked'),
|
||||
hidden: $('#group-hidden').is(':checked'),
|
||||
disableJoinRequests: $('#group-disableJoinRequests').is(':checked'),
|
||||
disableLeave: $('#group-disableLeave').is(':checked'),
|
||||
},
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
var newName = $('#change-group-name').val();
|
||||
|
||||
// If the group name changed, change url
|
||||
if (groupName !== newName) {
|
||||
ajaxify.go('admin/manage/groups/' + encodeURIComponent(newName), undefined, true);
|
||||
}
|
||||
|
||||
app.alertSuccess('[[admin/manage/groups:edit.save-success]]');
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
if (cid) {
|
||||
var url = 'admin/manage/privileges/' + cid + '?group=' + ajaxify.data.group.name;
|
||||
if (app.flags && app.flags._unsaved === true) {
|
||||
translator.translate('[[global:unsaved-changes]]', function (text) {
|
||||
bootbox.confirm(text, function (navigate) {
|
||||
if (navigate) {
|
||||
app.flags._unsaved = false;
|
||||
ajaxify.go(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
ajaxify.go(url);
|
||||
}
|
||||
}
|
||||
|
||||
return Groups;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('admin/manage/post-queue', function () {
|
||||
define('admin/manage/post-queue', ['categorySelector'], function (categorySelector) {
|
||||
var PostQueue = {};
|
||||
|
||||
PostQueue.init = function () {
|
||||
@@ -22,31 +22,64 @@ define('admin/manage/post-queue', function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.posts-list').on('click', '.post-content', function () {
|
||||
handleContentEdit('.post-content', '.post-content-editable', 'textarea');
|
||||
handleContentEdit('.topic-title', '.topic-title-editable', 'input');
|
||||
|
||||
$('.posts-list').on('click', '.topic-category[data-editable]', function () {
|
||||
var $this = $(this);
|
||||
var id = $this.parents('[data-id]').attr('data-id');
|
||||
categorySelector.modal(ajaxify.data.allCategories, function (cid) {
|
||||
var category = ajaxify.data.allCategories.find(function (c) {
|
||||
return parseInt(c.cid, 10) === parseInt(cid, 10);
|
||||
});
|
||||
socket.emit('posts.editQueuedContent', {
|
||||
id: id,
|
||||
cid: cid,
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
app.parseAndTranslate('admin/manage/post-queue', 'posts', {
|
||||
posts: [{
|
||||
category: category,
|
||||
}],
|
||||
}, function (html) {
|
||||
$this.replaceWith(html.find('.topic-category'));
|
||||
});
|
||||
});
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
function handleContentEdit(displayClass, editableClass, inputSelector) {
|
||||
$('.posts-list').on('click', displayClass, function () {
|
||||
var el = $(this);
|
||||
el.addClass('hidden');
|
||||
var textareaParent = el.parent().find('.post-content-editable');
|
||||
textareaParent.removeClass('hidden').find('textarea').focus();
|
||||
var inputEl = el.parent().find(editableClass);
|
||||
inputEl.removeClass('hidden').find(inputSelector).focus();
|
||||
});
|
||||
|
||||
$('.posts-list').on('blur', '.post-content-editable textarea', function () {
|
||||
$('.posts-list').on('blur', editableClass + ' ' + inputSelector, function () {
|
||||
var textarea = $(this);
|
||||
var preview = textarea.parent().parent().find('.post-content');
|
||||
var preview = textarea.parent().parent().find(displayClass);
|
||||
var id = textarea.parents('[data-id]').attr('data-id');
|
||||
var titleEdit = displayClass === '.topic-title';
|
||||
|
||||
socket.emit('posts.editQueuedContent', {
|
||||
id: id,
|
||||
content: textarea.val(),
|
||||
title: titleEdit ? textarea.val() : undefined,
|
||||
content: titleEdit ? undefined : textarea.val(),
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return app.alertError(err);
|
||||
}
|
||||
preview.html(data.postData.content);
|
||||
preview.html(titleEdit ? data.postData.title : data.postData.content);
|
||||
textarea.parent().addClass('hidden');
|
||||
preview.removeClass('hidden');
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return PostQueue;
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ define('admin/manage/privileges', [
|
||||
'benchpress',
|
||||
'categorySelector',
|
||||
], function (autocomplete, translator, Benchpress, categorySelector) {
|
||||
var Privileges = {};
|
||||
var Privileges = {};
|
||||
|
||||
var cid;
|
||||
|
||||
@@ -21,6 +21,8 @@ define('admin/manage/privileges', [
|
||||
});
|
||||
|
||||
Privileges.setupPrivilegeTable();
|
||||
|
||||
highlightRow();
|
||||
};
|
||||
|
||||
Privileges.setupPrivilegeTable = function () {
|
||||
@@ -79,7 +81,7 @@ define('admin/manage/privileges', [
|
||||
Privileges.exposeAssumedPrivileges();
|
||||
};
|
||||
|
||||
Privileges.refreshPrivilegeTable = function () {
|
||||
Privileges.refreshPrivilegeTable = function (groupToHighlight) {
|
||||
socket.emit('admin.categories.getPrivilegeSettings', cid, function (err, privileges) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
@@ -92,6 +94,10 @@ define('admin/manage/privileges', [
|
||||
translator.translate(html, function (html) {
|
||||
$('.privilege-table-container').html(html);
|
||||
Privileges.exposeAssumedPrivileges();
|
||||
|
||||
if (groupToHighlight) {
|
||||
$('[data-group-name="' + groupToHighlight + '"]').addClass('selected');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -182,24 +188,7 @@ define('admin/manage/privileges', [
|
||||
inputEl.focus();
|
||||
|
||||
autocomplete.group(inputEl, function (ev, ui) {
|
||||
var defaultPrivileges;
|
||||
if (ajaxify.data.url === '/admin/manage/privileges/admin') {
|
||||
defaultPrivileges = ['groups:admin:dashboard'];
|
||||
} else {
|
||||
defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat'];
|
||||
}
|
||||
|
||||
socket.emit('admin.categories.setPrivilege', {
|
||||
cid: isNaN(cid) ? 0 : cid,
|
||||
privilege: defaultPrivileges,
|
||||
set: true,
|
||||
member: ui.item.group.name,
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
Privileges.refreshPrivilegeTable();
|
||||
addGroupToCategory(ui.item.group.name, function () {
|
||||
modal.modal('hide');
|
||||
});
|
||||
});
|
||||
@@ -235,5 +224,42 @@ define('admin/manage/privileges', [
|
||||
});
|
||||
};
|
||||
|
||||
function highlightRow() {
|
||||
var params = utils.params();
|
||||
if (params.group) {
|
||||
var el = $('[data-group-name="' + params.group + '"]');
|
||||
if (el.length) {
|
||||
return el.addClass('selected');
|
||||
}
|
||||
|
||||
addGroupToCategory(params.group);
|
||||
}
|
||||
}
|
||||
|
||||
function addGroupToCategory(group, cb) {
|
||||
var defaultPrivileges;
|
||||
if (ajaxify.data.url === '/admin/manage/privileges/admin') {
|
||||
defaultPrivileges = ['groups:admin:dashboard'];
|
||||
} else {
|
||||
defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat'];
|
||||
}
|
||||
|
||||
socket.emit('admin.categories.setPrivilege', {
|
||||
cid: isNaN(cid) ? 0 : cid,
|
||||
privilege: defaultPrivileges,
|
||||
set: true,
|
||||
member: group,
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
Privileges.refreshPrivilegeTable(group);
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Privileges;
|
||||
});
|
||||
|
||||
@@ -38,8 +38,6 @@ app.cacheBuster = null;
|
||||
});
|
||||
|
||||
app.load = function () {
|
||||
app.loadProgressiveStylesheet();
|
||||
|
||||
overrides.overrideTimeago();
|
||||
|
||||
var url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash);
|
||||
@@ -751,14 +749,6 @@ app.cacheBuster = null;
|
||||
});
|
||||
};
|
||||
|
||||
app.loadProgressiveStylesheet = function () {
|
||||
var linkEl = document.createElement('link');
|
||||
linkEl.rel = 'stylesheet';
|
||||
linkEl.href = config.relative_path + '/assets/js-enabled.css?' + app.cacheBuster;
|
||||
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
app.showCookieWarning = function () {
|
||||
require(['translator', 'storage'], function (translator, storage) {
|
||||
if (!config.cookies.enabled || !navigator.cookieEnabled) {
|
||||
|
||||
@@ -56,6 +56,7 @@ define('forum/chats', [
|
||||
Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]'));
|
||||
Chats.addLeaveHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="leave"]'));
|
||||
Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content'));
|
||||
Chats.addScrollBottomHandler($('.chat-content'));
|
||||
Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]'));
|
||||
Chats.addIPHandler($('[component="chat/main-wrapper"]'));
|
||||
Chats.createAutoComplete($('[component="chat/input"]'));
|
||||
@@ -101,6 +102,7 @@ define('forum/chats', [
|
||||
Chats.addScrollHandler = function (roomId, uid, el) {
|
||||
var loading = false;
|
||||
el.off('scroll').on('scroll', function () {
|
||||
messages.toggleScrollUpAlert(el);
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
@@ -144,6 +146,14 @@ define('forum/chats', [
|
||||
});
|
||||
};
|
||||
|
||||
Chats.addScrollBottomHandler = function (chatContent) {
|
||||
chatContent.parent()
|
||||
.find('[component="chat/messages/scroll-up-alert"]')
|
||||
.off('click').on('click', function () {
|
||||
messages.scrollToBottom(chatContent);
|
||||
});
|
||||
};
|
||||
|
||||
Chats.addCharactersLeftHandler = function (parent) {
|
||||
var element = parent.find('[component="chat/input"]');
|
||||
element.on('change keyup paste', function () {
|
||||
|
||||
@@ -84,11 +84,13 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
|
||||
|
||||
function onMessagesParsed(chatContentEl, html) {
|
||||
var newMessage = $(html);
|
||||
|
||||
var isAtBottom = messages.isAtBottom(chatContentEl);
|
||||
newMessage.appendTo(chatContentEl);
|
||||
newMessage.find('.timeago').timeago();
|
||||
newMessage.find('img:not(.not-responsive)').addClass('img-responsive');
|
||||
messages.scrollToBottom(chatContentEl);
|
||||
if (isAtBottom) {
|
||||
messages.scrollToBottom(chatContentEl);
|
||||
}
|
||||
|
||||
$(window).trigger('action:chat.received', {
|
||||
messageEl: newMessage,
|
||||
@@ -112,13 +114,31 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
|
||||
}
|
||||
};
|
||||
|
||||
messages.isAtBottom = function (containerEl, threshold) {
|
||||
if (containerEl.length) {
|
||||
var distanceToBottom = containerEl[0].scrollHeight - (
|
||||
containerEl.outerHeight() + containerEl.scrollTop()
|
||||
);
|
||||
return distanceToBottom < (threshold || 100);
|
||||
}
|
||||
};
|
||||
|
||||
messages.scrollToBottom = function (containerEl) {
|
||||
if (containerEl.length) {
|
||||
if (containerEl && containerEl.length) {
|
||||
containerEl.scrollTop(containerEl[0].scrollHeight - containerEl.height());
|
||||
containerEl.parent()
|
||||
.find('[component="chat/messages/scroll-up-alert"]')
|
||||
.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
messages.toggleScrollUpAlert = function (containerEl) {
|
||||
var isAtBottom = messages.isAtBottom(containerEl, 300);
|
||||
containerEl.parent()
|
||||
.find('[component="chat/messages/scroll-up-alert"]')
|
||||
.toggleClass('hidden', isAtBottom);
|
||||
};
|
||||
|
||||
messages.prepEdit = function (inputEl, messageId, roomId) {
|
||||
socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function (err, raw) {
|
||||
if (err) {
|
||||
|
||||
@@ -91,7 +91,9 @@ define('chat', [
|
||||
|
||||
if (modal.is(':visible')) {
|
||||
taskbar.updateActive(modal.attr('data-uuid'));
|
||||
ChatsMessages.scrollToBottom(modal.find('.chat-content'));
|
||||
if (ChatsMessages.isAtBottom(modal.find('.chat-content'))) {
|
||||
ChatsMessages.scrollToBottom(modal.find('.chat-content'));
|
||||
}
|
||||
} else if (!ajaxify.data.template.chats) {
|
||||
module.toggleNew(modal.attr('data-uuid'), true, true);
|
||||
}
|
||||
@@ -239,6 +241,7 @@ define('chat', [
|
||||
Chats.createAutoComplete(chatModal.find('[component="chat/input"]'));
|
||||
|
||||
Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content'));
|
||||
Chats.addScrollBottomHandler(chatModal.find('.chat-content'));
|
||||
|
||||
Chats.addCharactersLeftHandler(chatModal);
|
||||
Chats.addIPHandler(chatModal);
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
}
|
||||
|
||||
if (namespace && !key) {
|
||||
warn('Missing key in translation token "' + name + '"');
|
||||
warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"');
|
||||
return Promise.resolve('[[' + namespace + ']]');
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
return translation.then(function (translated) {
|
||||
// check if the translation is missing first
|
||||
if (!translated) {
|
||||
warn('Missing translation "' + name + '"');
|
||||
warn('Missing translation "' + name + '" for language "' + self.lang + '"');
|
||||
return backup || key;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
const user = require('../../user');
|
||||
const meta = require('../../meta');
|
||||
const plugins = require('../../plugins');
|
||||
const helpers = require('../helpers');
|
||||
const groups = require('../../groups');
|
||||
const accountHelpers = require('./helpers');
|
||||
@@ -67,9 +66,7 @@ editController.get = async function (req, res, next) {
|
||||
},
|
||||
]);
|
||||
userData.editButtons = [];
|
||||
|
||||
const result = await plugins.fireHook('filter:user.account.edit', userData);
|
||||
res.render('account/edit', result);
|
||||
res.render('account/edit', userData);
|
||||
};
|
||||
|
||||
editController.password = async function (req, res, next) {
|
||||
|
||||
@@ -56,11 +56,9 @@ notificationsController.get = async function (req, res, next) {
|
||||
nids = nids.slice(start, stop + 1);
|
||||
|
||||
const notifications = await user.notifications.getNotifications(nids, req.uid);
|
||||
const data = await plugins.fireHook('filter:notifications.get', {
|
||||
notifications: notifications,
|
||||
});
|
||||
|
||||
res.render('notifications', {
|
||||
notifications: data.notifications,
|
||||
notifications: notifications,
|
||||
pagination: pagination.create(page, pageCount, req.query),
|
||||
filters: allFilters,
|
||||
regularFilters: regularFilters,
|
||||
|
||||
@@ -6,7 +6,6 @@ const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const posts = require('../../posts');
|
||||
const categories = require('../../categories');
|
||||
const plugins = require('../../plugins');
|
||||
const meta = require('../../meta');
|
||||
const accountHelpers = require('./helpers');
|
||||
const helpers = require('../helpers');
|
||||
@@ -58,8 +57,7 @@ profileController.get = async function (req, res, next) {
|
||||
userData.selectedGroup = userData.groups.filter(group => group && userData.groupTitleArray.includes(group.name))
|
||||
.sort((a, b) => userData.groupTitleArray.indexOf(a.name) - userData.groupTitleArray.indexOf(b.name));
|
||||
|
||||
const results = await plugins.fireHook('filter:user.account', { userData: userData, uid: req.uid });
|
||||
res.render('account/profile', results.userData);
|
||||
res.render('account/profile', userData);
|
||||
};
|
||||
|
||||
async function incrementProfileViews(req, userData) {
|
||||
|
||||
@@ -5,6 +5,7 @@ const validator = require('validator');
|
||||
|
||||
const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const categories = require('../../categories');
|
||||
const groups = require('../../groups');
|
||||
const meta = require('../../meta');
|
||||
const pagination = require('../../pagination');
|
||||
@@ -50,12 +51,15 @@ groupsController.get = async function (req, res, next) {
|
||||
};
|
||||
});
|
||||
|
||||
const allCategories = await categories.buildForSelectAll();
|
||||
|
||||
res.render('admin/manage/group', {
|
||||
group: group,
|
||||
groupNames: groupNameData,
|
||||
allowPrivateGroups: meta.config.allowPrivateGroups,
|
||||
maximumGroupNameLength: meta.config.maximumGroupNameLength,
|
||||
maximumGroupTitleLength: meta.config.maximumGroupTitleLength,
|
||||
categories: allCategories,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,12 @@ async function getNodeInfo() {
|
||||
release: os.release(),
|
||||
load: os.loadavg().map(function (load) { return load.toFixed(2); }).join(', '),
|
||||
},
|
||||
nodebb: {
|
||||
isCluster: nconf.get('isCluster'),
|
||||
isPrimary: nconf.get('isPrimary'),
|
||||
runJobs: nconf.get('runJobs'),
|
||||
jobsDisabled: nconf.get('jobsDisabled'),
|
||||
},
|
||||
};
|
||||
data.process.cpuUsage.user /= 1000000;
|
||||
data.process.cpuUsage.user = data.process.cpuUsage.user.toFixed(2);
|
||||
@@ -109,5 +115,5 @@ async function getGitInfo() {
|
||||
getAsync('git rev-parse HEAD'),
|
||||
getAsync('git rev-parse --abbrev-ref HEAD'),
|
||||
]);
|
||||
return { hash: hash, branch: branch };
|
||||
return { hash: hash, hashShort: hash.substr(0, 6), branch: branch };
|
||||
}
|
||||
|
||||
@@ -181,12 +181,17 @@ modsController.postQueue = async function (req, res, next) {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const postsPerPage = 20;
|
||||
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids] = await Promise.all([
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids, allCategories] = await Promise.all([
|
||||
db.getSortedSetRange('post:queue', 0, -1),
|
||||
user.isAdminOrGlobalMod(req.uid),
|
||||
user.getModeratedCids(req.uid),
|
||||
categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']),
|
||||
]);
|
||||
|
||||
allCategories.forEach((c) => {
|
||||
c.disabledClass = !isAdminOrGlobalMod && !moderatedCids.includes(String(c.cid));
|
||||
});
|
||||
|
||||
let postData = await getQueuedPosts(ids);
|
||||
postData = postData.filter(p => p && (isAdminOrGlobalMod || moderatedCids.includes(String(p.category.cid))));
|
||||
|
||||
@@ -198,6 +203,7 @@ modsController.postQueue = async function (req, res, next) {
|
||||
res.render('admin/manage/post-queue', {
|
||||
title: '[[pages:post-queue]]',
|
||||
posts: postData,
|
||||
allCategories: allCategories,
|
||||
pagination: pagination.create(page, pageCount),
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]),
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ const meta = require('../meta');
|
||||
const topics = require('../topics');
|
||||
const posts = require('../posts');
|
||||
const privileges = require('../privileges');
|
||||
const plugins = require('../plugins');
|
||||
const helpers = require('./helpers');
|
||||
const pagination = require('../pagination');
|
||||
const utils = require('../utils');
|
||||
@@ -69,8 +68,6 @@ topicsController.get = async function getTopic(req, res, callback) {
|
||||
|
||||
topics.modifyPostsByPrivilege(topicData, userPrivileges);
|
||||
|
||||
const hookData = await plugins.fireHook('filter:controllers.topic.get', { topicData: topicData, uid: req.uid });
|
||||
|
||||
topicData.privileges = userPrivileges;
|
||||
topicData.topicStaleDays = meta.config.topicStaleDays;
|
||||
topicData['reputation:disabled'] = meta.config['reputation:disabled'];
|
||||
@@ -91,7 +88,7 @@ topicsController.get = async function getTopic(req, res, callback) {
|
||||
topicData.postIndex = postIndex;
|
||||
|
||||
await Promise.all([
|
||||
buildBreadcrumbs(hookData.topicData),
|
||||
buildBreadcrumbs(topicData),
|
||||
addTags(topicData, req, res),
|
||||
]);
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ unreadController.get = async function (req, res, next) {
|
||||
req.query.page = Math.max(1, Math.min(data.pageCount, page));
|
||||
return helpers.redirect(res, '/unread?' + querystring.stringify(req.query));
|
||||
}
|
||||
data.showSelect = isPrivileged;
|
||||
data.showSelect = true;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = watchedCategories.categories;
|
||||
data.allCategoriesUrl = 'unread' + helpers.buildQueryString('', filter, '');
|
||||
|
||||
@@ -26,10 +26,6 @@ function requireSharp() {
|
||||
|
||||
image.isFileTypeAllowed = async function (path) {
|
||||
const plugins = require('./plugins');
|
||||
// deprecated: remove in 1.14.0
|
||||
if (plugins.hasListeners('filter:file.isFileTypeAllowed')) {
|
||||
return await plugins.fireHook('filter:file.isFileTypeAllowed', path);
|
||||
}
|
||||
if (plugins.hasListeners('filter:image.isFileTypeAllowed')) {
|
||||
return await plugins.fireHook('filter:image.isFileTypeAllowed', path);
|
||||
}
|
||||
|
||||
@@ -50,10 +50,11 @@ module.exports = function (Messaging) {
|
||||
db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid),
|
||||
]);
|
||||
await Promise.all([
|
||||
Messaging.addSystemMessage('user-join', uid, roomId), // chat owner should also get the user-join system message
|
||||
Messaging.addUsersToRoom(uid, toUids, roomId),
|
||||
Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now),
|
||||
]);
|
||||
// chat owner should also get the user-join system message
|
||||
await Messaging.addSystemMessage('user-join', uid, roomId);
|
||||
|
||||
return roomId;
|
||||
};
|
||||
|
||||
@@ -56,10 +56,6 @@ Tags.parse = async (req, data, meta, link) => {
|
||||
}, {
|
||||
rel: 'manifest',
|
||||
href: nconf.get('relative_path') + '/manifest.json',
|
||||
}, {
|
||||
rel: 'preload',
|
||||
href: nconf.get('relative_path') + '/assets/js-enabled.css?' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
|
||||
as: 'style',
|
||||
}];
|
||||
|
||||
if (plugins.hasListeners('filter:search.query')) {
|
||||
|
||||
@@ -6,11 +6,7 @@ const utils = require('../utils');
|
||||
|
||||
module.exports = function (Plugins) {
|
||||
Plugins.deprecatedHooks = {
|
||||
'filter:controllers.topic.get': 'filter:topic.build',
|
||||
'filter:user.account': 'filter:account/profile.build',
|
||||
'filter:user.account.edit': 'filter:account/edit.build',
|
||||
'filter:notifications.get': 'filter:notifications.build',
|
||||
'filter:file.isFileTypeAllowed': 'filter:image.isFileTypeAllowed',
|
||||
|
||||
};
|
||||
|
||||
Plugins.internals = {
|
||||
|
||||
@@ -147,23 +147,31 @@ module.exports = function (Posts) {
|
||||
socketHelpers.notifyNew(data.uid, 'newPost', result);
|
||||
}
|
||||
|
||||
Posts.editQueuedContent = async function (uid, id, content) {
|
||||
const canEditQueue = await Posts.canEditQueue(uid, id);
|
||||
Posts.editQueuedContent = async function (uid, editData) {
|
||||
const canEditQueue = await Posts.canEditQueue(uid, editData);
|
||||
if (!canEditQueue) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const data = await getParsedObject(id);
|
||||
const data = await getParsedObject(editData.id);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
data.data.content = content;
|
||||
await db.setObjectField('post:queue:' + id, 'data', JSON.stringify(data.data));
|
||||
if (editData.content !== undefined) {
|
||||
data.data.content = editData.content;
|
||||
}
|
||||
if (editData.title !== undefined) {
|
||||
data.data.title = editData.title;
|
||||
}
|
||||
if (editData.cid !== undefined) {
|
||||
data.data.cid = editData.cid;
|
||||
}
|
||||
await db.setObjectField('post:queue:' + editData.id, 'data', JSON.stringify(data.data));
|
||||
};
|
||||
|
||||
Posts.canEditQueue = async function (uid, id) {
|
||||
Posts.canEditQueue = async function (uid, editData) {
|
||||
const [isAdminOrGlobalMod, data] = await Promise.all([
|
||||
user.isAdminOrGlobalMod(uid),
|
||||
getParsedObject(id),
|
||||
getParsedObject(editData.id),
|
||||
]);
|
||||
if (!data) {
|
||||
return false;
|
||||
@@ -179,6 +187,11 @@ module.exports = function (Posts) {
|
||||
} else if (data.type === 'reply') {
|
||||
cid = await topics.getTopicField(data.data.tid, 'cid');
|
||||
}
|
||||
return await user.isModerator(uid, cid);
|
||||
const isModerator = await user.isModerator(uid, cid);
|
||||
let isModeratorOfTargetCid = true;
|
||||
if (editData.cid) {
|
||||
isModeratorOfTargetCid = await user.isModerator(uid, editData.cid);
|
||||
}
|
||||
return isModerator && isModeratorOfTargetCid;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ Auth.reloadRoutes = async function (params) {
|
||||
loginStrategies.forEach(function (strategy) {
|
||||
if (strategy.url) {
|
||||
router.get(strategy.url, Auth.middleware.applyCSRF, function (req, res, next) {
|
||||
req.session.ssoState = req.csrfToken();
|
||||
req.session.ssoState = req.csrfToken && req.csrfToken();
|
||||
passport.authenticate(strategy.name, {
|
||||
scope: strategy.scope,
|
||||
prompt: strategy.prompt || undefined,
|
||||
|
||||
@@ -150,6 +150,7 @@ async function onMessage(socket, payload) {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
winston.error(err.stack ? err.stack : err.message);
|
||||
callback({ message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ SocketPosts.reject = async function (socket, data) {
|
||||
};
|
||||
|
||||
async function acceptOrReject(method, socket, data) {
|
||||
const canEditQueue = await posts.canEditQueue(socket.uid, data.id);
|
||||
const canEditQueue = await posts.canEditQueue(socket.uid, data);
|
||||
if (!canEditQueue) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
@@ -174,11 +174,14 @@ async function acceptOrReject(method, socket, data) {
|
||||
}
|
||||
|
||||
SocketPosts.editQueuedContent = async function (socket, data) {
|
||||
if (!data || !data.id || !data.content) {
|
||||
if (!data || !data.id || (!data.content && !data.title && !data.cid)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
await posts.editQueuedContent(socket.uid, data.id, data.content);
|
||||
return await plugins.fireHook('filter:parse.post', { postData: data });
|
||||
await posts.editQueuedContent(socket.uid, data);
|
||||
if (data.content) {
|
||||
return await plugins.fireHook('filter:parse.post', { postData: data });
|
||||
}
|
||||
return { postData: data };
|
||||
};
|
||||
|
||||
require('../promisify')(SocketPosts);
|
||||
|
||||
@@ -13,6 +13,11 @@ module.exports = function (SocketTopics) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const canMove = await privileges.categories.isAdminOrMod(data.cid, socket.uid);
|
||||
if (!canMove) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const uids = await user.getUidsFromSet('users:online', 0, -1);
|
||||
|
||||
await async.eachLimit(data.tids, 10, async function (tid) {
|
||||
|
||||
@@ -29,13 +29,14 @@ Digest.execute = async function (payload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
|
||||
await Digest.send({
|
||||
interval: payload.interval,
|
||||
subscribers: subscribers,
|
||||
});
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
|
||||
winston.info('[user/jobs] Digest (' + payload.interval + ') complete.');
|
||||
} catch (err) {
|
||||
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err.stack);
|
||||
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')\n' + err.stack);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -81,7 +82,10 @@ Digest.getSubscribers = async function (interval) {
|
||||
});
|
||||
subUids = await user.bans.filterBanned(subUids);
|
||||
subscribers = subscribers.concat(subUids);
|
||||
}, { interval: 1000 });
|
||||
}, {
|
||||
interval: 1000,
|
||||
batch: 500,
|
||||
});
|
||||
|
||||
const results = await plugins.fireHook('filter:digest.subscribers', {
|
||||
interval: interval,
|
||||
@@ -91,15 +95,13 @@ Digest.getSubscribers = async function (interval) {
|
||||
};
|
||||
|
||||
Digest.send = async function (data) {
|
||||
var emailsSent = 0;
|
||||
let emailsSent = 0;
|
||||
if (!data || !data.subscribers || !data.subscribers.length) {
|
||||
return emailsSent;
|
||||
}
|
||||
const now = new Date();
|
||||
|
||||
const users = await user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline']);
|
||||
|
||||
async.eachLimit(users, 100, async function (userObj) {
|
||||
await async.eachLimit(data.subscribers, 100, async function (uid) {
|
||||
const userObj = await user.getUserFields(uid, ['uid', 'username', 'userslug', 'lastonline']);
|
||||
let [notifications, topicsData] = await Promise.all([
|
||||
user.notifications.getUnreadInterval(userObj.uid, data.interval),
|
||||
getTermTopics(data.interval, userObj.uid, 0, 9),
|
||||
@@ -121,13 +123,14 @@ Digest.send = async function (data) {
|
||||
|
||||
// Fix relative paths in topic data
|
||||
topicsData = topicsData.map(function (topicObj) {
|
||||
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
|
||||
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser && topicObj.teaser.user ? topicObj.teaser.user : topicObj.user;
|
||||
if (user && user.picture && utils.isRelativeUrl(user.picture)) {
|
||||
user.picture = nconf.get('base_url') + user.picture;
|
||||
}
|
||||
return topicObj;
|
||||
});
|
||||
emailsSent += 1;
|
||||
const now = new Date();
|
||||
try {
|
||||
await emailer.send('digest', userObj.uid, {
|
||||
subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
|
||||
@@ -139,15 +142,14 @@ Digest.send = async function (data) {
|
||||
showUnsubscribe: true,
|
||||
});
|
||||
} catch (err) {
|
||||
winston.error('[user/jobs] Could not send digest email', err.stack);
|
||||
winston.error('[user/jobs] Could not send digest email\n' + err.stack);
|
||||
}
|
||||
|
||||
if (data.interval !== 'alltime') {
|
||||
await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid);
|
||||
}
|
||||
}, function () {
|
||||
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
|
||||
});
|
||||
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
|
||||
};
|
||||
|
||||
Digest.getDeliveryTimes = async (start, stop) => {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<td>[[admin/development/info:host]]</td>
|
||||
<td class="text-center">[[admin/development/info:primary]]</td>
|
||||
<td>[[admin/development/info:pid]]</td>
|
||||
<td>[[admin/development/info:nodejs]]</td>
|
||||
<td>[[admin/development/info:online]]</td>
|
||||
@@ -25,6 +26,10 @@
|
||||
<!-- BEGIN info -->
|
||||
<tr>
|
||||
<td>{info.os.hostname}:{info.process.port}</td>
|
||||
<td class="text-center">
|
||||
{{{if info.nodebb.isPrimary}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}} /
|
||||
{{{if info.nodebb.runJobs}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}}
|
||||
</td>
|
||||
<td>{info.process.pid}</td>
|
||||
<td>{info.process.version}</td>
|
||||
<td>
|
||||
@@ -32,7 +37,7 @@
|
||||
<span title="[[admin/development/info:guests]]">{info.stats.onlineGuestCount}</span> /
|
||||
<span title="[[admin/development/info:sockets]]">{info.stats.socketCount}</span>
|
||||
</td>
|
||||
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hash}</a></td>
|
||||
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hashShort}</a></td>
|
||||
<td>{info.process.cpuUsage.user} / {info.process.cpuUsage.system}</td>
|
||||
<td>{info.process.memoryUsage.humanReadable} mb</td>
|
||||
<td>{info.os.load}</td>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="[[global:search]]" id="category-search">
|
||||
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
<div component="category/no-matches" class="hidden">[[admin/manage/categories:no-matches]]</div>
|
||||
<div class="categories"></div>
|
||||
|
||||
<button data-action="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<hr/>
|
||||
<a href="{config.relative_path}/admin/manage/privileges/{category.cid}" class="btn btn-info btn-block">
|
||||
<i class="fa fa-gear"></i> [[admin/manage/privileges:edit-privileges]]
|
||||
</a>
|
||||
<button class="btn btn-info btn-block copy-settings">
|
||||
<i class="fa fa-files-o"></i> [[admin/manage/categories:copy-settings]]
|
||||
</button>
|
||||
|
||||
@@ -113,6 +113,11 @@
|
||||
<option value="{groupNames.encodedName}" <!-- IF groupNames.selected -->selected<!-- ENDIF groupNames.selected -->>{groupNames.displayName}</option>
|
||||
<!-- END groupNames -->
|
||||
</select>
|
||||
<br />
|
||||
<div class="well">
|
||||
<strong class="pull-left">[[admin/manage/privileges:edit-privileges]]</strong><br />
|
||||
<!-- IMPORT partials/category-selector.tpl -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-3 pull-right">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" id="group-search">
|
||||
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row groups">
|
||||
<div class="col-xs-12">
|
||||
<div>
|
||||
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
|
||||
</div>
|
||||
|
||||
<table class="table table-striped groups-list">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[[admin/manage/post-queue:user]]</th>
|
||||
<th>[[admin/manage/post-queue:category]]</th>
|
||||
<th>[[admin/manage/post-queue:title]]</th>
|
||||
<th>[[admin/manage/post-queue:category]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:title]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:content]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
|
||||
<th>[[admin/manage/post-queue:posted]]</th>
|
||||
<th></th>
|
||||
@@ -35,8 +35,8 @@
|
||||
{posts.user.username}
|
||||
<!-- ENDIF posts.user.userslug -->
|
||||
</td>
|
||||
<td class="col-md-2">
|
||||
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.categiry.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
|
||||
<td class="col-md-2 topic-category" {{{if posts.data.cid}}}data-editable="editable"{{{end}}}">
|
||||
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.category.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
|
||||
</td>
|
||||
<td class="col-md-2 topic-title">
|
||||
<!-- IF posts.data.tid -->
|
||||
@@ -44,9 +44,14 @@
|
||||
<!-- ENDIF posts.data.tid -->
|
||||
{posts.data.title}
|
||||
</td>
|
||||
{{{if !posts.data.tid}}}
|
||||
<td class="col-md-2 topic-title-editable hidden">
|
||||
<input class="form-control" type="text" value="{posts.data.title}"/>
|
||||
</td>
|
||||
{{{end}}}
|
||||
<td class="col-md-5 post-content">{posts.data.content}</td>
|
||||
<td class="col-md-5 post-content-editable hidden">
|
||||
<textarea>{posts.data.rawContent}</textarea>
|
||||
<textarea class="form-control">{posts.data.rawContent}</textarea>
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<span class="timeago" title={posts.data.timestampISO}></span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ul data-cid="{cid}">
|
||||
<!-- BEGIN categories -->
|
||||
<li data-cid="{categories.cid}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
|
||||
<li data-cid="{categories.cid}" data-parent-cid="{categories.parentCid}" data-name="{categories.name}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
|
||||
<div class="row category-row">
|
||||
<div class="col-md-9">
|
||||
<div class="clearfix">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a role="menu-item">[[search:no-matches]]</a>
|
||||
</li>
|
||||
<!-- BEGIN categories -->
|
||||
<li role="presentation" class="category" data-cid="{categories.cid}" data-name="{categories.name}">
|
||||
<li role="presentation" class="category {{{if categories.disabledClass}}}disabled{{{end}}}" data-cid="{categories.cid}" data-name="{categories.name}">
|
||||
<a role="menu-item">{categories.level}<span component="category-markup"><!-- IF categories.icon --><span class="fa-stack" style="{function.generateCategoryBackground}"><i style="color: {categories.color};" class="fa fa-stack-1x fa-fw {categories.icon}"></i></span><!-- ENDIF categories.icon --> {categories.name}</span></a>
|
||||
</li>
|
||||
<!-- END categories -->
|
||||
|
||||
@@ -137,6 +137,8 @@ before(async function () {
|
||||
nconf.set('bcrypt_rounds', 1);
|
||||
nconf.set('socket.io:origins', '*:*');
|
||||
nconf.set('version', packageInfo.version);
|
||||
nconf.set('runJobs', false);
|
||||
nconf.set('jobsDisabled', false);
|
||||
|
||||
await meta.dependencies.check();
|
||||
|
||||
|
||||
@@ -1012,6 +1012,7 @@ describe('Post\'s', function () {
|
||||
describe('post queue', function () {
|
||||
var uid;
|
||||
var queueId;
|
||||
var topicQueueId;
|
||||
var jar;
|
||||
before(function (done) {
|
||||
meta.config.postQueue = 1;
|
||||
@@ -1033,6 +1034,7 @@ describe('Post\'s', function () {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(result.queued, true);
|
||||
assert.equal(result.message, '[[success:post-queued]]');
|
||||
topicQueueId = result.id;
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -1082,6 +1084,33 @@ describe('Post\'s', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit topic title in queue', function (done) {
|
||||
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' }, function (err) {
|
||||
assert.ifError(err);
|
||||
request(nconf.get('url') + '/api/post-queue', { jar: jar, json: true }, function (err, res, body) {
|
||||
assert.ifError(err);
|
||||
assert.equal(body.posts[0].type, 'topic');
|
||||
assert.equal(body.posts[0].data.title, 'new topic title');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit topic category in queue', function (done) {
|
||||
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: 2 }, function (err) {
|
||||
assert.ifError(err);
|
||||
request(nconf.get('url') + '/api/post-queue', { jar: jar, json: true }, function (err, res, body) {
|
||||
assert.ifError(err);
|
||||
assert.equal(body.posts[0].type, 'topic');
|
||||
assert.equal(body.posts[0].data.cid, 2);
|
||||
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: cid }, function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should prevent regular users from approving posts', function (done) {
|
||||
socketPosts.accept({ uid: uid }, { id: queueId }, function (err) {
|
||||
assert.equal(err.message, '[[error:no-privileges]]');
|
||||
|
||||
14
test/user.js
14
test/user.js
@@ -101,7 +101,7 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if username is already taken', async function () {
|
||||
it('should error if username is already taken or rename user', async function () {
|
||||
let err;
|
||||
async function tryCreate(data) {
|
||||
try {
|
||||
@@ -111,11 +111,19 @@ describe('User', function () {
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
const [uid1, uid2] = await Promise.all([
|
||||
tryCreate({ username: 'dupe1' }),
|
||||
tryCreate({ username: 'dupe1' }),
|
||||
]);
|
||||
assert.strictEqual(err.message, '[[error:username-taken]]');
|
||||
if (err) {
|
||||
assert.strictEqual(err.message, '[[error:username-taken]]');
|
||||
} else {
|
||||
const userData = await User.getUsersFields([uid1, uid2], ['username']);
|
||||
const userNames = userData.map(u => u.username);
|
||||
// make sure only 1 dupe1 is created
|
||||
assert.equal(userNames.filter(username => username === 'dupe1').length, 1);
|
||||
assert.equal(userNames.filter(username => username === 'dupe1 0').length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should error if email is already taken', async function () {
|
||||
|
||||
Reference in New Issue
Block a user