fix(db/postgres): Resolve SQL identifier casing issues
Improves SQL query execution by ensuring proper casing for identifiers across various database types, particularly for Postgres. Enhances security and compatibility by using an identifier escaping method, preventing potential errors due to case sensitivity in SQL queries. Addresses potential issues with existing queries for better reliability and consistency.
This commit is contained in:
parent
69b98980f1
commit
2d839e3609
9 changed files with 81 additions and 47 deletions
|
|
@ -78,7 +78,7 @@ namespace TShockAPI.DB
|
||||||
}
|
}
|
||||||
catch (DllNotFoundException)
|
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)"));
|
throw new Exception(GetString("Could not find a database library (probably Sqlite3.dll)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,7 +355,9 @@ namespace TShockAPI.DB
|
||||||
return Bans[id];
|
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())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
|
|
@ -380,10 +382,11 @@ namespace TShockAPI.DB
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public IEnumerable<Ban> RetrieveBansByIdentifier(string identifier, bool currentOnly = true)
|
public IEnumerable<Ban> 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)
|
if (currentOnly)
|
||||||
{
|
{
|
||||||
query += $" AND Expiration > {DateTime.UtcNow.Ticks}";
|
query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}";
|
||||||
}
|
}
|
||||||
|
|
||||||
using var reader = database.QueryReader(query, identifier);
|
using var reader = database.QueryReader(query, identifier);
|
||||||
|
|
@ -412,11 +415,11 @@ namespace TShockAPI.DB
|
||||||
//Generate a sequence of '@0, @1, @2, ... etc'
|
//Generate a sequence of '@0, @1, @2, ... etc'
|
||||||
var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}"));
|
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)
|
if (currentOnly)
|
||||||
{
|
{
|
||||||
query += $" AND Expiration > {DateTime.UtcNow.Ticks}";
|
query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}";
|
||||||
}
|
}
|
||||||
|
|
||||||
using var reader = database.QueryReader(query, identifiers);
|
using var reader = database.QueryReader(query, identifiers);
|
||||||
|
|
@ -449,7 +452,7 @@ namespace TShockAPI.DB
|
||||||
List<Ban> banlist = new List<Ban>();
|
List<Ban> banlist = new List<Ban>();
|
||||||
try
|
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())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|
@ -490,12 +493,12 @@ namespace TShockAPI.DB
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<BanSortMethod, string> SortToOrderByMap = new()
|
private string SortToOrderByMap(BanSortMethod sortMethod) => sortMethod switch
|
||||||
{
|
{
|
||||||
{ BanSortMethod.AddedNewestToOldest, "Date DESC" },
|
BanSortMethod.AddedNewestToOldest => $"{"Date".EscapeSqlId(database)} DESC",
|
||||||
{ BanSortMethod.AddedOldestToNewest, "Date ASC" },
|
BanSortMethod.AddedOldestToNewest => $"{"Date".EscapeSqlId(database)} ASC",
|
||||||
{ BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" },
|
BanSortMethod.ExpirationSoonestToLatest => $"{"Expiration".EscapeSqlId(database)} ASC",
|
||||||
{ BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" }
|
BanSortMethod.ExpirationLatestToSoonest => $"{"Expiration".EscapeSqlId(database)} DESC"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ namespace TShockAPI.DB
|
||||||
|
|
||||||
try
|
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())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
playerData.exists = true;
|
playerData.exists = true;
|
||||||
|
|
|
||||||
|
|
@ -314,15 +314,22 @@ namespace TShockAPI.DB
|
||||||
group.Parent = parent;
|
group.Parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
string query = (TShock.Config.Settings.StorageType.ToLower() == "sqlite")
|
string query = database.GetSqlType() switch
|
||||||
? "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";
|
SqlType.Sqlite => "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);",
|
||||||
if (database.Query(query, name, parentname, permissions, chatcolor) == 1)
|
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);
|
groups.Add(group);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
throw new GroupManagerException(GetString($"Failed to add group {name}."));
|
throw new GroupManagerException(GetString($"Failed to add group {name}."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -362,9 +369,12 @@ namespace TShockAPI.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure any group validation is also persisted to the DB.
|
// Ensure any group validation is also persisted to the DB.
|
||||||
var newGroup = new Group(name, parent, chatcolor, permissions);
|
var newGroup = new Group(name, parent, chatcolor, permissions)
|
||||||
newGroup.Prefix = prefix;
|
{
|
||||||
newGroup.Suffix = suffix;
|
Prefix = prefix,
|
||||||
|
Suffix = suffix
|
||||||
|
};
|
||||||
|
|
||||||
string query = "UPDATE GroupList SET Parent=@0, Commands=@1, ChatColor=@2, Suffix=@3, Prefix=@4 WHERE GroupName=@5";
|
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)
|
if (database.Query(query, parentname, newGroup.Permissions, newGroup.ChatColor, suffix, prefix, name) != 1)
|
||||||
throw new GroupManagerException(GetString($"Failed to update group \"{name}\"."));
|
throw new GroupManagerException(GetString($"Failed to update group \"{name}\"."));
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ public class PostgresQueryCreator : GenericQueryCreator
|
||||||
.Where(c => c.Unique).Select(c => $"\"{c.Name}\"")
|
.Where(c => c.Unique).Select(c => $"\"{c.Name}\"")
|
||||||
.ToArray(); // No re-enumeration
|
.ToArray(); // No re-enumeration
|
||||||
|
|
||||||
return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name),
|
return $"CREATE TABLE {EscapeTableName(table.Name)} ({string.Join(", ", columns)} {(uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "")})";
|
||||||
string.Join(", ", columns),
|
|
||||||
uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ namespace TShockAPI.DB
|
||||||
{
|
{
|
||||||
try
|
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();
|
Regions.Clear();
|
||||||
|
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
int id = reader.Get<int>("Id");
|
int id = reader.Get<int>("Id");
|
||||||
|
|
@ -135,10 +135,17 @@ namespace TShockAPI.DB
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
database.Query(
|
string query = database.GetSqlType() switch
|
||||||
"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);
|
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;
|
int id;
|
||||||
|
|
||||||
using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid))
|
using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid))
|
||||||
{
|
{
|
||||||
if (res.Read())
|
if (res.Read())
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ namespace TShockAPI.DB
|
||||||
var columns = GetColumns(table);
|
var columns = GetColumns(table);
|
||||||
if (columns.Count > 0)
|
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());
|
var from = new SqlTable(table.Name, columns.Select(s => new SqlColumn(s, MySqlDbType.String)).ToList());
|
||||||
database.Query(creator.AlterTable(from, table));
|
database.Query(creator.AlterTable(from, table));
|
||||||
|
|
@ -70,6 +72,7 @@ namespace TShockAPI.DB
|
||||||
database.Query(creator.CreateTable(table));
|
database.Query(creator.CreateTable(table));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,8 +105,8 @@ namespace TShockAPI.DB
|
||||||
}
|
}
|
||||||
case SqlType.Postgres:
|
case SqlType.Postgres:
|
||||||
{
|
{
|
||||||
using QueryResult reader =
|
// HACK: Using "ilike" op to ignore case, due to weird case issues adapting for pgsql
|
||||||
database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name=@0", table.Name);
|
using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name ILIKE @0", table.Name);
|
||||||
|
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ namespace TShockAPI.DB
|
||||||
{
|
{
|
||||||
try
|
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())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
return reader.Get<int>("ID");
|
return reader.Get<int>("ID");
|
||||||
|
|
@ -293,13 +293,13 @@ namespace TShockAPI.DB
|
||||||
object arg;
|
object arg;
|
||||||
if (account.ID != 0)
|
if (account.ID != 0)
|
||||||
{
|
{
|
||||||
query = "SELECT * FROM Users WHERE ID=@0";
|
query = $"SELECT * FROM Users WHERE {"ID".EscapeSqlId(_database)}=@0";
|
||||||
arg = account.ID;
|
arg = account.ID;
|
||||||
type = "id";
|
type = "id";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
query = "SELECT * FROM Users WHERE Username=@0";
|
query = $"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0";
|
||||||
arg = account.Name;
|
arg = account.Name;
|
||||||
type = "name";
|
type = "name";
|
||||||
}
|
}
|
||||||
|
|
@ -358,9 +358,9 @@ namespace TShockAPI.DB
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<UserAccount> accounts = new List<UserAccount>();
|
List<UserAccount> accounts = new List<UserAccount>();
|
||||||
string search = notAtStart ? string.Format("%{0}%", username) : string.Format("{0}%", username);
|
string search = $"{(notAtStart ? "%" : "")}{username}%";
|
||||||
using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0",
|
using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)} LIKE @0", search);
|
||||||
search);
|
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader));
|
accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader));
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ namespace TShockAPI.DB
|
||||||
{
|
{
|
||||||
Warps.Clear();
|
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());
|
Main.worldID.ToString());
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using MySql.Data.MySqlClient;
|
using MySql.Data.MySqlClient;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
|
@ -42,17 +43,17 @@ namespace TShockAPI.DB
|
||||||
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
|
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
|
||||||
public static int Query(this IDbConnection olddb, string query, params object[] args)
|
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();
|
com.AddParameter("@" + i, args[i] ?? DBNull.Value);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return com.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -271,6 +272,18 @@ namespace TShockAPI.DB
|
||||||
|
|
||||||
return (T)reader.GetValue(column);
|
return (T)reader.GetValue(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Escapes an identifier for use in a SQL query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The identifier to escape, typically a table or column name.</param>
|
||||||
|
/// <returns>The escaped identifier.</returns>
|
||||||
|
[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
|
public enum SqlType
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue