plugins/admin-sql-threaded.sp
author Scott Ehlert <ds@alliedmods.net>
Wed Aug 15 06:19:30 2007 +0000 (2007-08-15)
changeset 1330 ab94316bb52b
parent 1326 d24c8f8a2e07
child 1332 917ded8333d5
permissions -rw-r--r--
Updated license headers on virtually all files with extremely minor changes in the name of some sort of strange consistency.
All plugin and include file headers also have been changed to say about GPL3 instead of GPL2.

(This day shall henceforth be known as the Eighty Column Massacre of '07)
     1 /**
     2  * vim: set ts=4 :
     3  * =============================================================================
     4  * SourceMod SQL Admins Plugin (Threaded)
     5  * Fetches admins from an SQL database dynamically.
     6  *
     7  * SourceMod (C)2004-2007 AlliedModders LLC.  All rights reserved.
     8  * =============================================================================
     9  *
    10  * This program is free software; you can redistribute it and/or modify it under
    11  * the terms of the GNU General Public License, version 3.0, as published by the
    12  * Free Software Foundation.
    13  * 
    14  * This program is distributed in the hope that it will be useful, but WITHOUT
    15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    16  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    17  * details.
    18  *
    19  * You should have received a copy of the GNU General Public License along with
    20  * this program.  If not, see <http://www.gnu.org/licenses/>.
    21  *
    22  * As a special exception, AlliedModders LLC gives you permission to link the
    23  * code of this program (as well as its derivative works) to "Half-Life 2," the
    24  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    25  * by the Valve Corporation.  You must obey the GNU General Public License in
    26  * all respects for all other code used.  Additionally, AlliedModders LLC grants
    27  * this exception to all derivative works.  AlliedModders LLC defines further
    28  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    29  * or <http://www.sourcemod.net/license.php>.
    30  *
    31  * Version: $Id$
    32  */Version: $Id$
    33  */
    34 
    35 /* We like semicolons */
    36 #pragma semicolon 1
    37 
    38 #include <sourcemod>
    39 
    40 public Plugin:myinfo = 
    41 {
    42 	name = "SQL Admins (Threaded)",
    43 	author = "AlliedModders LLC",
    44 	description = "Reads admins from SQL dynamically",
    45 	version = SOURCEMOD_VERSION,
    46 	url = "http://www.sourcemod.net/"
    47 };
    48 
    49 /**
    50  * Notes:
    51  *
    52  * 1) All queries in here are high priority.  This is because the admin stuff 
    53  *    is very important.  Do not take this to mean that in your script, 
    54  *    everything should be high priority.  
    55  *
    56  * 2) All callbacks are locked with "sequence numbers."  This is to make sure 
    57  *    that multiple calls to sm_reloadadmins and the like do not make us 
    58  *    store the results from two or more callbacks accidentally.  Instead, we 
    59  *    check the sequence number in each callback with the current "allowed" 
    60  *    sequence number, and if it doesn't match, the callback is cancelled.
    61  *
    62  * 3) Sequence numbers for groups and overrides are not cleared unless there 
    63  *    was a 100% success in the fetch.  This is so we can potentially implement 
    64  *    connection retries in the future.
    65  *
    66  * 4) Sequence numbers for the user cache are ignored except for being 
    67  *    non-zero, which means players in-game should be re-checked for admin 
    68  *    powers.
    69  */
    70 
    71 new Handle:hDatabase = INVALID_HANDLE;			/** Database connection */
    72 new g_sequence = 0;								/** Global unique sequence number */
    73 new ConnectLock = 0;							/** Connect sequence number */
    74 new RebuildCachePart[3] = {0};					/** Cache part sequence numbers */
    75 new PlayerSeq[MAXPLAYERS+1];					/** Player-specific sequence numbers */
    76 new bool:PlayerAuth[MAXPLAYERS+1];				/** Whether a player has been "pre-authed" */
    77 
    78 #define _DEBUG
    79 
    80 public OnMapEnd()
    81 {
    82 	/**
    83 	 * Clean up on map end just so we can start a fresh connection when we need it later.
    84 	 */
    85 	if (hDatabase != INVALID_HANDLE)
    86 	{
    87 		CloseHandle(hDatabase);
    88 		hDatabase = INVALID_HANDLE;
    89 	}
    90 }
    91 
    92 public bool:OnClientConnect(client, String:rejectmsg[], maxlen)
    93 {
    94 	PlayerSeq[client] = 0;
    95 	PlayerAuth[client] = false;
    96 	return true;
    97 }
    98 
    99 public OnClientDisconnect(client)
   100 {
   101 	PlayerSeq[client] = 0;
   102 	PlayerAuth[client] = false;
   103 }
   104 
   105 public OnDatabaseConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
   106 {
   107 #if defined _DEBUG
   108 	PrintToServer("OnDatabaseConnect(%x,%x,%d) ConnectLock=%d", owner, hndl, data, ConnectLock);
   109 #endif
   110 
   111 	/**
   112 	 * If this happens to be an old connection request, ignore it.
   113 	 */
   114 	if (data != ConnectLock || hDatabase != INVALID_HANDLE)
   115 	{
   116 		if (hndl != INVALID_HANDLE)
   117 		{
   118 			CloseHandle(hndl);
   119 		}
   120 		return;
   121 	}
   122 	
   123 	ConnectLock = 0;
   124 	hDatabase = hndl;
   125 	
   126 	/**
   127 	 * See if the connection is valid.  If not, don't un-mark the caches
   128 	 * as needing rebuilding, in case the next connection request works.
   129 	 */
   130 	if (hDatabase == INVALID_HANDLE)
   131 	{
   132 		LogError("Failed to connect to database: %s", error);
   133 		return;
   134 	}
   135 	
   136 	/**
   137 	 * See if we need to get any of the cache stuff now.
   138 	 */
   139 	new sequence;
   140 	if ((sequence = RebuildCachePart[_:AdminCache_Overrides]) != 0)
   141 	{
   142 		FetchOverrides(hDatabase, sequence);
   143 	}
   144 	if ((sequence = RebuildCachePart[_:AdminCache_Groups]) != 0)
   145 	{
   146 		FetchGroups(hDatabase, sequence);
   147 	}
   148 	if ((sequence = RebuildCachePart[_:AdminCache_Admins]) != 0)
   149 	{
   150 		FetchUsersWeCan(hDatabase);
   151 	}
   152 }
   153 
   154 RequestDatabaseConnection()
   155 {
   156 	ConnectLock = ++g_sequence;
   157 	if (SQL_CheckConfig("admins"))
   158 	{
   159 		SQL_TConnect(OnDatabaseConnect, "admins", ConnectLock);
   160 	} else {
   161 		SQL_TConnect(OnDatabaseConnect, "default", ConnectLock);
   162 	}
   163 }
   164 
   165 public OnRebuildAdminCache(AdminCachePart:part)
   166 {
   167 	/**
   168 	 * Mark this part of the cache as being rebuilt.  This is used by the 
   169 	 * callback system to determine whether the results should still be 
   170 	 * used.
   171 	 */
   172 	new sequence = ++g_sequence;
   173 	RebuildCachePart[_:part] = sequence;
   174 	
   175 	/**
   176 	 * If we don't have a database connection, we can't do any lookups just yet.
   177 	 */
   178 	if (!hDatabase)
   179 	{
   180 		/**
   181 		 * Ask for a new connection if we need it.
   182 		 */
   183 		if (!ConnectLock)
   184 		{
   185 			RequestDatabaseConnection();
   186 		}
   187 		return;
   188 	}
   189 	
   190 	if (part == AdminCache_Overrides)
   191 	{
   192 		FetchOverrides(hDatabase, sequence);
   193 	} else if (part == AdminCache_Groups) {
   194 		FetchGroups(hDatabase, sequence);
   195 	} else if (part == AdminCache_Admins) {
   196 		FetchUsersWeCan(hDatabase);
   197 	}
   198 }
   199 
   200 public Action:OnClientPreAdminCheck(client)
   201 {
   202 	PlayerAuth[client] = true;
   203 	
   204 	/**
   205 	 * Play nice with other plugins.  If there's no database, don't delay the 
   206 	 * connection process.  Unfortunately, we can't attempt anything else and 
   207 	 * we just have to hope either the database is waiting or someone will type 
   208 	 * sm_reloadadmins.
   209 	 */
   210 	if (hDatabase == INVALID_HANDLE)
   211 	{
   212 		return Plugin_Continue;
   213 	}
   214 	
   215 	/**
   216 	 * Similarly, if the cache is in the process of being rebuilt, don't delay 
   217 	 * the user's normal connection flow.  The database will soon auth the user 
   218 	 * normally.
   219 	 */
   220 	if (RebuildCachePart[_:AdminCache_Admins] != 0)
   221 	{
   222 		return Plugin_Continue;
   223 	}
   224 	
   225 	/**
   226 	 * If someone has already assigned an admin ID (bad bad bad), don't 
   227 	 * bother waiting.
   228 	 */
   229 	if (GetUserAdmin(client) != INVALID_ADMIN_ID)
   230 	{
   231 		return Plugin_Continue;
   232 	}
   233 	
   234 	FetchUser(hDatabase, client);
   235 	
   236 	return Plugin_Handled;
   237 }
   238 
   239 public OnReceiveUserGroups(Handle:owner, Handle:hndl, const String:error[], any:data)
   240 {
   241 	new Handle:pk = Handle:data;
   242 	ResetPack(pk);
   243 	
   244 	new client = ReadPackCell(pk);
   245 	new sequence = ReadPackCell(pk);
   246 	
   247 	/**
   248 	 * Make sure it's the same client.
   249 	 */
   250 	if (PlayerSeq[client] != sequence)
   251 	{
   252 		CloseHandle(pk);
   253 		return;
   254 	}
   255 	
   256 	new AdminId:adm = AdminId:ReadPackCell(pk);
   257 	
   258 	/**
   259 	 * Someone could have sneakily changed the admin id while we waited.
   260 	 */
   261 	if (GetUserAdmin(client) != adm)
   262 	{
   263 		NotifyPostAdminCheck(client);
   264 		CloseHandle(pk);
   265 		return;
   266 	}
   267 	
   268 	/**
   269 	 * See if we got results.
   270 	 */
   271 	if (hndl == INVALID_HANDLE)
   272 	{
   273 		decl String:query[255];
   274 		ReadPackString(pk, query, sizeof(query));
   275 		LogError("SQL error receiving user: %s", error);
   276 		LogError("Query dump: %s", query);
   277 		NotifyPostAdminCheck(client);
   278 		CloseHandle(pk);
   279 		return;
   280 	}
   281 	
   282 	decl String:name[80];
   283 	new GroupId:gid;
   284 	
   285 	while (SQL_FetchRow(hndl))
   286 	{
   287 		SQL_FetchString(hndl, 0, name, sizeof(name));
   288 		
   289 		if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
   290 		{
   291 			continue;
   292 		}
   293 		
   294 #if defined _DEBUG
   295 		PrintToServer("Binding user group (%d, %d, %d, %s, %d)", client, sequence, adm, name, gid);
   296 #endif
   297 		
   298 		AdminInheritGroup(adm, gid);
   299 	}
   300 	
   301 	/**
   302 	 * We're DONE! Omg.
   303 	 */
   304 	NotifyPostAdminCheck(client);
   305 	CloseHandle(pk);
   306 }
   307 
   308 public OnReceiveUser(Handle:owner, Handle:hndl, const String:error[], any:data)
   309 {
   310 	new Handle:pk = Handle:data;
   311 	ResetPack(pk);
   312 	
   313 	new client = ReadPackCell(pk);
   314 	
   315 	/**
   316 	 * Check if this is the latest result request.
   317 	 */
   318 	new sequence = ReadPackCell(pk);
   319 	if (PlayerSeq[client] != sequence)
   320 	{
   321 		/* Discard everything, since we're out of sequence. */
   322 		CloseHandle(pk);
   323 		return;
   324 	}
   325 	
   326 	/**
   327 	 * If we need to use the results, make sure they succeeded.
   328 	 */
   329 	if (hndl == INVALID_HANDLE)
   330 	{
   331 		decl String:query[255];
   332 		ReadPackString(pk, query, sizeof(query));
   333 		LogError("SQL error receiving user: %s", error);
   334 		LogError("Query dump: %s", query);
   335 		RunAdminCacheChecks(client);
   336 		NotifyPostAdminCheck(client);
   337 		CloseHandle(pk);
   338 		return;
   339 	}
   340 	
   341 	new num_accounts = SQL_GetRowCount(hndl);
   342 	if (num_accounts == 0)
   343 	{
   344 		RunAdminCacheChecks(client);
   345 		NotifyPostAdminCheck(client);
   346 		CloseHandle(pk);
   347 		return;
   348 	}
   349 	
   350 	decl String:authtype[16];
   351 	decl String:identity[80];
   352 	decl String:password[80];
   353 	decl String:flags[32];
   354 	decl String:name[80];
   355 	new AdminId:adm, id;
   356 	
   357 	/**
   358 	 * Cache user info -- [0] = db id, [1] = cache id, [2] = groups
   359 	 */
   360 	decl user_lookup[num_accounts][3];
   361 	new total_users = 0;
   362 	
   363 	while (SQL_FetchRow(hndl))
   364 	{
   365 		id = SQL_FetchInt(hndl, 0);
   366 		SQL_FetchString(hndl, 1, authtype, sizeof(authtype));
   367 		SQL_FetchString(hndl, 2, identity, sizeof(identity));
   368 		SQL_FetchString(hndl, 3, password, sizeof(password));
   369 		SQL_FetchString(hndl, 4, flags, sizeof(flags));
   370 		SQL_FetchString(hndl, 5, name, sizeof(name));
   371 		
   372 		/* For dynamic admins we clear anything already in the cache. */
   373 		if ((adm = FindAdminByIdentity(authtype, identity)) != INVALID_ADMIN_ID)
   374 		{
   375 			RemoveAdmin(adm);
   376 		}
   377 		
   378 		adm = CreateAdmin(name);
   379 		if (!BindAdminIdentity(adm, authtype, identity))
   380 		{
   381 			LogError("Could not bind prefetched SQL admin (authtype \"%s\") (identity \"%s\")", authtype, identity);
   382 			continue;
   383 		}
   384 		
   385 		user_lookup[total_users][0] = id;
   386 		user_lookup[total_users][1] = _:adm;
   387 		user_lookup[total_users][2] = SQL_FetchInt(hndl, 6);
   388 		total_users++;
   389 		
   390 #if defined _DEBUG
   391 		PrintToServer("Found SQL admin (%d,%s,%s,%s,%s,%s):%d:%d", id, authtype, identity, password, flags, name, adm, user_lookup[total_users-1][2]);
   392 #endif
   393 		
   394 		/* See if this admin wants a password */
   395 		if (password[0] != '\0')
   396 		{
   397 			SetAdminPassword(adm, password);
   398 		}
   399 		
   400 		/* Apply each flag */
   401 		new len = strlen(flags);
   402 		new AdminFlag:flag;
   403 		for (new i=0; i<len; i++)
   404 		{
   405 			if (!FindFlagByChar(flags[i], flag))
   406 			{
   407 				continue;
   408 			}
   409 			SetAdminFlag(adm, flag, true);
   410 		}
   411 	}
   412 	
   413 	/**
   414 	 * Try binding the user.
   415 	 */	
   416 	new group_count = 0;
   417 	RunAdminCacheChecks(client);
   418 	adm = GetUserAdmin(client);
   419 	id = 0;
   420 	
   421 	
   422 	for (new i=0; i<total_users; i++)
   423 	{
   424 		if (user_lookup[i][1] == _:adm)
   425 		{
   426 			id = user_lookup[i][0];
   427 			group_count = user_lookup[i][2];
   428 			break;
   429 		}
   430 	}
   431 	
   432 #if defined _DEBUG
   433 	PrintToServer("Binding client (%d, %d) resulted in: (%d, %d, %d)", client, sequence, id, adm, group_count);
   434 #endif
   435 	
   436 	/**
   437 	 * If we can't verify that we assigned a database admin, or the user has no 
   438 	 * groups, don't bother doing anything.
   439 	 */
   440 	if (!id || !group_count)
   441 	{
   442 		NotifyPostAdminCheck(client);
   443 		CloseHandle(pk);
   444 		return;
   445 	}
   446 	
   447 	/**
   448 	 * The user has groups -- we need to fetch them!
   449 	 */
   450 	decl String:query[255];
   451 	Format(query, sizeof(query), "SELECT g.name FROM sm_admins_groups ag JOIN sm_groups g ON ag.group_id = g.id WHERE ag.admin_id = %d", id);
   452 	 
   453 	ResetPack(pk);
   454 	WritePackCell(pk, client);
   455 	WritePackCell(pk, sequence);
   456 	WritePackCell(pk, _:adm);
   457 	WritePackString(pk, query);
   458 	
   459 	SQL_TQuery(owner, OnReceiveUserGroups, query, pk, DBPrio_High);
   460 }
   461 
   462 FetchUser(Handle:db, client)
   463 {
   464 	decl String:name[65];
   465 	decl String:safe_name[140];
   466 	decl String:steamid[32];
   467 	decl String:ipaddr[24];
   468 	
   469 	/**
   470 	 * Get authentication information from the client.
   471 	 */
   472 	GetClientName(client, name, sizeof(name));
   473 	GetClientIP(client, ipaddr, sizeof(ipaddr));
   474 	
   475 	steamid[0] = '\0';
   476 	if (GetClientAuthString(client, steamid, sizeof(steamid)))
   477 	{
   478 		if (StrEqual(steamid, "STEAM_ID_LAN"))
   479 		{
   480 			steamid[0] = '\0';
   481 		}
   482 	}
   483 	
   484 	SQL_QuoteString(db, name, safe_name, sizeof(safe_name));
   485 	
   486 	/**
   487 	 * Construct the query using the information the user gave us.
   488 	 */
   489 	decl String:query[512];
   490 	new len = 0;
   491 	
   492 	len += Format(query[len], sizeof(query)-len, "SELECT a.id, a.authtype, a.identity, a.password, a.flags, a.name, COUNT(ag.group_id)");
   493 	len += Format(query[len], sizeof(query)-len, " FROM sm_admins a LEFT JOIN sm_admins_groups ag ON a.id = ag.admin_id WHERE ");
   494 	len += Format(query[len], sizeof(query)-len, " (a.authtype = 'ip' AND a.identity = '%s')", ipaddr);
   495 	len += Format(query[len], sizeof(query)-len, " OR (a.authtype = 'name' AND a.identity = '%s')", safe_name);
   496 	if (steamid[0] != '\0')
   497 	{
   498 		len += Format(query[len], sizeof(query)-len, " OR (a.authtype = 'steam' AND a.identity = '%s')", steamid);
   499 	}
   500 	len += Format(query[len], sizeof(query)-len, " GROUP BY a.id");
   501 	
   502 	/**
   503 	 * Send the actual query.
   504 	 */	
   505 	PlayerSeq[client] = ++g_sequence;
   506 	
   507 	new Handle:pk;
   508 	pk = CreateDataPack();
   509 	WritePackCell(pk, client);
   510 	WritePackCell(pk, PlayerSeq[client]);
   511 	WritePackString(pk, query);
   512 	
   513 #if defined _DEBUG
   514 	PrintToServer("Sending user query: %s", query);
   515 #endif
   516 	
   517 	SQL_TQuery(db, OnReceiveUser, query, pk, DBPrio_High);
   518 }
   519 
   520 FetchUsersWeCan(Handle:db)
   521 {
   522 	new max_clients = GetMaxClients();
   523 	
   524 	for (new i=1; i<=max_clients; i++)
   525 	{
   526 		if (PlayerAuth[i] && GetUserAdmin(i) == INVALID_ADMIN_ID)
   527 		{
   528 			FetchUser(db, i);
   529 		}
   530 	}
   531 	
   532 	/**
   533 	 * This round of updates is done.  Go in peace.
   534 	 */
   535 	RebuildCachePart[_:AdminCache_Admins] = 0;
   536 }
   537 
   538 
   539 public OnReceiveGroupImmunity(Handle:owner, Handle:hndl, const String:error[], any:data)
   540 {
   541 	new Handle:pk = Handle:data;
   542 	ResetPack(pk);
   543 	
   544 	/**
   545 	 * Check if this is the latest result request.
   546 	 */
   547 	new sequence = ReadPackCell(pk);
   548 	if (RebuildCachePart[_:AdminCache_Groups] != sequence)
   549 	{
   550 		/* Discard everything, since we're out of sequence. */
   551 		CloseHandle(pk);
   552 		return;
   553 	}
   554 	
   555 	/**
   556 	 * If we need to use the results, make sure they succeeded.
   557 	 */
   558 	if (hndl == INVALID_HANDLE)
   559 	{
   560 		decl String:query[255];
   561 		ReadPackString(pk, query, sizeof(query));		
   562 		LogError("SQL error receiving group immunity: %s", error);
   563 		LogError("Query dump: %s", query);
   564 		CloseHandle(pk);	
   565 		return;
   566 	}
   567 	
   568 	/* We're done with the pack forever. */
   569 	CloseHandle(pk);
   570 	
   571 	while (SQL_FetchRow(hndl))
   572 	{
   573 		decl String:group1[80];
   574 		decl String:group2[80];
   575 		new GroupId:gid1, GroupId:gid2;
   576 		
   577 		SQL_FetchString(hndl, 0, group1, sizeof(group1));
   578 		SQL_FetchString(hndl, 1, group2, sizeof(group2));
   579 		
   580 		if (((gid1 = FindAdmGroup(group1)) == INVALID_GROUP_ID)
   581 			|| (gid2 = FindAdmGroup(group2)) == INVALID_GROUP_ID)
   582 		{
   583 			continue;
   584 		}
   585 		
   586 		SetAdmGroupImmuneFrom(gid1, gid2);
   587 #if defined _DEBUG
   588 		PrintToServer("SetAdmGroupImmuneFrom(%d, %d)", gid1, gid2);
   589 #endif
   590 	}
   591 	
   592 	/* Clear the sequence so another connect doesn't refetch */
   593 	RebuildCachePart[_:AdminCache_Groups] = 0;
   594 }
   595 
   596 public OnReceiveGroupOverrides(Handle:owner, Handle:hndl, const String:error[], any:data)
   597 {
   598 	new Handle:pk = Handle:data;
   599 	ResetPack(pk);
   600 	
   601 	/**
   602 	 * Check if this is the latest result request.
   603 	 */
   604 	new sequence = ReadPackCell(pk);
   605 	if (RebuildCachePart[_:AdminCache_Groups] != sequence)
   606 	{
   607 		/* Discard everything, since we're out of sequence. */
   608 		CloseHandle(pk);
   609 		return;
   610 	}
   611 	
   612 	/**
   613 	 * If we need to use the results, make sure they succeeded.
   614 	 */
   615 	if (hndl == INVALID_HANDLE)
   616 	{
   617 		decl String:query[255];
   618 		ReadPackString(pk, query, sizeof(query));		
   619 		LogError("SQL error receiving group overrides: %s", error);
   620 		LogError("Query dump: %s", query);
   621 		CloseHandle(pk);	
   622 		return;
   623 	}
   624 	
   625 	/**
   626 	 * Fetch the overrides.
   627 	 */
   628 	decl String:name[80];
   629 	decl String:type[16];
   630 	decl String:command[64];
   631 	decl String:access[16];
   632 	new GroupId:gid;
   633 	while (SQL_FetchRow(hndl))
   634 	{
   635 		SQL_FetchString(hndl, 0, name, sizeof(name));
   636 		SQL_FetchString(hndl, 1, type, sizeof(type));
   637 		SQL_FetchString(hndl, 2, command, sizeof(command));
   638 		SQL_FetchString(hndl, 3, access, sizeof(access));
   639 		
   640 		/* Find the group.  This is actually faster than doing the ID lookup. */
   641 		if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
   642 		{
   643 			/* Oh well, just ignore it. */
   644 			continue;
   645 		}
   646 		
   647 		new OverrideType:o_type = Override_Command;
   648 		if (StrEqual(type, "group"))
   649 		{
   650 			o_type = Override_CommandGroup;
   651 		}
   652 				
   653 		new OverrideRule:o_rule = Command_Deny;
   654 		if (StrEqual(access, "allow"))
   655 		{
   656 			o_rule = Command_Allow;
   657 		}
   658 				
   659 #if defined _DEBUG
   660 		PrintToServer("AddAdmGroupCmdOverride(%d, %s, %d, %d)", gid, command, o_type, o_rule);
   661 #endif
   662 				
   663 		AddAdmGroupCmdOverride(gid, command, o_type, o_rule);
   664 	}
   665 	
   666 	/**
   667 	 * It's time to get the group immunity list.
   668 	 */
   669 	new len = 0;
   670 	decl String:query[256];
   671 	len += Format(query[len], sizeof(query)-len, "SELECT g1.name, g2.name FROM sm_group_immunity gi");
   672 	len += Format(query[len], sizeof(query)-len, " LEFT JOIN sm_groups g1 ON g1.id = gi.group_id ");
   673 	len += Format(query[len], sizeof(query)-len, " LEFT JOIN sm_groups g2 ON g2.id = gi.other_id");
   674 
   675 	ResetPack(pk);
   676 	WritePackCell(pk, sequence);
   677 	WritePackString(pk, query);
   678 	
   679 	SQL_TQuery(owner, OnReceiveGroupImmunity, query, pk, DBPrio_High);
   680 }
   681 
   682 public OnReceiveGroups(Handle:owner, Handle:hndl, const String:error[], any:data)
   683 {
   684 	new Handle:pk = Handle:data;
   685 	ResetPack(pk);
   686 	
   687 	/**
   688 	 * Check if this is the latest result request.
   689 	 */
   690 	new sequence = ReadPackCell(pk);
   691 	if (RebuildCachePart[_:AdminCache_Groups] != sequence)
   692 	{
   693 		/* Discard everything, since we're out of sequence. */
   694 		CloseHandle(pk);
   695 		return;
   696 	}
   697 	
   698 	/**
   699 	 * If we need to use the results, make sure they succeeded.
   700 	 */
   701 	if (hndl == INVALID_HANDLE)
   702 	{
   703 		decl String:query[255];
   704 		ReadPackString(pk, query, sizeof(query));
   705 		LogError("SQL error receiving groups: %s", error);
   706 		LogError("Query dump: %s", query);
   707 		CloseHandle(pk);
   708 		return;
   709 	}
   710 	
   711 	/**
   712 	 * Now start fetching groups.
   713 	 */
   714 	decl String:immunity[16];
   715 	decl String:flags[32];
   716 	decl String:name[128];
   717 	while (SQL_FetchRow(hndl))
   718 	{
   719 		SQL_FetchString(hndl, 0, immunity, sizeof(immunity));
   720 		SQL_FetchString(hndl, 1, flags, sizeof(flags));
   721 		SQL_FetchString(hndl, 2, name, sizeof(name));
   722 		
   723 #if defined _DEBUG
   724 		PrintToServer("Adding group (%s, %s, %s)", immunity, flags, name);
   725 #endif
   726 		
   727 		/* Find or create the group */
   728 		new GroupId:gid;
   729 		if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
   730 		{
   731 			gid = CreateAdmGroup(name);
   732 		}
   733 		
   734 		/* Add flags from the database to the group */
   735 		new num_flag_chars = strlen(flags);
   736 		for (new i=0; i<num_flag_chars; i++)
   737 		{
   738 			decl AdminFlag:flag;
   739 			if (!FindFlagByChar(flags[i], flag))
   740 			{
   741 				continue;
   742 			}
   743 			SetAdmGroupAddFlag(gid, flag, true);
   744 		}
   745 		
   746 		/* Set the immunity level this group has */
   747 		if (StrEqual(immunity, "all"))
   748 		{
   749 			SetAdmGroupImmunity(gid, Immunity_Default, true);
   750 			SetAdmGroupImmunity(gid, Immunity_Global, true);
   751 		} else if (StrEqual(immunity, "default")) {
   752 			SetAdmGroupImmunity(gid, Immunity_Default, true);
   753 		} else if (StrEqual(immunity, "global")) {
   754 			SetAdmGroupImmunity(gid, Immunity_Global, true);
   755 		}
   756 	}
   757 	
   758 	/**
   759 	 * It's time to get the group override list.
   760 	 */
   761 	decl String:query[255];
   762 	Format(query, 
   763 		sizeof(query), 
   764 		"SELECT g.name, og.type, og.name, og.access FROM sm_group_overrides og JOIN sm_groups g ON og.group_id = g.id ORDER BY g.id DESC");
   765 
   766 	ResetPack(pk);
   767 	WritePackCell(pk, sequence);
   768 	WritePackString(pk, query);
   769 	
   770 	SQL_TQuery(owner, OnReceiveGroupOverrides, query, pk, DBPrio_High);
   771 }
   772 
   773 FetchGroups(Handle:db, sequence)
   774 {
   775 	decl String:query[255];
   776 	new Handle:pk;
   777 	
   778 	Format(query, sizeof(query), "SELECT immunity, flags, name FROM sm_groups");
   779 
   780 	pk = CreateDataPack();
   781 	WritePackCell(pk, sequence);
   782 	WritePackString(pk, query);
   783 	
   784 	SQL_TQuery(db, OnReceiveGroups, query, pk, DBPrio_High);
   785 }
   786 
   787 public OnReceiveOverrides(Handle:owner, Handle:hndl, const String:error[], any:data)
   788 {
   789 	new Handle:pk = Handle:data;
   790 	ResetPack(pk);
   791 	
   792 	/**
   793 	 * Check if this is the latest result request.
   794 	 */
   795 	new sequence = ReadPackCell(pk);
   796 	if (RebuildCachePart[_:AdminCache_Overrides] != sequence)
   797 	{
   798 		/* Discard everything, since we're out of sequence. */
   799 		CloseHandle(pk);
   800 		return;
   801 	}
   802 	
   803 	/**
   804 	 * If we need to use the results, make sure they succeeded.
   805 	 */
   806 	if (hndl == INVALID_HANDLE)
   807 	{
   808 		decl String:query[255];
   809 		ReadPackString(pk, query, sizeof(query));
   810 		LogError("SQL error receiving overrides: %s", error);
   811 		LogError("Query dump: %s", query);
   812 		CloseHandle(pk);
   813 		return;
   814 	}
   815 	
   816 	/**
   817 	 * We're done with you, now.
   818 	 */
   819 	CloseHandle(pk);
   820 	
   821 	decl String:type[64];
   822 	decl String:name[64];
   823 	decl String:flags[32];
   824 	new flag_bits;
   825 	while (SQL_FetchRow(hndl))
   826 	{
   827 		SQL_FetchString(hndl, 0, type, sizeof(type));
   828 		SQL_FetchString(hndl, 1, name, sizeof(name));
   829 		SQL_FetchString(hndl, 2, flags, sizeof(flags));
   830 		
   831 #if defined _DEBUG
   832 		PrintToServer("Adding override (%s, %s, %s)", type, name, flags);
   833 #endif
   834 		
   835 		flag_bits = ReadFlagString(flags);
   836 		if (StrEqual(type, "command"))
   837 		{
   838 			AddCommandOverride(name, Override_Command, flag_bits);
   839 		} else if (StrEqual(type, "group")) {
   840 			AddCommandOverride(name, Override_CommandGroup, flag_bits);
   841 		}
   842 	}
   843 	
   844 	/* Clear the sequence so another connect doesn't refetch */
   845 	RebuildCachePart[_:AdminCache_Overrides] = 0;
   846 }
   847 
   848 FetchOverrides(Handle:db, sequence)
   849 {
   850 	decl String:query[255];
   851 	new Handle:pk;
   852 	
   853 	Format(query, sizeof(query), "SELECT type, name, flags FROM sm_overrides");
   854 
   855 	pk = CreateDataPack();
   856 	WritePackCell(pk, sequence);
   857 	WritePackString(pk, query);
   858 	
   859 	SQL_TQuery(db, OnReceiveOverrides, query, pk, DBPrio_High);
   860 }
   861 
   862 /**
   863  * Finds an entry in a temporary group list (used by immunity resolver).
   864  * 
   865  * @param group_list		Array(3,n) (0=db id, 1=gid, 2=immunity list)
   866  * @param id				group database id
   867  * @return					GroupId of the database id or INVALID_GROUP_ID.
   868  */
   869 stock GroupId:UTIL_FindGroupInCache(Handle:group_list, id)
   870 {
   871 	new size = GetArraySize(group_list);
   872 	
   873 	for (new i=0; i<size; i++)
   874 	{
   875 		decl data[3];
   876 		GetArrayArray(group_list, i, data);
   877 		
   878 		if (data[0] == id)
   879 		{
   880 			return GroupId:data[1];
   881 		}
   882 	}
   883 	
   884 	return INVALID_GROUP_ID;
   885 }
   886 
   887 /**
   888  * Breaks a comma-delimited string into a list of numbers, returning a new 
   889  * dynamic array.
   890  *
   891  * @param text				The string to split.
   892  * @return					A new array Handle which must be closed, or 
   893  *							INVALID_HANDLE on no strings found.
   894  */
   895 stock Handle:UTIL_ExplodeNumberString(const String:text[])
   896 {
   897 	new Handle:array = INVALID_HANDLE;
   898 	decl String:buffer[32];
   899 	new reloc_idx, idx;
   900 	
   901 	while ((idx = SplitString(text[reloc_idx], ",", buffer, sizeof(buffer))) != -1)
   902 	{
   903 		reloc_idx += idx;
   904 		if (text[reloc_idx] == '\0')
   905 		{
   906 			break;
   907 		}
   908 		if (array == INVALID_HANDLE)
   909 		{
   910 			array = CreateArray();
   911 		}
   912 		PushArrayCell(array, StringToInt(buffer));
   913 	}
   914 	
   915 	if (text[reloc_idx] != '\0')
   916 	{
   917 		if (array == INVALID_HANDLE)
   918 		{
   919 			array = CreateArray();
   920 		}
   921 		PushArrayCell(array, StringToInt(text[reloc_idx]));
   922 	}
   923 	
   924 	return array;
   925 }
   926