diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index bdb68c7b..21d05dcc 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -2047,6 +2047,57 @@ namespace TShockAPI } } #endregion + return; + + case "parent": + #region Parent + { + if (args.Parameters.Count < 2) + { + args.Player.SendErrorMessage("Invalid syntax! Proper syntax: /group parent [new parent group name]"); + return; + } + + string groupName = args.Parameters[1]; + Group group = TShock.Groups.GetGroupByName(groupName); + if (group == null) + { + args.Player.SendErrorMessage("No such group \"{0}\".", groupName); + return; + } + + if (args.Parameters.Count > 2) + { + string newParentGroupName = string.Join(" ", args.Parameters.Skip(2)); + if (!string.IsNullOrWhiteSpace(newParentGroupName) && !TShock.Groups.GroupExists(newParentGroupName)) + { + args.Player.SendErrorMessage("No such group \"{0}\".", newParentGroupName); + return; + } + + try + { + TShock.Groups.UpdateGroup(groupName, newParentGroupName, group.Permissions, group.ChatColor); + + if (!string.IsNullOrWhiteSpace(newParentGroupName)) + args.Player.SendSuccessMessage("Parent of group \"{0}\" set to \"{1}\".", groupName, newParentGroupName); + else + args.Player.SendSuccessMessage("Removed parent of group \"{0}\".", groupName); + } + catch (GroupManagerException ex) + { + args.Player.SendErrorMessage(ex.Message); + } + } + else + { + if (group.Parent != null) + args.Player.SendSuccessMessage("Parent of \"{0}\" is \"{1}\".", group.Name, group.Parent.Name); + else + args.Player.SendSuccessMessage("Group \"{0}\" has no parent.", group.Name); + } + } + #endregion return; case "del": #region Delete group @@ -2108,12 +2159,6 @@ namespace TShockAPI } #endregion return; - case "help": - args.Player.SendInfoMessage("Syntax: /group [arguments]"); - args.Player.SendInfoMessage("Commands: add, addperm, del, delperm, list, listperm"); - args.Player.SendInfoMessage("Arguments: add , addperm , del "); - args.Player.SendInfoMessage("Arguments: delperm , list [page], listperm [page]"); - return; case "list": #region List groups { @@ -2160,6 +2205,12 @@ namespace TShockAPI }); } #endregion + return; + case "help": + args.Player.SendInfoMessage("Syntax: /group [arguments]"); + args.Player.SendInfoMessage("Commands: add, addperm, parent, del, delperm, list, listperm"); + args.Player.SendInfoMessage("Arguments: add , addperm , del "); + args.Player.SendInfoMessage("Arguments: delperm , list [page], listperm [page]"); return; } } diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 5d962eb0..ad6bdf44 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -18,7 +18,8 @@ along with this program. If not, see . using System; using System.Collections; using System.Collections.Generic; -using System.Data; +using System.Data; +using System.Diagnostics; using System.IO; using System.Linq; using MySql.Data.MySqlClient; @@ -114,7 +115,7 @@ namespace TShockAPI.DB if (!string.IsNullOrWhiteSpace(parentname)) { var parent = groups.FirstOrDefault(gp => gp.Name == parentname); - if (parent == null) + if (parent == null || name == parentname) { var error = "Invalid parent {0} for group {1}".SFormat(parentname, group.Name); if (exceptions) @@ -165,28 +166,41 @@ namespace TShockAPI.DB /// permissions /// chatcolor public void UpdateGroup(string name, string parentname, string permissions, string chatcolor) - { - if (!GroupExists(name)) + { + Group group = GetGroupByName(name); + if (group == null) throw new GroupNotExistException(name); Group parent = null; if (!string.IsNullOrWhiteSpace(parentname)) { - parent = groups.FirstOrDefault(gp => gp.Name == parentname); - if (null == parent) - throw new GroupManagerException("Invalid parent {0} for group {1}".SFormat(parentname, name)); - } - - // NOTE: we use newgroup.XYZ to ensure any validation is also persisted to the DB - var newgroup = new Group(name, parent, chatcolor, permissions); + parent = GetGroupByName(parentname); + if (parent == null || parent == group) + throw new GroupManagerException("Invalid parent \"{0}\" for group \"{1}\".".SFormat(parentname, name)); + + // Check if the new parent would cause loops. + List groupChain = new List { group, parent }; + Group checkingGroup = parent.Parent; + while (checkingGroup != null) + { + if (groupChain.Contains(checkingGroup)) + throw new GroupManagerException( + string.Format("Invalid parent \"{0}\" for group \"{1}\" would cause loops in the parent chain.", parentname, name)); + + groupChain.Add(checkingGroup); + checkingGroup = checkingGroup.Parent; + } + } + + // Ensure any group validation is also persisted to the DB. + var newGroup = new Group(name, parent, chatcolor, permissions); string query = "UPDATE GroupList SET Parent=@0, Commands=@1, ChatColor=@2 WHERE GroupName=@3"; - if (database.Query(query, parentname, newgroup.Permissions, string.Format("{0},{1},{2}", newgroup.R, newgroup.G, newgroup.B), name) != 1) - throw new GroupManagerException("Failed to update group '" + name + "'"); + if (database.Query(query, parentname, newGroup.Permissions, string.Format("{0},{1},{2}", newGroup.R, newGroup.G, newGroup.B), name) != 1) + throw new GroupManagerException(string.Format("Failed to update group \"{0}\".", name)); - Group group = TShock.Utils.GetGroup(name); group.ChatColor = chatcolor; group.Permissions = permissions; - group.Parent = TShock.Utils.GetGroup(parentname); + group.Parent = parent; } #if COMPAT_SIGS @@ -251,54 +265,107 @@ namespace TShockAPI.DB } public void LoadPermisions() - { - // Create a temporary list so if there is an error it doesn't override the currently loaded groups with broken groups. - var tempgroups = new List(); - tempgroups.Add(new SuperAdminGroup()); - - if (groups == null || groups.Count < 2) - { - groups.Clear(); - groups.AddRange(tempgroups); - } - - try - { - var groupsparents = new List>(); + { + try + { + List newGroups = new List(groups.Count); + Dictionary newGroupParents = new Dictionary(groups.Count); using (var reader = database.QueryReader("SELECT * FROM GroupList")) { while (reader.Read()) - { - var group = new Group(reader.Get("GroupName"), null, reader.Get("ChatColor"), reader.Get("Commands")); - group.Prefix = reader.Get("Prefix"); - group.Suffix = reader.Get("Suffix"); - groupsparents.Add(Tuple.Create(group, reader.Get("Parent"))); - } - } + { + string groupName = reader.Get("GroupName"); + if (groupName == "superadmin") + { + Log.ConsoleInfo("WARNING: Group \"superadmin\" is defined in the database even though it's a reserved group name."); + continue; + } - foreach (var t in groupsparents) - { - var group = t.Item1; - var parentname = t.Item2; - if (!string.IsNullOrWhiteSpace(parentname)) - { - var parent = groupsparents.FirstOrDefault(gp => gp.Item1.Name == parentname); - if (parent == null) - { - Log.ConsoleError("Invalid parent {0} for group {1}".SFormat(parentname, group.Name)); - return; + newGroups.Add(new Group(groupName, null, reader.Get("ChatColor"), reader.Get("Commands")) { + Prefix = reader.Get("Prefix"), + Suffix = reader.Get("Suffix"), + }); + + try + { + newGroupParents.Add(groupName, reader.Get("Parent")); + } + catch (ArgumentException) + { + // Just in case somebody messed with the unique primary key. + Log.ConsoleError("ERROR: Group name \"{0}\" occurs more than once. Keeping current group settings."); + return; } - group.Parent = parent.Item1; } - tempgroups.Add(group); - } - - groups.Clear(); - groups.AddRange(tempgroups); - } - catch (Exception ex) - { - Log.Error(ex.ToString()); + } + + try + { + // Get rid of deleted groups. + for (int i = 0; i < groups.Count; i++) + if (newGroups.All(g => g.Name != groups[i].Name)) + groups.RemoveAt(i--); + + // Apply changed group settings while keeping the current instances and add new groups. + foreach (Group newGroup in newGroups) + { + Group currentGroup = groups.FirstOrDefault(g => g.Name == newGroup.Name); + if (currentGroup != null) + newGroup.AssignTo(currentGroup); + else + groups.Add(newGroup); + } + + // Resolve parent groups. + Debug.Assert(newGroups.Count == newGroupParents.Count); + for (int i = 0; i < groups.Count; i++) + { + Group group = groups[i]; + string parentGroupName; + if (!newGroupParents.TryGetValue(group.Name, out parentGroupName) || string.IsNullOrEmpty(parentGroupName)) + continue; + + group.Parent = groups.FirstOrDefault(g => g.Name == parentGroupName); + if (group.Parent == null) + { + Log.ConsoleError( + "ERROR: Group \"{0}\" is referencing non existent parent group \"{1}\", parent reference was removed.", + group.Name, parentGroupName); + } + else + { + if (group.Parent == group) + Log.ConsoleInfo( + "WARNING: Group \"{0}\" is referencing itself as parent group, parent reference was removed.", group.Name); + + List groupChain = new List { group }; + Group checkingGroup = group; + while (checkingGroup.Parent != null) + { + if (groupChain.Contains(checkingGroup.Parent)) + { + Log.ConsoleError( + "ERROR: Group \"{0}\" is referencing parent group \"{1}\" which is already part of the parent chain. Parent reference removed.", + checkingGroup.Name, checkingGroup.Parent.Name); + + checkingGroup.Parent = null; + break; + } + groupChain.Add(checkingGroup); + checkingGroup = checkingGroup.Parent; + } + } + } + } + finally + { + if (!groups.Any(g => g is SuperAdminGroup)) + groups.Add(new SuperAdminGroup()); + } + } + catch (Exception ex) + { + Log.ConsoleError("Error on reloading groups: " + ex); } } } diff --git a/TShockAPI/Group.cs b/TShockAPI/Group.cs index a7a2a909..9e869dfe 100644 --- a/TShockAPI/Group.cs +++ b/TShockAPI/Group.cs @@ -204,7 +204,7 @@ namespace TShockAPI return true; if (traversed.Contains(cur)) { - throw new Exception("Infinite group parenting ({0})".SFormat(cur.Name)); + throw new InvalidOperationException("Infinite group parenting ({0})".SFormat(cur.Name)); } traversed.Add(cur); cur = cur.Parent; @@ -270,6 +270,26 @@ namespace TShockAPI return; } permissions.Remove(permission); + } + + /// + /// Assigns all fields of this instance to another. + /// + /// The other instance. + public void AssignTo(Group otherGroup) + { + otherGroup.Name = Name; + otherGroup.Parent = Parent; + otherGroup.Prefix = Prefix; + otherGroup.Suffix = Suffix; + otherGroup.R = R; + otherGroup.G = G; + otherGroup.B = B; + otherGroup.Permissions = Permissions; + } + + public override string ToString() { + return this.Name; } }