Merge branch 'master' into develop

v1.18.x
Julian Lam 7 years ago
commit 457194b333

@ -38,5 +38,5 @@
"bookmarkThreshold": 5,
"topicsPerList": 20,
"autoDetectLang": 1,
"privileges:flag": 0
"min:rep:flag": 0
}

@ -69,9 +69,9 @@
"nodebb-plugin-spam-be-gone": "0.5.1",
"nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "5.0.0",
"nodebb-theme-persona": "7.2.11",
"nodebb-theme-persona": "7.2.16",
"nodebb-theme-slick": "1.1.2",
"nodebb-theme-vanilla": "8.1.5",
"nodebb-theme-vanilla": "8.1.7",
"nodebb-widget-essentials": "4.0.1",
"nodemailer": "4.4.1",
"passport": "^0.4.0",

@ -10,6 +10,7 @@ var less = require('less');
var async = require('async');
var uglify = require('uglify-js');
var nconf = require('nconf');
var _ = require('lodash');
var Benchpress = require('benchpressjs');
var app = express();
@ -103,14 +104,16 @@ function welcome(req, res) {
}
function install(req, res) {
req.setTimeout(0);
var setupEnvVars = _.assign({}, process.env);
for (var i in req.body) {
if (req.body.hasOwnProperty(i) && !process.env.hasOwnProperty(i)) {
process.env[i.replace(':', '__')] = req.body[i];
setupEnvVars[i.replace(':', '__')] = req.body[i];
}
}
var child = require('child_process').fork('app', ['--setup'], {
env: process.env,
env: setupEnvVars,
});
child.on('close', function (data) {

@ -4,5 +4,5 @@
"home-page-route": "Startseitenpfad",
"custom-route": "Eigener Startseitenpfad",
"allow-user-home-pages": "Benutzern eigene Startseiten erlauben",
"home-page-title": "Title of the home page (default \"Home\")"
"home-page-title": "Titel der Startseite (Standardmäßig \"Home\")"
}

@ -27,8 +27,8 @@
"pills.banned": "Gebannt",
"pills.search": "Benutzer Suche",
"search.uid": "By User ID",
"search.uid-placeholder": "Enter a user ID to search",
"search.uid": "Nach Benutzer-ID",
"search.uid-placeholder": "Gib eine Benutzer-ID ein um danach zu suchen",
"search.username": "Nach Nutzernamen",
"search.username-placeholder": "Einen Nutzernamen eingeben, um danach zu suchen",
"search.email": "Nach E-Mail",
@ -71,15 +71,15 @@
"alerts.lockout-reset-success": "Ausschlüsse zurückgesetzt",
"alerts.flag-reset-success": "Meldung(en) zurückgesetzt!",
"alerts.no-remove-yourself-admin": "Du kannst dich nicht selbst als Administrator degradieren!",
"alerts.make-admin-success": "User is now administrator.",
"alerts.confirm-remove-admin": "Do you really want to remove this administrator?",
"alerts.remove-admin-success": "User is no longer administrator.",
"alerts.make-global-mod-success": "User is now global moderator.",
"alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?",
"alerts.remove-global-mod-success": "User is no longer global noderator",
"alerts.make-moderator-success": "User is now moderator.",
"alerts.confirm-remove-moderator": "Do you really want to remove this moderator?",
"alerts.remove-moderator-success": "User is no longer moderator.",
"alerts.make-admin-success": "Der Benutzer ist nun ein Administrator",
"alerts.confirm-remove-admin": "Willst du wirklich diesen Administrator entfernen?",
"alerts.remove-admin-success": "Der Benutzer ist kein Administrator mehr",
"alerts.make-global-mod-success": "Der Benutzer ist nun ein globaler Moderator",
"alerts.confirm-remove-global-mod": "Willst du wirklich diesen globalen Moderator entfernen?",
"alerts.remove-global-mod-success": "Der Benutzer ist kein globaler Moderator mehr",
"alerts.make-moderator-success": "Der Benutzer ist nun ein Moderator",
"alerts.confirm-remove-moderator": "Willst du wirklich diesen Moderator entfernen?",
"alerts.remove-moderator-success": "Der Benutzer ist kein Moderator mehr",
"alerts.confirm-validate-email": "Möchtest Du wirklich die E-Mails dieser Benutzer/dieses Benutzers bestätigen?",
"alerts.validate-email-success": "E-Mails bestätigt",
"alerts.password-reset-confirm": "Möchtest Du wirklich (eine) Passwort-Reset-Email(s) an diese(n) Benutzer schicken?",

@ -9,7 +9,7 @@
"section-manage": "Verwalten",
"manage/categories": "Kategorien",
"manage/privileges": "Privileges",
"manage/privileges": "Privilegien",
"manage/tags": "Tags",
"manage/users": "Benutzer",
"manage/admins-mods": "Admins & Mods",

@ -6,6 +6,6 @@
"max-length": "Maximale Chatnachrichtenlänge",
"max-room-size": "Maximale Anzahl an Nutzern pro Chat-Room",
"delay": "Zeit zwischen Chatnachrichten in Millisekunden",
"restrictions.seconds-edit-after": "Number of seconds before users are allowed to edit chat messages after posting. (0 disabled)",
"restrictions.seconds-delete-after": "Number of seconds before users are allowed to delete chat messages after posting. (0 disabled)"
"restrictions.seconds-edit-after": "Zeit in Sekunden bevor Benutzer Chat-Nachrichten editieren dürfen, nachdem sie erstellt wurden. (0 bedeutet deaktiviert)",
"restrictions.seconds-delete-after": "Zeit in Sekunden bevor Benutzer Chat-Nachrichten löschen dürfen, nachdem sie erstellt wurden. (0 bedeutet deaktiviert)"
}

@ -6,25 +6,25 @@
"sorting.most-votes": "Meiste Bewertungen",
"sorting.most-posts": "Meiste Beiträge",
"sorting.topic-default": "Standardmäßige Themensortierung",
"length": "Post Length",
"length": "Beitragslänge",
"restrictions": "Posting beschränkungen",
"restrictions-new": "New User Restrictions",
"restrictions-new": "Beschränkungen für neue Benutzer",
"restrictions.post-queue": "Beitragswarteschlange verwenden",
"restrictions-new.post-queue": "Enable new user restrictions",
"restrictions-new.post-queue": "Aktiviere Beschränkungen für neue Benutzer",
"restrictions.post-queue-help": "Das verwenden der Beitragswarteschlange wird Beiträge von neuen Benutzern in eine Warteschlange zur Genehmigung setzen.",
"restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users.",
"restrictions.seconds-between": "Seconds between posts",
"restrictions.seconds-between-new": "Seconds between posts for new users",
"restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted",
"restrictions-new.post-queue-help": "Das aktivieren von Beschränkungen für neue Benutzer wird von neuen Benutzern erstelltw Beiträge beschränken.",
"restrictions.seconds-between": "Sekunden zwischen Beiträgen",
"restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer",
"restrictions.rep-threshold": "Mindesreputation bevor die Beschränkungen aufgehoben werden",
"restrictions.seconds-defore-new": "Sekunden befor ein neuer Nutzer einen Beitrag erstellen kann",
"restrictions.seconds-edit-after": "Number of seconds before users are allowed to edit posts after posting. (0 disabled)",
"restrictions.seconds-delete-after": "Number of seconds before users are allowed to delete posts after posting. (0 disabled)",
"restrictions.seconds-edit-after": "Zeit in Sekunden bevor Benutzer ihre Beiträge editieren dürfen, nachdem sie erstellt wurden. (0 bedeutet deaktiviert)",
"restrictions.seconds-delete-after": "Zeit in Sekunden bevor Benutzer ihre Beiträge löschen dürfen, nachdem sie erstellt wurden. (0 bedeutet deaktiviert)",
"restrictions.replies-no-delete": "Anzahl der Antworten auf einen Thema, die Benötigt werden um das löschen des Themas durch den Besitzer zu verhindern. (0 = deaktiviert)",
"restrictions.min-title-length": "Minimale Titellänge",
"restrictions.max-title-length": "Maximale Titellänge",
"restrictions.min-post-length": "Minimale Beitragslänge",
"restrictions.max-post-length": "Maximale Beitragslänge",
"restrictions.days-until-stale": "Days until topic is considered stale",
"restrictions.days-until-stale": "Tage bis ein Thema als alt angesehen wird",
"restrictions.stale-help": "Wenn ein Thema als \"veraltet\" angesehen wird, wird Nutzern die versuchen diesem Thema zu antworten eine Warnung gezeigt",
"timestamp": "Zeitstempel",
"timestamp.cut-off": "Tageslimit für Relative Zeitangaben (in Tagen)",

@ -17,7 +17,7 @@
"invalid-login-credentials": "Ungültige Zugangsdaten",
"invalid-username-or-password": "Bitte gib sowohl einen Benutzernamen als auch ein Passwort an",
"invalid-search-term": "Ungültige Suchanfrage",
"invalid-url": "Invalid URL",
"invalid-url": "Ungültige URL",
"csrf-invalid": "Dein Login war nicht erfolgreich da wahrscheinlich deine Sitzung abgelaufen ist. Bitte versuche es noch einmal",
"invalid-pagination-value": "Ungültige Seitennummerierung, muss mindestens %1 und maximal %2 sein",
"username-taken": "Der Benutzername ist bereits vergeben",
@ -114,8 +114,8 @@
"cant-edit-chat-message": "Du darfst diese Nachricht nicht ändern",
"cant-remove-last-user": "Du kannst den letzten Benutzer nicht entfernen",
"cant-delete-chat-message": "Du darfst diese Nachricht nicht löschen",
"chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting",
"chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting",
"chat-edit-duration-expired": "Du darfst Chat-Nachrichten nur bis zu %1 Sekunde(n) nach der erstellung verändern",
"chat-delete-duration-expired": "Du darfst Chat-Nachrichten nur bis zu %1 Sekunde(n) nach der erstellung löschen",
"already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.",
"reputation-system-disabled": "Das Reputationssystem ist deaktiviert.",
"downvoting-disabled": "Downvotes sind deaktiviert.",

@ -53,7 +53,7 @@
"topics": "Themen",
"posts": "Beiträge",
"best": "Bestbewertet",
"votes": "Votes",
"votes": "Stimmen",
"upvoters": "Upvoter",
"upvoted": "Positiv bewertet",
"downvoters": "Downvoter",

@ -6,7 +6,7 @@
"popular-month": "Beliebte Themen dieses Monats",
"popular-alltime": "Beliebteste Themen",
"recent": "Neueste Themen",
"top": "Top Voted Topics",
"top": "Bestbewertetste Themen",
"moderator-tools": "Moderator-Werkzeuge",
"flagged-content": "Gemeldeter Inhalt",
"ip-blacklist": "IP Blacklist",

@ -1,7 +1,7 @@
{
"custom-css": "Custom CSS",
"custom-css.description": "Enter your own CSS declarations here, which will be applied after all other styles.",
"custom-css.enable": "Enable Custom CSS",
"custom-css": "Custom CSS/LESS",
"custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.",
"custom-css.enable": "Enable Custom CSS/LESS",
"custom-js": "Custom Javascript",
"custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.",

@ -6,6 +6,7 @@
"description": "Select tags via clicking and/or dragging, use shift to select multiple.",
"create": "Create Tag",
"modify": "Modify Tags",
"rename": "Rename Tags",
"delete": "Delete Selected Tags",
"search": "Search for tags...",
"settings": "Click <a href=\"%1\">here</a> to visit the tag settings page.",

@ -5,5 +5,8 @@
"votes-are-public": "All Votes Are Public",
"thresholds": "Activity Thresholds",
"min-rep-downvote": "Minimum reputation to downvote posts",
"min-rep-flag": "Minimum reputation to flag posts"
"min-rep-flag": "Minimum reputation to flag posts",
"min-rep-website": "Minimum reputation to add \"Website\" to user profile",
"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
"min-rep-signature": "Minimum reputation to add \"Signature\" to user profile"
}

@ -148,6 +148,9 @@
"downvoting-disabled": "Downvoting is disabled",
"not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post",
"not-enough-reputation-to-flag": "You do not have enough reputation to flag this post",
"not-enough-reputation-min-rep-website": "You do not have enough reputation to add a website",
"not-enough-reputation-min-rep-aboutme": "You do not have enough reputation to add an about me",
"not-enough-reputation-min-rep-signature": "You do not have enough reputation to add a signature",
"already-flagged": "You have already flagged this post",
"self-vote": "You cannot vote on your own post",

@ -31,8 +31,8 @@
"notices": "Informations",
"restart-not-required": "Pas de redémarrage nécessaire",
"restart-required": "Redémarrage requis",
"search-plugin-installed": "Recherche un plugin installé",
"search-plugin-not-installed": "Rechercher un plugin non installé",
"search-plugin-installed": "Le plugin de recherche est installé",
"search-plugin-not-installed": "Le plugin de recherche n'est pas installé",
"search-plugin-tooltip": "Installer un plugin de recherche depuis la page des plugins pour activer la fonctionnalité de recherche",
"control-panel": "Contrôle du système",

@ -17,7 +17,7 @@
"invalid-login-credentials": "Неважећи акредитиви за пријављивање",
"invalid-username-or-password": "Молимо наведите и корисничко име и лозинку",
"invalid-search-term": "Неисправан упит за претрагу",
"invalid-url": "Invalid URL",
"invalid-url": "Неважећа адреса",
"csrf-invalid": "Нисмо успели да вас пријавимо, вероватно због истека сесије. Молимо покушајте поново",
"invalid-pagination-value": "Неважећа вредност приликом нумерисања страница, мора бити најмање %1 а највише %2 ",
"username-taken": "Корисничко име је заузето",
@ -114,8 +114,8 @@
"cant-edit-chat-message": "Није вам дозвољено да уређујете ову поруку",
"cant-remove-last-user": "Не можете уклонити последњег корисника",
"cant-delete-chat-message": "Није вам дозвољено да избришете ову поруку",
"chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting",
"chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting",
"chat-edit-duration-expired": "Време у којем вам је дозвољено уређивање порука ћаскања након објављивања: %1 сек.",
"chat-delete-duration-expired": "Време у којем вам је дозвољено брисање порука ћаскања након објављивања: %1 сек.",
"already-voting-for-this-post": "Већ сте гласали за ову поруку.",
"reputation-system-disabled": "Угледи су онемогућени.",
"downvoting-disabled": "Негативно гласање је онемогућено",

@ -53,7 +53,7 @@
"topics": "Теме",
"posts": "Поруке",
"best": "Најбоље",
"votes": "Votes",
"votes": "Гласови",
"upvoters": "Позитивно гласали",
"upvoted": "Позитивно гласано",
"downvoters": "Негативно гласали",

@ -6,7 +6,7 @@
"popular-month": "Популарне теме овог месеца",
"popular-alltime": "Популарне теме свих времена",
"recent": "Недавне теме",
"top": "Top Voted Topics",
"top": "Најгласаније теме",
"moderator-tools": "Алати модератора",
"flagged-content": "Садржај означен заставицом",
"ip-blacklist": "Црна листа IP адреса",

@ -52,7 +52,7 @@
"not-watching.description": "Немој ме обавештавати о новим одговорима.<br/>Прикажи тему у непрочитаним ако категорија није игнорисана.",
"ignoring.description": "Немој ме обавештавати о новим одговорима.<br/>Не приказуј тему у непрочитаним",
"thread_tools.title": "Алатке теме",
"thread_tools.markAsUnreadForAll": "Mark Unread For All",
"thread_tools.markAsUnreadForAll": "Означи као непрочитано за све",
"thread_tools.pin": "Закачи тему",
"thread_tools.unpin": "Откачи тему",
"thread_tools.lock": "Закључај тему",

@ -30,7 +30,7 @@
"notif.chat.unsub.info": "Bu bildirim şectiğiniz ayarlar yüzünden gönderildi.",
"notif.post.cta": "Konunun tamamını okumak için buraya tıklayın",
"notif.post.unsub.info": "Bu yazı bildirimi size abonelik ayarlarınız nedeni ile gönderilmiştir.",
"notif.cta": "Click here to go to forum",
"notif.cta": "Foruma gitmek için buraya tıklayın",
"test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.",
"unsub.cta": "Buraya tıklayarak ayarlarınızı değiştirebilirsiniz.",
"banned.subject": "%1 'den yasaklandınız",

@ -53,7 +53,7 @@
"topics": "Başlık",
"posts": "İleti",
"best": "En İyi",
"votes": "Votes",
"votes": "Oy",
"upvoters": "Artı",
"upvoted": "Artı",
"downvoters": "Eksi",

@ -6,7 +6,7 @@
"popular-month": "Bu ayki popüler başlıklar",
"popular-alltime": "En popüler başlıklar",
"recent": "Güncel Konular",
"top": "Top Voted Topics",
"top": "En Çok Oylanan Başlıklar",
"moderator-tools": "Moderatör Araçları",
"flagged-content": "Bayraklanan İçerik",
"ip-blacklist": "IP Kara Listesi",
@ -45,7 +45,7 @@
"account/bookmarks": "%1'in yer imine eklenmiş iletiler",
"account/settings": "Kullanıcı Ayarları",
"account/watched": "%1 tarafından izlenen konular",
"account/ignored": "Topics ignored by %1",
"account/ignored": "%1 tarafından konu yok sayıldı",
"account/upvoted": "%1 tarafından artılanan gönderiler",
"account/downvoted": "%1 tarafından eksilenen gönderiler",
"account/best": "%1 tarafından en iyi gönderiler",

@ -85,7 +85,7 @@
"has_no_posts": "Bu kullanıcı henüz herhangi bir ileti yazmamış.",
"has_no_topics": "Bu kullanıcı henüz hiç bir başlık açmamış.",
"has_no_watched_topics": "Bu kullanıcı henüz hiç bir başlık okumamış.",
"has_no_ignored_topics": "This user hasn't ignored any topics yet.",
"has_no_ignored_topics": "Bu kullanıcı henüz hiçbir başlığı yok saymadı.",
"has_no_upvoted_posts": "Bu kullanıcı henüz hiç bir gönderiyi artılamamış.",
"has_no_downvoted_posts": "Bu kullanıcı henüz hiç bir gönderiyi eksilememiş.",
"has_no_voted_posts": "Bu kullanıcının hiç oylanmış gönderisi yok.",
@ -101,11 +101,11 @@
"outgoing-message-sound": "Giden ileti sesi",
"notification-sound": "Bildirim sesi",
"no-sound": "Ses yok",
"upvote-notif-freq": "Upvote Notification Frequency",
"upvote-notif-freq": "Artı Oy Bildiri Sıklığı",
"upvote-notif-freq.all": "Büyün Artı Oylar",
"upvote-notif-freq.everyTen": "Every Ten Upvotes",
"upvote-notif-freq.logarithmic": "On 10, 100, 1000...",
"upvote-notif-freq.disabled": "Disabled",
"upvote-notif-freq.everyTen": "Her Artı On Oy",
"upvote-notif-freq.logarithmic": "10, 100, 1000...",
"upvote-notif-freq.disabled": "Devre dışı",
"browsing": "Tarayıcı Ayaları",
"open_links_in_new_tab": "Dışarı giden bağlantıları yeni sekmede aç",
"enable_topic_searching": "Konu içi aramayı aktive et",
@ -126,9 +126,9 @@
"sso.title": "Tek giriş servisleri",
"sso.associated": "Birleştirilmiş",
"sso.not-associated": "Birleştirmek için buraya tıklayın",
"sso.dissociate": "Dissociate",
"sso.dissociate-confirm-title": "Confirm Dissociation",
"sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?",
"sso.dissociate": "Ayrış",
"sso.dissociate-confirm-title": "Ayrışmayı Onayla",
"sso.dissociate-confirm": "%1 'den ayrışmak istediğinizden emin misiniz?",
"info.latest-flags": "Son Bayraklar",
"info.no-flags": "Hiç bayraklanan bir ileti bulunamadı",
"info.ban-history": "Yasaklama Olayları",

@ -4,5 +4,5 @@
"home-page-route": "主页路由",
"custom-route": "自定义路由",
"allow-user-home-pages": "允许用户主页",
"home-page-title": "Title of the home page (default \"Home\")"
"home-page-title": "首页标题(默认“Home”)"
}

@ -27,8 +27,8 @@
"pills.banned": "被封禁",
"pills.search": "搜寻用户",
"search.uid": "By User ID",
"search.uid-placeholder": "Enter a user ID to search",
"search.uid": "通过用户名",
"search.uid-placeholder": "搜索用户ID",
"search.username": "通过用户名",
"search.username-placeholder": "输入你想找的用户名",
"search.email": "通过邮箱",
@ -71,9 +71,9 @@
"alerts.lockout-reset-success": "闭锁已重置!",
"alerts.flag-reset-success": "举报已重置!",
"alerts.no-remove-yourself-admin": "你无法撤销自己的管理员身份!",
"alerts.make-admin-success": "User is now administrator.",
"alerts.confirm-remove-admin": "Do you really want to remove this administrator?",
"alerts.remove-admin-success": "User is no longer administrator.",
"alerts.make-admin-success": "用户已成为管理员",
"alerts.confirm-remove-admin": "真的想要删除这个管理员?",
"alerts.remove-admin-success": "此用户不再是管理员",
"alerts.make-global-mod-success": "User is now global moderator.",
"alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?",
"alerts.remove-global-mod-success": "User is no longer global noderator",

@ -14,7 +14,7 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se
var customHTML = ace.edit('customHTML');
customCSS.setTheme('ace/theme/twilight');
customCSS.getSession().setMode('ace/mode/css');
customCSS.getSession().setMode('ace/mode/less');
customCSS.on('change', function () {
app.flags = app.flags || {};

@ -15,6 +15,7 @@ define('admin/manage/tags', [
handleCreate();
handleSearch();
handleModify();
handleRename();
handleDeleteSelected();
};
@ -103,15 +104,25 @@ define('admin/manage/tags', [
var modal = $('.bootbox');
var bgColor = modal.find('[data-name="bgColor"]').val();
var color = modal.find('[data-name="color"]').val();
var data = [];
tagsToModify.each(function (idx, tag) {
tag = $(tag);
data.push({
value: tag.attr('data-tag'),
color: modal.find('[data-name="color"]').val(),
bgColor: modal.find('[data-name="bgColor"]').val(),
});
tag.find('[data-name="bgColor"]').val(bgColor);
tag.find('[data-name="color"]').val(color);
tag.find('.tag-item').css('background-color', bgColor).css('color', color);
});
save(tag);
socket.emit('admin.tags.update', data, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/tags:alerts.update-success]]');
});
},
},
@ -122,6 +133,46 @@ define('admin/manage/tags', [
});
}
function handleRename() {
$('#rename').on('click', function () {
var tagsToModify = $('.tag-row.ui-selected');
if (!tagsToModify.length) {
return;
}
var firstTag = $(tagsToModify[0]);
var title = tagsToModify.length > 1 ? '[[admin/manage/tags:alerts.editing-multiple]]' : '[[admin/manage/tags:alerts.editing-x, ' + firstTag.find('.tag-item').attr('data-tag') + ']]';
var modal = bootbox.dialog({
title: title,
message: $('.rename-modal').html(),
buttons: {
success: {
label: 'Save',
className: 'btn-primary save',
callback: function () {
var data = [];
tagsToModify.each(function (idx, tag) {
tag = $(tag);
data.push({
value: tag.attr('data-tag'),
newName: modal.find('[data-name="value"]').val(),
});
});
socket.emit('admin.tags.rename', data, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/tags:alerts.update-success]]');
});
},
},
},
});
});
}
function handleDeleteSelected() {
$('#deleteSelected').on('click', function () {
var tagsToDelete = $('.tag-row.ui-selected');
@ -158,21 +209,5 @@ define('admin/manage/tags', [
modal.find('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker);
}
function save(tag) {
var data = {
tag: tag.attr('data-tag'),
bgColor: tag.find('[data-name="bgColor"]').val(),
color: tag.find('[data-name="color"]').val(),
};
socket.emit('admin.tags.update', data, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/tags:alerts.update-success]]');
});
}
return Tags;
});

@ -31,7 +31,7 @@ define('forum/account/edit/email', ['forum/account/header'], function (header) {
return app.alertError(err.message);
}
ajaxify.go('user/' + ajaxify.data.userslug);
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
});
return false;

@ -82,8 +82,11 @@ define('forum/account/edit/password', ['forum/account/header', 'translator', 'zx
onPasswordConfirmChanged();
return app.alertError(err.message);
}
window.location.href = config.relative_path + '/login';
if (parseInt(app.user.uid, 10) === parseInt(ajaxify.data.uid, 10)) {
window.location.href = config.relative_path + '/login';
} else {
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
}
});
} else {
if (!passwordsmatch) {

@ -39,7 +39,7 @@ define('forum/account/edit/username', ['forum/account/header'], function (header
$('[component="header/usericon"]').css('background-color', data['icon:bgColor']).text(data['icon:text']);
}
ajaxify.go('user/' + userslug);
ajaxify.go('user/' + userslug + '/edit');
});
return false;

@ -545,6 +545,20 @@
return str.toString().replace(escapeChars, replaceChar);
},
addNoReferrer: function (containerEl) {
containerEl.find('a').attr('rel', function (idx, value) {
value = value ? value.split(' ') : [];
['noopener', 'noreferrer'].forEach(function (property) {
if (!value.includes(property)) {
value.push(property);
}
});
return value.join(' ');
});
},
isAndroidBrowser: function () {
// http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
var nua = navigator.userAgent;
@ -732,6 +746,27 @@
rtrim: function (str) {
return str.replace(/\s+$/g, '');
},
debounce: function (func, wait, immediate) {
// modified from https://davidwalsh.name/javascript-debounce-function
var timeout;
return function () {
var context = this;
var args = arguments;
var later = function () {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
},
};
/* eslint "no-extend-native": "off" */

@ -58,6 +58,7 @@
title: $(this).attr('title'),
});
});
utils.addNoReferrer(widgetAreas);
$(window).trigger('action:widgets.loaded', {});
callback();
};

@ -339,10 +339,6 @@ Categories.buildForSelect = function (uid, privilege, callback) {
Categories.buildForSelectCategories = function (categories, callback) {
function recursive(category, categoriesData, level, depth) {
if (category.link) {
return;
}
var bullet = level ? '&bull; ' : '';
category.value = category.cid;
category.level = level;
@ -358,7 +354,7 @@ Categories.buildForSelectCategories = function (categories, callback) {
var categoriesData = [];
categories = categories.filter(function (category) {
return category && !category.link && !parseInt(category.parentCid, 10);
return category && !parseInt(category.parentCid, 10);
});
categories.forEach(function (category) {

@ -8,9 +8,9 @@ var dirname = require('./paths').baseDir;
// check to make sure dependencies are installed
try {
require('../../package.json');
fs.accessSync(path.join(dirname, 'package.json'), fs.constants.R_OK);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
if (e.code === 'ENOENT') {
console.warn('package.json not found.');
console.log('Populating package.json...');
@ -18,6 +18,8 @@ try {
packageInstall.preserveExtraneousPlugins();
try {
fs.accessSync(path.join(dirname, 'node_modules/colors/package.json'), fs.constants.R_OK);
require('colors');
console.log('OK'.green);
} catch (e) {
@ -29,6 +31,8 @@ try {
}
try {
fs.accessSync(path.join(dirname, 'node_modules/semver/package.json'), fs.constants.R_OK);
var semver = require('semver');
var defaultPackage = require('../../install/package.json');
@ -50,6 +54,7 @@ try {
console.warn('Dependencies outdated or not yet installed.');
console.log('Installing them now...\n');
packageInstall.updatePackageFile();
packageInstall.installAll();
require('colors');
@ -292,16 +297,12 @@ program
}
});
program
.command('*', {}, {
noHelp: true,
})
.action(function () {
program.help();
});
require('./colors');
if (process.argv.length === 2) {
program.help();
}
program.executables = false;
program.parse(process.argv);

@ -33,6 +33,7 @@ function installAll() {
var prod = global.env !== 'development';
var command = 'npm install';
try {
fs.accessSync(path.join(modulesPath, 'nconf/package.json'), fs.constants.R_OK);
var packageManager = require('nconf').get('package_manager');
if (packageManager === 'yarn') {
command = 'yarn';

@ -28,6 +28,9 @@ editController.get = function (req, res, callback) {
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
userData.allowWebsite = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:website'], 10) || 0);
userData.allowAboutMe = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:aboutme'], 10) || 0);
userData.allowSignature = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:signature'], 10) || 0);
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 200;
userData.defaultAvatar = user.getDefaultAvatar();

@ -126,7 +126,7 @@ modsController.flags.detail = function (req, res, next) {
assignees: results.assignees,
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
if (cur !== 'empty') {
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
memo[cur] = results.flagData.type === cur && (!results.flagData.target || !!Object.keys(results.flagData.target).length);
} else {
memo[cur] = !Object.keys(results.flagData.target).length;
}

@ -54,6 +54,10 @@ searchController.search = function (req, res, next) {
return next(err);
}
results.categories = results.categories.filter(function (category) {
return category && !category.link;
});
var categoriesData = [
{ value: 'all', text: '[[unread:all_categories]]' },
{ value: 'watched', text: '[[category:watched-categories]]' },

@ -37,19 +37,22 @@ redisModule.questions = [
];
redisModule.init = function (callback) {
redisClient = redisModule.connect();
callback = callback || function () { };
redisClient = redisModule.connect({}, function (err) {
if (err) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err);
return callback(err);
}
redisModule.client = redisClient;
require('./redis/main')(redisClient, redisModule);
require('./redis/hash')(redisClient, redisModule);
require('./redis/sets')(redisClient, redisModule);
require('./redis/sorted')(redisClient, redisModule);
require('./redis/list')(redisClient, redisModule);
redisModule.client = redisClient;
require('./redis/main')(redisClient, redisModule);
require('./redis/hash')(redisClient, redisModule);
require('./redis/sets')(redisClient, redisModule);
require('./redis/sorted')(redisClient, redisModule);
require('./redis/list')(redisClient, redisModule);
if (typeof callback === 'function') {
callback();
}
});
};
redisModule.initSessionStore = function (callback) {
@ -66,7 +69,8 @@ redisModule.initSessionStore = function (callback) {
}
};
redisModule.connect = function (options) {
redisModule.connect = function (options, callback) {
callback = callback || function () {};
var redis_socket_or_host = nconf.get('redis:host');
var cxn;
@ -88,7 +92,11 @@ redisModule.connect = function (options) {
cxn.on('error', function (err) {
winston.error(err.stack);
process.exit(1);
callback(err);
});
cxn.on('ready', function () {
callback();
});
if (nconf.get('redis:password')) {
@ -99,7 +107,7 @@ redisModule.connect = function (options) {
if (dbIdx >= 0) {
cxn.select(dbIdx, function (err) {
if (err) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err);
winston.error('NodeBB could not select Redis database. Redis returned the following error', err);
throw err;
}
});

@ -10,6 +10,7 @@ var htmlToText = require('html-to-text');
var url = require('url');
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var User = require('./user');
var Plugins = require('./plugins');
@ -289,11 +290,10 @@ function buildCustomTemplates(config) {
file.walk(viewsDir, next);
},
function (paths, next) {
paths = paths.reduce(function (obj, p) {
var relative = path.relative(viewsDir, p);
obj['/' + relative] = p;
return obj;
}, {});
paths = _.fromPairs(paths.map(function (p) {
var relative = path.relative(viewsDir, p).replace(/\\/g, '/');
return [relative, p];
}));
meta.templates.processImports(paths, template.path, template.text, next);
},
function (source, next) {

@ -241,7 +241,7 @@ Flags.validate = function (payload, callback) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
@ -257,7 +257,7 @@ Flags.validate = function (payload, callback) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));

@ -157,16 +157,17 @@ function completeConfigSetup(config, next) {
}
}
nconf.overrides(config);
async.waterfall([
function (next) {
install.save(config, next);
},
function (next) {
require('./database').init(next);
},
function (next) {
require('./database').createIndices(next);
},
function (next) {
install.save(config, next);
},
], next);
}
@ -523,7 +524,7 @@ install.setup = function (callback) {
], function (err, results) {
if (err) {
winston.warn('NodeBB Setup Aborted.\n ' + err.stack);
process.exit();
process.exit(1);
} else {
var data = {};
if (results[6]) {

@ -343,6 +343,11 @@ JS.buildBundle = function (target, fork, callback) {
function (next) {
getBundleScriptList(target, next);
},
function (files, next) {
mkdirp(path.join(__dirname, '../../build/public'), function (err) {
next(err, files);
});
},
function (files, next) {
var minify = global.env !== 'development';
var filePath = path.join(__dirname, '../../build/public', fileNames[target]);

@ -75,7 +75,7 @@ function forkAction(action, callback) {
freeChild(proc);
if (message.type === 'error') {
return callback(message.err);
return callback(message.message);
}
if (message.type === 'end') {
@ -103,7 +103,7 @@ if (process.env.minifier_child) {
if (typeof actions[action.act] !== 'function') {
process.send({
type: 'error',
err: Error('Unknown action'),
message: 'Unknown action',
});
return;
}
@ -112,7 +112,7 @@ if (process.env.minifier_child) {
if (err) {
process.send({
type: 'error',
err: err,
message: err.stack,
});
return;
}

@ -7,6 +7,7 @@ var async = require('async');
var path = require('path');
var fs = require('fs');
var nconf = require('nconf');
var _ = require('lodash');
var plugins = require('../plugins');
var file = require('../file');
@ -24,7 +25,7 @@ function processImports(paths, templatePath, source, callback) {
return callback(null, source);
}
var partial = '/' + matches[1];
var partial = matches[1];
if (paths[partial] && templatePath !== partial) {
fs.readFile(paths[partial], 'utf8', function (err, partialSource) {
if (err) {
@ -43,124 +44,108 @@ function processImports(paths, templatePath, source, callback) {
}
Templates.processImports = processImports;
Templates.compile = function (callback) {
callback = callback || function () {};
function getTemplateDirs(callback) {
var pluginTemplates = _.values(plugins.pluginsData)
.filter(function (pluginData) {
return !pluginData.id.startsWith('nodebb-theme-');
})
.map(function (pluginData) {
return path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.templates || 'templates');
});
var themeConfig = require(nconf.get('theme_config'));
var baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')];
var theme = themeConfig.baseTheme;
var themePath;
var themeTemplates = [nconf.get('theme_templates_path')];
while (theme) {
themePath = path.join(nconf.get('themes_path'), theme);
themeConfig = require(path.join(themePath, 'theme.json'));
themeTemplates.push(path.join(themePath, themeConfig.templates || 'templates'));
theme = themeConfig.baseTheme;
}
themeTemplates.push(nconf.get('base_templates_path'));
themeTemplates = _.uniq(themeTemplates.reverse());
var coreTemplatesPath = nconf.get('core_templates_path');
var templateDirs = _.uniq([coreTemplatesPath].concat(themeTemplates, pluginTemplates));
async.filter(templateDirs, file.exists, callback);
}
function getTemplateFiles(dirs, callback) {
async.waterfall([
function (cb) {
async.map(dirs, function (dir, next) {
file.walk(dir, function (err, files) {
if (err) { return next(err); }
files = files.filter(function (path) {
return path.endsWith('.tpl');
}).map(function (file) {
return {
name: path.relative(dir, file).replace(/\\/g, '/'),
path: file,
};
});
next(null, files);
});
}, cb);
},
function (buckets, cb) {
var dict = {};
buckets.forEach(function (files) {
files.forEach(function (file) {
dict[file.name] = file.path;
});
});
cb(null, dict);
},
], callback);
}
function compile(callback) {
callback = callback || function () {};
async.waterfall([
function (next) {
preparePaths(baseTemplatesPaths, next);
rimraf(viewsPath, function (err) { next(err); });
},
function (next) {
mkdirp(viewsPath, function (err) { next(err); });
},
function (paths, next) {
async.each(Object.keys(paths), function (relativePath, next) {
getTemplateDirs,
getTemplateFiles,
function (files, next) {
async.each(Object.keys(files), function (name, next) {
var filePath = files[name];
async.waterfall([
function (next) {
fs.readFile(paths[relativePath], 'utf8', next);
fs.readFile(filePath, 'utf8', next);
},
function (source, next) {
processImports(paths, relativePath, source, next);
processImports(files, name, source, next);
},
function (source, next) {
mkdirp(path.join(viewsPath, path.dirname(relativePath)), function (err) {
mkdirp(path.join(viewsPath, path.dirname(name)), function (err) {
next(err, source);
});
},
function (compiled, next) {
fs.writeFile(path.join(viewsPath, relativePath), compiled, next);
fs.writeFile(path.join(viewsPath, name), compiled, next);
},
], next);
}, next);
},
function (next) {
rimraf(path.join(viewsPath, '*.js'), next);
},
function (next) {
winston.verbose('[meta/templates] Successfully compiled templates.');
next();
},
], callback);
};
function getBaseTemplates(theme) {
var baseTemplatesPaths = [];
var baseThemePath;
var baseThemeConfig;
while (theme) {
baseThemePath = path.join(nconf.get('themes_path'), theme);
baseThemeConfig = require(path.join(baseThemePath, 'theme.json'));
baseTemplatesPaths.push(path.join(baseThemePath, baseThemeConfig.templates || 'templates'));
theme = baseThemeConfig.baseTheme;
}
return baseTemplatesPaths.reverse();
}
function preparePaths(baseTemplatesPaths, callback) {
var coreTemplatesPath = nconf.get('core_templates_path');
var pluginTemplates;
async.waterfall([
function (next) {
rimraf(viewsPath, next);
},
function (next) {
mkdirp(viewsPath, next);
},
function (viewsPath, next) {
plugins.fireHook('static:templates.precompile', {}, next);
},
function (next) {
plugins.getTemplates(next);
},
function (_pluginTemplates, next) {
pluginTemplates = _pluginTemplates;
winston.verbose('[meta/templates] Compiling templates');
async.parallel({
coreTpls: function (next) {
file.walk(coreTemplatesPath, next);
},
baseThemes: function (next) {
async.map(baseTemplatesPaths, function (baseTemplatePath, next) {
file.walk(baseTemplatePath, function (err, paths) {
paths = paths.map(function (tpl) {
return {
base: baseTemplatePath,
path: tpl.replace(baseTemplatePath, ''),
};
});
next(err, paths);
});
}, next);
},
}, next);
},
function (data, next) {
var baseThemes = data.baseThemes;
var coreTpls = data.coreTpls;
var paths = {};
coreTpls.forEach(function (el, i) {
paths[coreTpls[i].replace(coreTemplatesPath, '')] = coreTpls[i];
});
baseThemes.forEach(function (baseTpls) {
baseTpls.forEach(function (el, i) {
paths[baseTpls[i].path] = path.join(baseTpls[i].base, baseTpls[i].path);
});
});
for (var tpl in pluginTemplates) {
if (pluginTemplates.hasOwnProperty(tpl)) {
paths[tpl] = pluginTemplates[tpl];
}
}
next(null, paths);
},
], callback);
}
Templates.compile = compile;

@ -138,10 +138,13 @@ Plugins.reloadRoutes = function (callback) {
});
};
// DEPRECATED: remove in v1.8.0
Plugins.getTemplates = function (callback) {
var templates = {};
var tplName;
winston.warn('[deprecated] Plugins.getTemplates is DEPRECATED to be removed in v1.8.0');
Plugins.data.getActive(function (err, plugins) {
if (err) {
return callback(err);

@ -140,7 +140,7 @@ module.exports = function (Posts) {
db.setObject('topic:' + tid, results.topic, next);
},
function (next) {
topics.updateTags(tid, data.tags, next);
topics.updateTopicTags(tid, data.tags, next);
},
function (next) {
topics.getTopicTagsObjects(tid, next);

@ -179,7 +179,7 @@ module.exports = function (Posts) {
return callback(new Error('[[error:self-vote]]'));
}
if (command === 'downvote' && parseInt(results.reputation, 10) < parseInt(meta.config['privileges:downvote'], 10)) {
if (command === 'downvote' && parseInt(results.reputation, 10) < parseInt(meta.config['min:rep:downvote'], 10)) {
return callback(new Error('[[error:not-enough-reputation-to-downvote]]'));
}

@ -200,7 +200,7 @@ module.exports = function (privileges) {
}, next);
},
function (results, next) {
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
var canFlag = results.isAdminOrMod || parseInt(results.userReputation, 10) >= minimumReputation;
next(null, { flag: canFlag });
},

@ -13,11 +13,19 @@ Tags.create = function (socket, data, callback) {
};
Tags.update = function (socket, data, callback) {
if (!data) {
if (!Array.isArray(data)) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.updateTags(data, callback);
};
Tags.rename = function (socket, data, callback) {
if (!Array.isArray(data)) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.updateTag(data.tag, data, callback);
topics.renameTags(data, callback);
};
Tags.deleteTags = function (socket, data, callback) {

@ -9,7 +9,7 @@ var meta = require('../meta');
var _ = require('lodash');
var plugins = require('../plugins');
var utils = require('../utils');
var batch = require('../batch');
module.exports = function (Topics) {
Topics.createTags = function (tags, tid, timestamp, callback) {
@ -96,13 +96,61 @@ module.exports = function (Topics) {
], callback);
};
Topics.updateTag = function (tag, data, callback) {
if (!tag) {
return setImmediate(callback, new Error('[[error:invalid-tag]]'));
}
db.setObject('tag:' + tag, data, callback);
Topics.updateTags = function (data, callback) {
async.eachSeries(data, function (tagData, next) {
db.setObject('tag:' + tagData.value, {
color: tagData.color,
bgColor: tagData.bgColor,
}, next);
}, callback);
};
Topics.renameTags = function (data, callback) {
async.eachSeries(data, function (tagData, next) {
renameTag(tagData.value, tagData.newName, next);
}, callback);
};
function renameTag(tag, newTagName, callback) {
if (!newTagName || tag === newTagName) {
return setImmediate(callback);
}
async.waterfall([
function (next) {
Topics.createEmptyTag(newTagName, next);
},
function (next) {
batch.processSortedSet('tag:' + tag + ':topics', function (tids, next) {
async.waterfall([
function (next) {
db.sortedSetScores('tag:' + tag + ':topics', tids, next);
},
function (scores, next) {
db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids, next);
},
function (next) {
var keys = tids.map(function (tid) {
return 'topic:' + tid + ':tags';
});
async.series([
async.apply(db.sortedSetRemove, 'tag:' + tag + ':topics', tids),
async.apply(db.setsRemove, keys, tag),
async.apply(db.setsAdd, keys, newTagName),
], next);
},
], next);
}, next);
},
function (next) {
Topics.deleteTag(tag, next);
},
function (next) {
updateTagCount(newTagName, next);
},
], callback);
}
function updateTagCount(tag, callback) {
callback = callback || function () {};
async.waterfall([
@ -148,7 +196,9 @@ module.exports = function (Topics) {
return 'tag:' + tag;
}), next);
},
], callback);
], function (err) {
callback(err);
});
};
function removeTagsFromTopics(tags, callback) {
@ -266,7 +316,7 @@ module.exports = function (Topics) {
], callback);
};
Topics.updateTags = function (tid, tags, callback) {
Topics.updateTopicTags = function (tid, tags, callback) {
callback = callback || function () {};
async.waterfall([
function (next) {

@ -48,7 +48,7 @@ module.exports = {
done = true;
return next();
}
delete item.expireAt;
if (Object.keys(item).length === 3 && item.hasOwnProperty('_key') && item.hasOwnProperty('value')) {
client.collection('objects').update({ _key: item._key }, { $rename: { value: 'data' } }, next);
} else {

@ -0,0 +1,25 @@
'use strict';
var db = require('../../database');
module.exports = {
name: 'Rename privileges:downvote and privileges:flag to min:rep:downvote, min:rep:flag respectively',
timestamp: Date.UTC(2018, 0, 12),
method: function (callback) {
db.getObjectFields('config', ['privileges:downvote', 'privileges:flag'], function (err, config) {
if (err) {
return callback(err);
}
db.setObject('config', {
'min:rep:downvote': parseInt(config['privileges:downvote'], 10) || 0,
'min:rep:flag': parseInt(config['privileges:downvote'], 10) || 0,
}, function (err) {
if (err) {
return callback(err);
}
db.deleteObjectFields('config', ['privileges:downvote', 'privileges:flag'], callback);
});
});
},
};

@ -17,14 +17,6 @@ module.exports = function (User) {
var updateUid = data.uid;
var oldData;
if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
}
if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
}
async.waterfall([
function (next) {
plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }, next);
@ -33,13 +25,7 @@ module.exports = function (User) {
fields = data.fields;
data = data.data;
async.series([
async.apply(isEmailAvailable, data, updateUid),
async.apply(isUsernameAvailable, data, updateUid),
async.apply(isGroupTitleValid, data),
], function (err) {
next(err);
});
validateData(uid, data, next);
},
function (next) {
User.getUserFields(updateUid, fields, next);
@ -73,6 +59,19 @@ module.exports = function (User) {
], callback);
};
function validateData(callerUid, data, callback) {
async.series([
async.apply(isEmailAvailable, data, data.uid),
async.apply(isUsernameAvailable, data, data.uid),
async.apply(isGroupTitleValid, data),
async.apply(isWebsiteValid, callerUid, data),
async.apply(isAboutMeValid, callerUid, data),
async.apply(isSignatureValid, callerUid, data),
], function (err) {
callback(err);
});
}
function isEmailAvailable(data, uid, callback) {
if (!data.email) {
return callback();
@ -141,6 +140,52 @@ module.exports = function (User) {
}
}
function isWebsiteValid(callerUid, data, callback) {
if (!data.website) {
return setImmediate(callback);
}
checkMinReputation(callerUid, data.uid, 'min:rep:website', callback);
}
function isAboutMeValid(callerUid, data, callback) {
if (!data.aboutme) {
return setImmediate(callback);
}
if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
}
checkMinReputation(callerUid, data.uid, 'min:rep:aboutme', callback);
}
function isSignatureValid(callerUid, data, callback) {
if (!data.signature) {
return setImmediate(callback);
}
if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
}
checkMinReputation(callerUid, data.uid, 'min:rep:signature', callback);
}
function checkMinReputation(callerUid, uid, setting, callback) {
var isSelf = parseInt(callerUid, 10) === parseInt(uid, 10);
if (!isSelf) {
return setImmediate(callback);
}
async.waterfall([
function (next) {
User.getUserField(uid, 'reputation', next);
},
function (reputation, next) {
if (parseInt(reputation, 10) < (parseInt(meta.config[setting], 10) || 0)) {
return next(new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]'));
}
next();
},
], callback);
}
function updateEmail(uid, newEmail, callback) {
async.waterfall([
function (next) {

@ -41,6 +41,7 @@
<p>[[admin/manage/tags:description]]</p>
<button class="btn btn-primary btn-block" id="create">[[admin/manage/tags:create]]</button>
<button class="btn btn-primary btn-block" id="modify">[[admin/manage/tags:modify]]</button>
<button class="btn btn-primary btn-block" id="rename">[[admin/manage/tags:rename]]</button>
<button class="btn btn-warning btn-block" id="deleteSelected">[[admin/manage/tags:delete]]</button>
</div>
</div>
@ -74,4 +75,11 @@
</div>
</div>
</div>
<div class="rename-modal hidden">
<div class="form-group">
<label for="value">[[admin/manage/tags:name]]</label>
<input id="value" data-name="value" value="{tags.value}" class="form-control" />
</div>
</div>
</div>

@ -32,8 +32,11 @@
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:thresholds]]</div>
<div class="col-sm-10 col-xs-12">
<form>
<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="privileges:downvote"><br />
<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="privileges:flag"><br />
<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:downvote"><br />
<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:flag"><br />
<strong>[[admin/settings/reputation:min-rep-website]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:website"><br />
<strong>[[admin/settings/reputation:min-rep-aboutme]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme"><br />
<strong>[[admin/settings/reputation:min-rep-signature]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:signature"><br />
</form>
</div>
</div>

@ -591,21 +591,21 @@ describe('Admin Controllers', function () {
it('should error with not enough reputation to flag', function (done) {
var socketFlags = require('../src/socket.io/flags');
var oldValue = meta.config['privileges:flag'];
meta.config['privileges:flag'] = 1000;
var oldValue = meta.config['min:rep:flag'];
meta.config['min:rep:flag'] = 1000;
socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err) {
assert.equal(err.message, '[[error:not-enough-reputation-to-flag]]');
meta.config['privileges:flag'] = oldValue;
meta.config['min:rep:flag'] = oldValue;
done();
});
});
it('should return flag details', function (done) {
var socketFlags = require('../src/socket.io/flags');
var oldValue = meta.config['privileges:flag'];
meta.config['privileges:flag'] = 0;
var oldValue = meta.config['min:rep:flag'];
meta.config['min:rep:flag'] = 0;
socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err, data) {
meta.config['privileges:flag'] = oldValue;
meta.config['min:rep:flag'] = oldValue;
assert.ifError(err);
request(nconf.get('url') + '/api/flags/' + data.flagId, { jar: moderatorJar, json: true }, function (err, res, body) {
assert.ifError(err);

@ -364,7 +364,7 @@ describe('Flags', function () {
});
it('should not pass validation if flag threshold is set and user rep does not meet it', function (done) {
Meta.configs.set('privileges:flag', '50', function (err) {
Meta.configs.set('min:rep:flag', '50', function (err) {
assert.ifError(err);
Flags.validate({
@ -374,7 +374,7 @@ describe('Flags', function () {
}, function (err) {
assert.ok(err);
assert.strictEqual('[[error:not-enough-reputation-to-flag]]', err.message);
Meta.configs.set('privileges:flag', 0, done);
Meta.configs.set('min:rep:flag', 0, done);
});
});
});

@ -1350,22 +1350,22 @@ describe('Topic\'s', function () {
});
});
it('should error if data.tag is invalid', function (done) {
it('should error if data is not an array', function (done) {
socketAdmin.tags.update({ uid: adminUid }, {
bgColor: '#ff0000',
color: '#00ff00',
}, function (err) {
assert.equal(err.message, '[[error:invalid-tag]]');
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should update tag', function (done) {
socketAdmin.tags.update({ uid: adminUid }, {
tag: 'emptytag',
socketAdmin.tags.update({ uid: adminUid }, [{
value: 'emptytag',
bgColor: '#ff0000',
color: '#00ff00',
}, function (err) {
}], function (err) {
assert.ifError(err);
db.getObject('tag:emptytag', function (err, data) {
assert.ifError(err);
@ -1376,6 +1376,35 @@ describe('Topic\'s', function () {
});
});
it('should rename tags', function (done) {
async.parallel({
topic1: function (next) {
topics.post({ uid: adminUid, tags: ['plugins'], title: 'topic tagged with plugins', content: 'topic 1 content', cid: topic.categoryId }, next);
},
topic2: function (next) {
topics.post({ uid: adminUid, tags: ['plugin'], title: 'topic tagged with plugin', content: 'topic 2 content', cid: topic.categoryId }, next);
},
}, function (err, result) {
assert.ifError(err);
socketAdmin.tags.rename({ uid: adminUid }, [{
value: 'plugin',
newName: 'plugins',
}], function (err) {
assert.ifError(err);
topics.getTagTids('plugins', 0, -1, function (err, tids) {
assert.ifError(err);
assert.equal(tids.length, 2);
topics.getTopicTags(result.topic2.topicData.tid, function (err, tags) {
assert.ifError(err);
assert.equal(tags.length, 1);
assert.equal(tags[0], 'plugins');
done();
});
});
});
});
});
it('should return related topics', function (done) {
var meta = require('../src/meta');
meta.config.maximumRelatedTopics = 2;

Loading…
Cancel
Save