v1.18.x
psychobunny 8 years ago
commit a2847dc851

@ -58,7 +58,7 @@
"nodebb-plugin-emoji-one": "1.1.5",
"nodebb-plugin-markdown": "7.1.0",
"nodebb-plugin-mentions": "1.1.3",
"nodebb-plugin-soundpack-default": "0.1.6",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.4.10",
"nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "3.0.15",

@ -1,9 +1,9 @@
{
"loading": "Načítání motivů…",
"homepage": "Homepage",
"select-skin": "Select Skin",
"current-skin": "Current Skin",
"skin-updated": "Skin Updated",
"homepage": "Domovská stránka",
"select-skin": "Vyber motiv",
"current-skin": "Současný motiv",
"skin-updated": "Motiv aktualizován",
"applied-success": "%1 skin was succesfully applied",
"revert-success": "Skin reverted to base colours"
}

@ -1,21 +1,21 @@
{
"authentication": "Authentication",
"allow-local-login": "Allow local login",
"require-email-confirmation": "Require Email Confirmation",
"authentication": "Ověření",
"allow-local-login": "Povolit místní přihlášení",
"require-email-confirmation": "Vyžadovat potvrzení e-mailem",
"email-confirm-interval": "User may not resend a confirmation email until",
"email-confirm-email2": "minutes have elapsed",
"email-confirm-email2": "minut uplynulo",
"allow-login-with": "Allow login with",
"allow-login-with.username-email": "Username or Email",
"allow-login-with.username": "Username Only",
"allow-login-with.email": "Email Only",
"account-settings": "Account Settings",
"disable-username-changes": "Disable username changes",
"disable-email-changes": "Disable email changes",
"disable-password-changes": "Disable password changes",
"allow-account-deletion": "Allow account deletion",
"allow-login-with.username-email": "Uživatelské jméno nebo e-mail",
"allow-login-with.username": "Pouze uživatelské jméno",
"allow-login-with.email": "Pouze e-mail",
"account-settings": "Nastavení účtu",
"disable-username-changes": "Zakázat změnu uživatelského jména",
"disable-email-changes": "Zakázat změnu e-mailu",
"disable-password-changes": "Zakázat změnu hesla",
"allow-account-deletion": "Povolit smazání účtu",
"user-info-private": "Make user info private",
"themes": "Themes",
"disable-user-skins": "Prevent users from choosing a custom skin",
"themes": "Témata",
"disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu",
"account-protection": "Account Protection",
"login-attempts": "Login attempts per hour",
"login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time",
@ -34,10 +34,10 @@
"registration.max-invites": "Maximum Invitations per User",
"max-invites": "Maximum Invitations per User",
"max-invites-help": "0 for no restriction. Admins get infinite invitations<br>Only applicable for \"Invite Only\"",
"min-username-length": "Minimum Username Length",
"max-username-length": "Maximum Username Length",
"min-password-length": "Minimum Password Length",
"max-about-me-length": "Maximum About Me Length",
"min-username-length": "Minimální délka uživatelského jména",
"max-username-length": "Maximální délka uživatelského jména",
"min-password-length": "Minimální délka hesla",
"max-about-me-length": "Maximální délka hesla",
"terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>",
"user-search": "User Search",
"user-search-results-per-page": "Number of results to display",
@ -48,10 +48,10 @@
"outgoing-new-tab": "Open outgoing links in new tab",
"topic-search": "Enable In-Topic Searching",
"digest-freq": "Subscribe to Digest",
"digest-freq.off": "Off",
"digest-freq.daily": "Daily",
"digest-freq.weekly": "Weekly",
"digest-freq.monthly": "Monthly",
"digest-freq.off": "Vypnuto",
"digest-freq.daily": "Denně",
"digest-freq.weekly": "Týdně",
"digest-freq.monthly": "Měsíčně",
"email-chat-notifs": "Send an email if a new chat message arrives and I am not online",
"email-post-notif": "Send an email when replies are made to topics I am subscribed to",
"follow-created-topics": "Follow topics you create",

@ -8,5 +8,5 @@
"failed_login_attempt": "Přihlášení neúspěšné",
"login_successful": "Přihlášení proběhlo úspěšně!",
"dont_have_account": "Nemáte účet?",
"logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity"
"logged-out-due-to-inactivity": "Z důvodu nečinnosti jste byl odhlášen z ovládacího panelu administrátora"
}

@ -13,7 +13,7 @@
"chat.contacts": "Kontakty",
"chat.message-history": "Historie zpráv",
"chat.pop-out": "Skrýt chat",
"chat.minimize": "Minimize",
"chat.minimize": "Minimalizovat",
"chat.maximize": "Maximalizovat",
"chat.seven_days": "7 dní",
"chat.thirty_days": "30 dní",

@ -1,6 +1,6 @@
{
"register": "Registrace",
"cancel_registration": "Cancel Registration",
"cancel_registration": "Zrušit registraci",
"help.email": "Ve výchozím nastavení bude váš e-mail skrytý.",
"help.username_restrictions": "Jedinečné uživatelské jméno dlouhé %1 až %2 znaků. Ostatní uživatelé Vás mohou zmínit jako @<span id='yourUsername'>uživatelské-jméno</span>.",
"help.minimum_password_length": "Délka vašeho hesla musí být alespoň %1 znaků.",

@ -94,7 +94,7 @@
"topics_per_page": "Témat na stránce",
"posts_per_page": "Příspěvků na stránce",
"notification_sounds": "Přehrát zvuk když dostanete notifikaci",
"notifications_and_sounds": "Notifications & Sounds",
"notifications_and_sounds": "Upozornění a zvuky",
"incoming-message-sound": "Incoming message sound",
"outgoing-message-sound": "Outgoing message sound",
"notification-sound": "Notification sound",

@ -1,6 +1,6 @@
{
"title": "未読",
"no_unread_topics": "未読のスレッドがあります。",
"no_unread_topics": "未読のスレッドはありません。",
"load_more": "もっと見る",
"mark_as_read": "既読にする",
"selected": "選択済み",

@ -1,11 +1,11 @@
{
"post-cache": "Кэш записи",
"posts-in-cache": "Записей в кэше",
"average-post-size": "Average Post Size",
"length-to-max": "Length / Max",
"average-post-size": "Средний размер записи",
"length-to-max": "Длина / Максимальная",
"percent-full": "%1% Full",
"post-cache-size": "Post Cache Size",
"post-cache-size": "Размер записи в кэше",
"items-in-cache": "Items in Cache",
"control-panel": "Control Panel",
"update-settings": "Update Cache Settings"
"control-panel": "Панель управления",
"update-settings": "Обновить настройки кэша"
}

@ -1,14 +1,14 @@
{
"figure-x": "Znázorniť %1",
"error-events-per-day": "<code>%1</code> events per day",
"error.404": "404 Not Found",
"error.503": "503 Service Unavailable",
"error.404": "404 Nenájdené",
"error.503": "503 Služba nie je k dispozícií",
"manage-error-log": "Manage Error Log",
"export-error-log": "Export Error Log (CSV)",
"clear-error-log": "Clear Error Log",
"route": "Route",
"count": "Count",
"no-routes-not-found": "Hooray! No 404 errors!",
"no-routes-not-found": "Hurá! Žiadne chyby 404!",
"clear404-confirm": "Are you sure you wish to clear the 404 error logs?",
"clear404-success": "\"404 Not Found\" errors cleared"
"clear404-success": "Chybné hlásenia \"404 Nenájdené\" vyčistené"
}

@ -1,6 +1,6 @@
{
"events": "Udalosti",
"no-events": "There are no events",
"control-panel": "Events Control Panel",
"delete-events": "Delete Events"
"no-events": "Zatiaľ neexistujô žiadne udalosti",
"control-panel": "Ovládací panel udalostí",
"delete-events": "Odstrániť udalosť"
}

@ -1,7 +1,7 @@
{
"logs": "Protokoly",
"control-panel": "Logs Control Panel",
"reload": "Reload Logs",
"clear": "Clear Logs",
"clear-success": "Logs Cleared!"
"logs": "Záznamy",
"control-panel": "Ovládací panel záznamov",
"reload": "Znovu načítať záznamy",
"clear": "Vyčistiť záznamy",
"clear-success": "Záznamy vyčistené!"
}

@ -1,9 +1,9 @@
{
"loading": "Loading Skins...",
"homepage": "Homepage",
"select-skin": "Select Skin",
"current-skin": "Current Skin",
"skin-updated": "Skin Updated",
"applied-success": "%1 skin was succesfully applied",
"revert-success": "Skin reverted to base colours"
"loading": "Načítať vzhľady...",
"homepage": "Domovska stránka",
"select-skin": "Vybrať vzhľad",
"current-skin": "Aktuálny vzhľad",
"skin-updated": "Vzhľad aktualizovaný",
"applied-success": "%1 vzhľad bol úspešne aplikovaný",
"revert-success": "Vzhľad bol obnovený do základných farieb"
}

@ -1,11 +1,11 @@
{
"checking-for-installed": "Checking for installed themes...",
"homepage": "Homepage",
"select-theme": "Select Theme",
"current-theme": "Current Theme",
"no-themes": "No installed themes found",
"revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
"theme-changed": "Theme Changed",
"revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
"restart-to-activate": "Please restart your NodeBB to fully activate this theme"
"checking-for-installed": "Kontrola nainštalovaných motívov...",
"homepage": "Domovská stránka",
"select-theme": "Vybrať motív",
"current-theme": "Aktuálny motív",
"no-themes": "Žiadne nainštalované motívy neboli nájdené",
"revert-confirm": "Ste si istý, že chcete obnoviť predvolený NodeBB motív?",
"theme-changed": "Motív zmenený",
"revert-success": "Úspešne sa Vám podarilo obnoviť Váš NodeBB do predvoleného motívu.",
"restart-to-activate": "Prosím, reštartujte Váš NodeBB pre úplne aktivovanie tohto motívu."
}

@ -1,20 +1,20 @@
{
"forum-traffic": "Forum Traffic",
"page-views": "Page Views",
"unique-visitors": "Unique Visitors",
"users": "Users",
"posts": "Posts",
"topics": "Topics",
"page-views-last-month": "Page views Last Month",
"page-views-this-month": "Page views This Month",
"page-views-last-day": "Page views in last 24 hours",
"stats.day": "Day",
"stats.week": "Week",
"stats.month": "Month",
"stats.all": "All Time",
"updates": "Updates",
"forum-traffic": "Prevádzka fóra",
"page-views": "Zobrazenia stránok",
"unique-visitors": "Unikátne návštevy",
"users": "Užívatelia",
"posts": "Príspevky",
"topics": "Témy",
"page-views-last-month": "Zobrazenia stránok za posledný mesiac",
"page-views-this-month": "Zobrazenia stránok za tento mesiac",
"page-views-last-day": "Zobrazenia stránok za posledných 24 hodín",
"stats.day": "D",
"stats.week": "Týždeň",
"stats.month": "Mesiac",
"stats.all": "Celé obdobie",
"updates": "Aktualizácie",
"running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.",
"keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.",
"up-to-date": "<p>You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i></p>",
@ -22,42 +22,42 @@
"prerelease-upgrade-available": "<p>This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/en/latest/upgrading/index.html\">upgrading your NodeBB</a>.</p>",
"prerelease-warning": "<p>This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i></p>",
"notices": "Notices",
"restart-not-required": "Restart not required",
"restart-required": "Restart required",
"search-plugin-installed": "Search Plugin installed",
"search-plugin-not-installed": "Search Plugin not installed",
"notices": "Upozornenie",
"restart-not-required": "Reštart nie je potrebný",
"restart-required": "Reštart je potrebný",
"search-plugin-installed": "Vyhľadávací doplnok bol nainštalovaný",
"search-plugin-not-installed": "Vyhľadávací doplnok nebol nainštalovaný",
"search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality",
"control-panel": "System Control",
"reload": "Reload",
"restart": "Restart",
"reload": "Obnoviť",
"restart": "Reštartovať",
"restart-warning": "Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.",
"maintenance-mode": "Maintenance Mode",
"maintenance-mode-title": "Click here to set up maintenance mode for NodeBB",
"realtime-chart-updates": "Realtime Chart Updates",
"active-users": "Active Users",
"active-users.users": "Users",
"active-users.guests": "Guests",
"active-users.total": "Total",
"active-users.connections": "Connections",
"active-users": "Aktívny užívatelia",
"active-users.users": "Užívatelia",
"active-users.guests": "Hostia",
"active-users.total": "Celkovo",
"active-users.connections": "Pripojení",
"anonymous-registered-users": "Anonymous vs Registered Users",
"anonymous": "Anonymous",
"registered": "Registered",
"anonymous-registered-users": "Neznámy vs Zaregistrovaný užívatelia",
"anonymous": "Neznámy",
"registered": "Zaregistrovaný",
"user-presence": "User Presence",
"on-categories": "On categories list",
"reading-posts": "Reading posts",
"browsing-topics": "Browsing topics",
"recent": "Recent",
"unread": "Unread",
"recent": "Nedávne",
"unread": "Neprečitané",
"high-presence-topics": "High Presence Topics",
"graphs.page-views": "Page Views",
"graphs.unique-visitors": "Unique Visitors",
"graphs.registered-users": "Registered Users",
"graphs.anonymous-users": "Anonymous Users"
"graphs.page-views": "Zobrazenia stránok",
"graphs.unique-visitors": "Unikátny navštevníci",
"graphs.registered-users": "Zarestrovaný užívatelia",
"graphs.anonymous-users": "Neznámy užívatelia"
}

@ -6,7 +6,7 @@
"description": "Select tags via clicking and/or dragging, use shift to select multiple.",
"create": "Create Tag",
"modify": "Modify Tags",
"delete": "Delete Selected Tags",
"delete": "Odstrániť vybraté značky",
"search": "Search for tags...",
"settings": "Click <a href=\"%1\">here</a> to visit the tag settings page.",
"name": "Tag Name",

@ -1,6 +1,6 @@
#navigation {
#active-navigation {
width: 100%;
float: none;
min-height: 50px;
border: 1px solid #eee;
overflow: auto;

@ -1,7 +1,7 @@
"use strict";
/* global app, define, socket */
define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Settings) {
define('admin/general/sounds', ['sounds', 'settings', 'admin/settings'], function (Sounds, Settings, AdminSettings) {
var SoundsAdmin = {};
SoundsAdmin.init = function () {
@ -9,8 +9,8 @@ define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Setting
$('.sounds').find('button[data-action="play"]').on('click', function (e) {
e.preventDefault();
var fileName = $(this).parent().parent().find('select').val();
Sounds.playFile(fileName);
var soundName = $(this).parent().parent().find('select').val();
Sounds.playSound(soundName);
});
// Load Form Values
@ -26,6 +26,8 @@ define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Setting
app.alertSuccess('[[admin/general/sounds:saved]]');
});
});
AdminSettings.prepare();
};
return SoundsAdmin;

@ -73,7 +73,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
function handleImageChange() {
$('#changePictureBtn').on('click', function () {
socket.emit('user.getProfilePictures', {uid: ajaxify.data.uid}, function (err, pictures) {
socket.emit('user.getProfilePictures', {
uid: ajaxify.data.uid
}, function (err, pictures) {
if (err) {
return app.alertError(err.message);
}
@ -216,10 +218,13 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
pictureCropper.show({
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
aspectRatio: 1 / 1,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
fileSize: ajaxify.data.maximumProfileImageSize,
allowSkippingCrop: false,
restrictImageDimension: true,
imageDimension: ajaxify.data.profileImageDimension,
title: '[[user:upload_picture]]',
description: '[[user:upload_a_picture]]',
accept: '.png,.jpg,.bmp'
@ -249,6 +254,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
url: url,
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
allowSkippingCrop: false,
restrictImageDimension: true,
imageDimension: ajaxify.data.profileImageDimension,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
@ -262,7 +270,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
});
modal.find('[data-action="remove-uploaded"]').on('click', function () {
socket.emit('user.removeUploadedPicture', {uid: ajaxify.data.theirid}, function (err) {
socket.emit('user.removeUploadedPicture', {
uid: ajaxify.data.theirid
}, function (err) {
modal.modal('hide');
if (err) {
return app.alertError(err.message);

@ -83,7 +83,9 @@ define('forum/account/header', [
pictureCropper.show({
title: '[[user:upload_cover_picture]]',
socketMethod: 'user.updateCover',
aspectRatio: '16 / 9',
aspectRatio: NaN,
allowSkippingCrop: true,
restrictImageDimension: false,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
accept: '.png,.jpg,.bmp'
@ -131,7 +133,11 @@ define('forum/account/header', [
}, {});
var until = parseInt(formData.length, 10) ? (Date.now() + formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) : 0;
socket.emit('user.banUsers', { uids: [ajaxify.data.theirid], until: until, reason: formData.reason || '' }, function (err) {
socket.emit('user.banUsers', {
uids: [ajaxify.data.theirid],
until: until,
reason: formData.reason || ''
}, function (err) {
if (err) {
return app.alertError(err.message);
}

@ -36,8 +36,8 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds'
$('.account').find('button[data-action="play"]').on('click', function (e) {
e.preventDefault();
var fileName = $(this).parent().parent().find('select').val();
sounds.playFile(fileName);
var soundName = $(this).parent().parent().find('select').val();
sounds.playSound(soundName);
});
toggleCustomRoute();
@ -89,7 +89,7 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds'
}
}
sounds.reloadMapping();
sounds.loadMap();
if (requireReload && parseInt(app.user.uid, 10) === parseInt(ajaxify.data.theirid, 10)) {
app.alert({

@ -34,7 +34,9 @@ define('forum/groups/details', [
pictureCropper.show({
title: '[[groups:upload-group-cover]]',
socketMethod: 'groups.cover.update',
aspectRatio: '16 / 9',
aspectRatio: NaN,
allowSkippingCrop: true,
restrictImageDimension: false,
paramName: 'groupName',
paramValue: groupName
}, function (imageUrlOnServer) {
@ -59,62 +61,62 @@ define('forum/groups/details', [
uid = userRow.attr('data-uid'),
action = btnEl.attr('data-action');
switch(action) {
case 'toggleOwnership':
socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), {
toUid: uid,
groupName: groupName
}, function (err) {
if (!err) {
ownerFlagEl.toggleClass('invisible');
} else {
app.alertError(err.message);
}
});
break;
case 'kick':
socket.emit('groups.kick', {
uid: uid,
groupName: groupName
}, function (err) {
if (!err) {
userRow.slideUp().remove();
} else {
app.alertError(err.message);
}
});
break;
case 'update':
Details.update();
break;
case 'delete':
Details.deleteGroup();
break;
case 'join': // intentional fall-throughs!
case 'leave':
case 'accept':
case 'reject':
case 'issueInvite':
case 'rescindInvite':
case 'acceptInvite':
case 'rejectInvite':
case 'acceptAll':
case 'rejectAll':
socket.emit('groups.' + action, {
toUid: uid,
groupName: groupName
}, function (err) {
if (!err) {
ajaxify.refresh();
} else {
app.alertError(err.message);
}
});
break;
switch (action) {
case 'toggleOwnership':
socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), {
toUid: uid,
groupName: groupName
}, function (err) {
if (!err) {
ownerFlagEl.toggleClass('invisible');
} else {
app.alertError(err.message);
}
});
break;
case 'kick':
socket.emit('groups.kick', {
uid: uid,
groupName: groupName
}, function (err) {
if (!err) {
userRow.slideUp().remove();
} else {
app.alertError(err.message);
}
});
break;
case 'update':
Details.update();
break;
case 'delete':
Details.deleteGroup();
break;
case 'join': // intentional fall-throughs!
case 'leave':
case 'accept':
case 'reject':
case 'issueInvite':
case 'rescindInvite':
case 'acceptInvite':
case 'rejectInvite':
case 'acceptAll':
case 'rejectAll':
socket.emit('groups.' + action, {
toUid: uid,
groupName: groupName
}, function (err) {
if (!err) {
ajaxify.refresh();
} else {
app.alertError(err.message);
}
});
break;
}
});
};

@ -67,7 +67,7 @@ define('chat', [
if (!isSelf && (!modal.is(':visible') || !app.isFocused)) {
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]');
sounds.play('chat-incoming');
sounds.play('chat-incoming', 'chat.incoming:' + data.message.mid);
taskbar.push('chat', modal.attr('UUID'), {
title: username,
@ -89,7 +89,7 @@ define('chat', [
module.toggleNew(modal.attr('UUID'), !isSelf, true);
if (!isSelf) {
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]');
sounds.play('chat-incoming');
sounds.play('chat-incoming', 'chat.incoming:' + data.message.mid);
}
});
});

@ -2,7 +2,7 @@
/* globals define, socket, app, ajaxify, templates, Tinycon*/
define('notifications', ['sounds', 'translator', 'components'], function (sound, translator, components) {
define('notifications', ['sounds', 'translator', 'components'], function (sounds, translator, components) {
var Notifications = {};
var unreadNotifs = {};
@ -105,7 +105,7 @@ define('notifications', ['sounds', 'translator', 'components'], function (sound,
});
if (!unreadNotifs[notifData.nid]) {
sound.play('notification');
sounds.play('notification', notifData.nid);
unreadNotifs[notifData.nid] = true;
}
});

@ -44,7 +44,30 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe
var cropperTool = new cropper.default(img, {
aspectRatio: data.aspectRatio,
viewMode: 1,
cropmove: function () {
if (data.restrictImageDimension) {
if (cropperTool.cropBoxData.width > data.imageDimension) {
cropperTool.setCropBoxData({
width: data.imageDimension
});
}
if (cropperTool.cropBoxData.height > data.imageDimension) {
cropperTool.setCropBoxData({
height: data.imageDimension
});
}
}
},
ready: function () {
if (data.restrictImageDimension) {
var origDimension = (img.width < img.height) ? img.width : img.height;
var dimension = (origDimension > data.imageDimension) ? data.imageDimension : origDimension;
cropperTool.setCropBoxData({
width: dimension,
height: dimension
});
}
cropperModal.find('.rotate').on('click', function () {
var degrees = this.getAttribute("data-degrees");
cropperTool.rotate(degrees);
@ -132,6 +155,9 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe
imageType: imageType,
socketMethod: data.socketMethod,
aspectRatio: data.aspectRatio,
allowSkippingCrop: data.allowSkippingCrop,
restrictImageDimension: data.restrictImageDimension,
imageDimension: data.imageDimension,
paramName: data.paramName,
paramValue: data.paramValue
}, callback);

@ -1,90 +1,94 @@
"use strict";
/* global app, define, socket, config */
define('sounds', ['buzz'], function (buzz) {
define('sounds', function () {
var Sounds = {};
var loadedSounds = {};
var eventSoundMapping;
var files;
var fileMap;
var soundMap;
var cache = {};
socket.on('event:sounds.reloadMapping', function () {
Sounds.reloadMapping();
});
Sounds.reloadMapping = function () {
socket.emit('modules.sounds.getMapping', function (err, mapping) {
Sounds.loadMap = function loadMap(callback) {
socket.emit('modules.sounds.getUserSoundMap', function (err, map) {
if (err) {
return app.alertError(err.message);
}
eventSoundMapping = mapping;
soundMap = map;
if (callback) {
callback();
}
});
};
function loadData(callback) {
socket.emit('modules.sounds.getData', function (err, data) {
if (err) {
return app.alertError('[sounds] Could not load sound mapping!');
}
eventSoundMapping = data.mapping;
files = data.files;
callback();
});
}
function isSoundLoaded(fileName) {
return loadedSounds[fileName];
}
function loadFile(fileName, callback) {
function createSound() {
if (files && files[fileName]) {
loadedSounds[fileName] = new buzz.sound(files[fileName]);
var outstanding = 2;
function after() {
outstanding -= 1;
if (outstanding === 0 && callback) {
callback();
}
callback();
}
if (isSoundLoaded(fileName)) {
return callback();
if (fileMap) {
outstanding -= 1;
} else {
$.getJSON(config.relative_path + '/assets/sounds/fileMap.json', function (map) {
fileMap = map;
after();
});
}
if (!files || !files[fileName]) {
return loadData(createSound);
}
createSound();
Sounds.loadMap(after);
}
Sounds.play = function (name) {
function play() {
Sounds.playFile(eventSoundMapping[name]);
Sounds.playSound = function playSound(soundName) {
if (!soundMap || !fileMap) {
return loadData(after);
}
if (!eventSoundMapping) {
return loadData(play);
function after() {
if (!fileMap[soundName]) {
return;
}
var audio = cache[soundName] = cache[soundName] || new Audio(config.relative_path + '/assets/sounds/' + fileMap[soundName]);
audio.pause();
audio.currentTime = 0;
audio.play();
}
play();
after();
};
Sounds.playFile = function (fileName) {
if (!fileName) {
return;
}
Sounds.play = function play(type, id) {
function after() {
if (!soundMap[type]) {
return;
}
if (id) {
var item = 'sounds.handled:' + id;
if (sessionStorage.getItem(item)) {
return;
}
sessionStorage.setItem(item, true);
function play() {
if (loadedSounds[fileName]) {
loadedSounds[fileName].play();
} else {
app.alertError('[sounds] Not found: ' + fileName);
setTimeout(function () {
sessionStorage.removeItem(item);
}, 5000);
}
Sounds.playSound(soundMap[type]);
}
if (isSoundLoaded(fileName)) {
play();
} else {
loadFile(fileName, play);
if (!soundMap || !fileMap) {
return loadData(after);
}
after();
};
socket.on('event:sounds.reloadMapping', function () {
Sounds.loadMap();
});
return Sounds;
});

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

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

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

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

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

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

@ -389,8 +389,8 @@ function createMenuItems(next) {
if (err || exists) {
return next(err);
}
var navigation = require('./navigation/admin'),
data = require('../install/data/navigation.json');
var navigation = require('./navigation/admin');
var data = require('../install/data/navigation.json');
navigation.save(data, next);
});

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

@ -3,15 +3,18 @@
var ip = require('ip');
var winston = require('winston');
var async = require('async');
var db = require('../database');
var pubsub = require('../pubsub');
var Blacklist = {
_rules: []
};
_rules: []
};
Blacklist.load = function (callback) {
callback = callback || function () {};
async.waterfall([
async.apply(db.get, 'ip-blacklist-rules'),
async.apply(Blacklist.get),
async.apply(Blacklist.validate)
], function (err, rules) {
if (err) {
@ -33,13 +36,18 @@ Blacklist.load = function (callback) {
});
};
pubsub.on('blacklist:reload', Blacklist.load);
Blacklist.save = function (rules, callback) {
db.set('ip-blacklist-rules', rules, function (err) {
if (err) {
return callback(err);
async.waterfall([
function (next) {
db.set('ip-blacklist-rules', rules, next);
},
function (next) {
Blacklist.load(next);
pubsub.publish('blacklist:reload');
}
Blacklist.load(callback);
});
], callback);
};
Blacklist.get = function (callback) {
@ -48,14 +56,14 @@ Blacklist.get = function (callback) {
Blacklist.test = function (clientIp, callback) {
if (
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
&& Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list
&& !Blacklist._rules.cidr.some(function (subnet) {
Blacklist._rules.ipv4.indexOf(clientIp) === -1 &&// not explicitly specified in ipv4 list
Blacklist._rules.ipv6.indexOf(clientIp) === -1 &&// not explicitly specified in ipv6 list
!Blacklist._rules.cidr.some(function (subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
}) // not in a blacklisted cidr range
) {
if (typeof callback === 'function') {
callback();
setImmediate(callback);
} else {
return false;
}
@ -64,7 +72,7 @@ Blacklist.test = function (clientIp, callback) {
err.code = 'blacklisted-ip';
if (typeof callback === 'function') {
callback(err);
setImmediate(callback, err);
} else {
return true;
}
@ -78,9 +86,9 @@ Blacklist.validate = function (rules, callback) {
var cidr = [];
var invalid = [];
var isCidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/,
inlineCommentMatch = /#.*$/,
whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];
var isCidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
var inlineCommentMatch = /#.*$/;
var whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];
// Filter out blank lines and lines starting with the hash character (comments)
// Also trim inputs and remove inline comments

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

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

@ -3,7 +3,7 @@
var uglifyjs = require('uglify-js');
var async = require('async');
var fs = require('fs');
var file = require('./src/file');
var file = require('../file');
var Minifier = {
js: {}

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

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

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

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

@ -1,10 +1,10 @@
'use strict';
var nconf = require('nconf'),
util = require('util'),
winston = require('winston'),
EventEmitter = require('events').EventEmitter;
var nconf = require('nconf');
var util = require('util');
var winston = require('winston');
var EventEmitter = require('events').EventEmitter;
var channelName;

@ -1,9 +1,6 @@
'use strict';
var async = require('async');
var winston = require('winston');
var user = require('../user');
var meta = require('../meta');

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

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

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

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

@ -61,9 +61,7 @@ module.exports = function (User) {
if (exists) {
return next(new Error('[[error:email-taken]]'));
}
next();
},
function (next) {
async.parallel([
function (next) {
db.setAdd('invitation:uid:' + uid, email, next);
@ -131,11 +129,11 @@ module.exports = function (User) {
return next(new Error('[[error:invalid-username]]'));
}
async.parallel([
function deleteFromReferenceList(next) {
db.setRemove('invitation:uid:' + invitedByUid, email, next);
function (next) {
deleteFromReferenceList(invitedByUid, email, next);
},
function deleteInviteKey(next) {
db.delete('invitation:email:' + email, callback);
function (next) {
db.delete('invitation:email:' + email, next);
}
], function (err) {
next(err);
@ -146,7 +144,37 @@ module.exports = function (User) {
User.deleteInvitationKey = function (email, callback) {
callback = callback || function () {};
db.delete('invitation:email:' + email, callback);
async.waterfall([
function (next) {
User.getInvitingUsers(next);
},
function (uids, next) {
async.each(uids, function (uid, next) {
deleteFromReferenceList(uid, email, next);
}, next);
},
function (next) {
db.delete('invitation:email:' + email, next);
}
], callback);
};
function deleteFromReferenceList(uid, email, callback) {
async.waterfall([
function (next) {
db.setRemove('invitation:uid:' + uid, email, next);
},
function (next) {
db.setCount('invitation:uid:' + uid, next);
},
function (count, next) {
if (count === 0) {
return db.setRemove('invitation:uids', uid, next);
}
setImmediate(next);
}
], callback);
}
};

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

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

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

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

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

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

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

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

@ -1,4 +1,4 @@
<div id="navigation">
<div class="row" id="navigation">
<div class="col-lg-9">
<div class="clearfix">
<ul id="active-navigation" class="nav navbar-nav">

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

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

@ -1,5 +1,4 @@
<div class="flags">
<div class="row flags">
<div class="col-lg-12">
<div class="text-center">

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

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

@ -1,5 +1,4 @@
<div class="tags row">
<div class="col-lg-9">
<div class="panel panel-default tag-management">
<div class="panel-body">
@ -76,5 +75,4 @@
</div>
</div>
</div>
</div>

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

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

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

@ -103,9 +103,9 @@ function initializeNodeBB(callback) {
},
function (next) {
async.series([
async.apply(meta.sounds.init),
async.apply(languages.init),
async.apply(meta.blacklist.load)
meta.sounds.addUploads,
languages.init,
meta.blacklist.load,
], next);
}
], callback);

@ -0,0 +1,79 @@
'use strict';
/*global require, after, before*/
var async = require('async');
var assert = require('assert');
var db = require('./mocks/databasemock');
var groups = require('../src/groups');
var user = require('../src/user');
var blacklist = require('../src/meta/blacklist');
describe('blacklist', function () {
var adminUid;
before(function (done) {
groups.resetCache();
user.create({username: 'admin'}, function (err, uid) {
assert.ifError(err);
adminUid = uid;
groups.join('administrators', adminUid, done);
});
});
var socketBlacklist = require('../src/socket.io/blacklist');
var rules = '1.1.1.1\n2.2.2.2\n::ffff:0:2.2.2.2\n127.0.0.1\n192.168.100.0/22';
it('should validate blacklist', function (done) {
socketBlacklist.validate({uid: adminUid}, {
rules: rules
}, function (err, data) {
assert.ifError(err);
done();
});
});
it('should error if not admin', function (done) {
socketBlacklist.save({uid: 0}, rules, function (err) {
assert.equal(err.message, '[[error:no-privileges]]');
done();
});
});
it('should save blacklist', function (done) {
socketBlacklist.save({uid: adminUid}, rules, function (err) {
assert.ifError(err);
done();
});
});
it('should pass ip test against blacklist async', function (done) {
blacklist.test('3.3.3.3', function (err) {
assert.ifError(err);
done();
});
});
it('should pass ip test against blacklist sync', function (done) {
assert(!blacklist.test('3.3.3.3'));
done();
});
it('should fail ip test against blacklist async', function (done) {
blacklist.test('1.1.1.1', function (err) {
assert.equal(err.message, '[[error:blacklisted-ip]]');
done();
});
});
it('should fail ip test against blacklist sync', function (done) {
assert(blacklist.test('1.1.1.1'));
done();
});
after(function (done) {
db.emptydb(done);
});
});

@ -29,6 +29,12 @@ describe('Controllers', function () {
},
user: function (next) {
user.create({username: 'foo', password: 'barbar'}, next);
},
navigation: function (next) {
var navigation = require('../src/navigation/admin');
var data = require('../install/data/navigation.json');
navigation.save(data, next);
}
}, function (err, results) {
if (err) {

@ -0,0 +1,177 @@
<html>
<head>
<title>Excessive Load Warning</title>
<link href='https://fonts.googleapis.com/css?family=Ubuntu:400,500,700' rel='stylesheet' type='text/css'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
body {
background: #00A9EA;
color: white;
font-family: 'Ubuntu', sans-serif;
text-align: center;
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
}
h1 {
font-size: 250px;
color: #fff;
opacity: 0.5;
margin: 10px;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
}
p {
font-size: 20px;
}
p strong {
font-size: 28px;
}
@media (max-width: 640px) {
h1 {
font-size: 125px;
}
p {
font-size: 16px;
}
p strong {
font-size: 20px;
}
}
.center {
position: relative;
top: 50%;
-webkit-transform: translateY(50%);
-ms-transform: translateY(50%);
transform: translateY(50%);
}
@-webkit-keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
.bounce {
-webkit-animation-name: bounce;
animation-name: bounce;
-webkit-transform-origin: center bottom;
-ms-transform-origin: center bottom;
transform-origin: center bottom;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.hide {
display: none;
}
</style>
<script type="text/javascript">
window.onload = function() {
var count = 0,
bounce = document.getElementById('click-me');
bounce.onclick = function() {
count++;
bounce.className = '';
setTimeout(function() {
bounce.className = 'animated bounce';
}, 50);
if (count > 5) {
document.getElementById('hide').className = '';
}
};
}
</script>
</head>
<body>
<div class="wrapper">
<div class="center">
<h1 id="click-me" class="animated bounce">503</h1>
<p>
<strong>This forum is temporarily unavailable due to excessive load.</strong>
</p>
<p>
We shouldn't be down for long. Please check back shortly. Sorry for the inconvenience!
</p>
<p>
&nbsp;<small id="hide" class="hide">Alright. You can stop clicking... it's not going to make the site come back sooner!</small>
</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@ -707,8 +707,8 @@ describe('Groups', function () {
describe('groups cover', function () {
var socketGroups = require('../src/socket.io/groups');
var regularUid;
var logoPath = path.join(__dirname, '../public/logo.png');
var imagePath = path.join(__dirname, '../public/groupcover.png');
var logoPath = path.join(__dirname, '../test/files/test.png');
var imagePath = path.join(__dirname, '../test/files/groupcover.png');
before(function (done) {
User.create({username: 'regularuser', password: '123456'}, function (err, uid) {
assert.ifError(err);

@ -121,6 +121,28 @@ describe('Messaging Library', function () {
});
});
it('should notify offline users of message', function (done) {
Messaging.notificationSendDelay = 100;
db.sortedSetAdd('users:online', Date.now() - 350000, herpUid, function (err) {
assert.ifError(err);
socketModules.chats.send({uid: fooUid}, {roomId: roomId, message: 'second chat message'}, function (err) {
assert.ifError(err);
setTimeout(function () {
User.notifications.get(herpUid, function (err, data) {
assert.ifError(err);
assert(data.unread[0]);
var notification = data.unread[0];
assert.equal(notification.bodyShort, '[[notifications:new_message_from, foo]]');
assert.equal(notification.nid, 'chat_' + fooUid + '_' + roomId);
assert.equal(notification.path, '/chats/' + roomId);
done();
});
}, 1500);
});
});
});
it('should get messages from room', function (done) {
socketModules.chats.getMessages({uid: fooUid}, {
uid: fooUid,

@ -755,6 +755,144 @@ describe('Post\'s', function () {
});
});
describe('socket methods', function () {
var pid;
before(function (done) {
topics.reply({
uid: voterUid,
tid: topicData.tid,
timestamp: Date.now(),
content: 'raw content'
}, function (err, postData) {
assert.ifError(err);
pid = postData.pid;
privileges.categories.rescind(['read'], cid, 'guests', done);
});
});
var socketPosts = require('../src/socket.io/posts');
it('should error with invalid data', function (done) {
socketPosts.reply({uid: 0}, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should error with invalid tid', function (done) {
socketPosts.reply({uid: 0}, {tid: 0, content: 'derp'}, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should fail to get raw post because of privilege', function (done) {
socketPosts.getRawPost({uid: 0}, pid, function (err) {
assert.equal(err.message, '[[error:no-privileges]]');
done();
});
});
it('should fail to get raw post because post is deleted', function (done) {
posts.setPostField(pid, 'deleted', 1, function (err) {
assert.ifError(err);
socketPosts.getRawPost({uid: voterUid}, pid, function (err) {
assert.equal(err.message, '[[error:no-post]]');
done();
});
});
});
it('should get raw post content', function (done) {
posts.setPostField(pid, 'deleted', 0, function (err) {
assert.ifError(err);
socketPosts.getRawPost({uid: voterUid}, pid, function (err, postContent) {
assert.ifError(err);
assert.equal(postContent, 'raw content');
done();
});
});
});
it('should get post', function (done) {
socketPosts.getPost({uid: voterUid}, pid, function (err, postData) {
assert.ifError(err);
assert(postData);
done();
});
});
it('shold error with invalid data', function (done) {
socketPosts.loadMoreBookmarks({uid: voterUid}, {uid: voterUid, after: null}, function (err, postData) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should load more bookmarks', function (done) {
socketPosts.loadMoreBookmarks({uid: voterUid}, {uid: voterUid, after: 0}, function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should load more user posts', function (done) {
socketPosts.loadMoreUserPosts({uid: voterUid}, {uid: voterUid, after: 0}, function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should load more best posts', function (done) {
socketPosts.loadMoreBestPosts({uid: voterUid}, {uid: voterUid, after: 0}, function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should load more up voted posts', function (done) {
socketPosts.loadMoreUpVotedPosts({uid: voterUid}, {uid: voterUid, after: 0}, function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should load more down voted posts', function (done) {
socketPosts.loadMoreDownVotedPosts({uid: voterUid}, {uid: voterUid, after: 0}, function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should get post category', function (done) {
socketPosts.getCategory({uid: voterUid}, pid, function (err, postCid) {
assert.ifError(err);
assert.equal(cid, postCid);
done();
});
});
it('should error with invalid data', function (done) {
socketPosts.getPidIndex({uid: voterUid}, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should get pid index', function (done) {
socketPosts.getPidIndex({uid: voterUid}, {pid: pid, tid: topicData.tid, topicPostSort: 'oldest-to-newest'}, function (err, index) {
assert.ifError(err);
assert.equal(index, 2);
done();
});
});
});
after(function (done) {
db.emptydb(done);
});

@ -130,6 +130,13 @@ describe('Topic\'s', function () {
});
});
it('should error if pid is not a number', function (done) {
socketPosts.getReplies({uid: 0}, 'abc', function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should fail to create new reply with invalid user id', function (done) {
topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function (err) {
assert.equal(err.message, '[[error:no-privileges]]');

@ -67,18 +67,18 @@ describe('Upload Controllers', function () {
});
it('should upload a profile picture', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/user/regular/uploadpicture', path.join(__dirname, '../public/logo.png'), {}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/user/regular/uploadpicture', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
assert.equal(body.length, 1);
assert.equal(body[0].url, '/assets/uploads/profile/' + regularUid + '-profileimg.png');
assert.equal(body[0].url, '/assets/uploads/profile/' + regularUid + '-profileavatar.png');
done();
});
});
it('should upload an image to a post', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../public/logo.png'), {cid: cid}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), {cid: cid}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
@ -91,7 +91,7 @@ describe('Upload Controllers', function () {
it('should upload a file to a post', function (done) {
meta.config.allowFileUploads = 1;
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../public/503.html'), {cid: cid}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/503.html'), {cid: cid}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
@ -118,7 +118,7 @@ describe('Upload Controllers', function () {
});
it('should upload site logo', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadlogo', path.join(__dirname, '../public/logo.png'), {}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadlogo', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
@ -128,7 +128,7 @@ describe('Upload Controllers', function () {
});
it('should upload category image', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../public/logo.png'), {params: JSON.stringify({cid: cid})}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/test.png'), {params: JSON.stringify({cid: cid})}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
@ -138,7 +138,7 @@ describe('Upload Controllers', function () {
});
it('should upload favicon', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadfavicon', path.join(__dirname, '../public/favicon.ico'), {}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadfavicon', path.join(__dirname, '../test/files/favicon.ico'), {}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));
@ -148,7 +148,7 @@ describe('Upload Controllers', function () {
});
it('should upload touch icon', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadTouchIcon', path.join(__dirname, '../public/logo.png'), {}, jar, csrf_token, function (err, res, body) {
helpers.uploadFile(nconf.get('url') + '/api/admin/uploadTouchIcon', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(Array.isArray(body));

@ -2,6 +2,7 @@
var assert = require('assert');
var async = require('async');
var path = require('path');
var nconf = require('nconf');
var request = require('request');
@ -510,27 +511,32 @@ describe('User', function () {
});
it('should upload profile picture', function (done) {
var path = require('path');
var picture = {
path: path.join(nconf.get('base_dir'), 'public', 'logo.png'),
size: 7189,
name: 'logo.png'
};
User.uploadPicture(uid, picture, function (err, uploadedPicture) {
helpers.copyFile(
path.join(nconf.get('base_dir'), 'test/files/test.png'),
path.join(nconf.get('base_dir'), 'test/files/test_copy.png'), function (err) {
assert.ifError(err);
assert.equal(uploadedPicture.url, '/assets/uploads/profile/' + uid + '-profileimg.png');
assert.equal(uploadedPicture.path, path.join(nconf.get('base_dir'), 'public', 'uploads', 'profile', uid + '-profileimg.png'));
done();
var picture = {
path: path.join(nconf.get('base_dir'), 'test/files/test_copy.png'),
size: 7189,
name: 'test_copy.png',
type: 'image/png'
};
User.uploadPicture(uid, picture, function (err, uploadedPicture) {
assert.ifError(err);
assert.equal(uploadedPicture.url, '/assets/uploads/profile/' + uid + '-profileavatar.png');
assert.equal(uploadedPicture.path, path.join(nconf.get('base_dir'), 'public', 'uploads', 'profile', uid + '-profileavatar.png'));
done();
});
});
});
it('should return error if profile image uploads disabled', function (done) {
meta.config.allowProfileImageUploads = 0;
var path = require('path');
var picture = {
path: path.join(nconf.get('base_dir'), 'public', 'logo.png'),
path: path.join(nconf.get('base_dir'), 'test/files/test.png'),
size: 7189,
name: 'logo.png'
name: 'test.png',
type: 'image/png'
};
User.uploadPicture(uid, picture, function (err) {
assert.equal(err.message, '[[error:profile-image-uploads-disabled]]');
@ -540,11 +546,11 @@ describe('User', function () {
it('should return error if profile image is too big', function (done) {
meta.config.allowProfileImageUploads = 1;
var path = require('path');
var picture = {
path: path.join(nconf.get('base_dir'), 'public', 'logo.png'),
path: path.join(nconf.get('base_dir'), 'test/files/test.png'),
size: 265000,
name: 'logo.png'
name: 'test.png',
type: 'image/png'
};
User.uploadPicture(uid, picture, function (err) {
assert.equal(err.message, '[[error:file-too-big, 256]]');
@ -552,12 +558,11 @@ describe('User', function () {
});
});
it('should return error if profile image file has no extension', function (done) {
var path = require('path');
it('should return error if profile image has no mime type', function (done) {
var picture = {
path: path.join(nconf.get('base_dir'), 'public', 'logo.png'),
path: path.join(nconf.get('base_dir'), 'test/files/test.png'),
size: 7189,
name: 'logo'
name: 'test'
};
User.uploadPicture(uid, picture, function (err) {
assert.equal(err.message, '[[error:invalid-image-extension]]');
@ -566,7 +571,7 @@ describe('User', function () {
});
it('should return error if no plugins listening for filter:uploadImage when uploading from url', function (done) {
var url = nconf.get('url') + '/logo.png';
var url = nconf.get('url') + '/assets/logo.png';
User.uploadFromUrl(uid, url, function (err) {
assert.equal(err.message, '[[error:no-plugin]]');
done();
@ -577,7 +582,6 @@ describe('User', function () {
var url = nconf.get('url') + '/favicon.ico';
function filterMethod(data, callback) {
data.foo += 5;
callback(null, data);
}
@ -590,11 +594,10 @@ describe('User', function () {
});
it('should return error if the file is too big when uploading from url', function (done) {
var url = nconf.get('url') + '/logo.png';
var url = nconf.get('url') + '/assets/logo.png';
meta.config.maximumProfileImageSize = 1;
function filterMethod(data, callback) {
data.foo += 5;
callback(null, data);
}
@ -606,20 +609,29 @@ describe('User', function () {
});
});
it('should error with invalid data', function (done) {
var socketUser = require('../src/socket.io/user');
socketUser.uploadProfileImageFromUrl({uid: uid}, {uid: uid, url: ''}, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should upload picture when uploading from url', function (done) {
var url = nconf.get('url') + '/logo.png';
var socketUser = require('../src/socket.io/user');
var url = nconf.get('url') + '/assets/logo.png';
meta.config.maximumProfileImageSize = '';
function filterMethod(data, callback) {
data.foo += 5;
callback(null, {url: url});
}
plugins.registerHook('test-plugin', {hook: 'filter:uploadImage', method: filterMethod});
User.uploadFromUrl(uid, url, function (err, uploadedPicture) {
socketUser.uploadProfileImageFromUrl({uid: uid}, {uid: uid, url: url}, function (err, uploadedPicture) {
assert.ifError(err);
assert.equal(uploadedPicture.url, url);
assert.equal(uploadedPicture, url);
done();
});
});
@ -993,6 +1005,152 @@ describe('User', function () {
});
});
});
});
describe('invites', function () {
var socketUser = require('../src/socket.io/user');
var inviterUid;
before(function (done) {
User.create({
username: 'inviter',
email: 'inviter@nodebb.org'
}, function (err, uid) {
assert.ifError(err);
inviterUid = uid;
done();
});
});
it('should error with invalid data', function (done) {
socketUser.invite({uid: inviterUid}, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should eror if forum is not invite only', function (done) {
socketUser.invite({uid: inviterUid}, 'invite1@test.com', function (err) {
assert.equal(err.message, '[[error:forum-not-invite-only]]');
done();
});
});
it('should error if user is not admin and type is admin-invite-only', function (done) {
meta.config.registrationType = 'admin-invite-only';
socketUser.invite({uid: inviterUid}, 'invite1@test.com', function (err) {
assert.equal(err.message, '[[error:no-privileges]]');
done();
});
});
it('should send invitation email', function (done) {
meta.config.registrationType = 'invite-only';
socketUser.invite({uid: inviterUid}, 'invite1@test.com', function (err) {
assert.ifError(err);
done();
});
});
it('should error if ouf of invitations', function (done) {
meta.config.maximumInvites = 1;
socketUser.invite({uid: inviterUid}, 'invite2@test.com', function (err) {
assert.equal(err.message, '[[error:invite-maximum-met, ' + 1 + ', ' + 1 + ']]');
meta.config.maximumInvites = 5;
done();
});
});
it('should error if email exists', function (done) {
socketUser.invite({uid: inviterUid}, 'inviter@nodebb.org', function (err) {
assert.equal(err.message, '[[error:email-taken]]');
done();
});
});
it('should send invitation email', function (done) {
socketUser.invite({uid: inviterUid}, 'invite2@test.com', function (err) {
assert.ifError(err);
done();
});
});
it('should get user\'s invites', function (done) {
User.getInvites(inviterUid, function (err, data) {
assert.ifError(err);
assert.notEqual(data.indexOf('invite1@test.com'), -1);
assert.notEqual(data.indexOf('invite2@test.com'), -1);
done();
});
});
it('should get all invites', function (done) {
User.getAllInvites(function (err, data) {
assert.ifError(err);
assert.equal(data[0].uid, inviterUid);
assert.notEqual(data[0].invitations.indexOf('invite1@test.com'), -1);
assert.notEqual(data[0].invitations.indexOf('invite2@test.com'), -1);
done();
});
});
it('should fail to verify invitation with invalid data', function (done) {
User.verifyInvitation({token: '', email: ''}, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should fail to verify invitation with invalid email', function (done) {
User.verifyInvitation({token: 'test', email: 'doesnotexist@test.com'}, function (err) {
assert.equal(err.message, '[[error:invalid-token]]');
done();
});
});
it('should verify installation with no errors', function (done) {
db.get('invitation:email:' + 'invite1@test.com', function (err, token) {
assert.ifError(err);
User.verifyInvitation({token: token, email: 'invite1@test.com'}, function (err) {
assert.ifError(err);
done();
});
});
});
it('should error with invalid username', function (done) {
User.deleteInvitation('doesnotexist', 'test@test.com', function (err) {
assert.equal(err.message, '[[error:invalid-username]]');
done();
});
});
it('should delete invitation', function (done) {
var socketAdmin = require('../src/socket.io/admin');
socketAdmin.user.deleteInvitation({uid: inviterUid}, {invitedBy: 'inviter', email: 'invite1@test.com'}, function (err) {
assert.ifError(err);
db.isSetMember('invitation:uid:' + inviterUid, 'invite1@test.com', function (err, isMember) {
assert.ifError(err);
assert.equal(isMember, false);
done();
});
});
});
it('should delete invitation key', function (done) {
User.deleteInvitationKey('invite2@test.com', function (err) {
assert.ifError(err);
db.isSetMember('invitation:uid:' + inviterUid, 'invite2@test.com', function (err, isMember) {
assert.ifError(err);
assert.equal(isMember, false);
db.isSetMember('invitation:uids', inviterUid, function (err, isMember) {
assert.ifError(err);
assert.equal(isMember, false);
done();
});
});
});
});
});

Loading…
Cancel
Save