From 75ff60e4d54745e9ca55ea51198fdb21f964b534 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Nov 2016 11:57:57 -0500 Subject: [PATCH 001/131] partial revert of a9984bb, adding in a layer to translate ISO timestamp to datetime attribute and save localised string into title attribute. Fixes #5109 --- public/src/overrides.js | 17 +++++---- .../vendor/jquery/timeago/jquery.timeago.js | 36 +------------------ 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/public/src/overrides.js b/public/src/overrides.js index a2d213f45c..af8d80f659 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -152,13 +152,18 @@ if ('undefined' !== typeof window) { var timeagoFn = $.fn.timeago; $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * 30; $.fn.timeago = function () { - var els = timeagoFn.apply(this, arguments); + var els = $(this); - if (els) { - els.each(function () { - $(this).attr('title', (new Date($(this).attr('title'))).toString()); - }); - } + // Convert "old" format to new format (#5108) + var options = { year: 'numeric', month: 'long', day: 'numeric' }; + var iso; + els.each(function() { + iso = this.getAttribute('title'); + this.setAttribute('datetime', iso); + this.setAttribute('title', new Date(iso).toLocaleString(config.userLang.replace('_', '-'), options)); + }); + + timeagoFn.apply(this, arguments); }; }; diff --git a/public/vendor/jquery/timeago/jquery.timeago.js b/public/vendor/jquery/timeago/jquery.timeago.js index 8727fa7adb..c85ebcc5b0 100644 --- a/public/vendor/jquery/timeago/jquery.timeago.js +++ b/public/vendor/jquery/timeago/jquery.timeago.js @@ -194,41 +194,7 @@ $(this).text(inWords(data.datetime)); } else { if ($(this).attr('title').length > 0) { - //$(this).text($(this).attr('title')); - var languageCode = void 0; - switch (config.userLang) { - case 'en_GB': - case 'en_US': - languageCode = 'en'; - break; - - case 'fa_IR': - languageCode = 'fa'; - break; - - case 'pt_BR': - languageCode = 'pt-br'; - break; - - case 'nb': - languageCode = 'no'; - break; - - case 'zh_TW': - languageCode = 'zh-TW'; - break; - - case 'zh_CN': - languageCode = 'zh-CN'; - break; - - default: - languageCode = config.userLang; - break; - } - - var options = { year: 'numeric', month: 'long', day: 'numeric' }; - $(this).text(new Date($(this).attr('title')).toLocaleString(languageCode, options)); + $(this).text($(this).attr('title')); } } } From 885316d78f989002041fbe19160a29bc97ace813 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Nov 2016 12:44:46 -0500 Subject: [PATCH 002/131] also show minutes and hours in cut off timestamp --- public/src/overrides.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/overrides.js b/public/src/overrides.js index af8d80f659..0ebb1ebbb8 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -155,7 +155,7 @@ if ('undefined' !== typeof window) { var els = $(this); // Convert "old" format to new format (#5108) - var options = { year: 'numeric', month: 'long', day: 'numeric' }; + var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; var iso; els.each(function() { iso = this.getAttribute('title'); From 7d523fae0f3b8d0586e4eec740ff7e80e7ab4e23 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Nov 2016 13:00:23 -0500 Subject: [PATCH 003/131] closes #5200 --- public/src/overrides.js | 2 +- src/controllers/api.js | 1 + src/views/admin/settings/post.tpl | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/public/src/overrides.js b/public/src/overrides.js index 0ebb1ebbb8..b4e0f96031 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -150,7 +150,7 @@ if ('undefined' !== typeof window) { overrides.overrideTimeago = function () { var timeagoFn = $.fn.timeago; - $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * 30; + $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * (parseInt(config.timeagoCutoff, 10) || 60); $.fn.timeago = function () { var els = $(this); diff --git a/src/controllers/api.js b/src/controllers/api.js index cd877aea7d..10a5c8ad02 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -62,6 +62,7 @@ apiController.getConfig = function (req, res, next) { config.csrf_token = req.csrfToken(); config.searchEnabled = plugins.hasListeners('filter:search.query'); config.bootswatchSkin = 'default'; + config.timeagoCutoff = meta.config.timeagoCutoff; config.cookies = { enabled: parseInt(meta.config.cookieConsentEnabled, 10) === 1, diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index f58f524aca..559f7094b0 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -85,7 +85,24 @@
-
Teaser Settings
+
Timestamp
+
+
+
+ + +

+ Dates & times will be shown in a relative manner (e.g. "3 hours ago" / "5 days ago"), and localised into various + languages. After a certain point, this text can be switched to display the localised date itself + (e.g. 5 Nov 2016 15:30).
(Default: 30, or one month) +

+
+
+
+
+ +
+
Teaser
@@ -100,7 +117,6 @@
-
Unread Settings
From 0f9320612225421bcf0b06be6658c0155735a281 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Nov 2016 13:13:06 -0500 Subject: [PATCH 004/131] lint --- public/src/overrides.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/overrides.js b/public/src/overrides.js index b4e0f96031..a81b6cf525 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -157,7 +157,7 @@ if ('undefined' !== typeof window) { // Convert "old" format to new format (#5108) var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; var iso; - els.each(function() { + els.each(function () { iso = this.getAttribute('title'); this.setAttribute('datetime', iso); this.setAttribute('title', new Date(iso).toLocaleString(config.userLang.replace('_', '-'), options)); From 38f11f8c28b11329e92b514884cb57e64442c17a Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Fri, 11 Nov 2016 15:17:14 -0500 Subject: [PATCH 005/131] Incremented version number --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 65ec9a9fcb..654c45bff8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.2.1", + "version": "1.3.0", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -133,4 +133,4 @@ "url": "https://github.com/barisusakli" } ] -} +} \ No newline at end of file From ede7a71db7fe4163a0994512b401a629ce6c06b8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Nov 2016 16:47:59 -0500 Subject: [PATCH 006/131] Fixes #5186 On socket.io connection, all clients join a room pertaining to their express session id. We use this room to keep track of any sessions in different browser windows (but the same cookie jar), so if a login/logout occurs, we can throw a session mismatch modal. This room can also be used to emit messages across windows/tabs... --- public/src/app.js | 8 ++++++++ public/src/client/login.js | 9 +++++++++ src/controllers/authentication.js | 13 ++++++++++++- src/socket.io/index.js | 1 + 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/public/src/app.js b/public/src/app.js index 8ac3e1ebff..ba1f6d39ff 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -88,6 +88,14 @@ app.cacheBuster = null; app.logout = function () { $(window).trigger('action:app.logout'); + + /* + Set session refresh flag (otherwise the session check will trip and throw invalid session modal) + We know the session is/will be invalid (uid mismatch) because the user is logging out + */ + app.flags = app.flags || {}; + app.flags._sessionRefresh = true; + $.ajax(config.relative_path + '/logout', { type: 'POST', headers: { diff --git a/public/src/client/login.js b/public/src/client/login.js index f798347c73..1de1218fd0 100644 --- a/public/src/client/login.js +++ b/public/src/client/login.js @@ -23,6 +23,14 @@ define('forum/login', ['translator'], function (translator) { } submitEl.addClass('disabled'); + + /* + Set session refresh flag (otherwise the session check will trip and throw invalid session modal) + We know the session is/will be invalid (uid mismatch) because the user is attempting a login + */ + app.flags = app.flags || {}; + app.flags._sessionRefresh = true; + formEl.ajaxSubmit({ headers: { 'x-csrf-token': config.csrf_token @@ -37,6 +45,7 @@ define('forum/login', ['translator'], function (translator) { errorEl.find('p').translateText(data.responseText); errorEl.show(); submitEl.removeClass('disabled'); + app.flags._sessionRefresh = false; // Select the entire password if that field has focus if ($('#password:focus').size()) { diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 260a0e7281..1d45b2cbd5 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -15,6 +15,8 @@ var plugins = require('../plugins'); var utils = require('../../public/src/utils'); var Password = require('../password'); +var sockets = require('../socket.io'); + var authenticationController = {}; authenticationController.register = function (req, res, next) { @@ -326,6 +328,10 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) { if (err) { return callback(err); } + + // Force session check for all connected socket.io clients with the same session id + sockets.in('sess_' + req.sessionID).emit('checkSession', uid); + plugins.fireHook('action:user.loggedIn', uid); callback(); }); @@ -405,7 +411,9 @@ authenticationController.localLogin = function (req, username, password, next) { authenticationController.logout = function (req, res, next) { if (req.user && parseInt(req.user.uid, 10) > 0 && req.sessionID) { var uid = parseInt(req.user.uid, 10); - user.auth.revokeSession(req.sessionID, uid, function (err) { + var sessionID = req.sessionID; + + user.auth.revokeSession(sessionID, uid, function (err) { if (err) { return next(err); } @@ -416,6 +424,9 @@ authenticationController.logout = function (req, res, next) { plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function () { res.status(200).send(''); + + // Force session check for all connected socket.io clients with the same session id + sockets.in('sess_' + sessionID).emit('checkSession', 0); }); }); } else { diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 41f6739d36..38e73c1cc1 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -57,6 +57,7 @@ var ratelimit = require('../middleware/ratelimit'); socket.join('online_guests'); } + socket.join('sess_' + socket.request.signedCookies[nconf.get('sessionKey')]); io.sockets.sockets[socket.id].emit('checkSession', socket.uid); } From 6aa93362df1fafc442165a9e00573e297db131f0 Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Sat, 12 Nov 2016 09:02:15 -0500 Subject: [PATCH 007/131] Latest translations and fallbacks --- public/language/fr/error.json | 2 +- public/language/fr/global.json | 6 +++--- public/language/fr/topic.json | 2 +- public/language/ru/error.json | 2 +- public/language/ru/global.json | 6 +++--- public/language/sr/global.json | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/public/language/fr/error.json b/public/language/fr/error.json index ee051ba871..8bf9995e41 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -29,7 +29,7 @@ "username-too-long": "Nom d'utilisateur trop long", "password-too-long": "Mot de passe trop long", "user-banned": "Utilisateur banni", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Désolé, ce compte a été banni (Raison : %1)", "user-too-new": "Désolé, vous devez attendre encore %1 seconde(s) avant d'envoyer votre premier message", "blacklisted-ip": "Désolé, votre adresse IP a été bannie de cette communauté. Si vous pensez que c'est une erreur, veuillez contacter un administrateur.", "ban-expiry-missing": "Veuillez entrer une date de fin de banissement.", diff --git a/public/language/fr/global.json b/public/language/fr/global.json index 4cb94114ea..d45e801270 100644 --- a/public/language/fr/global.json +++ b/public/language/fr/global.json @@ -100,7 +100,7 @@ "unsaved-changes": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir naviguer tout de même ?", "reconnecting-message": "Il semble que votre connexion ait été perdue, veuillez patienter pendant que nous vous re-connectons.", "play": "Lire", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Ce site utilise des cookies pour vous permettre d'avoir la meilleure expérience possible.", + "cookies.accept": "Compris !", + "cookies.learn_more": "En savoir plus" } \ No newline at end of file diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index eb3604f04e..8d4144760e 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -13,7 +13,7 @@ "notify_me": "Être notifié des réponses dans ce sujet", "quote": "Citer", "reply": "Répondre", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Réponses : %1", "reply-as-topic": "Répondre à l'aide d'un sujet", "guest-login-reply": "Se connecter pour répondre", "edit": "Éditer", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index c87d49da8d..7b2d73ccc4 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -29,7 +29,7 @@ "username-too-long": "Имя пользователя слишком длинное", "password-too-long": "Пароль слишком длинный", "user-banned": "Участник заблокирован", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Учетная запись заблокирована (Причина: %1)", "user-too-new": "Вы можете написать своё первое сообщение через %1 сек.", "blacklisted-ip": "Извините, ваш IP адрес был забанен этим сообществом. Если вы считаете, что это ошибка, пожалуйста, свяжитесь с администратором.", "ban-expiry-missing": "Пожалуйста, укажите дату окончания этой блокировки", diff --git a/public/language/ru/global.json b/public/language/ru/global.json index 3564ddfeca..e86cad1624 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -100,7 +100,7 @@ "unsaved-changes": "У вас есть несохранённые изменения. Вы уверены, что хотите уйти?", "reconnecting-message": "Похоже, подключение к %1 было разорвано, подождите, пока мы пытаемся восстановить соединение.", "play": "Воспроизвести", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "cookies.message": "Этот сайт использует cookies для более удобного взаимодействия.", + "cookies.accept": "Понял", + "cookies.learn_more": "Подробнее" } \ No newline at end of file diff --git a/public/language/sr/global.json b/public/language/sr/global.json index 4071ddc6e1..89c8383361 100644 --- a/public/language/sr/global.json +++ b/public/language/sr/global.json @@ -53,12 +53,12 @@ "topics": "Теме", "posts": "Поруке", "best": "Најбоље", - "upvoters": "Позитивно гласају", - "upvoted": "Позитивни гласано", - "downvoters": "Негативно гласају", - "downvoted": "Негативни гласано", + "upvoters": "Позитивно гласали", + "upvoted": "Позитивно гласано", + "downvoters": "Негативно гласали", + "downvoted": "Негативно гласано", "views": "Прегледи", - "reputation": "Репутација", + "reputation": "Углед", "read_more": "прочитајте више", "more": "Више", "posted_ago_by_guest": "објављено %1 од стране госта.", From 0bf51c7fff705d32779af9255ea39b6d114b14ff Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sun, 13 Nov 2016 09:19:03 +0300 Subject: [PATCH 008/131] remove node 5 from build matrix --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fed11bb742..168469fd0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,6 @@ addons: - g++-4.8 node_js: - "6" - - "5" - "4" branches: only: From e8b38bb9f75483e0ad6e5dc1b513deb20199cf49 Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Mon, 14 Nov 2016 09:02:33 -0500 Subject: [PATCH 009/131] Latest translations and fallbacks --- public/language/vi/error.json | 16 ++++++++-------- public/language/vi/global.json | 16 ++++++++-------- public/language/vi/groups.json | 4 ++-- public/language/vi/login.json | 2 +- public/language/vi/pages.json | 4 ++-- public/language/vi/topic.json | 34 +++++++++++++++++----------------- public/language/vi/user.json | 34 +++++++++++++++++----------------- public/language/vi/users.json | 2 +- 8 files changed, 56 insertions(+), 56 deletions(-) diff --git a/public/language/vi/error.json b/public/language/vi/error.json index f9f0ec4bc3..5e45cc0ee1 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -29,7 +29,7 @@ "username-too-long": "Tên đăng nhập quá dài", "password-too-long": "Mật khẩu quá dài", "user-banned": "Tài khoản bị ban", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Xin lỗi, tài khoản này đã bị khóa (Lí do: %1)", "user-too-new": "Rất tiếc, bạn phải chờ %1 giây để đăng bài viết đầu tiên.", "blacklisted-ip": "Rất tiếc, địa chỉ IP của bạn đã bị cấm khỏi cộng đồng. Nếu bạn cảm thấy có gì không đúng, hãy liên lạc với người quản trị.", "ban-expiry-missing": "Vui lòng cung cấp ngày hết hạn của lệnh cấm", @@ -56,13 +56,13 @@ "post-delete-duration-expired-hours-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s) 2 phút(s)", "post-delete-duration-expired-days": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s)", "post-delete-duration-expired-days-hours": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", - "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", - "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "cant-delete-topic-has-reply": "Bạn không thể xóa chủ đề vì đã có 1 bình luận", + "cant-delete-topic-has-replies": "Bạn không thể xóa chủ đề này vì đã có %1 bình luận", "content-too-short": "Vui lòng nhập một bài viết dài hơn. Bài viết phải có tối thiểu %1 ký tự.", "content-too-long": "Vui lòng nhập một bài viết ngắn hơn. Bài viết chỉ có thể có tối đa %1 ký tự.", "title-too-short": "Vui lòng nhập tiêu đề dài hơn. Tiêu đề phải có tối thiểu %1 ký tự.", "title-too-long": "Vui lòng nhập tiêu đề ngắn hơn. Tiêu đề chỉ có thể có tối đa %1 ký tự.", - "category-not-selected": "Category not selected.", + "category-not-selected": "Chưa chọn category", "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", "too-many-posts-newbie": "Bạn chỉ có thể đăng bài mỗi %1 giây cho đến khi bạn tích luỹ được %2 điểm tín nhiệm - vui lòng đợi để tiếp tục đăng bài.", "tag-too-short": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", @@ -72,8 +72,8 @@ "still-uploading": "Vui lòng chờ upload", "file-too-big": "Kích cỡ file được cho phép tối đa là %1 kB - vui lòng tải lên file có dung lượng nhỏ hơn.", "guest-upload-disabled": "Khách (chưa có tài khoản) không có quyền tải lên file.", - "already-bookmarked": "You have already bookmarked this post", - "already-unbookmarked": "You have already unbookmarked this post", + "already-bookmarked": "Bạn đã bookmark chủ đề này rồi", + "already-unbookmarked": "Bạn đã hủy bookmark chủ đề này rồi", "cant-ban-other-admins": "Bạn không thể cấm được các quản trị viên khác", "cant-remove-last-admin": "Bạn là quản trị viên duy nhất. Hãy cho thành viên khác làm quản trị viên trước khi huỷ bỏ quyền quản trị của bạn.", "cant-delete-admin": "Hủy quyền quản trị của tài khoản này trước khi xóa", @@ -126,6 +126,6 @@ "cant-kick-self": "Bạn không thể kick chính bạn ra khỏi nhóm", "no-users-selected": "Chưa có người dùng(s) nào", "invalid-home-page-route": "Đường dẫn trang chủ không hợp lệ", - "invalid-session": "Session Mismatch", - "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." + "invalid-session": "Không đúng session", + "invalid-session-text": "Có vẻ như phiên đăng nhập của bạn đã không còn hoạt động nữa, hoặc không còn đúng với thông tin trên máy chủ. Vui lòng tải lại trang này" } \ No newline at end of file diff --git a/public/language/vi/global.json b/public/language/vi/global.json index 2af8e015f9..e151b6f629 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -10,7 +10,7 @@ "500.title": "Internal Error.", "500.message": "Úi chà! Có vẻ như có trục trặc rồi!", "400.title": "Bad Request.", - "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "400.message": "Có vẻ như đường dẫn này không hợp lệ, vui lòng kiểm tra và thử lại lần nữa. Bạn cũng có thể quay về trang chủ ngay.", "register": "Đăng ký", "login": "Đăng nhập", "please_log_in": "Xin hãy đăng nhập", @@ -75,7 +75,7 @@ "norecenttopics": "Không có chủ đề gần đây", "recentposts": "Số bài viết gần đây", "recentips": "Các IP vừa mới đăng nhập", - "moderator_tools": "Moderator Tools", + "moderator_tools": "Công cụ quản lí", "away": "Đang đi vắng", "dnd": "Đừng làm phiền", "invisible": "Ẩn", @@ -97,10 +97,10 @@ "upload_file": "Tải file lên", "upload": "Tải lên", "allowed-file-types": "Các định dạng file được cho phép là %1", - "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", - "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", - "play": "Play", - "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", - "cookies.accept": "Got it!", - "cookies.learn_more": "Learn More" + "unsaved-changes": "Có một vài thay đổi chưa được lưu. Bạn muốn rời đi ngay?", + "reconnecting-message": "Có vẻ như bạn đã mất kết nối tới %1, vui lòng đợi một lúc để chúng tôi thử kết nối lại.", + "play": "Chơi", + "cookies.message": "Trang web này sử dụng cookie để đảm bảo trải nghiệm tốt nhất cho người dùng", + "cookies.accept": "Đã rõ!", + "cookies.learn_more": "Xem thêm" } \ No newline at end of file diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json index d8a037b662..7ae06112fd 100644 --- a/public/language/vi/groups.json +++ b/public/language/vi/groups.json @@ -51,6 +51,6 @@ "membership.reject": "Từ chối", "new-group.group_name": "Tên nhóm", "upload-group-cover": "Tải ảnh bìa lên cho nhóm", - "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", - "bulk-invite": "Bulk Invite" + "bulk-invite-instructions": "Nhập danh sách username, ngăn cách bằng dấu phẩy, để mời vào nhóm", + "bulk-invite": "Mời nhiều người" } \ No newline at end of file diff --git a/public/language/vi/login.json b/public/language/vi/login.json index ea130d9ada..5097ddf701 100644 --- a/public/language/vi/login.json +++ b/public/language/vi/login.json @@ -8,5 +8,5 @@ "failed_login_attempt": "Đăng nhập không thành công", "login_successful": "Bạn đã đăng nhập thành công!", "dont_have_account": "Chưa có tài khoản?", - "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" + "logged-out-due-to-inactivity": "Bạn đã bị đăng xuất khỏi Admin Control Panel do không hoạt động quá lâu" } \ No newline at end of file diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json index 5adfa71850..4ac7cf00a9 100644 --- a/public/language/vi/pages.json +++ b/public/language/vi/pages.json @@ -13,7 +13,7 @@ "users/sort-posts": "Thành viên có nhiều bài đăng nhất", "users/sort-reputation": "Thành viên có điểm tín nhiệm cao nhất", "users/banned": "Thành viên đã bị cấm", - "users/most-flags": "Most flagged users", + "users/most-flags": "Những thành viên bị gắn cờ nhiều nhất", "users/search": "Tìm kiếm thành viên", "notifications": "Thông báo", "tags": "Tag", @@ -37,7 +37,7 @@ "account/posts": "Bài viết được đăng bởi %1", "account/topics": "Chủ đề được tạo bởi %1", "account/groups": "Nhóm của %1", - "account/bookmarks": "%1's Bookmarked Posts", + "account/bookmarks": "Đã bookmark %1's chủ đề", "account/settings": "Thiết lập", "account/watched": "Chủ đề %1 đang theo dõi", "account/upvoted": "Bài viết %1 tán thành", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 971c13a978..6ef833d39e 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -13,7 +13,7 @@ "notify_me": "Được thông báo khi có trả lời mới trong chủ đề này", "quote": "Trích dẫn", "reply": "Trả lời", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "%1 trả lời", "reply-as-topic": "Trả lời dưới dạng chủ đề", "guest-login-reply": "Hãy đăng nhập để trả lời", "edit": "Chỉnh sửa", @@ -32,21 +32,21 @@ "bookmark_instructions": "Bấm vào đây để quay về đọc bài viết mới nhất trong chủ đề này.", "flag_title": "Flag bài viết này để chỉnh sửa", "flag_success": "Chủ đề này đã được flag để chỉnh sửa", - "flag_manage_title": "Flagged post in %1", - "flag_manage_history": "Action History", - "flag_manage_no_history": "No event history to report", - "flag_manage_assignee": "Assignee", - "flag_manage_state": "State", + "flag_manage_title": "Bài viết bị gắn cờ trong %1", + "flag_manage_history": "Lịch sử hoạt động", + "flag_manage_no_history": "Không có lịch sử sự kiện nào", + "flag_manage_assignee": "Người giao việc", + "flag_manage_state": "Trạng thái", "flag_manage_state_open": "New/Open", - "flag_manage_state_wip": "Work in Progress", - "flag_manage_state_resolved": "Resolved", - "flag_manage_state_rejected": "Rejected", - "flag_manage_notes": "Shared Notes", - "flag_manage_update": "Update Flag Status", - "flag_manage_history_assignee": "Assigned to %1", - "flag_manage_history_state": "Updated state to %1", - "flag_manage_history_notes": "Updated flag notes", - "flag_manage_saved": "Flag Details Updated", + "flag_manage_state_wip": "Công việc đang thực thi", + "flag_manage_state_resolved": "Đã hoàn thành", + "flag_manage_state_rejected": "Đã từ chối", + "flag_manage_notes": "Những ghi chú được chia sẻ", + "flag_manage_update": "Cập nhật trạng thái gắn c", + "flag_manage_history_assignee": "Đã giao cho %1", + "flag_manage_history_state": "Cập nhật trạng thái thành %1", + "flag_manage_history_notes": "Đã cập nhật ghi chú gắn c", + "flag_manage_saved": "Đã cập nhật nội dung gắn c", "deleted_message": "Chủ đề này đã bị xóa. Chỉ ban quản trị mới xem được.", "following_topic.message": "Từ giờ bạn sẽ nhận được thông báo khi có ai đó gửi bài viết trong chủ đề này", "not_following_topic.message": "Bạn có thể xem chủ đề này trong danh sách chủ đề chưa xem, nhưng bạn sẽ không nhận thông báo khi có ai đó đăng bài viết trong chủ đề này", @@ -67,7 +67,7 @@ "not-watching.description": "Không thông báo tôi các trả lời mới.
Hiển thị mục chưa đọc nếu danh mục bị bỏ qua.", "ignoring.description": "Không thông báo tôi các trả lời mới.
Không hiển thị các mục chưa đọc.", "thread_tools.title": "Công cụ", - "thread_tools.markAsUnreadForAll": "Mark unread for all", + "thread_tools.markAsUnreadForAll": "Đánh dấu tất cả thành chưa đọc", "thread_tools.pin": "Pin chủ đề", "thread_tools.unpin": "Bỏ pin chủ đề", "thread_tools.lock": "Khóa chủ đề", @@ -92,7 +92,7 @@ "confirm_fork": "Tạo bảo sao", "bookmark": "Bookmark", "bookmarks": "Bookmarks", - "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "bookmarks.has_no_bookmarks": "Bạn chưa bookmark bài viết nào cả.", "loading_more_posts": "Tải thêm các bài gửi khác", "move_topic": "Chuyển chủ đề", "move_topics": "Di chuyển chủ đề", diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 61100679e7..9a98f20b7c 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -89,11 +89,11 @@ "topics_per_page": "Số chủ đề trong một trang", "posts_per_page": "Số bài viết trong một trang", "notification_sounds": "Phát âm thanh khi bạn nhận được thông báo mới", - "notifications_and_sounds": "Notifications & Sounds", - "incoming-message-sound": "Incoming message sound", - "outgoing-message-sound": "Outgoing message sound", - "notification-sound": "Notification sound", - "no-sound": "No sound", + "notifications_and_sounds": "Thông báo & Âm thanh", + "incoming-message-sound": "Âm báo tin nhắn tới", + "outgoing-message-sound": "Âm báo tin nhắn đi", + "notification-sound": "Âm thanh thông báo", + "no-sound": "Không có âm thanh", "browsing": "Đang xem cài đặt", "open_links_in_new_tab": "Mở link trong tab mới.", "enable_topic_searching": "Bật In-topic Searching", @@ -101,8 +101,8 @@ "delay_image_loading": "Việc tải ảnh đang bị chậm", "image_load_delay_help": "Nếu được bật, toàn bộ ảnh trong chủ đề sẽ chỉ được tải khi người dùng kéo chuột tới", "scroll_to_my_post": "Sau khi đăng một trả lời thì hiển thị bài viết mới", - "follow_topics_you_reply_to": "Watch topics that you reply to", - "follow_topics_you_create": "Watch topics you create", + "follow_topics_you_reply_to": "Theo dõi những chủ đề mà bạn đã bình luận", + "follow_topics_you_create": "Theo dõi những chủ đề do bạn t", "grouptitle": "Tên nhóm", "no-group-title": "Không có tên nhóm", "select-skin": "Chọn một giao diện", @@ -114,16 +114,16 @@ "sso.title": "Đăng nhập một lần", "sso.associated": "Đã liên kết với", "sso.not-associated": "Nhấn vào đây để liên kết với", - "info.latest-flags": "Latest Flags", - "info.no-flags": "No Flagged Posts Found", - "info.ban-history": "Recent Ban History", - "info.no-ban-history": "This user has never been banned", - "info.banned-until": "Banned until %1", + "info.latest-flags": "Cờ mới nhất", + "info.no-flags": "Không có bài viết nào bị gắn c", + "info.ban-history": "Lịch sử khóa tài khoản gần đây", + "info.no-ban-history": "Người dùng này chưa từng bị khóa tài khoản", + "info.banned-until": "Bị khóa tài khoản tới %1", "info.banned-permanently": "Bị cấm vĩnh viễn", "info.banned-reason-label": "Lý do", - "info.banned-no-reason": "No reason given.", - "info.username-history": "Username History", - "info.email-history": "Email History", - "info.moderation-note": "Moderation Note", - "info.moderation-note.success": "Moderation note saved" + "info.banned-no-reason": "Không có lí do.", + "info.username-history": "Lịch sử tên người d", + "info.email-history": "Lịch sử email", + "info.moderation-note": "Ghi chú quản lí", + "info.moderation-note.success": "Đã lưu ghi chú quản l" } \ No newline at end of file diff --git a/public/language/vi/users.json b/public/language/vi/users.json index 5a830cf2de..b694a2bd32 100644 --- a/public/language/vi/users.json +++ b/public/language/vi/users.json @@ -2,7 +2,7 @@ "latest_users": "Thành viên mới nhất", "top_posters": "Thành viên đăng bài nhiều nhất", "most_reputation": "Thành viên có điểm tín nhiệm cao nhất", - "most_flags": "Most Flags", + "most_flags": "Bị gắn cờ nhiều nhất", "search": "Tìm kiếm", "enter_username": "Gõ tên thành viên để tìm kiếm", "load_more": "Tải thêm", From 2f2eb1457dd3f0c27b1a26b979343dd9ec180020 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Nov 2016 14:01:05 -0500 Subject: [PATCH 010/131] auto-redirect to SSO flow should work in subfolders too --- src/controllers/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/index.js b/src/controllers/index.js index 8bf4a798e4..2671dc0657 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -132,7 +132,7 @@ Controllers.login = function (req, res, next) { external: data.authentication[0].url }); } else { - return res.redirect(data.authentication[0].url); + return res.redirect(nconf.get('relative_path') + data.authentication[0].url); } } if (req.uid) { From d19171decc6478489b13bf12e05954415322017f Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Mon, 14 Nov 2016 19:20:56 +0000 Subject: [PATCH 011/131] updated new chat strings for profile --- public/language/ar/user.json | 3 ++- public/language/bg/user.json | 3 ++- public/language/bn/user.json | 3 ++- public/language/cs/user.json | 3 ++- public/language/da/user.json | 3 ++- public/language/de/user.json | 3 ++- public/language/el/user.json | 3 ++- public/language/en@pirate/user.json | 3 ++- public/language/en_US/user.json | 3 ++- public/language/es/user.json | 3 ++- public/language/et/user.json | 3 ++- public/language/fa_IR/user.json | 3 ++- public/language/fi/user.json | 3 ++- public/language/fr/user.json | 3 ++- public/language/gl/user.json | 3 ++- public/language/he/user.json | 3 ++- public/language/hu/user.json | 3 ++- public/language/id/user.json | 3 ++- public/language/it/user.json | 3 ++- public/language/ja/user.json | 3 ++- public/language/ko/user.json | 3 ++- public/language/lt/user.json | 3 ++- public/language/ms/user.json | 3 ++- public/language/nb/user.json | 3 ++- public/language/nl/user.json | 3 ++- public/language/pl/user.json | 3 ++- public/language/pt_BR/user.json | 3 ++- public/language/ro/user.json | 3 ++- public/language/ru/user.json | 3 ++- public/language/rw/user.json | 3 ++- public/language/sc/user.json | 3 ++- public/language/sk/user.json | 3 ++- public/language/sl/user.json | 3 ++- public/language/sr/user.json | 3 ++- public/language/sv/user.json | 3 ++- public/language/th/user.json | 3 ++- public/language/tr/user.json | 3 ++- public/language/vi/user.json | 3 ++- public/language/zh_CN/user.json | 3 ++- public/language/zh_TW/user.json | 3 ++- 40 files changed, 80 insertions(+), 40 deletions(-) diff --git a/public/language/ar/user.json b/public/language/ar/user.json index b3515464d8..1387e8b2bb 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -31,7 +31,8 @@ "signature": "توقيع", "birthday": "عيد ميلاد", "chat": "محادثة", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "تابع", "unfollow": "إلغاء المتابعة", "more": "المزيد", diff --git a/public/language/bg/user.json b/public/language/bg/user.json index c9e8d97b78..7ab3f61f40 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -31,7 +31,8 @@ "signature": "Подпис", "birthday": "Рождена дата", "chat": "Разговор", - "chat_with": "Разговор с %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Следване", "unfollow": "Спиране на следването", "more": "Още", diff --git a/public/language/bn/user.json b/public/language/bn/user.json index 40aa4b92a2..296614eeb8 100644 --- a/public/language/bn/user.json +++ b/public/language/bn/user.json @@ -31,7 +31,8 @@ "signature": "স্বাক্ষর", "birthday": "জন্মদিন", "chat": "বার্তালাপ", - "chat_with": "চ্যাট উইথ %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "অনুসরন করুন", "unfollow": "অনুসরন করা থেকে বিরত থাকুন", "more": "আরো...", diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 7aff6b7cbf..8f25d1a1d1 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Datum narození", "chat": "Chat", - "chat_with": "Chatovat s %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sledovat", "unfollow": "Nesledovat", "more": "Více", diff --git a/public/language/da/user.json b/public/language/da/user.json index c47328889b..a83e677248 100644 --- a/public/language/da/user.json +++ b/public/language/da/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Fødselsdag", "chat": "Chat", - "chat_with": "Chat med %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Følg", "unfollow": "Følg ikke", "more": "Mere", diff --git a/public/language/de/user.json b/public/language/de/user.json index 2b8e884fa8..5d861943c4 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Geburtstag", "chat": "Chat", - "chat_with": "Chat mit %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Folgen", "unfollow": "Nicht mehr folgen", "more": "Mehr", diff --git a/public/language/el/user.json b/public/language/el/user.json index de5f4aa56b..796edb79b7 100644 --- a/public/language/el/user.json +++ b/public/language/el/user.json @@ -31,7 +31,8 @@ "signature": "Υπογραφή", "birthday": "Γενέθλια", "chat": "Συνομιλία", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ακολούθησε", "unfollow": "Μην Ακολουθείς", "more": "More", diff --git a/public/language/en@pirate/user.json b/public/language/en@pirate/user.json index 4ebf6d9984..eec5dc8cd8 100644 --- a/public/language/en@pirate/user.json +++ b/public/language/en@pirate/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Birthday", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Follow", "unfollow": "Unfollow", "more": "More", diff --git a/public/language/en_US/user.json b/public/language/en_US/user.json index 05002d86af..00fc3826df 100644 --- a/public/language/en_US/user.json +++ b/public/language/en_US/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Birthday", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Follow", "unfollow": "Unfollow", "more": "More", diff --git a/public/language/es/user.json b/public/language/es/user.json index 972031128d..61c7d6d8e6 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Cumpleaños", "chat": "Chat", - "chat_with": "Chatear con %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Seguir", "unfollow": "Dejar de seguir", "more": "Más", diff --git a/public/language/et/user.json b/public/language/et/user.json index f37129994a..e01449cc31 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -31,7 +31,8 @@ "signature": "Allkiri", "birthday": "Sünnipäev", "chat": "Vestlus", - "chat_with": "Vestle kasutajaga %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Jälgi", "unfollow": "Ära jälgi enam", "more": "Rohkem", diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index 0c320a6e98..c00a7f4258 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -31,7 +31,8 @@ "signature": "امضا", "birthday": "روز تولد", "chat": "چت", - "chat_with": "چت کردن با %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "دنبال کن", "unfollow": "دنبال نکن", "more": "بیشتر", diff --git a/public/language/fi/user.json b/public/language/fi/user.json index f445bd1e7d..7fe5ad7de0 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -31,7 +31,8 @@ "signature": "Allekirjoitus", "birthday": "Syntymäpäivä", "chat": "Keskustele", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Seuraa", "unfollow": "Älä seuraa", "more": "More", diff --git a/public/language/fr/user.json b/public/language/fr/user.json index 98cb990439..650bda5962 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -31,7 +31,8 @@ "signature": "Signature", "birthday": "Anniversaire", "chat": "Discussion", - "chat_with": "Discussion avec %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "S'abonner", "unfollow": "Se désabonner", "more": "Plus", diff --git a/public/language/gl/user.json b/public/language/gl/user.json index 235222da8d..b37803f778 100644 --- a/public/language/gl/user.json +++ b/public/language/gl/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Aniversario", "chat": "Chat", - "chat_with": "Parolando con %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Seguir", "unfollow": "Deixar de seguir", "more": "máis", diff --git a/public/language/he/user.json b/public/language/he/user.json index 014fc5053e..40e3ce5bb8 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -31,7 +31,8 @@ "signature": "חתימה", "birthday": "יום הולדת", "chat": "צ'אט", - "chat_with": "צ'אט עם %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "עקוב", "unfollow": "הפסק לעקוב", "more": "עוד", diff --git a/public/language/hu/user.json b/public/language/hu/user.json index fbfc423747..fa9ea58af2 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -31,7 +31,8 @@ "signature": "Aláírás", "birthday": "Szülinap", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Követés", "unfollow": "Nem követem", "more": "More", diff --git a/public/language/id/user.json b/public/language/id/user.json index fa486a4d85..48f835b5dc 100644 --- a/public/language/id/user.json +++ b/public/language/id/user.json @@ -31,7 +31,8 @@ "signature": "Tanda Pengenal", "birthday": "Hari Lahir", "chat": "Percakapan", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ikuti", "unfollow": "Tinggalkan", "more": "More", diff --git a/public/language/it/user.json b/public/language/it/user.json index 259aa5ca7c..faa884c131 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Data di nascita", "chat": "Chat", - "chat_with": "Chatta con %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Segui", "unfollow": "Smetti di seguire", "more": "Altro", diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 14081a8b2b..e4180d8fe1 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -31,7 +31,8 @@ "signature": "署名", "birthday": "誕生日", "chat": "チャット", - "chat_with": "%1とチャットをする", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "フォロー", "unfollow": "フォロー解除", "more": "つづき", diff --git a/public/language/ko/user.json b/public/language/ko/user.json index aadde8ab0a..5317c21dbc 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -31,7 +31,8 @@ "signature": "서명", "birthday": "생일", "chat": "채팅", - "chat_with": "%1 님과 대화", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "팔로우", "unfollow": "팔로우 취소", "more": "더 보기", diff --git a/public/language/lt/user.json b/public/language/lt/user.json index 5a484bb03d..6924abc638 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -31,7 +31,8 @@ "signature": "Parašas", "birthday": "Gimimo diena", "chat": "Susirašinėti", - "chat_with": "Susirašinėti su %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sekti", "unfollow": "Nesekti", "more": "Daugiau", diff --git a/public/language/ms/user.json b/public/language/ms/user.json index 0857bfea14..bc4e59e3ec 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -31,7 +31,8 @@ "signature": "Tandatangan", "birthday": "Tarikh lahir", "chat": "Bersembang", - "chat_with": "Sembang dengan %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Ikuti", "unfollow": "Henti mengikuti", "more": "Lagi", diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 5cf658d9c5..68f52d6882 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Bursdag", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Følg", "unfollow": "Avfølg", "more": "Mer", diff --git a/public/language/nl/user.json b/public/language/nl/user.json index de02a1b8c5..1d54103a98 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -31,7 +31,8 @@ "signature": "Handtekening", "birthday": "Verjaardag", "chat": "Chat", - "chat_with": "Chatten met %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Volgen", "unfollow": "Ontvolgen", "more": "Meer", diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 03ef649329..272536dbf7 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -31,7 +31,8 @@ "signature": "Sygnatura", "birthday": "Urodziny", "chat": "Rozmawiaj", - "chat_with": "Rozmawiaj z %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Śledź", "unfollow": "Przestań śledzić", "more": "Więcej", diff --git a/public/language/pt_BR/user.json b/public/language/pt_BR/user.json index da3dba46e5..cb72857600 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt_BR/user.json @@ -31,7 +31,8 @@ "signature": "Assinatura", "birthday": "Aniversário", "chat": "Chat", - "chat_with": "Conversar com %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Seguir", "unfollow": "Deixar de Seguir", "more": "Mais", diff --git a/public/language/ro/user.json b/public/language/ro/user.json index d38857c5b0..ce17895230 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -31,7 +31,8 @@ "signature": "Semnătură", "birthday": "Zi de naștere", "chat": "Conversație", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Urmărește", "unfollow": "Oprește urmărirea", "more": "Mai multe", diff --git a/public/language/ru/user.json b/public/language/ru/user.json index a2fbf3f9a0..3e63279cec 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -31,7 +31,8 @@ "signature": "Подпись", "birthday": "День рождения", "chat": "Чат", - "chat_with": "Чат с участником %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Подписаться", "unfollow": "Отписаться", "more": "Больше", diff --git a/public/language/rw/user.json b/public/language/rw/user.json index e569405b0b..97a41fc438 100644 --- a/public/language/rw/user.json +++ b/public/language/rw/user.json @@ -31,7 +31,8 @@ "signature": "Intero", "birthday": "Itariki y'Amavuko", "chat": "Mu Gikari", - "chat_with": "Ganira na %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Kurikira", "unfollow": "Ntukurikire", "more": "Ibindi", diff --git a/public/language/sc/user.json b/public/language/sc/user.json index ff9e4b9478..003b4d9860 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -31,7 +31,8 @@ "signature": "Firma", "birthday": "Cumpleannu", "chat": "Tzarra", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Sighi", "unfollow": "Non sighes prus", "more": "More", diff --git a/public/language/sk/user.json b/public/language/sk/user.json index 354a2c084d..393140e29a 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Dátum narodenia", "chat": "Konverzácia", - "chat_with": "Rozhovor s %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Nasledovať", "unfollow": "Prestať sledovať", "more": "Viac", diff --git a/public/language/sl/user.json b/public/language/sl/user.json index aee9a0107b..88ec8e16f0 100644 --- a/public/language/sl/user.json +++ b/public/language/sl/user.json @@ -31,7 +31,8 @@ "signature": "Podpis", "birthday": "Rojstni datum", "chat": "Klepet", - "chat_with": "Klepet z %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Spremljaj", "unfollow": "Ne spremljaj", "more": "Več", diff --git a/public/language/sr/user.json b/public/language/sr/user.json index b30b6825c2..a93a7fa059 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -31,7 +31,8 @@ "signature": "Потпис", "birthday": "Рођендан", "chat": "Ђаскање", - "chat_with": "Ћаскање са %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Прати", "unfollow": "Не прати", "more": "Више", diff --git a/public/language/sv/user.json b/public/language/sv/user.json index 6116e08ba7..e058143f6d 100644 --- a/public/language/sv/user.json +++ b/public/language/sv/user.json @@ -31,7 +31,8 @@ "signature": "Signatur", "birthday": "Födelsedag", "chat": "Chatta", - "chat_with": "Chatta med %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Följ", "unfollow": "Sluta följ", "more": "Mer", diff --git a/public/language/th/user.json b/public/language/th/user.json index c40a748a91..1bd6509f61 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -31,7 +31,8 @@ "signature": "ลายเซ็น", "birthday": "วันเกิด", "chat": "แชท", - "chat_with": "Chat with %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "ติดตาม", "unfollow": "เลิกติดตาม", "more": "More", diff --git a/public/language/tr/user.json b/public/language/tr/user.json index d507332b0c..9dc167a5b2 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -31,7 +31,8 @@ "signature": "İmza", "birthday": "Doğum Tarihi", "chat": "Sohbet", - "chat_with": "%1 ile Sohbet", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Takip Et", "unfollow": "Takip etme", "more": "Daha Fazla", diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 9a98f20b7c..6b8678c82c 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -31,7 +31,8 @@ "signature": "Chữ ký", "birthday": "Ngày sinh ", "chat": "Chat", - "chat_with": "Chat với %1", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "Theo dõi", "unfollow": "Hủy theo dõi", "more": "Xem thêm", diff --git a/public/language/zh_CN/user.json b/public/language/zh_CN/user.json index a487b7ee56..816df8ef63 100644 --- a/public/language/zh_CN/user.json +++ b/public/language/zh_CN/user.json @@ -31,7 +31,8 @@ "signature": "签名档", "birthday": "生日", "chat": "聊天", - "chat_with": "与 %1 聊天", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "关注", "unfollow": "取消关注", "more": "更多", diff --git a/public/language/zh_TW/user.json b/public/language/zh_TW/user.json index 7eebcd2ec8..ff00e53d09 100644 --- a/public/language/zh_TW/user.json +++ b/public/language/zh_TW/user.json @@ -31,7 +31,8 @@ "signature": "簽名", "birthday": "生日", "chat": "聊天", - "chat_with": "與 %1 聊天", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", "follow": "跟隨", "unfollow": "取消跟隨", "more": "更多", From 2476ab368418190221a8e806eb35da7ff047609f Mon Sep 17 00:00:00 2001 From: barisusakli Date: Tue, 15 Nov 2016 12:45:00 +0300 Subject: [PATCH 012/131] closes #5202 --- src/controllers/accounts/edit.js | 6 ++---- src/controllers/accounts/helpers.js | 17 +++++++++++------ src/middleware/user.js | 8 +++----- src/privileges/users.js | 25 +++++++++++++++++++++++++ src/socket.io/user.js | 8 +++----- src/socket.io/user/profile.js | 18 +++++++++++++----- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 3fe2c57092..1c61756da8 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -12,6 +12,7 @@ var plugins = require('../../plugins'); var helpers = require('../helpers'); var groups = require('../../groups'); var accountHelpers = require('./helpers'); +var privileges = require('../../privileges'); var editController = {}; @@ -118,11 +119,8 @@ editController.uploadPicture = function (req, res, next) { }, function (uid, next) { updateUid = uid; - if (parseInt(req.uid, 10) === parseInt(uid, 10)) { - return next(null, true); - } - user.isAdminOrGlobalMod(req.uid, next); + privileges.users.canEdit(req.uid, uid, next); }, function (isAllowed, next) { if (!isAllowed) { diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 1e120cc0d0..7a90930053 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -24,13 +24,16 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { } async.parallel({ - userData : function (next) { + userData: function (next) { user.getUserData(uid, next); }, - userSettings : function (next) { + isTargetAdmin: function (next) { + user.isAdministrator(uid, next); + }, + userSettings: function (next) { user.getSettings(uid, next); }, - isAdmin : function (next) { + isAdmin: function (next) { user.isAdministrator(callerUID, next); }, isGlobalModerator: function (next) { @@ -87,7 +90,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { userData.fullname = ''; } - if (isAdmin || isGlobalModerator || isSelf) { + if (isAdmin || isSelf || (isGlobalModerator && !results.isTargetAdmin)) { userData.ips = results.ips; } @@ -98,16 +101,18 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { userData.uid = userData.uid; userData.yourid = callerUID; userData.theirid = userData.uid; + userData.isTargetAdmin = results.isTargetAdmin; userData.isAdmin = isAdmin; userData.isGlobalModerator = isGlobalModerator; userData.isModerator = isModerator; userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator; userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator; - userData.canBan = isAdmin || isGlobalModerator; + userData.canEdit = isAdmin || (isGlobalModerator && !results.isTargetAdmin); + userData.canBan = isAdmin || (isGlobalModerator && !results.isTargetAdmin); userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1); userData.isSelf = isSelf; userData.isFollowing = results.isFollowing; - userData.showHidden = isSelf || isAdmin || isGlobalModerator; + userData.showHidden = isSelf || isAdmin || (isGlobalModerator && !results.isTargetAdmin); userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : []; userData.disableSignatures = meta.config.disableSignatures !== undefined && parseInt(meta.config.disableSignatures, 10) === 1; userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; diff --git a/src/middleware/user.js b/src/middleware/user.js index c4c39a1e81..4b4af39c5f 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -2,8 +2,10 @@ var async = require('async'); var nconf = require('nconf'); + var meta = require('../meta'); var user = require('../user'); +var privileges = require('../privileges'); var controllers = { helpers: require('../controllers/helpers') @@ -29,11 +31,7 @@ module.exports = function (middleware) { user.getUidByUserslug(req.params.userslug, next); }, function (uid, next) { - if (parseInt(uid, 10) === req.uid) { - return next(null, true); - } - - user.isAdminOrGlobalMod(req.uid, next); + privileges.users.canEdit(req.uid, uid, next); }, function (allowed, next) { if (allowed) { diff --git a/src/privileges/users.js b/src/privileges/users.js index 4f6341be9f..82553647a4 100644 --- a/src/privileges/users.js +++ b/src/privileges/users.js @@ -136,4 +136,29 @@ module.exports = function (privileges) { }); } + privileges.users.canEdit = function (callerUid, uid, callback) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return process.nextTick(callback, null, true); + } + + async.parallel({ + isAdmin: function (next) { + privileges.users.isAdministrator(callerUid, next); + }, + isGlobalMod: function (next) { + privileges.users.isGlobalModerator(callerUid, next); + }, + isTargetAdmin: function (next) { + privileges.users.isAdministrator(uid, next); + } + }, function (err, results) { + if (err) { + return callback(err); + } + var canEdit = results.isAdmin || (results.isGlobalMod && !results.isTargetAdmin); + + callback(null, canEdit); + }); + }; + }; \ No newline at end of file diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 4d5f752b5d..1c322fe1e4 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -13,6 +13,7 @@ var events = require('../events'); var emailer = require('../emailer'); var db = require('../database'); var apiController = require('../controllers/api'); +var privileges = require('../privileges'); var SocketUser = {}; @@ -211,10 +212,7 @@ SocketUser.saveSettings = function (socket, data, callback) { async.waterfall([ function (next) { - if (socket.uid === parseInt(data.uid, 10)) { - return next(null, true); - } - user.isAdminOrGlobalMod(socket.uid, next); + privileges.users.canEdit(socket.uid, data.uid, next); }, function (allowed, next) { if (!allowed) { @@ -326,7 +324,7 @@ SocketUser.setModerationNote = function (socket, data, callback) { async.waterfall([ function (next) { - user.isAdminOrGlobalMod(socket.uid, next); + privileges.users.canEdit(socket.uid, data.uid, next); }, function (allowed, next) { if (allowed) { diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 8e2cbda7bd..89d49f59c3 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -5,6 +5,7 @@ var async = require('async'); var user = require('../../user'); var meta = require('../../meta'); var events = require('../../events'); +var privileges = require('../../privileges'); module.exports = function (SocketUser) { @@ -127,18 +128,25 @@ module.exports = function (SocketUser) { return next(new Error('[[error:invalid-data]]')); } - user.isAdminOrGlobalMod(socket.uid, next); + async.parallel({ + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(socket.uid, next); + }, + canEdit: function (next) { + privileges.users.canEdit(socket.uid, data.uid, next); + } + }, next); }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod && socket.uid !== parseInt(data.uid, 10)) { + function (results, next) { + if (!results.canEdit) { return next(new Error('[[error:no-privileges]]')); } - if (!isAdminOrGlobalMod && parseInt(meta.config['username:disableEdit'], 10) === 1) { + if (!results.isAdminOrGlobalMod && parseInt(meta.config['username:disableEdit'], 10) === 1) { data.username = oldUserData.username; } - if (!isAdminOrGlobalMod && parseInt(meta.config['email:disableEdit'], 10) === 1) { + if (!results.isAdminOrGlobalMod && parseInt(meta.config['email:disableEdit'], 10) === 1) { data.email = oldUserData.email; } From 3380f61985c0b2bcfe67aba69c17c92487f60939 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Tue, 15 Nov 2016 14:01:06 +0300 Subject: [PATCH 013/131] more tests --- src/socket.io/admin/user.js | 42 +++++++------- src/user/auth.js | 64 ++++++++++----------- test/socket.io.js | 108 +++++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 59 deletions(-) diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 31fa107cfb..fb6d6e31ec 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -60,7 +60,6 @@ User.createUser = function (socket, userData, callback) { user.create(userData, callback); }; - User.resetLockouts = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); @@ -185,25 +184,25 @@ function deleteUsers(socket, uids, method, callback) { } User.search = function (socket, data, callback) { - user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function (err, searchData) { - if (err) { - return callback(err); - } - if (!searchData.users.length) { - return callback(null, searchData); - } - - var userData = searchData.users; - var uids = userData.map(function (user) { - return user && user.uid; - }); - - user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate'], function (err, userInfo) { - if (err) { - return callback(err); + var searchData; + async.waterfall([ + function (next) { + user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, next); + }, + function (_searchData, next) { + searchData = _searchData; + if (!searchData.users.length) { + return callback(null, searchData); } - userData.forEach(function (user, index) { + var uids = searchData.users.map(function (user) { + return user && user.uid; + }); + + user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate'], next); + }, + function (userInfo, next) { + searchData.users.forEach(function (user, index) { if (user && userInfo[index]) { user.email = validator.escape(String(userInfo[index].email || '')); user.flags = userInfo[index].flags || 0; @@ -211,10 +210,9 @@ User.search = function (socket, data, callback) { user.joindateISO = userInfo[index].joindateISO; } }); - - callback(null, searchData); - }); - }); + next(null, searchData); + } + ], callback); }; User.deleteInvitation = function (socket, data, callback) { diff --git a/src/user/auth.js b/src/user/auth.js index a60f59fea4..a6222728e4 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -10,43 +10,37 @@ module.exports = function (User) { User.auth = {}; User.auth.logAttempt = function (uid, ip, callback) { - db.exists('lockout:' + uid, function (err, exists) { - if (err) { - return callback(err); - } - - if (exists) { - return callback(new Error('[[error:account-locked]]')); - } - - db.increment('loginAttempts:' + uid, function (err, attempts) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.exists('lockout:' + uid, next); + }, + function (exists, next) { + if (exists) { + return callback(new Error('[[error:account-locked]]')); } - - if ((meta.config.loginAttempts || 5) < attempts) { - // Lock out the account - db.set('lockout:' + uid, '', function (err) { - if (err) { - return callback(err); - } - var duration = 1000 * 60 * (meta.config.lockoutDuration || 60); - - db.delete('loginAttempts:' + uid); - db.pexpire('lockout:' + uid, duration); - events.log({ - type: 'account-locked', - uid: uid, - ip: ip - }); - callback(new Error('[[error:account-locked]]')); - }); - } else { - db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60); - callback(); + db.increment('loginAttempts:' + uid, next); + }, + function (attemps, next) { + var loginAttempts = parseInt(meta.config.loginAttempts, 10) || 5; + if (attemps <= loginAttempts) { + return db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60, callback); } - }); - }); + // Lock out the account + db.set('lockout:' + uid, '', next); + }, + function (next) { + var duration = 1000 * 60 * (meta.config.lockoutDuration || 60); + + db.delete('loginAttempts:' + uid); + db.pexpire('lockout:' + uid, duration); + events.log({ + type: 'account-locked', + uid: uid, + ip: ip + }); + next(new Error('[[error:account-locked]]')); + } + ], callback); }; User.auth.clearLoginAttempts = function (uid) { diff --git a/test/socket.io.js b/test/socket.io.js index 95fc8d911b..345d8782f4 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -30,7 +30,7 @@ describe('socket.io', function () { before(function (done) { async.series([ async.apply(user.create, { username: 'admin', password: 'adminpwd' }), - async.apply(user.create, { username: 'regular', password: 'regularpwd' }), + async.apply(user.create, { username: 'regular', password: 'regularpwd', email: 'regular@test.com'}), async.apply(categories.create, { name: 'Test Category', description: 'Test category created by testing script' @@ -42,7 +42,7 @@ describe('socket.io', function () { adminUid = data[0]; regularUid = data[1]; cid = data[2].cid; - + groups.resetCache(); groups.join('administrators', data[0], done); }); }); @@ -171,6 +171,110 @@ describe('socket.io', function () { }); }); + it('should make user admin', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.makeAdmins({uid: adminUid}, [regularUid], function (err) { + assert.ifError(err); + groups.isMember(regularUid, 'administrators', function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }); + }); + + it('should make user non-admin', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.removeAdmins({uid: adminUid}, [regularUid], function (err) { + assert.ifError(err); + groups.isMember(regularUid, 'administrators', function (err, isMember) { + assert.ifError(err); + assert(!isMember); + done(); + }); + }); + }); + + describe('create/delete', function () { + var socketAdmin = require('../src/socket.io/admin'); + var uid; + it('should create a user', function (done) { + socketAdmin.user.createUser({uid: adminUid}, {username: 'foo1'}, function (err, _uid) { + assert.ifError(err); + uid = _uid; + groups.isMember(uid, 'registered-users', function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }); + }); + + it('should delete users', function (done) { + socketAdmin.user.deleteUsers({uid: adminUid}, [uid], function (err) { + assert.ifError(err); + groups.isMember(uid, 'registered-users', function (err, isMember) { + assert.ifError(err); + assert(!isMember); + done(); + }); + }); + }); + + it('should delete users and their content', function (done) { + socketAdmin.user.deleteUsersAndContent({uid: adminUid}, [uid], function (err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('should error with invalid data', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.createUser({uid: adminUid}, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should reset lockouts', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.resetLockouts({uid: adminUid}, [regularUid], function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should reset flags', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.resetFlags({uid: adminUid}, [regularUid], function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should validate emails', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.validateEmail({uid: adminUid}, [regularUid], function (err) { + assert.ifError(err); + user.getUserField(regularUid, 'email:confirmed', function (err, emailConfirmed) { + assert.ifError(err); + assert.equal(parseInt(emailConfirmed, 10), 1); + done(); + }); + }); + }); + + it('should search users', function (done) { + var socketAdmin = require('../src/socket.io/admin'); + socketAdmin.user.search({uid: adminUid}, {query: 'reg', searchBy: 'username'}, function (err, data) { + assert.ifError(err); + assert.equal(data.matchCount, 1); + assert.equal(data.users[0].username, 'regular'); + done(); + }); + }); + after(function (done) { db.emptydb(done); }); From 682afbb440d32052340d4337a4744b90e5818d03 Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Tue, 15 Nov 2016 09:02:16 -0500 Subject: [PATCH 014/131] Latest translations and fallbacks --- public/language/bg/user.json | 4 ++-- public/language/es/user.json | 4 ++-- public/language/fa_IR/user.json | 4 ++-- public/language/fr/user.json | 4 ++-- public/language/pl/error.json | 2 +- public/language/pl/topic.json | 2 +- public/language/pl/user.json | 4 ++-- public/language/sk/user.json | 4 ++-- public/language/sr/user.json | 4 ++-- public/language/tr/user.json | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/language/bg/user.json b/public/language/bg/user.json index 7ab3f61f40..61b6fa839d 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -31,8 +31,8 @@ "signature": "Подпис", "birthday": "Рождена дата", "chat": "Разговор", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Продължаване на разговора с %1", + "new_chat_with": "Започване на нов разговор с %1", "follow": "Следване", "unfollow": "Спиране на следването", "more": "Още", diff --git a/public/language/es/user.json b/public/language/es/user.json index 61c7d6d8e6..7f2b0e6674 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -31,8 +31,8 @@ "signature": "Firma", "birthday": "Cumpleaños", "chat": "Chat", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Continuar chat con %1", + "new_chat_with": "Empezar chat con %1", "follow": "Seguir", "unfollow": "Dejar de seguir", "more": "Más", diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index c00a7f4258..6a92520690 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -31,8 +31,8 @@ "signature": "امضا", "birthday": "روز تولد", "chat": "چت", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "ادامه چت با %1", + "new_chat_with": "شروع چت جدید با %1", "follow": "دنبال کن", "unfollow": "دنبال نکن", "more": "بیشتر", diff --git a/public/language/fr/user.json b/public/language/fr/user.json index 650bda5962..6f9dc88759 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -31,8 +31,8 @@ "signature": "Signature", "birthday": "Anniversaire", "chat": "Discussion", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Continuer la discussion avec %1", + "new_chat_with": "Commencer une nouvelle discussion avec %1", "follow": "S'abonner", "unfollow": "Se désabonner", "more": "Plus", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 16bbd15697..5da5652888 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -29,7 +29,7 @@ "username-too-long": "Zbyt długa nazwa użytkownika", "password-too-long": "Hasło jest za długie", "user-banned": "Użytkownik zbanowany", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason": "Twoje konto zostało zablokowane (Powód: %1)", "user-too-new": "Przepraszamy, musisz odczekać %1 sekund(y) przed utworzeniem pierwszego posta", "blacklisted-ip": "Twój adres IP został zablokowany na tej społeczności. Jeśli uważasz to za błąd, zgłoś to administratorowi", "ban-expiry-missing": "Wprowadź datę końca blokady", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 01a26e94da..e821a3f114 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Powiadamiaj mnie o nowych odpowiedziach w tym temacie", "quote": "Cytuj", "reply": "Odpowiedz", - "replies_to_this_post": "Replies: %1", + "replies_to_this_post": "Odpowiedzi: %1", "reply-as-topic": "Odpowiedz na temat", "guest-login-reply": "Zaloguj się, aby odpowiedzieć.", "edit": "Edytuj", diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 272536dbf7..3578277457 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -31,8 +31,8 @@ "signature": "Sygnatura", "birthday": "Urodziny", "chat": "Rozmawiaj", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Kontynuuj rozmowę z %1", + "new_chat_with": "Rozpocznij rozmowę z %1", "follow": "Śledź", "unfollow": "Przestań śledzić", "more": "Więcej", diff --git a/public/language/sk/user.json b/public/language/sk/user.json index 393140e29a..dccddcd346 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -31,8 +31,8 @@ "signature": "Podpis", "birthday": "Dátum narodenia", "chat": "Konverzácia", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Pokračovať v konverzácií s %1", + "new_chat_with": "Začať novú konverzáciu s %1", "follow": "Nasledovať", "unfollow": "Prestať sledovať", "more": "Viac", diff --git a/public/language/sr/user.json b/public/language/sr/user.json index a93a7fa059..ad3c531dd4 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -31,8 +31,8 @@ "signature": "Потпис", "birthday": "Рођендан", "chat": "Ђаскање", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Настави ћаскање са %1", + "new_chat_with": "Започни ново ћаскање са %1", "follow": "Прати", "unfollow": "Не прати", "more": "Више", diff --git a/public/language/tr/user.json b/public/language/tr/user.json index 9dc167a5b2..2d8e654944 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -31,8 +31,8 @@ "signature": "İmza", "birthday": "Doğum Tarihi", "chat": "Sohbet", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "%1 ile sohbete devam et", + "new_chat_with": "%1 ile yeni sohbete başla", "follow": "Takip Et", "unfollow": "Takip etme", "more": "Daha Fazla", From a20027d8f54b719b94552d602c4cd1be89d32320 Mon Sep 17 00:00:00 2001 From: Stuart Williams Date: Wed, 16 Nov 2016 00:47:08 -0500 Subject: [PATCH 015/131] Add missing relative path to Admin tags and widgets links --- src/views/admin/extend/widgets.tpl | 2 +- src/views/admin/manage/tags.tpl | 2 +- src/views/admin/settings/tags.tpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl index aa313b9a4d..381d03794e 100644 --- a/src/views/admin/extend/widgets.tpl +++ b/src/views/admin/extend/widgets.tpl @@ -33,7 +33,7 @@

Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.

-
No widgets found! Activate the essential widgets plugin in the plugins control panel.
+
No widgets found! Activate the essential widgets plugin in the plugins control panel.


- Click here to visit the tag settings page. + Click here to visit the tag settings page.

diff --git a/src/views/admin/settings/tags.tpl b/src/views/admin/settings/tags.tpl index e1e8f01f1c..2b6ea4ed21 100644 --- a/src/views/admin/settings/tags.tpl +++ b/src/views/admin/settings/tags.tpl @@ -21,7 +21,7 @@
- Click here to visit the tag management page. + Click here to visit the tag management page. From 7a0a77d0fa985ba4a65e0dd060ccb1e717cfb814 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 16 Nov 2016 15:10:35 +0300 Subject: [PATCH 016/131] revoke session tests --- test/controllers.js | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test/controllers.js b/test/controllers.js index 71b8e32a6d..5b5b1d7ce5 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -491,6 +491,78 @@ describe('Controllers', function () { }); }); + + describe('revoke session', function () { + var uid; + var jar; + var csrf_token; + var helpers = require('./helpers'); + before(function (done) { + user.create({username: 'revokeme', password: 'barbar'}, function (err, _uid) { + assert.ifError(err); + uid = _uid; + helpers.loginUser('revokeme', 'barbar', function (err, _jar, io, _csrf_token) { + assert.ifError(err); + jar = _jar; + csrf_token = _csrf_token; + done(); + }); + }); + }); + + it('should fail to revoke session with missing uuid', function (done) { + request.del(nconf.get('url') + '/api/user/revokeme/session', { + jar: jar, + headers: { + 'x-csrf-token': csrf_token + } + }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should fail if user doesn\'t exist', function (done) { + request.del(nconf.get('url') + '/api/user/doesnotexist/session/1112233', { + jar: jar, + headers: { + 'x-csrf-token': csrf_token + } + }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 500); + assert.equal(body, '[[error:no-session-found]]'); + done(); + }); + }); + + it('should revoke user session', function (done) { + db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1, function (err, sids) { + assert.ifError(err); + var sid = sids[0]; + + db.sessionStore.get(sid, function (err, sessionObj) { + assert.ifError(err); + request.del(nconf.get('url') + '/api/user/revokeme/session/' + sessionObj.meta.uuid, { + jar: jar, + headers: { + 'x-csrf-token': csrf_token + } + }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body, 'OK'); + console.log(err, res.statusCode, body); + done(); + }); + }); + }); + }); + + }); + + after(function (done) { db.emptydb(done); }); From f2e4d9ce538950b710e891c3b388adda4bcb3e00 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 16 Nov 2016 16:24:21 +0300 Subject: [PATCH 017/131] change flag tests so they use socket methods as well --- src/socket.io/posts/flag.js | 9 ++++--- test/posts.js | 51 +++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js index 42c789eb54..077b88bfc9 100644 --- a/src/socket.io/posts/flag.js +++ b/src/socket.io/posts/flag.js @@ -11,6 +11,7 @@ var privileges = require('../../privileges'); var notifications = require('../../notifications'); var plugins = require('../../plugins'); var meta = require('../../meta'); +var utils = require('../../../public/src/utils'); module.exports = function (SocketPosts) { @@ -51,7 +52,8 @@ module.exports = function (SocketPosts) { }, next); }, function (user, next) { - if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < parseInt(meta.config['privileges:flag'] || 1, 10)) { + var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1; + if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) { return next(new Error('[[error:not-enough-reputation-to-flag]]')); } @@ -163,9 +165,8 @@ module.exports = function (SocketPosts) { return memo; }, payload); - next(null, socket.uid, data.pid, payload); - }, - async.apply(posts.updateFlagData) + posts.updateFlagData(socket.uid, data.pid, payload, next); + } ], callback); }; }; diff --git a/test/posts.js b/test/posts.js index 621a45e018..506945526e 100644 --- a/test/posts.js +++ b/test/posts.js @@ -317,7 +317,17 @@ describe('Post\'s', function () { }); describe('flagging a post', function () { + var socketPosts = require('../src/socket.io/posts'); + it('should fail to flag a post due to low reputation', function (done) { + flagPost(function (err) { + assert.equal(err.message, '[[error:not-enough-reputation-to-flag]]'); + done(); + }); + }); + it('should flag a post', function (done) { + var meta = require('../src/meta'); + meta.config['privileges:flag'] = -1; flagPost(function (err) { assert.ifError(err); done(); @@ -325,32 +335,33 @@ describe('Post\'s', function () { }); it('should return nothing without a uid or a reason', function (done) { - posts.flag(postData, null, "reason", function () { - assert.equal(arguments.length, 0); - posts.flag(postData, voteeUid, null, function () { - assert.equal(arguments.length, 0); + socketPosts.flag({uid: 0}, {pid: postData.pid, reason: 'reason'}, function (err) { + assert.equal(err.message, '[[error:not-logged-in]]'); + socketPosts.flag({uid: voteeUid}, {}, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); done(); }); }); }); it('should return an error without an existing post', function (done) { - posts.flag({}, voteeUid, "reason", function (err) { - assert.ifError(!err); + socketPosts.flag({uid: voteeUid}, {pid: 12312312, reason: 'reason'}, function (err) { + assert.equal(err.message, '[[error:no-post]]'); done(); }); }); it('should return an error if the flag already exists', function (done) { - posts.flag(postData, voteeUid, "reason", function (err) { - assert.ifError(!err); + socketPosts.flag({uid: voteeUid}, {pid: postData.pid, reason: 'reason'}, function (err) { + assert.equal(err.message, '[[error:already-flagged]]'); done(); }); }); }); function flagPost(next) { - posts.flag(postData, voteeUid, "reason", next); + var socketPosts = require('../src/socket.io/posts'); + socketPosts.flag({uid: voteeUid}, {pid: postData.pid, reason: 'reason'}, next); } describe('get flag data', function () { @@ -380,12 +391,22 @@ describe('Post\'s', function () { }); describe('updating a flag', function () { + var socketPosts = require('../src/socket.io/posts'); + var groups = require('../src/groups'); + + before(function (done) { + groups.join('Global Moderators', voteeUid, done); + }); + it('should update a flag', function (done) { async.waterfall([ function (next) { - posts.updateFlagData(voteeUid, postData.pid, { - assignee: `${voteeUid}`, - notes: 'notes' + socketPosts.updateFlag({uid: voteeUid}, { + pid: postData.pid, + data: [ + {name: 'assignee', value: `${voteeUid}`}, + {name: 'notes', value: 'notes'} + ] }, function (err) { assert.ifError(err); posts.getFlags('posts:flagged', cid, voteeUid, 0, -1, function (err, flagData) { @@ -469,8 +490,10 @@ describe('Post\'s', function () { }); describe('dismissing a flag', function () { + var socketPosts = require('../src/socket.io/posts'); + it('should dismiss a flag', function (done) { - posts.dismissFlag(postData.pid, function (err) { + socketPosts.dismissFlag({uid: voteeUid}, postData.pid, function (err) { assert.ifError(err); posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) { assert.ifError(err); @@ -498,7 +521,7 @@ describe('Post\'s', function () { }); it('should dismiss all flags', function (done) { - posts.dismissAllFlags(function (err) { + socketPosts.dismissAllFlags({uid: voteeUid}, {}, function (err) { assert.ifError(err); posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) { assert.ifError(err); From c452df5a7fb241b64603b70c9157c1a7ce2a202b Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Wed, 16 Nov 2016 09:02:18 -0500 Subject: [PATCH 018/131] Latest translations and fallbacks --- public/language/pt_BR/user.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/pt_BR/user.json b/public/language/pt_BR/user.json index cb72857600..74345ca2d3 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt_BR/user.json @@ -31,8 +31,8 @@ "signature": "Assinatura", "birthday": "Aniversário", "chat": "Chat", - "chat_with": "Continue chat with %1", - "new_chat_with": "Start new chat with %1", + "chat_with": "Continuar a conversa com %1", + "new_chat_with": "Iniciar uma nova conversa com %1", "follow": "Seguir", "unfollow": "Deixar de Seguir", "more": "Mais", From 135bb6a524d16af7df31a8c94aa1e91846435c01 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 12:20:44 -0500 Subject: [PATCH 019/131] fixes #5209 --- loader.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/loader.js b/loader.js index 02a668c174..beb1d62691 100644 --- a/loader.js +++ b/loader.js @@ -79,7 +79,7 @@ Loader.addWorkerEvents = function (worker) { if (!(worker.suicide || code === 0)) { console.log('[cluster] Spinning up another process...'); - forkWorker(worker.index, worker.isPrimary); + forkWorker(worker.index, worker.isPrimary, true); } }); @@ -153,7 +153,7 @@ Loader.start = function (callback) { console.log('Clustering enabled: Spinning up ' + numProcs + ' process(es).\n'); for (var x = 0; x < numProcs; ++x) { - forkWorker(x, x === 0); + forkWorker(x, x === 0, false); } if (callback) { @@ -161,8 +161,9 @@ Loader.start = function (callback) { } }; -function forkWorker(index, isPrimary) { +function forkWorker(index, isPrimary, isRestart) { var ports = getPorts(); + var args = []; if(!ports[index]) { return console.log('[cluster] invalid port for worker : ' + index + ' ports: ' + ports.length); @@ -172,7 +173,12 @@ function forkWorker(index, isPrimary) { process.env.isCluster = ports.length > 1 ? true : false; process.env.port = ports[index]; - var worker = fork('app.js', [], { + // If primary node restarts, there's no need to mark it primary any longer (isPrimary used on startup only) + if (isPrimary && isRestart) { + args.push('--from-file', 'js,clientLess,acpLess,tpl'); + } + + var worker = fork('app.js', args, { silent: silent, env: process.env }); From 560dc15819f0a484a4b23d19fa06ec45a0548e17 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 19:48:23 -0500 Subject: [PATCH 020/131] updated winston config to only use json logging if --json-logging flag is passed in or set in config --- app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index b2397f3080..1998e4c60c 100644 --- a/app.js +++ b/app.js @@ -38,11 +38,11 @@ winston.add(winston.transports.Console, { colorize: true, timestamp: function () { var date = new Date(); - return (global.env === 'production') ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; + return (!!nconf.get('json-logging')) ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'), - json: (global.env === 'production'), - stringify: (global.env === 'production') + json: (!!nconf.get('json-logging')), + stringify: (!!nconf.get('json-logging')) }); From 179a28d73ae3b3b831fac7b66271ff17619c5229 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 19:49:50 -0500 Subject: [PATCH 021/131] basic build compilation logic, re: #5211 --- app.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 1998e4c60c..9e2be418e5 100644 --- a/app.js +++ b/app.js @@ -79,6 +79,8 @@ if (nconf.get('setup') || nconf.get('install')) { activate(); } else if (nconf.get('plugins')) { listPlugins(); +} else if (nconf.get('build')) { + build(nconf.get('build')); } else { start(); } @@ -281,7 +283,76 @@ function setup() { }); } -function upgrade() { +function build (targets) { + var db = require('./src/database'); + var meta = require('./src/meta'); + var valid = ['js', 'css', 'tpl']; + var step = function(target, next) { + winston.info('[build] Build step completed in ' + ((Date.now() - startTime) / 1000) + 's'); + next(); + }; + var startTime; + + targets = (targets === true ? valid : targets.split(',').filter(function(target) { + return valid.indexOf(target) !== -1; + })); + + if (!targets) { + winston.error('[build] No valid build targets found. Aborting.'); + return process.exit(0); + } + + async.series([ + async.apply(db.init), + async.apply(meta.themes.setupPaths) + ], function (err) { + if (err) { + winston.error('[build] Encountered error preparing for build: ' + err.message); + return process.exit(1); + } + + // eachSeries because it potentially(tm) runs faster on Windows this way + async.eachSeries(targets, function(target, next) { + switch(target) { + case 'js': + winston.info('[build] Building javascript'); + startTime = Date.now(); + async.series([ + async.apply(meta.js.minify, 'nodebb.min.js'), + async.apply(meta.js.minify, 'acp.min.js') + ], step.bind(this, target, next)); + break; + + case 'css': + winston.info('[build] Building CSS stylesheets'); + startTime = Date.now(); + meta.css.minify(step.bind(this, target, next)); + break; + + case 'tpl': + winston.info('[build] Building templates'); + startTime = Date.now(); + meta.templates.compile(step.bind(this, target, next)); + break; + + default: + winston.warn('[build] Unknown target: \'' + target + '\''); + setImmediate(next); + break; + } + }, function(err) { + if (err) { + winston.error('[build] Encountered error during build step: ' + err.message); + return process.exit(1); + } + + winston.info('[build] Asset compilation successful. Exiting.'); + process.exit(0); + }); + }); +}; + +function upgrade () { require('./src/database').init(function (err) { if (err) { winston.error(err.stack); @@ -291,7 +362,7 @@ function upgrade() { require('./src/upgrade').upgrade(); }); }); -} +}; function activate() { var db = require('./src/database'); From 03cf5d8da54e1ce63623bc193a24a4a2b1b1b86c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 20:24:10 -0500 Subject: [PATCH 022/131] upgrade and setup steps call build step now, re: #5211 --- app.js | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/app.js b/app.js index 9e2be418e5..599508f548 100644 --- a/app.js +++ b/app.js @@ -85,7 +85,7 @@ if (nconf.get('setup') || nconf.get('install')) { start(); } -function loadConfig() { +function loadConfig(callback) { winston.verbose('* using configuration stored in: %s', configFile); nconf.file({ @@ -112,6 +112,10 @@ function loadConfig() { if (nconf.get('url')) { nconf.set('url_parsed', url.parse(nconf.get('url'))); } + + if (typeof callback === 'function') { + callback(); + } } @@ -252,7 +256,14 @@ function setup() { process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n'); process.stdout.write('Press enter to accept the default setting (shown in brackets).\n'); - install.setup(function (err, data) { + async.series([ + async.apply(install.setup), + async.apply(loadConfig), + async.apply(build, true) + ], function(err, data) { + // Disregard build step data + data = data[0]; + var separator = ' '; if (process.stdout.columns > 10) { for(var x = 0,cols = process.stdout.columns - 10; x < cols; x++) { @@ -281,9 +292,9 @@ function setup() { process.exit(); }); -} +}; -function build (targets) { +function build (targets, callback) { var db = require('./src/database'); var meta = require('./src/meta'); var valid = ['js', 'css', 'tpl']; @@ -336,7 +347,7 @@ function build (targets) { break; default: - winston.warn('[build] Unknown target: \'' + target + '\''); + winston.warn('[build] Unknown build target: \'' + target + '\''); setImmediate(next); break; } @@ -346,21 +357,34 @@ function build (targets) { return process.exit(1); } - winston.info('[build] Asset compilation successful. Exiting.'); - process.exit(0); + winston.info('[build] Asset compilation successful.'); + + if (typeof callback === 'function') { + callback(); + } else { + process.exit(0); + } }); }); }; function upgrade () { - require('./src/database').init(function (err) { + var db = require('./src/database'); + var meta = require('./src/meta'); + var upgrade = require('./src/upgrade'); + + async.series([ + async.apply(db.init), + async.apply(meta.configs.init), + async.apply(upgrade.upgrade), + async.apply(build, true) + ], function(err) { if (err) { winston.error(err.stack); - process.exit(); + process.exit(1); + } else { + process.exit(0); } - require('./src/meta').configs.init(function () { - require('./src/upgrade').upgrade(); - }); }); }; From 1617c99e7b58399df165fc9b3a21ffe48b0e2021 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 20:26:47 -0500 Subject: [PATCH 023/131] linting app.js --- app.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index 599508f548..466e81745b 100644 --- a/app.js +++ b/app.js @@ -260,7 +260,7 @@ function setup() { async.apply(install.setup), async.apply(loadConfig), async.apply(build, true) - ], function(err, data) { + ], function (err, data) { // Disregard build step data data = data[0]; @@ -294,17 +294,17 @@ function setup() { }); }; -function build (targets, callback) { +function build(targets, callback) { var db = require('./src/database'); var meta = require('./src/meta'); var valid = ['js', 'css', 'tpl']; - var step = function(target, next) { + var step = function (target, next) { winston.info('[build] Build step completed in ' + ((Date.now() - startTime) / 1000) + 's'); next(); }; var startTime; - targets = (targets === true ? valid : targets.split(',').filter(function(target) { + targets = (targets === true ? valid : targets.split(',').filter(function (target) { return valid.indexOf(target) !== -1; })); @@ -323,7 +323,7 @@ function build (targets, callback) { } // eachSeries because it potentially(tm) runs faster on Windows this way - async.eachSeries(targets, function(target, next) { + async.eachSeries(targets, function (target, next) { switch(target) { case 'js': winston.info('[build] Building javascript'); @@ -351,7 +351,7 @@ function build (targets, callback) { setImmediate(next); break; } - }, function(err) { + }, function (err) { if (err) { winston.error('[build] Encountered error during build step: ' + err.message); return process.exit(1); @@ -368,7 +368,7 @@ function build (targets, callback) { }); }; -function upgrade () { +function upgrade() { var db = require('./src/database'); var meta = require('./src/meta'); var upgrade = require('./src/upgrade'); @@ -378,7 +378,7 @@ function upgrade () { async.apply(meta.configs.init), async.apply(upgrade.upgrade), async.apply(build, true) - ], function(err) { + ], function (err) { if (err) { winston.error(err.stack); process.exit(1); From f92758c7647ac75f316e1c8cadd8798e006e3be2 Mon Sep 17 00:00:00 2001 From: Stuart Williams Date: Wed, 16 Nov 2016 19:57:23 -0500 Subject: [PATCH 024/131] Replace only base URL in login redirect URL, fixes #5205 --- src/controllers/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/index.js b/src/controllers/index.js index 2671dc0657..a8cfa96d89 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -104,7 +104,7 @@ Controllers.login = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; var allowLoginWith = (meta.config.allowLoginWith || 'username-email'); - var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('url'), ''); + var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url'), ''); var errorText; if (req.query.error === 'csrf-invalid') { From 9bf0f6c5cdfc320bd47ee967d19e6507438fec25 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 20:53:20 -0500 Subject: [PATCH 025/131] prep for @psychobunny, re: #5211 --- Gruntfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Gruntfile.js b/Gruntfile.js index 9f1585f301..73eaad38f4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -38,6 +38,7 @@ module.exports = function (grunt) { return incomplete.indexOf(ext) === -1; }); + // @psychobunny, re: #5211, instead of this, just call `node app --build js` or `node app --build css,tpl` updateArgs.push('--from-file=' + fromFile.join(',')); incomplete.push(compiling); From 299fcb99f182452e8d694eb7031b3199b7ee5210 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 20:54:57 -0500 Subject: [PATCH 026/131] more progress on #5211 --- install/web.js | 10 --------- loader.js | 5 ----- src/meta/css.js | 50 +++++++++++++++++++++++++------------------ src/meta/templates.js | 9 ++------ src/webserver.js | 19 ++++++---------- 5 files changed, 38 insertions(+), 55 deletions(-) diff --git a/install/web.js b/install/web.js index 72284fea5f..b81fdeb545 100644 --- a/install/web.js +++ b/install/web.js @@ -124,11 +124,6 @@ function launch(req, res) { } function compileLess(callback) { - if ((nconf.get('from-file') || '').indexOf('less') !== -1) { - winston.info('LESS compilation skipped'); - return callback(false); - } - fs.readFile(path.join(__dirname, '../public/less/install.less'), function (err, style) { if (err) { return winston.error('Unable to read LESS install file: ', err); @@ -145,11 +140,6 @@ function compileLess(callback) { } function compileJS(callback) { - if ((nconf.get('from-file') || '').indexOf('js') !== -1) { - winston.info('Client-side JS compilation skipped'); - return callback(false); - } - var scriptPath = path.join(__dirname, '..'); var result = uglify.minify(scripts.map(function (script) { return path.join(scriptPath, script); diff --git a/loader.js b/loader.js index beb1d62691..6b388affcd 100644 --- a/loader.js +++ b/loader.js @@ -173,11 +173,6 @@ function forkWorker(index, isPrimary, isRestart) { process.env.isCluster = ports.length > 1 ? true : false; process.env.port = ports[index]; - // If primary node restarts, there's no need to mark it primary any longer (isPrimary used on startup only) - if (isPrimary && isRestart) { - args.push('--from-file', 'js,clientLess,acpLess,tpl'); - } - var worker = fork('app.js', args, { silent: silent, env: process.env diff --git a/src/meta/css.js b/src/meta/css.js index d33245f5bb..d664fdd5d2 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -81,25 +81,9 @@ module.exports = function (Meta) { acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - var fromFile = nconf.get('from-file') || ''; - async.series([ - function (next) { - if (fromFile.match('clientLess')) { - winston.info('[minifier] Compiling front-end LESS files skipped'); - return Meta.css.getFromFile(path.join(__dirname, '../../public/stylesheet.css'), 'cache', next); - } - - minify(source, paths, 'cache', next); - }, - function (next) { - if (fromFile.match('acpLess')) { - winston.info('[minifier] Compiling ACP LESS files skipped'); - return Meta.css.getFromFile(path.join(__dirname, '../../public/admin.css'), 'acpCache', next); - } - - minify(acpSource, paths, 'acpCache', next); - } + async.apply(minify, source, paths, 'cache'), + async.apply(minify, acpSource, paths, 'acpCache') ], function (err, minified) { if (err) { return callback(err); @@ -109,8 +93,8 @@ module.exports = function (Meta) { if (process.send) { process.send({ action: 'css-propagate', - cache: fromFile.match('clientLess') ? Meta.css.cache : minified[0], - acpCache: fromFile.match('acpLess') ? Meta.css.acpCache : minified[1] + cache: minified[0], + acpCache: minified[1] }); } @@ -122,6 +106,30 @@ module.exports = function (Meta) { }); }; + Meta.css.getFromFile = function(callback) { + async.series([ + async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'), + async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache') + ], function (err, minified) { + if (err) { + return callback(err); + } + + // Propagate to other workers + if (process.send) { + process.send({ + action: 'css-propagate', + cache: Meta.css.cache, + acpCache: Meta.css.acpCache + }); + } + + emitter.emit('meta:css.compiled'); + + callback(); + }); + }; + function getStyleSource(files, prefix, extension, callback) { var pluginDirectories = [], source = ''; @@ -166,7 +174,7 @@ module.exports = function (Meta) { }); }; - Meta.css.getFromFile = function (filePath, filename, callback) { + Meta.css.loadFile = function (filePath, filename, callback) { winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file'); fs.readFile(filePath, function (err, file) { diff --git a/src/meta/templates.js b/src/meta/templates.js index d335709461..6d8f579034 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -17,14 +17,9 @@ var searchIndex = {}; Templates.compile = function (callback) { callback = callback || function () {}; - var fromFile = nconf.get('from-file') || ''; - - if (nconf.get('isPrimary') === 'false' || fromFile.match('tpl')) { - if (fromFile.match('tpl')) { - emitter.emit('templates:compiled'); - winston.info('[minifier] Compiling templates skipped'); - } + if (nconf.get('isPrimary') === 'false') { + emitter.emit('templates:compiled'); return callback(); } diff --git a/src/webserver.js b/src/webserver.js index bedcb07f02..37e242e889 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -84,16 +84,8 @@ module.exports.listen = function (callback) { function initializeNodeBB(callback) { winston.info('initializing NodeBB ...'); - - var skipJS; - var fromFile = nconf.get('from-file') || ''; var middleware = require('./middleware'); - if (fromFile.match('js')) { - winston.info('[minifier] Minifying client-side JS skipped'); - skipJS = true; - } - async.waterfall([ async.apply(meta.themes.setupPaths), function (next) { @@ -116,10 +108,13 @@ function initializeNodeBB(callback) { }, function (next) { async.series([ - async.apply(meta.templates.compile), - async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'nodebb.min.js'), - async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'acp.min.js'), - async.apply(meta.css.minify), + async.apply(function(next) { + emitter.emit('templates:compiled'); + setImmediate(next); + }), + async.apply(meta.js.getFromFile, 'nodebb.min.js'), + async.apply(meta.js.getFromFile, 'acp.min.js'), + async.apply(meta.css.getFromFile), async.apply(meta.sounds.init), async.apply(languages.init), async.apply(meta.blacklist.load) From 97d7b57db3b2b5cf73234e67f3a9f6a1694cabae Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Nov 2016 21:18:51 -0500 Subject: [PATCH 027/131] nodebb executable integration for #5211 --- nodebb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nodebb b/nodebb index 342032fe1a..62115e110b 100755 --- a/nodebb +++ b/nodebb @@ -335,6 +335,13 @@ switch(process.argv[2]) { }); break; + case 'build': + var args = process.argv.slice(0); + args[2] = '--' + args[2]; + + fork(args); + break; + case 'setup': cproc.fork('app.js', ['--setup'], { cwd: __dirname, @@ -401,13 +408,14 @@ switch(process.argv[2]) { default: process.stdout.write('\nWelcome to NodeBB\n\n'.bold); - process.stdout.write('Usage: ./nodebb {start|stop|reload|restart|log|setup|reset|upgrade|dev}\n\n'); + process.stdout.write('Usage: ./nodebb {start|slog|stop|reload|restart|log|build|setup|reset|upgrade|dev}\n\n'); process.stdout.write('\t' + 'start'.yellow + '\t\tStart the NodeBB server\n'); process.stdout.write('\t' + 'slog'.yellow + '\t\tStarts the NodeBB server and displays the live output log\n'); process.stdout.write('\t' + 'stop'.yellow + '\t\tStops the NodeBB server\n'); process.stdout.write('\t' + 'reload'.yellow + '\t\tRestarts NodeBB\n'); process.stdout.write('\t' + 'restart'.yellow + '\t\tRestarts NodeBB\n'); process.stdout.write('\t' + 'log'.yellow + '\t\tOpens the logging interface (useful for debugging)\n'); + process.stdout.write('\t' + 'build'.yellow + '\t\tCompiles javascript, css stylesheets, and templates\n'); process.stdout.write('\t' + 'setup'.yellow + '\t\tRuns the NodeBB setup script\n'); process.stdout.write('\t' + 'reset'.yellow + '\t\tDisables all plugins, restores the default theme.\n'); process.stdout.write('\t' + 'activate'.yellow + '\tActivates a plugin for the next startup of NodeBB.\n'); From 4f0e93732a47c44d27ec294ec4d36a9caa4778eb Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 17 Nov 2016 12:56:00 +0300 Subject: [PATCH 028/131] #5211 remove passing js/css between procs --- app.js | 14 ---------- loader.js | 65 +++---------------------------------------- src/meta/css.js | 43 +++------------------------- src/meta/js.js | 47 ++++++------------------------- src/meta/templates.js | 17 +++-------- src/socket.io/meta.js | 16 +++++------ src/socket.io/user.js | 2 +- src/webserver.js | 22 ++++----------- 8 files changed, 34 insertions(+), 192 deletions(-) diff --git a/app.js b/app.js index 466e81745b..1832095052 100644 --- a/app.js +++ b/app.js @@ -164,20 +164,6 @@ function start() { case 'reload': meta.reload(); break; - case 'js-propagate': - meta.js.target = message.data; - emitter.emit('meta:js.compiled'); - winston.verbose('[cluster] Client-side javascript and mapping propagated to worker %s', process.pid); - break; - case 'css-propagate': - meta.css.cache = message.cache; - meta.css.acpCache = message.acpCache; - emitter.emit('meta:css.compiled'); - winston.verbose('[cluster] Stylesheets propagated to worker %s', process.pid); - break; - case 'templates:compiled': - emitter.emit('templates:compiled'); - break; } }); diff --git a/loader.js b/loader.js index 6b388affcd..df528871e9 100644 --- a/loader.js +++ b/loader.js @@ -22,15 +22,7 @@ var pidFilePath = __dirname + '/pidfile', workers = [], Loader = { - timesStarted: 0, - js: { - target: {} - }, - css: { - cache: undefined, - acpCache: undefined - }, - templatesCompiled: false + timesStarted: 0 }; Loader.init = function (callback) { @@ -79,37 +71,13 @@ Loader.addWorkerEvents = function (worker) { if (!(worker.suicide || code === 0)) { console.log('[cluster] Spinning up another process...'); - forkWorker(worker.index, worker.isPrimary, true); + forkWorker(worker.index, worker.isPrimary); } }); worker.on('message', function (message) { if (message && typeof message === 'object' && message.action) { switch (message.action) { - case 'ready': - if (Loader.js.target['nodebb.min.js'] && Loader.js.target['acp.min.js'] && !worker.isPrimary) { - worker.send({ - action: 'js-propagate', - data: Loader.js.target - }); - } - - if (Loader.css.cache && !worker.isPrimary) { - worker.send({ - action: 'css-propagate', - cache: Loader.css.cache, - acpCache: Loader.css.acpCache - }); - } - - if (Loader.templatesCompiled && !worker.isPrimary) { - worker.send({ - action: 'templates:compiled' - }); - } - - - break; case 'restart': console.log('[cluster] Restarting...'); Loader.restart(); @@ -118,31 +86,6 @@ Loader.addWorkerEvents = function (worker) { console.log('[cluster] Reloading...'); Loader.reload(); break; - case 'js-propagate': - Loader.js.target = message.data; - - Loader.notifyWorkers({ - action: 'js-propagate', - data: message.data - }, worker.pid); - break; - case 'css-propagate': - Loader.css.cache = message.cache; - Loader.css.acpCache = message.acpCache; - - Loader.notifyWorkers({ - action: 'css-propagate', - cache: message.cache, - acpCache: message.acpCache - }, worker.pid); - break; - case 'templates:compiled': - Loader.templatesCompiled = true; - - Loader.notifyWorkers({ - action: 'templates:compiled', - }, worker.pid); - break; } } }); @@ -153,7 +96,7 @@ Loader.start = function (callback) { console.log('Clustering enabled: Spinning up ' + numProcs + ' process(es).\n'); for (var x = 0; x < numProcs; ++x) { - forkWorker(x, x === 0, false); + forkWorker(x, x === 0); } if (callback) { @@ -161,7 +104,7 @@ Loader.start = function (callback) { } }; -function forkWorker(index, isPrimary, isRestart) { +function forkWorker(index, isPrimary) { var ports = getPorts(); var args = []; diff --git a/src/meta/css.js b/src/meta/css.js index d664fdd5d2..3336fc0a0e 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -11,7 +11,6 @@ var postcss = require('postcss'); var clean = require('postcss-clean'); var plugins = require('../plugins'); -var emitter = require('../emitter'); var db = require('../database'); var file = require('../file'); var utils = require('../../public/src/utils'); @@ -24,10 +23,6 @@ module.exports = function (Meta) { Meta.css.minify = function (callback) { callback = callback || function () {}; - if (nconf.get('isPrimary') !== 'true') { - winston.verbose('[meta/css] Cluster worker ' + process.pid + ' skipping LESS/CSS compilation'); - return callback(); - } winston.verbose('[meta/css] Minifying LESS/CSS'); db.getObjectFields('config', ['theme:type', 'theme:id'], function (err, themeData) { @@ -84,23 +79,8 @@ module.exports = function (Meta) { async.series([ async.apply(minify, source, paths, 'cache'), async.apply(minify, acpSource, paths, 'acpCache') - ], function (err, minified) { - if (err) { - return callback(err); - } - - // Propagate to other workers - if (process.send) { - process.send({ - action: 'css-propagate', - cache: minified[0], - acpCache: minified[1] - }); - } - - emitter.emit('meta:css.compiled'); - - callback(); + ], function (err) { + callback(err); }); }); }); @@ -110,23 +90,8 @@ module.exports = function (Meta) { async.series([ async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'), async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache') - ], function (err, minified) { - if (err) { - return callback(err); - } - - // Propagate to other workers - if (process.send) { - process.send({ - action: 'css-propagate', - cache: Meta.css.cache, - acpCache: Meta.css.acpCache - }); - } - - emitter.emit('meta:css.compiled'); - - callback(); + ], function (err) { + callback(err); }); }; diff --git a/src/meta/js.js b/src/meta/js.js index cfb588125d..a68d7a53de 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -8,7 +8,6 @@ var nconf = require('nconf'); var fs = require('fs'); var file = require('../file'); var plugins = require('../plugins'); -var emitter = require('../emitter'); var utils = require('../../public/src/utils'); module.exports = function (Meta) { @@ -122,14 +121,6 @@ module.exports = function (Meta) { }; Meta.js.minify = function (target, callback) { - if (nconf.get('isPrimary') !== 'true') { - if (typeof callback === 'function') { - callback(); - } - - return; - } - winston.verbose('[meta/js] Minifying ' + target); var forkProcessParams = setupDebugging(); @@ -137,7 +128,10 @@ module.exports = function (Meta) { Meta.js.target[target] = {}; - Meta.js.prepare(target, function () { + Meta.js.prepare(target, function (err) { + if (err) { + return callback(err); + } minifier.send({ action: 'js', minify: global.env !== 'development', @@ -153,24 +147,10 @@ module.exports = function (Meta) { winston.verbose('[meta/js] ' + target + ' minification complete'); minifier.kill(); - if (process.send && Meta.js.target['nodebb.min.js'] && Meta.js.target['acp.min.js']) { - process.send({ - action: 'js-propagate', - data: Meta.js.target - }); - } - if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) { - return Meta.js.commitToFile(target, function () { - if (typeof callback === 'function') { - callback(); - } - }); + return Meta.js.commitToFile(target, callback); } else { - emitter.emit('meta:js.compiled'); - if (typeof callback === 'function') { - return callback(); - } + return callback(); } break; @@ -178,11 +158,7 @@ module.exports = function (Meta) { winston.error('[meta/js] Could not compile ' + target + ': ' + message.message); minifier.kill(); - if (typeof callback === 'function') { - callback(new Error(message.message)); - } else { - process.exit(0); - } + callback(new Error(message.message)); break; } }); @@ -236,13 +212,7 @@ module.exports = function (Meta) { Meta.js.commitToFile = function (target, callback) { fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) { - if (err) { - winston.error('[meta/js] ' + err.message); - process.exit(0); - } - - emitter.emit('meta:js.compiled'); - callback(); + callback(err); }); }; @@ -277,7 +247,6 @@ module.exports = function (Meta) { map: files[1] || '' }; - emitter.emit('meta:js.compiled'); callback(); }); }); diff --git a/src/meta/templates.js b/src/meta/templates.js index 6d8f579034..4e7f934624 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -8,7 +8,6 @@ var path = require('path'); var fs = require('fs'); var nconf = require('nconf'); -var emitter = require('../emitter'); var plugins = require('../plugins'); var utils = require('../../public/src/utils'); @@ -18,11 +17,6 @@ var searchIndex = {}; Templates.compile = function (callback) { callback = callback || function () {}; - if (nconf.get('isPrimary') === 'false') { - emitter.emit('templates:compiled'); - return callback(); - } - compile(callback); }; @@ -149,15 +143,12 @@ function compile(callback) { return callback(err); } - compileIndex(viewsPath, function () { + compileIndex(viewsPath, function (err) { + if (err) { + return callback(err); + } winston.verbose('[meta/templates] Successfully compiled templates.'); - emitter.emit('templates:compiled'); - if (process.send) { - process.send({ - action: 'templates:compiled' - }); - } callback(); }); }); diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 13fe018654..35a5da17e1 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -1,15 +1,15 @@ 'use strict'; -var meta = require('../meta'), - user = require('../user'), - topics = require('../topics'), - emitter = require('../emitter'), +var meta = require('../meta'); +var user = require('../user'); +var topics = require('../topics'); +var emitter = require('../emitter'); - websockets = require('./'), +var websockets = require('./'); - SocketMeta = { - rooms: {} - }; +var SocketMeta = { + rooms: {} +}; SocketMeta.reconnected = function (socket, data, callback) { if (socket.uid) { diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 1c322fe1e4..41c2e9c2c5 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -1,6 +1,6 @@ 'use strict'; -var async = require('async'); +var async = require('async'); var winston = require('winston'); var user = require('../user'); diff --git a/src/webserver.js b/src/webserver.js index 37e242e889..964ae60bfd 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -62,24 +62,16 @@ module.exports.listen = function (callback) { logger.init(app); - emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function () { + initializeNodeBB(function (err) { + if (err) { + return callback(err); + } + winston.info('NodeBB Ready'); emitter.emit('nodebb:ready'); listen(callback); }); - - initializeNodeBB(function (err) { - if (err) { - winston.error(err); - process.exit(); - } - if (process.send) { - process.send({ - action: 'ready' - }); - } - }); }; function initializeNodeBB(callback) { @@ -108,10 +100,6 @@ function initializeNodeBB(callback) { }, function (next) { async.series([ - async.apply(function(next) { - emitter.emit('templates:compiled'); - setImmediate(next); - }), async.apply(meta.js.getFromFile, 'nodebb.min.js'), async.apply(meta.js.getFromFile, 'acp.min.js'), async.apply(meta.css.getFromFile), From 71c7ef9109d138297965d2caea9bd7596733362f Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 17 Nov 2016 13:05:59 +0300 Subject: [PATCH 029/131] fix test --- app.js | 8 ++------ src/meta/css.js | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 1832095052..d0dbea56e7 100644 --- a/app.js +++ b/app.js @@ -201,7 +201,7 @@ function start() { require('./src/user').startJobs(); } - webserver.listen(); + webserver.listen(next); } ], function (err) { if (err) { @@ -219,11 +219,7 @@ function start() { winston.warn(' ./nodebb upgrade'); break; default: - if (err.stacktrace !== false) { - winston.error(err.stack); - } else { - winston.error(err.message); - } + winston.error(err); break; } diff --git a/src/meta/css.js b/src/meta/css.js index 3336fc0a0e..1e0cbc6b56 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -86,7 +86,7 @@ module.exports = function (Meta) { }); }; - Meta.css.getFromFile = function(callback) { + Meta.css.getFromFile = function (callback) { async.series([ async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'), async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache') From b1f23c8c4b5bb58a0bc62a8c5b92345aefd96fa2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 17 Nov 2016 07:47:13 -0500 Subject: [PATCH 030/131] read req.uid instead of req.user.uid in admin groups list --- src/controllers/admin/groups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 38563d96a5..db940c8324 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -44,7 +44,7 @@ groupsController.list = function (req, res, next) { res.render('admin/manage/groups', { groups: data.groups, pagination: data.pagination, - yourid: req.user.uid + yourid: req.uid }); }); }; From 232b38765233769062a5338e9596645f1622fac6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 17 Nov 2016 11:51:38 -0500 Subject: [PATCH 031/131] set up plugins in build step as well --- app.js | 4 +++- src/plugins.js | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index d0dbea56e7..f308b56d50 100644 --- a/app.js +++ b/app.js @@ -279,6 +279,7 @@ function setup() { function build(targets, callback) { var db = require('./src/database'); var meta = require('./src/meta'); + var plugins = require('./src/plugins'); var valid = ['js', 'css', 'tpl']; var step = function (target, next) { winston.info('[build] Build step completed in ' + ((Date.now() - startTime) / 1000) + 's'); @@ -297,7 +298,8 @@ function build(targets, callback) { async.series([ async.apply(db.init), - async.apply(meta.themes.setupPaths) + async.apply(meta.themes.setupPaths), + async.apply(plugins.init, null, null) ], function (err) { if (err) { winston.error('[build] Encountered error preparing for build: ' + err.message); diff --git a/src/plugins.js b/src/plugins.js index 042561759a..1a20597564 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -48,9 +48,11 @@ var middleware; return callback(); } - app = nbbApp; - middleware = nbbMiddleware; - hotswap.prepare(nbbApp); + if (nbbApp) { + app = nbbApp; + middleware = nbbMiddleware; + hotswap.prepare(nbbApp); + } if (global.env === 'development') { winston.verbose('[plugins] Initializing plugins system'); From 9bab0b53b0644539be0a3fd404186ae3394d2f6d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 17 Nov 2016 12:11:56 -0500 Subject: [PATCH 032/131] re: #5211, broke out meta.css.minify to accept targets, made build output marginally nicer looking --- app.js | 16 +++++++++++----- src/meta/css.js | 39 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/app.js b/app.js index f308b56d50..65cc06af30 100644 --- a/app.js +++ b/app.js @@ -280,9 +280,9 @@ function build(targets, callback) { var db = require('./src/database'); var meta = require('./src/meta'); var plugins = require('./src/plugins'); - var valid = ['js', 'css', 'tpl']; + var valid = ['js', 'clientCSS', 'acpCSS', 'tpl']; var step = function (target, next) { - winston.info('[build] Build step completed in ' + ((Date.now() - startTime) / 1000) + 's'); + winston.info('[build] => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); next(); }; var startTime; @@ -318,10 +318,16 @@ function build(targets, callback) { ], step.bind(this, target, next)); break; - case 'css': - winston.info('[build] Building CSS stylesheets'); + case 'clientCSS': + winston.info('[build] Building client-side CSS'); startTime = Date.now(); - meta.css.minify(step.bind(this, target, next)); + meta.css.minify('stylesheet.css', step.bind(this, target, next)); + break; + + case 'acpCSS': + winston.info('[build] Building admin control panel CSS'); + startTime = Date.now(); + meta.css.minify('admin.css', step.bind(this, target, next)); break; case 'tpl': diff --git a/src/meta/css.js b/src/meta/css.js index 1e0cbc6b56..583927e04a 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -21,7 +21,7 @@ module.exports = function (Meta) { Meta.css.cache = undefined; Meta.css.acpCache = undefined; - Meta.css.minify = function (callback) { + Meta.css.minify = function (target, callback) { callback = callback || function () {}; winston.verbose('[meta/css] Minifying LESS/CSS'); @@ -61,27 +61,26 @@ module.exports = function (Meta) { var acpSource = source; - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; - source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; - source = '@import "./theme";\n' + source; + if (target !== 'admin.css') { + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; + source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; + source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; + source = '@import "./theme";\n' + source; - acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; - acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; + minify(source, paths, 'cache', callback); + } else { + acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; + acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - async.series([ - async.apply(minify, source, paths, 'cache'), - async.apply(minify, acpSource, paths, 'acpCache') - ], function (err) { - callback(err); - }); + minify(acpSource, paths, 'acpCache', callback); + } }); }); }; From 7a10cffb25d2bb6ffe2b75cf2ec1bb98804bad8c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 17 Nov 2016 13:56:47 -0500 Subject: [PATCH 033/131] fix Gruntfile.js to work with bew build step --- Gruntfile.js | 50 ++++++++++++++++++++++++++++---------------------- app.js | 6 ++++++ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 73eaad38f4..08baead7a8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,8 +2,9 @@ var fork = require('child_process').fork, env = process.env, - worker, - incomplete = []; + worker, updateWorker, + incomplete = [], + running = 0; module.exports = function (grunt) { @@ -19,39 +20,44 @@ module.exports = function (grunt) { time = Date.now(); if (target === 'lessUpdated_Client') { - fromFile = ['js', 'tpl', 'acpLess']; - compiling = 'clientLess'; + compiling = 'clientCSS'; } else if (target === 'lessUpdated_Admin') { - fromFile = ['js', 'tpl', 'clientLess']; - compiling = 'acpLess'; + compiling = 'acpCSS'; } else if (target === 'clientUpdated') { - fromFile = ['clientLess', 'acpLess', 'tpl']; compiling = 'js'; } else if (target === 'templatesUpdated') { - fromFile = ['js', 'clientLess', 'acpLess']; compiling = 'tpl'; } else if (target === 'serverUpdated') { - fromFile = ['clientLess', 'acpLess', 'js', 'tpl']; + // Do nothing, just restart } - fromFile = fromFile.filter(function (ext) { - return incomplete.indexOf(ext) === -1; - }); + if (incomplete.indexOf(compiling) === -1) { + incomplete.push(compiling); + } // @psychobunny, re: #5211, instead of this, just call `node app --build js` or `node app --build css,tpl` - updateArgs.push('--from-file=' + fromFile.join(',')); - incomplete.push(compiling); + updateArgs.push('--build'); + updateArgs.push(incomplete.join(',')); worker.kill(); - worker = fork('app.js', updateArgs, { env: env }); + if (updateWorker) { + updateWorker.kill('SIGKILL'); + } + updateWorker = fork('app.js', updateArgs, { env: env }); + ++running; + updateWorker.on('exit', function () { + --running; + if (running === 0) { + worker = fork('app.js', args, { env: env }); + worker.on('message', function () { + if (incomplete.length) { + incomplete = []; - worker.on('message', function () { - if (incomplete.length) { - incomplete = []; - - if (grunt.option('verbose')) { - grunt.log.writeln('NodeBB restarted in ' + (Date.now() - time) + ' ms'); - } + if (grunt.option('verbose')) { + grunt.log.writeln('NodeBB restarted in ' + (Date.now() - time) + ' ms'); + } + } + }); } }); } diff --git a/app.js b/app.js index 65cc06af30..57ba0e0000 100644 --- a/app.js +++ b/app.js @@ -226,6 +226,12 @@ function start() { // Either way, bad stuff happened. Abort start. process.exit(); } + + if (process.send) { + process.send({ + action: 'listening' + }); + } }); } From d4040ed52ebfa0ecda3bbe2f68ea228c6188f774 Mon Sep 17 00:00:00 2001 From: pichalite Date: Thu, 17 Nov 2016 22:12:45 +0000 Subject: [PATCH 034/131] Fixes #5213 --- public/src/admin/extend/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index bb89256959..59ef9348ab 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -1,7 +1,7 @@ "use strict"; /* global define, app, socket, bootbox */ -define('admin/extend/plugins', function () { +define('admin/extend/plugins', ['jqueryui'], function (jqueryui) { var Plugins = {}; Plugins.init = function () { var pluginsList = $('.plugins'), From 2d84c98565840e959924f37cff057e684f75a829 Mon Sep 17 00:00:00 2001 From: Timothy Fike Date: Thu, 17 Nov 2016 18:08:44 -0500 Subject: [PATCH 035/131] Allow sending Error objects to alertError instead of just plain strings. --- public/src/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/src/app.js b/public/src/app.js index ba1f6d39ff..cb2775eda2 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -134,6 +134,8 @@ app.cacheBuster = null; }; app.alertError = function (message, timeout) { + message = message.message || message + if (message === '[[error:invalid-session]]') { return app.handleInvalidSession(); } From f1e3e155c4c80fdbb64ee519342f7194fc00f774 Mon Sep 17 00:00:00 2001 From: Timothy Fike Date: Thu, 17 Nov 2016 18:21:54 -0500 Subject: [PATCH 036/131] Update app.js --- public/src/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/app.js b/public/src/app.js index cb2775eda2..dfcc06ac09 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -134,7 +134,7 @@ app.cacheBuster = null; }; app.alertError = function (message, timeout) { - message = message.message || message + message = message.message || message; if (message === '[[error:invalid-session]]') { return app.handleInvalidSession(); From f1a933210beb5bc80a8b17d2d28441e3e098237a Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 11:15:22 +0300 Subject: [PATCH 037/131] notifications.pushGroups --- src/notifications.js | 13 +++++++++++++ test/notifications.js | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/notifications.js b/src/notifications.js index c70c0cbb0a..ed2e98b7da 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -236,6 +236,19 @@ var utils = require('../public/src/utils'); }); }; + Notifications.pushGroups = function (notification, groupNames, callback) { + callback = callback || function () {}; + groups.getMembersOfGroups(groupNames, function (err, groupMembers) { + if (err) { + return callback(err); + } + + var members = _.unique(_.flatten(groupMembers)); + + Notifications.push(notification, members, callback); + }); + }; + Notifications.rescind = function (nid, callback) { callback = callback || function () {}; diff --git a/test/notifications.js b/test/notifications.js index 3eebe656bb..4a4f6c320d 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -67,6 +67,32 @@ describe('Notifications', function () { }); }); + it('should push a notification to a group', function (done) { + notifications.pushGroup(notification, 'registered-users', function (err) { + assert.ifError(err); + setTimeout(function () { + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }, 2000); + }); + }); + + it('should push a notification to groups', function (done) { + notifications.pushGroup(notification, ['registered-users', 'administrators'], function (err) { + assert.ifError(err); + setTimeout(function () { + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }, 2000); + }); + }); + it('should mark a notification read', function (done) { notifications.markRead(notification.nid, uid, function (err) { assert.ifError(err); From e86db0463181b0e9c7ee9dcfb7de7693b7b60ae3 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 12:01:33 +0300 Subject: [PATCH 038/131] #5211 run build step before tests move build to its own file --- app.js | 109 +++++-------------------------------- build.js | 91 +++++++++++++++++++++++++++++++ test/controllers.js | 1 - test/mocks/databasemock.js | 6 +- 4 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 build.js diff --git a/app.js b/app.js index 57ba0e0000..b3fe8f0fc2 100644 --- a/app.js +++ b/app.js @@ -23,13 +23,12 @@ var nconf = require('nconf'); nconf.argv().env('__'); -var url = require('url'), - async = require('async'), - winston = require('winston'), - colors = require('colors'), - path = require('path'), - pkg = require('./package.json'), - file = require('./src/file'); +var url = require('url'); +var async = require('async'); +var winston = require('winston'); +var path = require('path'); +var pkg = require('./package.json'); +var file = require('./src/file'); global.env = process.env.NODE_ENV || 'production'; @@ -80,7 +79,7 @@ if (nconf.get('setup') || nconf.get('install')) { } else if (nconf.get('plugins')) { listPlugins(); } else if (nconf.get('build')) { - build(nconf.get('build')); + require('./build').build(nconf.get('build')); } else { start(); } @@ -159,7 +158,7 @@ function start() { return; } var meta = require('./src/meta'); - var emitter = require('./src/emitter'); + switch (message.action) { case 'reload': meta.reload(); @@ -239,6 +238,7 @@ function setup() { winston.info('NodeBB Setup Triggered via Command Line'); var install = require('./src/install'); + var build = require('./build'); process.stdout.write('\nWelcome to NodeBB!\n'); process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n'); @@ -247,7 +247,7 @@ function setup() { async.series([ async.apply(install.setup), async.apply(loadConfig), - async.apply(build, true) + async.apply(build.build, true) ], function (err, data) { // Disregard build step data data = data[0]; @@ -280,100 +280,19 @@ function setup() { process.exit(); }); -}; - -function build(targets, callback) { - var db = require('./src/database'); - var meta = require('./src/meta'); - var plugins = require('./src/plugins'); - var valid = ['js', 'clientCSS', 'acpCSS', 'tpl']; - var step = function (target, next) { - winston.info('[build] => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); - next(); - }; - var startTime; - - targets = (targets === true ? valid : targets.split(',').filter(function (target) { - return valid.indexOf(target) !== -1; - })); - - if (!targets) { - winston.error('[build] No valid build targets found. Aborting.'); - return process.exit(0); - } - - async.series([ - async.apply(db.init), - async.apply(meta.themes.setupPaths), - async.apply(plugins.init, null, null) - ], function (err) { - if (err) { - winston.error('[build] Encountered error preparing for build: ' + err.message); - return process.exit(1); - } - - // eachSeries because it potentially(tm) runs faster on Windows this way - async.eachSeries(targets, function (target, next) { - switch(target) { - case 'js': - winston.info('[build] Building javascript'); - startTime = Date.now(); - async.series([ - async.apply(meta.js.minify, 'nodebb.min.js'), - async.apply(meta.js.minify, 'acp.min.js') - ], step.bind(this, target, next)); - break; - - case 'clientCSS': - winston.info('[build] Building client-side CSS'); - startTime = Date.now(); - meta.css.minify('stylesheet.css', step.bind(this, target, next)); - break; - - case 'acpCSS': - winston.info('[build] Building admin control panel CSS'); - startTime = Date.now(); - meta.css.minify('admin.css', step.bind(this, target, next)); - break; - - case 'tpl': - winston.info('[build] Building templates'); - startTime = Date.now(); - meta.templates.compile(step.bind(this, target, next)); - break; - - default: - winston.warn('[build] Unknown build target: \'' + target + '\''); - setImmediate(next); - break; - } - }, function (err) { - if (err) { - winston.error('[build] Encountered error during build step: ' + err.message); - return process.exit(1); - } - - winston.info('[build] Asset compilation successful.'); - - if (typeof callback === 'function') { - callback(); - } else { - process.exit(0); - } - }); - }); -}; +} function upgrade() { var db = require('./src/database'); var meta = require('./src/meta'); var upgrade = require('./src/upgrade'); + var build = require('./build'); async.series([ async.apply(db.init), async.apply(meta.configs.init), async.apply(upgrade.upgrade), - async.apply(build, true) + async.apply(build.build, true) ], function (err) { if (err) { winston.error(err.stack); @@ -382,7 +301,7 @@ function upgrade() { process.exit(0); } }); -}; +} function activate() { var db = require('./src/database'); diff --git a/build.js b/build.js new file mode 100644 index 0000000000..96b7d7af26 --- /dev/null +++ b/build.js @@ -0,0 +1,91 @@ +'use strict'; + +var async = require('async'); +var winston = require('winston'); + +exports.build = function build(targets, callback) { + var db = require('./src/database'); + var meta = require('./src/meta'); + var plugins = require('./src/plugins'); + var valid = ['js', 'clientCSS', 'acpCSS', 'tpl']; + + targets = (targets === true ? valid : targets.split(',').filter(function (target) { + return valid.indexOf(target) !== -1; + })); + + if (!targets) { + winston.error('[build] No valid build targets found. Aborting.'); + return process.exit(0); + } + + async.series([ + async.apply(db.init), + async.apply(meta.themes.setupPaths), + async.apply(plugins.init, null, null) + ], function (err) { + if (err) { + winston.error('[build] Encountered error preparing for build: ' + err.message); + return process.exit(1); + } + + exports.buildTargets(targets, callback); + }); +}; + +exports.buildTargets = function (targets, callback) { + var meta = require('./src/meta'); + var startTime; + var step = function (target, next) { + winston.info('[build] => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); + next(); + }; + // eachSeries because it potentially(tm) runs faster on Windows this way + async.eachSeries(targets, function (target, next) { + switch(target) { + case 'js': + winston.info('[build] Building javascript'); + startTime = Date.now(); + async.series([ + async.apply(meta.js.minify, 'nodebb.min.js'), + async.apply(meta.js.minify, 'acp.min.js') + ], step.bind(this, target, next)); + break; + + case 'clientCSS': + winston.info('[build] Building client-side CSS'); + startTime = Date.now(); + meta.css.minify('stylesheet.css', step.bind(this, target, next)); + break; + + case 'acpCSS': + winston.info('[build] Building admin control panel CSS'); + startTime = Date.now(); + meta.css.minify('admin.css', step.bind(this, target, next)); + break; + + case 'tpl': + winston.info('[build] Building templates'); + startTime = Date.now(); + meta.templates.compile(step.bind(this, target, next)); + break; + + default: + winston.warn('[build] Unknown build target: \'' + target + '\''); + setImmediate(next); + break; + } + }, function (err) { + if (err) { + winston.error('[build] Encountered error during build step: ' + err.message); + return process.exit(1); + } + + winston.info('[build] Asset compilation successful.'); + + if (typeof callback === 'function') { + callback(); + } else { + process.exit(0); + } + }); +}; \ No newline at end of file diff --git a/test/controllers.js b/test/controllers.js index 5b5b1d7ce5..bdaa1e908d 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -553,7 +553,6 @@ describe('Controllers', function () { assert.ifError(err); assert.equal(res.statusCode, 200); assert.equal(body, 'OK'); - console.log(err, res.statusCode, body); done(); }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 1bc780a2cc..42c14ed2f7 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -126,9 +126,13 @@ nconf.set('upload_url', nconf.get('upload_path').replace(/^\/public/, '')); nconf.set('core_templates_path', path.join(__dirname, '../../src/views')); - nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates')); + nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path')); + nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json')); + require('../../build').buildTargets(['js', 'clientCSS', 'acpCSS', 'tpl'], next); + }, + function (next) { var webserver = require('../../src/webserver'); var sockets = require('../../src/socket.io'); sockets.init(webserver.server); From 2a97e5478f42d1642a70eed006fa4e233ffc70a5 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 12:10:22 +0300 Subject: [PATCH 039/131] fix pushGroups test --- test/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/notifications.js b/test/notifications.js index 4a4f6c320d..5ccf0b1248 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -81,7 +81,7 @@ describe('Notifications', function () { }); it('should push a notification to groups', function (done) { - notifications.pushGroup(notification, ['registered-users', 'administrators'], function (err) { + notifications.pushGroups(notification, ['registered-users', 'administrators'], function (err) { assert.ifError(err); setTimeout(function () { db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { From 50a2a7abbee91a5c061217c167c0bbe695c8de41 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 14:03:06 +0300 Subject: [PATCH 040/131] plugins/install tests --- src/plugins/install.js | 87 ++++++++++++++++++++---------------------- test/plugins.js | 55 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/plugins/install.js b/src/plugins/install.js index acde7a22e7..44b9c3747b 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -1,15 +1,15 @@ 'use strict'; -var winston = require('winston'), - async = require('async'), - path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - os = require('os'), +var winston = require('winston'); +var async = require('async'); +var path = require('path'); +var fs = require('fs'); +var nconf = require('nconf'); +var os = require('os'); - db = require('../database'), - meta = require('../meta'), - pubsub = require('../pubsub'); +var db = require('../database'); +var meta = require('../meta'); +var pubsub = require('../pubsub'); module.exports = function (Plugins) { @@ -68,43 +68,38 @@ module.exports = function (Plugins) { }; function toggleInstall(id, version, callback) { - Plugins.isInstalled(id, function (err, installed) { - if (err) { - return callback(err); + var type; + var installed; + async.waterfall([ + function (next) { + Plugins.isInstalled(id, next); + }, + function (_installed, next) { + installed = _installed; + type = installed ? 'uninstall' : 'install'; + Plugins.isActive(id, next); + }, + function (active, next) { + if (active) { + Plugins.toggleActive(id, function (err, status) { + next(err); + }); + return; + } + next(); + }, + function (next) { + var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest')); + runNpmCommand(command, next); + }, + function (next) { + Plugins.get(id, next); + }, + function (pluginData, next) { + Plugins.fireHook('action:plugin.' + type, id); + next(null, pluginData); } - var type = installed ? 'uninstall' : 'install'; - async.waterfall([ - function (next) { - Plugins.isActive(id, next); - }, - function (active, next) { - if (active) { - Plugins.toggleActive(id, function (err, status) { - next(err); - }); - return; - } - next(); - }, - function (next) { - var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest')); - runNpmCommand(command, next); - } - ], function (err) { - if (err) { - return callback(err); - } - - Plugins.get(id, function (err, pluginData) { - if (err) { - return callback(err); - } - - Plugins.fireHook('action:plugin.' + type, id); - callback(null, pluginData); - }); - }); - }); + ], callback); } function runNpmCommand(command, callback) { @@ -113,7 +108,7 @@ module.exports = function (Plugins) { return callback(err); } winston.info('[plugins] ' + stdout); - callback(err); + callback(); }); } diff --git a/test/plugins.js b/test/plugins.js index 8cb76e80bb..9af8224dee 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -96,5 +96,60 @@ describe('Plugins', function () { }); }); + describe('install/activate/uninstall', function () { + var latest; + var pluginName = 'nodebb-plugin-imgur'; + it('should install a plugin', function (done) { + plugins.toggleInstall(pluginName, '1.0.16', function (err, pluginData) { + assert.ifError(err); + + latest = pluginData.latest; + + assert.equal(pluginData.name, pluginName); + assert.equal(pluginData.id, pluginName); + assert.equal(pluginData.url, 'https://github.com/barisusakli/nodebb-plugin-imgur#readme'); + assert.equal(pluginData.description, 'A Plugin that uploads images to imgur'); + assert.equal(pluginData.active, false); + assert.equal(pluginData.installed, true); + + done(); + }); + }); + + it('should activate plugin', function (done) { + plugins.toggleActive(pluginName, function (err) { + assert.ifError(err); + plugins.isActive(pluginName, function (err, isActive) { + assert.ifError(err); + assert(isActive); + done(); + }); + }); + }); + + it('should upgrade plugin', function (done) { + plugins.upgrade(pluginName, 'latest', function (err, isActive) { + assert.ifError(err); + assert(isActive); + plugins.loadPluginInfo(path.join(nconf.get('base_dir'), 'node_modules', pluginName), function (err, pluginInfo) { + assert.ifError(err); + assert.equal(pluginInfo.version, latest); + done(); + }); + }); + }); + + it('should uninstall a plugin', function (done) { + plugins.toggleInstall(pluginName, 'latest', function (err, pluginData) { + assert.ifError(err); + assert.equal(pluginData.installed, false); + assert.equal(pluginData.active, false); + done(); + }); + }); + }); + + + }); From 5e7fb4eeb240c9732051a8f0af8ff51972879f79 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 14:43:19 +0300 Subject: [PATCH 041/131] widget tests --- src/socket.io/admin.js | 2 +- test/controllers.js | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index e59e15d55c..8240dd8b36 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -120,7 +120,7 @@ SocketAdmin.plugins.upgrade = function (socket, data, callback) { }; SocketAdmin.widgets.set = function (socket, data, callback) { - if(!data) { + if (!data) { return callback(new Error('[[error:invalid-data]]')); } diff --git a/test/controllers.js b/test/controllers.js index bdaa1e908d..fb80b5a3dc 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -558,7 +558,59 @@ describe('Controllers', function () { }); }); }); + }); + describe('widgets', function () { + var widgets = require('../src/widgets'); + + before(function (done) { + async.waterfall([ + function (next) { + widgets.reset(next); + }, + function (next) { + var data = { + template: 'categories.tpl', + location: 'sidebar', + widgets: [ + { + widget: 'html', + data: [ { + widget: 'html', + data: { + html: 'test', + title: '', + container: '' + } + } ] + } + ] + }; + + widgets.setArea(data, next); + } + ], done); + }); + + it('should return {} if there is no template or locations', function (done) { + request(nconf.get('url') + '/api/widgets/render', {json: true}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert.equal(Object.keys(body), 0); + done(); + }); + }); + + it('should render templates', function (done) { + var url = nconf.get('url') + '/api/widgets/render?template=categories.tpl&url=&isMobile=false&locations%5B%5D=sidebar&locations%5B%5D=footer&locations%5B%5D=header'; + request(url, {json: true}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); }); From 853cea7fec0a67dc18f937f971d9871cc7a9a197 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 15:03:50 +0300 Subject: [PATCH 042/131] removed emitter.js --- src/emitter.js | 35 ----------------------------------- src/plugins.js | 2 -- src/socket.io/meta.js | 8 -------- src/webserver.js | 8 ++++++-- 4 files changed, 6 insertions(+), 47 deletions(-) delete mode 100644 src/emitter.js diff --git a/src/emitter.js b/src/emitter.js deleted file mode 100644 index ca262257b7..0000000000 --- a/src/emitter.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -var eventEmitter = new (require('events')).EventEmitter(); - - -eventEmitter.all = function (events, callback) { - var eventList = events.slice(0); - - events.forEach(function onEvent(event) { - eventEmitter.on(event, function () { - var index = eventList.indexOf(event); - if (index === -1) { - return; - } - eventList.splice(index, 1); - if (eventList.length === 0) { - callback(); - } - }); - }); -}; - -eventEmitter.any = function (events, callback) { - events.forEach(function onEvent(event) { - eventEmitter.on(event, function () { - if (events !== null) { - callback(); - } - - events = null; - }); - }); -}; - -module.exports = eventEmitter; \ No newline at end of file diff --git a/src/plugins.js b/src/plugins.js index 1a20597564..0f9348f9f3 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -9,7 +9,6 @@ var express = require('express'); var nconf = require('nconf'); var db = require('./database'); -var emitter = require('./emitter'); var utils = require('../public/src/utils'); var hotswap = require('./hotswap'); var file = require('./file'); @@ -69,7 +68,6 @@ var middleware; } Plugins.initialized = true; - emitter.emit('plugins:loaded'); callback(); }); }; diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 35a5da17e1..5714f24080 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -3,7 +3,6 @@ var meta = require('../meta'); var user = require('../user'); var topics = require('../topics'); -var emitter = require('../emitter'); var websockets = require('./'); @@ -18,13 +17,6 @@ SocketMeta.reconnected = function (socket, data, callback) { } }; -emitter.on('nodebb:ready', function () { - websockets.server.emit('event:nodebb.ready', { - 'cache-buster': meta.config['cache-buster'] - }); -}); - - /* Rooms */ SocketMeta.rooms.enter = function (socket, data, callback) { diff --git a/src/webserver.js b/src/webserver.js index 964ae60bfd..fec42974c6 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -26,7 +26,6 @@ var logger = require('./logger'); var plugins = require('./plugins'); var routes = require('./routes'); var auth = require('./routes/authentication'); -var emitter = require('./emitter'); var templates = require('templates.js'); var helpers = require('../public/src/modules/helpers'); @@ -68,7 +67,12 @@ module.exports.listen = function (callback) { } winston.info('NodeBB Ready'); - emitter.emit('nodebb:ready'); + + require('./socket.io').server.emit('event:nodebb.ready', { + 'cache-buster': meta.config['cache-buster'] + }); + + plugins.fireHook('action:nodebb.ready'); listen(callback); }); From e3616ab0f9125174cb4e4c4285383a5bd18da69c Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 15:32:35 +0300 Subject: [PATCH 043/131] socket/meta test --- src/socket.io/meta.js | 6 +++--- test/socket.io.js | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 5714f24080..baa0abc0aa 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -1,20 +1,20 @@ 'use strict'; -var meta = require('../meta'); + var user = require('../user'); var topics = require('../topics'); -var websockets = require('./'); - var SocketMeta = { rooms: {} }; SocketMeta.reconnected = function (socket, data, callback) { + callback = callback || function () {}; if (socket.uid) { topics.pushUnreadCount(socket.uid); user.notifications.pushCount(socket.uid); } + callback(); }; /* Rooms */ diff --git a/test/socket.io.js b/test/socket.io.js index 345d8782f4..ff255a0208 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -275,6 +275,53 @@ describe('socket.io', function () { }); }); + it('should push unread notifications on reconnect', function (done) { + var socketMeta = require('../src/socket.io/meta'); + socketMeta.reconnected({uid: 1}, {}, function (err) { + assert.ifError(err); + done(); + }); + }); + + + it('should error if the room is missing', function (done) { + io.emit('meta.rooms.enter', null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should return if uid is 0', function (done) { + var socketMeta = require('../src/socket.io/meta'); + socketMeta.rooms.enter({uid: 0}, null, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should join a room', function (done) { + io.emit('meta.rooms.enter', {enter: 'recent_topics'}, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should leave current room', function (done) { + io.emit('meta.rooms.leaveCurrent', {}, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should get server time', function (done) { + var socketMeta = require('../src/socket.io/meta'); + socketMeta.getServerTime({uid: 1}, null, function (err, time) { + assert.ifError(err); + assert(time); + done(); + }); + }); + after(function (done) { db.emptydb(done); }); From 9796f545803d04b8ba134f030447faeb0de1e677 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 15:57:53 +0300 Subject: [PATCH 044/131] analytics tests, reduce bcrypt rouds for tests --- src/analytics.js | 326 +++++++++++++++++++------------------ test/controllers.js | 8 +- test/mocks/databasemock.js | 1 + test/socket.io.js | 18 ++ 4 files changed, 190 insertions(+), 163 deletions(-) diff --git a/src/analytics.js b/src/analytics.js index ab834b75b2..6b248057da 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -6,192 +6,194 @@ var winston = require('winston'); var db = require('./database'); -(function (Analytics) { - var counters = {}; +var Analytics = module.exports; - var pageViews = 0; - var uniqueIPCount = 0; - var uniquevisitors = 0; +var counters = {}; - var isCategory = /^(?:\/api)?\/category\/(\d+)/; +var pageViews = 0; +var uniqueIPCount = 0; +var uniquevisitors = 0; - new cronJob('*/10 * * * *', function () { - Analytics.writeData(); - }, null, true); +var isCategory = /^(?:\/api)?\/category\/(\d+)/; - Analytics.increment = function (keys) { - keys = Array.isArray(keys) ? keys : [keys]; +new cronJob('*/10 * * * *', function () { + Analytics.writeData(); +}, null, true); - keys.forEach(function (key) { - counters[key] = counters[key] || 0; - ++counters[key]; - }); - }; +Analytics.increment = function (keys) { + keys = Array.isArray(keys) ? keys : [keys]; - Analytics.pageView = function (payload) { - ++pageViews; + keys.forEach(function (key) { + counters[key] = counters[key] || 0; + ++counters[key]; + }); +}; - if (payload.ip) { - db.sortedSetScore('ip:recent', payload.ip, function (err, score) { - if (err) { - return; - } - if (!score) { - ++uniqueIPCount; - } - var today = new Date(); - today.setHours(today.getHours(), 0, 0, 0); - if (!score || score < today.getTime()) { - ++uniquevisitors; - db.sortedSetAdd('ip:recent', Date.now(), payload.ip); - } - }); - } +Analytics.pageView = function (payload) { + ++pageViews; - if (payload.path) { - var categoryMatch = payload.path.match(isCategory), - cid = categoryMatch ? parseInt(categoryMatch[1], 10) : null; - - if (cid) { - Analytics.increment(['pageviews:byCid:' + cid]); - } - } - }; - - Analytics.writeData = function () { - var today = new Date(); - var month = new Date(); - var dbQueue = []; - - today.setHours(today.getHours(), 0, 0, 0); - month.setMonth(month.getMonth(), 1); - month.setHours(0, 0, 0, 0); - - if (pageViews > 0) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime())); - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime())); - pageViews = 0; - } - - if (uniquevisitors > 0) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime())); - uniquevisitors = 0; - } - - if (uniqueIPCount > 0) { - dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount)); - uniqueIPCount = 0; - } - - if (Object.keys(counters).length > 0) { - for(var key in counters) { - if (counters.hasOwnProperty(key)) { - dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime())); - delete counters[key]; - } - } - } - - async.parallel(dbQueue, function (err) { + if (payload.ip) { + db.sortedSetScore('ip:recent', payload.ip, function (err, score) { if (err) { - winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); + return; + } + if (!score) { + ++uniqueIPCount; + } + var today = new Date(); + today.setHours(today.getHours(), 0, 0, 0); + if (!score || score < today.getTime()) { + ++uniquevisitors; + db.sortedSetAdd('ip:recent', Date.now(), payload.ip); } }); - }; + } - Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { - var terms = {}, - hoursArr = []; + if (payload.path) { + var categoryMatch = payload.path.match(isCategory), + cid = categoryMatch ? parseInt(categoryMatch[1], 10) : null; - hour = new Date(hour); - hour.setHours(hour.getHours(), 0, 0, 0); + if (cid) { + Analytics.increment(['pageviews:byCid:' + cid]); + } + } +}; - for (var i = 0, ii = numHours; i < ii; i++) { - hoursArr.push(hour.getTime()); - hour.setHours(hour.getHours() - 1, 0, 0, 0); +Analytics.writeData = function (callback) { + callback = callback || function () {}; + var today = new Date(); + var month = new Date(); + var dbQueue = []; + + today.setHours(today.getHours(), 0, 0, 0); + month.setMonth(month.getMonth(), 1); + month.setHours(0, 0, 0, 0); + + if (pageViews > 0) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime())); + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime())); + pageViews = 0; + } + + if (uniquevisitors > 0) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime())); + uniquevisitors = 0; + } + + if (uniqueIPCount > 0) { + dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount)); + uniqueIPCount = 0; + } + + if (Object.keys(counters).length > 0) { + for(var key in counters) { + if (counters.hasOwnProperty(key)) { + dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime())); + delete counters[key]; + } + } + } + + async.parallel(dbQueue, function (err) { + if (err) { + winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); + } + callback(err); + }); +}; + +Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { + var terms = {}, + hoursArr = []; + + hour = new Date(hour); + hour.setHours(hour.getHours(), 0, 0, 0); + + for (var i = 0, ii = numHours; i < ii; i++) { + hoursArr.push(hour.getTime()); + hour.setHours(hour.getHours() - 1, 0, 0, 0); + } + + db.sortedSetScores(set, hoursArr, function (err, counts) { + if (err) { + return callback(err); } - db.sortedSetScores(set, hoursArr, function (err, counts) { + hoursArr.forEach(function (term, index) { + terms[term] = parseInt(counts[index], 10) || 0; + }); + + var termsArr = []; + + hoursArr.reverse(); + hoursArr.forEach(function (hour) { + termsArr.push(terms[hour]); + }); + + callback(null, termsArr); + }); +}; + +Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { + var daysArr = []; + + day = new Date(day); + day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values + day.setHours(0, 0, 0, 0); + + async.whilst(function () { + return numDays--; + }, function (next) { + Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { if (err) { - return callback(err); + return next(err); } - hoursArr.forEach(function (term, index) { - terms[term] = parseInt(counts[index], 10) || 0; - }); - - var termsArr = []; - - hoursArr.reverse(); - hoursArr.forEach(function (hour) { - termsArr.push(terms[hour]); - }); - - callback(null, termsArr); + daysArr.push(day.reduce(function (cur, next) { + return cur + next; + })); + next(); }); - }; + }, function (err) { + callback(err, daysArr); + }); +}; - Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { - var daysArr = []; +Analytics.getUnwrittenPageviews = function () { + return pageViews; +}; - day = new Date(day); - day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values - day.setHours(0, 0, 0, 0); +Analytics.getMonthlyPageViews = function (callback) { + var thisMonth = new Date(); + var lastMonth = new Date(); + thisMonth.setMonth(thisMonth.getMonth(), 1); + thisMonth.setHours(0, 0, 0, 0); + lastMonth.setMonth(thisMonth.getMonth() - 1, 1); + lastMonth.setHours(0, 0, 0, 0); - async.whilst(function () { - return numDays--; - }, function (next) { - Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { - if (err) { - return next(err); - } + var values = [thisMonth.getTime(), lastMonth.getTime()]; - daysArr.push(day.reduce(function (cur, next) { - return cur + next; - })); - next(); - }); - }, function (err) { - callback(err, daysArr); - }); - }; + db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { + if (err) { + return callback(err); + } + callback(null, {thisMonth: scores[0] || 0, lastMonth: scores[1] || 0}); + }); +}; - Analytics.getUnwrittenPageviews = function () { - return pageViews; - }; +Analytics.getCategoryAnalytics = function (cid, callback) { + async.parallel({ + 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), + 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), + 'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7), + 'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7), + }, callback); +}; - Analytics.getMonthlyPageViews = function (callback) { - var thisMonth = new Date(); - var lastMonth = new Date(); - thisMonth.setMonth(thisMonth.getMonth(), 1); - thisMonth.setHours(0, 0, 0, 0); - lastMonth.setMonth(thisMonth.getMonth() - 1, 1); - lastMonth.setHours(0, 0, 0, 0); +Analytics.getErrorAnalytics = function (callback) { + async.parallel({ + 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), + 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) + }, callback); +}; - var values = [thisMonth.getTime(), lastMonth.getTime()]; - - db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { - if (err) { - return callback(err); - } - callback(null, {thisMonth: scores[0] || 0, lastMonth: scores[1] || 0}); - }); - }; - - Analytics.getCategoryAnalytics = function (cid, callback) { - async.parallel({ - 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), - 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), - 'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7), - 'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7), - }, callback); - }; - - Analytics.getErrorAnalytics = function (callback) { - async.parallel({ - 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), - 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) - }, callback); - }; - -}(exports)); \ No newline at end of file diff --git a/test/controllers.js b/test/controllers.js index fb80b5a3dc..2ec3c896ce 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -45,6 +45,8 @@ describe('Controllers', function () { }); }); + + it('should load default home route', function (done) { request(nconf.get('url'), function (err, res, body) { assert.ifError(err); @@ -615,6 +617,10 @@ describe('Controllers', function () { after(function (done) { - db.emptydb(done); + var analytics = require('../src/analytics'); + analytics.writeData(function (err) { + assert.ifError(err); + db.emptydb(done); + }); }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 42c14ed2f7..61ab8b2bbe 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -129,6 +129,7 @@ nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path')); nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json')); + nconf.set('bcrypt_rounds', 6); require('../../build').buildTargets(['js', 'clientCSS', 'acpCSS', 'tpl'], next); }, diff --git a/test/socket.io.js b/test/socket.io.js index ff255a0208..b03753f362 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -322,6 +322,24 @@ describe('socket.io', function () { }); }); + it('should get daily analytics', function (done) { + io.emit('admin.analytics.get', {graph: 'traffic', units: 'days'}, function (err, data) { + assert.ifError(err); + assert(data); + assert(data.monthlyPageViews); + done(); + }); + }); + + it('should get hourly analytics', function (done) { + io.emit('admin.analytics.get', {graph: 'traffic', units: 'hours'}, function (err, data) { + assert.ifError(err); + assert(data); + assert(data.monthlyPageViews); + done(); + }); + }); + after(function (done) { db.emptydb(done); }); From 8fd5b12291def0b2f7a34819760dc79752b1e77d Mon Sep 17 00:00:00 2001 From: NodeBB Misty Date: Fri, 18 Nov 2016 09:02:26 -0500 Subject: [PATCH 045/131] Latest translations and fallbacks --- public/language/sk/global.json | 4 ++-- public/language/sk/pages.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/language/sk/global.json b/public/language/sk/global.json index c27e4f755c..c0b769406c 100644 --- a/public/language/sk/global.json +++ b/public/language/sk/global.json @@ -54,9 +54,9 @@ "posts": "Príspevky", "best": "Najlepšie", "upvoters": "Hlasovali za", - "upvoted": "Pridať hlas", + "upvoted": "Pridaný hlas", "downvoters": "Hlasovali proti", - "downvoted": "Odobrať hlas", + "downvoted": "Odobratý hlas", "views": "Zhliadnutí", "reputation": "Reputácia", "read_more": "čítaj viac", diff --git a/public/language/sk/pages.json b/public/language/sk/pages.json index 6e0cc9125e..c9328ef0dd 100644 --- a/public/language/sk/pages.json +++ b/public/language/sk/pages.json @@ -40,9 +40,9 @@ "account/bookmarks": "%1 príspevky v záložkach", "account/settings": "Užívateľské nastavenia", "account/watched": "Témy sledovalo %1", - "account/upvoted": "Príspevku dali hlas %1", - "account/downvoted": "Príspevku odobrali hlas %1", - "account/best": "Najlepšie príspevky uskutočnil %1", + "account/upvoted": "Príspevky, ktorým užívateľ %1 dal hlas", + "account/downvoted": "Príspevky, ktorým užívateľ %1 odobral hlas", + "account/best": "Najlepšie príspevky vytvorené užívateľom %1", "confirm": "E-mail potvrdený", "maintenance.text": "%1 v súčasnej dobe prebieha údržba. Prosíme, vráťte sa neskôr.", "maintenance.messageIntro": "Správca, dodatočne zanechal túto správu:", From 83c50f064826db023a89e5f8096aa97c9aa7e1d1 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 17:29:41 +0300 Subject: [PATCH 046/131] add back emitter --- src/emitter.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/emitter.js diff --git a/src/emitter.js b/src/emitter.js new file mode 100644 index 0000000000..ca262257b7 --- /dev/null +++ b/src/emitter.js @@ -0,0 +1,35 @@ +"use strict"; + +var eventEmitter = new (require('events')).EventEmitter(); + + +eventEmitter.all = function (events, callback) { + var eventList = events.slice(0); + + events.forEach(function onEvent(event) { + eventEmitter.on(event, function () { + var index = eventList.indexOf(event); + if (index === -1) { + return; + } + eventList.splice(index, 1); + if (eventList.length === 0) { + callback(); + } + }); + }); +}; + +eventEmitter.any = function (events, callback) { + events.forEach(function onEvent(event) { + eventEmitter.on(event, function () { + if (events !== null) { + callback(); + } + + events = null; + }); + }); +}; + +module.exports = eventEmitter; \ No newline at end of file From dada8585658ea6ab038a1214676edf6c0b842767 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 17:35:05 +0300 Subject: [PATCH 047/131] add canReply to messages --- package.json | 2 +- src/controllers/accounts/chats.js | 6 ++++-- src/messaging/rooms.js | 14 ++++++++++++++ src/socket.io/modules.js | 2 ++ src/user.js | 7 +++++++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 654c45bff8..44a2c1003b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "nodebb-plugin-spam-be-gone": "0.4.10", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.15", - "nodebb-theme-persona": "4.1.86", + "nodebb-theme-persona": "4.1.87", "nodebb-theme-vanilla": "5.1.56", "nodebb-widget-essentials": "2.0.13", "nodemailer": "2.6.4", diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index f70b9b4db8..c951bafdbd 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -56,13 +56,14 @@ chatsController.get = function (req, res, callback) { } async.parallel({ users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1), + canReply: async.apply(messaging.canReply, req.params.roomid, req.uid), + room: async.apply(messaging.getRoomData, req.params.roomid), messages: async.apply(messaging.getMessages, { callerUid: req.uid, uid: uid, roomId: req.params.roomid, isNew: false - }), - room: async.apply(messaging.getRoomData, req.params.roomid) + }) }, next); } ], function (err, data) { @@ -77,6 +78,7 @@ chatsController.get = function (req, res, callback) { return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; }); + room.canReply = data.canReply; room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; room.rooms = recentChats.rooms; room.uid = uid; diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 2687448c56..bea909946a 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -209,4 +209,18 @@ module.exports = function (Messaging) { ], callback); }; + Messaging.canReply = function (roomId, uid, callback) { + async.waterfall([ + function (next) { + db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, next); + }, + function (inRoom, next) { + plugins.fireHook('filter:messaging.canReply', {uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom}, next); + }, + function (data, next) { + next(null, data.canReply); + } + ], callback); + }; + }; \ No newline at end of file diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index f74e077c5c..46f3804c03 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -114,6 +114,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { async.parallel({ roomData: async.apply(Messaging.getRoomData, data.roomId), + canReply: async.apply(Messaging.canReply, data.roomId, socket.uid), users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1), messages: async.apply(Messaging.getMessages, { callerUid: socket.uid, @@ -125,6 +126,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { }, function (results, next) { results.roomData.users = results.users; + results.roomData.canReply = results.canReply; results.roomData.usernames = Messaging.generateUsernames(results.users, socket.uid); results.roomData.messages = results.messages; results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 2; diff --git a/src/user.js b/src/user.js index 31dda0d991..f6bdc07902 100644 --- a/src/user.js +++ b/src/user.js @@ -222,6 +222,13 @@ var meta = require('./meta'); db.sortedSetScore('email:uid', email.toLowerCase(), callback); }; + User.getUidsByEmails = function (emails, callback) { + emails = emails.map(function(email) { + return email && email.toLowerCase(); + }); + db.sortedSetScores('email:uid', emails, callback); + }; + User.getUsernameByEmail = function (email, callback) { db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) { if (err) { From a3efe42938bc4c7c5110543f633a3abe28716464 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Nov 2016 17:40:55 +0300 Subject: [PATCH 048/131] fix test --- src/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user.js b/src/user.js index f6bdc07902..7c0fa5e67e 100644 --- a/src/user.js +++ b/src/user.js @@ -223,7 +223,7 @@ var meta = require('./meta'); }; User.getUidsByEmails = function (emails, callback) { - emails = emails.map(function(email) { + emails = emails.map(function (email) { return email && email.toLowerCase(); }); db.sortedSetScores('email:uid', emails, callback); From 0e8bf17ff030547c1096256336ae51c8f1552266 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 18 Nov 2016 12:53:26 -0500 Subject: [PATCH 049/131] re: #5211, bringing back the RELOAD BUTTON :rage2: --- public/src/admin/admin.js | 20 +++++--- public/src/admin/modules/instance.js | 73 ++++++++++++++------------- src/socket.io/admin.js | 34 ++++++++++--- src/views/admin/general/dashboard.tpl | 8 ++- src/views/admin/partials/menu.tpl | 5 ++ 5 files changed, 89 insertions(+), 51 deletions(-) diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 13a2340d5f..c7a874e96a 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -124,6 +124,16 @@ } function setupRestartLinks() { + $('.reload').off('click').on('click', function () { + bootbox.confirm('Are you sure you wish to reload NodeBB?', function (confirm) { + if (confirm) { + require(['admin/modules/instance'], function (instance) { + instance.reload(); + }); + } + }); + }); + $('.restart').off('click').on('click', function () { bootbox.confirm('Are you sure you wish to restart NodeBB?', function (confirm) { if (confirm) { @@ -133,13 +143,7 @@ } }); }); - - $('.reload').off('click').on('click', function () { - require(['admin/modules/instance'], function (instance) { - instance.reload(); - }); - }); - } + }; function launchSnackbar(params) { var message = (params.title ? "" + params.title + "" : '') + (params.message ? params.message : ''); @@ -148,7 +152,7 @@ translator.translate(message, function (html) { var bar = $.snackbar({ content: html, - timeout: 3000, + timeout: params.timeout || 3000, htmlAllowed: true }); diff --git a/public/src/admin/modules/instance.js b/public/src/admin/modules/instance.js index d5c2164155..d057ff9853 100644 --- a/public/src/admin/modules/instance.js +++ b/public/src/admin/modules/instance.js @@ -14,45 +14,12 @@ define('admin/modules/instance', function () { timeout: 5000 }); - socket.emit('admin.reload', function (err) { - if (!err) { - app.alert({ - alert_id: 'instance_reload', - type: 'success', - title: ' Success', - message: 'NodeBB has successfully reloaded.', - timeout: 5000 - }); - } else { - app.alert({ - alert_id: 'instance_reload', - type: 'danger', - title: '[[global:alert.error]]', - message: '[[error:reload-failed, ' + err.message + ']]' - }); - } - - if (typeof callback === 'function') { - callback(); - } - }); - }; - - instance.restart = function (callback) { - app.alert({ - alert_id: 'instance_restart', - type: 'info', - title: 'Restarting... ', - message: 'NodeBB is restarting.', - timeout: 5000 - }); - $(window).one('action:reconnected', function () { app.alert({ - alert_id: 'instance_restart', + alert_id: 'instance_reload', type: 'success', title: ' Success', - message: 'NodeBB has successfully restarted.', + message: 'NodeBB has reloaded successfully.', timeout: 5000 }); @@ -61,7 +28,41 @@ define('admin/modules/instance', function () { } }); - socket.emit('admin.restart'); + socket.emit('admin.reload'); + }; + + instance.restart = function (callback) { + app.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Rebuilding... ', + message: 'NodeBB is rebiulding front-end assets (css, javascript, etc).', + timeout: 10000 + }); + + $(window).one('action:reconnected', function () { + app.alert({ + alert_id: 'instance_restart', + type: 'success', + title: ' Success', + message: 'NodeBB has successfully restarted.', + timeout: 10000 + }); + + if (typeof callback === 'function') { + callback(); + } + }); + + socket.emit('admin.restart', function () { + app.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Build Complete!... ', + message: 'NodeBB is reloading.', + timeout: 10000 + }); + }); }; return instance; diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 8240dd8b36..7e9568f960 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -3,6 +3,7 @@ var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); +var path = require('path'); var meta = require('../meta'); var plugins = require('../plugins'); @@ -49,7 +50,7 @@ SocketAdmin.before = function (socket, method, data, next) { }); }; -SocketAdmin.restart = function (socket, data, callback) { +SocketAdmin.reload = function (socket, data, callback) { events.log({ type: 'restart', uid: socket.uid, @@ -57,12 +58,33 @@ SocketAdmin.restart = function (socket, data, callback) { }); meta.restart(); callback(); -}; +} -/** - * Reload deprecated as of v1.1.2+, remove in v2.x - */ -SocketAdmin.reload = SocketAdmin.restart; +SocketAdmin.restart = function (socket, data, callback) { + // Rebuild assets and reload NodeBB + var child_process = require('child_process'); + var build_worker = child_process.fork('app.js', ['--build'], { + cwd: path.join(__dirname, '../../'), + stdio: 'pipe' + }); + + build_worker.on('exit', function() { + events.log({ + type: 'build', + uid: socket.uid, + ip: socket.ip + }); + + events.log({ + type: 'restart', + uid: socket.uid, + ip: socket.ip + }); + + meta.restart(); + callback(); + }); +}; SocketAdmin.fireEvent = function (socket, data, callback) { index.server.emit(data.name, data.payload || {}); diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index 4f7775b226..8cb0be4032 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -95,7 +95,13 @@
System Control

- +

+ + +
+

+

+ Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.

Maintenance Mode diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 7e4f890730..0fea719e46 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -122,6 +122,11 @@

From a4ebb7f56e391da609d7c8180f92abe99d7632d3 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 30 Nov 2016 18:21:30 +0300 Subject: [PATCH 121/131] test plugin static assets --- test/plugins.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/plugins.js b/test/plugins.js index 9af8224dee..67709bcf49 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -4,6 +4,7 @@ var assert = require('assert'); var path = require('path'); var nconf = require('nconf'); +var request = require('request'); var db = require('./mocks/databasemock'); var plugins = require('../src/plugins'); @@ -149,6 +150,35 @@ describe('Plugins', function () { }); }); + describe('static assets', function () { + it('should 404 if resource does not exist', function (done) { + request.get(nconf.get('url') + '/plugins/doesnotexist/should404.tpl', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + assert(body); + done(); + }); + }); + + it('should 404 if resource does not exist', function (done) { + request.get(nconf.get('url') + '/plugins/nodebb-plugin-dbsearch/dbsearch/templates/admin/plugins/should404.tpl', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + assert(body); + done(); + }); + }); + + it('should get resource', function (done) { + request.get(nconf.get('url') + '/plugins/nodebb-plugin-dbsearch/dbsearch/templates/admin/plugins/dbsearch.tpl', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + }); + }); From 02aadf79b525d833f1840143a12aef7650225bf7 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 30 Nov 2016 19:13:14 +0300 Subject: [PATCH 122/131] account/posts controller tests --- src/controllers/accounts/posts.js | 95 +++++++++++++++++-------------- test/controllers.js | 85 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 44 deletions(-) diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 53ff073dad..15cab4f4ca 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -97,56 +97,63 @@ postsController.getTopics = function (req, res, next) { getFromUserSet(data, req, res, next); }; -function getFromUserSet(data, req, res, next) { - async.parallel({ - settings: function (next) { - user.getSettings(req.uid, next); - }, - userData: function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); - } - }, function (err, results) { - if (err || !results.userData) { - return next(err); - } - - var userData = results.userData; - - var setName = 'uid:' + userData.uid + ':' + data.set; - - var page = Math.max(1, parseInt(req.query.page, 10) || 1); - var itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; - - async.parallel({ - itemCount: function (next) { - if (results.settings.usePagination) { - db.sortedSetCard(setName, next); - } else { - next(null, 0); +function getFromUserSet(data, req, res, callback) { + var userData; + var itemsPerPage; + var page = Math.max(1, parseInt(req.query.page, 10) || 1); + async.waterfall([ + function (next) { + async.parallel({ + settings: function (next) { + user.getSettings(req.uid, next); + }, + userData: function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); } - }, - data: function (next) { - var start = (page - 1) * itemsPerPage; - var stop = start + itemsPerPage - 1; - data.method(setName, req.uid, start, stop, next); - } - }, function (err, results) { - if (err) { - return next(err); + }, next); + }, + function (results, next) { + if (!results.userData) { + return callback(); } - userData[data.type] = results.data[data.type]; - userData.nextStart = results.data.nextStart; + userData = results.userData; - var pageCount = Math.ceil(results.itemCount / itemsPerPage); - userData.pagination = pagination.create(page, pageCount); + var setName = 'uid:' + userData.uid + ':' + data.set; - userData.noItemsFoundKey = data.noItemsFoundKey; - userData.title = '[[pages:' + data.template + ', ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: data.crumb}]); + itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; - res.render(data.template, userData); - }); + async.parallel({ + itemCount: function (next) { + if (results.settings.usePagination) { + db.sortedSetCard(setName, next); + } else { + next(null, 0); + } + }, + data: function (next) { + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + data.method(setName, req.uid, start, stop, next); + } + }, next); + } + ], function (err, results) { + if (err) { + return callback(err); + } + + userData[data.type] = results.data[data.type]; + userData.nextStart = results.data.nextStart; + + var pageCount = Math.ceil(results.itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount); + + userData.noItemsFoundKey = data.noItemsFoundKey; + userData.title = '[[pages:' + data.template + ', ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: data.crumb}]); + + res.render(data.template, userData); }); } diff --git a/test/controllers.js b/test/controllers.js index 04afade0fc..04bbf26c1b 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -703,6 +703,91 @@ describe('Controllers', function () { }); }); + describe('account post pages', function () { + var helpers = require('./helpers'); + var jar; + before(function (done) { + helpers.loginUser('foo', 'barbar', function (err, _jar) { + assert.ifError(err); + jar = _jar; + done(); + }); + }); + + it('should load /user/foo/posts', function (done) { + request(nconf.get('url') + '/api/user/foo/posts', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should 401 if not logged in', function (done) { + request(nconf.get('url') + '/api/user/foo/bookmarks', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 401); + assert(body); + done(); + }); + }); + + it('should load /user/foo/bookmarks', function (done) { + request(nconf.get('url') + '/api/user/foo/bookmarks', {jar: jar}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load /user/foo/upvoted', function (done) { + request(nconf.get('url') + '/api/user/foo/upvoted', {jar: jar}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load /user/foo/downvoted', function (done) { + request(nconf.get('url') + '/api/user/foo/downvoted', {jar: jar}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load /user/foo/best', function (done) { + request(nconf.get('url') + '/api/user/foo/best', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load /user/foo/watched', function (done) { + request(nconf.get('url') + '/api/user/foo/watched', {jar: jar}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load /user/foo/topics', function (done) { + request(nconf.get('url') + '/api/user/foo/topics', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + }); + after(function (done) { var analytics = require('../src/analytics'); analytics.writeData(function (err) { From d6c2779ed067d57f2cf4757964e348f1f467b4c8 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 11:31:10 +0300 Subject: [PATCH 123/131] remove placeholder #5242 --- src/views/admin/settings/post.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index a9b784957c..95f39c829a 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -90,7 +90,7 @@
- +

Dates & times will be shown in a relative manner (e.g. "3 hours ago" / "5 days ago"), and localised into various languages. After a certain point, this text can be switched to display the localised date itself From ea007e2da4ae1ec39afe8cd2a5b5b818ddb5c732 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 11:52:35 +0300 Subject: [PATCH 124/131] closes #5245 --- src/groups/create.js | 2 +- test/groups.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/groups/create.js b/src/groups/create.js index 0b38b3ebcc..1d16ea33cf 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -92,7 +92,7 @@ module.exports = function (Groups) { return callback(new Error('[[error:group-name-too-long]]')); } - if (name.indexOf('/') !== -1) { + if (name.indexOf('/') !== -1 || !utils.slugify(name)) { return callback(new Error('[[error:invalid-group-name]]')); } diff --git a/test/groups.js b/test/groups.js index 122645cc2b..35d3c17be6 100644 --- a/test/groups.js +++ b/test/groups.js @@ -184,6 +184,20 @@ describe('Groups', function () { done(); }); }); + + it('should fail to create group if slug is empty', function (done) { + Groups.create({name: '>>>>'}, function (err) { + assert.equal(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail if group name is invalid', function (done) { + Groups.create({name: 'not/valid'}, function (err) { + assert.equal(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); }); describe('.hide()', function () { From 76044bea366e8ca44474fab7414ca516a6cbc21c Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 13:51:14 +0300 Subject: [PATCH 125/131] build js in parallel --- build.js | 77 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/build.js b/build.js index 16856f8186..4ae2d34c77 100644 --- a/build.js +++ b/build.js @@ -39,47 +39,58 @@ exports.build = function build(targets, callback) { exports.buildTargets = function (targets, callback) { var meta = require('./src/meta'); buildStart = buildStart || Date.now(); - var startTime; - var step = function (target, next) { - winston.info('[build] => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); + + var step = function (startTime, target, next) { + winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); next(); }; - // eachSeries because it potentially(tm) runs faster on Windows this way - async.eachSeries(targets, function (target, next) { - switch(target) { - case 'js': + + async.parallel([ + function (next) { + if (targets.indexOf('js') !== -1) { winston.info('[build] Building javascript'); - startTime = Date.now(); + var startTime = Date.now(); async.series([ async.apply(meta.js.minify, 'nodebb.min.js'), async.apply(meta.js.minify, 'acp.min.js') - ], step.bind(this, target, next)); - break; - - case 'clientCSS': - winston.info('[build] Building client-side CSS'); - startTime = Date.now(); - meta.css.minify('stylesheet.css', step.bind(this, target, next)); - break; - - case 'acpCSS': - winston.info('[build] Building admin control panel CSS'); - startTime = Date.now(); - meta.css.minify('admin.css', step.bind(this, target, next)); - break; - - case 'tpl': - winston.info('[build] Building templates'); - startTime = Date.now(); - meta.templates.compile(step.bind(this, target, next)); - break; - - default: - winston.warn('[build] Unknown build target: \'' + target + '\''); + ], step.bind(this, startTime, 'js', next)); + } else { setImmediate(next); - break; + } + }, + function (next) { + async.eachSeries(targets, function (target, next) { + var startTime; + switch(target) { + case 'js': + setImmediate(next); + break; + case 'clientCSS': + winston.info('[build] Building client-side CSS'); + startTime = Date.now(); + meta.css.minify('stylesheet.css', step.bind(this, startTime, target, next)); + break; + + case 'acpCSS': + winston.info('[build] Building admin control panel CSS'); + startTime = Date.now(); + meta.css.minify('admin.css', step.bind(this, startTime, target, next)); + break; + + case 'tpl': + winston.info('[build] Building templates'); + startTime = Date.now(); + meta.templates.compile(step.bind(this, startTime, target, next)); + break; + + default: + winston.warn('[build] Unknown build target: \'' + target + '\''); + setImmediate(next); + break; + } + }, next); } - }, function (err) { + ], function (err) { if (err) { winston.error('[build] Encountered error during build step: ' + err.message); return process.exit(1); From 9ba93d8be93f885a9ba0ba688e9c931ad8b99795 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 17:21:09 +0300 Subject: [PATCH 126/131] group search tests --- src/groups/search.js | 4 --- src/socket.io/groups.js | 2 +- test/groups.js | 56 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/groups/search.js b/src/groups/search.js index 55bf5e5789..4826579f6c 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -66,10 +66,6 @@ module.exports = function (Groups) { Groups.searchMembers = function (data, callback) { function findUids(query, searchBy, callback) { - if (!query) { - return Groups.getMembers(data.groupName, 0, -1, callback); - } - query = query.toLowerCase(); async.waterfall([ diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 64e568f919..858d9bdeff 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -237,7 +237,7 @@ SocketGroups.search = function (socket, data, callback) { return; } - groups.search(data.query, data.options || {}, callback); + groups.search(data.query, data.options, callback); }; SocketGroups.loadMore = function (socket, data, callback) { diff --git a/test/groups.js b/test/groups.js index 35d3c17be6..c2b30b9d94 100644 --- a/test/groups.js +++ b/test/groups.js @@ -93,14 +93,68 @@ describe('Groups', function () { }); describe('.search()', function () { + var socketGroups = require('../src/socket.io/groups'); + + it('should return the groups when search query is empty', function (done) { + socketGroups.search({uid: adminUid}, {query: ''}, function (err, groups) { + assert.ifError(err); + assert.equal(3, groups.length); + done(); + }); + }); + it('should return the "Test" group when searched for', function (done) { - Groups.search('test', {}, function (err, groups) { + socketGroups.search({uid: adminUid}, {query: 'test'}, function (err, groups) { assert.ifError(err); assert.equal(1, groups.length); assert.strictEqual('Test', groups[0].name); done(); }); }); + + it('should return the "Test" group when searched for and sort by member count', function (done) { + Groups.search('test', {filterHidden: true, sort: 'count'}, function (err, groups) { + assert.ifError(err); + assert.equal(1, groups.length); + assert.strictEqual('Test', groups[0].name); + done(); + }); + }); + + it('should return the "Test" group when searched for and sort by creation time', function (done) { + Groups.search('test', {filterHidden: true, sort: 'date'}, function (err, groups) { + assert.ifError(err); + assert.equal(1, groups.length); + assert.strictEqual('Test', groups[0].name); + done(); + }); + }); + + it('should return all users if no query', function (done) { + User.create({ + username: 'newuser', + email: 'newuser@b.com' + }, function (err, uid) { + assert.ifError(err); + Groups.join('Test', uid, function (err) { + assert.ifError(err); + socketGroups.searchMembers({uid: adminUid}, {groupName: 'Test', query: ''}, function (err, data) { + assert.ifError(err); + assert.equal(data.users.length, 2); + done(); + }); + }); + }); + }); + + it('should search group members', function (done) { + socketGroups.searchMembers({uid: adminUid}, {groupName: 'Test', query: 'test'}, function (err, data) { + assert.ifError(err); + assert.strictEqual('testuser', data.users[0].username); + done(); + }); + }); + }); describe('.isMember()', function () { From 5cf80066406a20db687ea2558c9d3e4d628f8457 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 18:05:48 +0300 Subject: [PATCH 127/131] fix style --- src/groups/search.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/groups/search.js b/src/groups/search.js index 4826579f6c..3b6bfab9cd 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -1,10 +1,10 @@ 'use strict'; -var async = require('async'), +var async = require('async'); + +var user = require('../user'); +var db = require('./../database'); - user = require('../user'), - db = require('./../database'), - groups = module.parent.exports; module.exports = function (Groups) { @@ -17,7 +17,7 @@ module.exports = function (Groups) { async.apply(db.getObjectValues, 'groupslug:groupname'), function (groupNames, next) { // Ephemeral groups and the registered-users groups are searchable - groupNames = groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); + groupNames = Groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); groupNames = groupNames.filter(function (name) { return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators' && !Groups.isPrivilegeGroup(name); }); From c3980d0c2ef4baf802d9a36c9d17c50498503d26 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 18:36:43 +0300 Subject: [PATCH 128/131] follow tests --- src/user/follow.js | 39 ++++++++++++++++++--------------------- test/controllers.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/user/follow.js b/src/user/follow.js index 0812c1004d..e6c9624018 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -1,9 +1,9 @@ 'use strict'; -var async = require('async'), - plugins = require('../plugins'), - db = require('../database'); +var async = require('async'); +var plugins = require('../plugins'); +var db = require('../database'); module.exports = function (User) { @@ -73,25 +73,22 @@ module.exports = function (User) { if (!parseInt(uid, 10)) { return callback(null, []); } - - db.getSortedSetRevRange(type + ':' + uid, start, stop, function (err, uids) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getSortedSetRevRange(type + ':' + uid, start, stop, next); + }, + function (uids, next) { + plugins.fireHook('filter:user.' + type, { + uids: uids, + uid: uid, + start: start, + stop: stop + }, next); + }, + function (data, next) { + User.getUsers(data.uids, uid, next); } - - plugins.fireHook('filter:user.' + type, { - uids: uids, - uid: uid, - start: start, - stop: stop - }, function (err, data) { - if (err) { - return callback(err); - } - - User.getUsers(data.uids, uid, callback); - }); - }); + ], callback); } User.isFollowing = function (uid, theirid, callback) { diff --git a/test/controllers.js b/test/controllers.js index 04bbf26c1b..03d7d38e11 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -785,7 +785,47 @@ describe('Controllers', function () { done(); }); }); + }); + describe('account follow page', function () { + var uid; + before(function (done) { + user.create({username: 'follower'}, function (err, _uid) { + assert.ifError(err); + uid = _uid; + user.follow(uid, fooUid, done); + }); + }); + + it('should get followers page', function (done) { + request(nconf.get('url') + '/api/user/foo/followers', {json: true}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body.users[0].username, 'follower'); + done(); + }); + }); + + it('should get following page', function (done) { + request(nconf.get('url') + '/api/user/follower/following', {json: true}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body.users[0].username, 'foo'); + done(); + }); + }); + + it('should return empty after unfollow', function (done ) { + user.unfollow(uid, fooUid, function (err) { + assert.ifError(err); + request(nconf.get('url') + '/api/user/foo/followers', {json: true}, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body.users.length, 0); + done(); + }); + }); + }); }); after(function (done) { From 7f90e31a3876cfa14c371b1a55738ae55f63bd04 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 18:59:24 +0300 Subject: [PATCH 129/131] more socket user tests --- src/socket.io/user.js | 2 +- test/controllers.js | 5 +++-- test/user.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 41c2e9c2c5..69b229b4b1 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -46,7 +46,7 @@ SocketUser.deleteAccount = function (socket, data, callback) { user.deleteAccount(socket.uid, next); }, function (next) { - socket.broadcast.emit('event:user_status_change', {uid: socket.uid, status: 'offline'}); + require('./index').server.sockets.emit('event:user_status_change', {uid: socket.uid, status: 'offline'}); events.log({ type: 'user-delete', diff --git a/test/controllers.js b/test/controllers.js index 03d7d38e11..cd08039557 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -788,12 +788,13 @@ describe('Controllers', function () { }); describe('account follow page', function () { + var socketUser = require('../src/socket.io/user'); var uid; before(function (done) { user.create({username: 'follower'}, function (err, _uid) { assert.ifError(err); uid = _uid; - user.follow(uid, fooUid, done); + socketUser.follow({uid: uid}, {uid: fooUid}, done); }); }); @@ -816,7 +817,7 @@ describe('Controllers', function () { }); it('should return empty after unfollow', function (done ) { - user.unfollow(uid, fooUid, function (err) { + socketUser.unfollow({uid: uid}, {uid: fooUid}, function (err) { assert.ifError(err); request(nconf.get('url') + '/api/user/foo/followers', {json: true}, function (err, res, body) { assert.ifError(err); diff --git a/test/user.js b/test/user.js index 114c4dfdc3..c2f67ef709 100644 --- a/test/user.js +++ b/test/user.js @@ -610,7 +610,55 @@ describe('User', function () { }); }); }); + }); + describe('socket methods', function () { + var socketUser = require('../src/socket.io/user'); + + it('should fail with invalid data', function (done) { + socketUser.exists({uid: testUid}, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should return true if user/group exists', function (done) { + socketUser.exists({uid: testUid}, {username: 'registered-users'}, function (err, exists) { + assert.ifError(err); + assert(exists); + done(); + }); + }); + + it('should return true if user/group exists', function (done) { + socketUser.exists({uid: testUid}, {username: 'John Smith'}, function (err, exists) { + assert.ifError(err); + assert(exists); + done(); + }); + }); + + it('should return false if user/group does not exists', function (done) { + socketUser.exists({uid: testUid}, {username: 'doesnot exist'}, function (err, exists) { + assert.ifError(err); + assert(!exists); + done(); + }); + }); + + it('should delete user', function (done) { + User.create({username: 'tobedeleted'}, function (err, _uid) { + assert.ifError(err); + socketUser.deleteAccount({uid: _uid}, {}, function (err) { + assert.ifError(err); + socketUser.exists({uid: testUid}, {username: 'doesnot exist'}, function (err, exists) { + assert.ifError(err); + assert(!exists); + done(); + }); + }); + }); + }); }); From fb42b83e1ba41df755de365ff9b9ef1b1152155b Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 1 Dec 2016 18:59:50 +0300 Subject: [PATCH 130/131] remove hardcoded value @pichalite --- src/views/admin/settings/post.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 95f39c829a..0a58e3a3cf 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -90,7 +90,7 @@

- +

Dates & times will be shown in a relative manner (e.g. "3 hours ago" / "5 days ago"), and localised into various languages. After a certain point, this text can be switched to display the localised date itself From bcb3903446e9b40be899d9606d26ca490afcc69e Mon Sep 17 00:00:00 2001 From: psychobunny Date: Thu, 1 Dec 2016 16:07:40 -0500 Subject: [PATCH 131/131] priv table headers --- public/less/admin/manage/categories.less | 27 +++++++++++++++++++ .../admin/partials/categories/privileges.tpl | 16 +++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/public/less/admin/manage/categories.less b/public/less/admin/manage/categories.less index 76e68f340a..0f13af4deb 100644 --- a/public/less/admin/manage/categories.less +++ b/public/less/admin/manage/categories.less @@ -88,3 +88,30 @@ div.categories { } } + +.category { + .privilege-table { + tr > th:first-child { + min-width: 150px; + } + + .privilege-table-header { + background: white; + + th { + text-align: center; + border-top: 0; + text-transform: uppercase; + font-size: 9px; + } + + .arrowed:after { + border-bottom: 1px dashed #ccc; + content: ""; + width: 100%; + display: block; + padding-top: 5px; + } + } + } +} \ No newline at end of file diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index 497fa71ac0..28b5f0849f 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -1,4 +1,10 @@ - +
+ + + + + + @@ -34,7 +40,13 @@
Viewing PrivilegesPosting PrivilegesModeration Privileges
User
- +
+ + + + + +
Viewing PrivilegesPosting PrivilegesModeration Privileges
Group