Resolve merge conflicts, minify only .js files

This commit is contained in:
Peter Jaszkowiak
2017-02-23 18:31:49 -07:00
99 changed files with 4500 additions and 1751 deletions

View File

@@ -27,6 +27,7 @@ editController.get = function (req, res, callback) {
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
userData.groups = userData.groups.filter(function (group) {
return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
@@ -36,7 +37,15 @@ editController.get = function (req, res, callback) {
});
userData.title = '[[pages:account/edit, ' + userData.username + ']]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:edit]]' }]);
userData.breadcrumbs = helpers.buildBreadcrumbs([
{
text: userData.username,
url: '/user/' + userData.userslug,
},
{
text: '[[user:edit]]',
},
]);
userData.editButtons = [];
plugins.fireHook('filter:user.account.edit', userData, function (err, userData) {
@@ -76,9 +85,17 @@ function renderRoute(name, req, res, next) {
userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]';
userData.breadcrumbs = helpers.buildBreadcrumbs([
{ text: userData.username, url: '/user/' + userData.userslug },
{ text: '[[user:edit]]', url: '/user/' + userData.userslug + '/edit' },
{ text: '[[user:' + name + ']]' },
{
text: userData.username,
url: '/user/' + userData.userslug,
},
{
text: '[[user:edit]]',
url: '/user/' + userData.userslug + '/edit',
},
{
text: '[[user:' + name + ']]',
},
]);
res.render('account/edit/' + name, userData);
@@ -139,7 +156,10 @@ editController.uploadPicture = function (req, res, next) {
return next(err);
}
res.json([{ name: userPhoto.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]);
res.json([{
name: userPhoto.name,
url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url,
}]);
});
};
@@ -154,7 +174,9 @@ editController.uploadCoverPicture = function (req, res, next) {
return next(err);
}
res.json([{ url: image.url }]);
res.json([{
url: image.url,
}]);
});
};

View File

@@ -37,11 +37,8 @@ settingsController.get = function (req, res, callback) {
homePageRoutes: function (next) {
getHomePageRoutes(next);
},
sounds: function (next) {
meta.sounds.getFiles(next);
},
soundsMapping: function (next) {
meta.sounds.getMapping(userData.uid, next);
meta.sounds.getUserSoundMap(userData.uid, next);
},
}, next);
},
@@ -50,16 +47,44 @@ settingsController.get = function (req, res, callback) {
userData.languages = results.languages;
userData.homePageRoutes = results.homePageRoutes;
var soundSettings = {
notificationSound: 'notification',
incomingChatSound: 'chat-incoming',
outgoingChatSound: 'chat-outgoing',
var types = [
'notification',
'chat-incoming',
'chat-outgoing',
];
var aliases = {
notification: 'notificationSound',
'chat-incoming': 'incomingChatSound',
'chat-outgoing': 'outgoingChatSound',
};
Object.keys(soundSettings).forEach(function (setting) {
userData[setting] = Object.keys(results.sounds).map(function (name) {
return { name: name, selected: name === results.soundsMapping[soundSettings[setting]] };
types.forEach(function (type) {
var soundpacks = plugins.soundpacks.map(function (pack) {
var sounds = Object.keys(pack.sounds).map(function (soundName) {
var value = pack.name + ' | ' + soundName;
return {
name: soundName,
value: value,
selected: value === results.soundsMapping[type],
};
});
return {
name: pack.name,
sounds: sounds,
};
});
userData[type + '-sound'] = soundpacks;
// fallback
userData[aliases[type]] = soundpacks.concat.apply([], soundpacks.map(function (pack) {
return pack.sounds.map(function (sound) {
return {
name: sound.value,
selected: sound.selected,
};
});
}));
});
plugins.fireHook('filter:user.customSettings', { settings: results.settings, customSettings: [], uid: req.uid }, next);

View File

@@ -30,6 +30,14 @@ dashboardController.get = function (req, res, next) {
link: '/admin/extend/plugins',
},
];
if (global.env !== 'production') {
notices.push({
done: false,
notDoneText: '[[admin/general/dashboard:running-in-development]]',
});
}
plugins.fireHook('filter:admin.notices', notices, next);
},
}, function (err, results) {

View File

@@ -1,24 +1,46 @@
'use strict';
var meta = require('../../meta');
var plugins = require('../../plugins');
var db = require('../../database');
var soundsController = {};
soundsController.get = function (req, res, next) {
meta.sounds.getFiles(function (err, sounds) {
db.getObject('settings:sounds', function (err, settings) {
if (err) {
return next(err);
}
sounds = Object.keys(sounds).map(function (name) {
return {
name: name,
};
settings = settings || {};
var types = [
'notification',
'chat-incoming',
'chat-outgoing',
];
var output = {};
types.forEach(function (type) {
var soundpacks = plugins.soundpacks.map(function (pack) {
var sounds = Object.keys(pack.sounds).map(function (soundName) {
var value = pack.name + ' | ' + soundName;
return {
name: soundName,
value: value,
selected: value === settings[type],
};
});
return {
name: pack.name,
sounds: sounds,
};
});
output[type + '-sound'] = soundpacks;
});
res.render('admin/general/sounds', {
sounds: sounds,
});
res.render('admin/general/sounds', output);
});
};

View File

@@ -5,6 +5,8 @@ var path = require('path');
var async = require('async');
var nconf = require('nconf');
var winston = require('winston');
var meta = require('../../meta');
var file = require('../../file');
var image = require('../../image');
var plugins = require('../../plugins');
@@ -105,12 +107,7 @@ uploadsController.uploadSound = function (req, res, next) {
return next(err);
}
var soundsPath = path.join(__dirname, '../../../build/public/sounds');
var filePath = path.join(nconf.get('upload_path'), 'sounds', uploadedFile.name);
file.link(filePath, path.join(soundsPath, path.basename(filePath)));
fs.unlink(uploadedFile.path, function (err) {
meta.sounds.build(function (err) {
if (err) {
return next(err);
}

View File

@@ -1,15 +1,13 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var path = require('path');
var fs = require('fs');
var crypto = require('crypto');
var Jimp = require('jimp');
var mime = require('mime');
var winston = require('winston');
var db = require('../database');
var image = require('../image');
var uploadsController = require('../controllers/uploads');
module.exports = function (Groups) {
@@ -35,7 +33,7 @@ module.exports = function (Groups) {
if (tempPath) {
return next(null, tempPath);
}
writeImageDataToFile(data.imageData, next);
image.writeImageDataToTempFile(data.imageData, next);
},
function (_tempPath, next) {
tempPath = _tempPath;
@@ -95,24 +93,6 @@ module.exports = function (Groups) {
});
}
function writeImageDataToFile(imageData, callback) {
// Calculate md5sum of image
// This is required because user data can be private
var md5sum = crypto.createHash('md5');
md5sum.update(imageData);
md5sum = md5sum.digest('hex');
// Save image
var tempPath = path.join(nconf.get('upload_path'), md5sum + '.png');
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(tempPath, buffer, {
encoding: 'base64',
}, function (err) {
callback(err, tempPath);
});
}
Groups.removeCover = function (data, callback) {
db.deleteObjectFields('group:' + data.groupName, ['cover:url', 'cover:thumb:url', 'cover:position'], callback);
};

View File

@@ -1,8 +1,13 @@
'use strict';
var os = require('os');
var fs = require('fs');
var path = require('path');
var Jimp = require('jimp');
var async = require('async');
var crypto = require('crypto');
var file = require('./file');
var plugins = require('./plugins');
var image = module.exports;
@@ -65,9 +70,6 @@ image.resizeImage = function (data, callback) {
}
},
function (image, next) {
if (data.write === false) {
return next();
}
image.write(data.target || data.path, next);
},
], function (err) {
@@ -83,7 +85,7 @@ image.normalise = function (path, extension, callback) {
path: path,
extension: extension,
}, function (err) {
callback(err);
callback(err, path + '.png');
});
} else {
new Jimp(path, function (err, image) {
@@ -91,7 +93,7 @@ image.normalise = function (path, extension, callback) {
return callback(err);
}
image.write(path + '.png', function (err) {
callback(err);
callback(err, path + '.png');
});
});
}
@@ -116,3 +118,28 @@ image.convertImageToBase64 = function (path, callback) {
callback(err, data ? data.toString('base64') : null);
});
};
image.mimeFromBase64 = function (imageData) {
return imageData.slice(5, imageData.indexOf('base64') - 1);
};
image.extensionFromBase64 = function (imageData) {
return file.typeToExtension(image.mimeFromBase64(imageData));
};
image.writeImageDataToTempFile = function (imageData, callback) {
var filename = crypto.createHash('md5').update(imageData).digest('hex');
var type = image.mimeFromBase64(imageData);
var extension = file.typeToExtension(type);
var filepath = path.join(os.tmpdir(), filename + extension);
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(filepath, buffer, {
encoding: 'base64',
}, function (err) {
callback(err, filepath);
});
};

View File

@@ -13,72 +13,76 @@ var sockets = require('../socket.io');
module.exports = function (Messaging) {
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
Messaging.notificationSendDelay = 1000 * 60;
Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) {
if (err) {
return;
}
var data = {
roomId: roomId,
fromUid: fromUid,
message: messageObj,
};
uids.forEach(function (uid) {
data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0;
Messaging.pushUnreadCount(uid);
sockets.in('uid_' + uid).emit('event:chats.receive', data);
});
// Delayed notifications
var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId];
if (queueObj) {
queueObj.message.content += '\n' + messageObj.content;
clearTimeout(queueObj.timeout);
} else {
queueObj = {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, 0, -1, next);
},
function (uids, next) {
var data = {
roomId: roomId,
fromUid: fromUid,
message: messageObj,
};
Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj;
}
queueObj.timeout = setTimeout(function () {
sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) {
if (!err) {
delete Messaging.notifyQueue[fromUid + ':' + roomId];
}
uids.forEach(function (uid) {
data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0;
Messaging.pushUnreadCount(uid);
sockets.in('uid_' + uid).emit('event:chats.receive', data);
});
}, 1000 * 60); // wait 60s before sending
});
// Delayed notifications
var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId];
if (queueObj) {
queueObj.message.content += '\n' + messageObj.content;
clearTimeout(queueObj.timeout);
} else {
queueObj = {
message: messageObj,
};
Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj;
}
queueObj.timeout = setTimeout(function () {
sendNotifications(fromUid, uids, roomId, queueObj.message);
}, Messaging.notificationSendDelay);
next();
},
]);
};
function sendNotifications(fromuid, uids, roomId, messageObj, callback) {
user.isOnline(uids, function (err, isOnline) {
if (err) {
return callback(err);
}
function sendNotifications(fromuid, uids, roomId, messageObj) {
async.waterfall([
function (next) {
user.isOnline(uids, next);
},
function (isOnline, next) {
uids = uids.filter(function (uid, index) {
return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
});
uids = uids.filter(function (uid, index) {
return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
});
if (!uids.length) {
return callback();
}
notifications.create({
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyLong: messageObj.content,
nid: 'chat_' + fromuid + '_' + roomId,
from: fromuid,
path: '/chats/' + messageObj.roomId,
}, function (err, notification) {
if (!err && notification) {
notifications.push(notification, uids, callback);
if (!uids.length) {
return;
}
});
sendNotificationEmails(uids, messageObj);
notifications.create({
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyLong: messageObj.content,
nid: 'chat_' + fromuid + '_' + roomId,
from: fromuid,
path: '/chats/' + messageObj.roomId,
}, next);
},
], function (err, notification) {
if (!err) {
delete Messaging.notifyQueue[fromuid + ':' + roomId];
if (notification) {
notifications.push(notification, uids);
}
sendNotificationEmails(uids, messageObj);
}
});
}
@@ -87,38 +91,39 @@ module.exports = function (Messaging) {
return;
}
async.parallel({
userData: function (next) {
user.getUsersFields(uids, ['uid', 'username', 'userslug'], next);
async.waterfall([
function (next) {
async.parallel({
userData: function (next) {
user.getUsersFields(uids, ['uid', 'username', 'userslug'], next);
},
userSettings: function (next) {
user.getMultipleUserSettings(uids, next);
},
}, next);
},
userSettings: function (next) {
user.getMultipleUserSettings(uids, next);
function (results, next) {
results.userData = results.userData.filter(function (userData, index) {
return userData && results.userSettings[index] && results.userSettings[index].sendChatNotifications;
});
async.each(results.userData, function (userData, next) {
emailer.send('notif_chat', userData.uid, {
subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
message: messageObj,
site_title: meta.config.title || 'NodeBB',
url: nconf.get('url'),
roomId: messageObj.roomId,
username: userData.username,
userslug: userData.userslug,
}, next);
}, next);
},
}, function (err, results) {
], function (err) {
if (err) {
return winston.error(err);
}
results.userData = results.userData.filter(function (userData, index) {
return userData && results.userSettings[index] && results.userSettings[index].sendChatNotifications;
});
async.each(results.userData, function (userData, next) {
emailer.send('notif_chat', userData.uid, {
subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
message: messageObj,
site_title: meta.config.title || 'NodeBB',
url: nconf.get('url'),
roomId: messageObj.roomId,
username: userData.username,
userslug: userData.userslug,
}, next);
}, function (err) {
if (err) {
winston.error(err);
}
});
});
}
};

View File

@@ -3,16 +3,19 @@
var ip = require('ip');
var winston = require('winston');
var async = require('async');
var db = require('../database');
var pubsub = require('../pubsub');
var Blacklist = {
_rules: [],
};
Blacklist.load = function (callback) {
callback = callback || function () {};
async.waterfall([
async.apply(db.get, 'ip-blacklist-rules'),
async.apply(Blacklist.validate),
Blacklist.get,
Blacklist.validate,
], function (err, rules) {
if (err) {
return callback(err);
@@ -33,13 +36,18 @@ Blacklist.load = function (callback) {
});
};
pubsub.on('blacklist:reload', Blacklist.load);
Blacklist.save = function (rules, callback) {
db.set('ip-blacklist-rules', rules, function (err) {
if (err) {
return callback(err);
}
Blacklist.load(callback);
});
async.waterfall([
function (next) {
db.set('ip-blacklist-rules', rules, next);
},
function (next) {
Blacklist.load(next);
pubsub.publish('blacklist:reload');
},
], callback);
};
Blacklist.get = function (callback) {
@@ -48,14 +56,14 @@ Blacklist.get = function (callback) {
Blacklist.test = function (clientIp, callback) {
if (
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
&& Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list
&& !Blacklist._rules.cidr.some(function (subnet) {
Blacklist._rules.ipv4.indexOf(clientIp) === -1 &&// not explicitly specified in ipv4 list
Blacklist._rules.ipv6.indexOf(clientIp) === -1 &&// not explicitly specified in ipv6 list
!Blacklist._rules.cidr.some(function (subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
}) // not in a blacklisted cidr range
) {
if (typeof callback === 'function') {
callback();
setImmediate(callback);
} else {
return false;
}
@@ -64,7 +72,7 @@ Blacklist.test = function (clientIp, callback) {
err.code = 'blacklisted-ip';
if (typeof callback === 'function') {
callback(err);
setImmediate(callback, err);
} else {
return true;
}

View File

@@ -5,7 +5,7 @@ var winston = require('winston');
var buildStart;
var valid = ['js', 'clientCSS', 'acpCSS', 'tpl', 'lang'];
var valid = ['js', 'clientCSS', 'acpCSS', 'tpl', 'lang', 'sound'];
exports.buildAll = function (callback) {
exports.build(valid.join(','), callback);
@@ -46,32 +46,32 @@ exports.buildTargets = function (targets, callback) {
var cacheBuster = require('./cacheBuster');
var meta = require('../meta');
var numCpus = require('os').cpus().length;
var strategy = (targets.length > 1 && numCpus > 1);
var parallel = targets.length > 1 && numCpus > 1;
buildStart = buildStart || Date.now();
var step = function (startTime, target, next, err) {
if (err) {
winston.error('Build failed: ' + err.message);
winston.error('Build failed: ' + err.stack);
process.exit(1);
}
winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's');
next();
};
if (strategy) {
if (parallel) {
winston.verbose('[build] Utilising multiple cores/processes');
} else {
winston.verbose('[build] Utilising single-core');
}
async[strategy ? 'parallel' : 'series']([
async[parallel ? 'parallel' : 'series']([
function (next) {
if (targets.indexOf('js') !== -1) {
winston.info('[build] Building javascript');
var startTime = Date.now();
async.series([
meta.js.linkModules,
meta.js.buildModules,
meta.js.linkStatics,
async.apply(meta.js.minify, 'nodebb.min.js'),
async.apply(meta.js.minify, 'acp.min.js'),
@@ -111,6 +111,12 @@ exports.buildTargets = function (targets, callback) {
meta.languages.build(step.bind(this, startTime, target, next));
break;
case 'sound':
winston.info('[build] Linking sound files');
startTime = Date.now();
meta.sounds.build(step.bind(this, startTime, target, next));
break;
default:
winston.warn('[build] Unknown build target: \'' + target + '\'');
setImmediate(next);

View File

@@ -31,7 +31,9 @@ module.exports = function (Meta) {
'@import "../../public/less/generics.less";',
'@import "../../public/less/mixins.less";',
'@import "../../public/less/global.less";',
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
].map(function (str) {
return str.replace(/\//g, path.sep);
}).join('\n');
},
admin: function (source) {
return source + '\n' + [
@@ -41,7 +43,10 @@ module.exports = function (Meta) {
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
'@import (inline) "../public/vendor/mdl/material.css";',
].map(function (str) {
return str.replace(/\//g, path.sep);
}).join('\n');
},
};

View File

@@ -7,11 +7,14 @@ var async = require('async');
var fs = require('fs');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var uglifyjs = require('uglify-js');
var file = require('../file');
var plugins = require('../plugins');
var utils = require('../../public/src/utils');
var minifierPath = path.join(__dirname, 'minifier.js');
module.exports = function (Meta) {
Meta.js = {
target: {},
@@ -78,7 +81,7 @@ module.exports = function (Meta) {
'public/src/modules/string.js',
],
// modules listed below are routed through express (/src/modules) so they can be defined anonymously
// modules listed below are built (/src/modules) so they can be defined anonymously
modules: {
'Chart.js': './node_modules/chart.js/dist/Chart.min.js',
'mousetrap.js': './node_modules/mousetrap/mousetrap.min.js',
@@ -89,24 +92,123 @@ module.exports = function (Meta) {
},
};
Meta.js.linkModules = function (callback) {
rimraf(path.join(__dirname, '../../build/public/src/modules'), function (err) {
if (err) {
return callback(err);
}
async.eachLimit(Object.keys(Meta.js.scripts.modules), 1000, function (relPath, next) {
var filePath = path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]);
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
function minifyModules(modules, callback) {
async.eachLimit(modules, 500, function (mod, next) {
var filePath = mod.filePath;
var destPath = mod.destPath;
var minified;
mkdirp(path.dirname(destPath), function (err) {
if (err) {
return next(err);
}
async.parallel([
function (cb) {
mkdirp(path.dirname(destPath), cb);
},
function (cb) {
fs.readFile(filePath, function (err, buffer) {
if (err) {
return cb(err);
}
try {
minified = uglifyjs.minify(buffer.toString(), {
fromString: true,
compress: false,
});
} catch (e) {
return cb(e);
}
file.link(filePath, destPath, next);
});
}, callback);
cb();
});
},
], function (err) {
if (err) {
return next(err);
}
fs.writeFile(destPath, minified.code, next);
});
}, callback);
}
function linkModules(callback) {
var modules = Meta.js.scripts.modules;
async.eachLimit(Object.keys(modules), 1000, function (relPath, next) {
var filePath = path.join(__dirname, '../../', modules[relPath]);
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
mkdirp(path.dirname(destPath), function (err) {
if (err) {
return next(err);
}
file.link(filePath, destPath, next);
});
}, callback);
}
var moduleDirs = ['modules', 'admin', 'client'];
function getModuleList(callback) {
var modules = Object.keys(Meta.js.scripts.modules).map(function (relPath) {
return {
filePath: path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]),
destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
};
});
var dirs = moduleDirs.map(function (dir) {
return path.join(__dirname, '../../public/src', dir);
});
async.each(dirs, function (dir, next) {
utils.walk(dir, function (err, files) {
if (err) {
return next(err);
}
var mods = files.filter(function (filePath) {
return path.extname(filePath) === '.js';
}).map(function (filePath) {
return {
filePath: filePath,
destPath: path.join(__dirname, '../../build/public/src', path.relative(path.dirname(dir), filePath)),
};
});
modules = modules.concat(mods);
next();
});
}, function (err) {
callback(err, modules);
});
}
function clearModules(callback) {
var builtPaths = moduleDirs.map(function (p) {
return '../../build/public/src/' + p;
});
async.each(builtPaths, function (builtPath, next) {
rimraf(path.join(__dirname, builtPath), next);
}, function (err) {
callback(err);
});
}
Meta.js.buildModules = function (callback) {
async.waterfall([
clearModules,
function (next) {
if (global.env === 'development') {
return linkModules(callback);
}
getModuleList(next);
},
function (modules, next) {
minifyModules(modules, next);
},
], callback);
};
Meta.js.linkStatics = function (callback) {
@@ -133,7 +235,7 @@ module.exports = function (Meta) {
winston.verbose('[meta/js] Minifying ' + target);
var forkProcessParams = setupDebugging();
var minifier = fork('minifier.js', [], forkProcessParams);
var minifier = fork(minifierPath, [], forkProcessParams);
Meta.js.minifierProc = minifier;
Meta.js.target[target] = {};

80
src/meta/minifier.js Normal file
View File

@@ -0,0 +1,80 @@
'use strict';
var uglifyjs = require('uglify-js');
var async = require('async');
var fs = require('fs');
var file = require('../file');
var Minifier = {
js: {},
};
/* Javascript */
Minifier.js.minify = function (scripts, minify, callback) {
scripts = scripts.filter(function (file) {
return file && file.endsWith('.js');
});
async.filter(scripts, function (script, next) {
file.exists(script, function (exists) {
if (!exists) {
console.warn('[minifier] file not found, ' + script);
}
next(exists);
});
}, function (scripts) {
if (minify) {
minifyScripts(scripts, callback);
} else {
concatenateScripts(scripts, callback);
}
});
};
process.on('message', function (payload) {
switch (payload.action) {
case 'js':
Minifier.js.minify(payload.scripts, payload.minify, function (minified/* , sourceMap*/) {
process.send({
type: 'end',
// sourceMap: sourceMap,
minified: minified,
});
});
break;
}
});
function minifyScripts(scripts, callback) {
// The portions of code involving the source map are commented out as they're broken in UglifyJS2
// Follow along here: https://github.com/mishoo/UglifyJS2/issues/700
try {
var minified = uglifyjs.minify(scripts, {
// outSourceMap: "nodebb.min.js.map",
compress: false,
});
callback(minified.code/* , minified.map*/);
} catch (err) {
process.send({
type: 'error',
message: err.message,
});
}
}
function concatenateScripts(scripts, callback) {
async.map(scripts, fs.readFile, function (err, scripts) {
if (err) {
process.send({
type: 'error',
message: err.message,
});
return;
}
scripts = scripts.join(require('os').EOL + ';');
callback(scripts);
});
}

View File

@@ -2,68 +2,102 @@
var path = require('path');
var fs = require('fs');
var nconf = require('nconf');
var winston = require('winston');
var rimraf = require('rimraf');
var mkdirp = require('mkdirp');
var async = require('async');
var file = require('../file');
var plugins = require('../plugins');
var user = require('../user');
var db = require('../database');
var soundsPath = path.join(__dirname, '../../build/public/sounds');
var uploadsPath = path.join(__dirname, '../../public/uploads/sounds');
module.exports = function (Meta) {
Meta.sounds = {};
Meta.sounds.init = function (callback) {
if (nconf.get('isPrimary') === 'true') {
setupSounds(callback);
} else if (typeof callback === 'function') {
callback();
}
};
Meta.sounds.getFiles = function (callback) {
async.waterfall([
function (next) {
fs.readdir(path.join(__dirname, '../../build/public/sounds'), next);
},
function (sounds, next) {
fs.readdir(path.join(nconf.get('upload_path'), 'sounds'), function (err, uploaded) {
if (err) {
if (err.code === 'ENOENT') {
return next(null, sounds);
}
return next(err);
}
next(null, sounds.concat(uploaded));
});
},
], function (err, files) {
Meta.sounds.addUploads = function addUploads(callback) {
fs.readdir(uploadsPath, function (err, files) {
if (err) {
winston.error('Could not get local sound files:' + err.message);
console.log(err.stack);
return callback(null, []);
if (err.code !== 'ENOENT') {
return callback(err);
}
files = [];
}
var localList = {};
var uploadSounds = files.reduce(function (prev, fileName) {
var name = fileName.split('.');
if (!name.length || !name[0].length) {
return prev;
}
name = name[0];
name = name[0].toUpperCase() + name.slice(1);
// Filter out hidden files
files = files.filter(function (filename) {
return !filename.startsWith('.');
prev[name] = fileName;
return prev;
}, {});
plugins.soundpacks = plugins.soundpacks.filter(function (pack) {
return pack.name !== 'Uploads';
});
if (Object.keys(uploadSounds).length) {
plugins.soundpacks.push({
name: 'Uploads',
id: 'uploads',
dir: uploadsPath,
sounds: uploadSounds,
});
}
// Return proper paths
files.forEach(function (filename) {
localList[filename] = nconf.get('relative_path') + '/assets/sounds/' + filename;
});
callback(null, localList);
callback();
});
};
Meta.sounds.getMapping = function (uid, callback) {
var user = require('../user');
Meta.sounds.build = function build(callback) {
Meta.sounds.addUploads(function (err) {
if (err) {
return callback(err);
}
var map = plugins.soundpacks.map(function (pack) {
return Object.keys(pack.sounds).reduce(function (prev, soundName) {
var soundPath = pack.sounds[soundName];
prev[pack.name + ' | ' + soundName] = pack.id + '/' + soundPath;
return prev;
}, {});
});
map.unshift({});
map = Object.assign.apply(null, map);
async.series([
function (next) {
rimraf(soundsPath, next);
},
function (next) {
mkdirp(soundsPath, next);
},
function (cb) {
async.parallel([
function (next) {
fs.writeFile(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map), next);
},
function (next) {
async.each(plugins.soundpacks, function (pack, next) {
file.linkDirs(pack.dir, path.join(soundsPath, pack.id), next);
}, next);
},
], cb);
},
], callback);
});
};
var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
Meta.sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
async.parallel({
defaultMapping: function (next) {
db.getObject('settings:sounds', next);
@@ -75,82 +109,25 @@ module.exports = function (Meta) {
if (err) {
return callback(err);
}
var userSettings = results.userSettings;
userSettings = {
notification: userSettings.notificationSound,
'chat-incoming': userSettings.incomingChatSound,
'chat-outgoing': userSettings.outgoingChatSound,
};
var defaultMapping = results.defaultMapping || {};
var soundMapping = {};
soundMapping.notification = (userSettings.notificationSound || userSettings.notificationSound === '') ?
userSettings.notificationSound : defaultMapping.notification || '';
soundMapping['chat-incoming'] = (userSettings.incomingChatSound || userSettings.incomingChatSound === '') ?
userSettings.incomingChatSound : defaultMapping['chat-incoming'] || '';
soundMapping['chat-outgoing'] = (userSettings.outgoingChatSound || userSettings.outgoingChatSound === '') ?
userSettings.outgoingChatSound : defaultMapping['chat-outgoing'] || '';
keys.forEach(function (key) {
if (userSettings[key] || userSettings[key] === '') {
soundMapping[key] = userSettings[key] || null;
} else {
soundMapping[key] = defaultMapping[key] || null;
}
});
callback(null, soundMapping);
});
};
function setupSounds(callback) {
var soundsPath = path.join(__dirname, '../../build/public/sounds');
async.waterfall([
function (next) {
fs.readdir(path.join(nconf.get('upload_path'), 'sounds'), function (err, files) {
if (err) {
if (err.code === 'ENOENT') {
return next(null, []);
}
return next(err);
}
next(null, files);
});
},
function (uploaded, next) {
uploaded = uploaded.filter(function (filename) {
return !filename.startsWith('.');
}).map(function (filename) {
return path.join(nconf.get('upload_path'), 'sounds', filename);
});
plugins.fireHook('filter:sounds.get', uploaded, function (err, filePaths) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Clear the sounds directory
async.series([
function (next) {
rimraf(soundsPath, next);
},
function (next) {
mkdirp(soundsPath, next);
},
], function (err) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Link paths
async.each(filePaths, function (filePath, next) {
file.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
}, function (err) {
if (!err) {
winston.verbose('[sounds] Sounds OK');
} else {
winston.error('[sounds] Could not initialise sounds: ' + err.message);
}
if (typeof next === 'function') {
next();
}
});
});
});
},
], callback);
}
};

View File

@@ -59,114 +59,122 @@ module.exports = function (middleware) {
templateValues.configJSON = JSON.stringify(res.locals.config);
async.parallel({
scripts: function (next) {
plugins.fireHook('filter:scripts.get', [], next);
async.waterfall([
function (next) {
async.parallel({
scripts: function (next) {
plugins.fireHook('filter:scripts.get', [], next);
},
isAdmin: function (next) {
user.isAdministrator(req.uid, next);
},
isGlobalMod: function (next) {
user.isGlobalModerator(req.uid, next);
},
isModerator: function (next) {
user.isModeratorOfAnyCategory(req.uid, next);
},
user: function (next) {
var userData = {
uid: 0,
username: '[[global:guest]]',
userslug: '',
email: '',
picture: meta.config.defaultAvatar,
status: 'offline',
reputation: 0,
'email:confirmed': false,
};
if (req.uid) {
user.getUserFields(req.uid, Object.keys(userData), next);
} else {
next(null, userData);
}
},
isEmailConfirmSent: function (next) {
if (!meta.config.requireEmailConfirmation || !req.uid) {
return next(null, false);
}
db.get('uid:' + req.uid + ':confirm:email:sent', next);
},
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
banned: async.apply(user.isBanned, req.uid),
banReason: async.apply(user.getBannedReason, req.uid),
}, next);
},
isAdmin: function (next) {
user.isAdministrator(req.uid, next);
},
isGlobalMod: function (next) {
user.isGlobalModerator(req.uid, next);
},
isModerator: function (next) {
user.isModeratorOfAnyCategory(req.uid, next);
},
user: function (next) {
var userData = {
uid: 0,
username: '[[global:guest]]',
userslug: '',
email: '',
picture: meta.config.defaultAvatar,
status: 'offline',
reputation: 0,
'email:confirmed': false,
};
if (req.uid) {
user.getUserFields(req.uid, Object.keys(userData), next);
} else {
next(null, userData);
}
},
isEmailConfirmSent: function (next) {
if (!meta.config.requireEmailConfirmation || !req.uid) {
return next(null, false);
}
db.get('uid:' + req.uid + ':confirm:email:sent', next);
},
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
banned: async.apply(user.isBanned, req.uid),
banReason: async.apply(user.getBannedReason, req.uid),
}, function (err, results) {
if (err) {
return callback(err);
}
if (results.banned) {
req.logout();
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
}
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
results.user.isMod = !!results.isModerator;
results.user.uid = parseInt(results.user.uid, 10);
results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
if (res.locals.config && parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') {
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
}
templateValues.browserTitle = controllers.helpers.buildTitle(data.title);
templateValues.navigation = results.navigation;
templateValues.metaTags = results.tags.meta;
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod;
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
templateValues.user = results.user;
templateValues.userJSON = JSON.stringify(results.user);
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : '';
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;
templateValues.template = { name: res.locals.template };
templateValues.template[res.locals.template] = true;
templateValues.scripts = results.scripts.map(function (script) {
return { src: script };
});
if (req.route && req.route.path === '/') {
modifyTitle(templateValues);
}
plugins.fireHook('filter:middleware.renderHeader', { templateValues: templateValues, req: req, res: res }, function (err, data) {
if (err) {
return callback(err);
function (results, next) {
if (results.banned) {
req.logout();
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
}
req.app.render('header', data.templateValues, callback);
});
});
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
results.user.isMod = !!results.isModerator;
results.user.uid = parseInt(results.user.uid, 10);
results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
if (res.locals.config && parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') {
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
}
templateValues.browserTitle = controllers.helpers.buildTitle(data.title);
templateValues.navigation = results.navigation;
templateValues.metaTags = results.tags.meta;
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod;
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
templateValues.user = results.user;
templateValues.userJSON = JSON.stringify(results.user);
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : '';
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;
templateValues.template = { name: res.locals.template };
templateValues.template[res.locals.template] = true;
templateValues.scripts = results.scripts.map(function (script) {
return { src: script };
});
if (req.route && req.route.path === '/') {
modifyTitle(templateValues);
}
plugins.fireHook('filter:middleware.renderHeader', {
req: req,
res: res,
templateValues: templateValues,
}, next);
},
function (data, next) {
req.app.render('header', data.templateValues, next);
},
], callback);
};
middleware.renderFooter = function (req, res, data, callback) {
plugins.fireHook('filter:middleware.renderFooter', { templateValues: data, req: req, res: res }, function (err, data) {
if (err) {
return callback(err);
}
req.app.render('footer', data.templateValues, callback);
});
async.waterfall([
function (next) {
plugins.fireHook('filter:middleware.renderFooter', {
req: req,
res: res,
templateValues: data,
}, next);
},
function (data, next) {
req.app.render('footer', data.templateValues, next);
},
], callback);
};
function modifyTitle(obj) {

View File

@@ -1,13 +1,12 @@
'use strict';
var async = require('async');
var plugins = require('../plugins');
var db = require('../database');
var translator = require('../../public/src/modules/translator');
var pubsub = require('../pubsub');
var admin = {};
var admin = module.exports;
admin.cache = null;
pubsub.on('admin:navigation:save', function () {
@@ -70,5 +69,3 @@ function getAvailable(callback) {
plugins.fireHook('filter:navigation.available', core, callback);
}
module.exports = admin;

View File

@@ -32,6 +32,7 @@ var middleware;
Plugins.libraryPaths = [];
Plugins.versionWarning = [];
Plugins.languageCodes = [];
Plugins.soundpacks = [];
Plugins.initialized = false;

View File

@@ -41,6 +41,7 @@ module.exports = function (Plugins) {
Plugins.lessFiles.length = 0;
Plugins.clientScripts.length = 0;
Plugins.acpScripts.length = 0;
Plugins.soundpacks.length = 0;
async.waterfall([
async.apply(Plugins.getPluginPaths),
@@ -57,6 +58,7 @@ module.exports = function (Plugins) {
async.apply(mapClientSideScripts, pluginData),
async.apply(mapClientModules, pluginData),
async.apply(mapStaticDirectories, pluginData, pluginData.path),
async.apply(mapSoundpack, pluginData),
], next);
}, next);
},
@@ -93,6 +95,9 @@ module.exports = function (Plugins) {
function (next) {
mapClientModules(pluginData, next);
},
function (next) {
mapSoundpack(pluginData, next);
},
], function (err) {
if (err) {
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
@@ -251,6 +256,35 @@ module.exports = function (Plugins) {
callback();
}
function mapSoundpack(pluginData, callback) {
var soundpack = pluginData.soundpack;
if (!soundpack || !soundpack.dir || !soundpack.sounds) {
return callback();
}
soundpack.name = soundpack.name || pluginData.name;
soundpack.id = pluginData.id;
soundpack.dir = path.join(pluginData.path, soundpack.dir);
async.each(Object.keys(soundpack.sounds), function (key, next) {
file.exists(path.join(soundpack.dir, soundpack.sounds[key]), function (exists) {
if (!exists) {
delete soundpack.sounds[key];
}
next();
});
}, function (err) {
if (err) {
return callback(err);
}
if (Object.keys(soundpack.sounds).length) {
Plugins.soundpacks.push(soundpack);
}
callback();
});
}
function resolveModulePath(fullPath, relPath) {
/**
* With npm@3, dependencies can become flattened, and appear at the root level.

View File

@@ -33,6 +33,28 @@ Sockets.init = function (server) {
io.on('connection', onConnection);
/*
* Restrict socket.io listener to cookie domain. If none is set, infer based on url.
* Production only so you don't get accidentally locked out.
* Can be overridden via config (socket.io:origins)
*/
if (process.env.NODE_ENV !== 'development') {
var domain = nconf.get('cookieDomain');
var parsedUrl = url.parse(nconf.get('url'));
var override = nconf.get('socket.io:origins');
if (!domain) {
domain = parsedUrl.hostname; // cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353
}
if (!override) {
io.origins(parsedUrl.protocol + '//' + domain + ':*');
winston.info('[socket.io] Restricting access to origin: ' + parsedUrl.protocol + '//' + domain + ':*');
} else {
io.origins(override);
winston.info('[socket.io] Restricting access to origin: ' + override);
}
}
io.listen(server, {
transports: nconf.get('socket.io:transports'),
});

View File

@@ -342,20 +342,8 @@ SocketModules.chats.getMessages = function (socket, data, callback) {
};
/* Sounds */
SocketModules.sounds.getSounds = function (socket, data, callback) {
// Read sounds from local directory
meta.sounds.getFiles(callback);
};
SocketModules.sounds.getMapping = function (socket, data, callback) {
meta.sounds.getMapping(socket.uid, callback);
};
SocketModules.sounds.getData = function (socket, data, callback) {
async.parallel({
mapping: async.apply(meta.sounds.getMapping, socket.uid),
files: async.apply(meta.sounds.getFiles),
}, callback);
SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) {
meta.sounds.getUserSoundMap(socket.uid, callback);
};
module.exports = SocketModules;

View File

@@ -1,6 +1,6 @@
'use strict';
var async = require('async');
var async = require('async');
var posts = require('../posts');
var privileges = require('../privileges');
@@ -31,25 +31,26 @@ SocketPosts.reply = function (socket, data, callback) {
data.req = websockets.reqFromSocket(socket);
data.timestamp = Date.now();
topics.reply(data, function (err, postData) {
if (err) {
return callback(err);
}
async.waterfall([
function (next) {
topics.reply(data, next);
},
function (postData, next) {
var result = {
posts: [postData],
'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1,
'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1,
};
var result = {
posts: [postData],
'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1,
'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1,
};
next(null, postData);
callback(null, postData);
websockets.in('uid_' + socket.uid).emit('event:new_post', result);
websockets.in('uid_' + socket.uid).emit('event:new_post', result);
user.updateOnlineUsers(socket.uid);
user.updateOnlineUsers(socket.uid);
socketHelpers.notifyNew(socket.uid, 'newPost', result);
});
socketHelpers.notifyNew(socket.uid, 'newPost', result);
},
], callback);
};
SocketPosts.getRawPost = function (socket, pid, callback) {
@@ -120,7 +121,7 @@ SocketPosts.getPidIndex = function (socket, data, callback) {
SocketPosts.getReplies = function (socket, pid, callback) {
if (!utils.isNumber(pid)) {
return callback(new Error('[[error:invalid-data]'));
return callback(new Error('[[error:invalid-data]]'));
}
var postPrivileges;
async.waterfall([

View File

@@ -272,34 +272,34 @@ SocketUser.invite = function (socket, email, callback) {
return callback(new Error('[[error:forum-not-invite-only]]'));
}
var max = meta.config.maximumInvites;
async.waterfall([
function (next) {
user.isAdministrator(socket.uid, next);
},
function (isAdmin, next) {
if (registrationType === 'admin-invite-only' && !isAdmin) {
return next(new Error('[[error:no-privileges]]'));
}
var max = parseInt(meta.config.maximumInvites, 10);
if (!max) {
return user.sendInvitationEmail(socket.uid, email, callback);
}
user.isAdministrator(socket.uid, function (err, admin) {
if (err) {
return callback(err);
}
if (registrationType === 'admin-invite-only' && !admin) {
return callback(new Error('[[error:no-privileges]]'));
}
if (max) {
async.waterfall([
function (next) {
user.getInvitesNumber(socket.uid, next);
},
function (invites, next) {
if (!admin && invites > max) {
if (!isAdmin && invites >= max) {
return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'));
}
next();
},
function (next) {
user.sendInvitationEmail(socket.uid, email, next);
},
], callback);
} else {
user.sendInvitationEmail(socket.uid, email, callback);
}
});
], next);
},
], callback);
};
SocketUser.getUserByUID = function (socket, uid, callback) {

View File

@@ -27,23 +27,23 @@ module.exports = function (Topics) {
};
Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) {
callback = callback || function () {};
async.parallel({
posts: function (next) {
posts.getPostsFromSet(set, start, stop, uid, reverse, next);
async.waterfall([
function (next) {
async.parallel({
posts: function (next) {
posts.getPostsFromSet(set, start, stop, uid, reverse, next);
},
postCount: function (next) {
Topics.getTopicField(tid, 'postcount', next);
},
}, next);
},
postCount: function (next) {
Topics.getTopicField(tid, 'postcount', next);
function (results, next) {
Topics.calculatePostIndices(results.posts, start, stop, results.postCount, reverse);
Topics.addPostData(results.posts, uid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
Topics.calculatePostIndices(results.posts, start, stop, results.postCount, reverse);
Topics.addPostData(results.posts, uid, callback);
});
], callback);
};
Topics.addPostData = function (postData, uid, callback) {
@@ -58,88 +58,81 @@ module.exports = function (Topics) {
return callback(null, []);
}
async.parallel({
bookmarks: function (next) {
posts.hasBookmarked(pids, uid, next);
},
voteData: function (next) {
posts.getVoteStatusByPostIDs(pids, uid, next);
},
userData: function (next) {
var uids = [];
function getPostUserData(field, method, callback) {
var uids = [];
for (var i = 0; i < postData.length; i += 1) {
if (postData[i] && uids.indexOf(postData[i].uid) === -1) {
uids.push(postData[i].uid);
}
postData.forEach(function (postData) {
if (postData && postData[field] && uids.indexOf(postData[field]) === -1) {
uids.push(postData[field]);
}
});
posts.getUserInfoForPosts(uids, uid, function (err, users) {
if (err) {
return next(err);
}
async.waterfall([
function (next) {
method(uids, next);
},
function (users, next) {
var userData = {};
users.forEach(function (user, index) {
userData[uids[index]] = user;
});
next(null, userData);
},
], callback);
}
async.waterfall([
function (next) {
async.parallel({
bookmarks: function (next) {
posts.hasBookmarked(pids, uid, next);
},
voteData: function (next) {
posts.getVoteStatusByPostIDs(pids, uid, next);
},
userData: function (next) {
getPostUserData('uid', function (uids, next) {
posts.getUserInfoForPosts(uids, uid, next);
}, next);
},
editors: function (next) {
getPostUserData('editor', function (uids, next) {
user.getUsersFields(uids, ['uid', 'username', 'userslug'], next);
}, next);
},
parents: function (next) {
Topics.addParentPosts(postData, next);
},
}, next);
},
function (results, next) {
postData.forEach(function (postObj, i) {
if (postObj) {
postObj.deleted = parseInt(postObj.deleted, 10) === 1;
postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]);
postObj.editor = postObj.editor ? results.editors[postObj.editor] : null;
postObj.bookmarked = results.bookmarks[i];
postObj.upvoted = results.voteData.upvotes[i];
postObj.downvoted = results.voteData.downvotes[i];
postObj.votes = postObj.votes || 0;
postObj.replies = postObj.replies || 0;
postObj.selfPost = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(postObj.uid, 10);
// Username override for guests, if enabled
if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) {
postObj.user.username = validator.escape(String(postObj.handle));
}
}
});
plugins.fireHook('filter:topics.addPostData', {
posts: postData,
uid: uid,
}, next);
},
editors: function (next) {
var editors = [];
for (var i = 0; i < postData.length; i += 1) {
if (postData[i] && postData[i].editor && editors.indexOf(postData[i].editor) === -1) {
editors.push(postData[i].editor);
}
}
user.getUsersFields(editors, ['uid', 'username', 'userslug'], function (err, editors) {
if (err) {
return next(err);
}
var editorData = {};
editors.forEach(function (editor) {
editorData[editor.uid] = editor;
});
next(null, editorData);
});
function (data, next) {
next(null, data.posts);
},
parents: function (next) {
Topics.addParentPosts(postData, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
postData.forEach(function (postObj, i) {
if (postObj) {
postObj.deleted = parseInt(postObj.deleted, 10) === 1;
postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]);
postObj.editor = postObj.editor ? results.editors[postObj.editor] : null;
postObj.bookmarked = results.bookmarks[i];
postObj.upvoted = results.voteData.upvotes[i];
postObj.downvoted = results.voteData.downvotes[i];
postObj.votes = postObj.votes || 0;
postObj.replies = postObj.replies || 0;
postObj.selfPost = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(postObj.uid, 10);
// Username override for guests, if enabled
if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) {
postObj.user.username = validator.escape(String(postObj.handle));
}
}
});
plugins.fireHook('filter:topics.addPostData', {
posts: postData,
uid: uid,
}, function (err, data) {
callback(err, data ? data.posts : null);
});
});
], callback);
};
Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) {
@@ -172,7 +165,9 @@ module.exports = function (Topics) {
async.apply(posts.getPostsFields, parentPids, ['uid']),
function (_parentPosts, next) {
parentPosts = _parentPosts;
var parentUids = parentPosts.map(function (postObj) { return parseInt(postObj.uid, 10); }).filter(function (uid, idx, users) {
var parentUids = parentPosts.map(function (postObj) {
return parseInt(postObj.uid, 10);
}).filter(function (uid, idx, users) {
return users.indexOf(uid) === idx;
});
@@ -231,31 +226,31 @@ module.exports = function (Topics) {
var done = false;
var latestPid = null;
var index = 0;
var pids;
async.doWhilst(
function (next) {
db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, function (err, pids) {
if (err) {
return next(err);
}
if (!Array.isArray(pids) || !pids.length) {
done = true;
return next();
}
posts.getPostField(pids[0], 'deleted', function (err, deleted) {
if (err) {
return next(err);
async.waterfall([
function (_next) {
db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, _next);
},
function (_pids, _next) {
pids = _pids;
if (!Array.isArray(pids) || !pids.length) {
done = true;
return next();
}
posts.getPostField(pids[0], 'deleted', _next);
},
function (deleted, _next) {
isDeleted = parseInt(deleted, 10) === 1;
if (!isDeleted) {
latestPid = pids[0];
}
index += 1;
next();
});
});
_next();
},
], next);
},
function () {
return isDeleted && !done;
@@ -317,22 +312,24 @@ module.exports = function (Topics) {
};
Topics.getPids = function (tid, callback) {
async.parallel({
mainPid: function (next) {
Topics.getTopicField(tid, 'mainPid', next);
async.waterfall([
function (next) {
async.parallel({
mainPid: function (next) {
Topics.getTopicField(tid, 'mainPid', next);
},
pids: function (next) {
db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next);
},
}, next);
},
pids: function (next) {
db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next);
function (results, next) {
if (results.mainPid) {
results.pids = [results.mainPid].concat(results.pids);
}
next(null, results.pids);
},
}, function (err, results) {
if (err) {
return callback(err);
}
if (results.mainPid) {
results.pids = [results.mainPid].concat(results.pids);
}
callback(null, results.pids);
});
], callback);
};
Topics.increasePostCount = function (tid, callback) {
@@ -349,12 +346,14 @@ module.exports = function (Topics) {
function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) {
callback = callback || function () {};
db.incrObjectFieldBy('topic:' + tid, field, by, function (err, value) {
if (err) {
return callback(err);
}
db.sortedSetAdd(set, value, tid, callback);
});
async.waterfall([
function (next) {
db.incrObjectFieldBy('topic:' + tid, field, by, next);
},
function (value, next) {
db.sortedSetAdd(set, value, tid, next);
},
], callback);
}
Topics.getTitleByPid = function (pid, callback) {
@@ -362,21 +361,25 @@ module.exports = function (Topics) {
};
Topics.getTopicFieldByPid = function (field, pid, callback) {
posts.getPostField(pid, 'tid', function (err, tid) {
if (err) {
return callback(err);
}
Topics.getTopicField(tid, field, callback);
});
async.waterfall([
function (next) {
posts.getPostField(pid, 'tid', next);
},
function (tid, next) {
Topics.getTopicField(tid, field, next);
},
], callback);
};
Topics.getTopicDataByPid = function (pid, callback) {
posts.getPostField(pid, 'tid', function (err, tid) {
if (err) {
return callback(err);
}
Topics.getTopicData(tid, callback);
});
async.waterfall([
function (next) {
posts.getPostField(pid, 'tid', next);
},
function (tid, next) {
Topics.getTopicData(tid, next);
},
], callback);
};
Topics.getPostCount = function (tid, callback) {

View File

@@ -60,9 +60,7 @@ module.exports = function (User) {
if (exists) {
return next(new Error('[[error:email-taken]]'));
}
next();
},
function (next) {
async.parallel([
function (next) {
db.setAdd('invitation:uid:' + uid, email, next);
@@ -130,10 +128,10 @@ module.exports = function (User) {
return next(new Error('[[error:invalid-username]]'));
}
async.parallel([
function deleteFromReferenceList(next) {
db.setRemove('invitation:uid:' + invitedByUid, email, next);
function (next) {
deleteFromReferenceList(invitedByUid, email, next);
},
function deleteInviteKey(next) {
function (next) {
db.delete('invitation:email:' + email, next);
},
], function (err) {
@@ -145,6 +143,36 @@ module.exports = function (User) {
User.deleteInvitationKey = function (email, callback) {
callback = callback || function () {};
db.delete('invitation:email:' + email, callback);
async.waterfall([
function (next) {
User.getInvitingUsers(next);
},
function (uids, next) {
async.each(uids, function (uid, next) {
deleteFromReferenceList(uid, email, next);
}, next);
},
function (next) {
db.delete('invitation:email:' + email, next);
},
], callback);
};
function deleteFromReferenceList(uid, email, callback) {
async.waterfall([
function (next) {
db.setRemove('invitation:uid:' + uid, email, next);
},
function (next) {
db.setCount('invitation:uid:' + uid, next);
},
function (count, next) {
if (count === 0) {
return db.setRemove('invitation:uids', uid, next);
}
setImmediate(next);
},
], callback);
}
};

View File

@@ -1,11 +1,8 @@
'use strict';
var async = require('async');
var path = require('path');
var fs = require('fs');
var os = require('os');
var nconf = require('nconf');
var crypto = require('crypto');
var winston = require('winston');
var request = require('request');
var mime = require('mime');
@@ -18,77 +15,7 @@ var db = require('../database');
module.exports = function (User) {
User.uploadPicture = function (uid, picture, callback) {
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
var extension = path.extname(picture.name);
var updateUid = uid;
var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
var uploadedImage;
if (parseInt(meta.config.allowProfileImageUploads, 10) !== 1) {
return callback(new Error('[[error:profile-image-uploads-disabled]]'));
}
if (picture.size > uploadSize * 1024) {
return callback(new Error('[[error:file-too-big, ' + uploadSize + ']]'));
}
if (!extension) {
return callback(new Error('[[error:invalid-image-extension]]'));
}
async.waterfall([
function (next) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {
image: picture,
uid: updateUid,
}, next);
}
var filename = updateUid + '-profileimg' + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension);
async.waterfall([
function (next) {
file.isFileTypeAllowed(picture.path, next);
},
function (next) {
image.resizeImage({
path: picture.path,
extension: extension,
width: imageDimension,
height: imageDimension,
write: false,
}, next);
},
function (next) {
if (!convertToPNG) {
return next();
}
async.series([
async.apply(image.normalise, picture.path, extension),
async.apply(fs.rename, picture.path + '.png', picture.path),
], function (err) {
next(err);
});
},
function (next) {
file.saveFileToLocal(filename, 'profile', picture.path, next);
},
], next);
},
function (_image, next) {
uploadedImage = _image;
User.setUserFields(updateUid, {
uploadedpicture: uploadedImage.url,
picture: uploadedImage.url,
}, next);
},
function (next) {
next(null, uploadedImage);
},
], callback);
User.uploadCroppedPicture({ uid: uid, file: picture }, callback);
};
User.uploadFromUrl = function (uid, url, callback) {
@@ -139,7 +66,7 @@ module.exports = function (User) {
User.updateCoverPicture = function (data, callback) {
var url;
var image = {
var picture = {
name: 'profileCover',
uid: data.uid,
};
@@ -164,12 +91,14 @@ module.exports = function (User) {
return setImmediate(next, null, data.file.path);
}
saveImageDataToTempFile(data.imageData, next);
image.writeImageDataToTempFile(data.imageData, next);
},
function (path, next) {
image.path = path;
picture.path = path;
uploadProfileOrCover('profilecover', image, data.imageData, next);
var extension = data.file ? file.typeToExtension(data.file.type) : image.extensionFromBase64(data.imageData);
var filename = generateProfileImageFilename(data.uid, 'profilecover', extension);
uploadProfileOrCover(filename, picture, next);
},
function (uploadData, next) {
url = uploadData.url;
@@ -183,7 +112,7 @@ module.exports = function (User) {
}
},
], function (err) {
deleteFile(image.path);
deleteFile(picture.path);
callback(err, {
url: url,
});
@@ -191,75 +120,102 @@ module.exports = function (User) {
};
User.uploadCroppedPicture = function (data, callback) {
var url;
var image = {
if (parseInt(meta.config.allowProfileImageUploads, 10) !== 1) {
return callback(new Error('[[error:profile-image-uploads-disabled]]'));
}
if (!data.imageData && !data.file) {
return callback(new Error('[[error:invalid-data]]'));
}
var size = data.file ? data.file.size : data.imageData.length;
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
if (size > uploadSize * 1024) {
return callback(new Error('[[error:file-too-big, ' + meta.config.maximumProfileImageSize + ']]'));
}
var type = data.file ? data.file.type : image.mimeFromBase64(data.imageData);
var extension = file.typeToExtension(type);
if (!extension) {
return callback(new Error('[[error:invalid-image-extension]]'));
}
var uploadedImage;
var picture = {
name: 'profileAvatar',
uid: data.uid,
};
if (!data.imageData) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
var size = data.imageData.length;
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
if (size > uploadSize * 1024) {
return next(new Error('[[error:file-too-big, ' + meta.config.maximumProfileImageSize + ']]'));
if (data.file) {
return setImmediate(next, null, data.file.path);
}
saveImageDataToTempFile(data.imageData, next);
image.writeImageDataToTempFile(data.imageData, next);
},
function (path, next) {
image.path = path;
uploadProfileOrCover('profileavatar', image, data.imageData, next);
convertToPNG(path, extension, next);
},
function (uploadData, next) {
url = uploadData.url;
function (path, next) {
picture.path = path;
var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
image.resizeImage({
path: picture.path,
extension: extension,
width: imageDimension,
height: imageDimension,
}, next);
},
function (next) {
var filename = generateProfileImageFilename(data.uid, 'profileavatar', extension);
uploadProfileOrCover(filename, picture, next);
},
function (_uploadedImage, next) {
uploadedImage = _uploadedImage;
User.setUserFields(data.uid, {
uploadedpicture: url,
picture: url,
uploadedpicture: uploadedImage.url,
picture: uploadedImage.url,
}, next);
},
], function (err) {
deleteFile(image.path);
callback(err, {
url: url,
});
deleteFile(picture.path);
callback(err, uploadedImage);
});
};
function saveImageDataToTempFile(imageData, callback) {
var filename = crypto.createHash('md5').update(imageData).digest('hex');
var filepath = path.join(os.tmpdir(), filename);
function convertToPNG(path, extension, callback) {
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
if (!convertToPNG) {
return setImmediate(callback, null, path);
}
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(filepath, buffer, {
encoding: 'base64',
}, function (err) {
callback(err, filepath);
image.normalise(path, extension, function (err, newPath) {
if (err) {
return callback(err);
}
deleteFile(path);
callback(null, newPath);
});
}
function uploadProfileOrCover(type, image, imageData, callback) {
function uploadProfileOrCover(filename, image, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {
image: image,
uid: image.uid,
}, callback);
}
var filename = generateProfileImageFilename(image.uid, type, imageData);
saveFileToLocal(filename, image, callback);
}
function generateProfileImageFilename(uid, type, imageData) {
var extension = file.typeToExtension(imageData.slice(5, imageData.indexOf('base64') - 1));
function generateProfileImageFilename(uid, type, extension) {
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
var filename = uid + '-' + type + (keepAllVersions ? '-' + Date.now() : '') + (extension || '');
return filename;
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
return uid + '-' + type + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension);
}
function saveFileToLocal(filename, image, callback) {
@@ -273,6 +229,7 @@ module.exports = function (User) {
function (upload, next) {
next(null, {
url: nconf.get('relative_path') + upload.url,
path: upload.path,
name: image.name,
});
},

View File

@@ -1,5 +1,5 @@
<div class="post-cache">
<div class="row post-cache">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> [[admin/advanced/cache:post-cache]]</div>

View File

@@ -1,4 +1,4 @@
<div class="database">
<div class="row database">
<div class="col-sm-9">
<!-- IF mongo -->
<div class="panel panel-default">

View File

@@ -1,4 +1,4 @@
<div class="events">
<div class="row events">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> [[admin/advanced/events:events]]</div>

View File

@@ -1,4 +1,4 @@
<div class="logs">
<div class="row logs">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-file-text-o"></i> [[admin/advanced/logs:logs]]</div>

View File

@@ -1,4 +1,4 @@
<div id="skins" class="row skins">
<div id="skins" class="skins">
<div class="directory row" id="bootstrap_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
</div>

View File

@@ -1,4 +1,4 @@
<div class="logger">
<div class="row logger">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading">[[admin/development/logger:logger-settings]]</div>

View File

@@ -1,4 +1,4 @@
<div id="rewards">
<div id="rewards" class="row">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading">[[admin/extend/rewards:rewards]]</div>

View File

@@ -1,4 +1,4 @@
<div id="navigation">
<div class="row" id="navigation">
<div class="col-lg-9">
<div class="clearfix">
<ul id="active-navigation" class="nav navbar-nav">
@@ -74,7 +74,7 @@
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" name="property:loggedIn" <!-- IF enabled.properties.loggedIn -->checked<!-- ENDIF enabled.properties.loggedIn -->/>
<input class="mdl-switch__input" type="checkbox" name="property:loggedIn" <!-- IF enabled.properties.loggedIn -->checked<!-- ENDIF enabled.properties.loggedIn -->/>
<span class="mdl-switch__label"><strong>[[admin/general/navigation:only-logged-in]]</strong></span>
</label>
</div>

View File

@@ -1,4 +1,4 @@
<div class="social settings" class="row">
<div class="social settings">
<form role="form">
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">[[admin/general/social:post-sharing]]</div>

View File

@@ -1,4 +1,4 @@
<div class="sounds settings" class="row">
<div class="sounds settings row">
<div class="col-xs-12">
<form role="form">
<div class="row">
@@ -9,9 +9,15 @@
<div class="form-group col-xs-9">
<select class="form-control" id="notification" name="notification">
<option value="">[[user:no-sound]]</option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
<!-- BEGIN notification-sound -->
<optgroup label="{notification-sound.name}">
<!-- BEGIN notification-sound.sounds -->
<option value="{notification-sound.sounds.value}" <!-- IF notification-sound.sounds.selected -->selected<!-- ENDIF notification-sound.sounds.selected -->>
{notification-sound.sounds.name}
</option>
<!-- END notification-sound.sounds -->
</optgroup>
<!-- END notification-sound -->
</select>
</div>
<div class="btn-group col-xs-3">
@@ -29,9 +35,15 @@
<div class="form-group col-xs-9">
<select class="form-control" id="chat-incoming" name="chat-incoming">
<option value="">[[user:no-sound]]</option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
<!-- BEGIN chat-incoming-sound -->
<optgroup label="{chat-incoming-sound.name}">
<!-- BEGIN chat-incoming-sound.sounds -->
<option value="{chat-incoming-sound.sounds.value}" <!-- IF chat-incoming-sound.sounds.selected -->selected<!-- ENDIF chat-incoming-sound.sounds.selected -->>
{chat-incoming-sound.sounds.name}
</option>
<!-- END chat-incoming-sound.sounds -->
</optgroup>
<!-- END chat-incoming-sound -->
</select>
</div>
<div class="btn-group col-xs-3">
@@ -44,9 +56,15 @@
<div class="form-group col-xs-9">
<select class="form-control" id="chat-outgoing" name="chat-outgoing">
<option value="">[[user:no-sound]]</option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
<!-- BEGIN chat-outgoing-sound -->
<optgroup label="{chat-outgoing-sound.name}">
<!-- BEGIN chat-outgoing-sound.sounds -->
<option value="{chat-outgoing-sound.sounds.value}" <!-- IF chat-outgoing-sound.sounds.selected -->selected<!-- ENDIF chat-outgoing-sound.sounds.selected -->>
{chat-outgoing-sound.sounds.name}
</option>
<!-- END chat-outgoing-sound.sounds -->
</optgroup>
<!-- END chat-outgoing-sound -->
</select>
</div>
<div class="btn-group col-xs-3">
@@ -56,7 +74,15 @@
<div class="input-group">
<span class="input-group-btn">
<input data-action="upload" data-title="Upload Sound" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary" value="[[admin/general/sounds:upload-new-sound]]"></input>
<input
data-action="upload"
data-title="Upload Sound"
data-route="{config.relative_path}/api/admin/upload/sound"
data-accept="audio/*"
type="button"
class="btn btn-primary"
value="[[admin/general/sounds:upload-new-sound]]"
></input>
</span>
</div>
</div>

View File

@@ -3,11 +3,10 @@
<head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/admin.css?{cache-buster}" />
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/vendor/mdl/mdl.min.css?{cache-buster}" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<script>
var RELATIVE_PATH = "{relative_path}";
var config = JSON.parse('{{configJSON}}');
@@ -20,7 +19,7 @@
};
</script>
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.3/material.min.js"></script>
<script src="https://storage.googleapis.com/code.getmdl.io/1.3.0/material.min.js"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>

View File

@@ -1,5 +1,4 @@
<div class="flags">
<div class="row flags">
<div class="col-lg-12">
<div class="text-center">
@@ -106,7 +105,7 @@
</div>
<small>
<span class="pull-right">
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
<span class="timeago" title="{posts.timestampISO}"></span> &bull;
<a href="{config.relative_path}/post/{posts.pid}" target="_blank">[[admin/manage/flags:read-more]]</a>
</span>
@@ -114,7 +113,7 @@
</div>
</div>
<div class="col-sm-4">
<i class="fa fa-flag"></i>
<i class="fa fa-flag"></i>
[[admin/manage/flags:flagged-x-times, {posts.flags}]]
<blockquote class="flag-reporters">
<ul>

View File

@@ -39,7 +39,7 @@
<fieldset>
<div class="checkbox">
<label>
<input id="group-private" name="private" type="checkbox"<!-- IF group.private --> checked<!-- ENDIF group.private -->>
<input id="group-private" name="private" type="checkbox"<!-- IF group.private --> checked<!-- ENDIF group.private -->>
<strong>[[groups:details.private]]</strong>
<p class="help-block">
[[admin/manage/groups:edit.private-details]]
@@ -56,7 +56,7 @@
<fieldset>
<div class="checkbox">
<label>
<input id="group-disableJoinRequests" name="disableJoinRequests" type="checkbox"<!-- IF group.disableJoinRequests --> checked<!-- ENDIF group.disableJoinRequests -->>
<input id="group-disableJoinRequests" name="disableJoinRequests" type="checkbox"<!-- IF group.disableJoinRequests --> checked<!-- ENDIF group.disableJoinRequests -->>
<strong>[[admin/manage/groups:edit.disable-requests]]</strong>
</label>
</div>
@@ -65,7 +65,7 @@
<fieldset>
<div class="checkbox">
<label>
<input id="group-hidden" name="hidden" type="checkbox"<!-- IF group.hidden --> checked<!-- ENDIF group.hidden -->>
<input id="group-hidden" name="hidden" type="checkbox"<!-- IF group.hidden --> checked<!-- ENDIF group.hidden -->>
<strong>[[admin/manage/groups:edit.hidden]]</strong>
<p class="help-block">
[[admin/manage/groups:edit.hidden-details]]
@@ -87,36 +87,19 @@
<h3 class="panel-title"><i class="fa fa-users"></i> [[admin/manage/groups:edit.members]]</h3>
</div>
<div class="panel-body">
<!-- IMPORT partials/groups/memberlist.tpl -->
<!-- IMPORT admin/partials/groups/memberlist.tpl -->
</div>
</div>
</fieldset>
</div>
</div>
<div class="col-md-3 options acp-sidebar">
<div class="panel panel-default">
<div class="panel-heading">[[admin/manage/groups:control-panel]]</div>
<div class="panel-body">
<div class="btn-group btn-group-justified">
<div class="btn-group">
<button class="btn btn-primary save">[[global:save]]</button>
</div>
<div class="btn-group">
<button class="btn btn-default revert">
[[admin/manage/groups:revert]]
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
<i class="material-icons">save</i>
</button>
<div id="icons" style="display:none;">
<div class="icon-container">
<div class="row fa-icons">
@@ -125,4 +108,3 @@
</div>
</div>
</div>

View File

@@ -1,47 +1,45 @@
<div class="groups">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-body">
<table class="table table-striped groups-list">
<tr>
<th>[[admin/manage/groups:name]]</th>
<th>[[admin/manage/groups:description]]</th>
</tr>
<!-- BEGIN groups -->
<tr data-groupname="{groups.displayName}">
<td>
{groups.displayName}
<!-- IF groups.system -->
<span class="badge">[[admin/manage/groups:system]]</span>
<!-- ENDIF groups.system -->
</td>
<td>
<div class="btn-group pull-right">
<a href="{config.relative_path}/admin/manage/groups/{groups.nameEncoded}" class="btn btn-default btn-xs">
<i class="fa fa-edit"></i> [[admin/manage/groups:edit]]
</a>
<!-- IF !groups.system -->
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
<!-- ENDIF !groups.system -->
</div>
<p class="description">{groups.description}</p>
</td>
</tr>
<!-- END groups -->
</table>
<!-- IMPORT partials/paginator.tpl -->
</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>
</div>
<div class="col-lg-3 acp-sidebar">
<div class="panel panel-default">
<div class="panel-body">
<div>
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
</div>
</div>
</div>
<table class="table table-striped groups-list">
<thead>
<tr>
<th>[[admin/manage/groups:name]]</th>
<th class="hidden-xs">[[admin/manage/groups:description]]</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- BEGIN groups -->
<tr data-groupname="{groups.displayName}">
<td>
{groups.displayName}
<!-- IF groups.system -->
<span class="badge">[[admin/manage/groups:system]]</span>
<!-- ENDIF groups.system -->
</td>
<td class="hidden-xs">
<p class="description">{groups.description}</p>
</td>
<td>
<div class="btn-group ">
<a href="{config.relative_path}/admin/manage/groups/{groups.nameEncoded}" class="btn btn-default btn-xs">
<i class="fa fa-edit"></i> [[admin/manage/groups:edit]]
</a>
<!-- IF !groups.system -->
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
<!-- ENDIF !groups.system -->
</div>
</td>
</tr>
<!-- END groups -->
</tbody>
</table>
<!-- IMPORT partials/paginator.tpl -->
</div>
<div class="modal fade" id="create-modal">
@@ -75,10 +73,8 @@
</div>
</div>
</div>
</div>
<button id="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
<i class="material-icons">add</i>
</button>

View File

@@ -1,4 +1,4 @@
<div class="flags">
<div class="row ip-blacklist">
<div class="col-lg-12">
<p class="lead">
[[admin/manage/ip-blacklist:lead]]

View File

@@ -1,109 +1,121 @@
<div class="registration panel panel-primary">
<div class="panel-heading">
[[admin/manage/registration:queue]]
</div>
<!-- IF !users.length -->
<p class="panel-body">
[[admin/manage/registration:description, {config.relative_path}/admin/settings/user]]
</p>
<!-- ENDIF !users.length -->
<div class="table-responsive">
<table class="table table-striped users-list">
<tr>
<th>[[admin/manage/registration:list.name]]</th>
<th>[[admin/manage/registration:list.email]]</th>
<th class="hidden-xs">[[admin/manage/registration:list.ip]]</th>
<th class="hidden-xs">[[admin/manage/registration:list.time]]</th>
<!-- BEGIN customHeaders -->
<th class="hidden-xs">{customHeaders.label}</th>
<!-- END customHeaders -->
<th></th>
</tr>
<!-- BEGIN users -->
<tr data-username="{users.username}">
<td>
<!-- IF users.usernameSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.username-spam, {users.spamData.username.frequency}, {users.spamData.username.appears}, {users.spamData.username.confidence}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.usernameSpam -->
{users.username}
</td>
<td>
<!-- IF users.emailSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.email-spam, {users.spamData.email.frequency}, {users.spamData.email.appears}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.emailSpam -->
{users.email}
</td>
<td class="hidden-xs">
<!-- IF users.ipSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.ip-spam, {users.spamData.ip.frequency}, {users.spamData.ip.appears}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.ipSpam -->
{users.ip}
<!-- BEGIN users.ipMatch -->
<br>
<!-- IF users.ipMatch.picture -->
<img src="{users.ipMatch.picture}" class="user-img"/>
<!-- ELSE -->
<div class="user-img avatar avatar-sm" style="background-color: {users.ipMatch.icon:bgColor};">{users.ipMatch.icon:text}</div>
<!-- ENDIF users.ipMatch.picture -->
<a href="/uid/{users.ipMatch.uid}">{users.ipMatch.username}</a>
<!-- END users.ipMatch -->
</td>
<td class="hidden-xs">
<span class="timeago" title="{users.timestampISO}"></span>
</td>
<!-- BEGIN users.customRows -->
<td class="hidden-xs">{users.customRows.value}</td>
<!-- END users.customRows -->
<td>
<div class="btn-group pull-right">
<button class="btn btn-success btn-xs" data-action="accept"><i class="fa fa-check"></i></button>
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
</div>
</td>
</tr>
<!-- END users -->
</table>
</div>
<div class="row">
<div class="col-xs-12">
<div class="registration panel panel-primary">
<div class="panel-heading">
[[admin/manage/registration:queue]]
</div>
<!-- IF !users.length -->
<p class="panel-body">
[[admin/manage/registration:description, {config.relative_path}/admin/settings/user]]
</p>
<!-- ENDIF !users.length -->
<div class="table-responsive">
<table class="table table-striped users-list">
<thead>
<tr>
<th>[[admin/manage/registration:list.name]]</th>
<th>[[admin/manage/registration:list.email]]</th>
<th class="hidden-xs">[[admin/manage/registration:list.ip]]</th>
<th class="hidden-xs">[[admin/manage/registration:list.time]]</th>
<!-- BEGIN customHeaders -->
<th class="hidden-xs">{customHeaders.label}</th>
<!-- END customHeaders -->
<th></th>
</tr>
</thead>
<tbody>
<!-- BEGIN users -->
<tr data-username="{users.username}">
<td>
<!-- IF users.usernameSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.username-spam, {users.spamData.username.frequency}, {users.spamData.username.appears}, {users.spamData.username.confidence}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.usernameSpam -->
{users.username}
</td>
<td>
<!-- IF users.emailSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.email-spam, {users.spamData.email.frequency}, {users.spamData.email.appears}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.emailSpam -->
{users.email}
</td>
<td class="hidden-xs">
<!-- IF users.ipSpam -->
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.ip-spam, {users.spamData.ip.frequency}, {users.spamData.ip.appears}]]"></i>
<!-- ELSE -->
<i class="fa fa-check text-success"></i>
<!-- ENDIF users.ipSpam -->
{users.ip}
<!-- BEGIN users.ipMatch -->
<br>
<!-- IF users.ipMatch.picture -->
<img src="{users.ipMatch.picture}" class="user-img"/>
<!-- ELSE -->
<div class="user-img avatar avatar-sm" style="background-color: {users.ipMatch.icon:bgColor};">{users.ipMatch.icon:text}</div>
<!-- ENDIF users.ipMatch.picture -->
<a href="/uid/{users.ipMatch.uid}">{users.ipMatch.username}</a>
<!-- END users.ipMatch -->
</td>
<td class="hidden-xs">
<span class="timeago" title="{users.timestampISO}"></span>
</td>
<!-- IMPORT partials/paginator.tpl -->
</div>
<!-- BEGIN users.customRows -->
<td class="hidden-xs">{users.customRows.value}</td>
<!-- END users.customRows -->
<div class="invitations panel panel-success">
<div class="panel-heading">
[[admin/manage/registration:invitations]]
<td>
<div class="btn-group pull-right">
<button class="btn btn-success btn-xs" data-action="accept"><i class="fa fa-check"></i></button>
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
</div>
</td>
</tr>
<!-- END users -->
</tbody>
</table>
</div>
<!-- IMPORT partials/paginator.tpl -->
</div>
<div class="invitations panel panel-success">
<div class="panel-heading">
[[admin/manage/registration:invitations]]
</div>
<p class="panel-body">
[[admin/manage/registration:invitations.description]]
</p>
<div class="table-responsive">
<table class="table table-striped invites-list">
<thead>
<tr>
<th>[[admin/manage/registration:invitations.inviter-username]]</th>
<th>[[admin/manage/registration:invitations.invitee-email]]</th>
<th>[[admin/manage/registration:invitations.invitee-username]]</th>
</tr>
</thead>
<tbody>
<!-- BEGIN invites -->
<!-- BEGIN invites.invitations -->
<tr data-invitation-mail="{invites.invitations.email}"
data-invited-by="{invites.username}">
<td class ="invited-by"><!-- IF @first -->{invites.username}<!-- ENDIF @first --></td>
<td>{invites.invitations.email}</td>
<td>{invites.invitations.username}
<div class="btn-group pull-right">
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
</div>
</td>
</tr>
<!-- END invites.invitations -->
<!-- END invites -->
</tbody>
</table>
</div>
</div>
</div>
<p class="panel-body">
[[admin/manage/registration:invitations.description]]
</p>
<div class="table-responsive">
<table class="table table-striped invites-list">
<tr>
<th>[[admin/manage/registration:invitations.inviter-username]]</th>
<th>[[admin/manage/registration:invitations.invitee-email]]</th>
<th>[[admin/manage/registration:invitations.invitee-username]]</th>
</tr>
<!-- BEGIN invites -->
<!-- BEGIN invites.invitations -->
<tr data-invitation-mail="{invites.invitations.email}"
data-invited-by="{invites.username}">
<td class ="invited-by"><!-- IF @first -->{invites.username}<!-- ENDIF @first --></td>
<td>{invites.invitations.email}</td>
<td>{invites.invitations.username}
<div class="btn-group pull-right">
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
</div>
</td>
</tr>
<!-- END invites.invitations -->
<!-- END invites -->
</table>
</div>
</div>
</div>

View File

@@ -1,5 +1,4 @@
<div class="tags row">
<div class="col-lg-9">
<div class="panel panel-default tag-management">
<div class="panel-body">
@@ -11,12 +10,11 @@
<!-- BEGIN tags -->
<div class="tag-row" data-tag="{tags.value}">
<div data-value="{tags.value}">
<span class="tag-item" data-tag="{tags.value}" style="
<span class="mdl-chip mdl-chip--contact tag-item" data-tag="{tags.value}" style="
<!-- IF tags.color -->color: {tags.color};<!-- ENDIF tags.color -->
<!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->
">{tags.value}</span>
<span class="tag-topic-count">
<a href="{config.relative_path}/tags/{tags.value}" target="_blank">{tags.score}</a>
<!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->">
<span class="mdl-chip__contact mdl-color--light-blue mdl-color-text--white tag-topic-count">{tags.score}</span>
<span class="mdl-chip__text">{tags.value}</span>
</span>
</div>
<div class="tag-modal hidden">
@@ -76,5 +74,4 @@
</div>
</div>
</div>
</div>

View File

@@ -71,37 +71,41 @@
<div class="table-responsive">
<table class="table table-striped users-table">
<tr>
<th><input component="user/select/all" type="checkbox"/></th>
<th>[[admin/manage/users:users.uid]]</th>
<th>[[admin/manage/users:users.username]]</th>
<th>[[admin/manage/users:users.email]]</th>
<th class="text-right">[[admin/manage/users:users.postcount]]</th>
<th class="text-right">[[admin/manage/users:users.reputation]]</th>
<th class="text-right">[[admin/manage/users:users.flags]]</th>
<th>[[admin/manage/users:users.joined]]</th>
<th>[[admin/manage/users:users.last-online]]</th>
<th>[[admin/manage/users:users.banned]]</th>
</tr>
<!-- BEGIN users -->
<tr class="user-row">
<th><input component="user/select/single" data-uid="{users.uid}" type="checkbox"/></th>
<td class="text-right">{users.uid}</td>
<td><i class="administrator fa fa-shield text-success<!-- IF !users.administrator --> hidden<!-- ENDIF !users.administrator -->"></i><a href="{config.relative_path}/user/{users.userslug}"> {users.username}</a></td>
<td>
<!-- IF config.requireEmailConfirmation -->
<i class="validated fa fa-check text-success<!-- IF !users.email:confirmed --> hidden<!-- ENDIF !users.email:confirmed -->" title="validated"></i>
<i class="notvalidated fa fa-times text-danger<!-- IF users.email:confirmed --> hidden<!-- ENDIF users.email:confirmed -->" title="not validated"></i>
<!-- ENDIF config.requireEmailConfirmation --> {users.email}</td>
<td class="text-right">{users.postcount}</td>
<td class="text-right">{users.reputation}</td>
<td class="text-right"><!-- IF users.flags -->{users.flags}<!-- ELSE -->0<!-- ENDIF users.flags --></td>
<td><span class="timeago" title="{users.joindateISO}"></span></td>
<td><span class="timeago" title="{users.lastonlineISO}"></span></td>
<td class="text-center"><i class="ban fa fa-gavel text-danger<!-- IF !users.banned --> hidden<!-- ENDIF !users.banned -->"></i></td>
</tr>
<!-- END users -->
<thead>
<tr>
<th><input component="user/select/all" type="checkbox"/></th>
<th>[[admin/manage/users:users.uid]]</th>
<th>[[admin/manage/users:users.username]]</th>
<th>[[admin/manage/users:users.email]]</th>
<th class="text-right">[[admin/manage/users:users.postcount]]</th>
<th class="text-right">[[admin/manage/users:users.reputation]]</th>
<th class="text-right">[[admin/manage/users:users.flags]]</th>
<th>[[admin/manage/users:users.joined]]</th>
<th>[[admin/manage/users:users.last-online]]</th>
<th>[[admin/manage/users:users.banned]]</th>
</tr>
</thead>
<tbody>
<!-- BEGIN users -->
<tr class="user-row">
<th><input component="user/select/single" data-uid="{users.uid}" type="checkbox"/></th>
<td class="text-right">{users.uid}</td>
<td><i class="administrator fa fa-shield text-success<!-- IF !users.administrator --> hidden<!-- ENDIF !users.administrator -->"></i><a href="{config.relative_path}/user/{users.userslug}"> {users.username}</a></td>
<td>
<!-- IF config.requireEmailConfirmation -->
<i class="validated fa fa-check text-success<!-- IF !users.email:confirmed --> hidden<!-- ENDIF !users.email:confirmed -->" title="validated"></i>
<i class="notvalidated fa fa-times text-danger<!-- IF users.email:confirmed --> hidden<!-- ENDIF users.email:confirmed -->" title="not validated"></i>
<!-- ENDIF config.requireEmailConfirmation --> {users.email}</td>
<td class="text-right">{users.postcount}</td>
<td class="text-right">{users.reputation}</td>
<td class="text-right"><!-- IF users.flags -->{users.flags}<!-- ELSE -->0<!-- ENDIF users.flags --></td>
<td><span class="timeago" title="{users.joindateISO}"></span></td>
<td><span class="timeago" title="{users.lastonlineISO}"></span></td>
<td class="text-center"><i class="ban fa fa-gavel text-danger<!-- IF !users.banned --> hidden<!-- ENDIF !users.banned -->"></i></td>
</tr>
<!-- END users -->
</tbody>
</table>
</div>

View File

@@ -1,101 +1,109 @@
<table class="table table-striped privilege-table">
<tr class="privilege-table-header">
<th colspan="2"></th>
<th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]]
</th>
<th class="arrowed" colspan="7">
[[admin/manage/categories:privileges.section-posting]]
</th>
<th class="arrowed" colspan="2">
[[admin/manage/categories:privileges.section-moderation]]
</th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
<!-- BEGIN privileges.labels.users -->
<th class="text-center">{privileges.labels.users.name}</th>
<!-- END privileges.labels.users -->
</tr>
<!-- IF privileges.users.length -->
<!-- BEGIN privileges.users -->
<tr data-uid="{privileges.users.uid}">
<td>
<!-- IF ../picture -->
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
<!-- ENDIF ../picture -->
</td>
<td>{privileges.users.username}</td>
{function.spawnPrivilegeStates, privileges.users.username, privileges}
</tr>
<!-- END privileges.users -->
<tr>
<td colspan="{privileges.columnCount}">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ELSE -->
<tr>
<td colspan="{privileges.columnCount}">
[[admin/manage/categories:privileges.no-users]]
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ENDIF privileges.users.length -->
<thead>
<tr class="privilege-table-header">
<th colspan="2"></th>
<th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]]
</th>
<th class="arrowed" colspan="7">
[[admin/manage/categories:privileges.section-posting]]
</th>
<th class="arrowed" colspan="2">
[[admin/manage/categories:privileges.section-moderation]]
</th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
<!-- BEGIN privileges.labels.users -->
<th class="text-center">{privileges.labels.users.name}</th>
<!-- END privileges.labels.users -->
</tr>
</thead>
<tbody>
<!-- IF privileges.users.length -->
<!-- BEGIN privileges.users -->
<tr data-uid="{privileges.users.uid}">
<td>
<!-- IF ../picture -->
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
<!-- ENDIF ../picture -->
</td>
<td>{privileges.users.username}</td>
{function.spawnPrivilegeStates, privileges.users.username, privileges}
</tr>
<!-- END privileges.users -->
<tr>
<td colspan="{privileges.columnCount}">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ELSE -->
<tr>
<td colspan="{privileges.columnCount}">
[[admin/manage/categories:privileges.no-users]]
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ENDIF privileges.users.length -->
</tbody>
</table>
<table class="table table-striped privilege-table">
<tr class="privilege-table-header">
<th colspan="2"></th>
<th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]]
</th>
<th class="arrowed" colspan="7">
[[admin/manage/categories:privileges.section-posting]]
</th>
<th class="arrowed" colspan="2">
[[admin/manage/categories:privileges.section-moderation]]
</th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
<!-- BEGIN privileges.labels.groups -->
<th class="text-center">{privileges.labels.groups.name}</th>
<!-- END privileges.labels.groups -->
</tr>
<!-- BEGIN privileges.groups -->
<tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
<td>
<!-- IF privileges.groups.isPrivate -->
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
<!-- ENDIF privileges.groups.isPrivate -->
{privileges.groups.name}
</td>
<td></td>
{function.spawnPrivilegeStates, name, privileges}
</tr>
<!-- END privileges.groups -->
<tr>
<td colspan="{privileges.columnCount}">
<div class="btn-toolbar">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group">
[[admin/manage/categories:privileges.search-group]]
</button>
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyToChildren">
[[admin/manage/categories:privileges.copy-to-children]]
</button>
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyPrivilegesFrom">
[[admin/manage/categories:privileges.copy-from-category]]
</button>
</div>
</td>
</tr>
<thead>
<tr class="privilege-table-header">
<th colspan="2"></th>
<th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]]
</th>
<th class="arrowed" colspan="7">
[[admin/manage/categories:privileges.section-posting]]
</th>
<th class="arrowed" colspan="2">
[[admin/manage/categories:privileges.section-moderation]]
</th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
<!-- BEGIN privileges.labels.groups -->
<th class="text-center">{privileges.labels.groups.name}</th>
<!-- END privileges.labels.groups -->
</tr>
</thead>
<tbody>
<!-- BEGIN privileges.groups -->
<tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
<td>
<!-- IF privileges.groups.isPrivate -->
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
<!-- ENDIF privileges.groups.isPrivate -->
{privileges.groups.name}
</td>
<td></td>
{function.spawnPrivilegeStates, name, privileges}
</tr>
<!-- END privileges.groups -->
<tr>
<td colspan="{privileges.columnCount}">
<div class="btn-toolbar">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group">
[[admin/manage/categories:privileges.search-group]]
</button>
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyToChildren">
[[admin/manage/categories:privileges.copy-to-children]]
</button>
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyPrivilegesFrom">
[[admin/manage/categories:privileges.copy-from-category]]
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="help-block">
[[admin/manage/categories:privileges.inherit]]

View File

@@ -0,0 +1,37 @@
<div class="input-group">
<input class="form-control" type="text" component="groups/members/search" placeholder="[[global:search]]"/>
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
</div><br />
<table component="groups/members" class="table table-striped table-hover" data-nextstart="{group.membersNextStart}">
<tbody>
<!-- BEGIN members -->
<tr data-uid="{group.members.uid}">
<td>
<a href="{config.relative_path}/user/{group.members.userslug}">
<!-- IF group.members.picture -->
<img class="avatar avatar-sm" src="{group.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {group.members.icon:bgColor};">{group.members.icon:text}</div>
<!-- ENDIF group.members.picture -->
</a>
</td>
<td class="member-name">
<a href="{config.relative_path}/user/{group.members.userslug}">{group.members.username}</a> <i title="[[groups:owner]]" class="fa fa-star text-warning <!-- IF !group.members.isOwner -->invisible<!-- ENDIF !group.members.isOwner -->"></i>
<!-- IF group.isOwner -->
<div class="owner-controls btn-group pull-right">
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="toggleOwnership" title="[[groups:details.grant]]">
<i class="fa fa-star"></i>
</a>
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="kick" title="[[groups:details.kick]]">
<i class="fa fa-ban"></i>
</a>
</div>
<!-- ENDIF group.isOwner -->
</td>
</tr>
<!-- END members -->
</tbody>
</table>

View File

@@ -1,40 +1,39 @@
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="crop-picture">[[user:crop_picture]]</h3>
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="crop-picture">[[user:crop_picture]]</h3>
</div>
<div class="modal-body">
<div id="upload-progress-box" class="progress hide">
<div id="upload-progress-bar" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0">
</div>
</div>
<div class="modal-body">
<div id="upload-progress-box" class="progress hide">
<div id="upload-progress-bar" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0">
</div>
</div>
<div class="cropper">
<img id="cropped-image" src="{url}" >
</div>
<hr />
<div class="btn-group">
<button class="btn btn-primary rotate" data-degrees="-45"><i class="fa fa-rotate-left"></i></button>
<button class="btn btn-primary rotate" data-degrees="45"><i class="fa fa-rotate-right"></i></button>
</div>
<div class="btn-group">
<button class="btn btn-primary flip" data-option="-1" data-method="scaleX"><i class="fa fa-arrows-h"></i></button>
<button class="btn btn-primary flip" data-option="1" data-method="scaleY"><i class="fa fa-arrows-v"></i></button>
</div>
<div class="btn-group">
<button class="btn btn-primary reset"><i class="fa fa-refresh"></i></button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
<button class="btn btn-primary upload-btn">[[user:upload_picture]]</button>
<button class="btn btn-primary crop-btn">[[user:upload_cropped_picture]]</button>
<div class="cropper">
<img id="cropped-image" src="{url}">
</div>
<hr />
<div class="btn-group">
<button class="btn btn-primary rotate" data-degrees="-45"><i class="fa fa-rotate-left"></i></button>
<button class="btn btn-primary rotate" data-degrees="45"><i class="fa fa-rotate-right"></i></button>
</div>
<div class="btn-group">
<button class="btn btn-primary flip" data-option="-1" data-method="scaleX"><i class="fa fa-arrows-h"></i></button>
<button class="btn btn-primary flip" data-option="1" data-method="scaleY"><i class="fa fa-arrows-v"></i></button>
</div>
<div class="btn-group">
<button class="btn btn-primary reset"><i class="fa fa-refresh"></i></button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
<button class="btn btn-primary upload-btn <!-- IF !allowSkippingCrop -->hidden<!-- ENDIF !allowSkippingCrop -->">[[user:upload_picture]]</button>
<button class="btn btn-primary crop-btn">[[user:upload_cropped_picture]]</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -103,9 +103,9 @@ function initializeNodeBB(callback) {
},
function (next) {
async.series([
async.apply(meta.sounds.init),
async.apply(languages.init),
async.apply(meta.blacklist.load),
meta.sounds.addUploads,
languages.init,
meta.blacklist.load,
], next);
},
], callback);
@@ -159,7 +159,8 @@ function setupExpressApp(app) {
}
function setupFavicon(app) {
var faviconPath = path.join(nconf.get('base_dir'), 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico');
var faviconPath = meta.config['brand:favicon'] || 'favicon.ico';
faviconPath = path.join(nconf.get('base_dir'), 'public', faviconPath.replace(/assets\/uploads/, 'uploads'));
if (file.existsSync(faviconPath)) {
app.use(nconf.get('relative_path'), favicon(faviconPath));
}