From aa577f4adca2443dd1dc6165b4296289cdbba64c Mon Sep 17 00:00:00 2001
From: barisusakli <barisusakli@gmail.com>
Date: Tue, 19 May 2015 23:04:28 -0400
Subject: [PATCH] part 1

no upgrade script yet
---
 public/src/client/users.js   |  75 ++++++++++++-----------
 src/controllers/users.js     |  22 +++----
 src/database/mongo/sorted.js |  24 ++++++++
 src/database/redis/sorted.js |  10 ++++
 src/groups.js                |  21 ++-----
 src/socket.io/admin/user.js  |   2 +-
 src/socket.io/user.js        |  56 +++++++++++++++++-
 src/user/create.js           |   9 ++-
 src/user/delete.js           |   8 ++-
 src/user/profile.js          |  17 +++++-
 src/user/search.js           | 111 ++++++++---------------------------
 11 files changed, 203 insertions(+), 152 deletions(-)

diff --git a/public/src/client/users.js b/public/src/client/users.js
index eaa9467a00..efd589d42d 100644
--- a/public/src/client/users.js
+++ b/public/src/client/users.js
@@ -118,25 +118,18 @@ define('forum/users', ['translator'], function(translator) {
 		var notify = $('#user-notfound-notify');
 		page = page || 1;
 
+		if (!username) {
+			return loadPage(page);
+		}
+
 		notify.html('<i class="fa fa-spinner fa-spin"></i>');
-		var filters = [];
-		$('.user-filter').each(function() {
-			var $this = $(this);
-			if($this.is(':checked')) {
-				filters.push({
-					field:$this.attr('data-filter-field'),
-					type: $this.attr('data-filter-type'),
-					value: $this.attr('data-filter-value')
-				});
-			}
-		});
 
 		socket.emit('user.search', {
 			query: username,
 			page: page,
-			searchBy: ['username', 'fullname'],
+			searchBy: 'username',
 			sortBy: $('.search select').val(),
-			filterBy: filters
+			onlineOnly: $('.search .online-only').is(':checked')
 		}, function(err, data) {
 			if (err) {
 				reset();
@@ -147,33 +140,49 @@ define('forum/users', ['translator'], function(translator) {
 				return reset();
 			}
 
-			templates.parse('partials/paginator', {pagination: data.pagination}, function(html) {
-				$('.pagination-container').replaceWith(html);
-			});
+			renderSearchResults(data);
+		});
+	}
+
+
+	function loadPage(page) {
+		socket.emit('user.loadPage', {page: page, sortBy: $('.search select').val(), onlineOnly: $('.search .online-only').is(':checked')}, function(err, data) {
+			if (err) {
+				return app.alertError(err.message);
+			}
+
+			renderSearchResults(data);
+		});
+	}
 
-			templates.parse('users', 'users', data, function(html) {
-				translator.translate(html, function(translated) {
-					$('#users-container').html(translated);
-
-					if (!data.users.length) {
-						translator.translate('[[error:no-user]]', function(translated) {
-							notify.html(translated);
-							notify.parent().removeClass('btn-success label-success').addClass('btn-warning label-warning');
-						});
-					} else {
-						translator.translate('[[users:users-found-search-took, ' + data.matchCount + ', ' + data.timing + ']]', function(translated) {
-							notify.html(translated);
-							notify.parent().removeClass('btn-warning label-warning').addClass('btn-success label-success');
-						});
-					}
-				});
+	function renderSearchResults(data) {
+		var notify = $('#user-notfound-notify');
+		templates.parse('partials/paginator', {pagination: data.pagination}, function(html) {
+			$('.pagination-container').replaceWith(html);
+		});
+
+		templates.parse('users', 'users', data, function(html) {
+			translator.translate(html, function(translated) {
+				$('#users-container').html(translated);
+
+				if (!data.users.length) {
+					translator.translate('[[error:no-user]]', function(translated) {
+						notify.html(translated);
+						notify.parent().removeClass('btn-success label-success').addClass('btn-warning label-warning');
+					});
+				} else {
+					translator.translate('[[users:users-found-search-took, ' + data.matchCount + ', ' + data.timing + ']]', function(translated) {
+						notify.html(translated);
+						notify.parent().removeClass('btn-warning label-warning').addClass('btn-success label-success');
+					});
+				}
 			});
 		});
 	}
 
 	function onUserStatusChange(data) {
 		var section = getActiveSection();
-		
+
 		if ((section.startsWith('online') || section.startsWith('users'))) {
 			updateUser(data);
 		}
diff --git a/src/controllers/users.js b/src/controllers/users.js
index 29dd552b87..2c837b2861 100644
--- a/src/controllers/users.js
+++ b/src/controllers/users.js
@@ -48,26 +48,26 @@ usersController.getOnlineUsers = function(req, res, next) {
 };
 
 usersController.getUsersSortedByPosts = function(req, res, next) {
-	usersController.getUsers('users:postcount', 50, req, res, next);
+	usersController.getUsers('users:postcount', 0, 49, req, res, next);
 };
 
 usersController.getUsersSortedByReputation = function(req, res, next) {
-	usersController.getUsers('users:reputation', 50, req, res, next);
+	usersController.getUsers('users:reputation', 0, 49, req, res, next);
 };
 
 usersController.getUsersSortedByJoinDate = function(req, res, next) {
-	usersController.getUsers('users:joindate', 50, req, res, next);
+	usersController.getUsers('users:joindate', 0, 49, req, res, next);
 };
 
-usersController.getUsers = function(set, count, req, res, next) {
-	getUsersAndCount(set, req.uid, count, function(err, data) {
+usersController.getUsers = function(set, start, stop, req, res, next) {
+	usersController.getUsersAndCount(set, req.uid, start, stop, function(err, data) {
 		if (err) {
 			return next(err);
 		}
 		var pageCount = Math.ceil(data.count / (parseInt(meta.config.userSearchResultsPerPage, 10) || 20));
 		var userData = {
 			search_display: 'hidden',
-			loadmore_display: data.count > count ? 'block' : 'hide',
+			loadmore_display: data.count > (stop - start + 1) ? 'block' : 'hide',
 			users: data.users,
 			pagination: pagination.create(1, pageCount)
 		};
@@ -76,13 +76,13 @@ usersController.getUsers = function(set, count, req, res, next) {
 	});
 };
 
-function getUsersAndCount(set, uid, count, callback) {
+usersController.getUsersAndCount = function(set, uid, start, stop, callback) {
 	async.parallel({
 		users: function(next) {
-			user.getUsersFromSet(set, uid, 0, count - 1, next);
+			user.getUsersFromSet(set, uid, start, stop, next);
 		},
 		count: function(next) {
-			db.getObjectField('global', 'userCount', next);
+			db.sortedSetCard(set, next);
 		}
 	}, function(err, results) {
 		if (err) {
@@ -94,7 +94,7 @@ function getUsersAndCount(set, uid, count, callback) {
 
 		callback(null, results);
 	});
-}
+};
 
 usersController.getUsersForSearch = function(req, res, next) {
 	if (!req.uid) {
@@ -102,7 +102,7 @@ usersController.getUsersForSearch = function(req, res, next) {
 	}
 	var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
 
-	getUsersAndCount('users:joindate', req.uid, resultsPerPage, function(err, data) {
+	usersController.getUsersAndCount('users:joindate', req.uid, 0, resultsPerPage - 1, function(err, data) {
 		if (err) {
 			return next(err);
 		}
diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js
index 1d8f992ab9..fddda44388 100644
--- a/src/database/mongo/sorted.js
+++ b/src/database/mongo/sorted.js
@@ -509,4 +509,28 @@ module.exports = function(db, module) {
 			callback(err, result && result.value ? result.value.score : null);
 		});
 	};
+
+	module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) {
+		var query = {_key: key};
+		if (min !== '-') {
+			query.value = {$gte: min};
+		}
+		if (max !== '+') {
+			query.value = query.value || {};
+			query.value.$lte = max;
+		}
+		db.collection('objects').find(query, {_id: 0, value: 1})
+			.sort({value: 1})
+			.skip(start)
+			.limit(count === -1 ? 0 : count)
+			.toArray(function(err, data) {
+				if (err) {
+					return callback(err);
+				}
+				data = data.map(function(item) {
+					return item && item.value;
+				});
+				callback(err, data);
+		});
+	};
 };
\ No newline at end of file
diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js
index 195d9feec8..8d9b35896d 100644
--- a/src/database/redis/sorted.js
+++ b/src/database/redis/sorted.js
@@ -246,4 +246,14 @@ module.exports = function(redisClient, module) {
 	module.sortedSetIncrBy = function(key, increment, value, callback) {
 		redisClient.zincrby(key, increment, value, callback);
 	};
+
+	module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) {
+		if (min !== '-') {
+			min = '[' + min;
+		}
+		if (max !== '+') {
+			max = '(' + max;
+		}
+		redisClient.zrangebylex([key, min, max, 'LIMIT', start, count], callback);
+	};
 };
\ No newline at end of file
diff --git a/src/groups.js b/src/groups.js
index b46b23aca5..c6db87f3c3 100644
--- a/src/groups.js
+++ b/src/groups.js
@@ -1144,7 +1144,7 @@ var async = require('async'),
 
 	Groups.searchMembers = function(data, callback) {
 
-		function findUids(query, searchBy, startsWith, callback) {
+		function findUids(query, searchBy, callback) {
 			if (!query) {
 				return Groups.getMembers(data.groupName, 0, -1, callback);
 			}
@@ -1154,25 +1154,16 @@ var async = require('async'),
 					Groups.getMembers(data.groupName, 0, -1, next);
 				},
 				function(members, next) {
-					user.getMultipleUserFields(members, ['uid'].concat(searchBy), next);
+					user.getMultipleUserFields(members, ['uid'].concat([searchBy]), next);
 				},
 				function(users, next) {
 					var uids = [];
-
-					for(var k=0; k<searchBy.length; ++k) {
-						for(var i=0; i<users.length; ++i) {
-							var field = users[i][searchBy[k]];
-							if ((startsWith && field.toLowerCase().startsWith(query)) || (!startsWith && field.toLowerCase().indexOf(query) !== -1)) {
-								uids.push(users[i].uid);
-							}
+					for(var i=0; i<users.length; ++i) {
+						var field = users[i][searchBy[k]];
+						if (field.toLowerCase().startsWith(query)) {
+							uids.push(users[i].uid);
 						}
 					}
-					if (searchBy.length > 1) {
-						uids = uids.filter(function(uid, index, array) {
-							return array.indexOf(uid) === index;
-						});
-					}
-
 					next(null, uids);
 				}
 			], callback);
diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js
index 454a51741a..60494ca6e1 100644
--- a/src/socket.io/admin/user.js
+++ b/src/socket.io/admin/user.js
@@ -205,7 +205,7 @@ User.deleteUsers = function(socket, uids, callback) {
 };
 
 User.search = function(socket, data, callback) {
-	user.search({query: data.query, searchBy: data.searchBy, startsWith: false, uid: socket.uid}, function(err, searchData) {
+	user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function(err, searchData) {
 		if (err) {
 			return callback(err);
 		}
diff --git a/src/socket.io/user.js b/src/socket.io/user.js
index 65af901b08..8a954faeaf 100644
--- a/src/socket.io/user.js
+++ b/src/socket.io/user.js
@@ -77,7 +77,7 @@ SocketUser.search = function(socket, data, callback) {
 		page: data.page,
 		searchBy: data.searchBy,
 		sortBy: data.sortBy,
-		filterBy: data.filterBy,
+		onlineOnly: data.onlineOnly,
 		uid: socket.uid
 	}, callback);
 };
@@ -431,6 +431,60 @@ SocketUser.loadMore = function(socket, data, callback) {
 	});
 };
 
+SocketUser.loadPage = function(socket, data, callback) {
+	function done(err, result) {
+		if (err) {
+			return callback(err);
+		}
+		var pageCount = Math.ceil(result.count / resultsPerPage);
+		var userData = {
+			users: result.users,
+			pagination: pagination.create(data.page, pageCount)
+		};
+
+		callback(null, userData);
+	}
+
+	var controllers = require('../controllers/users');
+	var pagination = require('../pagination');
+	var set = '';
+	data.sortBy = data.sortBy || 'joindate';
+
+	var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
+	var start = Math.max(0, data.page - 1) * resultsPerPage;
+	var stop = start + resultsPerPage - 1;
+	if (data.onlineOnly) {
+		async.parallel({
+			users: function(next) {
+				user.getUsersFromSet('users:online', socket.uid, 0, 49, next);
+			},
+			count: function(next) {
+				var now = Date.now();
+				db.sortedSetCount('users:online', now - 300000, now, next);
+			}
+		}, done);
+	} else if (data.sortBy === 'username') {
+		async.parallel({
+			count: function(next) {
+				db.sortedSetCard('username:sorted', next);
+			},
+			users: function(next) {
+				db.getSortedSetRangeByLex('username:sorted', '-', '+', start, stop - start + 1, function(err, result) {
+					if (err) {
+						return next(err);
+					}
+					var uids = result.map(function(user) {
+						return user && user.split(':')[1];
+					});
+					user.getUsers(uids, socket.uid, next);
+				});
+			}
+		}, done);
+	} else {
+		controllers.getUsersAndCount('users:joindate', socket.uid, start, stop, done);
+	}
+};
+
 SocketUser.setStatus = function(socket, status, callback) {
 	if (!socket.uid) {
 		return callback(new Error('[[error:invalid-uid]]'));
diff --git a/src/user/create.js b/src/user/create.js
index 2580a738f1..9ce768b9ef 100644
--- a/src/user/create.js
+++ b/src/user/create.js
@@ -93,6 +93,9 @@ module.exports = function(User) {
 							function(next) {
 								db.sortedSetAdd('username:uid', userData.uid, userData.username, next);
 							},
+							function(next) {
+								db.sortedSetAdd('username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid, next);
+							},
 							function(next) {
 								db.sortedSetAdd('userslug:uid', userData.uid, userData.userslug, next);
 							},
@@ -107,7 +110,11 @@ module.exports = function(User) {
 							},
 							function(next) {
 								if (userData.email) {
-									db.sortedSetAdd('email:uid', userData.uid, userData.email.toLowerCase(), next);
+									async.parallel([
+										async.apply(db.sortedSetAdd, 'email:uid', userData.uid, userData.email.toLowerCase()),
+										async.apply(db.sortedSetAdd, 'email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid)
+									], next);
+
 									if (parseInt(userData.uid, 10) !== 1 && parseInt(meta.config.requireEmailConfirmation, 10) === 1) {
 										User.email.sendValidationEmail(userData.uid, userData.email);
 									}
diff --git a/src/user/delete.js b/src/user/delete.js
index 2d06caf745..2558399134 100644
--- a/src/user/delete.js
+++ b/src/user/delete.js
@@ -51,6 +51,9 @@ module.exports = function(User) {
 				function(next) {
 					db.sortedSetRemove('username:uid', userData.username, next);
 				},
+				function(next) {
+					db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid, next);
+				},
 				function(next) {
 					db.sortedSetRemove('userslug:uid', userData.userslug, next);
 				},
@@ -59,7 +62,10 @@ module.exports = function(User) {
 				},
 				function(next) {
 					if (userData.email) {
-						db.sortedSetRemove('email:uid', userData.email.toLowerCase(), next);
+						async.parallel([
+							async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()),
+							async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid)
+						], next);
 					} else {
 						next();
 					}
diff --git a/src/user/profile.js b/src/user/profile.js
index d10aba66db..db0ce9029b 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -164,8 +164,10 @@ module.exports = function(User) {
 			if (userData.email === newEmail) {
 				return callback();
 			}
-
-			db.sortedSetRemove('email:uid', userData.email.toLowerCase(), function(err) {
+			async.series([
+				async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()),
+				async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid)
+			], function(err) {
 				if (err) {
 					return callback(err);
 				}
@@ -178,6 +180,9 @@ module.exports = function(User) {
 					function(next) {
 						db.sortedSetAdd('email:uid', uid, newEmail.toLowerCase(), next);
 					},
+					function(next) {
+						db.sortedSetAdd('email:sorted',  0, newEmail.toLowerCase() + ':' + uid, next);
+					},
 					function(next) {
 						User.setUserField(uid, 'email', newEmail, next);
 					},
@@ -216,7 +221,13 @@ module.exports = function(User) {
 				function(next) {
 					var newUserslug = utils.slugify(newUsername);
 					updateUidMapping('userslug', uid, newUserslug, userData.userslug, next);
-				}
+				},
+				function(next) {
+					async.series([
+						async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid),
+						async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid)
+					], next);
+				},
 			], callback);
 		});
 	}
diff --git a/src/user/search.js b/src/user/search.js
index ce1ad0c6b2..43697791be 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -11,8 +11,7 @@ module.exports = function(User) {
 
 	User.search = function(data, callback) {
 		var query = data.query || '';
-		var searchBy = data.searchBy || ['username'];
-		var startsWith = data.hasOwnProperty('startsWith') ? data.startsWith : true;
+		var searchBy = data.searchBy || 'username';
 		var page = data.page || 1;
 		var uid = data.uid || 0;
 		var paginate = data.hasOwnProperty('paginate') ? data.paginate : true;
@@ -27,14 +26,13 @@ module.exports = function(User) {
 		async.waterfall([
 			function(next) {
 				if (data.findUids) {
-					data.findUids(query, searchBy, startsWith, next);
+					data.findUids(query, searchBy, next);
 				} else {
-					findUids(query, searchBy, startsWith, next);
+					findUids(query, searchBy, next);
 				}
 			},
 			function(uids, next) {
-				var filterBy = Array.isArray(data.filterBy) ? data.filterBy : [];
-				filterAndSortUids(uids, filterBy, data.sortBy, next);
+				filterAndSortUids(uids, data, next);
 			},
 			function(uids, next) {
 				plugins.fireHook('filter:users.search', {uids: uids, uid: uid}, next);
@@ -75,70 +73,39 @@ module.exports = function(User) {
 		};
 	};
 
-	function findUids(query, searchBy, startsWith, callback) {
+	function findUids(query, searchBy, callback) {
 		if (!query) {
-			return db.getSortedSetRevRange('users:joindate', 0, -1, callback);
+			return callback(null, []);
 		}
+		var min = query;
+		var max = query.substr(0, query.length - 1) + String.fromCharCode(query.charCodeAt(query.length - 1) + 1);
 
-		var keys = searchBy.map(function(searchBy) {
-			return searchBy + ':uid';
-		});
-
-		async.map(keys, function(key, next) {
-			db.getSortedSetRangeWithScores(key, 0, -1, next);
-		}, function(err, hashes) {
-			if (err || !hashes) {
-				return callback(err, []);
-			}
-
-			hashes = hashes.filter(Boolean);
-
-			query = query.toLowerCase();
-
-			var uids = [];
-			var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
-			var hardCap = resultsPerPage * 10;
-
-			for (var i=0; i<hashes.length; ++i) {
-				for (var k=0; k<hashes[i].length; ++k) {
-					var field = hashes[i][k].value;
-					if ((startsWith && field.toLowerCase().startsWith(query)) || (!startsWith && field.toLowerCase().indexOf(query) !== -1)) {
-						uids.push(hashes[i][k].score);
-						if (uids.length >= hardCap) {
-							break;
-						}
-					}
-				}
-				if (uids.length >= hardCap) {
-					break;
-				}
-			}
+		var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
+		var hardCap = resultsPerPage * 10;
 
-			if (hashes.length > 1) {
-				uids = uids.filter(function(uid, index, array) {
-					return array.indexOf(uid) === index;
-				});
+		db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function(err, data) {
+			if (err) {
+				return callback(err);
 			}
 
+			var uids = data.map(function(data) {
+				return data.split(':')[1];
+			});
 			callback(null, uids);
 		});
 	}
 
-	function filterAndSortUids(uids, filterBy, sortBy, callback) {
-		sortBy = sortBy || 'joindate';
+	function filterAndSortUids(uids, data, callback) {
+		var sortBy = data.sortBy || 'joindate';
 
-		var fields = filterBy.map(function(filter) {
-			return filter.field;
-		}).concat(['uid', sortBy]).filter(function(field, index, array) {
-			return array.indexOf(field) === index;
-		});
+		var fields = ['uid', 'status', sortBy];
 
 		async.parallel({
 			userData: function(next) {
 				User.getMultipleUserFields(uids, fields, next);
 			},
 			isOnline: function(next) {
-				if (fields.indexOf('status') !== -1) {
+				if (data.onlineOnly) {
 					require('../socket.io').isUsersOnline(uids, next);
 				} else {
 					next();
@@ -148,50 +115,22 @@ module.exports = function(User) {
 			if (err) {
 				return callback(err);
 			}
+
 			var userData = results.userData;
 
-			if (results.isOnline) {
-				userData.forEach(function(userData, index) {
-					userData.status = User.getStatus(userData.status, results.isOnline[index]);
+			if (data.onlineOnly) {
+				userData = userData.filter(function(user, index) {
+					return user && user.status !== 'offline' && results.isOnline[index];
 				});
 			}
 
-			userData = filterUsers(userData, filterBy);
-
 			sortUsers(userData, sortBy);
 
 			uids = userData.map(function(user) {
 				return user && user.uid;
 			});
-			callback(null, uids);
-		});
-	}
 
-	function filterUsers(userData, filterBy) {
-		function passesFilter(user, filter) {
-			if (!user || !filter) {
-				return false;
-			}
-			var userValue = user[filter.field];
-			if (filter.type === '=') {
-				return userValue === filter.value;
-			} else if (filter.type === '!=') {
-				return userValue !== filter.value;
-			}
-			return false;
-		}
-
-		if (!filterBy.length) {
-			return userData;
-		}
-
-		return userData.filter(function(user) {
-			for(var i=0; i<filterBy.length; ++i) {
-				if (!passesFilter(user, filterBy[i])) {
-					return false;
-				}
-			}
-			return true;
+			callback(null, uids);
 		});
 	}