-Improved group loading from the database:

--TShock will now attempt to load any available valid group data even if there are invalid records and will also report proper errors / warnings.
--"superadmin" is now a reserved group name.
--Groups with parents causing infinite parent loops, invaild parents, or parenting theirselfes will have their parent group reset.
--Double definitions of groups with the same name are no longer allowed.
--Group instances are now tried to be preserved instead of being recreated during a reload of the group data.

-Added command "/group parent" to get and set the parent of groups.
-REST Endpoint "/v2/groups/create" will no longer allow creating groups having theirselfes as parent.
-REST Endpoint "/v2/groups/update" will no longer allow setting a group's parent to theirself or setting a parent group resulting in an infinite parent loop.
-This commit should fix #482.
This commit is contained in:
CoderCow 2013-07-27 22:02:29 +02:00
parent 4d95b5594e
commit b828299a1c
3 changed files with 202 additions and 64 deletions

View file

@ -2048,6 +2048,57 @@ namespace TShockAPI
}
#endregion
return;
case "parent":
#region Parent
{
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Invalid syntax! Proper syntax: /group parent <group name> [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 <command> [arguments]");
args.Player.SendInfoMessage("Commands: add, addperm, del, delperm, list, listperm");
args.Player.SendInfoMessage("Arguments: add <group name>, addperm <group name> <permissions...>, del <group name>");
args.Player.SendInfoMessage("Arguments: delperm <group name> <permissions...>, list [page], listperm <group name> [page]");
return;
case "list":
#region List groups
{
@ -2161,6 +2206,12 @@ namespace TShockAPI
}
#endregion
return;
case "help":
args.Player.SendInfoMessage("Syntax: /group <command> [arguments]");
args.Player.SendInfoMessage("Commands: add, addperm, parent, del, delperm, list, listperm");
args.Player.SendInfoMessage("Arguments: add <group name>, addperm <group name> <permissions...>, del <group name>");
args.Player.SendInfoMessage("Arguments: delperm <group name> <permissions...>, list [page], listperm <group name> [page]");
return;
}
}
#endregion Group Management

View file

@ -19,6 +19,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
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)
@ -166,27 +167,40 @@ namespace TShockAPI.DB
/// <param name="chatcolor">chatcolor</param>
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));
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<Group> groupChain = new List<Group> { 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;
}
}
// NOTE: we use newgroup.XYZ to ensure any validation is also persisted to the DB
var newgroup = new Group(name, parent, chatcolor, permissions);
// 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
@ -252,53 +266,106 @@ 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<Group>();
tempgroups.Add(new SuperAdminGroup());
if (groups == null || groups.Count < 2)
{
groups.Clear();
groups.AddRange(tempgroups);
}
try
{
var groupsparents = new List<Tuple<Group, string>>();
List<Group> newGroups = new List<Group>(groups.Count);
Dictionary<string,string> newGroupParents = new Dictionary<string, string>(groups.Count);
using (var reader = database.QueryReader("SELECT * FROM GroupList"))
{
while (reader.Read())
{
var group = new Group(reader.Get<String>("GroupName"), null, reader.Get<String>("ChatColor"), reader.Get<String>("Commands"));
group.Prefix = reader.Get<String>("Prefix");
group.Suffix = reader.Get<String>("Suffix");
groupsparents.Add(Tuple.Create(group, reader.Get<string>("Parent")));
}
string groupName = reader.Get<string>("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)
newGroups.Add(new Group(groupName, null, reader.Get<string>("ChatColor"), reader.Get<string>("Commands")) {
Prefix = reader.Get<string>("Prefix"),
Suffix = reader.Get<string>("Suffix"),
});
try
{
var group = t.Item1;
var parentname = t.Item2;
if (!string.IsNullOrWhiteSpace(parentname))
newGroupParents.Add(groupName, reader.Get<string>("Parent"));
}
catch (ArgumentException)
{
var parent = groupsparents.FirstOrDefault(gp => gp.Item1.Name == parentname);
if (parent == null)
{
Log.ConsoleError("Invalid parent {0} for group {1}".SFormat(parentname, group.Name));
// 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);
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<Group> groupChain = new List<Group> { 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.Error(ex.ToString());
Log.ConsoleError("Error on reloading groups: " + ex);
}
}
}

View file

@ -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;
@ -271,6 +271,26 @@ namespace TShockAPI
}
permissions.Remove(permission);
}
/// <summary>
/// Assigns all fields of this instance to another.
/// </summary>
/// <param name="otherGroup">The other instance.</param>
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;
}
}
/// <summary>