diff --git a/package.json b/package.json
index e5907c0378..9bfacb03d9 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/language/cs/admin/appearance/skins.json b/public/language/cs/admin/appearance/skins.json
index 64edb7c071..c84298d5a2 100644
--- a/public/language/cs/admin/appearance/skins.json
+++ b/public/language/cs/admin/appearance/skins.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json
index bdabb075e9..607a2fbd92 100644
--- a/public/language/cs/admin/settings/user.json
+++ b/public/language/cs/admin/settings/user.json
@@ -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
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 (Leave blank to disable)",
"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",
diff --git a/public/language/cs/login.json b/public/language/cs/login.json
index 8d189a2d0e..e8652b4d33 100644
--- a/public/language/cs/login.json
+++ b/public/language/cs/login.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json
index 28a90319de..84afa3b052 100644
--- a/public/language/cs/modules.json
+++ b/public/language/cs/modules.json
@@ -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í",
diff --git a/public/language/cs/register.json b/public/language/cs/register.json
index fae5335f6a..fccff9ac25 100644
--- a/public/language/cs/register.json
+++ b/public/language/cs/register.json
@@ -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 @uživatelské-jméno.",
"help.minimum_password_length": "Délka vašeho hesla musí být alespoň %1 znaků.",
diff --git a/public/language/cs/user.json b/public/language/cs/user.json
index 01cf965cc8..25b2599e43 100644
--- a/public/language/cs/user.json
+++ b/public/language/cs/user.json
@@ -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",
diff --git a/public/language/ja/unread.json b/public/language/ja/unread.json
index 47909bf086..feaef7879f 100644
--- a/public/language/ja/unread.json
+++ b/public/language/ja/unread.json
@@ -1,6 +1,6 @@
{
"title": "未読",
- "no_unread_topics": "未読のスレッドがあります。",
+ "no_unread_topics": "未読のスレッドはありません。",
"load_more": "もっと見る",
"mark_as_read": "既読にする",
"selected": "選択済み",
diff --git a/public/language/ru/admin/advanced/cache.json b/public/language/ru/admin/advanced/cache.json
index 208775f991..c0487f399b 100644
--- a/public/language/ru/admin/advanced/cache.json
+++ b/public/language/ru/admin/advanced/cache.json
@@ -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": "Обновить настройки кэша"
}
\ No newline at end of file
diff --git a/public/language/sk/admin/advanced/errors.json b/public/language/sk/admin/advanced/errors.json
index 1b12ed100f..02a4a0c461 100644
--- a/public/language/sk/admin/advanced/errors.json
+++ b/public/language/sk/admin/advanced/errors.json
@@ -1,14 +1,14 @@
{
"figure-x": "Znázorniť %1",
"error-events-per-day": "%1
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é"
}
\ No newline at end of file
diff --git a/public/language/sk/admin/advanced/events.json b/public/language/sk/admin/advanced/events.json
index cce4546e34..ef963e4ce2 100644
--- a/public/language/sk/admin/advanced/events.json
+++ b/public/language/sk/admin/advanced/events.json
@@ -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ť"
}
\ No newline at end of file
diff --git a/public/language/sk/admin/advanced/logs.json b/public/language/sk/admin/advanced/logs.json
index 0dd71a3f30..1c30f4dca7 100644
--- a/public/language/sk/admin/advanced/logs.json
+++ b/public/language/sk/admin/advanced/logs.json
@@ -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é!"
}
\ No newline at end of file
diff --git a/public/language/sk/admin/appearance/skins.json b/public/language/sk/admin/appearance/skins.json
index 4db6fbdd8a..f7440bfa8e 100644
--- a/public/language/sk/admin/appearance/skins.json
+++ b/public/language/sk/admin/appearance/skins.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/sk/admin/appearance/themes.json b/public/language/sk/admin/appearance/themes.json
index 3148a01337..34dca8603e 100644
--- a/public/language/sk/admin/appearance/themes.json
+++ b/public/language/sk/admin/appearance/themes.json
@@ -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."
}
\ No newline at end of file
diff --git a/public/language/sk/admin/general/dashboard.json b/public/language/sk/admin/general/dashboard.json
index a70dda322e..12a34ba046 100644
--- a/public/language/sk/admin/general/dashboard.json
+++ b/public/language/sk/admin/general/dashboard.json
@@ -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",
+ "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": "Deň",
+ "stats.week": "Týždeň",
+ "stats.month": "Mesiac",
+ "stats.all": "Celé obdobie",
- "updates": "Updates",
+ "updates": "Aktualizácie",
"running-version": "You are running NodeBB v%1.",
"keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.",
"up-to-date": "
You are up-to-date
", @@ -22,42 +22,42 @@ "prerelease-upgrade-available": "This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.
", "prerelease-warning": "This is a pre-release version of NodeBB. Unintended bugs may occur.
", - "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" } diff --git a/public/language/sk/admin/manage/tags.json b/public/language/sk/admin/manage/tags.json index db40e9f098..775a9aed63 100644 --- a/public/language/sk/admin/manage/tags.json +++ b/public/language/sk/admin/manage/tags.json @@ -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 here to visit the tag settings page.", "name": "Tag Name", diff --git a/public/less/admin/general/navigation.less b/public/less/admin/general/navigation.less index 3e10639b89..583ec382e1 100644 --- a/public/less/admin/general/navigation.less +++ b/public/less/admin/general/navigation.less @@ -1,10 +1,10 @@ #navigation { #active-navigation { - width: 100%; + float: none; min-height: 50px; border: 1px solid #eee; overflow: auto; - + .active { background-color: #eee; } diff --git a/public/src/admin/general/sounds.js b/public/src/admin/general/sounds.js index 64926e60db..259b21f657 100644 --- a/public/src/admin/general/sounds.js +++ b/public/src/admin/general/sounds.js @@ -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; diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index bc80fd49e5..86b9dcafe6 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -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' @@ -242,17 +247,20 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' if (!url) { return; } - + uploadModal.modal('hide'); - + pictureCropper.handleImageCrop({ url: url, socketMethod: 'user.uploadCroppedPicture', aspectRatio: '1 / 1', + allowSkippingCrop: false, + restrictImageDimension: true, + imageDimension: ajaxify.data.profileImageDimension, paramName: 'uid', paramValue: ajaxify.data.theirid, }, onUploadComplete); - + return false; }); }); @@ -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); diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index 9e1a29577b..63869af05b 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -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); } diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index b2220d69a2..e04d5117ff 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -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({ diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 771c355fcc..6d0559add3 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -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; } }); }; @@ -273,7 +275,7 @@ define('forum/groups/details', [ if (!confirm) { return; } - + socket.emit('groups.cover.remove', { groupName: ajaxify.data.group.name }, function (err) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index f231fd765d..e8a108c46f 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -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); } }); }); diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index c668cbbfc7..f9972ca5a9 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -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; } }); diff --git a/public/src/modules/pictureCropper.js b/public/src/modules/pictureCropper.js index b04bc48e97..60dd073532 100644 --- a/public/src/modules/pictureCropper.js +++ b/public/src/modules/pictureCropper.js @@ -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); diff --git a/public/src/modules/sounds.js b/public/src/modules/sounds.js index a31bbe507a..26d64079ca 100644 --- a/public/src/modules/sounds.js +++ b/public/src/modules/sounds.js @@ -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; - } - - function play() { - if (loadedSounds[fileName]) { - loadedSounds[fileName].play(); - } else { - app.alertError('[sounds] Not found: ' + fileName); + 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); + + 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; }); diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index c9549a1def..58409060e4 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -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,8 +167,10 @@ editController.uploadCoverPicture = function (req, res, next) { return next(err); } - res.json([{ url: image.url }]); + res.json([{ + url: image.url + }]); }); }; -module.exports = editController; \ No newline at end of file +module.exports = editController; diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index 44499e7e68..68cef8be5f 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -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 } ]; diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js index 801a2067ac..b042048313 100644 --- a/src/controllers/admin/sounds.js +++ b/src/controllers/admin/sounds.js @@ -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); } + + settings = settings || {}; - sounds = Object.keys(sounds).map(function (name) { - return { - name: name - }; - }); + 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); }); }; diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 346e7765e4..0b7104f1de 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -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); } diff --git a/src/groups/cover.js b/src/groups/cover.js index 3512a235d5..8f18c60ad7 100644 --- a/src/groups/cover.js +++ b/src/groups/cover.js @@ -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); }; diff --git a/src/image.js b/src/image.js index 0ab68e5a23..7b428f2331 100644 --- a/src/image.js +++ b/src/image.js @@ -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); + }); +}; \ No newline at end of file diff --git a/src/install.js b/src/install.js index 8c14302d80..c5ae1e214b 100644 --- a/src/install.js +++ b/src/install.js @@ -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); }); diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 6d9500c4ba..2eeb562b5c 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -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); - } - }); }); } }; \ No newline at end of file diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 679dc3cec9..a078b89815 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -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 diff --git a/src/meta/build.js b/src/meta/build.js index 3f2feb8a2a..29cfbc45be 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -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); diff --git a/src/meta/js.js b/src/meta/js.js index b6b6e61ee6..90f757361a 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -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', @@ -89,25 +92,120 @@ 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); - mkdirp(path.dirname(destPath), function (err) { - if (err) { - return next(err); - } + 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); + }; - file.link(filePath, destPath, next); - }); - }, callback); + function linkModules(callback) { + var modules = Meta.js.scripts.modules; + + async.eachLimit(Object.keys(modules), 1000, function (relPath, next) { + var filePath = path.join(__dirname, '../../', modules[relPath]); + var destPath = path.join(__dirname, '../../build/public/src/modules', relPath); + + mkdirp(path.dirname(destPath), function (err) { + if (err) { + return next(err); + } + + file.link(filePath, destPath, next); + }); + }, callback); + }; + + var moduleDirs = ['modules', 'admin', 'client']; + + function getModuleList(callback) { + var modules = Object.keys(Meta.js.scripts.modules).map(function (relPath) { + return { + filePath: path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), + destPath: path.join(__dirname, '../../build/public/src/modules', relPath), + }; }); + + var dirs = moduleDirs.map(function (dir) { + return path.join(__dirname, '../../public/src', dir); + }); + + async.each(dirs, function (dir, next) { + utils.walk(dir, function (err, files) { + if (err) { + return next(err); + } + + 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] = {}; diff --git a/minifier.js b/src/meta/minifier.js similarity index 97% rename from minifier.js rename to src/meta/minifier.js index 25c0177175..5c47bc3e4e 100644 --- a/minifier.js +++ b/src/meta/minifier.js @@ -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: {} diff --git a/src/meta/sounds.js b/src/meta/sounds.js index d237a51273..00bcbc2410 100644 --- a/src/meta/sounds.js +++ b/src/meta/sounds.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); - } -}; \ No newline at end of file +}; diff --git a/src/navigation/admin.js b/src/navigation/admin.js index 39258e6702..ab1c5aff42 100644 --- a/src/navigation/admin.js +++ b/src/navigation/admin.js @@ -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; \ No newline at end of file diff --git a/src/plugins.js b/src/plugins.js index 789c92b72c..15f4b91650 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -32,6 +32,7 @@ var middleware; Plugins.libraryPaths = []; Plugins.versionWarning = []; Plugins.languageCodes = []; + Plugins.soundpacks = []; Plugins.initialized = false; diff --git a/src/plugins/load.js b/src/plugins/load.js index 01dedc4e45..c6a9429667 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -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. diff --git a/src/pubsub.js b/src/pubsub.js index a2e11746e1..f5c6c2f0ed 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -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; diff --git a/src/socket.io/blacklist.js b/src/socket.io/blacklist.js index a05d632f3f..8592a8b901 100644 --- a/src/socket.io/blacklist.js +++ b/src/socket.io/blacklist.js @@ -1,9 +1,6 @@ 'use strict'; -var async = require('async'); -var winston = require('winston'); - var user = require('../user'); var meta = require('../meta'); diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 58e31a78ea..f4732d9d9e 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -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') }); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index a23e9c64bb..473a32e5d9 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -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; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index fe729a5c11..8ded335780 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -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([ diff --git a/src/socket.io/user.js b/src/socket.io/user.js index c10a510e71..060c9e7b2e 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -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) { diff --git a/src/user/invite.js b/src/user/invite.js index 1066a662ac..043bf62a7b 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -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); + } + }; diff --git a/src/user/picture.js b/src/user/picture.js index ceb018b917..09b7d636e8 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -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 }); } diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index 70f82ab5db..126cba8db0 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -1,5 +1,5 @@ -