Merge branch 'master' into develop

v1.18.x
Julian Lam 8 years ago
commit c2f1d254cc

@ -2,7 +2,7 @@
{
"widget": "html",
"data" : {
"html": "<footer id=\"footer\" class=\"container footer\">\r\n\t<div class=\"copyright\">\r\n\t\tCopyright © 2016 <a target=\"_blank\" href=\"https://nodebb.org\">NodeBB Forums</a> | <a target=\"_blank\" href=\"//github.com/NodeBB/NodeBB/graphs/contributors\">Contributors</a>\r\n\t</div>\r\n</footer>",
"html": "<footer id=\"footer\" class=\"container footer\">\r\n\t<div>\r\n\t\tPowered by <a target=\"_blank\" href=\"https://nodebb.org\">NodeBB</a> | <a target=\"_blank\" href=\"//github.com/NodeBB/NodeBB/graphs/contributors\">Contributors</a>\r\n\t</div>\r\n</footer>",
"title":"",
"container":""
}

@ -55,19 +55,19 @@
"morgan": "^1.3.2",
"mousetrap": "^1.5.3",
"nconf": "~0.8.2",
"nodebb-plugin-composer-default": "4.4.18",
"nodebb-plugin-composer-default": "4.4.19",
"nodebb-plugin-dbsearch": "2.0.4",
"nodebb-plugin-emoji-extended": "1.1.1",
"nodebb-plugin-emoji-one": "1.2.1",
"nodebb-plugin-markdown": "7.1.1",
"nodebb-plugin-mentions": "2.1.2",
"nodebb-plugin-markdown": "7.1.2",
"nodebb-plugin-mentions": "2.1.5",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.5.0",
"nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "4.0.2",
"nodebb-theme-persona": "5.0.11",
"nodebb-theme-lavender": "4.0.5",
"nodebb-theme-persona": "5.0.12",
"nodebb-theme-slick": "1.1.0",
"nodebb-theme-vanilla": "6.0.7",
"nodebb-theme-vanilla": "6.0.9",
"nodebb-widget-essentials": "3.0.0",
"nodemailer": "2.6.4",
"nodemailer-sendmail-transport": "1.0.0",

@ -104,6 +104,6 @@
"cookies.accept": "Verstanden!",
"cookies.learn_more": "Erfahre mehr",
"edited": "Bearbeitet",
"disabled": "Disabled",
"select": "Select"
"disabled": "Deaktiviert",
"select": "Auswählen"
}

@ -59,7 +59,7 @@
"thread_tools.unlock": "Thema öffnen",
"thread_tools.move": "Thema verschieben",
"thread_tools.move_all": "Alle verschieben",
"thread_tools.select_category": "Select Category",
"thread_tools.select_category": "Kategorie auswählen",
"thread_tools.fork": "Thema aufspalten",
"thread_tools.delete": "Thema löschen",
"thread_tools.delete-posts": "Beiträge entfernen",

@ -1,7 +1,7 @@
{
"logs": "Logs",
"control-panel": "Logs Control Panel",
"reload": "Reload Logs",
"clear": "Clear Logs",
"clear-success": "Logs Cleared!"
"logs": "Nhật ký",
"control-panel": "Bảng điều khiển log",
"reload": "Tải lại log",
"clear": "Xóa các log",
"clear-success": "Các log đã được xóa!"
}

@ -1,9 +1,9 @@
{
"loading": "Loading Skins...",
"homepage": "Homepage",
"select-skin": "Select Skin",
"current-skin": "Current Skin",
"skin-updated": "Skin Updated",
"applied-success": "%1 skin was succesfully applied",
"revert-success": "Skin reverted to base colours"
"loading": "Đang tải giao diện ...",
"homepage": "Trang chủ",
"select-skin": "Chọn giao diện",
"current-skin": "Giao diện hiện tại",
"skin-updated": "Đã cập nhật giao diện",
"applied-success": "1% giao diện đã được sử dụng thành công",
"revert-success": "Đã trả giao diện về màu cơ bản"
}

@ -10,7 +10,7 @@
"custom-class": "自定义 Class",
"num-recent-replies": "最近回复数",
"ext-link": "外部链接",
"is-section": "Treat this category as a section",
"is-section": "将该板块作为段落",
"upload-image": "上传图片",
"delete-image": "移除",
"category-image": "版块图片",

@ -12,8 +12,6 @@ div.categories {
.row {
margin-left: -15px;
margin-right: -15px;
padding-bottom: 12px;
margin-bottom: 12px;
}
> li li:last-child {
@ -37,7 +35,6 @@ div.categories {
li {
min-height: @acp-line-height;
margin: @acp-base-line 0;
margin-top: 28px;
&.placeholder {
border: 1px dashed #2196F3;
@ -56,21 +53,40 @@ div.categories {
}
}
.toggle {
width: 24px;
height: 24px;
border-radius: 50%;
line-height: 24px;
text-align: center;
vertical-align: bottom;
background-size: cover;
float: left;
margin-right: 0px;
cursor: pointer;
.fa {
font-size: 85%;
}
}
.icon {
width: @acp-line-height;
height: @acp-line-height;
width: 24px;
height: 24px;
border-radius: 50%;
line-height: @acp-line-height;
line-height: 24px;
text-align: center;
vertical-align: bottom;
background-size: cover;
float: left;
margin-right: @acp-margin;
margin-right: 0px;
cursor: move;
.fa {
font-size: 85%;
}
}
.information {
padding-left: 70px;
padding-left: 60px;
}
.category-header {

@ -31,6 +31,12 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri
Categories.toggle([cid].concat(children), !disabled);
return false;
});
$('.categories').on('click', '.toggle', function () {
var el = $(this);
el.find('i').toggleClass('fa-minus').toggleClass('fa-plus');
el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden');
});
};
Categories.throwCreateModal = function () {

@ -204,7 +204,7 @@ $(document).ready(function () {
}
ajaxify.loadScript(tpl_url, done);
ajaxify.widgets.render(tpl_url, url, done);
ajaxify.widgets.render(tpl_url, done);
$(window).trigger('action:ajaxify.contentLoaded', { url: url, tpl: tpl_url });

@ -119,6 +119,10 @@ define('forum/search', ['search', 'autocomplete', 'storage'], function (searchMo
$('#show-as-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic);
$('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost);
}
$(window).trigger('action:search.fillOutForm', {
form: formData,
});
}
}
@ -151,7 +155,7 @@ define('forum/search', ['search', 'autocomplete', 'storage'], function (searchMo
function handleSavePreferences() {
$('#save-preferences').on('click', function () {
storage.setItem('search-preferences', JSON.stringify(getSearchDataFromDOMFromDOM()));
storage.setItem('search-preferences', JSON.stringify(getSearchDataFromDOM()));
app.alertSuccess('[[search:search-preferences-saved]]');
return false;
});

@ -77,7 +77,7 @@ define('search', ['navigator', 'translator', 'storage'], function (nav, translat
$(window).trigger('action:search.createQueryString', {
query: query,
dom: data,
data: data,
});
return decodeURIComponent($.param(query));

@ -580,39 +580,13 @@
prepareDOM: function prepareDOM() {
// Load the appropriate timeago locale file,
// and correct NodeBB language codes to timeago codes, if necessary
var languageCode;
switch (config.userLang) {
case 'en-GB':
case 'en-US':
languageCode = 'en';
break;
case 'fa-IR':
languageCode = 'fa';
break;
case 'pt-BR':
languageCode = 'pt-br';
break;
case 'nb':
languageCode = 'no';
break;
default:
languageCode = config.userLang;
break;
}
var languageCode = utils.userLangToTimeagoCode(config.userLang);
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () {
jQuery('.timeago').timeago();
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
// Retrieve the shorthand timeago values as well
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
// Switch back to long-form
adaptor.toggleTimeagoShorthand();
});
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
// Switch back to long-form
adaptor.toggleTimeagoShorthand();
});
// Add directional code if necessary

@ -103,7 +103,16 @@
hasLanguageKey: function (input) {
return utils.languageKeyRegex.test(input);
},
userLangToTimeagoCode: function (userLang) {
var mapping = {
'en-GB': 'en',
'en-US': 'en',
'fa-IR': 'fa',
'pt-BR': 'pt-br',
nb: 'no',
};
return mapping[userLang] || userLang;
},
// shallow objects merge
merge: function () {
var result = {};

@ -1,90 +1,64 @@
'use strict';
(function (ajaxify) {
ajaxify.widgets = {};
ajaxify.widgets.reposition = function (location) {
$('body [has-widget-class]').each(function () {
var $this = $(this);
if ($this.attr('has-widget-target') === location) {
$this.removeClass();
$this.addClass($this.attr('has-widget-class'));
}
});
};
ajaxify.widgets.render = function (template, url, callback) {
ajaxify.widgets.render = function (template, callback) {
callback = callback || function () {};
if (template.match(/^admin/)) {
return callback();
}
var widgetLocations = ['sidebar', 'footer', 'header'];
var locations = Object.keys(ajaxify.data.widgets);
$('#content [widget-area]').each(function () {
var location = $(this).attr('widget-area');
if ($.inArray(location, widgetLocations) === -1) {
widgetLocations.push(location);
locations.forEach(function (location) {
var area = $('#content [widget-area="' + location + '"]');
if (area.length) {
return;
}
});
$.get(config.relative_path + '/api/widgets/render?' + config['cache-buster'], {
locations: widgetLocations,
template: template + '.tpl',
url: url,
cid: ajaxify.data.cid,
isMobile: utils.isMobile(),
}, function (renderedAreas) {
for (var x = 0; x < renderedAreas.length; x += 1) {
var renderedWidgets = renderedAreas[x].widgets;
var location = renderedAreas[x].location;
var html = '';
var widgetsAtLocation = ajaxify.data.widgets[location] || [];
var html = '';
for (var i = 0; i < renderedWidgets.length; i += 1) {
html += templates.parse(renderedWidgets[i].html, {});
}
widgetsAtLocation.forEach(function (widget) {
html += widget.html;
var area = $('#content [widget-area="' + location + '"]');
if (!area.length && window.location.pathname.indexOf('/admin') === -1 && renderedWidgets.length) {
if (location === 'footer' && !$('#content [widget-area="footer"]').length) {
$('#content').append($('<div class="row"><div widget-area="footer" class="col-xs-12"></div></div>'));
} else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) {
if ($('[component="account/cover"]').length) {
$('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
} else if ($('[component="groups/cover"]').length) {
$('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
} else {
$('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
}
} else if (location === 'header' && !$('#content [widget-area="header"]').length) {
$('#content').prepend($('<div class="row"><div widget-area="header" class="col-xs-12"></div></div>'));
if (location === 'footer' && !$('#content [widget-area="footer"]').length) {
$('#content').append($('<div class="row"><div widget-area="footer" class="col-xs-12"></div></div>'));
} else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) {
if ($('[component="account/cover"]').length) {
$('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
} else if ($('[component="groups/cover"]').length) {
$('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
} else {
$('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
}
area = $('#content [widget-area="' + location + '"]');
} else if (location === 'header' && !$('#content [widget-area="header"]').length) {
$('#content').prepend($('<div class="row"><div widget-area="header" class="col-xs-12"></div></div>'));
}
});
area = $('#content [widget-area="' + location + '"]');
if (html && area.length) {
area.html(html);
}
if (renderedWidgets.length) {
area.removeClass('hidden');
ajaxify.widgets.reposition(location);
}
if (widgetsAtLocation.length) {
area.removeClass('hidden');
}
});
var widgetAreas = $('#content [widget-area]');
widgetAreas.find('img:not(.not-responsive)').addClass('img-responsive');
widgetAreas.find('.timeago').timeago();
widgetAreas.find('img[title].teaser-pic,img[title].user-img').each(function () {
$(this).tooltip({
placement: 'top',
title: $(this).attr('title'),
});
var widgetAreas = $('#content [widget-area]');
widgetAreas.find('img:not(.not-responsive)').addClass('img-responsive');
widgetAreas.find('.timeago').timeago();
widgetAreas.find('img[title].teaser-pic,img[title].user-img').each(function () {
$(this).tooltip({
placement: 'top',
title: $(this).attr('title'),
});
$(window).trigger('action:widgets.loaded', {});
callback(renderedAreas);
});
$(window).trigger('action:widgets.loaded', {});
callback();
};
}(ajaxify || {}));

@ -49,18 +49,20 @@ exports.processSortedSet = function (setKey, process, options, callback) {
return !done;
},
function (next) {
db.getSortedSetRange(setKey, start, stop, function (err, ids) {
if (err) {
return next(err);
}
if (!ids.length || options.doneIf(start, stop, ids)) {
done = true;
return next();
}
process(ids, function (err) {
if (err) {
return next(err);
async.waterfall([
function (next) {
db.getSortedSetRange(setKey, start, stop, next);
},
function (ids, _next) {
if (!ids.length || options.doneIf(start, stop, ids)) {
done = true;
return next();
}
process(ids, function (err) {
_next(err);
});
},
function (next) {
start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : options.batch + 1;
stop = start + options.batch;
@ -69,8 +71,8 @@ exports.processSortedSet = function (setKey, process, options, callback) {
} else {
next();
}
});
});
},
], next);
},
callback
);
@ -106,17 +108,21 @@ exports.processArray = function (array, process, options, callback) {
done = true;
return next();
}
process(currentBatch, function (err) {
if (err) {
return next(err);
}
start += batch;
if (options.interval) {
setTimeout(next, options.interval);
} else {
next();
}
});
async.waterfall([
function (next) {
process(currentBatch, function (err) {
next(err);
});
},
function (next) {
start += batch;
if (options.interval) {
setTimeout(next, options.interval);
} else {
next();
}
},
], next);
},
function (err) {
callback(err);

@ -12,7 +12,7 @@ var notificationsController = module.exports;
notificationsController.get = function (req, res, next) {
var regularFilters = [
{ name: '[[notifications:all]]', filter: '' },
{ name: '[[notifications:topics]]', filter: 'new-topic' },
{ name: '[[global:topics]]', filter: 'new-topic' },
{ name: '[[notifications:replies]]', filter: 'new-reply' },
{ name: '[[notifications:chat]]', filter: 'new-chat' },
{ name: '[[notifications:follows]]', filter: 'follow' },

@ -11,12 +11,11 @@ var topics = require('../topics');
var categories = require('../categories');
var privileges = require('../privileges');
var plugins = require('../plugins');
var widgets = require('../widgets');
var translator = require('../translator');
var apiController = module.exports;
apiController.getConfig = function (req, res, next) {
apiController.loadConfig = function (req, callback) {
var config = {};
config.environment = process.env.NODE_ENV;
config.relative_path = nconf.get('relative_path');
@ -59,7 +58,7 @@ apiController.getConfig = function (req, res, next) {
config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1;
config.topicPostSort = meta.config.topicPostSort || 'oldest_to_newest';
config.categoryTopicSort = meta.config.categoryTopicSort || 'newest_to_oldest';
config.csrf_token = req.csrfToken();
config.csrf_token = req.csrfToken && req.csrfToken();
config.searchEnabled = plugins.hasListeners('filter:search.query');
config.bootswatchSkin = meta.config.bootswatchSkin || 'noskin';
config.defaultBootswatchSkin = meta.config.bootswatchSkin || 'noskin';
@ -80,7 +79,7 @@ apiController.getConfig = function (req, res, next) {
async.waterfall([
function (next) {
if (!req.user) {
if (!req.uid) {
return next(null, config);
}
user.getSettings(req.uid, next);
@ -98,41 +97,22 @@ apiController.getConfig = function (req, res, next) {
config.bootswatchSkin = (settings.bootswatchSkin && settings.bootswatchSkin !== 'default') ? settings.bootswatchSkin : config.bootswatchSkin;
plugins.fireHook('filter:config.get', config, next);
},
], function (err, config) {
if (err) {
return next(err);
}
if (res.locals.isAPI) {
res.json(config);
} else {
next(null, config);
}
});
], callback);
};
apiController.renderWidgets = function (req, res, next) {
if (!req.query.template || !req.query.locations) {
return res.status(200).json({});
}
widgets.render(req.uid,
{
template: req.query.template,
url: req.query.url,
locations: req.query.locations,
isMobile: req.query.isMobile === 'true',
cid: req.query.cid,
apiController.getConfig = function (req, res, next) {
async.waterfall([
function (next) {
apiController.loadConfig(req, next);
},
req,
res,
function (err, widgets) {
if (err) {
return next(err);
function (config, next) {
if (res.locals.isAPI) {
res.json(config);
} else {
next(null, config);
}
res.status(200).json(widgets);
});
},
], next);
};
apiController.getPostData = function (pid, uid, callback) {

@ -53,7 +53,7 @@ helpers.notAllowed = function (req, res, error) {
helpers.redirect = function (res, url) {
if (res.locals.isAPI) {
res.set('X-Redirect', url).status(200).json(url);
res.set('X-Redirect', encodeURI(url)).status(200).json(url);
} else {
res.redirect(nconf.get('relative_path') + encodeURI(url));
}

@ -291,8 +291,8 @@ Controllers.confirmEmail = function (req, res) {
Controllers.robots = function (req, res) {
res.set('Content-Type', 'text/plain');
if (meta.config['robots.txt']) {
res.send(meta.config['robots.txt']);
if (meta.config['robots:txt']) {
res.send(meta.config['robots:txt']);
} else {
res.send('User-agent: *\n' +
'Disallow: ' + nconf.get('relative_path') + '/admin/\n' +

@ -6,7 +6,7 @@ helpers.toMap = function (data) {
var map = {};
for (var i = 0; i < data.length; i += 1) {
map[data[i]._key] = data[i];
data[i]._key = undefined;
delete data[i]._key;
}
return map;
};

@ -480,32 +480,33 @@ module.exports = function (db, module) {
return !done;
},
function (next) {
cursor.next(function (err, item) {
if (err) {
return next(err);
}
if (item === null) {
done = true;
} else {
ids.push(item.value);
}
if (ids.length < options.batch && (!done || ids.length === 0)) {
return next(null);
}
process(ids, function (err) {
if (err) {
return next(err);
async.waterfall([
function (next) {
cursor.next(next);
},
function (item, _next) {
if (item === null) {
done = true;
} else {
ids.push(item.value);
}
if (ids.length < options.batch && (!done || ids.length === 0)) {
return next(null);
}
process(ids, function (err) {
_next(err);
});
},
function (next) {
ids = [];
if (options.interval) {
setTimeout(next, options.interval);
} else {
next();
}
});
});
},
], next);
},
callback
);

@ -137,15 +137,9 @@ function getBundleMetadata(target, callback) {
var imports = cssImports + '\n' + lessImports;
imports = buildImports[target](imports);
next(null, imports);
next(null, { paths: paths, imports: imports });
},
], function (err, imports) {
if (err) {
return callback(err);
}
callback(null, { paths: paths, imports: imports });
});
], callback);
}
CSS.buildBundle = function (target, fork, callback) {

@ -9,6 +9,7 @@ var meta = require('../meta');
var plugins = require('../plugins');
var navigation = require('../navigation');
var translator = require('../translator');
var utils = require('../utils');
var controllers = {
api: require('../controllers/api'),
@ -152,6 +153,8 @@ module.exports = function (middleware) {
return { src: script };
});
addTimeagoLocaleScript(templateValues.scripts, res.locals.config.userLang);
if (req.route && req.route.path === '/') {
modifyTitle(templateValues);
}
@ -168,6 +171,11 @@ module.exports = function (middleware) {
], callback);
};
function addTimeagoLocaleScript(scripts, userLang) {
var languageCode = utils.userLangToTimeagoCode(userLang);
scripts.push({ src: nconf.get('relative_path') + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js' });
}
middleware.renderFooter = function (req, res, data, callback) {
async.waterfall([
function (next) {

@ -23,17 +23,5 @@ module.exports = function (middleware) {
next();
};
middleware.addExpiresHeaders = function (req, res, next) {
if (req.app.enabled('cache')) {
res.setHeader('Cache-Control', 'public, max-age=5184000');
res.setHeader('Expires', new Date(Date.now() + 5184000000).toUTCString());
} else {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Expires', new Date().toUTCString());
}
next();
};
};

@ -7,6 +7,7 @@ var winston = require('winston');
var plugins = require('../plugins');
var translator = require('../translator');
var widgets = require('../widgets');
module.exports = function (middleware) {
middleware.processRender = function (req, res, next) {
@ -49,6 +50,17 @@ module.exports = function (middleware) {
function (data, next) {
options = data.templateData;
widgets.render(req.uid, {
template: template + '.tpl',
url: options.url,
templateData: options,
req: req,
res: res,
}, next);
},
function (data, next) {
options.widgets = data;
res.locals.template = template;
options._locals = undefined;

@ -9,7 +9,6 @@ module.exports = function (app, middleware, controllers) {
app.use('/api', router);
router.get('/config', middleware.applyCSRF, controllers.api.getConfig);
router.get('/widgets/render', controllers.api.renderWidgets);
router.get('/me', middleware.checkGlobalPrivacySettings, controllers.user.getCurrentUser);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID);

@ -2,77 +2,10 @@
var express = require('express');
var nconf = require('nconf');
var winston = require('winston');
var user = require('../user');
var categories = require('../categories');
var topics = require('../topics');
var posts = require('../posts');
module.exports = function (app) {
var router = express.Router();
router.get('/uid/:uid', function (req, res) {
if (!req.params.uid) {
return res.redirect('/404');
}
user.getUserData(req.params.uid, function (err, data) {
if (err) {
winston.error(err);
}
if (data) {
res.send(data);
} else {
res.status(404).json({
error: "User doesn't exist!",
});
}
});
});
router.get('/cid/:cid', function (req, res) {
categories.getCategoryData(req.params.cid, function (err, data) {
if (err) {
winston.error(err);
}
if (data) {
res.send(data);
} else {
res.status(404).send("Category doesn't exist!");
}
});
});
router.get('/tid/:tid', function (req, res) {
topics.getTopicData(req.params.tid, function (err, data) {
if (err) {
winston.error(err);
}
if (data) {
res.send(data);
} else {
res.status(404).send("Topic doesn't exist!");
}
});
});
router.get('/pid/:pid', function (req, res) {
posts.getPostData(req.params.pid, function (err, data) {
if (err) {
winston.error(err);
}
if (data) {
res.send(data);
} else {
res.status(404).send("Post doesn't exist!");
}
});
});
router.get('/test', function (req, res) {
res.redirect(404);
});

@ -12,6 +12,7 @@ var categories = require('../categories');
var meta = require('../meta');
var helpers = require('../controllers/helpers');
var privileges = require('../privileges');
var db = require('../database');
var controllers404 = require('../controllers/404.js');
module.exports = function (app, middleware) {
@ -40,7 +41,7 @@ function validateTokenIfRequiresLogin(requiresLogin, cid, req, res, callback) {
async.waterfall([
function (next) {
user.getUserField(uid, 'rss_token', next);
db.getObjectField('user:' + uid, 'rss_token', next);
},
function (_token, next) {
if (token === _token) {

@ -8,6 +8,9 @@ var posts = require('../posts');
module.exports = function (Topics) {
Topics.getUserBookmark = function (tid, uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, null);
}
db.sortedSetScore('tid:' + tid + ':bookmarks', uid, callback);
};

@ -169,11 +169,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) {
return users.indexOf(uid) === idx;
});
var parentUids = _.uniq(parentPosts.map(function (postObj) {
return postObj && parseInt(postObj.uid, 10);
}));
user.getUsersFields(parentUids, ['username'], next);
},
@ -391,59 +389,70 @@ module.exports = function (Topics) {
};
function getPostReplies(pids, callerUid, callback) {
async.map(pids, function (pid, next) {
var replyPids;
var uids = [];
async.waterfall([
function (next) {
db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, next);
},
function (_replyPids, next) {
replyPids = _replyPids;
var count = 0;
async.until(function () {
return count === replyPids.length || uids.length === 6;
}, function (next) {
async.waterfall([
function (next) {
posts.getPostField(replyPids[count], 'uid', next);
},
function (uid, next) {
uid = parseInt(uid, 10);
if (uids.indexOf(uid) === -1) {
uids.push(uid);
}
count += 1;
next();
},
], next);
}, next);
},
function (next) {
async.parallel({
users: function (next) {
user.getUsersWithFields(uids, ['uid', 'username', 'userslug', 'picture'], callerUid, next);
},
timestampISO: function (next) {
posts.getPostField(replyPids[0], 'timestamp', function (err, timestamp) {
next(err, utils.toISOString(timestamp));
});
},
}, next);
},
function (replies, next) {
if (replies.users.length > 5) {
replies.users.shift();
replies.hasMore = true;
var arrayOfReplyPids;
var replyData;
var uniqueUids;
var uniquePids;
async.waterfall([
function (next) {
var keys = pids.map(function (pid) {
return 'pid:' + pid + ':replies';
});
db.getSortedSetsMembers(keys, next);
},
function (arrayOfPids, next) {
arrayOfReplyPids = arrayOfPids;
uniquePids = _.uniq(_.flatten(arrayOfPids));
posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp'], next);
},
function (_replyData, next) {
replyData = _replyData;
var uids = replyData.map(function (replyData) {
return replyData && replyData.uid;
});
uniqueUids = _.uniq(uids);
user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid, next);
},
function (userData, next) {
var uidMap = _.zipObject(uniqueUids, userData);
var pidMap = _.zipObject(uniquePids, replyData);
var returnData = arrayOfReplyPids.map(function (replyPids) {
var uidsUsed = {};
var currentData = {
hasMore: false,
users: [],
text: replyPids.length > 1 ? '[[topic:replies_to_this_post, ' + replyPids.length + ']]' : '[[topic:one_reply_to_this_post]]',
count: replyPids.length,
timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined,
};
replyPids.sort(function (a, b) {
return parseInt(a, 10) - parseInt(b, 10);
});
replyPids.forEach(function (replyPid) {
var replyData = pidMap[replyPid];
if (!uidsUsed[replyData.uid] && currentData.users.length < 6) {
currentData.users.push(uidMap[replyData.uid]);
uidsUsed[replyData.uid] = true;
}
});
if (currentData.users.length > 5) {
currentData.users.pop();
currentData.hasMore = true;
}
replies.count = replyPids.length;
replies.text = replies.count > 1 ? '[[topic:replies_to_this_post, ' + replies.count + ']]' : '[[topic:one_reply_to_this_post]]';
next(null, replies);
},
], next);
}, callback);
return currentData;
});
next(null, returnData);
},
], callback);
}
};

@ -95,7 +95,7 @@ module.exports = function (Topics) {
], callback);
}
/* not an orphan method, used in widget-essentials */
Topics.getLatestTopics = function (uid, start, stop, term, callback) {
async.waterfall([
function (next) {

@ -0,0 +1,22 @@
'use strict';
var async = require('async');
var batch = require('../../batch');
var db = require('../../database');
module.exports = {
name: 'Wipe all existing RSS tokens',
timestamp: Date.UTC(2017, 6, 5),
method: function (callback) {
var progress = this.progress;
batch.processSortedSet('users:joindate', function (uids, next) {
async.eachLimit(uids, 500, function (uid, next) {
progress.incr();
db.deleteObjectField('user:' + uid, 'rss_token', next);
}, next);
}, {
progress: progress,
}, callback);
},
};

@ -0,0 +1,35 @@
'use strict';
var async = require('async');
var db = require('../../database');
module.exports = {
name: 'Fix incorrect robots.txt schema',
timestamp: Date.UTC(2017, 6, 10),
method: function (callback) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function (config, next) {
if (!config) {
return callback();
}
// fix mongo nested data
if (config.robots && config.robots.txt) {
db.setObjectField('config', 'robots:txt', config.robots.txt, next);
} else if (typeof config['robots.txt'] === 'string' && config['robots.txt']) {
db.setObjectField('config', 'robots:txt', config['robots.txt'], next);
} else {
next();
}
},
function (next) {
db.deleteObjectField('config', 'robots', next);
},
function (next) {
db.deleteObjectField('config', 'robots.txt', next);
},
], callback);
},
};

@ -78,12 +78,24 @@ User.getUsersWithFields = function (uids, fields, uid, callback) {
function (results, next) {
results.userData.forEach(function (user, index) {
if (user) {
user.status = User.getStatus(user);
user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
user.banned_until = parseInt(user['banned:expire'], 10) || 0;
user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned';
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1;
if (user.hasOwnProperty('status')) {
user.status = User.getStatus(user);
}
if (user.hasOwnProperty('banned')) {
user.banned = parseInt(user.banned, 10) === 1;
}
if (user.hasOwnProperty('banned:expire')) {
user.banned_until = parseInt(user['banned:expire'], 10) || 0;
user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned';
}
if (user.hasOwnProperty(['email:confirmed'])) {
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1;
}
}
});
plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next);
@ -277,12 +289,30 @@ User.getModeratorUids = function (callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
function (cids, next) {
var groupNames = cids.map(function (cid) {
return 'cid:' + cid + ':privileges:moderate';
});
var groupNames = cids.reduce(function (memo, cid) {
memo.push('cid:' + cid + ':privileges:moderate');
memo.push('cid:' + cid + ':privileges:groups:moderate');
return memo;
}, []);
groups.getMembersOfGroups(groupNames, next);
},
function (memberSets, next) {
// Every other set is actually a list of user groups, not uids, so convert those to members
var sets = memberSets.reduce(function (memo, set, idx) {
if (idx % 2) {
memo.working.push(set);
} else {
memo.regular.push(set);
}
return memo;
}, { working: [], regular: [] });
groups.getMembersOfGroups(sets.working, function (err, memberSets) {
next(err, sets.regular.concat(memberSets || []));
});
},
function (memberSets, next) {
next(null, _.union.apply(_, memberSets));
},

@ -55,7 +55,7 @@ module.exports = function (User) {
var token;
async.waterfall([
function (next) {
User.getUserField(uid, 'rss_token', next);
db.getObjectField('user:' + uid, 'rss_token', next);
},
function (_token, next) {
token = _token || utils.generateUUID();

@ -122,6 +122,10 @@ module.exports = function (User) {
user.password = undefined;
}
if (user.rss_token) {
user.rss_token = undefined;
}
if (!parseInt(user.uid, 10)) {
user.uid = 0;
user.username = '[[global:guest]]';

@ -23,44 +23,31 @@ Digest.execute = function (payload, callback) {
return callback();
}
var subscribers;
async.waterfall([
function (next) {
async.parallel({
topics: async.apply(topics.getLatestTopics, 0, 0, 9, payload.interval),
subscribers: function (next) {
if (payload.subscribers) {
setImmediate(next, undefined, payload.subscribers);
} else {
Digest.getSubscribers(payload.interval, next);
}
},
}, next);
if (payload.subscribers) {
setImmediate(next, undefined, payload.subscribers);
} else {
Digest.getSubscribers(payload.interval, next);
}
},
function (data, next) {
subscribers = data.subscribers;
if (!data.subscribers.length) {
function (subscribers, next) {
if (!subscribers.length) {
return callback();
}
// Fix relative paths in topic data
data.topics.topics = data.topics.topics.map(function (topicObj) {
var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
if (user && user.picture && utils.isRelativeUrl(user.picture)) {
user.picture = nconf.get('base_url') + user.picture;
}
return topicObj;
});
var data = {
interval: payload.interval,
subscribers: subscribers,
};
data.interval = payload.interval;
Digest.send(data, next);
},
], function (err) {
], function (err, count) {
if (err) {
winston.error('[user/jobs] Could not send digests (' + payload.interval + '): ' + err.message);
} else {
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. ' + subscribers.length + ' email(s) sent.');
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. ' + count + ' email(s) sent.');
}
callback(err);
@ -116,12 +103,16 @@ Digest.send = function (data, callback) {
async.eachLimit(users, 100, function (userObj, next) {
async.waterfall([
function (next) {
user.notifications.getDailyUnread(userObj.uid, next);
async.parallel({
notifications: async.apply(user.notifications.getDailyUnread, userObj.uid),
topics: async.apply(topics.getPopular, data.interval, userObj.uid, 10),
}, next);
},
function (notifications, next) {
notifications = notifications.filter(Boolean);
function (data, next) {
var notifications = data.notifications.filter(Boolean);
// If there are no notifications and no new topics, don't bother sending a digest
if (!notifications.length && !data.topics.topics.length) {
if (!notifications.length && !data.topics.length) {
return next();
}
@ -131,6 +122,16 @@ Digest.send = function (data, callback) {
}
});
// Fix relative paths in topic data
data.topics = data.topics.map(function (topicObj) {
var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
if (user && user.picture && utils.isRelativeUrl(user.picture)) {
user.picture = nconf.get('base_url') + user.picture;
}
return topicObj;
});
emailer.send('digest', userObj.uid, {
subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
username: userObj.username,
@ -138,7 +139,7 @@ Digest.send = function (data, callback) {
url: nconf.get('url'),
site_title: meta.config.title || meta.config.browserTitle || 'NodeBB',
notifications: notifications,
recent: data.topics.topics,
recent: data.topics,
interval: data.interval,
});
next();
@ -147,6 +148,6 @@ Digest.send = function (data, callback) {
}, next);
},
], function (err) {
callback(err);
callback(err, data.subscribers.length);
});
};

@ -32,32 +32,32 @@
<div class="panel-body">
<div class="available-widgets">
<p>[[admin/extend/widgets:explanation]]</p>
<!-- IF !widgets.length -->
<!-- IF !availableWidgets.length -->
<div class="alert alert-info">[[none-installed, {config.relative_path}/admin/extend/plugins]]</div>
<!-- ENDIF !widgets.length -->
<!-- ENDIF !availableWidgets.length -->
<p>
<select id="widget-selector" class="form-control">
<!-- BEGIN widgets -->
<option value="{widgets.widget}">{widgets.name}</option>
<!-- END widgets -->
<!-- BEGIN availableWidgets -->
<option value="{availableWidgets.widget}">{availableWidgets.name}</option>
<!-- END availableWidgets -->
</select>
</p>
<div class="row">
<!-- BEGIN widgets -->
<!-- BEGIN availableWidgets -->
<div class="col-xs-12">
<div data-widget="{widgets.widget}" class="panel widget-panel panel-default pointer hide">
<div data-widget="{availableWidgets.widget}" class="panel widget-panel panel-default pointer hide">
<div class="panel-heading">
<strong>{widgets.name}</strong>
<small><br />{widgets.description}</small>
<strong>{availableWidgets.name}</strong>
<small><br />{availableWidgets.description}</small>
</div>
<div class="panel-body hidden">
<form>
{widgets.content}
{availableWidgets.content}
</form>
</div>
</div>
</div>
<!-- END widgets -->
<!-- END availableWidgets -->
</div>
</div>
</div>

@ -4,8 +4,11 @@
<div class="row">
<div class="col-md-9">
<div class="clearfix">
<div class="toggle">
<i class="fa fa-minus"></i>
</div>
<div class="icon" style="
color: {categories.color};
color: {categories.color};
background-color: {categories.bgColor};
<!-- IF categories.backgroundImage -->
background-image: url('{categories.backgroundImage}');
@ -15,24 +18,23 @@
</div>
<div class="information">
<h5 class="category-header">{categories.name}</h5>
<p class="description">{categories.descriptionParsed}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="clearfix pull-right text-right">
<div class="btn-group">
<button data-cid="{categories.cid}" data-action="toggle" data-disabled="{categories.disabled}" class="btn <!-- IF categories.disabled -->btn-primary<!-- ELSE -->btn-danger<!-- ENDIF categories.disabled -->">
<button data-cid="{categories.cid}" data-action="toggle" data-disabled="{categories.disabled}" class="btn btn-sm <!-- IF categories.disabled -->btn-primary<!-- ELSE -->btn-danger<!-- ENDIF categories.disabled -->">
<!-- IF categories.disabled -->
[[admin/manage/categories:enable]]
<!-- ELSE -->
[[admin/manage/categories:disable]]
<!-- ENDIF categories.disabled -->
</button>
<a href="./categories/{categories.cid}/analytics" class="btn btn-default">
<a href="./categories/{categories.cid}/analytics" class="btn btn-default btn-sm">
<i class="fa fa-line-chart"></i>
</a>
<a href="./categories/{categories.cid}" class="btn btn-default">
<a href="./categories/{categories.cid}" class="btn btn-default btn-sm">
[[admin/manage/categories:edit]]
</a>
</div>

@ -5,7 +5,7 @@
<div class="col-sm-10 col-xs-12">
<form>
<strong>[[admin/settings/web-crawler:robots-txt]]</strong><br />
<textarea class="form-control" data-field="robots.txt"></textarea>
<textarea class="form-control" data-field="robots:txt"></textarea>
</form>
</div>
</div>

@ -72,7 +72,7 @@ admin.get = function (callback) {
callback(false, {
templates: templates,
areas: widgetData.areas,
widgets: widgetData.widgets,
availableWidgets: widgetData.widgets,
});
});
});

@ -3,27 +3,35 @@
var async = require('async');
var winston = require('winston');
var templates = require('templates.js');
var _ = require('lodash');
var plugins = require('../plugins');
var translator = require('../translator');
var db = require('../database');
var apiController = require('../controllers/api');
var widgets = module.exports;
widgets.render = function (uid, area, req, res, callback) {
if (!area.locations || !area.template) {
widgets.render = function (uid, options, callback) {
if (!options.template) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
widgets.getAreas(['global', area.template], area.locations, next);
widgets.getWidgetDataForTemplates(['global', options.template], next);
},
function (data, next) {
var widgetsByLocation = {};
async.map(area.locations, function (location, done) {
widgetsByLocation[location] = data.global[location].concat(data[area.template][location]);
delete data.global.drafts;
var locations = _.uniq(Object.keys(data.global).concat(Object.keys(data[options.template])));
var returnData = {};
async.each(locations, function (location, done) {
widgetsByLocation[location] = (data.global[location] || []).concat(data[options.template][location] || []);
if (!widgetsByLocation[location].length) {
return done(null, { location: location, widgets: [] });
@ -33,28 +41,43 @@ widgets.render = function (uid, area, req, res, callback) {
if (!widget || !widget.data ||
(!!widget.data['hide-registered'] && uid !== 0) ||
(!!widget.data['hide-guests'] && uid === 0) ||
(!!widget.data['hide-mobile'] && area.isMobile)) {
(!!widget.data['hide-mobile'] && options.req.useragent.isMobile)) {
return next();
}
renderWidget(widget, uid, area, req, res, next);
}, function (err, result) {
done(err, { location: location, widgets: result.filter(Boolean) });
renderWidget(widget, uid, options, next);
}, function (err, renderedWidgets) {
if (err) {
return done(err);
}
returnData[location] = renderedWidgets.filter(Boolean);
done();
});
}, next);
}, function (err) {
next(err, returnData);
});
},
], callback);
};
function renderWidget(widget, uid, area, req, res, callback) {
function renderWidget(widget, uid, options, callback) {
async.waterfall([
function (next) {
if (options.res.locals.isAPI) {
apiController.loadConfig(options.req, next);
} else {
next(null, options.res.locals.config);
}
},
function (config, next) {
var templateData = _.assign(options.templateData, { config: config });
plugins.fireHook('filter:widget.render:' + widget.widget, {
uid: uid,
area: area,
area: options,
templateData: templateData,
data: widget.data,
req: req,
res: res,
req: options.req,
res: options.res,
}, next);
},
function (data, next) {
@ -84,23 +107,28 @@ function renderWidget(widget, uid, area, req, res, callback) {
], callback);
}
widgets.getAreas = function (templates, locations, callback) {
widgets.getWidgetDataForTemplates = function (templates, callback) {
var keys = templates.map(function (tpl) {
return 'widgets:' + tpl;
});
async.waterfall([
function (next) {
db.getObjectsFields(keys, locations, next);
db.getObjects(keys, next);
},
function (data, next) {
var returnData = {};
templates.forEach(function (template, index) {
returnData[template] = returnData[template] || {};
var templateWidgetData = data[index] || {};
var locations = Object.keys(templateWidgetData);
locations.forEach(function (location) {
if (data && data[index] && data[index][location]) {
if (templateWidgetData && templateWidgetData[location]) {
try {
returnData[template][location] = JSON.parse(data[index][location]);
returnData[template][location] = JSON.parse(templateWidgetData[location]);
} catch (err) {
winston.error('can not parse widget data. template: ' + template + ' location: ' + location);
returnData[template][location] = [];

@ -687,22 +687,23 @@ describe('Controllers', function () {
], done);
});
it('should return {} if there is no template or locations', function (done) {
request(nconf.get('url') + '/api/widgets/render', { json: true }, function (err, res, body) {
it('should return {} if there are no widgets', function (done) {
request(nconf.get('url') + '/api/category/' + cid, { json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
assert.equal(Object.keys(body), 0);
assert(body.widgets);
assert.equal(Object.keys(body.widgets), 0);
done();
});
});
it('should render templates', function (done) {
var url = nconf.get('url') + '/api/widgets/render?template=categories.tpl&url=&isMobile=false&locations%5B%5D=sidebar&locations%5B%5D=footer&locations%5B%5D=header';
var url = nconf.get('url') + '/api/categories';
request(url, { json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
assert(body.widgets);
assert(body.widgets.sidebar);
done();
});
});

@ -155,6 +155,51 @@ describe('User', function () {
});
});
describe('.getModeratorUids()', function () {
before(function (done) {
groups.join('cid:1:privileges:moderate', 1, done);
});
it('should retrieve all users with moderator bit in category privilege', function (done) {
User.getModeratorUids(function (err, uids) {
assert.ifError(err);
assert.strictEqual(1, uids.length);
assert.strictEqual(1, parseInt(uids[0], 10));
done();
});
});
after(function (done) {
groups.leave('cid:1:privileges:moderate', 1, done);
});
});
describe('.getModeratorUids()', function () {
before(function (done) {
async.series([
async.apply(groups.create, { name: 'testGroup' }),
async.apply(groups.join, 'cid:1:privileges:groups:moderate', 'testGroup'),
async.apply(groups.join, 'testGroup', 1),
], done);
});
it('should retrieve all users with moderator bit in category privilege', function (done) {
User.getModeratorUids(function (err, uids) {
assert.ifError(err);
assert.strictEqual(1, uids.length);
assert.strictEqual(1, parseInt(uids[0], 10));
done();
});
});
after(function (done) {
async.series([
async.apply(groups.leave, 'cid:1:privileges:groups:moderate', 'testGroup'),
async.apply(groups.destroy, 'testGroup'),
], done);
});
});
describe('.isReadyToPost()', function () {
it('should error when a user makes two posts in quick succession', function (done) {
Meta.config = Meta.config || {};

Loading…
Cancel
Save