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