From a11058bce298ddec9409b7650463e74a6f88dc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Oct 2017 10:32:24 -0400 Subject: [PATCH 01/13] closes #6004 --- src/middleware/admin.js | 5 +++-- test/controllers-admin.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/middleware/admin.js b/src/middleware/admin.js index d320e9c850..2d0968d50f 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -5,6 +5,7 @@ var winston = require('winston'); var user = require('../user'); var meta = require('../meta'); var plugins = require('../plugins'); +var jsesc = require('jsesc'); var controllers = { api: require('../controllers/api'), @@ -73,11 +74,11 @@ module.exports = function (middleware) { var templateValues = { config: results.config, - configJSON: JSON.stringify(results.config), + configJSON: jsesc(JSON.stringify(results.config), { isScriptContext: true }), relative_path: results.config.relative_path, adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), user: userData, - userJSON: JSON.stringify(userData).replace(/'/g, "\\'"), + userJSON: jsesc(JSON.stringify(userData), { isScriptContext: true }), plugins: results.custom_header.plugins, authentication: results.custom_header.authentication, scripts: results.scripts, diff --git a/test/controllers-admin.js b/test/controllers-admin.js index e7664ca53f..4a82ceb50c 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -578,4 +578,33 @@ describe('Admin Controllers', function () { }); }); }); + + it('should escape special characters in config', function (done) { + var plugins = require('../src/plugins'); + function onConfigGet(config, callback) { + config.someValue = '"foo"'; + config.otherValue = "'123'"; + config.script = ''; + callback(null, config); + } + plugins.registerHook('somePlugin', { hook: 'filter:config.get', method: onConfigGet }); + request(nconf.get('url') + '/admin', { jar: jar }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert(body.indexOf('"someValue":"\\\\"foo\\\\""') !== -1); + assert(body.indexOf('"otherValue":"\\\'123\\\'"') !== -1); + assert(body.indexOf('"script":"<\\/script>"') !== -1); + request(nconf.get('url'), { jar: jar }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert(body.indexOf('"someValue":"\\\\"foo\\\\""') !== -1); + assert(body.indexOf('"otherValue":"\\\'123\\\'"') !== -1); + assert(body.indexOf('"script":"<\\/script>"') !== -1); + plugins.unregisterHook('somePlugin', 'filter:config.get', onConfigGet); + done(); + }); + }); + }); }); From 98eddc78cbda00fb35134a4f5deeb6cdde420d3a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Oct 2017 17:54:55 -0400 Subject: [PATCH 02/13] escaping message text in parse.raw --- src/messaging.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/messaging.js b/src/messaging.js index 9a53f327ce..932f388fe1 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -78,6 +78,9 @@ Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { return callback(err); } + parsed = S(parsed).stripTags().decodeHTMLEntities().s; + parsed = validator.escape(String(parsed)); + var messageData = { message: message, parsed: parsed, From 2453ce3cb38b85471f4d541ca5e7728b2c56fee7 Mon Sep 17 00:00:00 2001 From: psychobunny Date: Thu, 26 Oct 2017 18:27:54 -0400 Subject: [PATCH 03/13] strip + validate before hook instead --- src/messaging.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/messaging.js b/src/messaging.js index 932f388fe1..d6339f013f 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -73,13 +73,14 @@ function canGet(hook, callerUid, uid, callback) { } Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { + message = S(message).stripTags().decodeHTMLEntities().s; + message = validator.escape(String(message)); + plugins.fireHook('filter:parse.raw', message, function (err, parsed) { if (err) { return callback(err); } - parsed = S(parsed).stripTags().decodeHTMLEntities().s; - parsed = validator.escape(String(parsed)); var messageData = { message: message, From c453fc7275ebbd07d8662f7890ba47c04daeca82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Oct 2017 22:42:16 -0400 Subject: [PATCH 04/13] add widget reset test --- src/widgets/index.js | 43 ++++++++++++++++++++----------------------- test/controllers.js | 15 ++++++++++++++- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/widgets/index.js b/src/widgets/index.js index 30f2791afa..968a5e7a9e 100644 --- a/src/widgets/index.js +++ b/src/widgets/index.js @@ -218,34 +218,31 @@ widgets.reset = function (callback) { }; widgets.resetTemplate = function (template, callback) { - db.getObject('widgets:' + template + '.tpl', function (err, area) { - if (err) { - return callback(); - } - - var toBeDrafted = []; - for (var location in area) { - if (area.hasOwnProperty(location)) { - toBeDrafted = toBeDrafted.concat(JSON.parse(area[location])); - } - } - - db.delete('widgets:' + template + '.tpl'); - db.getObjectField('widgets:global', 'drafts', function (err, draftWidgets) { - if (err) { - return callback(); + var toBeDrafted = []; + async.waterfall([ + function (next) { + db.getObject('widgets:' + template + '.tpl', next); + }, + function (area, next) { + for (var location in area) { + if (area.hasOwnProperty(location)) { + toBeDrafted = toBeDrafted.concat(JSON.parse(area[location])); + } } - + db.delete('widgets:' + template + '.tpl', next); + }, + function (next) { + db.getObjectField('widgets:global', 'drafts', next); + }, + function (draftWidgets, next) { draftWidgets = JSON.parse(draftWidgets).concat(toBeDrafted); - db.setObjectField('widgets:global', 'drafts', JSON.stringify(draftWidgets), callback); - }); - }); + db.setObjectField('widgets:global', 'drafts', JSON.stringify(draftWidgets), next); + }, + ], callback); }; widgets.resetTemplates = function (templates, callback) { - async.eachSeries(templates, function (template, next) { - widgets.resetTemplate(template, next); - }, callback); + async.eachSeries(templates, widgets.resetTemplate, callback); }; module.exports = widgets; diff --git a/test/controllers.js b/test/controllers.js index 04343b6d18..c4c46ac38d 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -702,7 +702,7 @@ describe('Controllers', function () { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body.widgets); - assert.equal(Object.keys(body.widgets), 0); + assert.equal(Object.keys(body.widgets).length, 0); done(); }); }); @@ -717,6 +717,19 @@ describe('Controllers', function () { done(); }); }); + + it('should reset templates', function (done) { + widgets.resetTemplates(['categories', 'category'], function (err) { + assert.ifError(err); + request(nconf.get('url') + '/api/categories', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.widgets); + assert.equal(Object.keys(body.widgets).length, 0); + done(); + }); + }); + }); }); describe('tags', function () { From 4a8114959136271b41d941cf7db45a2b769d17f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Oct 2017 10:29:01 -0400 Subject: [PATCH 05/13] up persona --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0b362a5d7..194c941454 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.1.1", - "nodebb-theme-persona": "6.1.3", + "nodebb-theme-persona": "6.1.4", "nodebb-theme-slick": "1.1.1", "nodebb-theme-vanilla": "7.1.2", "nodebb-widget-essentials": "3.0.7", From 9a3a5192c4d25fecea0d0002f3892c814ea6380a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Oct 2017 17:24:43 -0400 Subject: [PATCH 06/13] add filter:post.shouldQueue --- src/posts/queue.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/posts/queue.js b/src/posts/queue.js index 7e0e90720f..01836abba0 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -8,6 +8,7 @@ var meta = require('../meta'); var topics = require('../topics'); var notifications = require('../notifications'); var privileges = require('../privileges'); +var plugins = require('../plugins'); var socketHelpers = require('../socket.io/helpers'); module.exports = function (Posts) { @@ -18,7 +19,14 @@ module.exports = function (Posts) { }, function (userData, next) { var shouldQueue = parseInt(meta.config.postQueue, 10) === 1 && (!parseInt(uid, 10) || (parseInt(userData.reputation, 10) <= 0 && parseInt(userData.postcount, 10) <= 0)); - next(null, shouldQueue); + plugins.fireHook('filter:post.shouldQueue', { + shouldQueue: shouldQueue, + uid: uid, + data: data, + }, next); + }, + function (result, next) { + next(null, result.shouldQueue); }, ], callback); }; From 4854888fcf1d5f022aa4a4a3dc1390a7bdb9e1f3 Mon Sep 17 00:00:00 2001 From: Dr Luke Angel Date: Fri, 27 Oct 2017 22:16:01 -0700 Subject: [PATCH 07/13] update blacklist.js to strip ports from v4 Blacklist.test in blacklist.js to strip port from ipv4. my site was passing in 24.18.192.75:52506 and nodebb was giving 28/10 04:34:41 [6680] - error: /login Error: ipaddr: the address has neither IPv6 nor IPv4 format So i updated the client Ip to strip the port number if its a v4 IP --- src/meta/blacklist.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 6fa657c5e8..9fe0737b54 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -65,6 +65,9 @@ Blacklist.test = function (clientIp, callback) { // Some handy test addresses // clientIp = '2001:db8:85a3:0:0:8a2e:370:7334'; // IPv6 // clientIp = '127.0.15.1'; // IPv4 + // clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail + clientIp = clientIp.split(":").length == 2 ? clientIp.split(":")[0] : clientIp; + var addr = ipaddr.parse(clientIp); if ( From 999a7abc5d3fe34f849e3a82225533bab2ef9065 Mon Sep 17 00:00:00 2001 From: Dr Luke Angel Date: Fri, 27 Oct 2017 22:21:48 -0700 Subject: [PATCH 08/13] updated equality operator small update to equality operator --- src/meta/blacklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 9fe0737b54..995a9e4488 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -66,7 +66,7 @@ Blacklist.test = function (clientIp, callback) { // clientIp = '2001:db8:85a3:0:0:8a2e:370:7334'; // IPv6 // clientIp = '127.0.15.1'; // IPv4 // clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail - clientIp = clientIp.split(":").length == 2 ? clientIp.split(":")[0] : clientIp; + clientIp = clientIp.split(":").length === 2 ? clientIp.split(":")[0] : clientIp; var addr = ipaddr.parse(clientIp); From 6f029747629731b915f7255287421e5104b2aba0 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Mon, 30 Oct 2017 09:25:27 +0000 Subject: [PATCH 09/13] Latest translations and fallbacks --- public/language/nl/notifications.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index af0c136f51..55e75afe74 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -20,24 +20,24 @@ "my-flags": "Markeringen toegewezen aan mij", "bans": "Bans", "new_message_from": "Nieuw bericht van %1", - "upvoted_your_post_in": "%1 heeft voor een bericht gestemd in %2.", - "upvoted_your_post_in_dual": "%1 en %2 hebben voor een bericht in gestemd in %3.", - "upvoted_your_post_in_multiple": "%1 en %2 andere hebben in gestemd in %3.", + "upvoted_your_post_in": "%1 heeft voor je bericht gestemd in %2.", + "upvoted_your_post_in_dual": "%1 en %2 hebben voor je bericht gestemd in %3.", + "upvoted_your_post_in_multiple": "%1 en %2 anderen hebben voor je bericht gestemd in %3.", "moved_your_post": "%1 heeft je bericht verplaatst naar %2", "moved_your_topic": "%1 heeft %2 verplaatst", "user_flagged_post_in": "%1 rapporteerde een bericht in %2", - "user_flagged_post_in_dual": "%1 en %2 rapporteerde een bericht in %3", - "user_flagged_post_in_multiple": "%1 en %2 andere rapporteede een bericht in %3", + "user_flagged_post_in_dual": "%1 en %2 rapporteerden een bericht in %3", + "user_flagged_post_in_multiple": "%1 en %2 anderen rapporteerden een bericht in %3", "user_flagged_user": "%1 markeerde een gebruikersprofiel (%2)", "user_flagged_user_dual": "%1 en %2 markeerden een gebruikersprofiel (%3)", - "user_flagged_user_multiple": "%1 en %2 anderen markeerde een gebruikersprofiel (%3)", - "user_posted_to": "%1 heeft een reactie geplaatst in %2", + "user_flagged_user_multiple": "%1 en %2 anderen markeerden een gebruikersprofiel (%3)", + "user_posted_to": "%1 heeft een reactie geplaatst in: %2", "user_posted_to_dual": "%1 en %2 hebben een reactie geplaatst in: %3", "user_posted_to_multiple": "%1 en %2 hebben een reactie geplaatst in: %3", "user_posted_topic": "%1 heeft een nieuw onderwerp geplaatst: %2", "user_started_following_you": "%1 volgt jou nu.", "user_started_following_you_dual": "%1 en %2 volgen jou nu.", - "user_started_following_you_multiple": "%1 en %2 andere volgen jou nu.", + "user_started_following_you_multiple": "%1 en %2 anderen volgen jou nu.", "new_register": "%1 heeft een registratie verzoek aangevraagd.", "new_register_multiple": "Er is/zijn %1 registratieverzoek(en) die wacht(en) op goedkeuring.", "flag_assigned_to_you": "Flag %1 is aan u toegewezen", From 9f670fe586a9858826eec12667be161661c065e4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 30 Oct 2017 11:02:19 -0400 Subject: [PATCH 10/13] sanitizing uploaded filename without using slugify, in composer, re comment in: #6011 --- package.json | 2 +- src/file.js | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/package.json b/package.json index 194c941454..8c6fcfce63 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "morgan": "^1.9.0", "mousetrap": "^1.6.1", "nconf": "^0.8.5", - "nodebb-plugin-composer-default": "6.0.1", + "nodebb-plugin-composer-default": "6.0.2", "nodebb-plugin-dbsearch": "2.0.8", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.2.1", diff --git a/src/file.js b/src/file.js index ec86c624b1..eeb46121f3 100644 --- a/src/file.js +++ b/src/file.js @@ -8,20 +8,9 @@ var jimp = require('jimp'); var mkdirp = require('mkdirp'); var mime = require('mime'); -var utils = require('./utils'); - var file = module.exports; file.saveFileToLocal = function (filename, folder, tempPath, callback) { - /* - * remarkable doesn't allow spaces in hyperlinks, once that's fixed, remove this. - */ - filename = filename.split('.'); - filename.forEach(function (name, idx) { - filename[idx] = utils.slugify(name); - }); - filename = filename.join('.'); - var uploadPath = path.join(nconf.get('upload_path'), folder, filename); winston.verbose('Saving file ' + filename + ' to : ' + uploadPath); From cb80c7729b5afaf3c110d5926cb07b35ab4caffe Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 30 Oct 2017 11:41:44 -0400 Subject: [PATCH 11/13] linting --- src/meta/blacklist.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 995a9e4488..c8d5b6a9fb 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -65,9 +65,9 @@ Blacklist.test = function (clientIp, callback) { // Some handy test addresses // clientIp = '2001:db8:85a3:0:0:8a2e:370:7334'; // IPv6 // clientIp = '127.0.15.1'; // IPv4 - // clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail - clientIp = clientIp.split(":").length === 2 ? clientIp.split(":")[0] : clientIp; - + // clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail + clientIp = clientIp.split(':').length === 2 ? clientIp.split(':')[0] : clientIp; + var addr = ipaddr.parse(clientIp); if ( From 103d9a91a927f177a4910f48e29e4be6cb6d4e32 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 30 Oct 2017 14:45:41 -0400 Subject: [PATCH 12/13] Revert "sanitizing uploaded filename without using slugify, in composer, re comment in: #6011" This reverts commit 9f670fe586a9858826eec12667be161661c065e4. --- package.json | 2 +- src/file.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c6fcfce63..194c941454 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "morgan": "^1.9.0", "mousetrap": "^1.6.1", "nconf": "^0.8.5", - "nodebb-plugin-composer-default": "6.0.2", + "nodebb-plugin-composer-default": "6.0.1", "nodebb-plugin-dbsearch": "2.0.8", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.2.1", diff --git a/src/file.js b/src/file.js index eeb46121f3..ec86c624b1 100644 --- a/src/file.js +++ b/src/file.js @@ -8,9 +8,20 @@ var jimp = require('jimp'); var mkdirp = require('mkdirp'); var mime = require('mime'); +var utils = require('./utils'); + var file = module.exports; file.saveFileToLocal = function (filename, folder, tempPath, callback) { + /* + * remarkable doesn't allow spaces in hyperlinks, once that's fixed, remove this. + */ + filename = filename.split('.'); + filename.forEach(function (name, idx) { + filename[idx] = utils.slugify(name); + }); + filename = filename.join('.'); + var uploadPath = path.join(nconf.get('upload_path'), folder, filename); winston.verbose('Saving file ' + filename + ' to : ' + uploadPath); From a500e0019c3d823f0534f385aa02fbac647287d3 Mon Sep 17 00:00:00 2001 From: Baris Usakli Date: Mon, 30 Oct 2017 15:26:12 -0400 Subject: [PATCH 13/13] closes #6005 --- public/language/en-GB/unread.json | 3 +- public/src/client/recent.js | 56 +++++++++++++++++++++++++++++-- public/src/client/unread.js | 7 ++-- src/categories/recentreplies.js | 5 +-- src/controllers/helpers.js | 29 +++++++++++++--- src/controllers/recent.js | 6 ++-- src/controllers/unread.js | 5 ++- src/topics/recent.js | 25 +++++--------- src/topics/unread.js | 7 +++- test/topics.js | 2 +- 10 files changed, 107 insertions(+), 38 deletions(-) diff --git a/public/language/en-GB/unread.json b/public/language/en-GB/unread.json index 74634f9080..625852d998 100644 --- a/public/language/en-GB/unread.json +++ b/public/language/en-GB/unread.json @@ -10,5 +10,6 @@ "all-topics": "All Topics", "new-topics": "New Topics", "watched-topics": "Watched Topics", - "unreplied-topics": "Unreplied Topics" + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" } \ No newline at end of file diff --git a/public/src/client/recent.js b/public/src/client/recent.js index 71b91beced..398e8d11fb 100644 --- a/public/src/client/recent.js +++ b/public/src/client/recent.js @@ -18,6 +18,8 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function (infinit Recent.watchForNewPosts(); + Recent.handleCategorySelection(); + $('#new-topics-alert').on('click', function () { $(this).addClass('hide'); }); @@ -38,7 +40,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function (infinit }; function onNewTopic(data) { - if (ajaxify.data.selectedCategory && parseInt(ajaxify.data.selectedCategory.cid, 10) !== parseInt(data.cid, 10)) { + if (ajaxify.data.selectedCids && ajaxify.data.selectedCids.indexOf(parseInt(data.cid, 10)) === -1) { return; } @@ -64,7 +66,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function (infinit return; } - if (ajaxify.data.selectedCategory && parseInt(ajaxify.data.selectedCategory.cid, 10) !== parseInt(post.topic.cid, 10)) { + if (ajaxify.data.selectedCids && ajaxify.data.selectedCids.indexOf(parseInt(post.topic.cid, 10)) === -1) { return; } @@ -87,6 +89,56 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function (infinit showAlert(); } + Recent.handleCategorySelection = function () { + function getSelectedCids() { + var cids = []; + $('[component="category/list"] [data-cid]').each(function (index, el) { + if ($(el).find('i.fa-check').length) { + cids.push(parseInt($(el).attr('data-cid'), 10)); + } + }); + cids.sort(function (a, b) { + return a - b; + }); + return cids; + } + + $('[component="category/dropdown"]').on('hidden.bs.dropdown', function () { + var cids = getSelectedCids(); + var changed = ajaxify.data.selectedCids.length !== cids.length; + ajaxify.data.selectedCids.forEach(function (cid, index) { + if (cid !== cids[index]) { + changed = true; + } + }); + + if (changed) { + var url = ajaxify.data.selectedFilter.url; + if (cids.length) { + url += '?' + decodeURIComponent($.param({ cid: cids })); + } + ajaxify.go(url); + } + }); + + $('[component="category/list"]').on('click', '[data-cid]', function (ev) { + function selectChildren(parentCid, flag) { + $('[component="category/list"] [data-parent-cid="' + parentCid + '"] [component="category/select/icon"]').toggleClass('fa-check', flag); + $('[component="category/list"] [data-parent-cid="' + parentCid + '"]').each(function (index, el) { + selectChildren($(el).attr('data-cid'), flag); + }); + } + var categoryEl = $(this); + var cid = $(this).attr('data-cid'); + if (ev.ctrlKey) { + selectChildren(cid, !categoryEl.find('[component="category/select/icon"]').hasClass('fa-check')); + } + categoryEl.find('[component="category/select/icon"]').toggleClass('fa-check'); + $('[component="category/list"] li').first().find('i').toggleClass('fa-check', !getSelectedCids().length); + return false; + }); + }; + Recent.removeListeners = function () { socket.removeListener('event:new_topic', onNewTopic); socket.removeListener('event:new_post', onNewPost); diff --git a/public/src/client/unread.js b/public/src/client/unread.js index 01ef9cff52..9b6ddea7a0 100644 --- a/public/src/client/unread.js +++ b/public/src/client/unread.js @@ -19,6 +19,8 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' recent.watchForNewPosts(); + recent.handleCategorySelection(); + $(window).trigger('action:topics.loaded', { topics: ajaxify.data.topics }); $('#markSelectedRead').on('click', function () { @@ -88,12 +90,11 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' if (direction < 0 || !$('[component="category"]').length) { return; } - var params = utils.params(); - var cid = params.cid; + infinitescroll.loadMore('topics.loadMoreUnreadTopics', { after: $('[component="category"]').attr('data-nextstart'), count: config.topicsPerPage, - cid: cid, + cid: utils.params().cid, filter: ajaxify.data.selectedFilter.filter, }, function (data, done) { if (data.topics && data.topics.length) { diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 193300dc0e..13d3be2914 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -91,11 +91,8 @@ module.exports = function (Categories) { db.getSortedSetsMembers(keys, next); }, function (results, next) { - var tids = _.flatten(results); + var tids = _.uniq(_.flatten(results).filter(Boolean)); - tids = tids.filter(function (tid, index, array) { - return !!tid && array.indexOf(tid) === index; - }); privileges.topics.filterTids('read', tids, uid, next); }, function (tids, next) { diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 70de194323..260069ce4e 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -179,6 +179,9 @@ helpers.buildTitle = function (pageTitle) { }; helpers.getWatchedCategories = function (uid, selectedCid, callback) { + if (selectedCid && !Array.isArray(selectedCid)) { + selectedCid = [selectedCid]; + } async.waterfall([ function (next) { user.getWatchedCategories(uid, next); @@ -193,14 +196,30 @@ helpers.getWatchedCategories = function (uid, selectedCid, callback) { categoryData = categoryData.filter(function (category) { return category && !category.link; }); - - var selectedCategory; + var selectedCategory = []; + var selectedCids = []; categoryData.forEach(function (category) { - category.selected = parseInt(category.cid, 10) === parseInt(selectedCid, 10); + category.selected = selectedCid ? selectedCid.indexOf(String(category.cid)) !== -1 : false; if (category.selected) { - selectedCategory = category; + selectedCategory.push(category); + selectedCids.push(parseInt(category.cid, 10)); } }); + selectedCids.sort(function (a, b) { + return a - b; + }); + + if (selectedCategory.length > 1) { + selectedCategory = { + icon: 'fa-plus', + name: '[[unread:multiple-categories-selected]]', + bgColor: '#ddd', + }; + } else if (selectedCategory.length === 1) { + selectedCategory = selectedCategory[0]; + } else { + selectedCategory = undefined; + } var categoriesData = []; var tree = categories.getTree(categoryData, 0); @@ -209,7 +228,7 @@ helpers.getWatchedCategories = function (uid, selectedCid, callback) { recursive(category, categoriesData, ''); }); - next(null, { categories: categoriesData, selectedCategory: selectedCategory }); + next(null, { categories: categoriesData, selectedCategory: selectedCategory, selectedCids: selectedCids }); }, ], callback); }; diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 3bb337d2f0..d6947d53a7 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -3,7 +3,7 @@ var async = require('async'); var nconf = require('nconf'); -var validator = require('validator'); +var querystring = require('querystring'); var user = require('../user'); var topics = require('../topics'); @@ -53,6 +53,7 @@ recentController.get = function (req, res, next) { function (data) { data.categories = categoryData.categories; data.selectedCategory = categoryData.selectedCategory; + data.selectedCids = categoryData.selectedCids; data.nextStart = stop + 1; data.set = 'topics:recent'; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; @@ -74,7 +75,8 @@ recentController.get = function (req, res, next) { data.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[recent:title]]' }]); } - data.querystring = cid ? ('?cid=' + validator.escape(String(cid))) : ''; + data.querystring = cid ? '?' + querystring.stringify({ cid: cid }) : ''; + res.render('recent', data); }, ], next); diff --git a/src/controllers/unread.js b/src/controllers/unread.js index a3ee68192d..7891012e19 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -3,7 +3,6 @@ var async = require('async'); var querystring = require('querystring'); -var validator = require('validator'); var pagination = require('../pagination'); var user = require('../user'); @@ -64,6 +63,7 @@ unreadController.get = function (req, res, next) { data.categories = results.watchedCategories.categories; data.selectedCategory = results.watchedCategories.selectedCategory; + data.selectedCids = results.watchedCategories.selectedCids; if (req.path.startsWith('/api/unread') || req.path.startsWith('/unread')) { data.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[unread:title]]' }]); @@ -76,8 +76,7 @@ unreadController.get = function (req, res, next) { return filter && filter.selected; }); - data.querystring = cid ? ('?cid=' + validator.escape(String(cid))) : ''; - + data.querystring = cid ? '?' + querystring.stringify({ cid: cid }) : ''; res.render('unread', data); }, ], next); diff --git a/src/topics/recent.js b/src/topics/recent.js index c5dce9209f..a30c213fdd 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -3,11 +3,11 @@ 'use strict'; var async = require('async'); + var db = require('../database'); var plugins = require('../plugins'); var privileges = require('../privileges'); var user = require('../user'); -var categories = require('../categories'); var meta = require('../meta'); module.exports = function (Topics) { @@ -23,22 +23,15 @@ module.exports = function (Topics) { nextStart: 0, topics: [], }; - + if (cid && !Array.isArray(cid)) { + cid = [cid]; + } async.waterfall([ function (next) { - if (cid) { - categories.getTopicIds({ - cid: cid, - start: 0, - stop: 199, - sort: 'newest_to_oldest', - }, next); - } else { - db.getSortedSetRevRange('topics:recent', 0, 199, next); - } + db.getSortedSetRevRange('topics:recent', 0, 199, next); }, function (tids, next) { - filterTids(tids, uid, filter, next); + filterTids(tids, uid, filter, cid, next); }, function (tids, next) { recentTopics.topicCount = tids.length; @@ -53,8 +46,7 @@ module.exports = function (Topics) { ], callback); }; - - function filterTids(tids, uid, filter, callback) { + function filterTids(tids, uid, filter, cid, callback) { async.waterfall([ function (next) { if (filter === 'watched') { @@ -84,9 +76,10 @@ module.exports = function (Topics) { }, next); }, function (results, next) { + cid = cid && cid.map(String); tids = results.topicData.filter(function (topic) { if (topic && topic.cid) { - return results.ignoredCids.indexOf(topic.cid.toString()) === -1; + return results.ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || (cid.length && cid.indexOf(topic.cid.toString()) !== -1)); } return false; }).map(function (topic) { diff --git a/src/topics/unread.js b/src/topics/unread.js index 6abad117ef..7c068cfc40 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -75,6 +75,10 @@ module.exports = function (Topics) { var cutoff = params.cutoff || Topics.unreadCutoff(); + if (params.cid && !Array.isArray(params.cid)) { + params.cid = [params.cid]; + } + async.waterfall([ function (next) { async.parallel({ @@ -181,10 +185,11 @@ module.exports = function (Topics) { }, function (results, next) { var topics = results.topics; + cid = cid && cid.map(String); tids = topics.filter(function (topic, index) { return topic && topic.cid && (!!results.isTopicsFollowed[index] || results.ignoredCids.indexOf(topic.cid.toString()) === -1) && - (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10)); + (!cid || (cid.length && cid.indexOf(String(topic.cid)) !== -1)); }).map(function (topic) { return topic.tid; }); diff --git a/test/topics.js b/test/topics.js index f8c1ee5d16..e6c341cb9e 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1133,7 +1133,7 @@ describe('Topic\'s', function () { }); }); - it('should mark all read', function (done) { + it('should mark category topics read', function (done) { socketTopics.markUnread({ uid: adminUid }, tid, function (err) { assert.ifError(err); socketTopics.markCategoryTopicsRead({ uid: adminUid }, topic.categoryId, function (err) {