mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-20 15:30:39 +01:00
Resolve merge conflicts, minify only .js files
This commit is contained in:
33
Gruntfile.js
33
Gruntfile.js
@@ -4,14 +4,16 @@ var fork = require('child_process').fork;
|
|||||||
var env = process.env;
|
var env = process.env;
|
||||||
var worker;
|
var worker;
|
||||||
var updateWorker;
|
var updateWorker;
|
||||||
|
var initWorker;
|
||||||
var incomplete = [];
|
var incomplete = [];
|
||||||
var running = 0;
|
var running = 0;
|
||||||
|
|
||||||
|
|
||||||
module.exports = function (grunt) {
|
module.exports = function (grunt) {
|
||||||
var args = [];
|
var args = [];
|
||||||
|
var initArgs = ['--build'];
|
||||||
if (!grunt.option('verbose')) {
|
if (!grunt.option('verbose')) {
|
||||||
args.push('--log-level=info');
|
args.push('--log-level=info');
|
||||||
|
initArgs.push('--log-level=info');
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(action, filepath, target) {
|
function update(action, filepath, target) {
|
||||||
@@ -49,7 +51,9 @@ module.exports = function (grunt) {
|
|||||||
updateWorker.on('exit', function () {
|
updateWorker.on('exit', function () {
|
||||||
running -= 1;
|
running -= 1;
|
||||||
if (running === 0) {
|
if (running === 0) {
|
||||||
worker = fork('app.js', args, { env: env });
|
worker = fork('app.js', args, {
|
||||||
|
env: env,
|
||||||
|
});
|
||||||
worker.on('message', function () {
|
worker.on('message', function () {
|
||||||
if (incomplete.length) {
|
if (incomplete.length) {
|
||||||
incomplete = [];
|
incomplete = [];
|
||||||
@@ -131,15 +135,24 @@ module.exports = function (grunt) {
|
|||||||
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||||
|
|
||||||
if (grunt.option('skip')) {
|
grunt.registerTask('default', ['watch']);
|
||||||
grunt.registerTask('default', ['watch:serverUpdated']);
|
|
||||||
} else {
|
|
||||||
grunt.registerTask('default', ['watch']);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
env.NODE_ENV = 'development';
|
env.NODE_ENV = 'development';
|
||||||
|
|
||||||
worker = fork('app.js', args, { env: env });
|
if (grunt.option('skip')) {
|
||||||
|
worker = fork('app.js', args, {
|
||||||
|
env: env,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
initWorker = fork('app.js', initArgs, {
|
||||||
|
env: env,
|
||||||
|
});
|
||||||
|
|
||||||
|
initWorker.on('exit', function () {
|
||||||
|
worker = fork('app.js', args, {
|
||||||
|
env: env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
grunt.event.on('watch', update);
|
grunt.event.on('watch', update);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,9 +56,9 @@
|
|||||||
"nodebb-plugin-dbsearch": "1.0.5",
|
"nodebb-plugin-dbsearch": "1.0.5",
|
||||||
"nodebb-plugin-emoji-extended": "1.1.1",
|
"nodebb-plugin-emoji-extended": "1.1.1",
|
||||||
"nodebb-plugin-emoji-one": "1.1.5",
|
"nodebb-plugin-emoji-one": "1.1.5",
|
||||||
"nodebb-plugin-markdown": "7.1.0",
|
"nodebb-plugin-markdown": "7.1.1",
|
||||||
"nodebb-plugin-mentions": "1.1.3",
|
"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-plugin-spam-be-gone": "0.4.10",
|
||||||
"nodebb-rewards-essentials": "0.0.9",
|
"nodebb-rewards-essentials": "0.0.9",
|
||||||
"nodebb-theme-lavender": "3.0.15",
|
"nodebb-theme-lavender": "3.0.15",
|
||||||
|
|||||||
@@ -12,11 +12,47 @@
|
|||||||
"bootbox": true,
|
"bootbox": true,
|
||||||
"templates": true,
|
"templates": true,
|
||||||
"Visibility": true,
|
"Visibility": true,
|
||||||
"Tinycon": true
|
"Tinycon": true,
|
||||||
|
"Promise": true
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"jquery": true,
|
"jquery": true,
|
||||||
"amd": true,
|
"amd": true,
|
||||||
"browser": true
|
"browser": true,
|
||||||
|
"es6": false
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-dupe-class-members": "off",
|
||||||
|
"no-var": "off",
|
||||||
|
"object-shorthand": "off",
|
||||||
|
"prefer-arrow-callback": "off",
|
||||||
|
"prefer-spread": "off",
|
||||||
|
"prefer-reflect": "off",
|
||||||
|
"prefer-template": "off"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 5,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"arrowFunctions": false,
|
||||||
|
"classes": false,
|
||||||
|
"defaultParams": false,
|
||||||
|
"destructuring": false,
|
||||||
|
"experimentalObjectRestSpread": false,
|
||||||
|
"blockBindings": false,
|
||||||
|
"forOf": false,
|
||||||
|
"generators": false,
|
||||||
|
"globalReturn": false,
|
||||||
|
"jsx": false,
|
||||||
|
"modules": false,
|
||||||
|
"objectLiteralComputedProperties": false,
|
||||||
|
"objectLiteralDuplicateProperties": false,
|
||||||
|
"objectLiteralShorthandMethods": false,
|
||||||
|
"objectLiteralShorthandProperties": false,
|
||||||
|
"impliedStrict": false,
|
||||||
|
"restParams": false,
|
||||||
|
"spread": false,
|
||||||
|
"superInFunctions": false,
|
||||||
|
"templateStrings": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"loading": "Načítání motivů…",
|
"loading": "Načítání motivů…",
|
||||||
"homepage": "Homepage",
|
"homepage": "Domovská stránka",
|
||||||
"select-skin": "Select Skin",
|
"select-skin": "Vyber motiv",
|
||||||
"current-skin": "Current Skin",
|
"current-skin": "Současný motiv",
|
||||||
"skin-updated": "Skin Updated",
|
"skin-updated": "Motiv aktualizován",
|
||||||
"applied-success": "%1 skin was succesfully applied",
|
"applied-success": "%1 skin was succesfully applied",
|
||||||
"revert-success": "Skin reverted to base colours"
|
"revert-success": "Skin reverted to base colours"
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"authentication": "Authentication",
|
"authentication": "Ověření",
|
||||||
"allow-local-login": "Allow local login",
|
"allow-local-login": "Povolit místní přihlášení",
|
||||||
"require-email-confirmation": "Require Email Confirmation",
|
"require-email-confirmation": "Vyžadovat potvrzení e-mailem",
|
||||||
"email-confirm-interval": "User may not resend a confirmation email until",
|
"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": "Allow login with",
|
||||||
"allow-login-with.username-email": "Username or Email",
|
"allow-login-with.username-email": "Uživatelské jméno nebo e-mail",
|
||||||
"allow-login-with.username": "Username Only",
|
"allow-login-with.username": "Pouze uživatelské jméno",
|
||||||
"allow-login-with.email": "Email Only",
|
"allow-login-with.email": "Pouze e-mail",
|
||||||
"account-settings": "Account Settings",
|
"account-settings": "Nastavení účtu",
|
||||||
"disable-username-changes": "Disable username changes",
|
"disable-username-changes": "Zakázat změnu uživatelského jména",
|
||||||
"disable-email-changes": "Disable email changes",
|
"disable-email-changes": "Zakázat změnu e-mailu",
|
||||||
"disable-password-changes": "Disable password changes",
|
"disable-password-changes": "Zakázat změnu hesla",
|
||||||
"allow-account-deletion": "Allow account deletion",
|
"allow-account-deletion": "Povolit smazání účtu",
|
||||||
"user-info-private": "Make user info private",
|
"user-info-private": "Make user info private",
|
||||||
"themes": "Themes",
|
"themes": "Témata",
|
||||||
"disable-user-skins": "Prevent users from choosing a custom skin",
|
"disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu",
|
||||||
"account-protection": "Account Protection",
|
"account-protection": "Account Protection",
|
||||||
"login-attempts": "Login attempts per hour",
|
"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",
|
"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",
|
"registration.max-invites": "Maximum Invitations per User",
|
||||||
"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\"",
|
"max-invites-help": "0 for no restriction. Admins get infinite invitations<br>Only applicable for \"Invite Only\"",
|
||||||
"min-username-length": "Minimum Username Length",
|
"min-username-length": "Minimální délka uživatelského jména",
|
||||||
"max-username-length": "Maximum Username Length",
|
"max-username-length": "Maximální délka uživatelského jména",
|
||||||
"min-password-length": "Minimum Password Length",
|
"min-password-length": "Minimální délka hesla",
|
||||||
"max-about-me-length": "Maximum About Me Length",
|
"max-about-me-length": "Maximální délka hesla",
|
||||||
"terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>",
|
"terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>",
|
||||||
"user-search": "User Search",
|
"user-search": "User Search",
|
||||||
"user-search-results-per-page": "Number of results to display",
|
"user-search-results-per-page": "Number of results to display",
|
||||||
@@ -48,10 +48,10 @@
|
|||||||
"outgoing-new-tab": "Open outgoing links in new tab",
|
"outgoing-new-tab": "Open outgoing links in new tab",
|
||||||
"topic-search": "Enable In-Topic Searching",
|
"topic-search": "Enable In-Topic Searching",
|
||||||
"digest-freq": "Subscribe to Digest",
|
"digest-freq": "Subscribe to Digest",
|
||||||
"digest-freq.off": "Off",
|
"digest-freq.off": "Vypnuto",
|
||||||
"digest-freq.daily": "Daily",
|
"digest-freq.daily": "Denně",
|
||||||
"digest-freq.weekly": "Weekly",
|
"digest-freq.weekly": "Týdně",
|
||||||
"digest-freq.monthly": "Monthly",
|
"digest-freq.monthly": "Měsíčně",
|
||||||
"email-chat-notifs": "Send an email if a new chat message arrives and I am not online",
|
"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",
|
"email-post-notif": "Send an email when replies are made to topics I am subscribed to",
|
||||||
"follow-created-topics": "Follow topics you create",
|
"follow-created-topics": "Follow topics you create",
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"failed_login_attempt": "Přihlášení neúspěšné",
|
"failed_login_attempt": "Přihlášení neúspěšné",
|
||||||
"login_successful": "Přihlášení proběhlo úspěšně!",
|
"login_successful": "Přihlášení proběhlo úspěšně!",
|
||||||
"dont_have_account": "Nemáte účet?",
|
"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.contacts": "Kontakty",
|
||||||
"chat.message-history": "Historie zpráv",
|
"chat.message-history": "Historie zpráv",
|
||||||
"chat.pop-out": "Skrýt chat",
|
"chat.pop-out": "Skrýt chat",
|
||||||
"chat.minimize": "Minimize",
|
"chat.minimize": "Minimalizovat",
|
||||||
"chat.maximize": "Maximalizovat",
|
"chat.maximize": "Maximalizovat",
|
||||||
"chat.seven_days": "7 dní",
|
"chat.seven_days": "7 dní",
|
||||||
"chat.thirty_days": "30 dní",
|
"chat.thirty_days": "30 dní",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"register": "Registrace",
|
"register": "Registrace",
|
||||||
"cancel_registration": "Cancel Registration",
|
"cancel_registration": "Zrušit registraci",
|
||||||
"help.email": "Ve výchozím nastavení bude váš e-mail skrytý.",
|
"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.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ů.",
|
"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",
|
"topics_per_page": "Témat na stránce",
|
||||||
"posts_per_page": "Příspěvků na stránce",
|
"posts_per_page": "Příspěvků na stránce",
|
||||||
"notification_sounds": "Přehrát zvuk když dostanete notifikaci",
|
"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",
|
"incoming-message-sound": "Incoming message sound",
|
||||||
"outgoing-message-sound": "Outgoing message sound",
|
"outgoing-message-sound": "Outgoing message sound",
|
||||||
"notification-sound": "Notification sound",
|
"notification-sound": "Notification sound",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"upgrade-available": "<p>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>",
|
"upgrade-available": "<p>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-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-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>",
|
"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>",
|
||||||
|
"running-in-development": "<span>Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.</span>",
|
||||||
|
|
||||||
"notices": "Notices",
|
"notices": "Notices",
|
||||||
"restart-not-required": "Restart not required",
|
"restart-not-required": "Restart not required",
|
||||||
|
|||||||
@@ -107,10 +107,10 @@
|
|||||||
"more_guests": "ゲストさんが%1人",
|
"more_guests": "ゲストさんが%1人",
|
||||||
"users_and_others": "%1と他は%2",
|
"users_and_others": "%1と他は%2",
|
||||||
"sort_by": "並び替え",
|
"sort_by": "並び替え",
|
||||||
"oldest_to_newest": "新しい順に",
|
"oldest_to_newest": "古い\bものから新しい順",
|
||||||
"newest_to_oldest": "古い順に",
|
"newest_to_oldest": "新しいものから古い順",
|
||||||
"most_votes": "最高評価",
|
"most_votes": "最も投票された順",
|
||||||
"most_posts": "最高投稿",
|
"most_posts": "最も投稿された順",
|
||||||
"stale.title": "新しいスレッドを作りますか?",
|
"stale.title": "新しいスレッドを作りますか?",
|
||||||
"stale.warning": "あなたが返信しようとしてるスレッドが古いスレッドです。新しいスレッドを作って、そしてこのスレッドが参考として入れた方を勧めます。そうしますか?",
|
"stale.warning": "あなたが返信しようとしてるスレッドが古いスレッドです。新しいスレッドを作って、そしてこのスレッドが参考として入れた方を勧めます。そうしますか?",
|
||||||
"stale.create": "新しいスレッドを作ります。",
|
"stale.create": "新しいスレッドを作ります。",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "未読",
|
"title": "未読",
|
||||||
"no_unread_topics": "未読のスレッドがあります。",
|
"no_unread_topics": "未読のスレッドはありません。",
|
||||||
"load_more": "もっと見る",
|
"load_more": "もっと見る",
|
||||||
"mark_as_read": "既読にする",
|
"mark_as_read": "既読にする",
|
||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"post-cache": "Кэш записи",
|
"post-cache": "Кэш записи",
|
||||||
"posts-in-cache": "Записей в кэше",
|
"posts-in-cache": "Записей в кэше",
|
||||||
"average-post-size": "Average Post Size",
|
"average-post-size": "Средний размер записи",
|
||||||
"length-to-max": "Length / Max",
|
"length-to-max": "Длина / Максимальная",
|
||||||
"percent-full": "%1% Full",
|
"percent-full": "%1% Full",
|
||||||
"post-cache-size": "Post Cache Size",
|
"post-cache-size": "Размер записи в кэше",
|
||||||
"items-in-cache": "Items in Cache",
|
"items-in-cache": "Items in Cache",
|
||||||
"control-panel": "Control Panel",
|
"control-panel": "Панель управления",
|
||||||
"update-settings": "Update Cache Settings"
|
"update-settings": "Обновить настройки кэша"
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"figure-x": "Znázorniť %1",
|
"figure-x": "Znázorniť %1",
|
||||||
"error-events-per-day": "<code>%1</code> events per day",
|
"error-events-per-day": "<code>%1</code> events per day",
|
||||||
"error.404": "404 Not Found",
|
"error.404": "404 Nenájdené",
|
||||||
"error.503": "503 Service Unavailable",
|
"error.503": "503 Služba nie je k dispozícií",
|
||||||
"manage-error-log": "Manage Error Log",
|
"manage-error-log": "Manage Error Log",
|
||||||
"export-error-log": "Export Error Log (CSV)",
|
"export-error-log": "Export Error Log (CSV)",
|
||||||
"clear-error-log": "Clear Error Log",
|
"clear-error-log": "Clear Error Log",
|
||||||
"route": "Route",
|
"route": "Route",
|
||||||
"count": "Count",
|
"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-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",
|
"events": "Udalosti",
|
||||||
"no-events": "There are no events",
|
"no-events": "Zatiaľ neexistujô žiadne udalosti",
|
||||||
"control-panel": "Events Control Panel",
|
"control-panel": "Ovládací panel udalostí",
|
||||||
"delete-events": "Delete Events"
|
"delete-events": "Odstrániť udalosť"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"logs": "Protokoly",
|
"logs": "Záznamy",
|
||||||
"control-panel": "Logs Control Panel",
|
"control-panel": "Ovládací panel záznamov",
|
||||||
"reload": "Reload Logs",
|
"reload": "Znovu načítať záznamy",
|
||||||
"clear": "Clear Logs",
|
"clear": "Vyčistiť záznamy",
|
||||||
"clear-success": "Logs Cleared!"
|
"clear-success": "Záznamy vyčistené!"
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"loading": "Loading Skins...",
|
"loading": "Načítať vzhľady...",
|
||||||
"homepage": "Homepage",
|
"homepage": "Domovska stránka",
|
||||||
"select-skin": "Select Skin",
|
"select-skin": "Vybrať vzhľad",
|
||||||
"current-skin": "Current Skin",
|
"current-skin": "Aktuálny vzhľad",
|
||||||
"skin-updated": "Skin Updated",
|
"skin-updated": "Vzhľad aktualizovaný",
|
||||||
"applied-success": "%1 skin was succesfully applied",
|
"applied-success": "%1 vzhľad bol úspešne aplikovaný",
|
||||||
"revert-success": "Skin reverted to base colours"
|
"revert-success": "Vzhľad bol obnovený do základných farieb"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"checking-for-installed": "Checking for installed themes...",
|
"checking-for-installed": "Kontrola nainštalovaných motívov...",
|
||||||
"homepage": "Homepage",
|
"homepage": "Domovská stránka",
|
||||||
"select-theme": "Select Theme",
|
"select-theme": "Vybrať motív",
|
||||||
"current-theme": "Current Theme",
|
"current-theme": "Aktuálny motív",
|
||||||
"no-themes": "No installed themes found",
|
"no-themes": "Žiadne nainštalované motívy neboli nájdené",
|
||||||
"revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
|
"revert-confirm": "Ste si istý, že chcete obnoviť predvolený NodeBB motív?",
|
||||||
"theme-changed": "Theme Changed",
|
"theme-changed": "Motív zmenený",
|
||||||
"revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
|
"revert-success": "Úspešne sa Vám podarilo obnoviť Váš NodeBB do predvoleného motívu.",
|
||||||
"restart-to-activate": "Please restart your NodeBB to fully activate this theme"
|
"restart-to-activate": "Prosím, reštartujte Váš NodeBB pre úplne aktivovanie tohto motívu."
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"forum-traffic": "Forum Traffic",
|
"forum-traffic": "Prevádzka fóra",
|
||||||
"page-views": "Page Views",
|
"page-views": "Zobrazenia stránok",
|
||||||
"unique-visitors": "Unique Visitors",
|
"unique-visitors": "Unikátne návštevy",
|
||||||
"users": "Users",
|
"users": "Užívatelia",
|
||||||
"posts": "Posts",
|
"posts": "Príspevky",
|
||||||
"topics": "Topics",
|
"topics": "Témy",
|
||||||
"page-views-last-month": "Page views Last Month",
|
"page-views-last-month": "Zobrazenia stránok za posledný mesiac",
|
||||||
"page-views-this-month": "Page views This Month",
|
"page-views-this-month": "Zobrazenia stránok za tento mesiac",
|
||||||
"page-views-last-day": "Page views in last 24 hours",
|
"page-views-last-day": "Zobrazenia stránok za posledných 24 hodín",
|
||||||
|
|
||||||
"stats.day": "Day",
|
"stats.day": "Deň",
|
||||||
"stats.week": "Week",
|
"stats.week": "Týždeň",
|
||||||
"stats.month": "Month",
|
"stats.month": "Mesiac",
|
||||||
"stats.all": "All Time",
|
"stats.all": "Celé obdobie",
|
||||||
|
|
||||||
"updates": "Updates",
|
"updates": "Aktualizácie",
|
||||||
"running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.",
|
"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.",
|
"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>",
|
"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-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>",
|
"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",
|
"notices": "Upozornenie",
|
||||||
"restart-not-required": "Restart not required",
|
"restart-not-required": "Reštart nie je potrebný",
|
||||||
"restart-required": "Restart required",
|
"restart-required": "Reštart je potrebný",
|
||||||
"search-plugin-installed": "Search Plugin installed",
|
"search-plugin-installed": "Vyhľadávací doplnok bol nainštalovaný",
|
||||||
"search-plugin-not-installed": "Search Plugin not installed",
|
"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",
|
"search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality",
|
||||||
|
|
||||||
"control-panel": "System Control",
|
"control-panel": "System Control",
|
||||||
"reload": "Reload",
|
"reload": "Obnoviť",
|
||||||
"restart": "Restart",
|
"restart": "Reštartovať",
|
||||||
"restart-warning": "Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.",
|
"restart-warning": "Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.",
|
||||||
"maintenance-mode": "Maintenance Mode",
|
"maintenance-mode": "Maintenance Mode",
|
||||||
"maintenance-mode-title": "Click here to set up maintenance mode for NodeBB",
|
"maintenance-mode-title": "Click here to set up maintenance mode for NodeBB",
|
||||||
"realtime-chart-updates": "Realtime Chart Updates",
|
"realtime-chart-updates": "Realtime Chart Updates",
|
||||||
|
|
||||||
"active-users": "Active Users",
|
"active-users": "Aktívny užívatelia",
|
||||||
"active-users.users": "Users",
|
"active-users.users": "Užívatelia",
|
||||||
"active-users.guests": "Guests",
|
"active-users.guests": "Hostia",
|
||||||
"active-users.total": "Total",
|
"active-users.total": "Celkovo",
|
||||||
"active-users.connections": "Connections",
|
"active-users.connections": "Pripojení",
|
||||||
|
|
||||||
"anonymous-registered-users": "Anonymous vs Registered Users",
|
"anonymous-registered-users": "Neznámy vs Zaregistrovaný užívatelia",
|
||||||
"anonymous": "Anonymous",
|
"anonymous": "Neznámy",
|
||||||
"registered": "Registered",
|
"registered": "Zaregistrovaný",
|
||||||
|
|
||||||
"user-presence": "User Presence",
|
"user-presence": "User Presence",
|
||||||
"on-categories": "On categories list",
|
"on-categories": "On categories list",
|
||||||
"reading-posts": "Reading posts",
|
"reading-posts": "Reading posts",
|
||||||
"browsing-topics": "Browsing topics",
|
"browsing-topics": "Browsing topics",
|
||||||
"recent": "Recent",
|
"recent": "Nedávne",
|
||||||
"unread": "Unread",
|
"unread": "Neprečitané",
|
||||||
|
|
||||||
"high-presence-topics": "High Presence Topics",
|
"high-presence-topics": "High Presence Topics",
|
||||||
|
|
||||||
"graphs.page-views": "Page Views",
|
"graphs.page-views": "Zobrazenia stránok",
|
||||||
"graphs.unique-visitors": "Unique Visitors",
|
"graphs.unique-visitors": "Unikátny navštevníci",
|
||||||
"graphs.registered-users": "Registered Users",
|
"graphs.registered-users": "Zarestrovaný užívatelia",
|
||||||
"graphs.anonymous-users": "Anonymous Users"
|
"graphs.anonymous-users": "Neznámy užívatelia"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"description": "Select tags via clicking and/or dragging, use shift to select multiple.",
|
"description": "Select tags via clicking and/or dragging, use shift to select multiple.",
|
||||||
"create": "Create Tag",
|
"create": "Create Tag",
|
||||||
"modify": "Modify Tags",
|
"modify": "Modify Tags",
|
||||||
"delete": "Delete Selected Tags",
|
"delete": "Odstrániť vybraté značky",
|
||||||
"search": "Search for tags...",
|
"search": "Search for tags...",
|
||||||
"settings": "Click <a href=\"%1\">here</a> to visit the tag settings page.",
|
"settings": "Click <a href=\"%1\">here</a> to visit the tag settings page.",
|
||||||
"name": "Tag Name",
|
"name": "Tag Name",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"posts": "İletiler",
|
"posts": "İletiler",
|
||||||
"allow-files": "Allow users to upload regular files",
|
"allow-files": "Allow users to upload regular files",
|
||||||
"private": "Make uploaded files private",
|
"private": "Yüklenen dosyaları gizli yap",
|
||||||
"max-image-width": "Resize images down to specified width (in pixels)",
|
"max-image-width": "Resize images down to specified width (in pixels)",
|
||||||
"max-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)",
|
"max-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)",
|
||||||
"max-file-size": "Maksimum Dosya Boyutu (KiB)",
|
"max-file-size": "Maksimum Dosya Boyutu (KiB)",
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
"profile-image-dimension-help": "(in pixels, default: 128 pixels)",
|
"profile-image-dimension-help": "(in pixels, default: 128 pixels)",
|
||||||
"max-profile-image-size": "Maximum Profile Image File Size",
|
"max-profile-image-size": "Maximum Profile Image File Size",
|
||||||
"max-profile-image-size-help": "(in kilobytes, default: 256 KiB)",
|
"max-profile-image-size-help": "(in kilobytes, default: 256 KiB)",
|
||||||
"max-cover-image-size": "Maximum Cover Image File Size",
|
"max-cover-image-size": "Maksimum Kapak Görseli Dosya Boyutu",
|
||||||
"max-cover-image-size-help": "(in kilobytes, default: 2,048 KiB)",
|
"max-cover-image-size-help": "(in kilobytes, default: 2,048 KiB)",
|
||||||
"keep-all-user-images": "Keep old versions of avatars and profile covers on the server",
|
"keep-all-user-images": "Keep old versions of avatars and profile covers on the server",
|
||||||
"profile-covers": "Profile Covers",
|
"profile-covers": "Profil Kapakları",
|
||||||
"default-covers": "Default Cover Images",
|
"default-covers": "Varsayılan Kapak Görseli",
|
||||||
"default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image"
|
"default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ body {
|
|||||||
color: @gray-dark;
|
color: @gray-dark;
|
||||||
|
|
||||||
&:hover, &.selected {
|
&:hover, &.selected {
|
||||||
background: black;
|
background: @brand-primary;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#navigation {
|
#navigation {
|
||||||
#active-navigation {
|
#active-navigation {
|
||||||
width: 100%;
|
float: none;
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,25 @@
|
|||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: 100px;
|
|
||||||
.member-name {
|
.member-name {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#group-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups {
|
||||||
|
#group-search {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-list {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,30 +25,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-item {
|
|
||||||
border: solid 1px @gray-light;
|
|
||||||
background-color: transparent;
|
|
||||||
color: @gray-light;
|
|
||||||
padding: .2em .6em .3em;
|
|
||||||
font-size: 75%;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-topic-count {
|
.tag-topic-count {
|
||||||
border: solid 1px lighten(@brand-primary, 20%);
|
font-size: 14px;
|
||||||
background-color: lighten(@brand-primary, 20%);
|
|
||||||
padding: 6px;
|
|
||||||
font-size: 75%;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-left: none;
|
|
||||||
padding-right: 5px;
|
|
||||||
padding-left: 5px;
|
|
||||||
border-width: 1px 1px 2px medium;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Settings) {
|
define('admin/general/sounds', ['sounds', 'settings', 'admin/settings'], function (Sounds, Settings, AdminSettings) {
|
||||||
var SoundsAdmin = {};
|
var SoundsAdmin = {};
|
||||||
|
|
||||||
SoundsAdmin.init = function () {
|
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) {
|
$('.sounds').find('button[data-action="play"]').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var fileName = $(this).parent().parent().find('select').val();
|
var soundName = $(this).parent().parent().find('select').val();
|
||||||
Sounds.playFile(fileName);
|
Sounds.playSound(soundName);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load Form Values
|
// Load Form Values
|
||||||
@@ -26,6 +26,8 @@ define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Setting
|
|||||||
app.alertSuccess('[[admin/general/sounds:saved]]');
|
app.alertSuccess('[[admin/general/sounds:saved]]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AdminSettings.prepare();
|
||||||
};
|
};
|
||||||
|
|
||||||
return SoundsAdmin;
|
return SoundsAdmin;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ define('admin/manage/group', [
|
|||||||
'admin/modules/colorpicker',
|
'admin/modules/colorpicker',
|
||||||
'translator',
|
'translator',
|
||||||
], function (memberList, iconSelect, colorpicker, translator) {
|
], function (memberList, iconSelect, colorpicker, translator) {
|
||||||
var Groups = {};
|
var Groups = {};
|
||||||
|
|
||||||
Groups.init = function () {
|
Groups.init = function () {
|
||||||
var groupDetailsSearch = $('#group-details-search');
|
var groupDetailsSearch = $('#group-details-search');
|
||||||
@@ -40,7 +40,9 @@ define('admin/manage/group', [
|
|||||||
var searchText = groupDetailsSearch.val();
|
var searchText = groupDetailsSearch.val();
|
||||||
var foundUser;
|
var foundUser;
|
||||||
|
|
||||||
socket.emit('admin.user.search', { query: searchText }, function (err, results) {
|
socket.emit('admin.user.search', {
|
||||||
|
query: searchText,
|
||||||
|
}, function (err, results) {
|
||||||
if (!err && results && results.users.length > 0) {
|
if (!err && results && results.users.length > 0) {
|
||||||
var numResults = results.users.length;
|
var numResults = results.users.length;
|
||||||
var x;
|
var x;
|
||||||
@@ -53,7 +55,8 @@ define('admin/manage/group', [
|
|||||||
for (x = 0; x < numResults; x += 1) {
|
for (x = 0; x < numResults; x += 1) {
|
||||||
foundUser = $('<li />');
|
foundUser = $('<li />');
|
||||||
foundUser
|
foundUser
|
||||||
.attr({ title: results.users[x].username,
|
.attr({
|
||||||
|
title: results.users[x].username,
|
||||||
'data-uid': results.users[x].uid,
|
'data-uid': results.users[x].uid,
|
||||||
'data-username': results.users[x].username,
|
'data-username': results.users[x].username,
|
||||||
'data-userslug': results.users[x].userslug,
|
'data-userslug': results.users[x].userslug,
|
||||||
@@ -96,7 +99,12 @@ define('admin/manage/group', [
|
|||||||
'icon:text': userLabel.attr('data-usericon-text'),
|
'icon:text': userLabel.attr('data-usericon-text'),
|
||||||
};
|
};
|
||||||
|
|
||||||
templates.parse('partials/groups/memberlist', 'members', { group: { isOwner: ajaxify.data.group.isOwner, members: [member] } }, function (html) {
|
templates.parse('admin/partials/groups/memberlist', 'members', {
|
||||||
|
group: {
|
||||||
|
isOwner: ajaxify.data.group.isOwner,
|
||||||
|
members: [member],
|
||||||
|
},
|
||||||
|
}, function (html) {
|
||||||
translator.translate(html, function (html) {
|
translator.translate(html, function (html) {
|
||||||
$('[component="groups/members"] tbody').prepend(html);
|
$('[component="groups/members"] tbody').prepend(html);
|
||||||
});
|
});
|
||||||
@@ -154,7 +162,7 @@ define('admin/manage/group', [
|
|||||||
groupLabelPreview.css('background-color', '#' + hex);
|
groupLabelPreview.css('background-color', '#' + hex);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.save').on('click', function () {
|
$('#save').on('click', function () {
|
||||||
socket.emit('admin.groups.update', {
|
socket.emit('admin.groups.update', {
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
values: {
|
values: {
|
||||||
|
|||||||
@@ -61,12 +61,16 @@ define('admin/manage/tags', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(function () {
|
timeoutId = setTimeout(function () {
|
||||||
socket.emit('topics.searchAndLoadTags', { query: $('#tag-search').val() }, function (err, result) {
|
socket.emit('topics.searchAndLoadTags', {
|
||||||
|
query: $('#tag-search').val(),
|
||||||
|
}, function (err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.parseAndTranslate('admin/manage/tags', 'tags', { tags: result.tags }, function (html) {
|
app.parseAndTranslate('admin/manage/tags', 'tags', {
|
||||||
|
tags: result.tags,
|
||||||
|
}, function (html) {
|
||||||
$('.tag-list').html(html);
|
$('.tag-list').html(html);
|
||||||
utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
|
utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
|
||||||
timeoutId = 0;
|
timeoutId = 0;
|
||||||
@@ -86,7 +90,7 @@ define('admin/manage/tags', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
var firstTag = $(tagsToModify[0]);
|
var firstTag = $(tagsToModify[0]);
|
||||||
var title = tagsToModify.length > 1 ? '[[admin/manage/tags:alerts.editing-multiple]]' : '[[admin/manage/tags:alerts.editing-x, ' + firstTag.find('.tag-item').text() + ']]';
|
var title = tagsToModify.length > 1 ? '[[admin/manage/tags:alerts.editing-multiple]]' : '[[admin/manage/tags:alerts.editing-x, ' + firstTag.find('.tag-item').attr('data-tag') + ']]';
|
||||||
|
|
||||||
var modal = bootbox.dialog({
|
var modal = bootbox.dialog({
|
||||||
title: title,
|
title: title,
|
||||||
@@ -133,7 +137,9 @@ define('admin/manage/tags', [
|
|||||||
tagsToDelete.each(function (index, el) {
|
tagsToDelete.each(function (index, el) {
|
||||||
tags.push($(el).attr('data-tag'));
|
tags.push($(el).attr('data-tag'));
|
||||||
});
|
});
|
||||||
socket.emit('admin.tags.deleteTags', { tags: tags }, function (err) {
|
socket.emit('admin.tags.deleteTags', {
|
||||||
|
tags: tags,
|
||||||
|
}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
|
|||||||
|
|
||||||
function handleImageChange() {
|
function handleImageChange() {
|
||||||
$('#changePictureBtn').on('click', function () {
|
$('#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) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
@@ -212,10 +214,13 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
|
|||||||
|
|
||||||
pictureCropper.show({
|
pictureCropper.show({
|
||||||
socketMethod: 'user.uploadCroppedPicture',
|
socketMethod: 'user.uploadCroppedPicture',
|
||||||
aspectRatio: '1 / 1',
|
aspectRatio: 1 / 1,
|
||||||
paramName: 'uid',
|
paramName: 'uid',
|
||||||
paramValue: ajaxify.data.theirid,
|
paramValue: ajaxify.data.theirid,
|
||||||
fileSize: ajaxify.data.maximumProfileImageSize,
|
fileSize: ajaxify.data.maximumProfileImageSize,
|
||||||
|
allowSkippingCrop: false,
|
||||||
|
restrictImageDimension: true,
|
||||||
|
imageDimension: ajaxify.data.profileImageDimension,
|
||||||
title: '[[user:upload_picture]]',
|
title: '[[user:upload_picture]]',
|
||||||
description: '[[user:upload_a_picture]]',
|
description: '[[user:upload_a_picture]]',
|
||||||
accept: '.png,.jpg,.bmp',
|
accept: '.png,.jpg,.bmp',
|
||||||
@@ -245,6 +250,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
|
|||||||
url: url,
|
url: url,
|
||||||
socketMethod: 'user.uploadCroppedPicture',
|
socketMethod: 'user.uploadCroppedPicture',
|
||||||
aspectRatio: '1 / 1',
|
aspectRatio: '1 / 1',
|
||||||
|
allowSkippingCrop: false,
|
||||||
|
restrictImageDimension: true,
|
||||||
|
imageDimension: ajaxify.data.profileImageDimension,
|
||||||
paramName: 'uid',
|
paramName: 'uid',
|
||||||
paramValue: ajaxify.data.theirid,
|
paramValue: ajaxify.data.theirid,
|
||||||
}, onUploadComplete);
|
}, onUploadComplete);
|
||||||
@@ -258,7 +266,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
|
|||||||
});
|
});
|
||||||
|
|
||||||
modal.find('[data-action="remove-uploaded"]').on('click', function () {
|
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');
|
modal.modal('hide');
|
||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ define('forum/account/header', [
|
|||||||
pictureCropper.show({
|
pictureCropper.show({
|
||||||
title: '[[user:upload_cover_picture]]',
|
title: '[[user:upload_cover_picture]]',
|
||||||
socketMethod: 'user.updateCover',
|
socketMethod: 'user.updateCover',
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: NaN,
|
||||||
|
allowSkippingCrop: true,
|
||||||
|
restrictImageDimension: false,
|
||||||
paramName: 'uid',
|
paramName: 'uid',
|
||||||
paramValue: ajaxify.data.theirid,
|
paramValue: ajaxify.data.theirid,
|
||||||
accept: '.png,.jpg,.bmp',
|
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;
|
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) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds'
|
|||||||
$('.account').find('button[data-action="play"]').on('click', function (e) {
|
$('.account').find('button[data-action="play"]').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var fileName = $(this).parent().parent().find('select').val();
|
var soundName = $(this).parent().parent().find('select').val();
|
||||||
sounds.playFile(fileName);
|
sounds.playSound(soundName);
|
||||||
});
|
});
|
||||||
|
|
||||||
toggleCustomRoute();
|
toggleCustomRoute();
|
||||||
@@ -88,7 +88,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)) {
|
if (requireReload && parseInt(app.user.uid, 10) === parseInt(ajaxify.data.theirid, 10)) {
|
||||||
app.alert({
|
app.alert({
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ define('forum/groups/details', [
|
|||||||
pictureCropper.show({
|
pictureCropper.show({
|
||||||
title: '[[groups:upload-group-cover]]',
|
title: '[[groups:upload-group-cover]]',
|
||||||
socketMethod: 'groups.cover.update',
|
socketMethod: 'groups.cover.update',
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: NaN,
|
||||||
|
allowSkippingCrop: true,
|
||||||
|
restrictImageDimension: false,
|
||||||
paramName: 'groupName',
|
paramName: 'groupName',
|
||||||
paramValue: groupName,
|
paramValue: groupName,
|
||||||
}, function (imageUrlOnServer) {
|
}, function (imageUrlOnServer) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ define('chat', [
|
|||||||
|
|
||||||
if (!isSelf && (!modal.is(':visible') || !app.isFocused)) {
|
if (!isSelf && (!modal.is(':visible') || !app.isFocused)) {
|
||||||
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]');
|
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'), {
|
taskbar.push('chat', modal.attr('UUID'), {
|
||||||
title: username,
|
title: username,
|
||||||
@@ -88,7 +88,7 @@ define('chat', [
|
|||||||
module.toggleNew(modal.attr('UUID'), !isSelf, true);
|
module.toggleNew(modal.attr('UUID'), !isSelf, true);
|
||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]');
|
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]');
|
||||||
sounds.play('chat-incoming');
|
sounds.play('chat-incoming', 'chat.incoming:' + data.message.mid);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
define('notifications', ['sounds', 'translator', 'components'], function (sound, translator, components) {
|
define('notifications', ['sounds', 'translator', 'components'], function (sounds, translator, components) {
|
||||||
var Notifications = {};
|
var Notifications = {};
|
||||||
|
|
||||||
var unreadNotifs = {};
|
var unreadNotifs = {};
|
||||||
@@ -104,7 +104,7 @@ define('notifications', ['sounds', 'translator', 'components'], function (sound,
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!unreadNotifs[notifData.nid]) {
|
if (!unreadNotifs[notifData.nid]) {
|
||||||
sound.play('notification');
|
sounds.play('notification', notifData.nid);
|
||||||
unreadNotifs[notifData.nid] = true;
|
unreadNotifs[notifData.nid] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,30 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe
|
|||||||
var cropperTool = new cropper.default(img, {
|
var cropperTool = new cropper.default(img, {
|
||||||
aspectRatio: data.aspectRatio,
|
aspectRatio: data.aspectRatio,
|
||||||
viewMode: 1,
|
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 () {
|
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 () {
|
cropperModal.find('.rotate').on('click', function () {
|
||||||
var degrees = this.getAttribute('data-degrees');
|
var degrees = this.getAttribute('data-degrees');
|
||||||
cropperTool.rotate(degrees);
|
cropperTool.rotate(degrees);
|
||||||
@@ -135,6 +158,9 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe
|
|||||||
imageType: imageType,
|
imageType: imageType,
|
||||||
socketMethod: data.socketMethod,
|
socketMethod: data.socketMethod,
|
||||||
aspectRatio: data.aspectRatio,
|
aspectRatio: data.aspectRatio,
|
||||||
|
allowSkippingCrop: data.allowSkippingCrop,
|
||||||
|
restrictImageDimension: data.restrictImageDimension,
|
||||||
|
imageDimension: data.imageDimension,
|
||||||
paramName: data.paramName,
|
paramName: data.paramName,
|
||||||
paramValue: data.paramValue,
|
paramValue: data.paramValue,
|
||||||
}, callback);
|
}, callback);
|
||||||
|
|||||||
@@ -1,90 +1,95 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
define('sounds', ['buzz'], function (buzz) {
|
define('sounds', function () {
|
||||||
var Sounds = {};
|
var Sounds = {};
|
||||||
|
|
||||||
var loadedSounds = {};
|
var fileMap;
|
||||||
var eventSoundMapping;
|
var soundMap;
|
||||||
var files;
|
var cache = {};
|
||||||
|
|
||||||
socket.on('event:sounds.reloadMapping', function () {
|
Sounds.loadMap = function loadMap(callback) {
|
||||||
Sounds.reloadMapping();
|
socket.emit('modules.sounds.getUserSoundMap', function (err, map) {
|
||||||
});
|
|
||||||
|
|
||||||
Sounds.reloadMapping = function () {
|
|
||||||
socket.emit('modules.sounds.getMapping', function (err, mapping) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
eventSoundMapping = mapping;
|
soundMap = map;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadData(callback) {
|
function loadData(callback) {
|
||||||
socket.emit('modules.sounds.getData', function (err, data) {
|
var outstanding = 2;
|
||||||
if (err) {
|
function after() {
|
||||||
return app.alertError('[sounds] Could not load sound mapping!');
|
outstanding -= 1;
|
||||||
}
|
if (outstanding === 0 && callback) {
|
||||||
eventSoundMapping = data.mapping;
|
callback();
|
||||||
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]);
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSoundLoaded(fileName)) {
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!files || !files[fileName]) {
|
|
||||||
return loadData(createSound);
|
|
||||||
}
|
|
||||||
createSound();
|
|
||||||
}
|
|
||||||
|
|
||||||
Sounds.play = function (name) {
|
|
||||||
function play() {
|
|
||||||
Sounds.playFile(eventSoundMapping[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eventSoundMapping) {
|
|
||||||
return loadData(play);
|
|
||||||
}
|
|
||||||
|
|
||||||
play();
|
|
||||||
};
|
|
||||||
|
|
||||||
Sounds.playFile = function (fileName) {
|
|
||||||
if (!fileName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function play() {
|
|
||||||
if (loadedSounds[fileName]) {
|
|
||||||
loadedSounds[fileName].play();
|
|
||||||
} else {
|
|
||||||
app.alertError('[sounds] Not found: ' + fileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (fileMap) {
|
||||||
if (isSoundLoaded(fileName)) {
|
outstanding -= 1;
|
||||||
play();
|
|
||||||
} else {
|
} else {
|
||||||
loadFile(fileName, play);
|
$.getJSON(config.relative_path + '/assets/sounds/fileMap.json', function (map) {
|
||||||
|
fileMap = map;
|
||||||
|
after();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sounds.loadMap(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sounds.playSound = function playSound(soundName) {
|
||||||
|
if (!soundMap || !fileMap) {
|
||||||
|
return loadData(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
function after() {
|
||||||
|
if (!fileMap[soundName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var audio = cache[soundName] || new Audio(config.relative_path + '/assets/sounds/' + fileMap[soundName]);
|
||||||
|
cache[soundName] = audio;
|
||||||
|
audio.pause();
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
after();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (!soundMap || !fileMap) {
|
||||||
|
return loadData(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
after();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('event:sounds.reloadMapping', function () {
|
||||||
|
Sounds.loadMap();
|
||||||
|
});
|
||||||
|
|
||||||
return Sounds;
|
return Sounds;
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
public/vendor/mdl/mdl.min.css
vendored
1
public/vendor/mdl/mdl.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -27,6 +27,7 @@ editController.get = function (req, res, callback) {
|
|||||||
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
|
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
|
||||||
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
|
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
|
||||||
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
|
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
|
||||||
|
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
|
||||||
|
|
||||||
userData.groups = userData.groups.filter(function (group) {
|
userData.groups = userData.groups.filter(function (group) {
|
||||||
return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
|
return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
|
||||||
@@ -36,7 +37,15 @@ editController.get = function (req, res, callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
userData.title = '[[pages:account/edit, ' + userData.username + ']]';
|
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 = [];
|
userData.editButtons = [];
|
||||||
|
|
||||||
plugins.fireHook('filter:user.account.edit', userData, function (err, userData) {
|
plugins.fireHook('filter:user.account.edit', userData, function (err, userData) {
|
||||||
@@ -76,9 +85,17 @@ function renderRoute(name, req, res, next) {
|
|||||||
|
|
||||||
userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]';
|
userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]';
|
||||||
userData.breadcrumbs = helpers.buildBreadcrumbs([
|
userData.breadcrumbs = helpers.buildBreadcrumbs([
|
||||||
{ text: userData.username, url: '/user/' + userData.userslug },
|
{
|
||||||
{ text: '[[user:edit]]', url: '/user/' + userData.userslug + '/edit' },
|
text: userData.username,
|
||||||
{ text: '[[user:' + name + ']]' },
|
url: '/user/' + userData.userslug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '[[user:edit]]',
|
||||||
|
url: '/user/' + userData.userslug + '/edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '[[user:' + name + ']]',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.render('account/edit/' + name, userData);
|
res.render('account/edit/' + name, userData);
|
||||||
@@ -139,7 +156,10 @@ editController.uploadPicture = function (req, res, next) {
|
|||||||
return next(err);
|
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 +174,9 @@ editController.uploadCoverPicture = function (req, res, next) {
|
|||||||
return next(err);
|
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) {
|
homePageRoutes: function (next) {
|
||||||
getHomePageRoutes(next);
|
getHomePageRoutes(next);
|
||||||
},
|
},
|
||||||
sounds: function (next) {
|
|
||||||
meta.sounds.getFiles(next);
|
|
||||||
},
|
|
||||||
soundsMapping: function (next) {
|
soundsMapping: function (next) {
|
||||||
meta.sounds.getMapping(userData.uid, next);
|
meta.sounds.getUserSoundMap(userData.uid, next);
|
||||||
},
|
},
|
||||||
}, next);
|
}, next);
|
||||||
},
|
},
|
||||||
@@ -50,16 +47,44 @@ settingsController.get = function (req, res, callback) {
|
|||||||
userData.languages = results.languages;
|
userData.languages = results.languages;
|
||||||
userData.homePageRoutes = results.homePageRoutes;
|
userData.homePageRoutes = results.homePageRoutes;
|
||||||
|
|
||||||
var soundSettings = {
|
var types = [
|
||||||
notificationSound: 'notification',
|
'notification',
|
||||||
incomingChatSound: 'chat-incoming',
|
'chat-incoming',
|
||||||
outgoingChatSound: 'chat-outgoing',
|
'chat-outgoing',
|
||||||
|
];
|
||||||
|
var aliases = {
|
||||||
|
notification: 'notificationSound',
|
||||||
|
'chat-incoming': 'incomingChatSound',
|
||||||
|
'chat-outgoing': 'outgoingChatSound',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(soundSettings).forEach(function (setting) {
|
types.forEach(function (type) {
|
||||||
userData[setting] = Object.keys(results.sounds).map(function (name) {
|
var soundpacks = plugins.soundpacks.map(function (pack) {
|
||||||
return { name: name, selected: name === results.soundsMapping[soundSettings[setting]] };
|
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);
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ dashboardController.get = function (req, res, next) {
|
|||||||
link: '/admin/extend/plugins',
|
link: '/admin/extend/plugins',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (global.env !== 'production') {
|
||||||
|
notices.push({
|
||||||
|
done: false,
|
||||||
|
notDoneText: '[[admin/general/dashboard:running-in-development]]',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
plugins.fireHook('filter:admin.notices', notices, next);
|
plugins.fireHook('filter:admin.notices', notices, next);
|
||||||
},
|
},
|
||||||
}, function (err, results) {
|
}, function (err, results) {
|
||||||
|
|||||||
@@ -1,24 +1,46 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var meta = require('../../meta');
|
var plugins = require('../../plugins');
|
||||||
|
var db = require('../../database');
|
||||||
|
|
||||||
var soundsController = {};
|
var soundsController = {};
|
||||||
|
|
||||||
soundsController.get = function (req, res, next) {
|
soundsController.get = function (req, res, next) {
|
||||||
meta.sounds.getFiles(function (err, sounds) {
|
db.getObject('settings:sounds', function (err, settings) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
sounds = Object.keys(sounds).map(function (name) {
|
settings = settings || {};
|
||||||
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],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: pack.name,
|
||||||
|
sounds: sounds,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
output[type + '-sound'] = soundpacks;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.render('admin/general/sounds', {
|
res.render('admin/general/sounds', output);
|
||||||
sounds: sounds,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ var path = require('path');
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
var nconf = require('nconf');
|
var nconf = require('nconf');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
|
|
||||||
|
var meta = require('../../meta');
|
||||||
var file = require('../../file');
|
var file = require('../../file');
|
||||||
var image = require('../../image');
|
var image = require('../../image');
|
||||||
var plugins = require('../../plugins');
|
var plugins = require('../../plugins');
|
||||||
@@ -105,12 +107,7 @@ uploadsController.uploadSound = function (req, res, next) {
|
|||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var soundsPath = path.join(__dirname, '../../../build/public/sounds');
|
meta.sounds.build(function (err) {
|
||||||
var 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) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var nconf = require('nconf');
|
|
||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var crypto = require('crypto');
|
|
||||||
var Jimp = require('jimp');
|
var Jimp = require('jimp');
|
||||||
var mime = require('mime');
|
var mime = require('mime');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
|
|
||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
|
var image = require('../image');
|
||||||
var uploadsController = require('../controllers/uploads');
|
var uploadsController = require('../controllers/uploads');
|
||||||
|
|
||||||
module.exports = function (Groups) {
|
module.exports = function (Groups) {
|
||||||
@@ -35,7 +33,7 @@ module.exports = function (Groups) {
|
|||||||
if (tempPath) {
|
if (tempPath) {
|
||||||
return next(null, tempPath);
|
return next(null, tempPath);
|
||||||
}
|
}
|
||||||
writeImageDataToFile(data.imageData, next);
|
image.writeImageDataToTempFile(data.imageData, next);
|
||||||
},
|
},
|
||||||
function (_tempPath, next) {
|
function (_tempPath, next) {
|
||||||
tempPath = _tempPath;
|
tempPath = _tempPath;
|
||||||
@@ -95,24 +93,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) {
|
Groups.removeCover = function (data, callback) {
|
||||||
db.deleteObjectFields('group:' + data.groupName, ['cover:url', 'cover:thumb:url', 'cover:position'], callback);
|
db.deleteObjectFields('group:' + data.groupName, ['cover:url', 'cover:thumb:url', 'cover:position'], callback);
|
||||||
};
|
};
|
||||||
|
|||||||
37
src/image.js
37
src/image.js
@@ -1,8 +1,13 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var os = require('os');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
var Jimp = require('jimp');
|
var Jimp = require('jimp');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
var file = require('./file');
|
||||||
var plugins = require('./plugins');
|
var plugins = require('./plugins');
|
||||||
|
|
||||||
var image = module.exports;
|
var image = module.exports;
|
||||||
@@ -65,9 +70,6 @@ image.resizeImage = function (data, callback) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (image, next) {
|
function (image, next) {
|
||||||
if (data.write === false) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
image.write(data.target || data.path, next);
|
image.write(data.target || data.path, next);
|
||||||
},
|
},
|
||||||
], function (err) {
|
], function (err) {
|
||||||
@@ -83,7 +85,7 @@ image.normalise = function (path, extension, callback) {
|
|||||||
path: path,
|
path: path,
|
||||||
extension: extension,
|
extension: extension,
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
callback(err);
|
callback(err, path + '.png');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
new Jimp(path, function (err, image) {
|
new Jimp(path, function (err, image) {
|
||||||
@@ -91,7 +93,7 @@ image.normalise = function (path, extension, callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
image.write(path + '.png', function (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);
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,72 +13,76 @@ var sockets = require('../socket.io');
|
|||||||
module.exports = function (Messaging) {
|
module.exports = function (Messaging) {
|
||||||
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
|
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
|
||||||
|
|
||||||
|
Messaging.notificationSendDelay = 1000 * 60;
|
||||||
|
|
||||||
Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
|
Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
|
||||||
Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return;
|
Messaging.getUidsInRoom(roomId, 0, -1, next);
|
||||||
}
|
},
|
||||||
|
function (uids, next) {
|
||||||
var data = {
|
var data = {
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
fromUid: fromUid,
|
fromUid: fromUid,
|
||||||
message: messageObj,
|
|
||||||
};
|
|
||||||
uids.forEach(function (uid) {
|
|
||||||
data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0;
|
|
||||||
Messaging.pushUnreadCount(uid);
|
|
||||||
sockets.in('uid_' + uid).emit('event:chats.receive', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delayed notifications
|
|
||||||
var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId];
|
|
||||||
if (queueObj) {
|
|
||||||
queueObj.message.content += '\n' + messageObj.content;
|
|
||||||
clearTimeout(queueObj.timeout);
|
|
||||||
} else {
|
|
||||||
queueObj = {
|
|
||||||
message: messageObj,
|
message: messageObj,
|
||||||
};
|
};
|
||||||
Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueObj.timeout = setTimeout(function () {
|
uids.forEach(function (uid) {
|
||||||
sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) {
|
data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0;
|
||||||
if (!err) {
|
Messaging.pushUnreadCount(uid);
|
||||||
delete Messaging.notifyQueue[fromUid + ':' + roomId];
|
sockets.in('uid_' + uid).emit('event:chats.receive', data);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, 1000 * 60); // wait 60s before sending
|
|
||||||
});
|
// Delayed notifications
|
||||||
|
var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId];
|
||||||
|
if (queueObj) {
|
||||||
|
queueObj.message.content += '\n' + messageObj.content;
|
||||||
|
clearTimeout(queueObj.timeout);
|
||||||
|
} else {
|
||||||
|
queueObj = {
|
||||||
|
message: messageObj,
|
||||||
|
};
|
||||||
|
Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueObj.timeout = setTimeout(function () {
|
||||||
|
sendNotifications(fromUid, uids, roomId, queueObj.message);
|
||||||
|
}, Messaging.notificationSendDelay);
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendNotifications(fromuid, uids, roomId, messageObj, callback) {
|
function sendNotifications(fromuid, uids, roomId, messageObj) {
|
||||||
user.isOnline(uids, function (err, isOnline) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
user.isOnline(uids, next);
|
||||||
}
|
},
|
||||||
|
function (isOnline, next) {
|
||||||
|
uids = uids.filter(function (uid, index) {
|
||||||
|
return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
|
||||||
|
});
|
||||||
|
|
||||||
uids = uids.filter(function (uid, index) {
|
if (!uids.length) {
|
||||||
return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
if (!uids.length) {
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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,39 @@ module.exports = function (Messaging) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async.parallel({
|
async.waterfall([
|
||||||
userData: function (next) {
|
function (next) {
|
||||||
user.getUsersFields(uids, ['uid', 'username', 'userslug'], 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) {
|
if (err) {
|
||||||
return winston.error(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,16 +3,19 @@
|
|||||||
var ip = require('ip');
|
var ip = require('ip');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
|
var pubsub = require('../pubsub');
|
||||||
|
|
||||||
var Blacklist = {
|
var Blacklist = {
|
||||||
_rules: [],
|
_rules: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
Blacklist.load = function (callback) {
|
Blacklist.load = function (callback) {
|
||||||
|
callback = callback || function () {};
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
async.apply(db.get, 'ip-blacklist-rules'),
|
Blacklist.get,
|
||||||
async.apply(Blacklist.validate),
|
Blacklist.validate,
|
||||||
], function (err, rules) {
|
], function (err, rules) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
@@ -33,13 +36,18 @@ Blacklist.load = function (callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pubsub.on('blacklist:reload', Blacklist.load);
|
||||||
|
|
||||||
Blacklist.save = function (rules, callback) {
|
Blacklist.save = function (rules, callback) {
|
||||||
db.set('ip-blacklist-rules', rules, function (err) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
db.set('ip-blacklist-rules', rules, next);
|
||||||
}
|
},
|
||||||
Blacklist.load(callback);
|
function (next) {
|
||||||
});
|
Blacklist.load(next);
|
||||||
|
pubsub.publish('blacklist:reload');
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
Blacklist.get = function (callback) {
|
Blacklist.get = function (callback) {
|
||||||
@@ -48,14 +56,14 @@ Blacklist.get = function (callback) {
|
|||||||
|
|
||||||
Blacklist.test = function (clientIp, callback) {
|
Blacklist.test = function (clientIp, callback) {
|
||||||
if (
|
if (
|
||||||
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
|
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.ipv6.indexOf(clientIp) === -1 &&// not explicitly specified in ipv6 list
|
||||||
&& !Blacklist._rules.cidr.some(function (subnet) {
|
!Blacklist._rules.cidr.some(function (subnet) {
|
||||||
return ip.cidrSubnet(subnet).contains(clientIp);
|
return ip.cidrSubnet(subnet).contains(clientIp);
|
||||||
}) // not in a blacklisted cidr range
|
}) // not in a blacklisted cidr range
|
||||||
) {
|
) {
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback();
|
setImmediate(callback);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -64,7 +72,7 @@ Blacklist.test = function (clientIp, callback) {
|
|||||||
err.code = 'blacklisted-ip';
|
err.code = 'blacklisted-ip';
|
||||||
|
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback(err);
|
setImmediate(callback, err);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ var winston = require('winston');
|
|||||||
|
|
||||||
var buildStart;
|
var buildStart;
|
||||||
|
|
||||||
var valid = ['js', 'clientCSS', 'acpCSS', 'tpl', 'lang'];
|
var valid = ['js', 'clientCSS', 'acpCSS', 'tpl', 'lang', 'sound'];
|
||||||
|
|
||||||
exports.buildAll = function (callback) {
|
exports.buildAll = function (callback) {
|
||||||
exports.build(valid.join(','), callback);
|
exports.build(valid.join(','), callback);
|
||||||
@@ -46,32 +46,32 @@ exports.buildTargets = function (targets, callback) {
|
|||||||
var cacheBuster = require('./cacheBuster');
|
var cacheBuster = require('./cacheBuster');
|
||||||
var meta = require('../meta');
|
var meta = require('../meta');
|
||||||
var numCpus = require('os').cpus().length;
|
var numCpus = require('os').cpus().length;
|
||||||
var strategy = (targets.length > 1 && numCpus > 1);
|
var parallel = targets.length > 1 && numCpus > 1;
|
||||||
|
|
||||||
buildStart = buildStart || Date.now();
|
buildStart = buildStart || Date.now();
|
||||||
|
|
||||||
var step = function (startTime, target, next, err) {
|
var step = function (startTime, target, next, err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error('Build failed: ' + err.message);
|
winston.error('Build failed: ' + err.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's');
|
winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's');
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (strategy) {
|
if (parallel) {
|
||||||
winston.verbose('[build] Utilising multiple cores/processes');
|
winston.verbose('[build] Utilising multiple cores/processes');
|
||||||
} else {
|
} else {
|
||||||
winston.verbose('[build] Utilising single-core');
|
winston.verbose('[build] Utilising single-core');
|
||||||
}
|
}
|
||||||
|
|
||||||
async[strategy ? 'parallel' : 'series']([
|
async[parallel ? 'parallel' : 'series']([
|
||||||
function (next) {
|
function (next) {
|
||||||
if (targets.indexOf('js') !== -1) {
|
if (targets.indexOf('js') !== -1) {
|
||||||
winston.info('[build] Building javascript');
|
winston.info('[build] Building javascript');
|
||||||
var startTime = Date.now();
|
var startTime = Date.now();
|
||||||
async.series([
|
async.series([
|
||||||
meta.js.linkModules,
|
meta.js.buildModules,
|
||||||
meta.js.linkStatics,
|
meta.js.linkStatics,
|
||||||
async.apply(meta.js.minify, 'nodebb.min.js'),
|
async.apply(meta.js.minify, 'nodebb.min.js'),
|
||||||
async.apply(meta.js.minify, 'acp.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));
|
meta.languages.build(step.bind(this, startTime, target, next));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'sound':
|
||||||
|
winston.info('[build] Linking sound files');
|
||||||
|
startTime = Date.now();
|
||||||
|
meta.sounds.build(step.bind(this, startTime, target, next));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
winston.warn('[build] Unknown build target: \'' + target + '\'');
|
winston.warn('[build] Unknown build target: \'' + target + '\'');
|
||||||
setImmediate(next);
|
setImmediate(next);
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ module.exports = function (Meta) {
|
|||||||
'@import "../../public/less/generics.less";',
|
'@import "../../public/less/generics.less";',
|
||||||
'@import "../../public/less/mixins.less";',
|
'@import "../../public/less/mixins.less";',
|
||||||
'@import "../../public/less/global.less";',
|
'@import "../../public/less/global.less";',
|
||||||
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
|
].map(function (str) {
|
||||||
|
return str.replace(/\//g, path.sep);
|
||||||
|
}).join('\n');
|
||||||
},
|
},
|
||||||
admin: function (source) {
|
admin: function (source) {
|
||||||
return source + '\n' + [
|
return source + '\n' + [
|
||||||
@@ -41,7 +43,10 @@ module.exports = function (Meta) {
|
|||||||
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
|
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
|
||||||
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
|
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
|
||||||
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
|
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
|
||||||
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
|
'@import (inline) "../public/vendor/mdl/material.css";',
|
||||||
|
].map(function (str) {
|
||||||
|
return str.replace(/\//g, path.sep);
|
||||||
|
}).join('\n');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
136
src/meta/js.js
136
src/meta/js.js
@@ -7,11 +7,14 @@ var async = require('async');
|
|||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var rimraf = require('rimraf');
|
var rimraf = require('rimraf');
|
||||||
|
var uglifyjs = require('uglify-js');
|
||||||
|
|
||||||
var file = require('../file');
|
var file = require('../file');
|
||||||
var plugins = require('../plugins');
|
var plugins = require('../plugins');
|
||||||
var utils = require('../../public/src/utils');
|
var utils = require('../../public/src/utils');
|
||||||
|
|
||||||
|
var minifierPath = path.join(__dirname, 'minifier.js');
|
||||||
|
|
||||||
module.exports = function (Meta) {
|
module.exports = function (Meta) {
|
||||||
Meta.js = {
|
Meta.js = {
|
||||||
target: {},
|
target: {},
|
||||||
@@ -78,7 +81,7 @@ module.exports = function (Meta) {
|
|||||||
'public/src/modules/string.js',
|
'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: {
|
modules: {
|
||||||
'Chart.js': './node_modules/chart.js/dist/Chart.min.js',
|
'Chart.js': './node_modules/chart.js/dist/Chart.min.js',
|
||||||
'mousetrap.js': './node_modules/mousetrap/mousetrap.min.js',
|
'mousetrap.js': './node_modules/mousetrap/mousetrap.min.js',
|
||||||
@@ -89,24 +92,123 @@ module.exports = function (Meta) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Meta.js.linkModules = function (callback) {
|
function minifyModules(modules, callback) {
|
||||||
rimraf(path.join(__dirname, '../../build/public/src/modules'), function (err) {
|
async.eachLimit(modules, 500, function (mod, next) {
|
||||||
if (err) {
|
var filePath = mod.filePath;
|
||||||
return callback(err);
|
var destPath = mod.destPath;
|
||||||
}
|
var minified;
|
||||||
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) {
|
async.parallel([
|
||||||
if (err) {
|
function (cb) {
|
||||||
return next(err);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
file.link(filePath, destPath, next);
|
cb();
|
||||||
});
|
});
|
||||||
}, callback);
|
},
|
||||||
|
], function (err) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile(destPath, minified.code, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mods = files.filter(function (filePath) {
|
||||||
|
return path.extname(filePath) === '.js';
|
||||||
|
}).map(function (filePath) {
|
||||||
|
return {
|
||||||
|
filePath: filePath,
|
||||||
|
destPath: path.join(__dirname, '../../build/public/src', path.relative(path.dirname(dir), filePath)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
modules = modules.concat(mods);
|
||||||
|
|
||||||
|
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) {
|
Meta.js.linkStatics = function (callback) {
|
||||||
@@ -133,7 +235,7 @@ module.exports = function (Meta) {
|
|||||||
winston.verbose('[meta/js] Minifying ' + target);
|
winston.verbose('[meta/js] Minifying ' + target);
|
||||||
|
|
||||||
var forkProcessParams = setupDebugging();
|
var forkProcessParams = setupDebugging();
|
||||||
var minifier = fork('minifier.js', [], forkProcessParams);
|
var minifier = fork(minifierPath, [], forkProcessParams);
|
||||||
Meta.js.minifierProc = minifier;
|
Meta.js.minifierProc = minifier;
|
||||||
|
|
||||||
Meta.js.target[target] = {};
|
Meta.js.target[target] = {};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
var uglifyjs = require('uglify-js');
|
var uglifyjs = require('uglify-js');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var file = require('./src/file');
|
var file = require('../file');
|
||||||
|
|
||||||
var Minifier = {
|
var Minifier = {
|
||||||
js: {},
|
js: {},
|
||||||
@@ -2,68 +2,102 @@
|
|||||||
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var nconf = require('nconf');
|
|
||||||
var winston = require('winston');
|
|
||||||
var rimraf = require('rimraf');
|
var rimraf = require('rimraf');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
var file = require('../file');
|
var file = require('../file');
|
||||||
var plugins = require('../plugins');
|
var plugins = require('../plugins');
|
||||||
|
var user = require('../user');
|
||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
|
|
||||||
|
|
||||||
|
var soundsPath = path.join(__dirname, '../../build/public/sounds');
|
||||||
|
var uploadsPath = path.join(__dirname, '../../public/uploads/sounds');
|
||||||
|
|
||||||
module.exports = function (Meta) {
|
module.exports = function (Meta) {
|
||||||
Meta.sounds = {};
|
Meta.sounds = {};
|
||||||
|
|
||||||
Meta.sounds.init = function (callback) {
|
Meta.sounds.addUploads = function addUploads(callback) {
|
||||||
if (nconf.get('isPrimary') === 'true') {
|
fs.readdir(uploadsPath, function (err, files) {
|
||||||
setupSounds(callback);
|
|
||||||
} else if (typeof callback === 'function') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
], function (err, files) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error('Could not get local sound files:' + err.message);
|
if (err.code !== 'ENOENT') {
|
||||||
console.log(err.stack);
|
return callback(err);
|
||||||
return callback(null, []);
|
}
|
||||||
|
|
||||||
|
files = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var localList = {};
|
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);
|
||||||
|
|
||||||
// Filter out hidden files
|
prev[name] = fileName;
|
||||||
files = files.filter(function (filename) {
|
return prev;
|
||||||
return !filename.startsWith('.');
|
}, {});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Return proper paths
|
callback();
|
||||||
files.forEach(function (filename) {
|
|
||||||
localList[filename] = nconf.get('relative_path') + '/assets/sounds/' + filename;
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(null, localList);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Meta.sounds.getMapping = function (uid, callback) {
|
Meta.sounds.build = function build(callback) {
|
||||||
var user = require('../user');
|
Meta.sounds.addUploads(function (err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
|
||||||
|
|
||||||
|
Meta.sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
|
||||||
async.parallel({
|
async.parallel({
|
||||||
defaultMapping: function (next) {
|
defaultMapping: function (next) {
|
||||||
db.getObject('settings:sounds', next);
|
db.getObject('settings:sounds', next);
|
||||||
@@ -75,82 +109,25 @@ module.exports = function (Meta) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userSettings = results.userSettings;
|
var userSettings = results.userSettings;
|
||||||
|
userSettings = {
|
||||||
|
notification: userSettings.notificationSound,
|
||||||
|
'chat-incoming': userSettings.incomingChatSound,
|
||||||
|
'chat-outgoing': userSettings.outgoingChatSound,
|
||||||
|
};
|
||||||
var defaultMapping = results.defaultMapping || {};
|
var defaultMapping = results.defaultMapping || {};
|
||||||
var soundMapping = {};
|
var soundMapping = {};
|
||||||
soundMapping.notification = (userSettings.notificationSound || userSettings.notificationSound === '') ?
|
|
||||||
userSettings.notificationSound : defaultMapping.notification || '';
|
|
||||||
|
|
||||||
soundMapping['chat-incoming'] = (userSettings.incomingChatSound || userSettings.incomingChatSound === '') ?
|
keys.forEach(function (key) {
|
||||||
userSettings.incomingChatSound : defaultMapping['chat-incoming'] || '';
|
if (userSettings[key] || userSettings[key] === '') {
|
||||||
|
soundMapping[key] = userSettings[key] || null;
|
||||||
soundMapping['chat-outgoing'] = (userSettings.outgoingChatSound || userSettings.outgoingChatSound === '') ?
|
} else {
|
||||||
userSettings.outgoingChatSound : defaultMapping['chat-outgoing'] || '';
|
soundMapping[key] = defaultMapping[key] || null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
callback(null, soundMapping);
|
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,114 +59,122 @@ module.exports = function (middleware) {
|
|||||||
|
|
||||||
templateValues.configJSON = JSON.stringify(res.locals.config);
|
templateValues.configJSON = JSON.stringify(res.locals.config);
|
||||||
|
|
||||||
async.parallel({
|
async.waterfall([
|
||||||
scripts: function (next) {
|
function (next) {
|
||||||
plugins.fireHook('filter:scripts.get', [], next);
|
async.parallel({
|
||||||
|
scripts: function (next) {
|
||||||
|
plugins.fireHook('filter:scripts.get', [], next);
|
||||||
|
},
|
||||||
|
isAdmin: function (next) {
|
||||||
|
user.isAdministrator(req.uid, next);
|
||||||
|
},
|
||||||
|
isGlobalMod: function (next) {
|
||||||
|
user.isGlobalModerator(req.uid, next);
|
||||||
|
},
|
||||||
|
isModerator: function (next) {
|
||||||
|
user.isModeratorOfAnyCategory(req.uid, next);
|
||||||
|
},
|
||||||
|
user: function (next) {
|
||||||
|
var userData = {
|
||||||
|
uid: 0,
|
||||||
|
username: '[[global:guest]]',
|
||||||
|
userslug: '',
|
||||||
|
email: '',
|
||||||
|
picture: meta.config.defaultAvatar,
|
||||||
|
status: 'offline',
|
||||||
|
reputation: 0,
|
||||||
|
'email:confirmed': false,
|
||||||
|
};
|
||||||
|
if (req.uid) {
|
||||||
|
user.getUserFields(req.uid, Object.keys(userData), next);
|
||||||
|
} else {
|
||||||
|
next(null, userData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmailConfirmSent: function (next) {
|
||||||
|
if (!meta.config.requireEmailConfirmation || !req.uid) {
|
||||||
|
return next(null, false);
|
||||||
|
}
|
||||||
|
db.get('uid:' + req.uid + ':confirm:email:sent', next);
|
||||||
|
},
|
||||||
|
navigation: async.apply(navigation.get),
|
||||||
|
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
|
||||||
|
banned: async.apply(user.isBanned, req.uid),
|
||||||
|
banReason: async.apply(user.getBannedReason, req.uid),
|
||||||
|
}, next);
|
||||||
},
|
},
|
||||||
isAdmin: function (next) {
|
function (results, next) {
|
||||||
user.isAdministrator(req.uid, next);
|
if (results.banned) {
|
||||||
},
|
req.logout();
|
||||||
isGlobalMod: function (next) {
|
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
|
||||||
user.isGlobalModerator(req.uid, next);
|
|
||||||
},
|
|
||||||
isModerator: function (next) {
|
|
||||||
user.isModeratorOfAnyCategory(req.uid, next);
|
|
||||||
},
|
|
||||||
user: function (next) {
|
|
||||||
var userData = {
|
|
||||||
uid: 0,
|
|
||||||
username: '[[global:guest]]',
|
|
||||||
userslug: '',
|
|
||||||
email: '',
|
|
||||||
picture: meta.config.defaultAvatar,
|
|
||||||
status: 'offline',
|
|
||||||
reputation: 0,
|
|
||||||
'email:confirmed': false,
|
|
||||||
};
|
|
||||||
if (req.uid) {
|
|
||||||
user.getUserFields(req.uid, Object.keys(userData), next);
|
|
||||||
} else {
|
|
||||||
next(null, userData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isEmailConfirmSent: function (next) {
|
|
||||||
if (!meta.config.requireEmailConfirmation || !req.uid) {
|
|
||||||
return next(null, false);
|
|
||||||
}
|
|
||||||
db.get('uid:' + req.uid + ':confirm:email:sent', next);
|
|
||||||
},
|
|
||||||
navigation: async.apply(navigation.get),
|
|
||||||
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
|
|
||||||
banned: async.apply(user.isBanned, req.uid),
|
|
||||||
banReason: async.apply(user.getBannedReason, req.uid),
|
|
||||||
}, function (err, results) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.banned) {
|
|
||||||
req.logout();
|
|
||||||
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
|
|
||||||
}
|
|
||||||
|
|
||||||
results.user.isAdmin = results.isAdmin;
|
|
||||||
results.user.isGlobalMod = results.isGlobalMod;
|
|
||||||
results.user.isMod = !!results.isModerator;
|
|
||||||
results.user.uid = parseInt(results.user.uid, 10);
|
|
||||||
results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
||||||
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
|
|
||||||
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
|
|
||||||
|
|
||||||
if (res.locals.config && parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') {
|
|
||||||
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
templateValues.browserTitle = controllers.helpers.buildTitle(data.title);
|
|
||||||
templateValues.navigation = results.navigation;
|
|
||||||
templateValues.metaTags = results.tags.meta;
|
|
||||||
templateValues.linkTags = results.tags.link;
|
|
||||||
templateValues.isAdmin = results.user.isAdmin;
|
|
||||||
templateValues.isGlobalMod = results.user.isGlobalMod;
|
|
||||||
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
|
|
||||||
templateValues.user = results.user;
|
|
||||||
templateValues.userJSON = JSON.stringify(results.user);
|
|
||||||
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
|
|
||||||
templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : '';
|
|
||||||
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
|
|
||||||
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
|
|
||||||
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
|
|
||||||
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
|
|
||||||
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
|
|
||||||
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;
|
|
||||||
|
|
||||||
templateValues.template = { name: res.locals.template };
|
|
||||||
templateValues.template[res.locals.template] = true;
|
|
||||||
|
|
||||||
templateValues.scripts = results.scripts.map(function (script) {
|
|
||||||
return { src: script };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.route && req.route.path === '/') {
|
|
||||||
modifyTitle(templateValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins.fireHook('filter:middleware.renderHeader', { templateValues: templateValues, req: req, res: res }, function (err, data) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.app.render('header', data.templateValues, callback);
|
results.user.isAdmin = results.isAdmin;
|
||||||
});
|
results.user.isGlobalMod = results.isGlobalMod;
|
||||||
});
|
results.user.isMod = !!results.isModerator;
|
||||||
|
results.user.uid = parseInt(results.user.uid, 10);
|
||||||
|
results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
|
||||||
|
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
|
||||||
|
|
||||||
|
if (res.locals.config && parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') {
|
||||||
|
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
templateValues.browserTitle = controllers.helpers.buildTitle(data.title);
|
||||||
|
templateValues.navigation = results.navigation;
|
||||||
|
templateValues.metaTags = results.tags.meta;
|
||||||
|
templateValues.linkTags = results.tags.link;
|
||||||
|
templateValues.isAdmin = results.user.isAdmin;
|
||||||
|
templateValues.isGlobalMod = results.user.isGlobalMod;
|
||||||
|
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
|
||||||
|
templateValues.user = results.user;
|
||||||
|
templateValues.userJSON = JSON.stringify(results.user);
|
||||||
|
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
|
||||||
|
templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : '';
|
||||||
|
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
|
||||||
|
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
|
||||||
|
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
|
||||||
|
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
|
||||||
|
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
|
||||||
|
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;
|
||||||
|
|
||||||
|
templateValues.template = { name: res.locals.template };
|
||||||
|
templateValues.template[res.locals.template] = true;
|
||||||
|
|
||||||
|
templateValues.scripts = results.scripts.map(function (script) {
|
||||||
|
return { src: script };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.route && req.route.path === '/') {
|
||||||
|
modifyTitle(templateValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.fireHook('filter:middleware.renderHeader', {
|
||||||
|
req: req,
|
||||||
|
res: res,
|
||||||
|
templateValues: templateValues,
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function (data, next) {
|
||||||
|
req.app.render('header', data.templateValues, next);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
middleware.renderFooter = function (req, res, data, callback) {
|
middleware.renderFooter = function (req, res, data, callback) {
|
||||||
plugins.fireHook('filter:middleware.renderFooter', { templateValues: data, req: req, res: res }, function (err, data) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
plugins.fireHook('filter:middleware.renderFooter', {
|
||||||
}
|
req: req,
|
||||||
req.app.render('footer', data.templateValues, callback);
|
res: res,
|
||||||
});
|
templateValues: data,
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function (data, next) {
|
||||||
|
req.app.render('footer', data.templateValues, next);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
function modifyTitle(obj) {
|
function modifyTitle(obj) {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var plugins = require('../plugins');
|
var plugins = require('../plugins');
|
||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
var translator = require('../../public/src/modules/translator');
|
var translator = require('../../public/src/modules/translator');
|
||||||
var pubsub = require('../pubsub');
|
var pubsub = require('../pubsub');
|
||||||
|
|
||||||
var admin = {};
|
var admin = module.exports;
|
||||||
admin.cache = null;
|
admin.cache = null;
|
||||||
|
|
||||||
pubsub.on('admin:navigation:save', function () {
|
pubsub.on('admin:navigation:save', function () {
|
||||||
@@ -70,5 +69,3 @@ function getAvailable(callback) {
|
|||||||
|
|
||||||
plugins.fireHook('filter:navigation.available', core, callback);
|
plugins.fireHook('filter:navigation.available', core, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = admin;
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var middleware;
|
|||||||
Plugins.libraryPaths = [];
|
Plugins.libraryPaths = [];
|
||||||
Plugins.versionWarning = [];
|
Plugins.versionWarning = [];
|
||||||
Plugins.languageCodes = [];
|
Plugins.languageCodes = [];
|
||||||
|
Plugins.soundpacks = [];
|
||||||
|
|
||||||
Plugins.initialized = false;
|
Plugins.initialized = false;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ module.exports = function (Plugins) {
|
|||||||
Plugins.lessFiles.length = 0;
|
Plugins.lessFiles.length = 0;
|
||||||
Plugins.clientScripts.length = 0;
|
Plugins.clientScripts.length = 0;
|
||||||
Plugins.acpScripts.length = 0;
|
Plugins.acpScripts.length = 0;
|
||||||
|
Plugins.soundpacks.length = 0;
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
async.apply(Plugins.getPluginPaths),
|
async.apply(Plugins.getPluginPaths),
|
||||||
@@ -57,6 +58,7 @@ module.exports = function (Plugins) {
|
|||||||
async.apply(mapClientSideScripts, pluginData),
|
async.apply(mapClientSideScripts, pluginData),
|
||||||
async.apply(mapClientModules, pluginData),
|
async.apply(mapClientModules, pluginData),
|
||||||
async.apply(mapStaticDirectories, pluginData, pluginData.path),
|
async.apply(mapStaticDirectories, pluginData, pluginData.path),
|
||||||
|
async.apply(mapSoundpack, pluginData),
|
||||||
], next);
|
], next);
|
||||||
}, next);
|
}, next);
|
||||||
},
|
},
|
||||||
@@ -93,6 +95,9 @@ module.exports = function (Plugins) {
|
|||||||
function (next) {
|
function (next) {
|
||||||
mapClientModules(pluginData, next);
|
mapClientModules(pluginData, next);
|
||||||
},
|
},
|
||||||
|
function (next) {
|
||||||
|
mapSoundpack(pluginData, next);
|
||||||
|
},
|
||||||
], function (err) {
|
], function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
|
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
|
||||||
@@ -251,6 +256,35 @@ module.exports = function (Plugins) {
|
|||||||
callback();
|
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) {
|
function resolveModulePath(fullPath, relPath) {
|
||||||
/**
|
/**
|
||||||
* With npm@3, dependencies can become flattened, and appear at the root level.
|
* With npm@3, dependencies can become flattened, and appear at the root level.
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ Sockets.init = function (server) {
|
|||||||
|
|
||||||
io.on('connection', onConnection);
|
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.origins(parsedUrl.protocol + '//' + domain + ':*');
|
||||||
|
winston.info('[socket.io] Restricting access to origin: ' + parsedUrl.protocol + '//' + domain + ':*');
|
||||||
|
} else {
|
||||||
|
io.origins(override);
|
||||||
|
winston.info('[socket.io] Restricting access to origin: ' + override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
io.listen(server, {
|
io.listen(server, {
|
||||||
transports: nconf.get('socket.io:transports'),
|
transports: nconf.get('socket.io:transports'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -342,20 +342,8 @@ SocketModules.chats.getMessages = function (socket, data, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Sounds */
|
/* Sounds */
|
||||||
SocketModules.sounds.getSounds = function (socket, data, callback) {
|
SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) {
|
||||||
// Read sounds from local directory
|
meta.sounds.getUserSoundMap(socket.uid, callback);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = SocketModules;
|
module.exports = SocketModules;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
var posts = require('../posts');
|
var posts = require('../posts');
|
||||||
var privileges = require('../privileges');
|
var privileges = require('../privileges');
|
||||||
@@ -31,25 +31,26 @@ SocketPosts.reply = function (socket, data, callback) {
|
|||||||
data.req = websockets.reqFromSocket(socket);
|
data.req = websockets.reqFromSocket(socket);
|
||||||
data.timestamp = Date.now();
|
data.timestamp = Date.now();
|
||||||
|
|
||||||
topics.reply(data, function (err, postData) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
var result = {
|
next(null, postData);
|
||||||
posts: [postData],
|
|
||||||
'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1,
|
|
||||||
'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
callback(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) {
|
SocketPosts.getRawPost = function (socket, pid, callback) {
|
||||||
@@ -120,7 +121,7 @@ SocketPosts.getPidIndex = function (socket, data, callback) {
|
|||||||
|
|
||||||
SocketPosts.getReplies = function (socket, pid, callback) {
|
SocketPosts.getReplies = function (socket, pid, callback) {
|
||||||
if (!utils.isNumber(pid)) {
|
if (!utils.isNumber(pid)) {
|
||||||
return callback(new Error('[[error:invalid-data]'));
|
return callback(new Error('[[error:invalid-data]]'));
|
||||||
}
|
}
|
||||||
var postPrivileges;
|
var postPrivileges;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|||||||
@@ -272,34 +272,34 @@ SocketUser.invite = function (socket, email, callback) {
|
|||||||
return callback(new Error('[[error:forum-not-invite-only]]'));
|
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([
|
async.waterfall([
|
||||||
function (next) {
|
function (next) {
|
||||||
user.getInvitesNumber(socket.uid, next);
|
user.getInvitesNumber(socket.uid, next);
|
||||||
},
|
},
|
||||||
function (invites, next) {
|
function (invites, next) {
|
||||||
if (!admin && invites > max) {
|
if (!isAdmin && invites >= max) {
|
||||||
return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'));
|
return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'));
|
||||||
}
|
}
|
||||||
next();
|
|
||||||
},
|
|
||||||
function (next) {
|
|
||||||
user.sendInvitationEmail(socket.uid, email, next);
|
user.sendInvitationEmail(socket.uid, email, next);
|
||||||
},
|
},
|
||||||
], callback);
|
], next);
|
||||||
} else {
|
},
|
||||||
user.sendInvitationEmail(socket.uid, email, callback);
|
], callback);
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketUser.getUserByUID = function (socket, uid, callback) {
|
SocketUser.getUserByUID = function (socket, uid, callback) {
|
||||||
|
|||||||
@@ -27,23 +27,23 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) {
|
Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) {
|
||||||
callback = callback || function () {};
|
async.waterfall([
|
||||||
async.parallel({
|
function (next) {
|
||||||
posts: function (next) {
|
async.parallel({
|
||||||
posts.getPostsFromSet(set, start, stop, uid, reverse, next);
|
posts: function (next) {
|
||||||
|
posts.getPostsFromSet(set, start, stop, uid, reverse, next);
|
||||||
|
},
|
||||||
|
postCount: function (next) {
|
||||||
|
Topics.getTopicField(tid, 'postcount', next);
|
||||||
|
},
|
||||||
|
}, next);
|
||||||
},
|
},
|
||||||
postCount: function (next) {
|
function (results, next) {
|
||||||
Topics.getTopicField(tid, 'postcount', next);
|
Topics.calculatePostIndices(results.posts, start, stop, results.postCount, reverse);
|
||||||
|
|
||||||
|
Topics.addPostData(results.posts, uid, next);
|
||||||
},
|
},
|
||||||
}, function (err, results) {
|
], callback);
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
Topics.calculatePostIndices(results.posts, start, stop, results.postCount, reverse);
|
|
||||||
|
|
||||||
Topics.addPostData(results.posts, uid, callback);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.addPostData = function (postData, uid, callback) {
|
Topics.addPostData = function (postData, uid, callback) {
|
||||||
@@ -58,88 +58,81 @@ module.exports = function (Topics) {
|
|||||||
return callback(null, []);
|
return callback(null, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.parallel({
|
function getPostUserData(field, method, callback) {
|
||||||
bookmarks: function (next) {
|
var uids = [];
|
||||||
posts.hasBookmarked(pids, uid, next);
|
|
||||||
},
|
|
||||||
voteData: function (next) {
|
|
||||||
posts.getVoteStatusByPostIDs(pids, uid, next);
|
|
||||||
},
|
|
||||||
userData: function (next) {
|
|
||||||
var uids = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < postData.length; i += 1) {
|
postData.forEach(function (postData) {
|
||||||
if (postData[i] && uids.indexOf(postData[i].uid) === -1) {
|
if (postData && postData[field] && uids.indexOf(postData[field]) === -1) {
|
||||||
uids.push(postData[i].uid);
|
uids.push(postData[field]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
posts.getUserInfoForPosts(uids, uid, function (err, users) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return next(err);
|
method(uids, next);
|
||||||
}
|
},
|
||||||
|
function (users, next) {
|
||||||
var userData = {};
|
var userData = {};
|
||||||
users.forEach(function (user, index) {
|
users.forEach(function (user, index) {
|
||||||
userData[uids[index]] = user;
|
userData[uids[index]] = user;
|
||||||
});
|
});
|
||||||
|
|
||||||
next(null, userData);
|
next(null, userData);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
function (next) {
|
||||||
|
async.parallel({
|
||||||
|
bookmarks: function (next) {
|
||||||
|
posts.hasBookmarked(pids, uid, next);
|
||||||
|
},
|
||||||
|
voteData: function (next) {
|
||||||
|
posts.getVoteStatusByPostIDs(pids, uid, next);
|
||||||
|
},
|
||||||
|
userData: function (next) {
|
||||||
|
getPostUserData('uid', function (uids, next) {
|
||||||
|
posts.getUserInfoForPosts(uids, uid, next);
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
editors: function (next) {
|
||||||
|
getPostUserData('editor', function (uids, next) {
|
||||||
|
user.getUsersFields(uids, ['uid', 'username', 'userslug'], next);
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
parents: function (next) {
|
||||||
|
Topics.addParentPosts(postData, next);
|
||||||
|
},
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function (results, next) {
|
||||||
|
postData.forEach(function (postObj, i) {
|
||||||
|
if (postObj) {
|
||||||
|
postObj.deleted = parseInt(postObj.deleted, 10) === 1;
|
||||||
|
postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]);
|
||||||
|
postObj.editor = postObj.editor ? results.editors[postObj.editor] : null;
|
||||||
|
postObj.bookmarked = results.bookmarks[i];
|
||||||
|
postObj.upvoted = results.voteData.upvotes[i];
|
||||||
|
postObj.downvoted = results.voteData.downvotes[i];
|
||||||
|
postObj.votes = postObj.votes || 0;
|
||||||
|
postObj.replies = postObj.replies || 0;
|
||||||
|
postObj.selfPost = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(postObj.uid, 10);
|
||||||
|
|
||||||
|
// Username override for guests, if enabled
|
||||||
|
if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) {
|
||||||
|
postObj.user.username = validator.escape(String(postObj.handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
plugins.fireHook('filter:topics.addPostData', {
|
||||||
|
posts: postData,
|
||||||
|
uid: uid,
|
||||||
|
}, next);
|
||||||
},
|
},
|
||||||
editors: function (next) {
|
function (data, next) {
|
||||||
var editors = [];
|
next(null, data.posts);
|
||||||
for (var i = 0; i < postData.length; i += 1) {
|
|
||||||
if (postData[i] && postData[i].editor && editors.indexOf(postData[i].editor) === -1) {
|
|
||||||
editors.push(postData[i].editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user.getUsersFields(editors, ['uid', 'username', 'userslug'], function (err, editors) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
var editorData = {};
|
|
||||||
editors.forEach(function (editor) {
|
|
||||||
editorData[editor.uid] = editor;
|
|
||||||
});
|
|
||||||
next(null, editorData);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
parents: function (next) {
|
], callback);
|
||||||
Topics.addParentPosts(postData, next);
|
|
||||||
},
|
|
||||||
}, function (err, results) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
postData.forEach(function (postObj, i) {
|
|
||||||
if (postObj) {
|
|
||||||
postObj.deleted = parseInt(postObj.deleted, 10) === 1;
|
|
||||||
postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]);
|
|
||||||
postObj.editor = postObj.editor ? results.editors[postObj.editor] : null;
|
|
||||||
postObj.bookmarked = results.bookmarks[i];
|
|
||||||
postObj.upvoted = results.voteData.upvotes[i];
|
|
||||||
postObj.downvoted = results.voteData.downvotes[i];
|
|
||||||
postObj.votes = postObj.votes || 0;
|
|
||||||
postObj.replies = postObj.replies || 0;
|
|
||||||
postObj.selfPost = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(postObj.uid, 10);
|
|
||||||
|
|
||||||
// Username override for guests, if enabled
|
|
||||||
if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) {
|
|
||||||
postObj.user.username = validator.escape(String(postObj.handle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
plugins.fireHook('filter:topics.addPostData', {
|
|
||||||
posts: postData,
|
|
||||||
uid: uid,
|
|
||||||
}, function (err, data) {
|
|
||||||
callback(err, data ? data.posts : null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) {
|
Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) {
|
||||||
@@ -172,7 +165,9 @@ module.exports = function (Topics) {
|
|||||||
async.apply(posts.getPostsFields, parentPids, ['uid']),
|
async.apply(posts.getPostsFields, parentPids, ['uid']),
|
||||||
function (_parentPosts, next) {
|
function (_parentPosts, next) {
|
||||||
parentPosts = _parentPosts;
|
parentPosts = _parentPosts;
|
||||||
var parentUids = parentPosts.map(function (postObj) { return parseInt(postObj.uid, 10); }).filter(function (uid, idx, users) {
|
var parentUids = parentPosts.map(function (postObj) {
|
||||||
|
return parseInt(postObj.uid, 10);
|
||||||
|
}).filter(function (uid, idx, users) {
|
||||||
return users.indexOf(uid) === idx;
|
return users.indexOf(uid) === idx;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,31 +226,31 @@ module.exports = function (Topics) {
|
|||||||
var done = false;
|
var done = false;
|
||||||
var latestPid = null;
|
var latestPid = null;
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
var pids;
|
||||||
async.doWhilst(
|
async.doWhilst(
|
||||||
function (next) {
|
function (next) {
|
||||||
db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, function (err, pids) {
|
async.waterfall([
|
||||||
if (err) {
|
function (_next) {
|
||||||
return next(err);
|
db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, _next);
|
||||||
}
|
},
|
||||||
|
function (_pids, _next) {
|
||||||
if (!Array.isArray(pids) || !pids.length) {
|
pids = _pids;
|
||||||
done = true;
|
if (!Array.isArray(pids) || !pids.length) {
|
||||||
return next();
|
done = true;
|
||||||
}
|
return next();
|
||||||
|
|
||||||
posts.getPostField(pids[0], 'deleted', function (err, deleted) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts.getPostField(pids[0], 'deleted', _next);
|
||||||
|
},
|
||||||
|
function (deleted, _next) {
|
||||||
isDeleted = parseInt(deleted, 10) === 1;
|
isDeleted = parseInt(deleted, 10) === 1;
|
||||||
if (!isDeleted) {
|
if (!isDeleted) {
|
||||||
latestPid = pids[0];
|
latestPid = pids[0];
|
||||||
}
|
}
|
||||||
index += 1;
|
index += 1;
|
||||||
next();
|
_next();
|
||||||
});
|
},
|
||||||
});
|
], next);
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
return isDeleted && !done;
|
return isDeleted && !done;
|
||||||
@@ -317,22 +312,24 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.getPids = function (tid, callback) {
|
Topics.getPids = function (tid, callback) {
|
||||||
async.parallel({
|
async.waterfall([
|
||||||
mainPid: function (next) {
|
function (next) {
|
||||||
Topics.getTopicField(tid, 'mainPid', next);
|
async.parallel({
|
||||||
|
mainPid: function (next) {
|
||||||
|
Topics.getTopicField(tid, 'mainPid', next);
|
||||||
|
},
|
||||||
|
pids: function (next) {
|
||||||
|
db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next);
|
||||||
|
},
|
||||||
|
}, next);
|
||||||
},
|
},
|
||||||
pids: function (next) {
|
function (results, next) {
|
||||||
db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next);
|
if (results.mainPid) {
|
||||||
|
results.pids = [results.mainPid].concat(results.pids);
|
||||||
|
}
|
||||||
|
next(null, results.pids);
|
||||||
},
|
},
|
||||||
}, function (err, results) {
|
], callback);
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
if (results.mainPid) {
|
|
||||||
results.pids = [results.mainPid].concat(results.pids);
|
|
||||||
}
|
|
||||||
callback(null, results.pids);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.increasePostCount = function (tid, callback) {
|
Topics.increasePostCount = function (tid, callback) {
|
||||||
@@ -349,12 +346,14 @@ module.exports = function (Topics) {
|
|||||||
|
|
||||||
function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) {
|
function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) {
|
||||||
callback = callback || function () {};
|
callback = callback || function () {};
|
||||||
db.incrObjectFieldBy('topic:' + tid, field, by, function (err, value) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
db.incrObjectFieldBy('topic:' + tid, field, by, next);
|
||||||
}
|
},
|
||||||
db.sortedSetAdd(set, value, tid, callback);
|
function (value, next) {
|
||||||
});
|
db.sortedSetAdd(set, value, tid, next);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
Topics.getTitleByPid = function (pid, callback) {
|
Topics.getTitleByPid = function (pid, callback) {
|
||||||
@@ -362,21 +361,25 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTopicFieldByPid = function (field, pid, callback) {
|
Topics.getTopicFieldByPid = function (field, pid, callback) {
|
||||||
posts.getPostField(pid, 'tid', function (err, tid) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
posts.getPostField(pid, 'tid', next);
|
||||||
}
|
},
|
||||||
Topics.getTopicField(tid, field, callback);
|
function (tid, next) {
|
||||||
});
|
Topics.getTopicField(tid, field, next);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTopicDataByPid = function (pid, callback) {
|
Topics.getTopicDataByPid = function (pid, callback) {
|
||||||
posts.getPostField(pid, 'tid', function (err, tid) {
|
async.waterfall([
|
||||||
if (err) {
|
function (next) {
|
||||||
return callback(err);
|
posts.getPostField(pid, 'tid', next);
|
||||||
}
|
},
|
||||||
Topics.getTopicData(tid, callback);
|
function (tid, next) {
|
||||||
});
|
Topics.getTopicData(tid, next);
|
||||||
|
},
|
||||||
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.getPostCount = function (tid, callback) {
|
Topics.getPostCount = function (tid, callback) {
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ module.exports = function (User) {
|
|||||||
if (exists) {
|
if (exists) {
|
||||||
return next(new Error('[[error:email-taken]]'));
|
return next(new Error('[[error:email-taken]]'));
|
||||||
}
|
}
|
||||||
next();
|
|
||||||
},
|
|
||||||
function (next) {
|
|
||||||
async.parallel([
|
async.parallel([
|
||||||
function (next) {
|
function (next) {
|
||||||
db.setAdd('invitation:uid:' + uid, email, next);
|
db.setAdd('invitation:uid:' + uid, email, next);
|
||||||
@@ -130,10 +128,10 @@ module.exports = function (User) {
|
|||||||
return next(new Error('[[error:invalid-username]]'));
|
return next(new Error('[[error:invalid-username]]'));
|
||||||
}
|
}
|
||||||
async.parallel([
|
async.parallel([
|
||||||
function deleteFromReferenceList(next) {
|
function (next) {
|
||||||
db.setRemove('invitation:uid:' + invitedByUid, email, next);
|
deleteFromReferenceList(invitedByUid, email, next);
|
||||||
},
|
},
|
||||||
function deleteInviteKey(next) {
|
function (next) {
|
||||||
db.delete('invitation:email:' + email, next);
|
db.delete('invitation:email:' + email, next);
|
||||||
},
|
},
|
||||||
], function (err) {
|
], function (err) {
|
||||||
@@ -145,6 +143,36 @@ module.exports = function (User) {
|
|||||||
|
|
||||||
User.deleteInvitationKey = function (email, callback) {
|
User.deleteInvitationKey = function (email, callback) {
|
||||||
callback = callback || function () {};
|
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';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var os = require('os');
|
|
||||||
var nconf = require('nconf');
|
var nconf = require('nconf');
|
||||||
var crypto = require('crypto');
|
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
var request = require('request');
|
var request = require('request');
|
||||||
var mime = require('mime');
|
var mime = require('mime');
|
||||||
@@ -18,77 +15,7 @@ var db = require('../database');
|
|||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.uploadPicture = function (uid, picture, callback) {
|
User.uploadPicture = function (uid, picture, callback) {
|
||||||
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
|
User.uploadCroppedPicture({ uid: uid, file: picture }, callback);
|
||||||
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, 10) !== 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.uploadFromUrl = function (uid, url, callback) {
|
User.uploadFromUrl = function (uid, url, callback) {
|
||||||
@@ -139,7 +66,7 @@ module.exports = function (User) {
|
|||||||
|
|
||||||
User.updateCoverPicture = function (data, callback) {
|
User.updateCoverPicture = function (data, callback) {
|
||||||
var url;
|
var url;
|
||||||
var image = {
|
var picture = {
|
||||||
name: 'profileCover',
|
name: 'profileCover',
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
};
|
};
|
||||||
@@ -164,12 +91,14 @@ module.exports = function (User) {
|
|||||||
return setImmediate(next, null, data.file.path);
|
return setImmediate(next, null, data.file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveImageDataToTempFile(data.imageData, next);
|
image.writeImageDataToTempFile(data.imageData, next);
|
||||||
},
|
},
|
||||||
function (path, 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) {
|
function (uploadData, next) {
|
||||||
url = uploadData.url;
|
url = uploadData.url;
|
||||||
@@ -183,7 +112,7 @@ module.exports = function (User) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
], function (err) {
|
], function (err) {
|
||||||
deleteFile(image.path);
|
deleteFile(picture.path);
|
||||||
callback(err, {
|
callback(err, {
|
||||||
url: url,
|
url: url,
|
||||||
});
|
});
|
||||||
@@ -191,75 +120,102 @@ module.exports = function (User) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
User.uploadCroppedPicture = function (data, callback) {
|
User.uploadCroppedPicture = function (data, callback) {
|
||||||
var url;
|
if (parseInt(meta.config.allowProfileImageUploads, 10) !== 1) {
|
||||||
var image = {
|
return callback(new Error('[[error:profile-image-uploads-disabled]]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
name: 'profileAvatar',
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data.imageData) {
|
|
||||||
return callback(new Error('[[error:invalid-data]]'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function (next) {
|
function (next) {
|
||||||
var size = data.imageData.length;
|
if (data.file) {
|
||||||
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
|
return setImmediate(next, null, data.file.path);
|
||||||
if (size > uploadSize * 1024) {
|
|
||||||
return next(new Error('[[error:file-too-big, ' + meta.config.maximumProfileImageSize + ']]'));
|
|
||||||
}
|
}
|
||||||
|
image.writeImageDataToTempFile(data.imageData, next);
|
||||||
saveImageDataToTempFile(data.imageData, next);
|
|
||||||
},
|
},
|
||||||
function (path, next) {
|
function (path, next) {
|
||||||
image.path = path;
|
convertToPNG(path, extension, next);
|
||||||
|
|
||||||
uploadProfileOrCover('profileavatar', image, data.imageData, next);
|
|
||||||
},
|
},
|
||||||
function (uploadData, next) {
|
function (path, next) {
|
||||||
url = uploadData.url;
|
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, {
|
User.setUserFields(data.uid, {
|
||||||
uploadedpicture: url,
|
uploadedpicture: uploadedImage.url,
|
||||||
picture: url,
|
picture: uploadedImage.url,
|
||||||
}, next);
|
}, next);
|
||||||
},
|
},
|
||||||
], function (err) {
|
], function (err) {
|
||||||
deleteFile(image.path);
|
deleteFile(picture.path);
|
||||||
callback(err, {
|
callback(err, uploadedImage);
|
||||||
url: url,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function saveImageDataToTempFile(imageData, callback) {
|
function convertToPNG(path, extension, callback) {
|
||||||
var filename = crypto.createHash('md5').update(imageData).digest('hex');
|
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
|
||||||
var filepath = path.join(os.tmpdir(), filename);
|
if (!convertToPNG) {
|
||||||
|
return setImmediate(callback, null, path);
|
||||||
|
}
|
||||||
|
|
||||||
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
|
image.normalise(path, extension, function (err, newPath) {
|
||||||
|
if (err) {
|
||||||
fs.writeFile(filepath, buffer, {
|
return callback(err);
|
||||||
encoding: 'base64',
|
}
|
||||||
}, function (err) {
|
deleteFile(path);
|
||||||
callback(err, filepath);
|
callback(null, newPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadProfileOrCover(type, image, imageData, callback) {
|
function uploadProfileOrCover(filename, image, callback) {
|
||||||
if (plugins.hasListeners('filter:uploadImage')) {
|
if (plugins.hasListeners('filter:uploadImage')) {
|
||||||
return plugins.fireHook('filter:uploadImage', {
|
return plugins.fireHook('filter:uploadImage', {
|
||||||
image: image,
|
image: image,
|
||||||
uid: image.uid,
|
uid: image.uid,
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
var filename = generateProfileImageFilename(image.uid, type, imageData);
|
|
||||||
saveFileToLocal(filename, image, callback);
|
saveFileToLocal(filename, image, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateProfileImageFilename(uid, type, imageData) {
|
function generateProfileImageFilename(uid, type, extension) {
|
||||||
var extension = file.typeToExtension(imageData.slice(5, imageData.indexOf('base64') - 1));
|
|
||||||
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
|
var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1;
|
||||||
var filename = uid + '-' + type + (keepAllVersions ? '-' + Date.now() : '') + (extension || '');
|
var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
|
||||||
return filename;
|
return uid + '-' + type + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFileToLocal(filename, image, callback) {
|
function saveFileToLocal(filename, image, callback) {
|
||||||
@@ -273,6 +229,7 @@ module.exports = function (User) {
|
|||||||
function (upload, next) {
|
function (upload, next) {
|
||||||
next(null, {
|
next(null, {
|
||||||
url: nconf.get('relative_path') + upload.url,
|
url: nconf.get('relative_path') + upload.url,
|
||||||
|
path: upload.path,
|
||||||
name: image.name,
|
name: image.name,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<div class="post-cache">
|
<div class="row post-cache">
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><i class="fa fa-calendar-o"></i> [[admin/advanced/cache:post-cache]]</div>
|
<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">
|
<div class="col-sm-9">
|
||||||
<!-- IF mongo -->
|
<!-- IF mongo -->
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="events">
|
<div class="row events">
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><i class="fa fa-calendar-o"></i> [[admin/advanced/events:events]]</div>
|
<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="col-lg-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><i class="fa fa-file-text-o"></i> [[admin/advanced/logs:logs]]</div>
|
<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">
|
<div class="directory row" id="bootstrap_themes">
|
||||||
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
|
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="logger">
|
<div class="row logger">
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/development/logger:logger-settings]]</div>
|
<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="col-lg-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/extend/rewards:rewards]]</div>
|
<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="col-lg-9">
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<ul id="active-navigation" class="nav navbar-nav">
|
<ul id="active-navigation" class="nav navbar-nav">
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
||||||
<input class="mdl-switch__input" type="checkbox" name="property:loggedIn" <!-- IF enabled.properties.loggedIn -->checked<!-- ENDIF enabled.properties.loggedIn -->/>
|
<input class="mdl-switch__input" type="checkbox" name="property:loggedIn" <!-- IF enabled.properties.loggedIn -->checked<!-- ENDIF enabled.properties.loggedIn -->/>
|
||||||
<span class="mdl-switch__label"><strong>[[admin/general/navigation:only-logged-in]]</strong></span>
|
<span class="mdl-switch__label"><strong>[[admin/general/navigation:only-logged-in]]</strong></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="social settings" class="row">
|
<div class="social settings">
|
||||||
<form role="form">
|
<form role="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/general/social:post-sharing]]</div>
|
<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">
|
<div class="col-xs-12">
|
||||||
<form role="form">
|
<form role="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -9,9 +9,15 @@
|
|||||||
<div class="form-group col-xs-9">
|
<div class="form-group col-xs-9">
|
||||||
<select class="form-control" id="notification" name="notification">
|
<select class="form-control" id="notification" name="notification">
|
||||||
<option value="">[[user:no-sound]]</option>
|
<option value="">[[user:no-sound]]</option>
|
||||||
<!-- BEGIN sounds -->
|
<!-- BEGIN notification-sound -->
|
||||||
<option value="{sounds.name}">{sounds.name}</option>
|
<optgroup label="{notification-sound.name}">
|
||||||
<!-- END sounds -->
|
<!-- 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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group col-xs-3">
|
<div class="btn-group col-xs-3">
|
||||||
@@ -29,9 +35,15 @@
|
|||||||
<div class="form-group col-xs-9">
|
<div class="form-group col-xs-9">
|
||||||
<select class="form-control" id="chat-incoming" name="chat-incoming">
|
<select class="form-control" id="chat-incoming" name="chat-incoming">
|
||||||
<option value="">[[user:no-sound]]</option>
|
<option value="">[[user:no-sound]]</option>
|
||||||
<!-- BEGIN sounds -->
|
<!-- BEGIN chat-incoming-sound -->
|
||||||
<option value="{sounds.name}">{sounds.name}</option>
|
<optgroup label="{chat-incoming-sound.name}">
|
||||||
<!-- END sounds -->
|
<!-- 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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group col-xs-3">
|
<div class="btn-group col-xs-3">
|
||||||
@@ -44,9 +56,15 @@
|
|||||||
<div class="form-group col-xs-9">
|
<div class="form-group col-xs-9">
|
||||||
<select class="form-control" id="chat-outgoing" name="chat-outgoing">
|
<select class="form-control" id="chat-outgoing" name="chat-outgoing">
|
||||||
<option value="">[[user:no-sound]]</option>
|
<option value="">[[user:no-sound]]</option>
|
||||||
<!-- BEGIN sounds -->
|
<!-- BEGIN chat-outgoing-sound -->
|
||||||
<option value="{sounds.name}">{sounds.name}</option>
|
<optgroup label="{chat-outgoing-sound.name}">
|
||||||
<!-- END sounds -->
|
<!-- 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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group col-xs-3">
|
<div class="btn-group col-xs-3">
|
||||||
@@ -56,7 +74,15 @@
|
|||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-btn">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/admin.css?{cache-buster}" />
|
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/admin.css?{cache-buster}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/vendor/mdl/mdl.min.css?{cache-buster}" />
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var RELATIVE_PATH = "{relative_path}";
|
var RELATIVE_PATH = "{relative_path}";
|
||||||
var config = JSON.parse('{{configJSON}}');
|
var config = JSON.parse('{{configJSON}}');
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.3/material.min.js"></script>
|
<script src="https://storage.googleapis.com/code.getmdl.io/1.3.0/material.min.js"></script>
|
||||||
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
|
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
|
||||||
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
|
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
|
||||||
<script type="text/javascript" src="{relative_path}/assets/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
|
<script type="text/javascript" src="{relative_path}/assets/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<div class="flags">
|
<div class="row flags">
|
||||||
|
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -106,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
|
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
|
||||||
<span class="timeago" title="{posts.timestampISO}"></span> •
|
<span class="timeago" title="{posts.timestampISO}"></span> •
|
||||||
<a href="{config.relative_path}/post/{posts.pid}" target="_blank">[[admin/manage/flags:read-more]]</a>
|
<a href="{config.relative_path}/post/{posts.pid}" target="_blank">[[admin/manage/flags:read-more]]</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -114,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<i class="fa fa-flag"></i>
|
<i class="fa fa-flag"></i>
|
||||||
[[admin/manage/flags:flagged-x-times, {posts.flags}]]
|
[[admin/manage/flags:flagged-x-times, {posts.flags}]]
|
||||||
<blockquote class="flag-reporters">
|
<blockquote class="flag-reporters">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input id="group-private" name="private" type="checkbox"<!-- IF group.private --> checked<!-- ENDIF group.private -->>
|
<input id="group-private" name="private" type="checkbox"<!-- IF group.private --> checked<!-- ENDIF group.private -->>
|
||||||
<strong>[[groups:details.private]]</strong>
|
<strong>[[groups:details.private]]</strong>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
[[admin/manage/groups:edit.private-details]]
|
[[admin/manage/groups:edit.private-details]]
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input id="group-disableJoinRequests" name="disableJoinRequests" type="checkbox"<!-- IF group.disableJoinRequests --> checked<!-- ENDIF group.disableJoinRequests -->>
|
<input id="group-disableJoinRequests" name="disableJoinRequests" type="checkbox"<!-- IF group.disableJoinRequests --> checked<!-- ENDIF group.disableJoinRequests -->>
|
||||||
<strong>[[admin/manage/groups:edit.disable-requests]]</strong>
|
<strong>[[admin/manage/groups:edit.disable-requests]]</strong>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input id="group-hidden" name="hidden" type="checkbox"<!-- IF group.hidden --> checked<!-- ENDIF group.hidden -->>
|
<input id="group-hidden" name="hidden" type="checkbox"<!-- IF group.hidden --> checked<!-- ENDIF group.hidden -->>
|
||||||
<strong>[[admin/manage/groups:edit.hidden]]</strong>
|
<strong>[[admin/manage/groups:edit.hidden]]</strong>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
[[admin/manage/groups:edit.hidden-details]]
|
[[admin/manage/groups:edit.hidden-details]]
|
||||||
@@ -87,36 +87,19 @@
|
|||||||
<h3 class="panel-title"><i class="fa fa-users"></i> [[admin/manage/groups:edit.members]]</h3>
|
<h3 class="panel-title"><i class="fa fa-users"></i> [[admin/manage/groups:edit.members]]</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!-- IMPORT partials/groups/memberlist.tpl -->
|
<!-- IMPORT admin/partials/groups/memberlist.tpl -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 options acp-sidebar">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">[[admin/manage/groups:control-panel]]</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="btn-group btn-group-justified">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-primary save">[[global:save]]</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-default revert">
|
|
||||||
[[admin/manage/groups:revert]]
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||||
|
<i class="material-icons">save</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div id="icons" style="display:none;">
|
<div id="icons" style="display:none;">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<div class="row fa-icons">
|
<div class="row fa-icons">
|
||||||
@@ -125,4 +108,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,45 @@
|
|||||||
<div class="groups">
|
<div class="row groups">
|
||||||
<div class="col-lg-9">
|
<div class="col-xs-12">
|
||||||
<div class="panel panel-default">
|
<div>
|
||||||
<div class="panel-body">
|
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
|
||||||
<table class="table table-striped groups-list">
|
|
||||||
<tr>
|
|
||||||
<th>[[admin/manage/groups:name]]</th>
|
|
||||||
<th>[[admin/manage/groups:description]]</th>
|
|
||||||
</tr>
|
|
||||||
<!-- BEGIN groups -->
|
|
||||||
<tr data-groupname="{groups.displayName}">
|
|
||||||
<td>
|
|
||||||
{groups.displayName}
|
|
||||||
<!-- IF groups.system -->
|
|
||||||
<span class="badge">[[admin/manage/groups:system]]</span>
|
|
||||||
<!-- ENDIF groups.system -->
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group pull-right">
|
|
||||||
<a href="{config.relative_path}/admin/manage/groups/{groups.nameEncoded}" class="btn btn-default btn-xs">
|
|
||||||
<i class="fa fa-edit"></i> [[admin/manage/groups:edit]]
|
|
||||||
</a>
|
|
||||||
<!-- IF !groups.system -->
|
|
||||||
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
|
|
||||||
<!-- ENDIF !groups.system -->
|
|
||||||
</div>
|
|
||||||
<p class="description">{groups.description}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- END groups -->
|
|
||||||
</table>
|
|
||||||
<!-- IMPORT partials/paginator.tpl -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 acp-sidebar">
|
<table class="table table-striped groups-list">
|
||||||
<div class="panel panel-default">
|
<thead>
|
||||||
<div class="panel-body">
|
<tr>
|
||||||
<div>
|
<th>[[admin/manage/groups:name]]</th>
|
||||||
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
|
<th class="hidden-xs">[[admin/manage/groups:description]]</th>
|
||||||
</div>
|
<th></th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- BEGIN groups -->
|
||||||
|
<tr data-groupname="{groups.displayName}">
|
||||||
|
<td>
|
||||||
|
{groups.displayName}
|
||||||
|
<!-- IF groups.system -->
|
||||||
|
<span class="badge">[[admin/manage/groups:system]]</span>
|
||||||
|
<!-- ENDIF groups.system -->
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs">
|
||||||
|
<p class="description">{groups.description}</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group ">
|
||||||
|
<a href="{config.relative_path}/admin/manage/groups/{groups.nameEncoded}" class="btn btn-default btn-xs">
|
||||||
|
<i class="fa fa-edit"></i> [[admin/manage/groups:edit]]
|
||||||
|
</a>
|
||||||
|
<!-- IF !groups.system -->
|
||||||
|
<button class="btn btn-danger btn-xs" data-action="delete"><i class="fa fa-times"></i></button>
|
||||||
|
<!-- ENDIF !groups.system -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- END groups -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- IMPORT partials/paginator.tpl -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="create-modal">
|
<div class="modal fade" id="create-modal">
|
||||||
@@ -75,10 +73,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button id="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
<button id="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||||
<i class="material-icons">add</i>
|
<i class="material-icons">add</i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="flags">
|
<div class="row ip-blacklist">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
[[admin/manage/ip-blacklist:lead]]
|
[[admin/manage/ip-blacklist:lead]]
|
||||||
|
|||||||
@@ -1,109 +1,121 @@
|
|||||||
<div class="registration panel panel-primary">
|
<div class="row">
|
||||||
<div class="panel-heading">
|
<div class="col-xs-12">
|
||||||
[[admin/manage/registration:queue]]
|
<div class="registration panel panel-primary">
|
||||||
</div>
|
<div class="panel-heading">
|
||||||
<!-- IF !users.length -->
|
[[admin/manage/registration:queue]]
|
||||||
<p class="panel-body">
|
</div>
|
||||||
[[admin/manage/registration:description, {config.relative_path}/admin/settings/user]]
|
<!-- IF !users.length -->
|
||||||
</p>
|
<p class="panel-body">
|
||||||
<!-- ENDIF !users.length -->
|
[[admin/manage/registration:description, {config.relative_path}/admin/settings/user]]
|
||||||
<div class="table-responsive">
|
</p>
|
||||||
<table class="table table-striped users-list">
|
<!-- ENDIF !users.length -->
|
||||||
<tr>
|
<div class="table-responsive">
|
||||||
<th>[[admin/manage/registration:list.name]]</th>
|
<table class="table table-striped users-list">
|
||||||
<th>[[admin/manage/registration:list.email]]</th>
|
<thead>
|
||||||
<th class="hidden-xs">[[admin/manage/registration:list.ip]]</th>
|
<tr>
|
||||||
<th class="hidden-xs">[[admin/manage/registration:list.time]]</th>
|
<th>[[admin/manage/registration:list.name]]</th>
|
||||||
<!-- BEGIN customHeaders -->
|
<th>[[admin/manage/registration:list.email]]</th>
|
||||||
<th class="hidden-xs">{customHeaders.label}</th>
|
<th class="hidden-xs">[[admin/manage/registration:list.ip]]</th>
|
||||||
<!-- END customHeaders -->
|
<th class="hidden-xs">[[admin/manage/registration:list.time]]</th>
|
||||||
<th></th>
|
<!-- BEGIN customHeaders -->
|
||||||
</tr>
|
<th class="hidden-xs">{customHeaders.label}</th>
|
||||||
<!-- BEGIN users -->
|
<!-- END customHeaders -->
|
||||||
<tr data-username="{users.username}">
|
<th></th>
|
||||||
<td>
|
</tr>
|
||||||
<!-- IF users.usernameSpam -->
|
</thead>
|
||||||
<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>
|
<tbody>
|
||||||
<!-- ELSE -->
|
<!-- BEGIN users -->
|
||||||
<i class="fa fa-check text-success"></i>
|
<tr data-username="{users.username}">
|
||||||
<!-- ENDIF users.usernameSpam -->
|
<td>
|
||||||
{users.username}
|
<!-- IF users.usernameSpam -->
|
||||||
</td>
|
<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>
|
||||||
<td>
|
<!-- ELSE -->
|
||||||
<!-- IF users.emailSpam -->
|
<i class="fa fa-check text-success"></i>
|
||||||
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.email-spam, {users.spamData.email.frequency}, {users.spamData.email.appears}]]"></i>
|
<!-- ENDIF users.usernameSpam -->
|
||||||
<!-- ELSE -->
|
{users.username}
|
||||||
<i class="fa fa-check text-success"></i>
|
</td>
|
||||||
<!-- ENDIF users.emailSpam -->
|
<td>
|
||||||
{users.email}
|
<!-- IF users.emailSpam -->
|
||||||
</td>
|
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.email-spam, {users.spamData.email.frequency}, {users.spamData.email.appears}]]"></i>
|
||||||
<td class="hidden-xs">
|
<!-- ELSE -->
|
||||||
<!-- IF users.ipSpam -->
|
<i class="fa fa-check text-success"></i>
|
||||||
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.ip-spam, {users.spamData.ip.frequency}, {users.spamData.ip.appears}]]"></i>
|
<!-- ENDIF users.emailSpam -->
|
||||||
<!-- ELSE -->
|
{users.email}
|
||||||
<i class="fa fa-check text-success"></i>
|
</td>
|
||||||
<!-- ENDIF users.ipSpam -->
|
<td class="hidden-xs">
|
||||||
{users.ip}
|
<!-- IF users.ipSpam -->
|
||||||
<!-- BEGIN users.ipMatch -->
|
<i class="fa fa-times-circle text-danger" title="[[admin/manage/registration:list.ip-spam, {users.spamData.ip.frequency}, {users.spamData.ip.appears}]]"></i>
|
||||||
<br>
|
<!-- ELSE -->
|
||||||
<!-- IF users.ipMatch.picture -->
|
<i class="fa fa-check text-success"></i>
|
||||||
<img src="{users.ipMatch.picture}" class="user-img"/>
|
<!-- ENDIF users.ipSpam -->
|
||||||
<!-- ELSE -->
|
{users.ip}
|
||||||
<div class="user-img avatar avatar-sm" style="background-color: {users.ipMatch.icon:bgColor};">{users.ipMatch.icon:text}</div>
|
<!-- BEGIN users.ipMatch -->
|
||||||
<!-- ENDIF users.ipMatch.picture -->
|
<br>
|
||||||
<a href="/uid/{users.ipMatch.uid}">{users.ipMatch.username}</a>
|
<!-- IF users.ipMatch.picture -->
|
||||||
<!-- END users.ipMatch -->
|
<img src="{users.ipMatch.picture}" class="user-img"/>
|
||||||
</td>
|
<!-- ELSE -->
|
||||||
<td class="hidden-xs">
|
<div class="user-img avatar avatar-sm" style="background-color: {users.ipMatch.icon:bgColor};">{users.ipMatch.icon:text}</div>
|
||||||
<span class="timeago" title="{users.timestampISO}"></span>
|
<!-- ENDIF users.ipMatch.picture -->
|
||||||
</td>
|
<a href="/uid/{users.ipMatch.uid}">{users.ipMatch.username}</a>
|
||||||
|
<!-- END users.ipMatch -->
|
||||||
<!-- BEGIN users.customRows -->
|
</td>
|
||||||
<td class="hidden-xs">{users.customRows.value}</td>
|
<td class="hidden-xs">
|
||||||
<!-- END users.customRows -->
|
<span class="timeago" title="{users.timestampISO}"></span>
|
||||||
|
</td>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- IMPORT partials/paginator.tpl -->
|
<!-- BEGIN users.customRows -->
|
||||||
</div>
|
<td class="hidden-xs">{users.customRows.value}</td>
|
||||||
|
<!-- END users.customRows -->
|
||||||
|
|
||||||
<div class="invitations panel panel-success">
|
<td>
|
||||||
<div class="panel-heading">
|
<div class="btn-group pull-right">
|
||||||
[[admin/manage/registration:invitations]]
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
<p class="panel-body">
|
</div>
|
||||||
[[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>
|
|
||||||
</div>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<div class="tags row">
|
<div class="tags row">
|
||||||
|
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="panel panel-default tag-management">
|
<div class="panel panel-default tag-management">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -11,12 +10,11 @@
|
|||||||
<!-- BEGIN tags -->
|
<!-- BEGIN tags -->
|
||||||
<div class="tag-row" data-tag="{tags.value}">
|
<div class="tag-row" data-tag="{tags.value}">
|
||||||
<div data-value="{tags.value}">
|
<div data-value="{tags.value}">
|
||||||
<span class="tag-item" data-tag="{tags.value}" style="
|
<span class="mdl-chip mdl-chip--contact tag-item" data-tag="{tags.value}" style="
|
||||||
<!-- IF tags.color -->color: {tags.color};<!-- ENDIF tags.color -->
|
<!-- IF tags.color -->color: {tags.color};<!-- ENDIF tags.color -->
|
||||||
<!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->
|
<!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->">
|
||||||
">{tags.value}</span>
|
<span class="mdl-chip__contact mdl-color--light-blue mdl-color-text--white tag-topic-count">{tags.score}</span>
|
||||||
<span class="tag-topic-count">
|
<span class="mdl-chip__text">{tags.value}</span>
|
||||||
<a href="{config.relative_path}/tags/{tags.value}" target="_blank">{tags.score}</a>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-modal hidden">
|
<div class="tag-modal hidden">
|
||||||
@@ -76,5 +74,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,37 +71,41 @@
|
|||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped users-table">
|
<table class="table table-striped users-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th><input component="user/select/all" type="checkbox"/></th>
|
<tr>
|
||||||
<th>[[admin/manage/users:users.uid]]</th>
|
<th><input component="user/select/all" type="checkbox"/></th>
|
||||||
<th>[[admin/manage/users:users.username]]</th>
|
<th>[[admin/manage/users:users.uid]]</th>
|
||||||
<th>[[admin/manage/users:users.email]]</th>
|
<th>[[admin/manage/users:users.username]]</th>
|
||||||
<th class="text-right">[[admin/manage/users:users.postcount]]</th>
|
<th>[[admin/manage/users:users.email]]</th>
|
||||||
<th class="text-right">[[admin/manage/users:users.reputation]]</th>
|
<th class="text-right">[[admin/manage/users:users.postcount]]</th>
|
||||||
<th class="text-right">[[admin/manage/users:users.flags]]</th>
|
<th class="text-right">[[admin/manage/users:users.reputation]]</th>
|
||||||
<th>[[admin/manage/users:users.joined]]</th>
|
<th class="text-right">[[admin/manage/users:users.flags]]</th>
|
||||||
<th>[[admin/manage/users:users.last-online]]</th>
|
<th>[[admin/manage/users:users.joined]]</th>
|
||||||
<th>[[admin/manage/users:users.banned]]</th>
|
<th>[[admin/manage/users:users.last-online]]</th>
|
||||||
</tr>
|
<th>[[admin/manage/users:users.banned]]</th>
|
||||||
<!-- BEGIN users -->
|
</tr>
|
||||||
<tr class="user-row">
|
</thead>
|
||||||
<th><input component="user/select/single" data-uid="{users.uid}" type="checkbox"/></th>
|
<tbody>
|
||||||
<td class="text-right">{users.uid}</td>
|
<!-- BEGIN users -->
|
||||||
<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>
|
<tr class="user-row">
|
||||||
|
<th><input component="user/select/single" data-uid="{users.uid}" type="checkbox"/></th>
|
||||||
<td>
|
<td class="text-right">{users.uid}</td>
|
||||||
<!-- IF config.requireEmailConfirmation -->
|
<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>
|
||||||
<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>
|
<td>
|
||||||
<!-- ENDIF config.requireEmailConfirmation --> {users.email}</td>
|
<!-- IF config.requireEmailConfirmation -->
|
||||||
<td class="text-right">{users.postcount}</td>
|
<i class="validated fa fa-check text-success<!-- IF !users.email:confirmed --> hidden<!-- ENDIF !users.email:confirmed -->" title="validated"></i>
|
||||||
<td class="text-right">{users.reputation}</td>
|
<i class="notvalidated fa fa-times text-danger<!-- IF users.email:confirmed --> hidden<!-- ENDIF users.email:confirmed -->" title="not validated"></i>
|
||||||
<td class="text-right"><!-- IF users.flags -->{users.flags}<!-- ELSE -->0<!-- ENDIF users.flags --></td>
|
<!-- ENDIF config.requireEmailConfirmation --> {users.email}</td>
|
||||||
<td><span class="timeago" title="{users.joindateISO}"></span></td>
|
<td class="text-right">{users.postcount}</td>
|
||||||
<td><span class="timeago" title="{users.lastonlineISO}"></span></td>
|
<td class="text-right">{users.reputation}</td>
|
||||||
<td class="text-center"><i class="ban fa fa-gavel text-danger<!-- IF !users.banned --> hidden<!-- ENDIF !users.banned -->"></i></td>
|
<td class="text-right"><!-- IF users.flags -->{users.flags}<!-- ELSE -->0<!-- ENDIF users.flags --></td>
|
||||||
</tr>
|
<td><span class="timeago" title="{users.joindateISO}"></span></td>
|
||||||
<!-- END users -->
|
<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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +1,109 @@
|
|||||||
<table class="table table-striped privilege-table">
|
<table class="table table-striped privilege-table">
|
||||||
<tr class="privilege-table-header">
|
<thead>
|
||||||
<th colspan="2"></th>
|
<tr class="privilege-table-header">
|
||||||
<th class="arrowed" colspan="3">
|
<th colspan="2"></th>
|
||||||
[[admin/manage/categories:privileges.section-viewing]]
|
<th class="arrowed" colspan="3">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-viewing]]
|
||||||
<th class="arrowed" colspan="7">
|
</th>
|
||||||
[[admin/manage/categories:privileges.section-posting]]
|
<th class="arrowed" colspan="7">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-posting]]
|
||||||
<th class="arrowed" colspan="2">
|
</th>
|
||||||
[[admin/manage/categories:privileges.section-moderation]]
|
<th class="arrowed" colspan="2">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-moderation]]
|
||||||
</tr><tr><!-- zebrastripe reset --></tr>
|
</th>
|
||||||
<tr>
|
</tr><tr><!-- zebrastripe reset --></tr>
|
||||||
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
|
<tr>
|
||||||
<!-- BEGIN privileges.labels.users -->
|
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
|
||||||
<th class="text-center">{privileges.labels.users.name}</th>
|
<!-- BEGIN privileges.labels.users -->
|
||||||
<!-- END privileges.labels.users -->
|
<th class="text-center">{privileges.labels.users.name}</th>
|
||||||
</tr>
|
<!-- END privileges.labels.users -->
|
||||||
<!-- IF privileges.users.length -->
|
</tr>
|
||||||
<!-- BEGIN privileges.users -->
|
</thead>
|
||||||
<tr data-uid="{privileges.users.uid}">
|
<tbody>
|
||||||
<td>
|
<!-- IF privileges.users.length -->
|
||||||
<!-- IF ../picture -->
|
<!-- BEGIN privileges.users -->
|
||||||
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
|
<tr data-uid="{privileges.users.uid}">
|
||||||
<!-- ELSE -->
|
<td>
|
||||||
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
|
<!-- IF ../picture -->
|
||||||
<!-- ENDIF ../picture -->
|
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
|
||||||
</td>
|
<!-- ELSE -->
|
||||||
<td>{privileges.users.username}</td>
|
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
|
||||||
{function.spawnPrivilegeStates, privileges.users.username, privileges}
|
<!-- ENDIF ../picture -->
|
||||||
</tr>
|
</td>
|
||||||
<!-- END privileges.users -->
|
<td>{privileges.users.username}</td>
|
||||||
<tr>
|
{function.spawnPrivilegeStates, privileges.users.username, privileges}
|
||||||
<td colspan="{privileges.columnCount}">
|
</tr>
|
||||||
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
|
<!-- END privileges.users -->
|
||||||
[[admin/manage/categories:privileges.search-user]]
|
<tr>
|
||||||
</button>
|
<td colspan="{privileges.columnCount}">
|
||||||
</td>
|
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
|
||||||
</tr>
|
[[admin/manage/categories:privileges.search-user]]
|
||||||
<!-- ELSE -->
|
</button>
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{privileges.columnCount}">
|
</tr>
|
||||||
[[admin/manage/categories:privileges.no-users]]
|
<!-- ELSE -->
|
||||||
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
|
<tr>
|
||||||
[[admin/manage/categories:privileges.search-user]]
|
<td colspan="{privileges.columnCount}">
|
||||||
</button>
|
[[admin/manage/categories:privileges.no-users]]
|
||||||
</td>
|
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
|
||||||
</tr>
|
[[admin/manage/categories:privileges.search-user]]
|
||||||
<!-- ENDIF privileges.users.length -->
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- ENDIF privileges.users.length -->
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<table class="table table-striped privilege-table">
|
<table class="table table-striped privilege-table">
|
||||||
<tr class="privilege-table-header">
|
<thead>
|
||||||
<th colspan="2"></th>
|
<tr class="privilege-table-header">
|
||||||
<th class="arrowed" colspan="3">
|
<th colspan="2"></th>
|
||||||
[[admin/manage/categories:privileges.section-viewing]]
|
<th class="arrowed" colspan="3">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-viewing]]
|
||||||
<th class="arrowed" colspan="7">
|
</th>
|
||||||
[[admin/manage/categories:privileges.section-posting]]
|
<th class="arrowed" colspan="7">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-posting]]
|
||||||
<th class="arrowed" colspan="2">
|
</th>
|
||||||
[[admin/manage/categories:privileges.section-moderation]]
|
<th class="arrowed" colspan="2">
|
||||||
</th>
|
[[admin/manage/categories:privileges.section-moderation]]
|
||||||
</tr><tr><!-- zebrastripe reset --></tr>
|
</th>
|
||||||
<tr>
|
</tr><tr><!-- zebrastripe reset --></tr>
|
||||||
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
|
<tr>
|
||||||
<!-- BEGIN privileges.labels.groups -->
|
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
|
||||||
<th class="text-center">{privileges.labels.groups.name}</th>
|
<!-- BEGIN privileges.labels.groups -->
|
||||||
<!-- END privileges.labels.groups -->
|
<th class="text-center">{privileges.labels.groups.name}</th>
|
||||||
</tr>
|
<!-- END privileges.labels.groups -->
|
||||||
<!-- BEGIN privileges.groups -->
|
</tr>
|
||||||
<tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
|
</thead>
|
||||||
<td>
|
<tbody>
|
||||||
<!-- IF privileges.groups.isPrivate -->
|
<!-- BEGIN privileges.groups -->
|
||||||
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
|
<tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
|
||||||
<!-- ENDIF privileges.groups.isPrivate -->
|
<td>
|
||||||
{privileges.groups.name}
|
<!-- IF privileges.groups.isPrivate -->
|
||||||
</td>
|
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
|
||||||
<td></td>
|
<!-- ENDIF privileges.groups.isPrivate -->
|
||||||
{function.spawnPrivilegeStates, name, privileges}
|
{privileges.groups.name}
|
||||||
</tr>
|
</td>
|
||||||
<!-- END privileges.groups -->
|
<td></td>
|
||||||
<tr>
|
{function.spawnPrivilegeStates, name, privileges}
|
||||||
<td colspan="{privileges.columnCount}">
|
</tr>
|
||||||
<div class="btn-toolbar">
|
<!-- END privileges.groups -->
|
||||||
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group">
|
<tr>
|
||||||
[[admin/manage/categories:privileges.search-group]]
|
<td colspan="{privileges.columnCount}">
|
||||||
</button>
|
<div class="btn-toolbar">
|
||||||
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyToChildren">
|
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group">
|
||||||
[[admin/manage/categories:privileges.copy-to-children]]
|
[[admin/manage/categories:privileges.search-group]]
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyPrivilegesFrom">
|
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyToChildren">
|
||||||
[[admin/manage/categories:privileges.copy-from-category]]
|
[[admin/manage/categories:privileges.copy-to-children]]
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyPrivilegesFrom">
|
||||||
</td>
|
[[admin/manage/categories:privileges.copy-from-category]]
|
||||||
</tr>
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
[[admin/manage/categories:privileges.inherit]]
|
[[admin/manage/categories:privileges.inherit]]
|
||||||
|
|||||||
37
src/views/admin/partials/groups/memberlist.tpl
Normal file
37
src/views/admin/partials/groups/memberlist.tpl
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" type="text" component="groups/members/search" placeholder="[[global:search]]"/>
|
||||||
|
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||||
|
</div><br />
|
||||||
|
|
||||||
|
<table component="groups/members" class="table table-striped table-hover" data-nextstart="{group.membersNextStart}">
|
||||||
|
<tbody>
|
||||||
|
<!-- BEGIN members -->
|
||||||
|
<tr data-uid="{group.members.uid}">
|
||||||
|
<td>
|
||||||
|
<a href="{config.relative_path}/user/{group.members.userslug}">
|
||||||
|
<!-- IF group.members.picture -->
|
||||||
|
<img class="avatar avatar-sm" src="{group.members.picture}" />
|
||||||
|
<!-- ELSE -->
|
||||||
|
<div class="avatar avatar-sm" style="background-color: {group.members.icon:bgColor};">{group.members.icon:text}</div>
|
||||||
|
<!-- ENDIF group.members.picture -->
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="member-name">
|
||||||
|
<a href="{config.relative_path}/user/{group.members.userslug}">{group.members.username}</a> <i title="[[groups:owner]]" class="fa fa-star text-warning <!-- IF !group.members.isOwner -->invisible<!-- ENDIF !group.members.isOwner -->"></i>
|
||||||
|
|
||||||
|
<!-- IF group.isOwner -->
|
||||||
|
<div class="owner-controls btn-group pull-right">
|
||||||
|
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="toggleOwnership" title="[[groups:details.grant]]">
|
||||||
|
<i class="fa fa-star"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="kick" title="[[groups:details.kick]]">
|
||||||
|
<i class="fa fa-ban"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- ENDIF group.isOwner -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- END members -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -1,40 +1,39 @@
|
|||||||
<div id="crop-picture-modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="crop-picture" aria-hidden="true">
|
<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-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
<h3 id="crop-picture">[[user:crop_picture]]</h3>
|
<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>
|
||||||
<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>
|
|
||||||
|
|
||||||
<hr />
|
<div class="cropper">
|
||||||
|
<img id="cropped-image" src="{url}">
|
||||||
<div class="btn-group">
|
</div>
|
||||||
<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>
|
<hr />
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
<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 rotate" data-degrees="-45"><i class="fa fa-rotate-left"></i></button>
|
||||||
<button class="btn btn-primary flip" data-option="1" data-method="scaleY"><i class="fa fa-arrows-v"></i></button>
|
<button class="btn btn-primary rotate" data-degrees="45"><i class="fa fa-rotate-right"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-primary reset"><i class="fa fa-refresh"></i></button>
|
<button class="btn btn-primary flip" data-option="-1" data-method="scaleX"><i class="fa fa-arrows-h"></i></button>
|
||||||
</div>
|
<button class="btn btn-primary flip" data-option="1" data-method="scaleY"><i class="fa fa-arrows-v"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
<div class="btn-group">
|
||||||
<div class="modal-footer">
|
<button class="btn btn-primary reset"><i class="fa fa-refresh"></i></button>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -103,9 +103,9 @@ function initializeNodeBB(callback) {
|
|||||||
},
|
},
|
||||||
function (next) {
|
function (next) {
|
||||||
async.series([
|
async.series([
|
||||||
async.apply(meta.sounds.init),
|
meta.sounds.addUploads,
|
||||||
async.apply(languages.init),
|
languages.init,
|
||||||
async.apply(meta.blacklist.load),
|
meta.blacklist.load,
|
||||||
], next);
|
], next);
|
||||||
},
|
},
|
||||||
], callback);
|
], callback);
|
||||||
@@ -159,7 +159,8 @@ function setupExpressApp(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupFavicon(app) {
|
function setupFavicon(app) {
|
||||||
var faviconPath = path.join(nconf.get('base_dir'), 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico');
|
var faviconPath = meta.config['brand:favicon'] || 'favicon.ico';
|
||||||
|
faviconPath = path.join(nconf.get('base_dir'), 'public', faviconPath.replace(/assets\/uploads/, 'uploads'));
|
||||||
if (file.existsSync(faviconPath)) {
|
if (file.existsSync(faviconPath)) {
|
||||||
app.use(nconf.get('relative_path'), favicon(faviconPath));
|
app.use(nconf.get('relative_path'), favicon(faviconPath));
|
||||||
}
|
}
|
||||||
|
|||||||
79
test/blacklist.js
Normal file
79
test/blacklist.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ var db = require('./mocks/databasemock');
|
|||||||
|
|
||||||
describe('Build', function () {
|
describe('Build', function () {
|
||||||
it('should build all assets', function (done) {
|
it('should build all assets', function (done) {
|
||||||
|
this.timeout(50000);
|
||||||
var build = require('../src/meta/build');
|
var build = require('../src/meta/build');
|
||||||
build.buildAll(function (err) {
|
build.buildAll(function (err) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ describe('Controllers', function () {
|
|||||||
user: function (next) {
|
user: function (next) {
|
||||||
user.create({ username: 'foo', password: 'barbar' }, 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) {
|
}, function (err, results) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return done(err);
|
return done(err);
|
||||||
|
|||||||
177
test/files/503.html
Normal file
177
test/files/503.html
Normal file
@@ -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>
|
||||||
|
<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>
|
||||||
BIN
test/files/favicon.ico
Normal file
BIN
test/files/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
test/files/test.png
Normal file
BIN
test/files/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -705,8 +705,8 @@ describe('Groups', function () {
|
|||||||
describe('groups cover', function () {
|
describe('groups cover', function () {
|
||||||
var socketGroups = require('../src/socket.io/groups');
|
var socketGroups = require('../src/socket.io/groups');
|
||||||
var regularUid;
|
var regularUid;
|
||||||
var logoPath = path.join(__dirname, '../public/logo.png');
|
var logoPath = path.join(__dirname, '../test/files/test.png');
|
||||||
var imagePath = path.join(__dirname, '../public/groupcover.png');
|
var imagePath = path.join(__dirname, '../test/files/groupcover.png');
|
||||||
before(function (done) {
|
before(function (done) {
|
||||||
User.create({ username: 'regularuser', password: '123456' }, function (err, uid) {
|
User.create({ username: 'regularuser', password: '123456' }, function (err, uid) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ helpers.loginUser = function (username, password, callback) {
|
|||||||
this.open = function () {
|
this.open = function () {
|
||||||
stdOpen.apply(this, arguments);
|
stdOpen.apply(this, arguments);
|
||||||
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
||||||
|
this.setRequestHeader('Origin', nconf.get('url'));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ helpers.initSocketIO = function (callback) {
|
|||||||
this.open = function () {
|
this.open = function () {
|
||||||
stdOpen.apply(this, arguments);
|
stdOpen.apply(this, arguments);
|
||||||
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
||||||
|
this.setRequestHeader('Origin', nconf.get('url'));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
it('should get messages from room', function (done) {
|
||||||
socketModules.chats.getMessages({ uid: fooUid }, {
|
socketModules.chats.getMessages({ uid: fooUid }, {
|
||||||
uid: fooUid,
|
uid: fooUid,
|
||||||
|
|||||||
137
test/posts.js
137
test/posts.js
@@ -754,6 +754,143 @@ 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) {
|
after(function (done) {
|
||||||
db.emptydb(done);
|
db.emptydb(done);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe('socket.io', function () {
|
|||||||
this.open = function () {
|
this.open = function () {
|
||||||
stdOpen.apply(this, arguments);
|
stdOpen.apply(this, arguments);
|
||||||
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
this.setRequestHeader('Cookie', res.headers['set-cookie'][0].split(';')[0]);
|
||||||
|
this.setRequestHeader('Origin', nconf.get('url'));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,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) {
|
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) {
|
topics.reply({ uid: null, content: 'test post', tid: newTopic.tid }, function (err) {
|
||||||
assert.equal(err.message, '[[error:no-privileges]]');
|
assert.equal(err.message, '[[error:no-privileges]]');
|
||||||
@@ -174,9 +181,24 @@ describe('Topic\'s', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.getTopicData', function () {
|
|
||||||
it('should not receive errors', function (done) {
|
it('should not receive errors', function (done) {
|
||||||
topics.getTopicData(newTopic.tid, done);
|
topics.getTopicData(newTopic.tid, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get topic title by pid', function (done) {
|
||||||
|
topics.getTitleByPid(newPost.pid, function (err, title) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(title, topic.title);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get topic data by pid', function (done) {
|
||||||
|
topics.getTopicDataByPid(newPost.pid, function (err, data) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(data.tid, newTopic.tid);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -66,18 +66,18 @@ describe('Upload Controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload a profile picture', function (done) {
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
assert.equal(body.length, 1);
|
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();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should upload an image to a post', function (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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
@@ -90,7 +90,7 @@ describe('Upload Controllers', function () {
|
|||||||
|
|
||||||
it('should upload a file to a post', function (done) {
|
it('should upload a file to a post', function (done) {
|
||||||
meta.config.allowFileUploads = 1;
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
@@ -116,7 +116,7 @@ describe('Upload Controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload site logo', function (done) {
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
@@ -126,7 +126,7 @@ describe('Upload Controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload category image', function (done) {
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
@@ -136,7 +136,7 @@ describe('Upload Controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload favicon', function (done) {
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
@@ -146,7 +146,7 @@ describe('Upload Controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload touch icon', function (done) {
|
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.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(Array.isArray(body));
|
assert(Array.isArray(body));
|
||||||
|
|||||||
221
test/user.js
221
test/user.js
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
var path = require('path');
|
||||||
var nconf = require('nconf');
|
var nconf = require('nconf');
|
||||||
var request = require('request');
|
var request = require('request');
|
||||||
|
|
||||||
@@ -508,27 +509,34 @@ describe('User', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should upload profile picture', function (done) {
|
it('should upload profile picture', function (done) {
|
||||||
var path = require('path');
|
helpers.copyFile(
|
||||||
var picture = {
|
path.join(nconf.get('base_dir'), 'test/files/test.png'),
|
||||||
path: path.join(nconf.get('base_dir'), 'public', 'logo.png'),
|
path.join(nconf.get('base_dir'), 'test/files/test_copy.png'),
|
||||||
size: 7189,
|
function (err) {
|
||||||
name: 'logo.png',
|
assert.ifError(err);
|
||||||
};
|
var picture = {
|
||||||
User.uploadPicture(uid, picture, function (err, uploadedPicture) {
|
path: path.join(nconf.get('base_dir'), 'test/files/test_copy.png'),
|
||||||
assert.ifError(err);
|
size: 7189,
|
||||||
assert.equal(uploadedPicture.url, '/assets/uploads/profile/' + uid + '-profileimg.png');
|
name: 'test_copy.png',
|
||||||
assert.equal(uploadedPicture.path, path.join(nconf.get('base_dir'), 'public', 'uploads', 'profile', uid + '-profileimg.png'));
|
type: 'image/png',
|
||||||
done();
|
};
|
||||||
});
|
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) {
|
it('should return error if profile image uploads disabled', function (done) {
|
||||||
meta.config.allowProfileImageUploads = 0;
|
meta.config.allowProfileImageUploads = 0;
|
||||||
var path = require('path');
|
|
||||||
var picture = {
|
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,
|
size: 7189,
|
||||||
name: 'logo.png',
|
name: 'test.png',
|
||||||
|
type: 'image/png',
|
||||||
};
|
};
|
||||||
User.uploadPicture(uid, picture, function (err) {
|
User.uploadPicture(uid, picture, function (err) {
|
||||||
assert.equal(err.message, '[[error:profile-image-uploads-disabled]]');
|
assert.equal(err.message, '[[error:profile-image-uploads-disabled]]');
|
||||||
@@ -538,11 +546,11 @@ describe('User', function () {
|
|||||||
|
|
||||||
it('should return error if profile image is too big', function (done) {
|
it('should return error if profile image is too big', function (done) {
|
||||||
meta.config.allowProfileImageUploads = 1;
|
meta.config.allowProfileImageUploads = 1;
|
||||||
var path = require('path');
|
|
||||||
var picture = {
|
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,
|
size: 265000,
|
||||||
name: 'logo.png',
|
name: 'test.png',
|
||||||
|
type: 'image/png',
|
||||||
};
|
};
|
||||||
User.uploadPicture(uid, picture, function (err) {
|
User.uploadPicture(uid, picture, function (err) {
|
||||||
assert.equal(err.message, '[[error:file-too-big, 256]]');
|
assert.equal(err.message, '[[error:file-too-big, 256]]');
|
||||||
@@ -550,12 +558,11 @@ describe('User', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if profile image file has no extension', function (done) {
|
it('should return error if profile image has no mime type', function (done) {
|
||||||
var path = require('path');
|
|
||||||
var picture = {
|
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,
|
size: 7189,
|
||||||
name: 'logo',
|
name: 'test',
|
||||||
};
|
};
|
||||||
User.uploadPicture(uid, picture, function (err) {
|
User.uploadPicture(uid, picture, function (err) {
|
||||||
assert.equal(err.message, '[[error:invalid-image-extension]]');
|
assert.equal(err.message, '[[error:invalid-image-extension]]');
|
||||||
@@ -564,7 +571,7 @@ describe('User', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if no plugins listening for filter:uploadImage when uploading from url', function (done) {
|
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) {
|
User.uploadFromUrl(uid, url, function (err) {
|
||||||
assert.equal(err.message, '[[error:no-plugin]]');
|
assert.equal(err.message, '[[error:no-plugin]]');
|
||||||
done();
|
done();
|
||||||
@@ -575,7 +582,6 @@ describe('User', function () {
|
|||||||
var url = nconf.get('url') + '/favicon.ico';
|
var url = nconf.get('url') + '/favicon.ico';
|
||||||
|
|
||||||
function filterMethod(data, callback) {
|
function filterMethod(data, callback) {
|
||||||
data.foo += 5;
|
|
||||||
callback(null, data);
|
callback(null, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,11 +594,10 @@ describe('User', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if the file is too big when uploading from url', function (done) {
|
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;
|
meta.config.maximumProfileImageSize = 1;
|
||||||
|
|
||||||
function filterMethod(data, callback) {
|
function filterMethod(data, callback) {
|
||||||
data.foo += 5;
|
|
||||||
callback(null, data);
|
callback(null, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,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) {
|
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 = '';
|
meta.config.maximumProfileImageSize = '';
|
||||||
|
|
||||||
function filterMethod(data, callback) {
|
function filterMethod(data, callback) {
|
||||||
data.foo += 5;
|
|
||||||
callback(null, { url: url });
|
callback(null, { url: url });
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.registerHook('test-plugin', { hook: 'filter:uploadImage', method: filterMethod });
|
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.ifError(err);
|
||||||
assert.equal(uploadedPicture.url, url);
|
assert.equal(uploadedPicture, url);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -991,6 +1005,153 @@ 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) {
|
||||||
|
var email = 'invite1@test.com';
|
||||||
|
db.get('invitation:email:' + email, 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
after(function (done) {
|
after(function (done) {
|
||||||
db.emptydb(done);
|
db.emptydb(done);
|
||||||
|
|||||||
Reference in New Issue
Block a user