Merge branch 'master' into develop

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

@ -2,7 +2,7 @@
{ {
"widget": "html", "widget": "html",
"data" : { "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":"", "title":"",
"container":"" "container":""
} }

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

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

@ -59,7 +59,7 @@
"thread_tools.unlock": "Thema öffnen", "thread_tools.unlock": "Thema öffnen",
"thread_tools.move": "Thema verschieben", "thread_tools.move": "Thema verschieben",
"thread_tools.move_all": "Alle 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.fork": "Thema aufspalten",
"thread_tools.delete": "Thema löschen", "thread_tools.delete": "Thema löschen",
"thread_tools.delete-posts": "Beiträge entfernen", "thread_tools.delete-posts": "Beiträge entfernen",

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

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

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

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

@ -31,6 +31,12 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri
Categories.toggle([cid].concat(children), !disabled); Categories.toggle([cid].concat(children), !disabled);
return false; 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 () { Categories.throwCreateModal = function () {

@ -204,7 +204,7 @@ $(document).ready(function () {
} }
ajaxify.loadScript(tpl_url, done); 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 }); $(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-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic);
$('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost); $('#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() { function handleSavePreferences() {
$('#save-preferences').on('click', function () { $('#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]]'); app.alertSuccess('[[search:search-preferences-saved]]');
return false; return false;
}); });

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

@ -580,39 +580,13 @@
prepareDOM: function prepareDOM() { prepareDOM: function prepareDOM() {
// Load the appropriate timeago locale file, // Load the appropriate timeago locale file,
// and correct NodeBB language codes to timeago codes, if necessary // and correct NodeBB language codes to timeago codes, if necessary
var languageCode; var languageCode = utils.userLangToTimeagoCode(config.userLang);
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;
}
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
jQuery('.timeago').timeago();
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 () {
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { // Switch back to long-form
// Switch back to long-form adaptor.toggleTimeagoShorthand();
adaptor.toggleTimeagoShorthand();
});
}); });
// Add directional code if necessary // Add directional code if necessary

@ -103,7 +103,16 @@
hasLanguageKey: function (input) { hasLanguageKey: function (input) {
return utils.languageKeyRegex.test(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 // shallow objects merge
merge: function () { merge: function () {
var result = {}; var result = {};

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

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

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

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

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

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

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

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

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

@ -9,6 +9,7 @@ var meta = require('../meta');
var plugins = require('../plugins'); var plugins = require('../plugins');
var navigation = require('../navigation'); var navigation = require('../navigation');
var translator = require('../translator'); var translator = require('../translator');
var utils = require('../utils');
var controllers = { var controllers = {
api: require('../controllers/api'), api: require('../controllers/api'),
@ -152,6 +153,8 @@ module.exports = function (middleware) {
return { src: script }; return { src: script };
}); });
addTimeagoLocaleScript(templateValues.scripts, res.locals.config.userLang);
if (req.route && req.route.path === '/') { if (req.route && req.route.path === '/') {
modifyTitle(templateValues); modifyTitle(templateValues);
} }
@ -168,6 +171,11 @@ module.exports = function (middleware) {
], callback); ], 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) { middleware.renderFooter = function (req, res, data, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {

@ -23,17 +23,5 @@ module.exports = function (middleware) {
next(); 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 plugins = require('../plugins');
var translator = require('../translator'); var translator = require('../translator');
var widgets = require('../widgets');
module.exports = function (middleware) { module.exports = function (middleware) {
middleware.processRender = function (req, res, next) { middleware.processRender = function (req, res, next) {
@ -49,6 +50,17 @@ module.exports = function (middleware) {
function (data, next) { function (data, next) {
options = data.templateData; 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; res.locals.template = template;
options._locals = undefined; options._locals = undefined;

@ -9,7 +9,6 @@ module.exports = function (app, middleware, controllers) {
app.use('/api', router); app.use('/api', router);
router.get('/config', middleware.applyCSRF, controllers.api.getConfig); 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('/me', middleware.checkGlobalPrivacySettings, controllers.user.getCurrentUser);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID); router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID);

@ -2,77 +2,10 @@
var express = require('express'); var express = require('express');
var nconf = require('nconf'); 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) { module.exports = function (app) {
var router = express.Router(); 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) { router.get('/test', function (req, res) {
res.redirect(404); res.redirect(404);
}); });

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

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

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

@ -95,7 +95,7 @@ module.exports = function (Topics) {
], callback); ], callback);
} }
/* not an orphan method, used in widget-essentials */
Topics.getLatestTopics = function (uid, start, stop, term, callback) { Topics.getLatestTopics = function (uid, start, stop, term, callback) {
async.waterfall([ async.waterfall([
function (next) { 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) { function (results, next) {
results.userData.forEach(function (user, index) { results.userData.forEach(function (user, index) {
if (user) { if (user) {
user.status = User.getStatus(user);
user.administrator = results.isAdmin[index]; user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
user.banned_until = parseInt(user['banned:expire'], 10) || 0; if (user.hasOwnProperty('status')) {
user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned'; user.status = User.getStatus(user);
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1; }
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); plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next);
@ -277,12 +289,30 @@ User.getModeratorUids = function (callback) {
async.waterfall([ async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
function (cids, next) { function (cids, next) {
var groupNames = cids.map(function (cid) { var groupNames = cids.reduce(function (memo, cid) {
return 'cid:' + cid + ':privileges:moderate'; memo.push('cid:' + cid + ':privileges:moderate');
}); memo.push('cid:' + cid + ':privileges:groups:moderate');
return memo;
}, []);
groups.getMembersOfGroups(groupNames, next); 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) { function (memberSets, next) {
next(null, _.union.apply(_, memberSets)); next(null, _.union.apply(_, memberSets));
}, },

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

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

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

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

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

@ -5,7 +5,7 @@
<div class="col-sm-10 col-xs-12"> <div class="col-sm-10 col-xs-12">
<form> <form>
<strong>[[admin/settings/web-crawler:robots-txt]]</strong><br /> <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> </form>
</div> </div>
</div> </div>

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

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

@ -687,22 +687,23 @@ describe('Controllers', function () {
], done); ], done);
}); });
it('should return {} if there is no template or locations', function (done) { it('should return {} if there are no widgets', function (done) {
request(nconf.get('url') + '/api/widgets/render', { json: true }, function (err, res, body) { request(nconf.get('url') + '/api/category/' + cid, { json: true }, function (err, res, body) {
assert.ifError(err); assert.ifError(err);
assert.equal(res.statusCode, 200); assert.equal(res.statusCode, 200);
assert(body); assert(body.widgets);
assert.equal(Object.keys(body), 0); assert.equal(Object.keys(body.widgets), 0);
done(); done();
}); });
}); });
it('should render templates', function (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) { request(url, { json: true }, function (err, res, body) {
assert.ifError(err); assert.ifError(err);
assert.equal(res.statusCode, 200); assert.equal(res.statusCode, 200);
assert(body); assert(body.widgets);
assert(body.widgets.sidebar);
done(); 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 () { describe('.isReadyToPost()', function () {
it('should error when a user makes two posts in quick succession', function (done) { it('should error when a user makes two posts in quick succession', function (done) {
Meta.config = Meta.config || {}; Meta.config = Meta.config || {};

Loading…
Cancel
Save