using System; using System.Collections.Generic; using System.IO; using System.Text; using ScrewTurn.Wiki.PluginFramework; namespace ScrewTurn.Wiki { /// /// Implements a Users Storage Provider. /// public class UsersStorageProvider : IUsersStorageProviderV30 { private const string UsersFile = "Users.cs"; private const string UsersDataFile = "UsersData.cs"; private const string GroupsFile = "Groups.cs"; private readonly ComponentInformation info = new ComponentInformation("Local Users Provider", "ScrewTurn Software", Settings.WikiVersion, "http://www.screwturn.eu", null); private IHostV30 host; private UserGroup[] groupsCache = null; private UserInfo[] usersCache = null; private string GetFullPath(string filename) { return Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), filename); } /// /// Initializes the Provider. /// /// The Host of the Provider. /// The Configuration data, if any. /// If host or config are null. /// If config is not valid or is incorrect. public void Init(IHostV30 host, string config) { if(host == null) throw new ArgumentNullException("host"); if(config == null) throw new ArgumentNullException("config"); this.host = host; if(!LocalProvidersTools.CheckWritePermissions(host.GetSettingValue(SettingName.PublicDirectory))) { throw new InvalidConfigurationException("Cannot write into the public directory - check permissions"); } if(!File.Exists(GetFullPath(UsersFile))) { File.Create(GetFullPath(UsersFile)).Close(); } if(!File.Exists(GetFullPath(UsersDataFile))) { File.Create(GetFullPath(UsersDataFile)).Close(); } if(!File.Exists(GetFullPath(GroupsFile))) { File.Create(GetFullPath(GroupsFile)).Close(); } VerifyAndPerformUpgrade(); } /// /// Verifies the need for a data upgrade, and performs it when needed. /// private void VerifyAndPerformUpgrade() { // Load file lines // Parse first line (if any) with old (v2) algorithm // If parsing is successful, then the file must be converted // Conversion consists in removing the 'ADMIN|USER' field, creating the proper default groups and setting user membership // Structure v2: // Username|PasswordHash|Email|Active-Inactive|DateTime|Admin-User //string[] lines = File.ReadAllLines(GetFullPath(UsersFile)); // Use this method because version 2.0 file might have started with a blank line string[] lines = File.ReadAllText(GetFullPath(UsersFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if(lines.Length > 0) { bool upgradeIsNeeded = false; LocalUserInfo[] users = new LocalUserInfo[lines.Length]; bool[] oldStyleAdmin = new bool[lines.Length]; // Values are valid only if upgradeIsNeeded=true char[] splitter = new char[] { '|' }; for(int i = 0; i < lines.Length; i++) { string line = lines[i]; string[] fields = line.Split(splitter, StringSplitOptions.RemoveEmptyEntries); string displayName = null; if(fields.Length == 6) { if(fields[5] == "ADMIN" || fields[5] == "USER") { // Version 2.0 upgradeIsNeeded = true; oldStyleAdmin[i] = fields[5] == "ADMIN"; } else { // Version 3.0 with DisplayName specified oldStyleAdmin[i] = false; displayName = fields[5]; } } else { // Can be a version 3.0 file, with empty DisplayName oldStyleAdmin[i] = false; } users[i] = new LocalUserInfo(fields[0], displayName, fields[2], fields[3].ToLowerInvariant() == "active", DateTime.Parse(fields[4]), this, fields[1]); } if(upgradeIsNeeded) { // Dump users // Create default groups // Set membership for old users // Tell the host to set the permissions for the default groups string backupFile = GetFullPath(Path.GetFileNameWithoutExtension(UsersFile) + "_v2" + Path.GetExtension(UsersFile)); File.Copy(GetFullPath(UsersFile), backupFile); host.LogEntry("Upgrading users format from 2.0 to 3.0", LogEntryType.General, null, this); DumpUsers(users); UserGroup adminsGroup = AddUserGroup(host.GetSettingValue(SettingName.AdministratorsGroup), "Built-in Administrators"); UserGroup usersGroup = AddUserGroup(host.GetSettingValue(SettingName.UsersGroup), "Built-in Users"); for(int i = 0; i < users.Length; i++) { if(oldStyleAdmin[i]) { SetUserMembership(users[i], new string[] { adminsGroup.Name }); } else { SetUserMembership(users[i], new string[] { usersGroup.Name }); } } host.UpgradeSecurityFlagsToGroupsAcl(adminsGroup, usersGroup); } } } /// /// Method invoked on shutdown. /// /// This method might not be invoked in some cases. public void Shutdown() { } /// /// Gets the Information about the Provider. /// public ComponentInformation Information { get { return info; } } /// /// Gets a brief summary of the configuration string format, in HTML. Returns null if no configuration is needed. /// public string ConfigHelpHtml { get { return null; } } /// /// Gets a value indicating whether user accounts are read-only. /// public bool UserAccountsReadOnly { get { return false; } } /// /// Gets a value indicating whether user groups are read-only. If so, the provider /// should support default user groups as defined in the wiki configuration. /// public bool UserGroupsReadOnly { get { return false; } } /// /// Gets a value indicating whether group membership is read-only (if /// is false, then this property must be false). If this property is true, the provider /// should return membership data compatible with default user groups. /// public bool GroupMembershipReadOnly { get { return false; } } /// /// Gets a value indicating whether users' data is read-only. /// public bool UsersDataReadOnly { get { return false; } } /// /// Tests a Password for a User account. /// /// The User account. /// The Password to test. /// True if the Password is correct. /// If user or password are null. public bool TestAccount(UserInfo user, string password) { if(user == null) throw new ArgumentNullException("user"); if(password == null) throw new ArgumentNullException("password"); return TryManualLogin(user.Username, password) != null; } /// /// Gets the complete list of Users. /// /// All the Users, sorted by username. public UserInfo[] GetUsers() { lock(this) { if(usersCache == null) { UserGroup[] groups = GetUserGroups(); string[] lines = File.ReadAllLines(GetFullPath(UsersFile)); UserInfo[] result = new UserInfo[lines.Length]; char[] splitter = new char[] { '|' }; string[] fields; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split(splitter, StringSplitOptions.RemoveEmptyEntries); // Structure (version 3.0 - file previously converted): // Username|PasswordHash|Email|Active-Inactive|DateTime[|DisplayName] string displayName = fields.Length == 6 ? fields[5] : null; result[i] = new LocalUserInfo(fields[0], displayName, fields[2], fields[3].ToLowerInvariant().Equals("active"), DateTime.Parse(fields[4]), this, fields[1]); result[i].Groups = GetGroupsForUser(result[i].Username, groups); } Array.Sort(result, new UsernameComparer()); usersCache = result; } return usersCache; } } /// /// Gets the names of all the groups a user is member of. /// /// The username. /// The groups. /// The names of the groups the user is member of. private string[] GetGroupsForUser(string user, UserGroup[] groups) { List result = new List(3); foreach(UserGroup group in groups) { if(Array.Find(group.Users, delegate(string u) { return u == user; }) != null) { result.Add(group.Name); } } return result.ToArray(); } /// /// Loads a proper local instance of a user account. /// /// The user account. /// The local instance, or null. private LocalUserInfo LoadLocalInstance(UserInfo user) { UserInfo[] users = GetUsers(); UsernameComparer comp = new UsernameComparer(); for(int i = 0; i < users.Length; i++) { if(comp.Compare(users[i], user) == 0) return users[i] as LocalUserInfo; } return null; } /// /// Searches for a User. /// /// The User to search for. /// True if the User already exists. private bool UserExists(UserInfo user) { UserInfo[] users = GetUsers(); UsernameComparer comp = new UsernameComparer(); for(int i = 0; i < users.Length; i++) { if(comp.Compare(users[i], user) == 0) return true; } return false; } /// /// Adds a new User. /// /// The Username. /// The display name (can be null). /// The Password. /// The Email address. /// A value specifying whether or not the account is active. /// The Account creation Date/Time. /// The correct object or null. /// If username, password or email are null. /// If username, password or email are empty. public UserInfo AddUser(string username, string displayName, string password, string email, bool active, DateTime dateTime) { if(username == null) throw new ArgumentNullException("username"); if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username"); if(password == null) throw new ArgumentNullException("password"); if(password.Length == 0) throw new ArgumentException("Password cannot be empty", "password"); if(email == null) throw new ArgumentNullException("email"); if(email.Length == 0) throw new ArgumentException("Email cannot be empty", "email"); lock(this) { if(UserExists(new UserInfo(username, displayName, "", true, DateTime.Now, this))) return null; BackupUsersFile(); StringBuilder sb = new StringBuilder(); sb.Append(username); sb.Append("|"); sb.Append(Hash.Compute(password)); sb.Append("|"); sb.Append(email); sb.Append("|"); sb.Append(active ? "ACTIVE" : "INACTIVE"); sb.Append("|"); sb.Append(dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); // ADMIN|USER no more used in version 3.0 //sb.Append("|"); //sb.Append(admin ? "ADMIN" : "USER"); if(!string.IsNullOrEmpty(displayName)) { sb.Append("|"); sb.Append(displayName); } sb.Append("\r\n"); File.AppendAllText(GetFullPath(UsersFile), sb.ToString()); usersCache = null; return new LocalUserInfo(username, displayName, email, active, dateTime, this, Hash.Compute(password)); } } /// /// Modifies a User. /// /// The Username of the user to modify. /// The new display name (can be null). /// The new Password (null or blank to keep the current password). /// The new Email address. /// A value indicating whether the account is active. /// The correct object or null. /// If user or newEmail are null. /// If newEmail is empty. public UserInfo ModifyUser(UserInfo user, string newDisplayName, string newPassword, string newEmail, bool newActive) { if(user == null) throw new ArgumentNullException("user"); if(newEmail == null) throw new ArgumentNullException("newEmail"); if(newEmail.Length == 0) throw new ArgumentException("New Email cannot be empty", "newEmail"); lock(this) { LocalUserInfo local = LoadLocalInstance(user); if(local == null) return null; UserInfo[] allUsers = GetUsers(); UsernameComparer comp = new UsernameComparer(); usersCache = null; for(int i = 0; i < allUsers.Length; i++) { if(comp.Compare(allUsers[i], user) == 0) { LocalUserInfo result = new LocalUserInfo(user.Username, newDisplayName, newEmail, newActive, user.DateTime, this, string.IsNullOrEmpty(newPassword) ? local.PasswordHash : Hash.Compute(newPassword)); result.Groups = allUsers[i].Groups; allUsers[i] = result; DumpUsers(allUsers); return result; } } } return null; } /// /// Removes a User. /// /// The User to remove. /// True if the User has been removed successfully. /// If user is null. public bool RemoveUser(UserInfo user) { if(user == null) throw new ArgumentNullException("user"); lock(this) { UserInfo[] users = GetUsers(); UsernameComparer comp = new UsernameComparer(); int idx = -1; for(int i = 0; i < users.Length; i++) { if(comp.Compare(users[i], user) == 0) { idx = i; break; } } if(idx < 0) return false; // Remove user's data string lowercaseUsername = user.Username.ToLowerInvariant(); string[] lines = File.ReadAllLines(GetFullPath(UsersDataFile)); List newLines = new List(lines.Length); string[] fields; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split('|'); if(fields[0].ToLowerInvariant() != lowercaseUsername) { newLines.Add(lines[i]); } } File.WriteAllLines(GetFullPath(UsersDataFile), newLines.ToArray()); // Remove user List tmp = new List(users); tmp.Remove(tmp[idx]); DumpUsers(tmp.ToArray()); usersCache = null; } return true; } private void BackupUsersFile() { lock(this) { File.Copy(GetFullPath(UsersFile), GetFullPath(Path.GetFileNameWithoutExtension(UsersFile) + ".bak" + Path.GetExtension(UsersFile)), true); } } /// /// Writes on disk all the Users. /// /// The User list. /// This method does not lock resources, therefore a lock is need in the caller. private void DumpUsers(UserInfo[] users) { lock(this) { BackupUsersFile(); StringBuilder sb = new StringBuilder(); for(int i = 0; i < users.Length; i++) { LocalUserInfo u = (LocalUserInfo)users[i]; sb.Append(u.Username); sb.Append("|"); sb.Append(u.PasswordHash); sb.Append("|"); sb.Append(u.Email); sb.Append("|"); sb.Append(u.Active ? "ACTIVE" : "INACTIVE"); sb.Append("|"); sb.Append(u.DateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); // ADMIN|USER no more used in version 3.0 //sb.Append("|"); //sb.Append(u.Admin ? "ADMIN" : "USER"); if(!string.IsNullOrEmpty(u.DisplayName)) { sb.Append("|"); sb.Append(u.DisplayName); } sb.Append("\r\n"); } File.WriteAllText(GetFullPath(UsersFile), sb.ToString()); } } /// /// Finds a user group. /// /// The name of the group to find. /// The or null if no data is found. private UserGroup FindGroup(string name) { lock(this) { UserGroup[] allUsers = GetUserGroups(); UserGroupComparer comp = new UserGroupComparer(); UserGroup target = new UserGroup(name, "", this); foreach(UserGroup g in allUsers) { if(comp.Compare(g, target) == 0) return g; } return null; } } /// /// Gets all the user groups. /// /// All the groups, sorted by name. public UserGroup[] GetUserGroups() { lock(this) { if(groupsCache == null) { string[] lines = File.ReadAllLines(GetFullPath(GroupsFile)); UserGroup[] result = new UserGroup[lines.Length]; string[] fields; string[] users; for(int count = 0; count < lines.Length; count++) { // Structure - description can be empty // Name|Description|User1|User2|... fields = lines[count].Split('|'); users = new string[fields.Length - 2]; for(int i = 0; i < fields.Length - 2; i++) { users[i] = fields[i + 2]; } result[count] = new UserGroup(fields[0], fields[1], this); result[count].Users = users; } Array.Sort(result, new UserGroupComparer()); groupsCache = result; } return groupsCache; } } /// /// Adds a new user group. /// /// The name of the group. /// The description of the group. /// The correct object or null. /// If name or description are null. /// If name is empty. public UserGroup AddUserGroup(string name, string description) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(description == null) throw new ArgumentNullException("description"); lock(this) { if(FindGroup(name) != null) return null; BackupGroupsFile(); groupsCache = null; // Structure - description can be empty // Name|Description|User1|User2|... File.AppendAllText(GetFullPath(GroupsFile), name + "|" + description + "\r\n"); return new UserGroup(name, description, this); } } private void BackupGroupsFile() { lock(this) { File.Copy(GetFullPath(GroupsFile), GetFullPath(Path.GetFileNameWithoutExtension(GroupsFile) + ".bak" + Path.GetExtension(GroupsFile)), true); } } /// /// Dumps user groups on disk. /// /// The user groups to dump. private void DumpUserGroups(UserGroup[] groups) { lock(this) { StringBuilder sb = new StringBuilder(1000); foreach(UserGroup group in groups) { // Structure - description can be empty // Name|Description|User1|User2|... sb.Append(group.Name); sb.Append("|"); sb.Append(group.Description); if(group.Users.Length > 0) { foreach(string user in group.Users) { sb.Append("|"); sb.Append(user); } } sb.Append("\r\n"); } BackupGroupsFile(); File.WriteAllText(GetFullPath(GroupsFile), sb.ToString()); } } /// /// Modifies a user group. /// /// The group to modify. /// The new description of the group. /// The correct object or null. /// If group or description are null. public UserGroup ModifyUserGroup(UserGroup group, string description) { if(group == null) throw new ArgumentNullException("group"); if(description == null) throw new ArgumentNullException("description"); lock(this) { UserGroup[] allGroups = GetUserGroups(); groupsCache = null; UserGroupComparer comp = new UserGroupComparer(); for(int i = 0; i < allGroups.Length; i++) { if(comp.Compare(allGroups[i], group) == 0) { allGroups[i].Description = description; DumpUserGroups(allGroups); return allGroups[i]; } } return null; } } /// /// Removes a user group. /// /// The group to remove. /// true if the group is removed, false otherwise. /// If group is null. public bool RemoveUserGroup(UserGroup group) { if(group == null) throw new ArgumentNullException("group"); lock(this) { UserGroup[] allGroups = GetUserGroups(); List result = new List(allGroups.Length); UserGroupComparer comp = new UserGroupComparer(); foreach(UserGroup g in allGroups) { if(comp.Compare(g, group) != 0) { result.Add(g); } } DumpUserGroups(result.ToArray()); groupsCache = null; return result.Count == allGroups.Length - 1; } } /// /// Sets the group memberships of a user account. /// /// The user account. /// The groups the user account is member of. /// The correct object or null. /// If user or groups are null. public UserInfo SetUserMembership(UserInfo user, string[] groups) { if(user == null) throw new ArgumentNullException("user"); if(groups == null) throw new ArgumentNullException("groups"); lock(this) { foreach(string g in groups) { if(FindGroup(g) == null) return null; } LocalUserInfo local = LoadLocalInstance(user); if(local == null) return null; UserGroup[] allGroups = GetUserGroups(); List users; for(int i = 0; i < allGroups.Length; i++) { users = new List(allGroups[i].Users); if(IsSelected(allGroups[i], groups)) { // Current group is one of the selected, add user to it if(!users.Contains(user.Username)) users.Add(user.Username); } else { // Current group is not bound with the user, remove user users.Remove(user.Username); } allGroups[i].Users = users.ToArray(); } groupsCache = null; usersCache = null; DumpUserGroups(allGroups); LocalUserInfo result = new LocalUserInfo(local.Username, local.DisplayName, local.Email, local.Active, local.DateTime, this, local.PasswordHash); result.Groups = groups; return result; } } /// /// Determines whether a user group is contained in an array of user group names. /// /// The user group to check. /// The user group names array. /// true if users contains user.Name, false otherwise. private static bool IsSelected(UserGroup group, string[] groups) { StringComparer comp = StringComparer.OrdinalIgnoreCase; return Array.Find(groups, delegate(string g) { return comp.Compare(g, group.Name) == 0; }) != null; } /// /// Tries to login a user directly through the provider. /// /// The username. /// The password. /// The correct UserInfo object, or null. /// If username or password are null. public UserInfo TryManualLogin(string username, string password) { if(username == null) throw new ArgumentNullException("username"); if(password == null) throw new ArgumentNullException("password"); // Shortcut if(username.Length == 0) return null; if(password.Length == 0) return null; lock(this) { string hash = Hash.Compute(password); UserInfo[] all = GetUsers(); foreach(UserInfo u in all) { if(u.Active && //string.Compare(u.Username, username, false, System.Globalization.CultureInfo.InvariantCulture) == 0 && //string.Compare(((LocalUserInfo)u).PasswordHash, hash, false, System.Globalization.CultureInfo.InvariantCulture) == 0) { string.CompareOrdinal(u.Username, username) == 0 && string.CompareOrdinal(((LocalUserInfo)u).PasswordHash, hash) == 0) { return u; } } } return null; } /// /// Tries to login a user directly through the provider using /// the current HttpContext and without username/password. /// /// The current HttpContext. /// The correct UserInfo object, or null. /// If context is null. public UserInfo TryAutoLogin(System.Web.HttpContext context) { if(context == null) throw new ArgumentNullException("context"); return null; } /// /// Tries to retrieve the information about a user account. /// /// The username. /// The correct UserInfo object, or null. /// If username is null. /// If username is empty. public UserInfo GetUser(string username) { if(username == null) throw new ArgumentNullException("username"); if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username"); lock(this) { UserInfo[] all = GetUsers(); foreach(UserInfo u in all) { if(string.Compare(u.Username, username, false, System.Globalization.CultureInfo.InvariantCulture) == 0) { return u; } } } return null; } /// /// Tries to retrieve the information about a user account. /// /// The email address. /// The first user found with the specified email address, or null. /// If email is null. /// If email is empty. public UserInfo GetUserByEmail(string email) { if(email == null) throw new ArgumentNullException("email"); if(email.Length == 0) throw new ArgumentException("Email cannot be empty", "email"); lock(this) { foreach(UserInfo user in GetUsers()) { if(user.Email == email) return user; } } return null; } /// /// Notifies the provider that a user has logged in through the authentication cookie. /// /// The user who has logged in. /// If user is null. public void NotifyCookieLogin(UserInfo user) { if(user == null) throw new ArgumentNullException("user"); // Nothing to do } /// /// Notifies the provider that a user has logged out. /// /// The user who has logged out. /// If user is null. public void NotifyLogout(UserInfo user) { if(user == null) throw new ArgumentNullException("user"); // Nothing to do } /// /// Stores a user data element, overwriting the previous one if present. /// /// The user the data belongs to. /// The key of the data element (case insensitive). /// The value of the data element, null for deleting the data. /// true if the data element is stored, false otherwise. /// If user or key are null. /// If key is empty. public bool StoreUserData(UserInfo user, string key, string value) { if(user == null) throw new ArgumentNullException("user"); if(key == null) throw new ArgumentNullException("key"); if(key.Length == 0) throw new ArgumentException("Key cannot be empty", "key"); // Format // User|Key|Value lock(this) { if(GetUser(user.Username) == null) return false; // Find a previously existing key and replace it if found // If not found, add a new line string lowercaseUsername = user.Username.ToLowerInvariant(); string lowercaseKey = key.ToLowerInvariant(); string[] lines = File.ReadAllLines(GetFullPath(UsersDataFile)); string[] fields; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split('|'); if(fields[0].ToLowerInvariant() == lowercaseUsername && fields[1].ToLowerInvariant() == lowercaseKey) { if(value != null) { // Replace the value, then save file lines[i] = fields[0] + "|" + fields[1] + "|" + value; } else { // Remove the element string[] newLines = new string[lines.Length - 1]; Array.Copy(lines, newLines, i); Array.Copy(lines, i + 1, newLines, i, lines.Length - i - 1); lines = newLines; } File.WriteAllLines(GetFullPath(UsersDataFile), lines); return true; } } // If the program gets here, the element was not present, append it File.AppendAllText(GetFullPath(UsersDataFile), user.Username + "|" + key + "|" + value + "\r\n"); return true; } } /// /// Gets a user data element, if any. /// /// The user the data belongs to. /// The key of the data element. /// The value of the data element, or null if the element is not found. /// If user or key are null. /// If key is empty. public string RetrieveUserData(UserInfo user, string key) { if(user == null) throw new ArgumentNullException("user"); if(key == null) throw new ArgumentNullException("key"); if(key.Length == 0) throw new ArgumentException("Key cannot be empty", "key"); lock(this) { string lowercaseUsername = user.Username.ToLowerInvariant(); string lowercaseKey = key.ToLowerInvariant(); string[] lines = File.ReadAllLines(GetFullPath(UsersDataFile)); string[] fields; foreach(string line in lines) { fields = line.Split('|'); if(fields[0].ToLowerInvariant() == lowercaseUsername && fields[1].ToLowerInvariant() == lowercaseKey) { return fields[2]; } } } return null; } /// /// Retrieves all the user data elements for a user. /// /// The user. /// The user data elements (key->value). /// If user is null. public IDictionary RetrieveAllUserData(UserInfo user) { if(user == null) throw new ArgumentNullException("user"); lock(this) { string lowercaseUsername = user.Username.ToLowerInvariant(); string[] lines = File.ReadAllLines(GetFullPath(UsersDataFile)); Dictionary result = new Dictionary(10); string[] fields; foreach(string line in lines) { fields = line.Split('|'); if(fields[0].ToLowerInvariant() == lowercaseUsername) { result.Add(fields[1], fields[2]); } } return result; } } /// /// Gets all the users that have the specified element in their data. /// /// The key of the data. /// The users and the data. /// If key is null. /// If key is empty. public IDictionary GetUsersWithData(string key) { if(key == null) throw new ArgumentNullException("key"); if(key.Length == 0) throw new ArgumentException("Key cannot be empty", "key"); lock(this) { UserInfo[] allUsers = GetUsers(); string[] lines = File.ReadAllLines(GetFullPath(UsersDataFile)); Dictionary result = new Dictionary(lines.Length / 4); string[] fields; foreach(string line in lines) { fields = line.Split('|'); if(fields[1] == key) { UserInfo currentUser = Array.Find(allUsers, delegate(UserInfo user) { return user.Username == fields[0]; }); if(currentUser != null) result.Add(currentUser, fields[2]); } } return result; } } } }