Merge remote-tracking branch 'origin/master' into develop

v1.18.x
Julian Lam 7 years ago
commit d1ccd78ac2

@ -59,7 +59,7 @@
"morgan": "^1.9.0", "morgan": "^1.9.0",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"nconf": "^0.9.1", "nconf": "^0.9.1",
"nodebb-plugin-composer-default": "6.0.7", "nodebb-plugin-composer-default": "6.0.8",
"nodebb-plugin-dbsearch": "2.0.9", "nodebb-plugin-dbsearch": "2.0.9",
"nodebb-plugin-emoji": "2.0.9", "nodebb-plugin-emoji": "2.0.9",
"nodebb-plugin-emoji-android": "2.0.0", "nodebb-plugin-emoji-android": "2.0.0",
@ -69,7 +69,7 @@
"nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-plugin-spam-be-gone": "0.5.1",
"nodebb-rewards-essentials": "0.0.9", "nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "5.0.0", "nodebb-theme-lavender": "5.0.0",
"nodebb-theme-persona": "7.2.8", "nodebb-theme-persona": "7.2.9",
"nodebb-theme-slick": "1.1.2", "nodebb-theme-slick": "1.1.2",
"nodebb-theme-vanilla": "8.1.4", "nodebb-theme-vanilla": "8.1.4",
"nodebb-widget-essentials": "4.0.1", "nodebb-widget-essentials": "4.0.1",

@ -3,12 +3,12 @@
"custom-css.description": "Zadejte vlastní deklarace CSS, které budou použity na všechny ostatních styly.", "custom-css.description": "Zadejte vlastní deklarace CSS, které budou použity na všechny ostatních styly.",
"custom-css.enable": "Povolit uživatelské CSS", "custom-css.enable": "Povolit uživatelské CSS",
"custom-js": "Custom Javascript", "custom-js": "Uživatelský Javascript",
"custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", "custom-js.description": "Zadejte zde váš javascriptový kód. Bude spuštěn, jakmile se stránka plně načte.",
"custom-js.enable": "Enable Custom Javascript", "custom-js.enable": "Povolit uživatelský Javascript",
"custom-header": "Uživatelská hlavička", "custom-header": "Uživatelská hlavička",
"custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code>&lt;head&gt;</code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.", "custom-header.description": "Zde zadejte uživatelské HTML (mimo Meta Tags, atd.), které bude připojeno k části značek <code>&lt;head&gt;</code> vašeho fóra.. Značky pro „script” jsou povoleny, ale nedoporučujeme je, neboť je dostupný <a href=\"#custom-header\" data-toggle=\"tab\">Uživatelský Javascript</a> .",
"custom-header.enable": "Povolit uživatelskou hlavičku", "custom-header.enable": "Povolit uživatelskou hlavičku",
"custom-css.livereload": "Povolit aktuální znovu načtení", "custom-css.livereload": "Povolit aktuální znovu načtení",

@ -7,5 +7,5 @@
"content": "Obsah", "content": "Obsah",
"posted": "Přidáno", "posted": "Přidáno",
"reply-to": "Odpovědět na \"%1\"", "reply-to": "Odpovědět na \"%1\"",
"content-editable": "You can click on individual content to edit before posting." "content-editable": "Kvůli úpravám a před odesláním příspěvku můžete klikat na obsah."
} }

@ -39,7 +39,7 @@
"section-appearance": "Vzhled", "section-appearance": "Vzhled",
"appearance/themes": "Motivy", "appearance/themes": "Motivy",
"appearance/skins": "Vzhledy", "appearance/skins": "Vzhledy",
"appearance/customise": "Custom Content (HTML/JS/CSS)", "appearance/customise": "Uživatelský obsah (HTML/JS/CSS)",
"section-extend": "Rozšířit", "section-extend": "Rozšířit",
"extend/plugins": "Rozšíření", "extend/plugins": "Rozšíření",
@ -65,7 +65,7 @@
"logout": "Odhlásit", "logout": "Odhlásit",
"view-forum": "Zobrazit fórum", "view-forum": "Zobrazit fórum",
"search.placeholder": "Search for settings", "search.placeholder": "Hledat nastavení",
"search.no-results": "Žádné výsledky…", "search.no-results": "Žádné výsledky…",
"search.search-forum": "Prohledat fórum pro <strong></strong>", "search.search-forum": "Prohledat fórum pro <strong></strong>",
"search.keep-typing": "Pište dále pro zobrazení výsledků…", "search.keep-typing": "Pište dále pro zobrazení výsledků…",

@ -2,5 +2,5 @@
"notifications": "Oznámení", "notifications": "Oznámení",
"welcome-notification": "Uvítání", "welcome-notification": "Uvítání",
"welcome-notification-link": "Odkaz na uvítání", "welcome-notification-link": "Odkaz na uvítání",
"welcome-notification-uid": "Welcome Notification User (UID)" "welcome-notification-uid": "Uvítání uživatele (UID)"
} }

@ -3,9 +3,9 @@
"enable": "Stránkovat témata a příspěvky namísto nekonečného posouvání", "enable": "Stránkovat témata a příspěvky namísto nekonečného posouvání",
"topics": "Stránkování témat", "topics": "Stránkování témat",
"posts-per-page": "Příspěvků na stránku", "posts-per-page": "Příspěvků na stránku",
"max-posts-per-page": "Maximum posts per page", "max-posts-per-page": "Maximální množství příspěvků na stránku",
"categories": "Stránkování kategorii", "categories": "Stránkování kategorii",
"topics-per-page": "Témat na stránku", "topics-per-page": "Témat na stránku",
"max-topics-per-page": "Maximum topics per page", "max-topics-per-page": "Maximální množství témat na stránku",
"initial-num-load": "Počáteční počet témat pro načtení u nepřečtených, posledních a polulárních" "initial-num-load": "Počáteční počet témat pro načtení u nepřečtených, posledních a polulárních"
} }

@ -3,8 +3,8 @@
"sorting.post-default": "Výchozí třídění příspěvků", "sorting.post-default": "Výchozí třídění příspěvků",
"sorting.oldest-to-newest": "Od nejstarších po nejnovější", "sorting.oldest-to-newest": "Od nejstarších po nejnovější",
"sorting.newest-to-oldest": "Od nejnovějších po nejstarší", "sorting.newest-to-oldest": "Od nejnovějších po nejstarší",
"sorting.most-votes": "Dle hlasování", "sorting.most-votes": "Dle počtu hlasů",
"sorting.most-posts": "Most Posts", "sorting.most-posts": "Dle počtu příspěvků",
"sorting.topic-default": "Výchozí třídění tématu", "sorting.topic-default": "Výchozí třídění tématu",
"restrictions": "Omezení příspěvků", "restrictions": "Omezení příspěvků",
"restrictions.post-queue": "Povolit frontu pro příspěvky", "restrictions.post-queue": "Povolit frontu pro příspěvky",

@ -19,8 +19,8 @@
"themes": "Motivy", "themes": "Motivy",
"disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu", "disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu",
"account-protection": "Ochrana účtu", "account-protection": "Ochrana účtu",
"admin-relogin-duration": "Admin relogin duration (minutes)", "admin-relogin-duration": "Doba pro opětovné přihlášení správce (minuty)",
"admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", "admin-relogin-duration-help": "Po nastavení počtu přístupu do správcovské části, bude vyžadováno opětovné přihlášení. Pro zakázání, nastavte na 0.",
"login-attempts": "Počet pokusů o přihlášení za hodinu", "login-attempts": "Počet pokusů o přihlášení za hodinu",
"login-attempts-help": "Překročí-li pokusy o přihlášení uživatele/ů tuto hranici, účet bude uzamknut na určený čas", "login-attempts-help": "Překročí-li pokusy o přihlášení uživatele/ů tuto hranici, účet bude uzamknut na určený čas",
"lockout-duration": "Délka blokování účtu (v minutách)", "lockout-duration": "Délka blokování účtu (v minutách)",

@ -30,7 +30,7 @@
"notif.chat.unsub.info": "Toto upozornění na chat vám bylo odesláno na základě vašeho nastavení odběru.", "notif.chat.unsub.info": "Toto upozornění na chat vám bylo odesláno na základě vašeho nastavení odběru.",
"notif.post.cta": "Klikněte zde pro přečtené celého tématu", "notif.post.cta": "Klikněte zde pro přečtené celého tématu",
"notif.post.unsub.info": "Toto upozornění na příspěvek vám bylo odesláno na základě vašeho nastavení odběru.", "notif.post.unsub.info": "Toto upozornění na příspěvek vám bylo odesláno na základě vašeho nastavení odběru.",
"notif.cta": "Click here to go to forum", "notif.cta": "Pro přejití na fórum, klikněte zde",
"test.text1": "Tento testovací e-mail slouží k ověření, že je e-mailer správně nastaven pro práci s NodeBB.", "test.text1": "Tento testovací e-mail slouží k ověření, že je e-mailer správně nastaven pro práci s NodeBB.",
"unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.", "unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.",
"banned.subject": "Byl jste zablokován od %1", "banned.subject": "Byl jste zablokován od %1",

@ -11,7 +11,7 @@
"invalid-uid": "Neplatné ID uživatele", "invalid-uid": "Neplatné ID uživatele",
"invalid-username": "Neplatné uživatelské jméno", "invalid-username": "Neplatné uživatelské jméno",
"invalid-email": "Neplatný e-mail", "invalid-email": "Neplatný e-mail",
"invalid-title": "Invalid title", "invalid-title": "Neplatný název",
"invalid-user-data": "Neplatná uživatelská data", "invalid-user-data": "Neplatná uživatelská data",
"invalid-password": "Neplatné heslo", "invalid-password": "Neplatné heslo",
"invalid-login-credentials": "Neplatné přihlašovací údaje", "invalid-login-credentials": "Neplatné přihlašovací údaje",
@ -81,7 +81,7 @@
"cant-ban-other-admins": "Nemůžete zablokovat jiné správce.", "cant-ban-other-admins": "Nemůžete zablokovat jiné správce.",
"cant-remove-last-admin": "Jste jediným správcem. Před vlastním odebráním oprávnění správce nejdříve přidejte jiného uživatele jako správce", "cant-remove-last-admin": "Jste jediným správcem. Před vlastním odebráním oprávnění správce nejdříve přidejte jiného uživatele jako správce",
"cant-delete-admin": "Před odstraněním účtu mu nejprve odeberte oprávnění správce.", "cant-delete-admin": "Před odstraněním účtu mu nejprve odeberte oprávnění správce.",
"invalid-image": "Invalid image", "invalid-image": "Neplatný obrázek",
"invalid-image-type": "Neplatný typ obrázku. Povolené typy jsou: %1", "invalid-image-type": "Neplatný typ obrázku. Povolené typy jsou: %1",
"invalid-image-extension": "Neplatná přípona obrázku", "invalid-image-extension": "Neplatná přípona obrázku",
"invalid-file-type": "Neplatný typ souboru. Povolené typy jsou: %1", "invalid-file-type": "Neplatný typ souboru. Povolené typy jsou: %1",
@ -119,13 +119,13 @@
"not-enough-reputation-to-downvote": "Nemáte dostatečnou reputaci pro vyjádření nesouhlasu u tohoto příspěvku", "not-enough-reputation-to-downvote": "Nemáte dostatečnou reputaci pro vyjádření nesouhlasu u tohoto příspěvku",
"not-enough-reputation-to-flag": "Pro označení tohoto příspěvku nemáte dostatečnou reputaci", "not-enough-reputation-to-flag": "Pro označení tohoto příspěvku nemáte dostatečnou reputaci",
"already-flagged": "Tento příspěvek jste již označil", "already-flagged": "Tento příspěvek jste již označil",
"self-vote": "You cannot vote on your own post", "self-vote": "U svého vlastního příspěvku nemůžete hlasovat",
"reload-failed": "Vyskytla se chyba v NodeBB při znovu načtení: \"%1\". NodeBB bude pokračovat v běhu na straně klienta, nicméně byste měl/a přenastavit zpět to, co jste udělal/a před opětovným načtením.", "reload-failed": "Vyskytla se chyba v NodeBB při znovu načtení: \"%1\". NodeBB bude pokračovat v běhu na straně klienta, nicméně byste měl/a přenastavit zpět to, co jste udělal/a před opětovným načtením.",
"registration-error": "Chyba při registraci", "registration-error": "Chyba při registraci",
"parse-error": "Při analýze odpovědi serveru nastala chyba", "parse-error": "Při analýze odpovědi serveru nastala chyba",
"wrong-login-type-email": "Pro přihlášení použijte vaši e-mailovou adresu", "wrong-login-type-email": "Pro přihlášení použijte vaši e-mailovou adresu",
"wrong-login-type-username": "Pro přihlášení použijte vaše uživatelské jméno", "wrong-login-type-username": "Pro přihlášení použijte vaše uživatelské jméno",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", "sso-registration-disabled": "Registrace byla zakázána pro účty - %1. Nejprve si zaregistrujte e-mailovou adresu",
"invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).", "invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).",
"no-session-found": "Nebyla nalezena relace s přihlášením.", "no-session-found": "Nebyla nalezena relace s přihlášením.",
"not-in-room": "Uživatel není přítomen v místnosti", "not-in-room": "Uživatel není přítomen v místnosti",
@ -135,5 +135,5 @@
"invalid-home-page-route": "Neplatná cesta k domovské stránkce", "invalid-home-page-route": "Neplatná cesta k domovské stránkce",
"invalid-session": "Nesoulad v relacích", "invalid-session": "Nesoulad v relacích",
"invalid-session-text": "Zdá se, že vše relace s přihlášením již není aktivní nebo již neodpovídá s relací na serveru. Obnovte prosím tuto stránku.", "invalid-session-text": "Zdá se, že vše relace s přihlášením již není aktivní nebo již neodpovídá s relací na serveru. Obnovte prosím tuto stránku.",
"no-topics-selected": "No topics selected!" "no-topics-selected": "Žádná vybraná témata."
} }

@ -54,11 +54,11 @@
"modal-body": "Zadejte váš důvod k označení %1 %2 pro kontrolu. Nebo použijte tlačítko je-li dostupné.", "modal-body": "Zadejte váš důvod k označení %1 %2 pro kontrolu. Nebo použijte tlačítko je-li dostupné.",
"modal-reason-spam": "Spam", "modal-reason-spam": "Spam",
"modal-reason-offensive": "Urážlivé", "modal-reason-offensive": "Urážlivé",
"modal-reason-other": "Other (specify below)", "modal-reason-other": "Jiné (popište níže)",
"modal-reason-custom": "Důvod ohlášení tohoto obsahu…", "modal-reason-custom": "Důvod ohlášení tohoto obsahu…",
"modal-submit": "Předat hlášení", "modal-submit": "Předat hlášení",
"modal-submit-success": "Obsah byl označen pro moderaci.", "modal-submit-success": "Obsah byl označen pro moderaci.",
"modal-submit-confirm": "Confirm Submission", "modal-submit-confirm": "Potvrdit hlášení",
"modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", "modal-submit-confirm-text": "Již jste zadal/a nějaký důvod. Jste si jist/a, že chcete nahlásit pomocí rychlé zprávy?",
"modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." "modal-submit-confirm-text-help": "Zaslání rychlé zprávy přepíše jiné zadané důvody."
} }

@ -8,8 +8,8 @@
"outgoing_link_message": "Opouštíte %1", "outgoing_link_message": "Opouštíte %1",
"continue_to": "Pokračovat na %1", "continue_to": "Pokračovat na %1",
"return_to": "Vrátit se na %1", "return_to": "Vrátit se na %1",
"new_notification": "Nové upozornění", "new_notification": "Nové oznámení",
"new_notification_from": "You have a new Notification from %1", "new_notification_from": "Máte nové upozornění od %1",
"you_have_unread_notifications": "Máte nepřečtená upozornění.", "you_have_unread_notifications": "Máte nepřečtená upozornění.",
"all": "Vše", "all": "Vše",
"topics": "Témata", "topics": "Témata",
@ -47,18 +47,18 @@
"email-confirmed-message": "Děkujeme za ověření vaší e-mailové adresy. Váš účet je nyní aktivní.", "email-confirmed-message": "Děkujeme za ověření vaší e-mailové adresy. Váš účet je nyní aktivní.",
"email-confirm-error-message": "Nastal problém s ověřením vaší e-mailové adresy. Kód je pravděpodobně neplatný nebo jeho platnost vypršela.", "email-confirm-error-message": "Nastal problém s ověřením vaší e-mailové adresy. Kód je pravděpodobně neplatný nebo jeho platnost vypršela.",
"email-confirm-sent": "Ověřovací e-mail odeslán.", "email-confirm-sent": "Ověřovací e-mail odeslán.",
"none": "None", "none": "Nic",
"notification_only": "Notification Only", "notification_only": "Jen oznámení",
"email_only": "Email Only", "email_only": "Jen e-mail",
"notification_and_email": "Notification & Email", "notification_and_email": "Oznámení a e-mail",
"notificationType_upvote": "When someone upvotes your post", "notificationType_upvote": "Vyjádří-li někdo souhlas s vaším příspěvkem",
"notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-topic": "Začne-li někdo sledovat příspěvky a téma",
"notificationType_new-reply": "When a new reply is posted in a topic you are watching", "notificationType_new-reply": "Bude-li přidán nový příspěvek v tématu, které sledujete",
"notificationType_follow": "When someone starts following you", "notificationType_follow": "Začne-li vás někdo sledovat",
"notificationType_new-chat": "When you receive a chat message", "notificationType_new-chat": "Obdržíte-li novou konverzační zprávu",
"notificationType_group-invite": "When you receive a group invite", "notificationType_group-invite": "Obdržíte-li pozvání ke skupině",
"notificationType_new-register": "When someone gets added to registration queue", "notificationType_new-register": "Bude-li někdo přidán do registrační fronty",
"notificationType_post-queue": "When a new post is queued", "notificationType_post-queue": "Bude-li přidán nový příspěvek do fronty",
"notificationType_new-post-flag": "When a post is flagged", "notificationType_new-post-flag": "Bude-li příspěvek označen",
"notificationType_new-user-flag": "When a user is flagged" "notificationType_new-user-flag": "Bude-li uživatel označen"
} }

@ -44,7 +44,7 @@
"account/bookmarks": "%1's zazáložkované příspěvky", "account/bookmarks": "%1's zazáložkované příspěvky",
"account/settings": "Uživatelské nastavení", "account/settings": "Uživatelské nastavení",
"account/watched": "Témata sledovaná uživatelem %1", "account/watched": "Témata sledovaná uživatelem %1",
"account/ignored": "Topics ignored by %1", "account/ignored": "Témata ignorovaná uživatelem %1",
"account/upvoted": "Souhlasí s příspěvkem %1", "account/upvoted": "Souhlasí s příspěvkem %1",
"account/downvoted": "Nesouhlasí s příspěvkem %1", "account/downvoted": "Nesouhlasí s příspěvkem %1",
"account/best": "Nejlepší příspěvky od %1", "account/best": "Nejlepší příspěvky od %1",

@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Jste si jist/a, že chcete toto téma obnovit?", "thread_tools.restore_confirm": "Jste si jist/a, že chcete toto téma obnovit?",
"thread_tools.purge": "Vyčistit téma", "thread_tools.purge": "Vyčistit téma",
"thread_tools.purge_confirm": "Jste si jist/a, že chcete vyčistit toto téma?", "thread_tools.purge_confirm": "Jste si jist/a, že chcete vyčistit toto téma?",
"thread_tools.merge_topics": "Merge Topics", "thread_tools.merge_topics": "Sloučit témata",
"thread_tools.merge": "Merge", "thread_tools.merge": "Sloučit",
"topic_move_success": "Toto téma bylo úspěšně přesunuto do %1", "topic_move_success": "Toto téma bylo úspěšně přesunuto do %1",
"post_delete_confirm": "Jste si jist/a, že chcete odstranit tento příspěvek?", "post_delete_confirm": "Jste si jist/a, že chcete odstranit tento příspěvek?",
"post_restore_confirm": "Jste si jist/a, že chcete obnovit tento příspěvek?", "post_restore_confirm": "Jste si jist/a, že chcete obnovit tento příspěvek?",
@ -91,7 +91,7 @@
"fork_pid_count": "Vybráno %1 příspěvek/ů", "fork_pid_count": "Vybráno %1 příspěvek/ů",
"fork_success": "Téma úspěšně rozděleno. Pro přejití na rozdělené téma, zde klikněte.", "fork_success": "Téma úspěšně rozděleno. Pro přejití na rozdělené téma, zde klikněte.",
"delete_posts_instruction": "Klikněte na příspěvek, který chcete odstranit/vyčistit", "delete_posts_instruction": "Klikněte na příspěvek, který chcete odstranit/vyčistit",
"merge_topics_instruction": "Click the topics you want to merge", "merge_topics_instruction": "Pro sloučení témat, klikněte na ně",
"composer.title_placeholder": "Zadejte název tématu…", "composer.title_placeholder": "Zadejte název tématu…",
"composer.handle_placeholder": "Jméno", "composer.handle_placeholder": "Jméno",
"composer.discard": "Zrušit", "composer.discard": "Zrušit",

@ -10,6 +10,6 @@
"all-topics": "Všechna témata", "all-topics": "Všechna témata",
"new-topics": "Nová témata", "new-topics": "Nová témata",
"watched-topics": "Sledovaná témata", "watched-topics": "Sledovaná témata",
"unreplied-topics": "Unreplied Topics", "unreplied-topics": "Neodpovězené témata",
"multiple-categories-selected": "Multiple Selected" "multiple-categories-selected": "Vícenásobný výběr"
} }

@ -25,7 +25,7 @@
"reputation": "Reputace", "reputation": "Reputace",
"bookmarks": "Záložky", "bookmarks": "Záložky",
"watched": "Sledován", "watched": "Sledován",
"ignored": "Ignored", "ignored": "Ignorován",
"followers": "Sledují ho", "followers": "Sledují ho",
"following": "Sleduje", "following": "Sleduje",
"aboutme": "O mně", "aboutme": "O mně",
@ -85,7 +85,7 @@
"has_no_posts": "Tento uživatel ještě nic nenapsal.", "has_no_posts": "Tento uživatel ještě nic nenapsal.",
"has_no_topics": "Tento uživatel ještě nezaložil žádné téma.", "has_no_topics": "Tento uživatel ještě nezaložil žádné téma.",
"has_no_watched_topics": "Tento uživatel zatím nesleduje žádná témata.", "has_no_watched_topics": "Tento uživatel zatím nesleduje žádná témata.",
"has_no_ignored_topics": "This user hasn't ignored any topics yet.", "has_no_ignored_topics": "Tento uživatel ještě neignoruje žádné témata.",
"has_no_upvoted_posts": "Tento uživatel zatím nevyjádřil souhlas u žádného příspěvku.", "has_no_upvoted_posts": "Tento uživatel zatím nevyjádřil souhlas u žádného příspěvku.",
"has_no_downvoted_posts": "Tento uživatel zatím nevyjádřil nesouhlas u žádného příspěvku.", "has_no_downvoted_posts": "Tento uživatel zatím nevyjádřil nesouhlas u žádného příspěvku.",
"has_no_voted_posts": "Tento uživatel nemá žádné hlasovací příspěvky", "has_no_voted_posts": "Tento uživatel nemá žádné hlasovací příspěvky",
@ -101,11 +101,11 @@
"outgoing-message-sound": "Zvuk odchozí zprávy", "outgoing-message-sound": "Zvuk odchozí zprávy",
"notification-sound": "Zvuk oznámení", "notification-sound": "Zvuk oznámení",
"no-sound": "Bez zvuku", "no-sound": "Bez zvuku",
"upvote-notif-freq": "Upvote Notification Frequency", "upvote-notif-freq": "Frekvence upozornění na souhlasy",
"upvote-notif-freq.all": "All Upvotes", "upvote-notif-freq.all": "Všechny souhlasy",
"upvote-notif-freq.everyTen": "Every Ten Upvotes", "upvote-notif-freq.everyTen": "Každý desátý souhlas",
"upvote-notif-freq.logarithmic": "On 10, 100, 1000...", "upvote-notif-freq.logarithmic": "Dle 10, 100, 1000...",
"upvote-notif-freq.disabled": "Disabled", "upvote-notif-freq.disabled": "Zakázáno",
"browsing": "Nastavení prohlížení", "browsing": "Nastavení prohlížení",
"open_links_in_new_tab": "Otevřít odchozí odkaz v nové záložce", "open_links_in_new_tab": "Otevřít odchozí odkaz v nové záložce",
"enable_topic_searching": "Povolit vyhledávání v tématu", "enable_topic_searching": "Povolit vyhledávání v tématu",
@ -126,9 +126,9 @@
"sso.title": "Služby jednotného přihlášení", "sso.title": "Služby jednotného přihlášení",
"sso.associated": "Přiřazeno k", "sso.associated": "Přiřazeno k",
"sso.not-associated": "Zde klikněte pro přiřazení k", "sso.not-associated": "Zde klikněte pro přiřazení k",
"sso.dissociate": "Dissociate", "sso.dissociate": "Odloučit",
"sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm-title": "Potvrdit odloučení",
"sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", "sso.dissociate-confirm": "Jste si jist/a, že chcete odloučit váš účet z %1?",
"info.latest-flags": "Poslední označené", "info.latest-flags": "Poslední označené",
"info.no-flags": "Nebyly nalezeny žádné označené příspěvky", "info.no-flags": "Nebyly nalezeny žádné označené příspěvky",
"info.ban-history": "Poslední historie blokovaných", "info.ban-history": "Poslední historie blokovaných",

@ -0,0 +1,10 @@
{
"administrators": "Administrators",
"global-moderators": "Global Moderators",
"no-global-moderators": "No Global Moderators",
"moderators-of-category": "%1 Moderators",
"no-moderators": "No Moderators",
"add-administrator": "Add Administrator",
"add-global-moderator": "Add Global Moderator",
"add-moderator": "Add Moderator"
}

@ -0,0 +1,4 @@
{
"global": "Global",
"global.no-users": "No user-specific global privileges."
}

@ -71,9 +71,15 @@
"alerts.lockout-reset-success": "Lockout(s) reset!", "alerts.lockout-reset-success": "Lockout(s) reset!",
"alerts.flag-reset-success": "Flags(s) reset!", "alerts.flag-reset-success": "Flags(s) reset!",
"alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!",
"alerts.make-admin-success": "User(s) are now administrators.", "alerts.make-admin-success": "User is now administrator.",
"alerts.confirm-remove-admin": "Do you really want to remove admins?", "alerts.confirm-remove-admin": "Do you really want to remove this administrator?",
"alerts.remove-admin-success": "User(s) are no longer administrators.", "alerts.remove-admin-success": "User is no longer administrator.",
"alerts.make-global-mod-success": "User is now global moderator.",
"alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?",
"alerts.remove-global-mod-success": "User is no longer global noderator",
"alerts.make-moderator-success": "User is now moderator.",
"alerts.confirm-remove-moderator": "Do you really want to remove this moderator?",
"alerts.remove-moderator-success": "User is no longer moderator.",
"alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?",
"alerts.validate-email-success": "Emails validated", "alerts.validate-email-success": "Emails validated",
"alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?",

@ -9,8 +9,10 @@
"section-manage": "Manage", "section-manage": "Manage",
"manage/categories": "Categories", "manage/categories": "Categories",
"manage/privileges": "Privileges",
"manage/tags": "Tags", "manage/tags": "Tags",
"manage/users": "Users", "manage/users": "Users",
"manage/admins-mods": "Admins & Mods",
"manage/registration": "Registration Queue", "manage/registration": "Registration Queue",
"manage/post-queue": "Post Queue", "manage/post-queue": "Post Queue",
"manage/groups": "Groups", "manage/groups": "Groups",

@ -3,9 +3,9 @@
"custom-css.description": "Entrez vos propres déclarations de CSS ici, elles seront appliquées après tous les autres styles.", "custom-css.description": "Entrez vos propres déclarations de CSS ici, elles seront appliquées après tous les autres styles.",
"custom-css.enable": "Activer les CSS personnalisés", "custom-css.enable": "Activer les CSS personnalisés",
"custom-js": "Custom Javascript", "custom-js": "Javascript personnalisé",
"custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", "custom-js.description": "Entrez votre Javascript ici. Celui-ci sera exécute après le chargement complet de la page.",
"custom-js.enable": "Enable Custom Javascript", "custom-js.enable": "Activer le Javascript personnalisé",
"custom-header": "En-tête personnalisé", "custom-header": "En-tête personnalisé",
"custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code>&lt;head&gt;</code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.", "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code>&lt;head&gt;</code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.",

@ -13,7 +13,7 @@
"reset.text1": "Abbiamo ricevuto una richiesta di reset della tua password, probabilmente perché l'hai dimenticata. Se non è così si prega di ignorare questa email.", "reset.text1": "Abbiamo ricevuto una richiesta di reset della tua password, probabilmente perché l'hai dimenticata. Se non è così si prega di ignorare questa email.",
"reset.text2": "Per confermare il reset della password per favore clicca il seguente link:", "reset.text2": "Per confermare il reset della password per favore clicca il seguente link:",
"reset.cta": "Clicca qui per resettare la tua password", "reset.cta": "Clicca qui per resettare la tua password",
"reset.notify.subject": "Possword modificata con successo.", "reset.notify.subject": "Password modificata con successo.",
"reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.", "reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.",
"reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.", "reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.",
"digest.notifications": "Hai una notifica non letta da %1:", "digest.notifications": "Hai una notifica non letta da %1:",
@ -33,9 +33,9 @@
"notif.cta": "Vai alla discussione", "notif.cta": "Vai alla discussione",
"test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.",
"unsub.cta": "Clicca qui per modificare queste impostazioni", "unsub.cta": "Clicca qui per modificare queste impostazioni",
"banned.subject": "You have been banned from %1", "banned.subject": "Sei stato bannato da %1",
"banned.text1": "The user %1 has been banned from %2.", "banned.text1": "%1 è stato bannato da %2",
"banned.text2": "This ban will last until %1.", "banned.text2": "Questo ban durerà fino a %1.",
"banned.text3": "This is the reason why you have been banned:", "banned.text3": "Il motivo del ban è:",
"closing": "Grazie!" "closing": "Grazie!"
} }

@ -11,10 +11,10 @@
"invalid-uid": "ID Utente non valido", "invalid-uid": "ID Utente non valido",
"invalid-username": "Nome utente non valido", "invalid-username": "Nome utente non valido",
"invalid-email": "Email non valida", "invalid-email": "Email non valida",
"invalid-title": "Invalid title", "invalid-title": "Titolo non Valido",
"invalid-user-data": "Dati Utente non validi", "invalid-user-data": "Dati Utente non validi",
"invalid-password": "Password non valida", "invalid-password": "Password non valida",
"invalid-login-credentials": "Invalid login credentials", "invalid-login-credentials": "Credenziali non Valide",
"invalid-username-or-password": "Si prega di specificare sia un nome utente che una password", "invalid-username-or-password": "Si prega di specificare sia un nome utente che una password",
"invalid-search-term": "Termine di ricerca non valido", "invalid-search-term": "Termine di ricerca non valido",
"csrf-invalid": "Non siamo riusciti a farti connettere, probabilmente perché la sessione è scaduta. Per favore riprova.", "csrf-invalid": "Non siamo riusciti a farti connettere, probabilmente perché la sessione è scaduta. Per favore riprova.",
@ -66,7 +66,7 @@
"content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.", "content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.",
"title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.", "title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.",
"title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.", "title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.",
"category-not-selected": "Category not selected.", "category-not-selected": "Categoria non selezionata.",
"too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo", "too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo",
"too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora",
"tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.",
@ -76,12 +76,12 @@
"still-uploading": "Per favore attendere il completamento degli uploads.", "still-uploading": "Per favore attendere il completamento degli uploads.",
"file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo", "file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo",
"guest-upload-disabled": "Il caricamento da ospite è stato disattivato", "guest-upload-disabled": "Il caricamento da ospite è stato disattivato",
"already-bookmarked": "You have already bookmarked this post", "already-bookmarked": "Hai già aggiunto questa discussione ai preferiti.",
"already-unbookmarked": "You have already unbookmarked this post", "already-unbookmarked": "Hai già rimosso questa discussione dai preferiti",
"cant-ban-other-admins": "Non puoi bannare altri amministratori!", "cant-ban-other-admins": "Non puoi bannare altri amministratori!",
"cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo", "cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo",
"cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.", "cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.",
"invalid-image": "Invalid image", "invalid-image": "Immagine non Valida",
"invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1", "invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1",
"invalid-image-extension": "Estensione immagine non valida", "invalid-image-extension": "Estensione immagine non valida",
"invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1", "invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1",
@ -109,7 +109,7 @@
"chat-disabled": "Il sistema di chat è stato disabilitato", "chat-disabled": "Il sistema di chat è stato disabilitato",
"too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.", "too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.",
"invalid-chat-message": "Messaggio chat non valido", "invalid-chat-message": "Messaggio chat non valido",
"chat-message-too-long": "Chat messages can not be longer than %1 characters.", "chat-message-too-long": "I messaggi in chat non possono superare i %1 caratteri.",
"cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio", "cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio",
"cant-remove-last-user": "Non puoi rimuovere l'ultimo utente", "cant-remove-last-user": "Non puoi rimuovere l'ultimo utente",
"cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio", "cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio",
@ -119,7 +119,7 @@
"not-enough-reputation-to-downvote": "Non hai i privilegi per votare negativamente questo post", "not-enough-reputation-to-downvote": "Non hai i privilegi per votare negativamente questo post",
"not-enough-reputation-to-flag": "Tu non hai abbastanza reputazione per segnalare questo Post", "not-enough-reputation-to-flag": "Tu non hai abbastanza reputazione per segnalare questo Post",
"already-flagged": "Hai già messo marcato questo post", "already-flagged": "Hai già messo marcato questo post",
"self-vote": "You cannot vote on your own post", "self-vote": "Non puoi votare la tua stessa discussione",
"reload-failed": "NodeBB ha incontrato un problema durante il ricaricamento: \"%1\". NodeBB continuerà a servire gli assets esistenti lato client, così puoi annullare quello che hai fatto prima di ricaricare.", "reload-failed": "NodeBB ha incontrato un problema durante il ricaricamento: \"%1\". NodeBB continuerà a servire gli assets esistenti lato client, così puoi annullare quello che hai fatto prima di ricaricare.",
"registration-error": "Errore nella registrazione", "registration-error": "Errore nella registrazione",
"parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server", "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server",
@ -135,5 +135,5 @@
"invalid-home-page-route": "Percorso della pagina iniziale non valido", "invalid-home-page-route": "Percorso della pagina iniziale non valido",
"invalid-session": "Discrepanza della sessione", "invalid-session": "Discrepanza della sessione",
"invalid-session-text": "Sembra che la tua sessione non sia più attiva, oppure non corrisponde con il server. Per favore ricarica la pagina.", "invalid-session-text": "Sembra che la tua sessione non sia più attiva, oppure non corrisponde con il server. Per favore ricarica la pagina.",
"no-topics-selected": "No topics selected!" "no-topics-selected": "Nessuna discussione selezionata!"
} }

@ -27,12 +27,12 @@
"quick-links": "Quick Links", "quick-links": "Quick Links",
"flagged-user": "Flagged User", "flagged-user": "Flagged User",
"view-profile": "View Profile", "view-profile": "Vedi Profilo",
"start-new-chat": "Start New Chat", "start-new-chat": "Start New Chat",
"go-to-target": "View Flag Target", "go-to-target": "View Flag Target",
"user-view": "View Profile", "user-view": "Vedi Profilo",
"user-edit": "Edit Profile", "user-edit": "Modifica Profilo",
"notes": "Flag Notes", "notes": "Flag Notes",
"add-note": "Add Note", "add-note": "Add Note",
@ -44,21 +44,21 @@
"state-all": "All states", "state-all": "All states",
"state-open": "New/Open", "state-open": "New/Open",
"state-wip": "Work in Progress", "state-wip": "Lavori in Corso",
"state-resolved": "Resolved", "state-resolved": "Risolto",
"state-rejected": "Rejected", "state-rejected": "Rejected",
"no-assignee": "Not Assigned", "no-assignee": "Non Assegnato",
"note-added": "Note Added", "note-added": "Note Added",
"modal-title": "Report Inappropriate Content", "modal-title": "Segnala Contenuto Inappropriato",
"modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.",
"modal-reason-spam": "Spam", "modal-reason-spam": "Spam",
"modal-reason-offensive": "Offensive", "modal-reason-offensive": "Offensivo",
"modal-reason-other": "Other (specify below)", "modal-reason-other": "Altro (Specificare di seguito)",
"modal-reason-custom": "Reason for reporting this content...", "modal-reason-custom": "Motivazione della segnalazione...",
"modal-submit": "Submit Report", "modal-submit": "Invia la Segnalazione",
"modal-submit-success": "Content has been flagged for moderation.", "modal-submit-success": "Il contenuto è stato segnalato.",
"modal-submit-confirm": "Confirm Submission", "modal-submit-confirm": "Conferma la Segnalazione",
"modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?",
"modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined."
} }

@ -104,6 +104,6 @@
"cookies.accept": "Ho capito!", "cookies.accept": "Ho capito!",
"cookies.learn_more": "Scopri di più", "cookies.learn_more": "Scopri di più",
"edited": "Modificato", "edited": "Modificato",
"disabled": "Disabled", "disabled": "Disabilitato",
"select": "Select" "select": "Seleziona"
} }

@ -27,7 +27,7 @@
"details.disableJoinRequests": "Disabilita le richieste d'adesione", "details.disableJoinRequests": "Disabilita le richieste d'adesione",
"details.grant": "Concedi / Rimuovi la Proprietà", "details.grant": "Concedi / Rimuovi la Proprietà",
"details.kick": "Espelli", "details.kick": "Espelli",
"details.kick_confirm": "Are you sure you want to remove this member from the group?", "details.kick_confirm": "Sei sicuro di voler rimuovere questo membro dal gruppo?",
"details.owner_options": "Amministratore del Grupo", "details.owner_options": "Amministratore del Grupo",
"details.group_name": "Nome Gruppo", "details.group_name": "Nome Gruppo",
"details.member_count": "Totale Membri", "details.member_count": "Totale Membri",
@ -53,6 +53,6 @@
"new-group.group_name": "Nome Gruppo:", "new-group.group_name": "Nome Gruppo:",
"upload-group-cover": "Carica copertina del gruppo", "upload-group-cover": "Carica copertina del gruppo",
"bulk-invite-instructions": "Inserisci una lista di nomi utente da invitare in questo gruppo separati da virgole", "bulk-invite-instructions": "Inserisci una lista di nomi utente da invitare in questo gruppo separati da virgole",
"bulk-invite": "Bulk Invite", "bulk-invite": "Invita in Massa",
"remove_group_cover_confirm": "Sei sicuro di voler rimuovere l'immagine copertina?" "remove_group_cover_confirm": "Sei sicuro di voler rimuovere l'immagine copertina?"
} }

@ -20,7 +20,7 @@
"chat.three_months": "3 Mesi", "chat.three_months": "3 Mesi",
"chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?", "chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?",
"chat.add-users-to-room": "Aggiungi utenti alla stanza", "chat.add-users-to-room": "Aggiungi utenti alla stanza",
"chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", "chat.confirm-chat-with-dnd-user": "Questo utente ha impostato il suo stato su Non Disturbare. Sei sicuro di voler iniziare una conversazione?",
"composer.compose": "Componi", "composer.compose": "Componi",
"composer.show_preview": "Visualizza Anteprima", "composer.show_preview": "Visualizza Anteprima",
"composer.hide_preview": "Nascondi Anteprima", "composer.hide_preview": "Nascondi Anteprima",

@ -29,9 +29,9 @@
"user_flagged_post_in": "<strong>%1</strong> ha segnalato un post in <strong>%2</strong>", "user_flagged_post_in": "<strong>%1</strong> ha segnalato un post in <strong>%2</strong>",
"user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un post in <strong>%3</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un post in <strong>%3</strong>",
"user_flagged_post_in_multiple": "<strong>%1</strong> ed altri %2 hanno segnalato un post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> ed altri %2 hanno segnalato un post in <strong>%3</strong>",
"user_flagged_user": "<strong>%1</strong> flagged a user profile (%2)", "user_flagged_user": "<strong>%1</strong> ha segnalato un utente (%2)",
"user_flagged_user_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a user profile (%3)", "user_flagged_user_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un utente (%3)",
"user_flagged_user_multiple": "<strong>%1</strong> and %2 others flagged a user profile (%3)", "user_flagged_user_multiple": "<strong>%1</strong> e altri %2 hanno segnalato un utente (%3)",
"user_posted_to": "<strong>%1</strong> ha postato una risposta a: <strong>%2</strong>", "user_posted_to": "<strong>%1</strong> ha postato una risposta a: <strong>%2</strong>",
"user_posted_to_dual": "<strong>%1</strong> e <strong>%2</strong> hanno postato una risposta su: <strong>%3</strong>", "user_posted_to_dual": "<strong>%1</strong> e <strong>%2</strong> hanno postato una risposta su: <strong>%3</strong>",
"user_posted_to_multiple": "<strong>%1</strong> ed altri %2 hanno postato una risposta su: <strong>%3</strong>", "user_posted_to_multiple": "<strong>%1</strong> ed altri %2 hanno postato una risposta su: <strong>%3</strong>",
@ -42,23 +42,23 @@
"new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.", "new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.",
"new_register_multiple": "Ci sono <strong>%1</strong> richieste di registrazione che attendono di essere esaminate.", "new_register_multiple": "Ci sono <strong>%1</strong> richieste di registrazione che attendono di essere esaminate.",
"flag_assigned_to_you": "<strong>Flag %1</strong> has been assigned to you", "flag_assigned_to_you": "<strong>Flag %1</strong> has been assigned to you",
"post_awaiting_review": "Post awaiting review", "post_awaiting_review": "Post in attesa di revisione",
"email-confirmed": "Email Confermata", "email-confirmed": "Email Confermata",
"email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.",
"email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.",
"email-confirm-sent": "Email di conferma inviata.", "email-confirm-sent": "Email di conferma inviata.",
"none": "None", "none": "Nessuna Notifica",
"notification_only": "Notification Only", "notification_only": "Solo Notifiche",
"email_only": "Email Only", "email_only": "Solo Email",
"notification_and_email": "Notification & Email", "notification_and_email": "Email e Notifica",
"notificationType_upvote": "When someone upvotes your post", "notificationType_upvote": "Quando il tuo post riceve un Mi Piace",
"notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-topic": "When someone you follow posts a topic",
"notificationType_new-reply": "When a new reply is posted in a topic you are watching", "notificationType_new-reply": "When a new reply is posted in a topic you are watching",
"notificationType_follow": "When someone starts following you", "notificationType_follow": "When someone starts following you",
"notificationType_new-chat": "When you receive a chat message", "notificationType_new-chat": "When you receive a chat message",
"notificationType_group-invite": "When you receive a group invite", "notificationType_group-invite": "When you receive a group invite",
"notificationType_new-register": "When someone gets added to registration queue", "notificationType_new-register": "When someone gets added to registration queue",
"notificationType_post-queue": "When a new post is queued", "notificationType_post-queue": "Quando un nuovo post è in attesa di revisione",
"notificationType_new-post-flag": "When a post is flagged", "notificationType_new-post-flag": "Quando un post viene segnalato",
"notificationType_new-user-flag": "When a user is flagged" "notificationType_new-user-flag": "Quando un utente viene segnalato"
} }

@ -7,7 +7,7 @@
"popular-alltime": "Discussioni più popolari di sempre", "popular-alltime": "Discussioni più popolari di sempre",
"recent": "Discussioni Recenti", "recent": "Discussioni Recenti",
"moderator-tools": "Moderator Tools", "moderator-tools": "Moderator Tools",
"flagged-content": "Flagged Content", "flagged-content": "Contenuti Segnalati",
"ip-blacklist": "Lista nera degli IP", "ip-blacklist": "Lista nera degli IP",
"post-queue": "Post Queue", "post-queue": "Post Queue",
"users/online": "Utenti Online", "users/online": "Utenti Online",
@ -44,7 +44,7 @@
"account/bookmarks": "%1 Post tra i favoriti", "account/bookmarks": "%1 Post tra i favoriti",
"account/settings": "Impostazioni Utente", "account/settings": "Impostazioni Utente",
"account/watched": "Discussioni osservate da %1", "account/watched": "Discussioni osservate da %1",
"account/ignored": "Topics ignored by %1", "account/ignored": "Discussioni ignorate da %1",
"account/upvoted": "Post apprezzati da %1", "account/upvoted": "Post apprezzati da %1",
"account/downvoted": "Post votati negativamente da %1", "account/downvoted": "Post votati negativamente da %1",
"account/best": "I migliori post di %1", "account/best": "I migliori post di %1",

@ -1,5 +1,5 @@
{ {
"register": "Registrazione", "register": "Registrati",
"cancel_registration": "Cancella Registrazione", "cancel_registration": "Cancella Registrazione",
"help.email": "Come opzione predefinita, il tuo indirizzo email non verrà reso pubblico.", "help.email": "Come opzione predefinita, il tuo indirizzo email non verrà reso pubblico.",
"help.username_restrictions": "Un nome utente unico, di almeno %1 caratteri e al massimo di %2. Gli altri utenti ti possono menzionare usando @<span id='yourUsername'>username</span>.", "help.username_restrictions": "Un nome utente unico, di almeno %1 caratteri e al massimo di %2. Gli altri utenti ti possono menzionare usando @<span id='yourUsername'>username</span>.",

@ -12,7 +12,7 @@
"reply-count": "Numero Risposte", "reply-count": "Numero Risposte",
"at-least": "Almeno", "at-least": "Almeno",
"at-most": "Al massimo", "at-most": "Al massimo",
"relevance": "Relevance", "relevance": "Rilevanza",
"post-time": "Ora invio", "post-time": "Ora invio",
"newer-than": "Più nuovi di", "newer-than": "Più nuovi di",
"older-than": "Più vecchi di", "older-than": "Più vecchi di",

@ -1,7 +1,7 @@
{ {
"success": "Riuscito", "success": "Riuscito",
"topic-post": "Hai postato correttamente.", "topic-post": "Hai postato correttamente.",
"post-queued": "Your post is queued for approval.", "post-queued": "La tua discussione è in attesa di approvazione.",
"authentication-successful": "Autenticazione Riuscita", "authentication-successful": "Autenticazione Riuscita",
"settings-saved": "Impostazioni salvate!" "settings-saved": "Impostazioni salvate!"
} }

@ -13,9 +13,9 @@
"notify_me": "Ricevi notifiche di nuove risposte in questa discussione", "notify_me": "Ricevi notifiche di nuove risposte in questa discussione",
"quote": "Cita", "quote": "Cita",
"reply": "Rispondi", "reply": "Rispondi",
"replies_to_this_post": "%1 Replies", "replies_to_this_post": "%1 Risposte",
"one_reply_to_this_post": "1 Reply", "one_reply_to_this_post": "1 Risposta",
"last_reply_time": "Last reply", "last_reply_time": "Ultima Risposta",
"reply-as-topic": "Topic risposta", "reply-as-topic": "Topic risposta",
"guest-login-reply": "Effettua il Log in per rispondere", "guest-login-reply": "Effettua il Log in per rispondere",
"edit": "Modifica", "edit": "Modifica",
@ -59,7 +59,7 @@
"thread_tools.unlock": "Sblocca Discussione", "thread_tools.unlock": "Sblocca Discussione",
"thread_tools.move": "Sposta Discussione", "thread_tools.move": "Sposta Discussione",
"thread_tools.move_all": "Sposta Tutto", "thread_tools.move_all": "Sposta Tutto",
"thread_tools.select_category": "Select Category", "thread_tools.select_category": "Seleziona Categoria",
"thread_tools.fork": "Dividi Discussione", "thread_tools.fork": "Dividi Discussione",
"thread_tools.delete": "Elimina Discussione", "thread_tools.delete": "Elimina Discussione",
"thread_tools.delete-posts": "Cancella post", "thread_tools.delete-posts": "Cancella post",
@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?", "thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?",
"thread_tools.purge": "Svuota Discussione", "thread_tools.purge": "Svuota Discussione",
"thread_tools.purge_confirm": "Sei sicuro di voler svuotare questa discussione?", "thread_tools.purge_confirm": "Sei sicuro di voler svuotare questa discussione?",
"thread_tools.merge_topics": "Merge Topics", "thread_tools.merge_topics": "Unisci le Discussioni",
"thread_tools.merge": "Merge", "thread_tools.merge": "Unisci",
"topic_move_success": "Questa discussione è stata correttamente spostata in %1", "topic_move_success": "Questa discussione è stata correttamente spostata in %1",
"post_delete_confirm": "Sei sicuro di voler cancellare questo post?", "post_delete_confirm": "Sei sicuro di voler cancellare questo post?",
"post_restore_confirm": "Sei sicuro di voler ripristinare questo post?", "post_restore_confirm": "Sei sicuro di voler ripristinare questo post?",
@ -91,7 +91,7 @@
"fork_pid_count": "%1 post selezionati", "fork_pid_count": "%1 post selezionati",
"fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.", "fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.",
"delete_posts_instruction": "Clicca sui post che vuoi cancellare/eliminare", "delete_posts_instruction": "Clicca sui post che vuoi cancellare/eliminare",
"merge_topics_instruction": "Click the topics you want to merge", "merge_topics_instruction": "Clicca sulle discussioni che vuoi unire",
"composer.title_placeholder": "Inserisci qui il titolo della discussione...", "composer.title_placeholder": "Inserisci qui il titolo della discussione...",
"composer.handle_placeholder": "Nome", "composer.handle_placeholder": "Nome",
"composer.discard": "Annulla", "composer.discard": "Annulla",

@ -10,6 +10,6 @@
"all-topics": "Tutte le Discussioni", "all-topics": "Tutte le Discussioni",
"new-topics": "Nuova Discussione", "new-topics": "Nuova Discussione",
"watched-topics": "Discussioni seguite", "watched-topics": "Discussioni seguite",
"unreplied-topics": "Nessuna Risposta", "unreplied-topics": "Discussioni Senza Risposta",
"multiple-categories-selected": "Multiple Selected" "multiple-categories-selected": "Più Categorie"
} }

@ -19,13 +19,13 @@
"location": "Località", "location": "Località",
"age": "Età", "age": "Età",
"joined": "Iscrizione", "joined": "Iscrizione",
"lastonline": "Ultima volta in linea", "lastonline": "Ultimo Accesso",
"profile": "Profilo", "profile": "Profilo",
"profile_views": "Visite al profilo", "profile_views": "Visite al profilo",
"reputation": "Reputazione", "reputation": "Reputazione",
"bookmarks": "Favoriti", "bookmarks": "Preferiti",
"watched": "Osservati", "watched": "Osservati",
"ignored": "Ignored", "ignored": "Ignorati",
"followers": "Da chi è seguito", "followers": "Da chi è seguito",
"following": "Chi segue", "following": "Chi segue",
"aboutme": "Su di me", "aboutme": "Su di me",
@ -61,7 +61,7 @@
"username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è <strong>%1</strong>", "username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è <strong>%1</strong>",
"password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password", "password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password",
"password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.", "password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.",
"weak_password": "Weak password.", "weak_password": "Password debole.",
"upload_picture": "Carica foto", "upload_picture": "Carica foto",
"upload_a_picture": "Carica una foto", "upload_a_picture": "Carica una foto",
"remove_uploaded_picture": "Elimina foto caricata", "remove_uploaded_picture": "Elimina foto caricata",
@ -85,7 +85,7 @@
"has_no_posts": "Questo utente non ha ancora scritto niente.", "has_no_posts": "Questo utente non ha ancora scritto niente.",
"has_no_topics": "Questo utente non ha ancora avviato discussioni.", "has_no_topics": "Questo utente non ha ancora avviato discussioni.",
"has_no_watched_topics": "Questo utente non sta osservando discussioni.", "has_no_watched_topics": "Questo utente non sta osservando discussioni.",
"has_no_ignored_topics": "This user hasn't ignored any topics yet.", "has_no_ignored_topics": "Questo utente non sta ignorando discussioni.",
"has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.", "has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.",
"has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post", "has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post",
"has_no_voted_posts": "Questo utente non ha post votati", "has_no_voted_posts": "Questo utente non ha post votati",
@ -94,18 +94,18 @@
"paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi", "paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi",
"topics_per_page": "Discussioni per Pagina", "topics_per_page": "Discussioni per Pagina",
"posts_per_page": "Post per Pagina", "posts_per_page": "Post per Pagina",
"max_items_per_page": "Maximum %1", "max_items_per_page": "Massimo %1",
"notification_sounds": "Riproduci un suono quando si riceve una notifica", "notification_sounds": "Riproduci un suono quando si riceve una notifica",
"notifications_and_sounds": "Notifiche e Suoni", "notifications_and_sounds": "Notifiche e Suoni",
"incoming-message-sound": "Suono messaggio in entrata", "incoming-message-sound": "Suono messaggio in entrata",
"outgoing-message-sound": "Suono messaggio in uscita", "outgoing-message-sound": "Suono messaggio in uscita",
"notification-sound": "Suono di notifica", "notification-sound": "Suono di notifica",
"no-sound": "Nessun suono", "no-sound": "Nessun suono",
"upvote-notif-freq": "Upvote Notification Frequency", "upvote-notif-freq": "Frequenza Notifiche dei Mi Piace ",
"upvote-notif-freq.all": "All Upvotes", "upvote-notif-freq.all": "Tutti i Mi Piace",
"upvote-notif-freq.everyTen": "Every Ten Upvotes", "upvote-notif-freq.everyTen": "Ogni Dieci Mi Piace",
"upvote-notif-freq.logarithmic": "On 10, 100, 1000...", "upvote-notif-freq.logarithmic": "Ogni 10, 100, 1000...",
"upvote-notif-freq.disabled": "Disabled", "upvote-notif-freq.disabled": "Disabilitate",
"browsing": "Impostazioni di Navigazione", "browsing": "Impostazioni di Navigazione",
"open_links_in_new_tab": "Apri i link web in una nuova pagina", "open_links_in_new_tab": "Apri i link web in una nuova pagina",
"enable_topic_searching": "Abilita la ricerca negli argomenti", "enable_topic_searching": "Abilita la ricerca negli argomenti",

@ -18,7 +18,7 @@
"reset.notify.text2": "Neem onmiddellijk contact met een beheerder op wanneer je hiervoor geen toestemming hebt gegeven.", "reset.notify.text2": "Neem onmiddellijk contact met een beheerder op wanneer je hiervoor geen toestemming hebt gegeven.",
"digest.notifications": "Er zijn ongelezen notificaties van %1:", "digest.notifications": "Er zijn ongelezen notificaties van %1:",
"digest.latest_topics": "De meest recente onderwerpen van %1", "digest.latest_topics": "De meest recente onderwerpen van %1",
"digest.cta": "Klik hier om deze website te bezoeken %1 ", "digest.cta": "Klik hier om %1 te bezoeken ",
"digest.unsub.info": "Deze samenvatting hebben we naar je verzonden omdat je dat hebt ingesteld.", "digest.unsub.info": "Deze samenvatting hebben we naar je verzonden omdat je dat hebt ingesteld.",
"digest.no_topics": "In de afgelopen %1 zijn er geen actieve onderwerpen geweest.", "digest.no_topics": "In de afgelopen %1 zijn er geen actieve onderwerpen geweest.",
"digest.day": "dag", "digest.day": "dag",
@ -30,7 +30,7 @@
"notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.", "notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.",
"notif.post.cta": "Klik hier om het volledige bericht te lezen", "notif.post.cta": "Klik hier om het volledige bericht te lezen",
"notif.post.unsub.info": "Deze notificatie is door ons verzonden vanwege gebruikersinstellingen voor abonnementen en berichten.", "notif.post.unsub.info": "Deze notificatie is door ons verzonden vanwege gebruikersinstellingen voor abonnementen en berichten.",
"notif.cta": "Click here to go to forum", "notif.cta": "Klik hier om naar het forum te gaan",
"test.text1": "Dit is een testbericht om te verifiëren dat NodeBB de e-mailberichtservice correct heeft opgezet.", "test.text1": "Dit is een testbericht om te verifiëren dat NodeBB de e-mailberichtservice correct heeft opgezet.",
"unsub.cta": "Klik hier om deze instellingen te wijzigen", "unsub.cta": "Klik hier om deze instellingen te wijzigen",
"banned.subject": "U bent verbannen van %1", "banned.subject": "U bent verbannen van %1",

@ -119,13 +119,13 @@
"not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen", "not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen",
"not-enough-reputation-to-flag": "Je hebt onvoldoende reputatie om dit bericht aan de beheerders te mogen melden", "not-enough-reputation-to-flag": "Je hebt onvoldoende reputatie om dit bericht aan de beheerders te mogen melden",
"already-flagged": "Je hebt deze post al gerapporteerd", "already-flagged": "Je hebt deze post al gerapporteerd",
"self-vote": "You cannot vote on your own post", "self-vote": "Het is niet mogelijk om op je eigen bericht te stemmen",
"reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.", "reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.",
"registration-error": "Fout tijdens registratie", "registration-error": "Fout tijdens registratie",
"parse-error": "Tijdens het verwerken van het antwoord van de server is er iets misgegaan.", "parse-error": "Tijdens het verwerken van het antwoord van de server is er iets misgegaan.",
"wrong-login-type-email": "Gebruik je e-mailadres om in te loggen", "wrong-login-type-email": "Gebruik je e-mailadres om in te loggen",
"wrong-login-type-username": "Gebruik je gebruikersnaam om in te loggen", "wrong-login-type-username": "Gebruik je gebruikersnaam om in te loggen",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", "sso-registration-disabled": "Registratie is uitgeschakeld voor %1 accounts, registreer eerst met een e-mailadres",
"invite-maximum-met": "Je heb het maximum aantal mensen uitgenodigd (%1 van de %2).", "invite-maximum-met": "Je heb het maximum aantal mensen uitgenodigd (%1 van de %2).",
"no-session-found": "Geen login sessie gevonden!", "no-session-found": "Geen login sessie gevonden!",
"not-in-room": "Gebruiker niet in de chat", "not-in-room": "Gebruiker niet in de chat",
@ -135,5 +135,5 @@
"invalid-home-page-route": "Onbekende homepage route", "invalid-home-page-route": "Onbekende homepage route",
"invalid-session": "Verkeerde sessie combinatie", "invalid-session": "Verkeerde sessie combinatie",
"invalid-session-text": "Het lijkt erop dat je login sessie niet meer actief is of niet langer synchroon is met de server. Ververs de pagina.", "invalid-session-text": "Het lijkt erop dat je login sessie niet meer actief is of niet langer synchroon is met de server. Ververs de pagina.",
"no-topics-selected": "No topics selected!" "no-topics-selected": "Geen onderwerpen geselecteerd!"
} }

@ -9,7 +9,7 @@
"continue_to": "Door naar %1", "continue_to": "Door naar %1",
"return_to": "Terug naar %1", "return_to": "Terug naar %1",
"new_notification": "Nieuwe notificatie", "new_notification": "Nieuwe notificatie",
"new_notification_from": "You have a new Notification from %1", "new_notification_from": "Je hebt een nieuwe notificatie van %1",
"you_have_unread_notifications": "Je hebt nieuwe notificaties.", "you_have_unread_notifications": "Je hebt nieuwe notificaties.",
"all": "Alles", "all": "Alles",
"topics": "Onderwerpen", "topics": "Onderwerpen",
@ -47,18 +47,18 @@
"email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Je account is nu volledig geactiveerd.", "email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Je account is nu volledig geactiveerd.",
"email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.", "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.",
"email-confirm-sent": "Bevestigingsmail verstuurd.", "email-confirm-sent": "Bevestigingsmail verstuurd.",
"none": "None", "none": "Geen",
"notification_only": "Notification Only", "notification_only": "Alleen notificatie",
"email_only": "Email Only", "email_only": "Alleen e-mail",
"notification_and_email": "Notification & Email", "notification_and_email": "Notificatie & e-mail",
"notificationType_upvote": "When someone upvotes your post", "notificationType_upvote": "Als iemand positief stemt voor je bericht",
"notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-topic": "Wanneer iemand die jij volgt een onderwerp post",
"notificationType_new-reply": "When a new reply is posted in a topic you are watching", "notificationType_new-reply": "Als een nieuwe reactie komt op een onderwerp dat je volgt",
"notificationType_follow": "When someone starts following you", "notificationType_follow": "Als iemand begint met jou te volgen",
"notificationType_new-chat": "When you receive a chat message", "notificationType_new-chat": "Als je een chat-bericht ontvangt",
"notificationType_group-invite": "When you receive a group invite", "notificationType_group-invite": "Als je een uitnodiging voor een groep ontvangt",
"notificationType_new-register": "When someone gets added to registration queue", "notificationType_new-register": "Als iemand wordt toegevoegd aan een registratiewachtrij",
"notificationType_post-queue": "When a new post is queued", "notificationType_post-queue": "Als een bericht aan de wachtrij wordt toegevoegd",
"notificationType_new-post-flag": "When a post is flagged", "notificationType_new-post-flag": "Als een bericht wordt gevlagd",
"notificationType_new-user-flag": "When a user is flagged" "notificationType_new-user-flag": "Als een gebruiker wordt gevlagd"
} }

@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?", "thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?",
"thread_tools.purge": "Wis onderwerp ", "thread_tools.purge": "Wis onderwerp ",
"thread_tools.purge_confirm": "Weet je zeker dat je dit onderwerp wil verwijderen?", "thread_tools.purge_confirm": "Weet je zeker dat je dit onderwerp wil verwijderen?",
"thread_tools.merge_topics": "Merge Topics", "thread_tools.merge_topics": "Onderwerpen samenvoegen",
"thread_tools.merge": "Merge", "thread_tools.merge": "Samenvoegen",
"topic_move_success": "Verplaatsen van onderwerp naar %1 succesvol", "topic_move_success": "Verplaatsen van onderwerp naar %1 succesvol",
"post_delete_confirm": "Is het absoluut de bedoeling dit bericht te verwijderen?", "post_delete_confirm": "Is het absoluut de bedoeling dit bericht te verwijderen?",
"post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?", "post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?",
@ -91,7 +91,7 @@
"fork_pid_count": "%1 bericht(en) geselecteerd", "fork_pid_count": "%1 bericht(en) geselecteerd",
"fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.", "fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.",
"delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden", "delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden",
"merge_topics_instruction": "Click the topics you want to merge", "merge_topics_instruction": "Klik op de onderwerpen die samengevoegd moeten worden",
"composer.title_placeholder": "Voer hier de titel van het onderwerp in...", "composer.title_placeholder": "Voer hier de titel van het onderwerp in...",
"composer.handle_placeholder": "Naam", "composer.handle_placeholder": "Naam",
"composer.discard": "Annuleren", "composer.discard": "Annuleren",

@ -101,11 +101,11 @@
"outgoing-message-sound": "Uitgaand bericht geluid", "outgoing-message-sound": "Uitgaand bericht geluid",
"notification-sound": "Notificatie geluid", "notification-sound": "Notificatie geluid",
"no-sound": "Geen geluid", "no-sound": "Geen geluid",
"upvote-notif-freq": "Upvote Notification Frequency", "upvote-notif-freq": "Notificatie frequentie voor Upvotes",
"upvote-notif-freq.all": "All Upvotes", "upvote-notif-freq.all": "Alle Upvotes",
"upvote-notif-freq.everyTen": "Every Ten Upvotes", "upvote-notif-freq.everyTen": "Elke tien Upvotes",
"upvote-notif-freq.logarithmic": "On 10, 100, 1000...", "upvote-notif-freq.logarithmic": "Bij 10, 100, 1000...",
"upvote-notif-freq.disabled": "Disabled", "upvote-notif-freq.disabled": "Uitgeschakeld",
"browsing": "Instellingen voor bladeren", "browsing": "Instellingen voor bladeren",
"open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad", "open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad",
"enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken", "enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken",

@ -14,6 +14,7 @@
@import "./manage/groups"; @import "./manage/groups";
@import "./manage/registration"; @import "./manage/registration";
@import "./manage/users"; @import "./manage/users";
@import "./manage/admins-mods";
@import "./appearance/customise"; @import "./appearance/customise";
@import "./appearance/themes"; @import "./appearance/themes";
@import "./extend/plugins"; @import "./extend/plugins";

@ -0,0 +1,27 @@
.admins-mods {
.user-card {
margin-right: 10px;
padding: 2px;
}
.remove-user-icon {
margin-right: 5px;
margin-left: 5px;
}
.category-depth-1 {
margin-left: 30px;
}
.category-depth-2 {
margin-left: 60px;
}
.category-depth-3 {
margin-left: 90px;
}
.category-depth-4 {
margin-left: 120px;
}
.category-depth-5 {
margin-left: 150px;
}
}

@ -0,0 +1,142 @@
'use strict';
define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], function (translator, Benchpress, autocomplete) {
var AdminsMods = {};
AdminsMods.init = function () {
autocomplete.user($('#admin-search'), function (ev, ui) {
socket.emit('admin.user.makeAdmins', [ui.item.user.uid], function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.make-admin-success]]');
$('#admin-search').val('');
if ($('.administrator-area [data-uid="' + ui.item.user.uid + '"]').length) {
return;
}
app.parseAndTranslate('admin/manage/admins-mods', 'admins.members', { admins: { members: [ui.item.user] } }, function (html) {
$('.administrator-area').prepend(html);
});
});
});
$('.administrator-area').on('click', '.remove-user-icon', function () {
var userCard = $(this).parents('[data-uid]');
var uid = userCard.attr('data-uid');
if (parseInt(uid, 10) === parseInt(app.user.uid, 10)) {
return app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]');
}
bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) {
if (confirm) {
socket.emit('admin.user.removeAdmins', [uid], function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.remove-admin-success]]');
userCard.remove();
});
}
});
});
autocomplete.user($('#global-mod-search'), function (ev, ui) {
socket.emit('admin.groups.join', {
groupName: 'Global Moderators',
uid: ui.item.user.uid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.make-global-mod-success]]');
$('#global-mod-search').val('');
if ($('.global-moderator-area [data-uid="' + ui.item.user.uid + '"]').length) {
return;
}
app.parseAndTranslate('admin/manage/admins-mods', 'globalMods.members', { globalMods: { members: [ui.item.user] } }, function (html) {
$('.global-moderator-area').prepend(html);
$('#no-global-mods-warning').addClass('hidden');
});
});
});
$('.global-moderator-area').on('click', '.remove-user-icon', function () {
var userCard = $(this).parents('[data-uid]');
var uid = userCard.attr('data-uid');
bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-global-mod]]', function (confirm) {
if (confirm) {
socket.emit('admin.groups.leave', { uid: uid, groupName: 'Global Moderators' }, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.remove-global-mod-success]]');
userCard.remove();
if (!$('.global-moderator-area').children().length) {
$('#no-global-mods-warning').removeClass('hidden');
}
});
}
});
});
autocomplete.user($('.moderator-search'), function (ev, ui) {
var input = $(ev.target);
var cid = $(ev.target).attr('data-cid');
socket.emit('admin.categories.setPrivilege', {
cid: cid,
privilege: ['moderate'],
set: true,
member: ui.item.user.uid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.make-moderator-success]]');
input.val('');
if ($('.moderator-area[data-cid="' + cid + '"] [data-uid="' + ui.item.user.uid + '"]').length) {
return;
}
app.parseAndTranslate('admin/manage/admins-mods', 'globalMods', { globalMods: [ui.item.user] }, function (html) {
$('.moderator-area[data-cid="' + cid + '"]').prepend(html);
$('.no-moderator-warning[data-cid="' + cid + '"]').addClass('hidden');
});
});
});
$('.moderator-area').on('click', '.remove-user-icon', function () {
var moderatorArea = $(this).parents('[data-cid]');
var cid = moderatorArea.attr('data-cid');
var userCard = $(this).parents('[data-uid]');
var uid = userCard.attr('data-uid');
bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-moderator]]', function (confirm) {
if (confirm) {
socket.emit('admin.categories.setPrivilege', {
cid: cid,
privilege: ['moderate'],
set: false,
member: uid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('[[admin/manage/users:alerts.remove-moderator-success]]');
userCard.remove();
if (!moderatorArea.children().length) {
$('.no-moderator-warning[data-cid="' + cid + '"]').removeClass('hidden');
}
});
}
});
});
};
return AdminsMods;
});

@ -1,6 +1,5 @@
'use strict'; 'use strict';
define('admin/manage/category', [ define('admin/manage/category', [
'uploader', 'uploader',
'iconSelect', 'iconSelect',
@ -8,8 +7,7 @@ define('admin/manage/category', [
'autocomplete', 'autocomplete',
'translator', 'translator',
'categorySelector', 'categorySelector',
'benchpress', ], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector) {
], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector, Benchpress) {
var Category = {}; var Category = {};
var modified_categories = {}; var modified_categories = {};
@ -100,7 +98,7 @@ define('admin/manage/category', [
}); });
$('.copy-settings').on('click', function () { $('.copy-settings').on('click', function () {
selectCategoryModal(function (cid) { categorySelector.modal(function (cid) {
socket.emit('admin.categories.copySettingsFrom', { fromCid: cid, toCid: ajaxify.data.category.cid }, function (err) { socket.emit('admin.categories.copySettingsFrom', { fromCid: cid, toCid: ajaxify.data.category.cid }, function (err) {
if (err) { if (err) {
return app.alertError(err.message); return app.alertError(err.message);
@ -169,8 +167,6 @@ define('admin/manage/category', [
$('button[data-action="setParent"]').removeClass('hide'); $('button[data-action="setParent"]').removeClass('hide');
}); });
}); });
Category.setupPrivilegeTable();
}; };
function modified(el) { function modified(el) {
@ -208,102 +204,12 @@ define('admin/manage/category', [
}); });
} }
Category.setupPrivilegeTable = function () {
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
var checkboxEl = $(this);
var privilege = checkboxEl.parent().attr('data-privilege');
var state = checkboxEl.prop('checked');
var rowEl = checkboxEl.parents('tr');
var member = rowEl.attr('data-group-name') || rowEl.attr('data-uid');
var isPrivate = parseInt(rowEl.attr('data-private') || 0, 10);
var isGroup = rowEl.attr('data-group-name') !== undefined;
if (member) {
if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) {
bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) {
if (confirm) {
Category.setPrivilege(member, privilege, state, checkboxEl);
} else {
checkboxEl.prop('checked', !checkboxEl.prop('checked'));
}
});
} else {
Category.setPrivilege(member, privilege, state, checkboxEl);
}
} else {
app.alertError('[[error:invalid-data]]');
}
});
$('.privilege-table-container').on('click', '[data-action="search.user"]', Category.addUserToPrivilegeTable);
$('.privilege-table-container').on('click', '[data-action="search.group"]', Category.addGroupToPrivilegeTable);
$('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Category.copyPrivilegesToChildren);
$('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Category.copyPrivilegesFromCategory);
Category.exposeAssumedPrivileges();
};
Category.refreshPrivilegeTable = function () {
socket.emit('admin.categories.getPrivilegeSettings', ajaxify.data.category.cid, function (err, privileges) {
if (err) {
return app.alertError(err.message);
}
Benchpress.parse('admin/partials/categories/privileges', {
privileges: privileges,
}, function (html) {
translator.translate(html, function (html) {
$('.privilege-table-container').html(html);
Category.exposeAssumedPrivileges();
});
});
});
};
Category.exposeAssumedPrivileges = function () {
/*
If registered-users has a privilege enabled, then all users and groups of that privilege
should be assumed to have that privilege as well, even if not set in the db, so reflect
this arrangement in the table
*/
var privs = [];
$('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) {
if ($(el).find('input').prop('checked')) {
privs.push(el.getAttribute('data-privilege'));
}
});
for (var x = 0, numPrivs = privs.length; x < numPrivs; x += 1) {
var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input');
inputs.each(function (idx, el) {
if (!el.checked) {
el.indeterminate = true;
}
});
}
};
Category.setPrivilege = function (member, privilege, state, checkboxEl) {
socket.emit('admin.categories.setPrivilege', {
cid: ajaxify.data.category.cid,
privilege: privilege,
set: state,
member: member,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
checkboxEl.replaceWith('<i class="fa fa-spin fa-spinner"></i>');
Category.refreshPrivilegeTable();
});
};
Category.launchParentSelector = function () { Category.launchParentSelector = function () {
var categories = ajaxify.data.allCategories.filter(function (category) { var categories = ajaxify.data.allCategories.filter(function (category) {
return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10);
}); });
selectCategoryModal(categories, function (parentCid) { categorySelector.modal(categories, function (parentCid) {
var payload = {}; var payload = {};
payload[ajaxify.data.category.cid] = { payload[ajaxify.data.category.cid] = {
@ -327,117 +233,5 @@ define('admin/manage/category', [
}); });
}; };
Category.addUserToPrivilegeTable = function () {
var modal = bootbox.dialog({
title: '[[admin/manage/categories:alert.find-user]]',
message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.user-search]]" />',
show: true,
});
modal.on('shown.bs.modal', function () {
var inputEl = modal.find('input');
autocomplete.user(inputEl, function (ev, ui) {
socket.emit('admin.categories.setPrivilege', {
cid: ajaxify.data.category.cid,
privilege: ['find', 'read', 'topics:read'],
set: true,
member: ui.item.user.uid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
Category.refreshPrivilegeTable();
modal.modal('hide');
});
});
});
};
Category.addGroupToPrivilegeTable = function () {
var modal = bootbox.dialog({
title: '[[admin/manage/categories:alert.find-group]]',
message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.group-search]]" />',
show: true,
});
modal.on('shown.bs.modal', function () {
var inputEl = modal.find('input');
autocomplete.group(inputEl, function (ev, ui) {
socket.emit('admin.categories.setPrivilege', {
cid: ajaxify.data.category.cid,
privilege: ['groups:find', 'groups:read', 'groups:topics:read'],
set: true,
member: ui.item.group.name,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
Category.refreshPrivilegeTable();
modal.modal('hide');
});
});
});
};
Category.copyPrivilegesToChildren = function () {
socket.emit('admin.categories.copyPrivilegesToChildren', ajaxify.data.category.cid, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('Privileges copied!');
});
};
Category.copyPrivilegesFromCategory = function () {
selectCategoryModal(function (cid) {
socket.emit('admin.categories.copyPrivilegesFrom', { toCid: ajaxify.data.category.cid, fromCid: cid }, function (err) {
if (err) {
return app.alertError(err.message);
}
ajaxify.refresh();
});
});
};
function selectCategoryModal(categories, callback) {
if (typeof categories === 'function') {
callback = categories;
categories = ajaxify.data.allCategories;
}
Benchpress.parse('admin/partials/categories/select-category', {
categories: categories,
}, function (html) {
translator.translate(html, function (html) {
var modal = bootbox.dialog({
title: '[[modules:composer.select_category]]',
message: html,
buttons: {
save: {
label: '[[global:select]]',
className: 'btn-primary',
callback: submit,
},
},
});
categorySelector.init(modal.find('[component="category-selector"]'));
function submit(ev) {
ev.preventDefault();
var selectedCategory = categorySelector.getSelectedCategory();
if (selectedCategory) {
callback(selectedCategory.cid);
modal.modal('hide');
}
return false;
}
modal.find('form').on('submit', submit);
});
});
}
return Category; return Category;
}); });

@ -0,0 +1,194 @@
'use strict';
define('admin/manage/privileges', [
'autocomplete',
'translator',
'benchpress',
'categorySelector',
], function (autocomplete, translator, Benchpress, categorySelector) {
var Privileges = {};
var cid;
Privileges.init = function () {
cid = ajaxify.data.cid || 0;
$('#category-selector').on('change', function () {
var val = $(this).val();
ajaxify.go('admin/manage/privileges/' + (val === 'global' ? '' : $(this).val()));
});
Privileges.setupPrivilegeTable();
};
Privileges.setupPrivilegeTable = function () {
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
var checkboxEl = $(this);
var privilege = checkboxEl.parent().attr('data-privilege');
var state = checkboxEl.prop('checked');
var rowEl = checkboxEl.parents('tr');
var member = rowEl.attr('data-group-name') || rowEl.attr('data-uid');
var isPrivate = parseInt(rowEl.attr('data-private') || 0, 10);
var isGroup = rowEl.attr('data-group-name') !== undefined;
if (member) {
if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) {
bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) {
if (confirm) {
Privileges.setPrivilege(member, privilege, state, checkboxEl);
} else {
checkboxEl.prop('checked', !checkboxEl.prop('checked'));
}
});
} else {
Privileges.setPrivilege(member, privilege, state, checkboxEl);
}
} else {
app.alertError('[[error:invalid-data]]');
}
});
$('.privilege-table-container').on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable);
$('.privilege-table-container').on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable);
$('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Privileges.copyPrivilegesToChildren);
$('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Privileges.copyPrivilegesFromCategory);
Privileges.exposeAssumedPrivileges();
};
Privileges.refreshPrivilegeTable = function () {
socket.emit('admin.categories.getPrivilegeSettings', cid, function (err, privileges) {
if (err) {
return app.alertError(err.message);
}
var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/partials/global/privileges';
Benchpress.parse(tpl, {
privileges: privileges,
}, function (html) {
translator.translate(html, function (html) {
$('.privilege-table-container').html(html);
Privileges.exposeAssumedPrivileges();
});
});
});
};
Privileges.exposeAssumedPrivileges = function () {
/*
If registered-users has a privilege enabled, then all users and groups of that privilege
should be assumed to have that privilege as well, even if not set in the db, so reflect
this arrangement in the table
*/
var privs = [];
$('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) {
if ($(el).find('input').prop('checked')) {
privs.push(el.getAttribute('data-privilege'));
}
});
for (var x = 0, numPrivs = privs.length; x < numPrivs; x += 1) {
var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input');
inputs.each(function (idx, el) {
if (!el.checked) {
el.indeterminate = true;
}
});
}
};
Privileges.setPrivilege = function (member, privilege, state, checkboxEl) {
socket.emit('admin.categories.setPrivilege', {
cid: cid,
privilege: privilege,
set: state,
member: member,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
checkboxEl.replaceWith('<i class="fa fa-spin fa-spinner"></i>');
Privileges.refreshPrivilegeTable();
});
};
Privileges.addUserToPrivilegeTable = function () {
var modal = bootbox.dialog({
title: '[[admin/manage/categories:alert.find-user]]',
message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.user-search]]" />',
show: true,
});
modal.on('shown.bs.modal', function () {
var inputEl = modal.find('input');
autocomplete.user(inputEl, function (ev, ui) {
var defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat'];
socket.emit('admin.categories.setPrivilege', {
cid: cid,
privilege: defaultPrivileges,
set: true,
member: ui.item.user.uid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
Privileges.refreshPrivilegeTable();
modal.modal('hide');
});
});
});
};
Privileges.addGroupToPrivilegeTable = function () {
var modal = bootbox.dialog({
title: '[[admin/manage/categories:alert.find-group]]',
message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.group-search]]" />',
show: true,
});
modal.on('shown.bs.modal', function () {
var inputEl = modal.find('input');
autocomplete.group(inputEl, function (ev, ui) {
var defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat'];
socket.emit('admin.categories.setPrivilege', {
cid: cid,
privilege: defaultPrivileges,
set: true,
member: ui.item.group.name,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
Privileges.refreshPrivilegeTable();
modal.modal('hide');
});
});
});
};
Privileges.copyPrivilegesToChildren = function () {
socket.emit('admin.categories.copyPrivilegesToChildren', cid, function (err) {
if (err) {
return app.alertError(err.message);
}
app.alertSuccess('Privileges copied!');
});
};
Privileges.copyPrivilegesFromCategory = function () {
categorySelector.modal(function (fromCid) {
socket.emit('admin.categories.copyPrivilegesFrom', { toCid: cid, fromCid: fromCid }, function (err) {
if (err) {
return app.alertError(err.message);
}
ajaxify.refresh();
});
});
};
return Privileges;
});

@ -127,36 +127,6 @@ define('admin/manage/users', ['translator', 'benchpress'], function (translator,
socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]')); socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]'));
}); });
$('.admin-user').on('click', function () {
var uids = getSelectedUids();
if (!uids.length) {
return;
}
if (uids.indexOf(app.user.uid.toString()) !== -1) {
app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]');
} else {
socket.emit('admin.user.makeAdmins', uids, done('[[admin/manage/users:alerts.make-admin-success]]', '.administrator', true));
}
});
$('.remove-admin-user').on('click', function () {
var uids = getSelectedUids();
if (!uids.length) {
return;
}
if (uids.indexOf(app.user.uid.toString()) !== -1) {
app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]');
} else {
bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) {
if (confirm) {
socket.emit('admin.user.removeAdmins', uids, done('[[admin/manage/users:alerts.remove-admin-success]]', '.administrator', false));
}
});
}
});
$('.validate-email').on('click', function () { $('.validate-email').on('click', function () {
var uids = getSelectedUids(); var uids = getSelectedUids();
if (!uids.length) { if (!uids.length) {

@ -63,7 +63,7 @@ define('forum/account/edit/password', ['forum/account/header', 'translator', 'zx
onPasswordConfirmChanged(); onPasswordConfirmChanged();
var btn = $(this); var btn = $(this);
if ((passwordvalid && passwordsmatch) || app.user.isAdmin) { if (passwordvalid && passwordsmatch) {
btn.addClass('disabled').find('i').removeClass('hide'); btn.addClass('disabled').find('i').removeClass('hide');
socket.emit('user.changePassword', { socket.emit('user.changePassword', {
currentPassword: currentPassword.val(), currentPassword: currentPassword.val(),

@ -335,7 +335,7 @@ define('forum/chats', [
ajaxify.data = payload; ajaxify.data = payload;
Chats.setActive(); Chats.setActive();
Chats.addEventListeners(); Chats.addEventListeners();
messages.scrollToBottom($('.expanded-chat ul'));
if (history.pushState) { if (history.pushState) {
history.pushState({ history.pushState({
url: 'user/' + payload.userslug + '/chats/' + payload.roomId, url: 'user/' + payload.userslug + '/chats/' + payload.roomId,

@ -29,6 +29,11 @@ define('autocomplete', function () {
uid: user.uid, uid: user.uid,
name: user.username, name: user.username,
slug: user.userslug, slug: user.userslug,
username: user.username,
userslug: user.userslug,
picture: user.picture,
'icon:text': user['icon:text'],
'icon:bgColor': user['icon:bgColor'],
}, },
}; };
}); });

@ -1,7 +1,6 @@
'use strict'; 'use strict';
define('categorySelector', ['benchpress', 'translator'], function (Benchpress, translator) {
define('categorySelector', function () {
var categorySelector = {}; var categorySelector = {};
var selectedCategory; var selectedCategory;
var el; var el;
@ -29,6 +28,42 @@ define('categorySelector', function () {
el.find('[component="category-selector-selected"]').html(categoryEl.find('[component="category-markup"]').html()); el.find('[component="category-selector-selected"]').html(categoryEl.find('[component="category-markup"]').html());
}; };
categorySelector.modal = function (categories, callback) {
if (typeof categories === 'function') {
callback = categories;
categories = ajaxify.data.allCategories;
}
Benchpress.parse('admin/partials/categories/select-category', {
categories: categories,
}, function (html) {
translator.translate(html, function (html) {
var modal = bootbox.dialog({
title: '[[modules:composer.select_category]]',
message: html,
buttons: {
save: {
label: '[[global:select]]',
className: 'btn-primary',
callback: submit,
},
},
});
categorySelector.init(modal.find('[component="category-selector"]'));
function submit(ev) {
ev.preventDefault();
var selectedCategory = categorySelector.getSelectedCategory();
if (selectedCategory) {
callback(selectedCategory.cid);
modal.modal('hide');
}
return false;
}
modal.find('form').on('submit', submit);
});
});
};
return categorySelector; return categorySelector;
}); });

@ -338,7 +338,7 @@ Categories.buildForSelect = function (uid, privilege, callback) {
}; };
Categories.buildForSelectCategories = function (categories, callback) { Categories.buildForSelectCategories = function (categories, callback) {
function recursive(category, categoriesData, level) { function recursive(category, categoriesData, level, depth) {
if (category.link) { if (category.link) {
return; return;
} }
@ -347,10 +347,11 @@ Categories.buildForSelectCategories = function (categories, callback) {
category.value = category.cid; category.value = category.cid;
category.level = level; category.level = level;
category.text = level + bullet + category.name; category.text = level + bullet + category.name;
category.depth = depth;
categoriesData.push(category); categoriesData.push(category);
category.children.forEach(function (child) { category.children.forEach(function (child) {
recursive(child, categoriesData, '&nbsp;&nbsp;&nbsp;&nbsp;' + level); recursive(child, categoriesData, '&nbsp;&nbsp;&nbsp;&nbsp;' + level, depth + 1);
}); });
} }
@ -361,7 +362,7 @@ Categories.buildForSelectCategories = function (categories, callback) {
}); });
categories.forEach(function (category) { categories.forEach(function (category) {
recursive(category, categoriesData, ''); recursive(category, categoriesData, '', 0);
}); });
callback(null, categoriesData); callback(null, categoriesData);
}; };

@ -62,7 +62,6 @@ module.exports = function (Categories) {
'posts:edit', 'posts:edit',
'posts:delete', 'posts:delete',
'topics:delete', 'topics:delete',
'upload:post:image',
]; ];
async.series([ async.series([

@ -31,7 +31,11 @@ module.exports = function (Categories) {
category.name = validator.escape(String(category.name || '')); category.name = validator.escape(String(category.name || ''));
category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined; category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined;
category.isSection = category.hasOwnProperty('isSection') ? parseInt(category.isSection, 10) === 1 : undefined; category.isSection = category.hasOwnProperty('isSection') ? parseInt(category.isSection, 10) === 1 : undefined;
category.icon = category.icon || 'hidden';
if (category.hasOwnProperty('icon')) {
category.icon = category.icon || 'hidden';
}
if (category.hasOwnProperty('post_count')) { if (category.hasOwnProperty('post_count')) {
category.post_count = category.post_count || 0; category.post_count = category.post_count || 0;
category.totalPostCount = category.post_count; category.totalPostCount = category.post_count;

@ -30,7 +30,7 @@ function updatePackageFile() {
exports.updatePackageFile = updatePackageFile; exports.updatePackageFile = updatePackageFile;
function installAll() { function installAll() {
process.stdout.write('\n'); process.stdout.write(' started\n'.green);
var prod = global.env !== 'development'; var prod = global.env !== 'development';
var command = 'npm install'; var command = 'npm install';

@ -212,7 +212,7 @@ function upgradePlugins(callback) {
}); });
} else { } else {
console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade -p'.green + '".'.reset); console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade -p'.green + '".'.reset);
callback(null, true); callback();
} }
}); });
}); });

@ -16,6 +16,7 @@ var steps = {
handler: function (next) { handler: function (next) {
packageInstall.updatePackageFile(); packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins(); packageInstall.preserveExtraneousPlugins();
process.stdout.write(' OK\n'.green);
next(); next();
}, },
}, },
@ -54,11 +55,8 @@ function runSteps(tasks) {
tasks = tasks.map(function (key, i) { tasks = tasks.map(function (key, i) {
return function (next) { return function (next) {
process.stdout.write('\n' + ((i + 1) + '. ').bold + steps[key].message.yellow); process.stdout.write('\n' + ((i + 1) + '. ').bold + steps[key].message.yellow);
return steps[key].handler(function (err, inhibitOk) { return steps[key].handler(function (err) {
if (err) { return next(err); } if (err) { return next(err); }
if (!inhibitOk) {
process.stdout.write(' OK'.green + '\n'.reset);
}
next(); next();
}); });
}; };

@ -5,6 +5,7 @@ var async = require('async');
var messaging = require('../../messaging'); var messaging = require('../../messaging');
var meta = require('../../meta'); var meta = require('../../meta');
var user = require('../../user'); var user = require('../../user');
var privileges = require('../../privileges');
var helpers = require('../helpers'); var helpers = require('../helpers');
var chatsController = module.exports; var chatsController = module.exports;
@ -26,6 +27,13 @@ chatsController.get = function (req, res, callback) {
if (!uid) { if (!uid) {
return callback(); return callback();
} }
privileges.global.can('chat', req.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
messaging.getRecentChats(req.uid, uid, 0, 19, next); messaging.getRecentChats(req.uid, uid, 0, 19, next);
}, },
function (_recentChats, next) { function (_recentChats, next) {

@ -3,6 +3,8 @@
var adminController = { var adminController = {
dashboard: require('./admin/dashboard'), dashboard: require('./admin/dashboard'),
categories: require('./admin/categories'), categories: require('./admin/categories'),
privileges: require('./admin/privileges'),
adminsMods: require('./admin/admins-mods'),
tags: require('./admin/tags'), tags: require('./admin/tags'),
postQueue: require('./admin/postqueue'), postQueue: require('./admin/postqueue'),
blacklist: require('./admin/blacklist'), blacklist: require('./admin/blacklist'),

@ -0,0 +1,50 @@
'use strict';
var async = require('async');
var groups = require('../../groups');
var categories = require('../../categories');
var AdminsMods = module.exports;
AdminsMods.get = function (req, res, next) {
async.waterfall([
function (next) {
async.parallel({
admins: function (next) {
groups.get('administrators', { uid: req.uid }, next);
},
globalMods: function (next) {
groups.get('Global Moderators', { uid: req.uid }, next);
},
categories: function (next) {
getModeratorsOfCategories(req.uid, next);
},
}, next);
},
function (results) {
res.render('admin/manage/admins-mods', results);
},
], next);
};
function getModeratorsOfCategories(uid, callback) {
async.waterfall([
function (next) {
categories.buildForSelect(uid, 'find', next);
},
function (categoryData, next) {
async.map(categoryData, function (category, next) {
async.waterfall([
function (next) {
categories.getModerators(category.cid, next);
},
function (moderators, next) {
category.moderators = moderators;
next(null, category);
},
], next);
}, next);
},
], callback);
}

@ -3,7 +3,6 @@
var async = require('async'); var async = require('async');
var categories = require('../../categories'); var categories = require('../../categories');
var privileges = require('../../privileges');
var analytics = require('../../analytics'); var analytics = require('../../analytics');
var plugins = require('../../plugins'); var plugins = require('../../plugins');
var translator = require('../../translator'); var translator = require('../../translator');
@ -15,7 +14,6 @@ categoriesController.get = function (req, res, callback) {
function (next) { function (next) {
async.parallel({ async.parallel({
category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid),
privileges: async.apply(privileges.categories.list, req.params.category_id),
allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), allCategories: async.apply(categories.buildForSelect, req.uid, 'read'),
}, next); }, next);
}, },
@ -36,7 +34,6 @@ categoriesController.get = function (req, res, callback) {
req: req, req: req,
res: res, res: res,
category: category, category: category,
privileges: data.privileges,
allCategories: data.allCategories, allCategories: data.allCategories,
}, next); }, next);
}, },
@ -44,7 +41,6 @@ categoriesController.get = function (req, res, callback) {
data.category.name = translator.escape(String(data.category.name)); data.category.name = translator.escape(String(data.category.name));
res.render('admin/manage/category', { res.render('admin/manage/category', {
category: data.category, category: data.category,
privileges: data.privileges,
allCategories: data.allCategories, allCategories: data.allCategories,
}); });
}, },

@ -0,0 +1,39 @@
'use strict';
var async = require('async');
var categories = require('../../categories');
var privileges = require('../../privileges');
var privilegesController = module.exports;
privilegesController.get = function (req, res, callback) {
var cid = req.params.cid ? req.params.cid : 0;
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
if (!cid) {
privileges.global.list(next);
} else {
privileges.categories.list(cid, next);
}
},
allCategories: async.apply(categories.buildForSelect, req.uid, 'read'),
}, next);
},
function (data) {
data.allCategories.forEach(function (category) {
if (category) {
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
}
});
res.render('admin/manage/privileges', {
privileges: data.privileges,
allCategories: data.allCategories,
cid: cid,
});
},
], callback);
};

@ -1,61 +1,56 @@
'use strict'; 'use strict';
var async = require('async');
var plugins = require('../plugins'); var plugins = require('../plugins');
var meta = require('../meta'); var meta = require('../meta');
var user = require('../user'); var user = require('../user');
var pubsub = require('../pubsub');
var adminHomePageRoute; function adminHomePageRoute() {
var getRoute; return (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories';
function configUpdated() {
adminHomePageRoute = (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories';
getRoute = parseInt(meta.config.allowUserHomePage, 10) ? getRouteAllowUserHomePage : getRouteDisableUserHomePage;
}
function getRouteDisableUserHomePage(uid, next) {
next(null, adminHomePageRoute);
} }
function getRouteAllowUserHomePage(uid, next) { function getUserHomeRoute(uid, callback) {
user.getSettings(uid, function (err, settings) { async.waterfall([
if (err) { function (next) {
return next(err); user.getSettings(uid, next);
} },
function (settings, next) {
var route = adminHomePageRoute; var route = adminHomePageRoute();
if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') {
route = settings.homePageRoute || route; route = settings.homePageRoute || route;
} }
next(null, route); next(null, route);
}); },
], callback);
} }
pubsub.on('config:update', configUpdated);
configUpdated();
function rewrite(req, res, next) { function rewrite(req, res, next) {
if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') { if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') {
return next(); return next();
} }
getRoute(req.uid, function (err, route) { async.waterfall([
if (err) { function (next) {
return next(err); if (parseInt(meta.config.allowUserHomePage, 10)) {
} getUserHomeRoute(req.uid, next);
} else {
var hook = 'action:homepage.get:' + route; next(null, adminHomePageRoute());
}
if (!plugins.hasListeners(hook)) { },
req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route; function (route, next) {
} else { var hook = 'action:homepage.get:' + route;
res.locals.homePageRoute = route;
} if (!plugins.hasListeners(hook)) {
req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route;
next(); } else {
}); res.locals.homePageRoute = route;
}
next();
},
], next);
} }
exports.rewrite = rewrite; exports.rewrite = rewrite;

@ -37,9 +37,6 @@ uploadsController.upload = function (req, res, filesIterator) {
uploadsController.uploadPost = function (req, res, next) { uploadsController.uploadPost = function (req, res, next) {
uploadsController.upload(req, res, function (uploadedFile, next) { uploadsController.upload(req, res, function (uploadedFile, next) {
if (!parseInt(req.body.cid, 10)) {
return next(new Error('[[error:category-not-selected]]'));
}
var isImage = uploadedFile.type.match(/image./); var isImage = uploadedFile.type.match(/image./);
if (isImage) { if (isImage) {
uploadAsImage(req, uploadedFile, next); uploadAsImage(req, uploadedFile, next);
@ -52,7 +49,7 @@ uploadsController.uploadPost = function (req, res, next) {
function uploadAsImage(req, uploadedFile, callback) { function uploadAsImage(req, uploadedFile, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.categories.can('upload:post:image', req.body.cid, req.uid, next); privileges.global.can('upload:post:image', req.uid, next);
}, },
function (canUpload, next) { function (canUpload, next) {
if (!canUpload) { if (!canUpload) {
@ -82,7 +79,7 @@ function uploadAsImage(req, uploadedFile, callback) {
function uploadAsFile(req, uploadedFile, callback) { function uploadAsFile(req, uploadedFile, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.categories.can('upload:post:file', req.body.cid, req.uid, next); privileges.global.can('upload:post:file', req.uid, next);
}, },
function (canUpload, next) { function (canUpload, next) {
if (!canUpload) { if (!canUpload) {

@ -83,8 +83,8 @@ module.exports = function (db, module) {
if (!key) { if (!key) {
return callback(); return callback();
} }
db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { value: 1 } }, { new: true, upsert: true }, function (err, result) { db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { data: 1 } }, { new: true, upsert: true }, function (err, result) {
callback(err, result && result.value ? result.value.value : null); callback(err, result && result.value ? result.value.data : null);
}); });
}; };
@ -108,6 +108,7 @@ module.exports = function (db, module) {
if (!data) { if (!data) {
return callback(null, null); return callback(null, null);
} }
delete data.expireAt;
var keys = Object.keys(data); var keys = Object.keys(data);
if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) {
return callback(null, 'zset'); return callback(null, 'zset');

@ -353,6 +353,11 @@ function createGlobalModeratorsGroup(next) {
], next); ], next);
} }
function giveGlobalPrivileges(next) {
var privileges = require('./privileges');
privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next);
}
function createCategories(next) { function createCategories(next) {
var Categories = require('./categories'); var Categories = require('./categories');
@ -498,6 +503,7 @@ install.setup = function (callback) {
createCategories, createCategories,
createAdministrator, createAdministrator,
createGlobalModeratorsGroup, createGlobalModeratorsGroup,
giveGlobalPrivileges,
createMenuItems, createMenuItems,
createWelcomePost, createWelcomePost,
enableDefaultPlugins, enableDefaultPlugins,

@ -76,6 +76,7 @@ module.exports = function (Messaging) {
notifications.create({ notifications.create({
type: 'new-chat', type: 'new-chat',
subject: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyLong: messageObj.content, bodyLong: messageObj.content,
nid: 'chat_' + fromuid + '_' + roomId, nid: 'chat_' + fromuid + '_' + roomId,

@ -212,7 +212,7 @@ function build(targets, callback) {
} }
winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.'); winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.');
callback(null, true); callback();
}); });
} }

@ -12,6 +12,7 @@ var meta = require('../meta');
var plugins = require('../plugins'); var plugins = require('../plugins');
var navigation = require('../navigation'); var navigation = require('../navigation');
var translator = require('../translator'); var translator = require('../translator');
var privileges = require('../privileges');
var utils = require('../utils'); var utils = require('../utils');
var controllers = { var controllers = {
@ -77,6 +78,9 @@ module.exports = function (middleware) {
isModerator: function (next) { isModerator: function (next) {
user.isModeratorOfAnyCategory(req.uid, next); user.isModeratorOfAnyCategory(req.uid, next);
}, },
privileges: function (next) {
privileges.global.get(req.uid, next);
},
user: function (next) { user: function (next) {
var userData = { var userData = {
uid: 0, uid: 0,
@ -132,6 +136,8 @@ module.exports = function (middleware) {
results.user.isAdmin = results.isAdmin; results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod; results.user.isGlobalMod = results.isGlobalMod;
results.user.isMod = !!results.isModerator; results.user.isMod = !!results.isModerator;
results.user.privileges = results.privileges;
results.user.uid = parseInt(results.user.uid, 10); results.user.uid = parseInt(results.user.uid, 10);
results.user.email = String(results.user.email); results.user.email = String(results.user.email);
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
@ -183,6 +189,7 @@ module.exports = function (middleware) {
templateValues.isAdmin = results.user.isAdmin; templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod; templateValues.isGlobalMod = results.user.isGlobalMod;
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
templateValues.canChat = results.canChat && parseInt(meta.config.disableChat, 10) !== 1;
templateValues.user = results.user; templateValues.user = results.user;
templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true });
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;

@ -220,7 +220,7 @@ function pushToUids(uids, notification, callback) {
async.eachLimit(uids, 3, function (uid, next) { async.eachLimit(uids, 3, function (uid, next) {
emailer.send('notification', uid, { emailer.send('notification', uid, {
path: notification.path, path: notification.path,
subject: '[[notifications:new_notification_from, ' + meta.config.title + ']]', subject: notification.subject || '[[notifications:new_notification_from, ' + meta.config.title + ']]',
intro: utils.stripHTMLTags(notification.bodyShort), intro: utils.stripHTMLTags(notification.bodyShort),
body: utils.stripHTMLTags(notification.bodyLong || ''), body: utils.stripHTMLTags(notification.bodyLong || ''),
showUnsubscribe: true, showUnsubscribe: true,

@ -213,8 +213,8 @@ Plugins.list = function (matching, callback) {
require('request')(url, { require('request')(url, {
json: true, json: true,
}, function (err, res, body) { }, function (err, res, body) {
if (err) { if (err || (res && res.statusCode !== 200)) {
winston.error('Error parsing plugins', err); winston.error('Error loading ' + url, err || body);
return Plugins.normalise([], callback); return Plugins.normalise([], callback);
} }
@ -225,7 +225,7 @@ Plugins.list = function (matching, callback) {
Plugins.normalise = function (apiReturn, callback) { Plugins.normalise = function (apiReturn, callback) {
var pluginMap = {}; var pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies; var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
apiReturn = apiReturn || []; apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
for (var i = 0; i < apiReturn.length; i += 1) { for (var i = 0; i < apiReturn.length; i += 1) {
apiReturn[i].id = apiReturn[i].name; apiReturn[i].id = apiReturn[i].name;
apiReturn[i].installed = false; apiReturn[i].installed = false;

@ -12,8 +12,6 @@ privileges.privilegeLabels = [
{ name: 'Edit Posts' }, { name: 'Edit Posts' },
{ name: 'Delete Posts' }, { name: 'Delete Posts' },
{ name: 'Delete Topics' }, { name: 'Delete Topics' },
{ name: 'Upload Images' },
{ name: 'Upload Files' },
{ name: 'Purge' }, { name: 'Purge' },
{ name: 'Moderate' }, { name: 'Moderate' },
]; ];
@ -28,8 +26,6 @@ privileges.userPrivilegeList = [
'posts:edit', 'posts:edit',
'posts:delete', 'posts:delete',
'topics:delete', 'topics:delete',
'upload:post:image',
'upload:post:file',
'purge', 'purge',
'moderate', 'moderate',
]; ];
@ -40,6 +36,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(function (privi
privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList); privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList);
require('./privileges/global')(privileges);
require('./privileges/categories')(privileges); require('./privileges/categories')(privileges);
require('./privileges/topics')(privileges); require('./privileges/topics')(privileges);
require('./privileges/posts')(privileges); require('./privileges/posts')(privileges);

@ -15,121 +15,20 @@ module.exports = function (privileges) {
privileges.categories.list = function (cid, callback) { privileges.categories.list = function (cid, callback) {
// Method used in admin/category controller to show all users/groups with privs in that given cid // Method used in admin/category controller to show all users/groups with privs in that given cid
var privilegeLabels = privileges.privilegeLabels.slice();
var userPrivilegeList = privileges.userPrivilegeList.slice();
var groupPrivilegeList = privileges.groupPrivilegeList.slice();
async.waterfall([ async.waterfall([
function (next) { function (next) {
async.parallel({ async.parallel({
labels: function (next) { labels: function (next) {
async.parallel({ async.parallel({
users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privilegeLabels), users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privileges.privilegeLabels.slice()),
groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privilegeLabels), groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privileges.privilegeLabels.slice()),
}, next); }, next);
}, },
users: function (next) { users: function (next) {
var userPrivileges; helpers.getUserPrivileges(cid, 'filter:privileges.list', privileges.userPrivilegeList, next);
var memberSets;
async.waterfall([
async.apply(plugins.fireHook, 'filter:privileges.list', userPrivilegeList),
function (_privs, next) {
userPrivileges = _privs;
groups.getMembersOfGroups(userPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
function (_memberSets, next) {
memberSets = _memberSets.map(function (set) {
return set.map(function (uid) {
return parseInt(uid, 10);
});
});
var members = _.uniq(_.flatten(memberSets));
user.getUsersFields(members, ['picture', 'username'], next);
},
function (memberData, next) {
memberData.forEach(function (member) {
member.privileges = {};
for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) {
member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1;
}
});
next(null, memberData);
},
], next);
}, },
groups: function (next) { groups: function (next) {
var groupPrivileges; helpers.getGroupPrivileges(cid, 'filter:privileges.groups.list', privileges.groupPrivilegeList, next);
async.waterfall([
async.apply(plugins.fireHook, 'filter:privileges.groups.list', groupPrivilegeList),
function (_privs, next) {
groupPrivileges = _privs;
async.parallel({
memberSets: function (next) {
groups.getMembersOfGroups(groupPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
groupNames: function (next) {
groups.getGroups('groups:createtime', 0, -1, next);
},
}, next);
},
function (results, next) {
var memberSets = results.memberSets;
var uniqueGroups = _.uniq(_.flatten(memberSets));
var groupNames = results.groupNames.filter(function (groupName) {
return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1;
});
groupNames = groups.ephemeralGroups.concat(groupNames);
var registeredUsersIndex = groupNames.indexOf('registered-users');
if (registeredUsersIndex !== -1) {
groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]);
} else {
groupNames = ['registered-users'].concat(groupNames);
}
var adminIndex = groupNames.indexOf('administrators');
if (adminIndex !== -1) {
groupNames.splice(adminIndex, 1);
}
var memberPrivs;
var memberData = groupNames.map(function (member) {
memberPrivs = {};
for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) {
memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1;
}
return {
name: member,
privileges: memberPrivs,
};
});
next(null, memberData);
},
function (memberData, next) {
// Grab privacy info for the groups as well
async.map(memberData, function (member, next) {
async.waterfall([
function (next) {
groups.isPrivate(member.name, next);
},
function (isPrivate, next) {
member.isPrivate = isPrivate;
next(null, member);
},
], next);
}, next);
},
], next);
}, },
}, next); }, next);
}, },
@ -299,19 +198,13 @@ module.exports = function (privileges) {
}; };
privileges.categories.give = function (privileges, cid, groupName, callback) { privileges.categories.give = function (privileges, cid, groupName, callback) {
giveOrRescind(groups.join, privileges, cid, groupName, callback); helpers.giveOrRescind(groups.join, privileges, cid, groupName, callback);
}; };
privileges.categories.rescind = function (privileges, cid, groupName, callback) { privileges.categories.rescind = function (privileges, cid, groupName, callback) {
giveOrRescind(groups.leave, privileges, cid, groupName, callback); helpers.giveOrRescind(groups.leave, privileges, cid, groupName, callback);
}; };
function giveOrRescind(method, privileges, cid, groupName, callback) {
async.eachSeries(privileges, function (privilege, next) {
method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next);
}, callback);
}
privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) { privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {

@ -0,0 +1,128 @@
'use strict';
var async = require('async');
var _ = require('lodash');
var user = require('../user');
var groups = require('../groups');
var helpers = require('./helpers');
var plugins = require('../plugins');
module.exports = function (privileges) {
privileges.global = {};
privileges.global.privilegeLabels = [
{ name: 'Chat' },
{ name: 'Upload Images' },
{ name: 'Upload Files' },
];
privileges.global.userPrivilegeList = [
'chat',
'upload:post:image',
'upload:post:file',
];
privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) {
return 'groups:' + privilege;
});
privileges.global.list = function (callback) {
async.waterfall([
function (next) {
async.parallel({
labels: function (next) {
async.parallel({
users: async.apply(plugins.fireHook, 'filter:privileges.global.list_human', privileges.global.privilegeLabels.slice()),
groups: async.apply(plugins.fireHook, 'filter:privileges.global.groups.list_human', privileges.global.privilegeLabels.slice()),
}, next);
},
users: function (next) {
helpers.getUserPrivileges(0, 'filter:privileges.global.list', privileges.global.userPrivilegeList, next);
},
groups: function (next) {
helpers.getGroupPrivileges(0, 'filter:privileges.global.groups.list', privileges.global.groupPrivilegeList, next);
},
}, next);
},
function (payload, next) {
// This is a hack because I can't do {labels.users.length} to echo the count in templates.js
payload.columnCount = payload.labels.users.length + 2;
next(null, payload);
},
], callback);
};
privileges.global.get = function (uid, callback) {
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
helpers.isUserAllowedTo(privileges.global.userPrivilegeList, uid, 0, next);
},
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
isGlobalModerator: function (next) {
user.isGlobalModerator(uid, next);
},
}, next);
},
function (results, next) {
var privData = _.zipObject(privileges.global.userPrivilegeList, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isGlobalModerator;
plugins.fireHook('filter:privileges.global.get', {
chat: privData.chat || isAdminOrMod,
'upload:post:image': privData['upload:post:image'] || isAdminOrMod,
'upload:post:file': privData['upload:post:file'] || isAdminOrMod,
}, next);
},
], callback);
};
privileges.global.can = function (privilege, uid, callback) {
helpers.some([
function (next) {
helpers.isUserAllowedTo(privilege, uid, [0], function (err, results) {
next(err, Array.isArray(results) && results.length ? results[0] : false);
});
},
function (next) {
user.isGlobalModerator(uid, next);
},
function (next) {
user.isAdministrator(uid, next);
},
], callback);
};
privileges.global.give = function (privileges, groupName, callback) {
helpers.giveOrRescind(groups.join, privileges, 0, groupName, callback);
};
privileges.global.rescind = function (privileges, groupName, callback) {
helpers.giveOrRescind(groups.leave, privileges, 0, groupName, callback);
};
privileges.global.userPrivileges = function (uid, callback) {
var tasks = {};
privileges.global.userPrivilegeList.forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, uid, 'cid:0:privileges:' + privilege);
});
async.parallel(tasks, callback);
};
privileges.global.groupPrivileges = function (groupName, callback) {
var tasks = {};
privileges.global.groupPrivilegeList.forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, groupName, 'cid:0:privileges:' + privilege);
});
async.parallel(tasks, callback);
};
};

@ -2,7 +2,11 @@
'use strict'; 'use strict';
var async = require('async'); var async = require('async');
var _ = require('lodash');
var groups = require('../groups'); var groups = require('../groups');
var user = require('../user');
var plugins = require('../plugins');
var helpers = module.exports; var helpers = module.exports;
@ -111,3 +115,115 @@ function isGuestAllowedToPrivileges(privileges, cid, callback) {
groups.isMemberOfGroups('guests', groupKeys, callback); groups.isMemberOfGroups('guests', groupKeys, callback);
} }
helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) {
var userPrivileges;
var memberSets;
async.waterfall([
async.apply(plugins.fireHook, hookName, userPrivilegeList.slice()),
function (_privs, next) {
userPrivileges = _privs;
groups.getMembersOfGroups(userPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
function (_memberSets, next) {
memberSets = _memberSets.map(function (set) {
return set.map(function (uid) {
return parseInt(uid, 10);
});
});
var members = _.uniq(_.flatten(memberSets));
user.getUsersFields(members, ['picture', 'username'], next);
},
function (memberData, next) {
memberData.forEach(function (member) {
member.privileges = {};
for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) {
member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1;
}
});
next(null, memberData);
},
], callback);
};
helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callback) {
var groupPrivileges;
async.waterfall([
async.apply(plugins.fireHook, hookName, groupPrivilegeList.slice()),
function (_privs, next) {
groupPrivileges = _privs;
async.parallel({
memberSets: function (next) {
groups.getMembersOfGroups(groupPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
groupNames: function (next) {
groups.getGroups('groups:createtime', 0, -1, next);
},
}, next);
},
function (results, next) {
var memberSets = results.memberSets;
var uniqueGroups = _.uniq(_.flatten(memberSets));
var groupNames = results.groupNames.filter(function (groupName) {
return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1;
});
groupNames = groups.ephemeralGroups.concat(groupNames);
var registeredUsersIndex = groupNames.indexOf('registered-users');
if (registeredUsersIndex !== -1) {
groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]);
} else {
groupNames = ['registered-users'].concat(groupNames);
}
var adminIndex = groupNames.indexOf('administrators');
if (adminIndex !== -1) {
groupNames.splice(adminIndex, 1);
}
var memberPrivs;
var memberData = groupNames.map(function (member) {
memberPrivs = {};
for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) {
memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1;
}
return {
name: member,
privileges: memberPrivs,
};
});
next(null, memberData);
},
function (memberData, next) {
// Grab privacy info for the groups as well
async.map(memberData, function (member, next) {
async.waterfall([
function (next) {
groups.isPrivate(member.name, next);
},
function (isPrivate, next) {
member.isPrivate = isPrivate;
next(null, member);
},
], next);
}, next);
},
], callback);
};
helpers.giveOrRescind = function (method, privileges, cid, groupName, callback) {
async.eachSeries(privileges, function (privilege, next) {
method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next);
}, callback);
};

@ -55,6 +55,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get); router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get);
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics); router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
router.get('/manage/privileges/:cid?', middlewares, controllers.admin.privileges.get);
router.get('/manage/tags', middlewares, controllers.admin.tags.get); router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get); router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get); router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
@ -71,6 +72,8 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/users/banned', middlewares, controllers.admin.users.banned); router.get('/manage/users/banned', middlewares, controllers.admin.users.banned);
router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue); router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue);
router.get('/manage/admins-mods', middlewares, controllers.admin.adminsMods.get);
router.get('/manage/groups', middlewares, controllers.admin.groups.list); router.get('/manage/groups', middlewares, controllers.admin.groups.list);
router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get); router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get);

@ -83,7 +83,11 @@ Categories.setPrivilege = function (socket, data, callback) {
}; };
Categories.getPrivilegeSettings = function (socket, cid, callback) { Categories.getPrivilegeSettings = function (socket, cid, callback) {
privileges.categories.list(cid, callback); if (!parseInt(cid, 10)) {
privileges.global.list(callback);
} else {
privileges.categories.list(cid, callback);
}
}; };
Categories.copyPrivilegesToChildren = function (socket, cid, callback) { Categories.copyPrivilegesToChildren = function (socket, cid, callback) {

@ -11,6 +11,7 @@ var Messaging = require('../messaging');
var utils = require('../utils'); var utils = require('../utils');
var server = require('./'); var server = require('./');
var user = require('../user'); var user = require('../user');
var privileges = require('../privileges');
var SocketModules = module.exports; var SocketModules = module.exports;
@ -73,6 +74,12 @@ SocketModules.chats.newRoom = function (socket, data, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.canMessageUser(socket.uid, data.touid, next); Messaging.canMessageUser(socket.uid, data.touid, next);
}, },
function (next) { function (next) {
@ -92,6 +99,13 @@ SocketModules.chats.send = function (socket, data, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
plugins.fireHook('filter:messaging.send', { plugins.fireHook('filter:messaging.send', {
data: data, data: data,
uid: socket.uid, uid: socket.uid,
@ -133,6 +147,13 @@ SocketModules.chats.loadRoom = function (socket, data, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.isUserInRoom(socket.uid, data.roomId, next); Messaging.isUserInRoom(socket.uid, data.roomId, next);
}, },
function (inRoom, next) { function (inRoom, next) {
@ -174,6 +195,13 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) {
var uid; var uid;
async.waterfall([ async.waterfall([
function (next) { function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.getUserCountInRoom(data.roomId, next); Messaging.getUserCountInRoom(data.roomId, next);
}, },
function (userCount, next) { function (userCount, next) {

@ -219,6 +219,7 @@ module.exports = function (Topics) {
notifications.create({ notifications.create({
type: 'new-reply', type: 'new-reply',
subject: title,
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
bodyLong: postData.content, bodyLong: postData.content,
pid: postData.pid, pid: postData.pid,

@ -0,0 +1,12 @@
'use strict';
var groups = require('../../groups');
module.exports = {
name: 'Give chat privilege to registered-users',
timestamp: Date.UTC(2017, 11, 18),
method: function (callback) {
groups.join('cid:0:privileges:groups:chat', 'registered-users', callback);
},
};

@ -0,0 +1,45 @@
'use strict';
var async = require('async');
var groups = require('../../groups');
var privileges = require('../../privileges');
var db = require('../../database');
module.exports = {
name: 'Give upload privilege to registered-users globally if it is given on a category',
timestamp: Date.UTC(2018, 0, 3),
method: function (callback) {
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
async.eachSeries(cids, function (cid, next) {
getGroupPrivileges(cid, function (err, groupPrivileges) {
if (err) {
return next(err);
}
var privs = [];
if (groupPrivileges['groups:upload:post:image']) {
privs.push('upload:post:image');
}
if (groupPrivileges['groups:upload:post:file']) {
privs.push('upload:post:file');
}
privileges.global.give(privs, 'registered-users', next);
});
}, callback);
});
},
};
function getGroupPrivileges(cid, callback) {
var tasks = {};
['groups:upload:post:image', 'groups:upload:post:file'].forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, 'registered-users', 'cid:' + cid + ':privileges:' + privilege);
});
async.parallel(tasks, callback);
}

@ -0,0 +1,64 @@
<div class="admins-mods">
<h4><!-- IF admins.icon --><i class="fa {admins.icon}"></i> <!-- ENDIF admins.icon -->[[admin/manage/admins-mods:administrators]]</h4>
<div class="administrator-area">
<!-- BEGIN admins.members -->
<div class="user-card pull-left" data-uid="{admins.members.uid}">
<!-- IF admins.members.picture -->
<img class="avatar avatar-sm" src="{admins.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {admins.members.icon:bgColor};">{admins.members.icon:text}</div>
<!-- ENDIF admins.members.picture -->
<a href="{config.relative_path}/user/{admins.members.userslug}">{admins.members.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END admins.members -->
</div>
<input id="admin-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-administrator]]" />
<br/>
<h4><!-- IF globalMods.icon --><i class="fa {globalMods.icon}"></i> <!-- ENDIF globalMods.icon -->[[admin/manage/admins-mods:global-moderators]]</h4>
<div class="global-moderator-area">
<!-- BEGIN globalMods.members -->
<div class="user-card pull-left" data-uid="{globalMods.members.uid}">
<!-- IF globalMods.members.picture -->
<img class="avatar avatar-sm" src="{globalMods.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {globalMods.members.icon:bgColor};">{globalMods.members.icon:text}</div>
<!-- ENDIF globalMods.members.picture -->
<a href="{config.relative_path}/user/{globalMods.members.userslug}">{globalMods.members.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END globalMods.members -->
</div>
<div id="no-global-mods-warning" class="<!-- IF globalMods.members.length -->hidden<!-- ENDIF globalMods.members.length -->">[[admin/manage/admins-mods:no-global-moderators]]</div>
<input id="global-mod-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-global-moderator]]" />
<br/>
<!-- BEGIN categories -->
<div class="categories category-wrapper category-depth-{categories.depth}">
<h4><!-- IF categories.icon --><i class="fa {categories.icon}"></i> <!-- ENDIF categories.icon -->[[admin/manage/admins-mods:moderators-of-category, {categories.name}]]</h4>
<div class="moderator-area" data-cid="{categories.cid}">
<!-- BEGIN categories.moderators -->
<div class="user-card pull-left" data-uid="{categories.moderators.uid}">
<!-- IF categories.moderators.picture -->
<img class="avatar avatar-sm" src="{categories.moderators.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {categories.moderators.icon:bgColor};">{categories.moderators.icon:text}</div>
<!-- ENDIF categories.moderators.picture -->
<a href="{config.relative_path}/user/{categories.moderators.userslug}">{categories.moderators.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END categories.moderators -->
</div>
<div data-cid="{categories.cid}" class="no-moderator-warning <!-- IF categories.moderators.length -->hidden<!-- ENDIF categories.moderators.length -->">[[admin/manage/admins-mods:no-moderators]]</div>
<input data-cid="{categories.cid}" class="form-control moderator-search" placeholder="[[admin/manage/admins-mods:add-moderator]]" />
</div>
<br/>
<!-- END categories -->
</div>

@ -2,15 +2,7 @@
<form role="form" class="category" data-cid="{category.cid}"> <form role="form" class="category" data-cid="{category.cid}">
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-3 pull-right">
<ul class="nav nav-pills">
<li class="active"><a href="#category-settings" data-toggle="tab">
[[admin/manage/categories:settings]]
</a></li>
<li><a href="#privileges" data-toggle="tab">[[admin/manage/categories:privileges]]</a></li>
</ul>
</div>
<div class="col-md-3">
<select id="category-selector" class="form-control"> <select id="category-selector" class="form-control">
<!-- BEGIN allCategories --> <!-- BEGIN allCategories -->
<option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option> <option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option>
@ -18,7 +10,7 @@
</select> </select>
</div> </div>
</div> </div>
<br/> <br/>
<div class="tab-content"> <div class="tab-content">
@ -174,19 +166,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade col-xs-12" id="privileges">
<p>
[[admin/manage/categories:privileges.description]]
</p>
<p class="text-warning">
[[admin/manage/categories:privileges.warning]]
</p>
<hr />
<div class="privilege-table-container">
<!-- IMPORT admin/partials/categories/privileges.tpl -->
</div>
</div>
</div> </div>
</form> </form>
</div> </div>

@ -0,0 +1,31 @@
<div class="row">
<form role="form" class="category">
<div class="row">
<div class="col-md-3 pull-right">
<select id="category-selector" class="form-control">
<option value="global" <!-- IF !cid --> selected <!-- ENDIF !cid -->>[[admin/manage/privileges:global]]</option>
<option disabled>_____________</option>
<!-- BEGIN allCategories -->
<option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option>
<!-- END allCategories -->
</select>
</div>
</div>
<br/>
<div class="">
<p>
[[admin/manage/categories:privileges.description]]
</p>
<hr />
<div class="privilege-table-container">
<!-- IF cid -->
<!-- IMPORT admin/partials/categories/privileges.tpl -->
<!-- ELSE -->
<!-- IMPORT admin/partials/global/privileges.tpl -->
<!-- ENDIF cid -->
</div>
</div>
</form>
</div>

@ -8,9 +8,6 @@
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" class="admin-user"><i class="fa fa-fw fa-shield"></i> [[admin/manage/users:make-admin]]</a></li>
<li><a href="#" class="remove-admin-user"><i class="fa fa-fw fa-ban"></i> [[admin/manage/users:remove-admin]]</a></li>
<li class="divider"></li>
<li><a href="#" class="validate-email"><i class="fa fa-fw fa-check"></i> [[admin/manage/users:validate-email]]</a></li> <li><a href="#" class="validate-email"><i class="fa fa-fw fa-check"></i> [[admin/manage/users:validate-email]]</a></li>
<li><a href="#" class="send-validation-email"><i class="fa fa-fw fa-mail-forward"></i> [[admin/manage/users:send-validation-email]]</a></li> <li><a href="#" class="send-validation-email"><i class="fa fa-fw fa-mail-forward"></i> [[admin/manage/users:send-validation-email]]</a></li>
<li><a href="#" class="password-reset-email"><i class="fa fa-fw fa-key"></i> [[admin/manage/users:password-reset-email]]</a></li> <li><a href="#" class="password-reset-email"><i class="fa fa-fw fa-key"></i> [[admin/manage/users:password-reset-email]]</a></li>

@ -5,7 +5,7 @@
<th class="arrowed" colspan="3"> <th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]] [[admin/manage/categories:privileges.section-viewing]]
</th> </th>
<th class="arrowed" colspan="8"> <th class="arrowed" colspan="6">
[[admin/manage/categories:privileges.section-posting]] [[admin/manage/categories:privileges.section-posting]]
</th> </th>
<th class="arrowed" colspan="2"> <th class="arrowed" colspan="2">
@ -61,7 +61,7 @@
<th class="arrowed" colspan="3"> <th class="arrowed" colspan="3">
[[admin/manage/categories:privileges.section-viewing]] [[admin/manage/categories:privileges.section-viewing]]
</th> </th>
<th class="arrowed" colspan="8"> <th class="arrowed" colspan="6">
[[admin/manage/categories:privileges.section-posting]] [[admin/manage/categories:privileges.section-posting]]
</th> </th>
<th class="arrowed" colspan="2"> <th class="arrowed" colspan="2">

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

@ -15,7 +15,9 @@
<h3 class="menu-section-title">[[admin/menu:section-manage]]</h3> <h3 class="menu-section-title">[[admin/menu:section-manage]]</h3>
<ul class="menu-section-list"> <ul class="menu-section-list">
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
@ -188,7 +190,9 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>

@ -638,7 +638,7 @@ describe('Categories', function () {
}); });
}); });
it('should load user privileges', function (done) { it('should load category user privileges', function (done) {
privileges.categories.userPrivileges(categoryObj.cid, 1, function (err, data) { privileges.categories.userPrivileges(categoryObj.cid, 1, function (err, data) {
assert.ifError(err); assert.ifError(err);
assert.deepEqual(data, { assert.deepEqual(data, {
@ -651,8 +651,6 @@ describe('Categories', function () {
'topics:tag': false, 'topics:tag': false,
'topics:delete': false, 'topics:delete': false,
'posts:edit': false, 'posts:edit': false,
'upload:post:file': false,
'upload:post:image': false,
purge: false, purge: false,
moderate: false, moderate: false,
}); });
@ -661,7 +659,20 @@ describe('Categories', function () {
}); });
}); });
it('should load group privileges', function (done) { it('should load global user privileges', function (done) {
privileges.global.userPrivileges(1, function (err, data) {
assert.ifError(err);
assert.deepEqual(data, {
chat: false,
'upload:post:image': false,
'upload:post:file': false,
});
done();
});
});
it('should load category group privileges', function (done) {
privileges.categories.groupPrivileges(categoryObj.cid, 'registered-users', function (err, data) { privileges.categories.groupPrivileges(categoryObj.cid, 'registered-users', function (err, data) {
assert.ifError(err); assert.ifError(err);
assert.deepEqual(data, { assert.deepEqual(data, {
@ -674,8 +685,6 @@ describe('Categories', function () {
'groups:posts:delete': true, 'groups:posts:delete': true,
'groups:read': true, 'groups:read': true,
'groups:topics:read': true, 'groups:topics:read': true,
'groups:upload:post:file': false,
'groups:upload:post:image': true,
'groups:purge': false, 'groups:purge': false,
'groups:moderate': false, 'groups:moderate': false,
}); });
@ -684,6 +693,19 @@ describe('Categories', function () {
}); });
}); });
it('should load global group privileges', function (done) {
privileges.global.groupPrivileges('registered-users', function (err, data) {
assert.ifError(err);
assert.deepEqual(data, {
'groups:chat': true,
'groups:upload:post:image': true,
'groups:upload:post:file': false,
});
done();
});
});
it('should return false if cid is falsy', function (done) { it('should return false if cid is falsy', function (done) {
privileges.categories.isUserAllowedTo('find', null, adminUid, function (err, isAllowed) { privileges.categories.isUserAllowedTo('find', null, adminUid, function (err, isAllowed) {
assert.ifError(err); assert.ifError(err);

@ -255,6 +255,14 @@ describe('Admin Controllers', function () {
}); });
}); });
it('should load /admin/manage/admins-mods', function (done) {
request(nconf.get('url') + '/api/admin/manage/admins-mods', { jar: jar, json: true }, function (err, res, body) {
assert.ifError(err);
assert(body);
done();
});
});
it('should return 403 if no referer', function (done) { it('should return 403 if no referer', function (done) {
request(nconf.get('url') + '/api/admin/users/csv', { jar: jar }, function (err, res, body) { request(nconf.get('url') + '/api/admin/users/csv', { jar: jar }, function (err, res, body) {
assert.ifError(err); assert.ifError(err);

@ -149,6 +149,21 @@ describe('Key methods', function () {
done(); done();
}); });
}); });
it('should set then increment a key', function (done) {
db.set('myIncrement', 1, function (err) {
assert.ifError(err);
db.increment('myIncrement', function (err, value) {
assert.ifError(err);
assert.equal(value, 2);
db.get('myIncrement', function (err, value) {
assert.ifError(err);
assert.equal(value, 2);
done();
});
});
});
});
}); });
describe('rename', function () { describe('rename', function () {

@ -71,9 +71,9 @@ describe('Groups', function () {
describe('.list()', function () { describe('.list()', function () {
it('should list the groups present', function (done) { it('should list the groups present', function (done) {
Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function (err, groups) { Groups.getGroupsFromSet('groups:visible:createtime', 0, 0, -1, function (err, groups) {
assert.ifError(err); assert.ifError(err);
assert.equal(groups.length, 7); assert.equal(groups.length, 4);
done(); done();
}); });
}); });

@ -414,7 +414,7 @@ describe('Messaging Library', function () {
it('should fail to load room if user is not in', function (done) { it('should fail to load room if user is not in', function (done) {
socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) { socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) {
assert.equal(err.message, '[[error:not-allowed]]'); assert.equal(err.message, '[[error:no-privileges]]');
done(); done();
}); });
}); });
@ -629,11 +629,12 @@ describe('Messaging Library', function () {
}); });
}); });
it('should 404 for guest', function (done) { it('should 500 for guest with no privilege error', function (done) {
meta.config.disableChat = 0; meta.config.disableChat = 0;
request(nconf.get('url') + '/user/baz/chats', function (err, response) { request(nconf.get('url') + '/api/user/baz/chats', { json: true }, function (err, response, body) {
assert.ifError(err); assert.ifError(err);
assert.equal(response.statusCode, 404); assert.equal(response.statusCode, 500);
assert.equal(body.error, '[[error:no-privileges]]');
done(); done();
}); });
}); });

@ -154,6 +154,9 @@ function setupMockDefaults(callback) {
winston.info('test_database flushed'); winston.info('test_database flushed');
setupDefaultConfigs(meta, next); setupDefaultConfigs(meta, next);
}, },
function (next) {
giveDefaultGlobalPrivileges(next);
},
function (next) { function (next) {
meta.configs.init(next); meta.configs.init(next);
}, },
@ -182,6 +185,11 @@ function setupDefaultConfigs(meta, next) {
meta.configs.setOnEmpty(defaults, next); meta.configs.setOnEmpty(defaults, next);
} }
function giveDefaultGlobalPrivileges(next) {
var privileges = require('../../src/privileges');
privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next);
}
function enableDefaultPlugins(callback) { function enableDefaultPlugins(callback) {
winston.info('Enabling default plugins\n'); winston.info('Enabling default plugins\n');

@ -62,7 +62,7 @@ describe('Upload Controllers', function () {
assert.ifError(err); assert.ifError(err);
jar = _jar; jar = _jar;
csrf_token = _csrf_token; csrf_token = _csrf_token;
privileges.categories.give(['upload:post:file'], cid, 'registered-users', done); privileges.global.give(['upload:post:file'], 'registered-users', done);
}); });
}); });
@ -77,17 +77,8 @@ describe('Upload Controllers', function () {
}); });
}); });
it('should fail to upload an image to a post with invalid cid', function (done) {
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), { cid: '0' }, jar, csrf_token, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 500);
assert.equal(body.error, '[[error:category-not-selected]]');
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, '../test/files/test.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'), {}, 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));
@ -100,7 +91,7 @@ describe('Upload Controllers', function () {
it('should resize and upload an image to a post', function (done) { it('should resize and upload an image to a post', function (done) {
var oldValue = meta.config.maximumImageWidth; var oldValue = meta.config.maximumImageWidth;
meta.config.maximumImageWidth = 10; meta.config.maximumImageWidth = 10;
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.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'), {}, 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 +107,7 @@ describe('Upload Controllers', function () {
meta.config.allowFileUploads = 1; meta.config.allowFileUploads = 1;
var oldValue = meta.config.allowedFileExtensions; var oldValue = meta.config.allowedFileExtensions;
meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; meta.config.allowedFileExtensions = 'png,jpg,bmp,html';
helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/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'), {}, jar, csrf_token, function (err, res, body) {
meta.config.allowedFileExtensions = oldValue; meta.config.allowedFileExtensions = oldValue;
assert.ifError(err); assert.ifError(err);
assert.equal(res.statusCode, 200); assert.equal(res.statusCode, 200);

Loading…
Cancel
Save