// Copyright (C) 2007 EQ2EMulator Development Team - GPL v3 License #pragma once #include #include #include #include #include #include #include #include #include "database_result.hpp" #include "../types.hpp" #include "../misc_functions.hpp" #include "../log.hpp" using namespace std; // Database configuration file names based on build target #ifdef LOGIN #define DB_INI_FILE "login_db.ini" #elif defined WORLD #define DB_INI_FILE "world_db.ini" #elif defined PARSER #define DB_INI_FILE "parser_db.ini" #elif defined PATCHER #define DB_INI_FILE "patcher_db.ini" #endif // Initial buffer size for query allocation #define QUERY_INITIAL_SIZE 512 // Core database class providing MySQL connection management and query execution class DatabaseCore { public: // Database connection status enumeration enum eStatus { Closed, Connected, Error }; // Constructor - initializes MySQL connection and member variables DatabaseCore() { mysql_init(&mysql); int timeout = 10; mysql_options(&mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); pHost = nullptr; pPort = 0; pUser = nullptr; pPassword = nullptr; pDatabase = nullptr; pCompress = false; pSSL = false; pStatus = Closed; MMysql.SetName("DatabaseCore::mysql"); } // Destructor - closes connection and frees allocated memory virtual ~DatabaseCore() { pStatus = Closed; mysql_close(&mysql); #if MYSQL_VERSION_ID >= 50003 mysql_library_end(); #else mysql_server_end(); #endif safe_delete_array(pHost); safe_delete_array(pUser); safe_delete_array(pPassword); safe_delete_array(pDatabase); } // Get current database connection status eStatus GetStatus() { return pStatus; } // Get MySQL error number from last operation unsigned int GetError() { return mysql_errno(&mysql); } // Get MySQL error message from last operation const char* GetErrorMsg() { return mysql_error(&mysql); } // Connect to database using configuration file settings bool Connect() { char line[256], *key, *val; char host[256], user[64], password[64], database[64], port[64]; bool found_section = false; FILE* f; if ((f = fopen(DB_INI_FILE, "r")) == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Unable to read %s\n", DB_INI_FILE); return false; } memset(host, 0, sizeof(host)); memset(user, 0, sizeof(user)); memset(password, 0, sizeof(password)); memset(database, 0, sizeof(database)); memset(port, 0, sizeof(port)); while (fgets(line, sizeof(line), f) != NULL) { if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue; if (!found_section) { if (strncasecmp(line, "[Database]", 10) == 0) found_section = true; } else { if ((key = strtok(line, "=")) != NULL) { if ((val = strtok(NULL, "\r\n")) != NULL) { if (strncasecmp(line, "host", 4) == 0) strncpy(host, val, sizeof(host) - 1); else if (strncasecmp(line, "user", 4) == 0) strncpy(user, val, sizeof(user) - 1); else if (strncasecmp(line, "password", 8) == 0) strncpy(password, val, sizeof(password) - 1); else if (strncasecmp(line, "database", 8) == 0) strncpy(database, val, sizeof(database) - 1); else if (strncasecmp(line, "port", 4) == 0) strncpy(port, val, sizeof(port) - 1); } } } } fclose(f); if (host[0] == '\0') { LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'host' in '%s'\n", DB_INI_FILE); return false; } if (user[0] == '\0') { LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'user' in '%s'\n", DB_INI_FILE); return false; } if (password[0] == '\0') { LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'password' in '%s'\n", DB_INI_FILE); return false; } if (database[0] == '\0') { LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'database' in '%s'\n", DB_INI_FILE); return false; } unsigned int portnum = atoul(port); return Connect(host, user, password, database, portnum); } // Connect to database with explicit connection parameters bool Connect(const char* host, const char* user, const char* password, const char* database, unsigned int port = 3306) { LockMutex lock(&MMysql); safe_delete_array(pHost); safe_delete_array(pUser); safe_delete_array(pPassword); safe_delete_array(pDatabase); pHost = new char[strlen(host) + 1]; strcpy(pHost, host); pUser = new char[strlen(user) + 1]; strcpy(pUser, user); pPassword = new char[strlen(password) + 1]; strcpy(pPassword, password); pDatabase = new char[strlen(database) + 1]; strcpy(pDatabase, database); pPort = port; if (mysql_real_connect(&mysql, pHost, pUser, pPassword, pDatabase, pPort, NULL, CLIENT_FOUND_ROWS) == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Unable to connect to MySQL server at %s:%u: %s\n", host, port, mysql_error(&mysql)); pStatus = Error; return false; } pStatus = Connected; return true; } // Comprehensive query execution method used by Query class and async operations bool RunQuery(const char* query, int32 querylen, char* errbuf = 0, MYSQL_RES** result = 0, int32* affected_rows = 0, int32* last_insert_id = 0, int32* errnum = 0, bool retry = true) { if (errnum) *errnum = 0; if (errbuf) errbuf[0] = 0; bool ret = false; LockMutex lock(&MMysql); if (pStatus != Connected) Connect(); LogWrite(DATABASE__QUERY, 0, "Database", query); if (mysql_real_query(&mysql, query, querylen)) { if (mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) pStatus = Error; if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) { if (retry) { LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover..."); ret = RunQuery(query, querylen, errbuf, result, affected_rows, last_insert_id, errnum, false); } else { pStatus = Error; if (errnum) *errnum = mysql_errno(&mysql); if (errbuf) snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql)); LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query); ret = false; } } else { if (errnum) *errnum = mysql_errno(&mysql); if (errbuf) snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql)); LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query); ret = false; } } else { if (result && mysql_field_count(&mysql)) { *result = mysql_store_result(&mysql); } else if (result) *result = 0; if (affected_rows) *affected_rows = mysql_affected_rows(&mysql); if (last_insert_id) *last_insert_id = mysql_insert_id(&mysql); if (result) { if (*result) { ret = true; } else { if (errnum) *errnum = UINT_MAX; if (errbuf) { if ((!affected_rows || (affected_rows && *affected_rows == 0)) && (!last_insert_id || (last_insert_id && *last_insert_id == 0))) LogWrite(DATABASE__RESULT, 1, "Database", "No Result."); } ret = false; } } else { ret = true; } } if (ret) { char tmp1[200] = { 0 }; char tmp2[200] = { 0 }; if (result && (*result)) snprintf(tmp1, sizeof(tmp1), ", %i rows returned", (int)mysql_num_rows(*result)); if (affected_rows) snprintf(tmp2, sizeof(tmp2), ", %i rows affected", (*affected_rows)); LogWrite(DATABASE__DEBUG, 0, "Database", "Query Successful%s%s", tmp1, tmp2); } else LogWrite(DATABASE__DEBUG, 0, "Database", "Query returned no results in %s!\n%s", __FUNCTION__, query); return ret; } // Execute non-SELECT query with printf-style formatting and automatic reconnection bool Query(const char* query, ...) { char* buf; size_t size = QUERY_INITIAL_SIZE; int num_chars; va_list args; bool ret = true; LockMutex lock(&MMysql); while (true) { if ((buf = (char*)malloc(size)) == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size); return false; } va_start(args, query); num_chars = vsnprintf(buf, size, query, args); va_end(args); if (num_chars > -1 && (size_t)num_chars < size) break; if (num_chars > -1) size = num_chars + 1; else size *= 2; free(buf); } if (pStatus != Connected) Connect(); LogWrite(DATABASE__QUERY, 0, "Database", query); if (mysql_real_query(&mysql, buf, num_chars) != 0) { if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) { LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query..."); Connect(); if (mysql_real_query(&mysql, buf, num_chars) != 0) { ret = false; } } else if (!IsIgnoredErrno(mysql_errno(&mysql))) { LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf); ret = false; } } free(buf); return ret; } // Execute SELECT query and store results in DatabaseResult object bool Select(DatabaseResult* result, const char* query, ...) { char* buf; size_t size = QUERY_INITIAL_SIZE; int num_chars; va_list args; MYSQL_RES* res; bool ret = true; LockMutex lock(&MMysql); while (true) { if ((buf = (char*)malloc(size)) == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size); return false; } va_start(args, query); num_chars = vsnprintf(buf, size, query, args); va_end(args); if (num_chars > -1 && (size_t)num_chars < size) break; if (num_chars > -1) size = num_chars + 1; else size *= 2; free(buf); } if (pStatus != Connected) Connect(); if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) { if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) { LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query..."); mysql_close(&mysql); Connect(); if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) { ret = false; } } else if (!IsIgnoredErrno(mysql_errno(&mysql))) { LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf); ret = false; } } if (ret && !IsIgnoredErrno(mysql_errno(&mysql))) { res = mysql_store_result(&mysql); if (res != NULL) { uint8 num_rows = mysql_affected_rows(&mysql); uint8 num_fields = mysql_field_count(&mysql); ret = result->StoreResult(res, num_fields, num_rows); } else { LogWrite(DATABASE__ERROR, 0, "Database", "Error storing MySql query result (%d): %s\n%s", mysql_errno(&mysql), mysql_error(&mysql), buf); ret = false; } } free(buf); return ret; } // Get the auto-increment ID from the last INSERT operation int32 LastInsertID() { return (int32)mysql_insert_id(&mysql); } // Get number of rows affected by the last operation long AffectedRows() { return mysql_affected_rows(&mysql); } // Escape string for safe SQL usage - caller must free() returned pointer char* Escape(const char* str, size_t len) { char* buf = (char*)malloc(len * 2 + 1); if (buf == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__); return NULL; } mysql_real_escape_string(&mysql, buf, str, len); return buf; } // Escape null-terminated string for safe SQL usage - caller must free() returned pointer char* Escape(const char* str) { return Escape(str, strlen(str)); } // Escape string and return as std::string - no manual memory management required string EscapeStr(const char* str, size_t len) { char* buf = (char*)malloc(len * 2 + 1); string ret; if (buf == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__); return ret; } mysql_real_escape_string(&mysql, buf, str, len); ret.append(buf); free(buf); return ret; } // Escape null-terminated string and return as std::string string EscapeStr(const char* str) { return EscapeStr(str, strlen(str)); } // Escape std::string and return as std::string string EscapeStr(string str) { return EscapeStr(str.c_str(), str.length()); } // Legacy function for compatibility - use EscapeStr instead string getSafeEscapeString(const char* from_string) { return EscapeStr(from_string); } // Execute multiple SQL statements from a file with proper result processing bool QueriesFromFile(const char* file) { bool success = true; long size; char* buf; int ret; MYSQL_RES* res; FILE* f; f = fopen(file, "rb"); if (f == NULL) { LogWrite(DATABASE__ERROR, 0, "Database", "Unable to open '%s' for reading: %s", file, strerror(errno)); return false; } fseek(f, 0, SEEK_END); size = ftell(f); fseek(f, 0, SEEK_SET); buf = (char*)malloc(size + 1); if (buf == NULL) { fclose(f); LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", size + 1, __FUNCTION__, __LINE__); return false; } if (fread(buf, sizeof(*buf), size, f) != (size_t)size) { LogWrite(DATABASE__ERROR, 0, "Database", "Failed to read from '%s': %s", file, strerror(errno)); fclose(f); free(buf); return false; } buf[size] = '\0'; fclose(f); mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_ON); ret = mysql_real_query(&mysql, buf, size); free(buf); if (ret != 0) { LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql)); success = false; } else { do { res = mysql_store_result(&mysql); if (res != NULL) mysql_free_result(res); ret = mysql_next_result(&mysql); if (ret > 0) { LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql)); success = false; } } while (ret == 0); } mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_OFF); return success; } // Add MySQL error number to ignored list for suppressing specific error logging void SetIgnoredErrno(unsigned int db_errno) { for (auto& errno_val : ignored_errnos) { if (errno_val == db_errno) return; } ignored_errnos.push_back(db_errno); } // Remove MySQL error number from ignored list void RemoveIgnoredErrno(unsigned int db_errno) { for (auto itr = ignored_errnos.begin(); itr != ignored_errnos.end(); itr++) { if ((*itr) == db_errno) { ignored_errnos.erase(itr); break; } } } // Check if MySQL error number should be ignored for logging bool IsIgnoredErrno(unsigned int db_errno) { for (auto& errno_val : ignored_errnos) { if (errno_val == db_errno) return true; } return false; } // Send keepalive ping to MySQL server and handle connection errors void Ping() { if (!MMysql.trylock()) { return; // If locked, someone's using it and it doesn't need a keepalive } mysql_ping(&mysql); int32 errnum = mysql_errno(&mysql); switch (errnum) { case CR_COMMANDS_OUT_OF_SYNC: case CR_SERVER_GONE_ERROR: case CR_UNKNOWN_ERROR: { LogWrite(DATABASE__ERROR, 0, "Database", "[Database] We lost connection to the database., errno: %i", errnum); break; } } MMysql.unlock(); } protected: MYSQL mysql; // MySQL connection handle Mutex MMysql; // Thread safety mutex for database operations eStatus pStatus; // Current connection status char* pHost; // Database host address char* pUser; // Database username char* pPassword; // Database password char* pDatabase; // Database name bool pCompress; // Enable compression flag int32 pPort; // Database port number bool pSSL; // Enable SSL flag vector ignored_errnos; // List of MySQL error numbers to ignore in logging };