diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index c56e3d0d..2dfb6322 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -78,7 +78,7 @@ namespace TShockAPI.DB } catch (DllNotFoundException) { - System.Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); + Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); throw new Exception(GetString("Could not find a database library (probably Sqlite3.dll)")); } @@ -355,7 +355,9 @@ namespace TShockAPI.DB return Bans[id]; } - using var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id); + string query = $"SELECT * FROM PlayerBans WHERE {"TicketNumber".EscapeSqlId(database)}=@0"; + + using var reader = database.QueryReader(query, id); if (reader.Read()) { @@ -380,10 +382,11 @@ namespace TShockAPI.DB /// public IEnumerable RetrieveBansByIdentifier(string identifier, bool currentOnly = true) { - string query = "SELECT * FROM PlayerBans WHERE Identifier=@0"; + string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)}=@0"; + if (currentOnly) { - query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifier); @@ -412,11 +415,11 @@ namespace TShockAPI.DB //Generate a sequence of '@0, @1, @2, ... etc' var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}")); - string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; + string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)} IN ({parameters})"; if (currentOnly) { - query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifiers); @@ -449,7 +452,7 @@ namespace TShockAPI.DB List banlist = new List(); try { - using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap[sortMethod]}"); + using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap(sortMethod)}"); while (reader.Read()) { @@ -490,12 +493,12 @@ namespace TShockAPI.DB return false; } - private readonly Dictionary SortToOrderByMap = new() + private string SortToOrderByMap(BanSortMethod sortMethod) => sortMethod switch { - { BanSortMethod.AddedNewestToOldest, "Date DESC" }, - { BanSortMethod.AddedOldestToNewest, "Date ASC" }, - { BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" }, - { BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" } + BanSortMethod.AddedNewestToOldest => $"{"Date".EscapeSqlId(database)} DESC", + BanSortMethod.AddedOldestToNewest => $"{"Date".EscapeSqlId(database)} ASC", + BanSortMethod.ExpirationSoonestToLatest => $"{"Expiration".EscapeSqlId(database)} ASC", + BanSortMethod.ExpirationLatestToSoonest => $"{"Expiration".EscapeSqlId(database)} DESC" }; } diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index 3b9890a3..08d6d1ec 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -82,7 +82,7 @@ namespace TShockAPI.DB try { - using var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid); + using var reader = database.QueryReader($"SELECT * FROM tsCharacter WHERE {"Account".EscapeSqlId(database)}=@0", acctid); if (reader.Read()) { playerData.exists = true; diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 553a1a33..ba82225a 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -314,15 +314,22 @@ namespace TShockAPI.DB group.Parent = parent; } - string query = (TShock.Config.Settings.StorageType.ToLower() == "sqlite") - ? "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);" - : "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3"; - if (database.Query(query, name, parentname, permissions, chatcolor) == 1) + string query = database.GetSqlType() switch + { + SqlType.Sqlite => "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);", + SqlType.Mysql => "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3", + SqlType.Postgres => "INSERT INTO GroupList (\"GroupName\", \"Parent\", \"Commands\", \"ChatColor\") VALUES (@0, @1, @2, @3) ON CONFLICT (\"GroupName\") DO NOTHING", + _ => throw new NotSupportedException(GetString("Unsupported database type.")) + }; + + if (database.Query(query, name, parentname, permissions, chatcolor) is 1) { groups.Add(group); } else + { throw new GroupManagerException(GetString($"Failed to add group {name}.")); + } } /// @@ -362,9 +369,12 @@ namespace TShockAPI.DB } // Ensure any group validation is also persisted to the DB. - var newGroup = new Group(name, parent, chatcolor, permissions); - newGroup.Prefix = prefix; - newGroup.Suffix = suffix; + var newGroup = new Group(name, parent, chatcolor, permissions) + { + Prefix = prefix, + Suffix = suffix + }; + string query = "UPDATE GroupList SET Parent=@0, Commands=@1, ChatColor=@2, Suffix=@3, Prefix=@4 WHERE GroupName=@5"; if (database.Query(query, parentname, newGroup.Permissions, newGroup.ChatColor, suffix, prefix, name) != 1) throw new GroupManagerException(GetString($"Failed to update group \"{name}\".")); diff --git a/TShockAPI/DB/Queries/PostgresQueryCreator.cs b/TShockAPI/DB/Queries/PostgresQueryCreator.cs index 779e38b0..5637d689 100644 --- a/TShockAPI/DB/Queries/PostgresQueryCreator.cs +++ b/TShockAPI/DB/Queries/PostgresQueryCreator.cs @@ -79,9 +79,7 @@ public class PostgresQueryCreator : GenericQueryCreator .Where(c => c.Unique).Select(c => $"\"{c.Name}\"") .ToArray(); // No re-enumeration - return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), - string.Join(", ", columns), - uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); + return $"CREATE TABLE {EscapeTableName(table.Name)} ({string.Join(", ", columns)} {(uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "")})"; } /// diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index d657980a..cc7eacdb 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -67,9 +67,9 @@ namespace TShockAPI.DB { try { - using var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString()); - + using var reader = database.QueryReader($"SELECT * FROM Regions WHERE {"WorldID".EscapeSqlId(database)}=@0", Main.worldID.ToString()); Regions.Clear(); + while (reader.Read()) { int id = reader.Get("Id"); @@ -135,10 +135,17 @@ namespace TShockAPI.DB } try { - database.Query( - "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, `Groups`, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", - tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); + string query = database.GetSqlType() switch + { + SqlType.Postgres => "INSERT INTO Regions (\"X1\", \"Y1\", \"width\", \"height\", \"RegionName\", \"WorldID\", \"UserIds\", \"Protected\", \"Groups\", \"Owner\", \"Z\") VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", + _ => "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, Groups, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", + + }; + + database.Query(query, tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); + int id; + using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid)) { if (res.Read()) diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs index 6bf7b8a8..ce955dbf 100644 --- a/TShockAPI/DB/SqlTable.cs +++ b/TShockAPI/DB/SqlTable.cs @@ -59,7 +59,9 @@ namespace TShockAPI.DB var columns = GetColumns(table); if (columns.Count > 0) { - if (!table.Columns.All(c => columns.Contains(c.Name)) || !columns.All(c => table.Columns.Any(c2 => c2.Name == c))) + // Use OrdinalIgnoreCase to account for pgsql automatically lowering cases. + if (!table.Columns.All(c => columns.Contains(c.Name, StringComparer.OrdinalIgnoreCase)) + || !columns.All(c => table.Columns.Any(c2 => c2.Name.Equals(c, StringComparison.OrdinalIgnoreCase)))) { var from = new SqlTable(table.Name, columns.Select(s => new SqlColumn(s, MySqlDbType.String)).ToList()); database.Query(creator.AlterTable(from, table)); @@ -70,6 +72,7 @@ namespace TShockAPI.DB database.Query(creator.CreateTable(table)); return true; } + return false; } @@ -102,8 +105,8 @@ namespace TShockAPI.DB } case SqlType.Postgres: { - using QueryResult reader = - database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name=@0", table.Name); + // HACK: Using "ilike" op to ignore case, due to weird case issues adapting for pgsql + using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name ILIKE @0", table.Name); while (reader.Read()) { diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index e705e3fc..47a1c15f 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -239,7 +239,7 @@ namespace TShockAPI.DB { try { - using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username=@0", username); + using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0", username); if (reader.Read()) { return reader.Get("ID"); @@ -293,13 +293,13 @@ namespace TShockAPI.DB object arg; if (account.ID != 0) { - query = "SELECT * FROM Users WHERE ID=@0"; + query = $"SELECT * FROM Users WHERE {"ID".EscapeSqlId(_database)}=@0"; arg = account.ID; type = "id"; } else { - query = "SELECT * FROM Users WHERE Username=@0"; + query = $"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0"; arg = account.Name; type = "name"; } @@ -358,9 +358,9 @@ namespace TShockAPI.DB try { List accounts = new List(); - string search = notAtStart ? string.Format("%{0}%", username) : string.Format("{0}%", username); - using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0", - search); + string search = $"{(notAtStart ? "%" : "")}{username}%"; + using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)} LIKE @0", search); + while (reader.Read()) { accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index 06723281..fb28f1a4 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -86,7 +86,7 @@ namespace TShockAPI.DB { Warps.Clear(); - using var reader = database.QueryReader("SELECT * FROM Warps WHERE WorldID = @0", + using var reader = database.QueryReader($"SELECT * FROM Warps WHERE {"WorldID".EscapeSqlId(database)} = @0", Main.worldID.ToString()); while (reader.Read()) { diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index d58a966e..55a681dd 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; using Npgsql; @@ -42,17 +43,17 @@ namespace TShockAPI.DB [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] public static int Query(this IDbConnection olddb, string query, params object[] args) { - using (var db = olddb.CloneEx()) + using IDbConnection db = olddb.CloneEx(); + db.Open(); + using IDbCommand com = db.CreateCommand(); + com.CommandText = query; + + for (int i = 0; i < args.Length; i++) { - db.Open(); - using (var com = db.CreateCommand()) - { - com.CommandText = query; - for (int i = 0; i < args.Length; i++) - com.AddParameter("@" + i, args[i] ?? DBNull.Value); - return com.ExecuteNonQuery(); - } + com.AddParameter("@" + i, args[i] ?? DBNull.Value); } + + return com.ExecuteNonQuery(); } /// @@ -271,6 +272,18 @@ namespace TShockAPI.DB return (T)reader.GetValue(column); } + + /// + /// Escapes an identifier for use in a SQL query. + /// + /// The identifier to escape, typically a table or column name. + /// The escaped identifier. + [Pure] + public static string EscapeSqlId(this string id, IDbConnection db) => db.GetSqlType() switch + { + SqlType.Postgres => $"\"{id}\"", // The main PITA and culprit + _ => id // Default case for agnostic SQL + }; } public enum SqlType