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)
3 * =============================================================================
4 * SourceMod SQL Admins Plugin (Threaded)
5 * Fetches admins from an SQL database dynamically.
7 * SourceMod (C)2004-2007 AlliedModders LLC. All rights reserved.
8 * =============================================================================
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.
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
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/>.
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>.
35 /* We like semicolons */
40 public Plugin:myinfo =
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/"
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.
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.
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.
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
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" */
83 * Clean up on map end just so we can start a fresh connection when we need it later.
85 if (hDatabase != INVALID_HANDLE)
87 CloseHandle(hDatabase);
88 hDatabase = INVALID_HANDLE;
92 public bool:OnClientConnect(client, String:rejectmsg[], maxlen)
94 PlayerSeq[client] = 0;
95 PlayerAuth[client] = false;
99 public OnClientDisconnect(client)
101 PlayerSeq[client] = 0;
102 PlayerAuth[client] = false;
105 public OnDatabaseConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
108 PrintToServer("OnDatabaseConnect(%x,%x,%d) ConnectLock=%d", owner, hndl, data, ConnectLock);
112 * If this happens to be an old connection request, ignore it.
114 if (data != ConnectLock || hDatabase != INVALID_HANDLE)
116 if (hndl != INVALID_HANDLE)
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.
130 if (hDatabase == INVALID_HANDLE)
132 LogError("Failed to connect to database: %s", error);
137 * See if we need to get any of the cache stuff now.
140 if ((sequence = RebuildCachePart[_:AdminCache_Overrides]) != 0)
142 FetchOverrides(hDatabase, sequence);
144 if ((sequence = RebuildCachePart[_:AdminCache_Groups]) != 0)
146 FetchGroups(hDatabase, sequence);
148 if ((sequence = RebuildCachePart[_:AdminCache_Admins]) != 0)
150 FetchUsersWeCan(hDatabase);
154 RequestDatabaseConnection()
156 ConnectLock = ++g_sequence;
157 if (SQL_CheckConfig("admins"))
159 SQL_TConnect(OnDatabaseConnect, "admins", ConnectLock);
161 SQL_TConnect(OnDatabaseConnect, "default", ConnectLock);
165 public OnRebuildAdminCache(AdminCachePart:part)
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
172 new sequence = ++g_sequence;
173 RebuildCachePart[_:part] = sequence;
176 * If we don't have a database connection, we can't do any lookups just yet.
181 * Ask for a new connection if we need it.
185 RequestDatabaseConnection();
190 if (part == AdminCache_Overrides)
192 FetchOverrides(hDatabase, sequence);
193 } else if (part == AdminCache_Groups) {
194 FetchGroups(hDatabase, sequence);
195 } else if (part == AdminCache_Admins) {
196 FetchUsersWeCan(hDatabase);
200 public Action:OnClientPreAdminCheck(client)
202 PlayerAuth[client] = true;
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
210 if (hDatabase == INVALID_HANDLE)
212 return Plugin_Continue;
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
220 if (RebuildCachePart[_:AdminCache_Admins] != 0)
222 return Plugin_Continue;
226 * If someone has already assigned an admin ID (bad bad bad), don't
229 if (GetUserAdmin(client) != INVALID_ADMIN_ID)
231 return Plugin_Continue;
234 FetchUser(hDatabase, client);
236 return Plugin_Handled;
239 public OnReceiveUserGroups(Handle:owner, Handle:hndl, const String:error[], any:data)
241 new Handle:pk = Handle:data;
244 new client = ReadPackCell(pk);
245 new sequence = ReadPackCell(pk);
248 * Make sure it's the same client.
250 if (PlayerSeq[client] != sequence)
256 new AdminId:adm = AdminId:ReadPackCell(pk);
259 * Someone could have sneakily changed the admin id while we waited.
261 if (GetUserAdmin(client) != adm)
263 NotifyPostAdminCheck(client);
269 * See if we got results.
271 if (hndl == INVALID_HANDLE)
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);
282 decl String:name[80];
285 while (SQL_FetchRow(hndl))
287 SQL_FetchString(hndl, 0, name, sizeof(name));
289 if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
295 PrintToServer("Binding user group (%d, %d, %d, %s, %d)", client, sequence, adm, name, gid);
298 AdminInheritGroup(adm, gid);
304 NotifyPostAdminCheck(client);
308 public OnReceiveUser(Handle:owner, Handle:hndl, const String:error[], any:data)
310 new Handle:pk = Handle:data;
313 new client = ReadPackCell(pk);
316 * Check if this is the latest result request.
318 new sequence = ReadPackCell(pk);
319 if (PlayerSeq[client] != sequence)
321 /* Discard everything, since we're out of sequence. */
327 * If we need to use the results, make sure they succeeded.
329 if (hndl == INVALID_HANDLE)
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);
341 new num_accounts = SQL_GetRowCount(hndl);
342 if (num_accounts == 0)
344 RunAdminCacheChecks(client);
345 NotifyPostAdminCheck(client);
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];
358 * Cache user info -- [0] = db id, [1] = cache id, [2] = groups
360 decl user_lookup[num_accounts][3];
363 while (SQL_FetchRow(hndl))
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));
372 /* For dynamic admins we clear anything already in the cache. */
373 if ((adm = FindAdminByIdentity(authtype, identity)) != INVALID_ADMIN_ID)
378 adm = CreateAdmin(name);
379 if (!BindAdminIdentity(adm, authtype, identity))
381 LogError("Could not bind prefetched SQL admin (authtype \"%s\") (identity \"%s\")", authtype, identity);
385 user_lookup[total_users][0] = id;
386 user_lookup[total_users][1] = _:adm;
387 user_lookup[total_users][2] = SQL_FetchInt(hndl, 6);
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]);
394 /* See if this admin wants a password */
395 if (password[0] != '\0')
397 SetAdminPassword(adm, password);
400 /* Apply each flag */
401 new len = strlen(flags);
403 for (new i=0; i<len; i++)
405 if (!FindFlagByChar(flags[i], flag))
409 SetAdminFlag(adm, flag, true);
414 * Try binding the user.
417 RunAdminCacheChecks(client);
418 adm = GetUserAdmin(client);
422 for (new i=0; i<total_users; i++)
424 if (user_lookup[i][1] == _:adm)
426 id = user_lookup[i][0];
427 group_count = user_lookup[i][2];
433 PrintToServer("Binding client (%d, %d) resulted in: (%d, %d, %d)", client, sequence, id, adm, group_count);
437 * If we can't verify that we assigned a database admin, or the user has no
438 * groups, don't bother doing anything.
440 if (!id || !group_count)
442 NotifyPostAdminCheck(client);
448 * The user has groups -- we need to fetch them!
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);
454 WritePackCell(pk, client);
455 WritePackCell(pk, sequence);
456 WritePackCell(pk, _:adm);
457 WritePackString(pk, query);
459 SQL_TQuery(owner, OnReceiveUserGroups, query, pk, DBPrio_High);
462 FetchUser(Handle:db, client)
464 decl String:name[65];
465 decl String:safe_name[140];
466 decl String:steamid[32];
467 decl String:ipaddr[24];
470 * Get authentication information from the client.
472 GetClientName(client, name, sizeof(name));
473 GetClientIP(client, ipaddr, sizeof(ipaddr));
476 if (GetClientAuthString(client, steamid, sizeof(steamid)))
478 if (StrEqual(steamid, "STEAM_ID_LAN"))
484 SQL_QuoteString(db, name, safe_name, sizeof(safe_name));
487 * Construct the query using the information the user gave us.
489 decl String:query[512];
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')
498 len += Format(query[len], sizeof(query)-len, " OR (a.authtype = 'steam' AND a.identity = '%s')", steamid);
500 len += Format(query[len], sizeof(query)-len, " GROUP BY a.id");
503 * Send the actual query.
505 PlayerSeq[client] = ++g_sequence;
508 pk = CreateDataPack();
509 WritePackCell(pk, client);
510 WritePackCell(pk, PlayerSeq[client]);
511 WritePackString(pk, query);
514 PrintToServer("Sending user query: %s", query);
517 SQL_TQuery(db, OnReceiveUser, query, pk, DBPrio_High);
520 FetchUsersWeCan(Handle:db)
522 new max_clients = GetMaxClients();
524 for (new i=1; i<=max_clients; i++)
526 if (PlayerAuth[i] && GetUserAdmin(i) == INVALID_ADMIN_ID)
533 * This round of updates is done. Go in peace.
535 RebuildCachePart[_:AdminCache_Admins] = 0;
539 public OnReceiveGroupImmunity(Handle:owner, Handle:hndl, const String:error[], any:data)
541 new Handle:pk = Handle:data;
545 * Check if this is the latest result request.
547 new sequence = ReadPackCell(pk);
548 if (RebuildCachePart[_:AdminCache_Groups] != sequence)
550 /* Discard everything, since we're out of sequence. */
556 * If we need to use the results, make sure they succeeded.
558 if (hndl == INVALID_HANDLE)
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);
568 /* We're done with the pack forever. */
571 while (SQL_FetchRow(hndl))
573 decl String:group1[80];
574 decl String:group2[80];
575 new GroupId:gid1, GroupId:gid2;
577 SQL_FetchString(hndl, 0, group1, sizeof(group1));
578 SQL_FetchString(hndl, 1, group2, sizeof(group2));
580 if (((gid1 = FindAdmGroup(group1)) == INVALID_GROUP_ID)
581 || (gid2 = FindAdmGroup(group2)) == INVALID_GROUP_ID)
586 SetAdmGroupImmuneFrom(gid1, gid2);
588 PrintToServer("SetAdmGroupImmuneFrom(%d, %d)", gid1, gid2);
592 /* Clear the sequence so another connect doesn't refetch */
593 RebuildCachePart[_:AdminCache_Groups] = 0;
596 public OnReceiveGroupOverrides(Handle:owner, Handle:hndl, const String:error[], any:data)
598 new Handle:pk = Handle:data;
602 * Check if this is the latest result request.
604 new sequence = ReadPackCell(pk);
605 if (RebuildCachePart[_:AdminCache_Groups] != sequence)
607 /* Discard everything, since we're out of sequence. */
613 * If we need to use the results, make sure they succeeded.
615 if (hndl == INVALID_HANDLE)
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);
626 * Fetch the overrides.
628 decl String:name[80];
629 decl String:type[16];
630 decl String:command[64];
631 decl String:access[16];
633 while (SQL_FetchRow(hndl))
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));
640 /* Find the group. This is actually faster than doing the ID lookup. */
641 if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
643 /* Oh well, just ignore it. */
647 new OverrideType:o_type = Override_Command;
648 if (StrEqual(type, "group"))
650 o_type = Override_CommandGroup;
653 new OverrideRule:o_rule = Command_Deny;
654 if (StrEqual(access, "allow"))
656 o_rule = Command_Allow;
660 PrintToServer("AddAdmGroupCmdOverride(%d, %s, %d, %d)", gid, command, o_type, o_rule);
663 AddAdmGroupCmdOverride(gid, command, o_type, o_rule);
667 * It's time to get the group immunity list.
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");
676 WritePackCell(pk, sequence);
677 WritePackString(pk, query);
679 SQL_TQuery(owner, OnReceiveGroupImmunity, query, pk, DBPrio_High);
682 public OnReceiveGroups(Handle:owner, Handle:hndl, const String:error[], any:data)
684 new Handle:pk = Handle:data;
688 * Check if this is the latest result request.
690 new sequence = ReadPackCell(pk);
691 if (RebuildCachePart[_:AdminCache_Groups] != sequence)
693 /* Discard everything, since we're out of sequence. */
699 * If we need to use the results, make sure they succeeded.
701 if (hndl == INVALID_HANDLE)
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);
712 * Now start fetching groups.
714 decl String:immunity[16];
715 decl String:flags[32];
716 decl String:name[128];
717 while (SQL_FetchRow(hndl))
719 SQL_FetchString(hndl, 0, immunity, sizeof(immunity));
720 SQL_FetchString(hndl, 1, flags, sizeof(flags));
721 SQL_FetchString(hndl, 2, name, sizeof(name));
724 PrintToServer("Adding group (%s, %s, %s)", immunity, flags, name);
727 /* Find or create the group */
729 if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID)
731 gid = CreateAdmGroup(name);
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++)
739 if (!FindFlagByChar(flags[i], flag))
743 SetAdmGroupAddFlag(gid, flag, true);
746 /* Set the immunity level this group has */
747 if (StrEqual(immunity, "all"))
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);
759 * It's time to get the group override list.
761 decl String:query[255];
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");
767 WritePackCell(pk, sequence);
768 WritePackString(pk, query);
770 SQL_TQuery(owner, OnReceiveGroupOverrides, query, pk, DBPrio_High);
773 FetchGroups(Handle:db, sequence)
775 decl String:query[255];
778 Format(query, sizeof(query), "SELECT immunity, flags, name FROM sm_groups");
780 pk = CreateDataPack();
781 WritePackCell(pk, sequence);
782 WritePackString(pk, query);
784 SQL_TQuery(db, OnReceiveGroups, query, pk, DBPrio_High);
787 public OnReceiveOverrides(Handle:owner, Handle:hndl, const String:error[], any:data)
789 new Handle:pk = Handle:data;
793 * Check if this is the latest result request.
795 new sequence = ReadPackCell(pk);
796 if (RebuildCachePart[_:AdminCache_Overrides] != sequence)
798 /* Discard everything, since we're out of sequence. */
804 * If we need to use the results, make sure they succeeded.
806 if (hndl == INVALID_HANDLE)
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);
817 * We're done with you, now.
821 decl String:type[64];
822 decl String:name[64];
823 decl String:flags[32];
825 while (SQL_FetchRow(hndl))
827 SQL_FetchString(hndl, 0, type, sizeof(type));
828 SQL_FetchString(hndl, 1, name, sizeof(name));
829 SQL_FetchString(hndl, 2, flags, sizeof(flags));
832 PrintToServer("Adding override (%s, %s, %s)", type, name, flags);
835 flag_bits = ReadFlagString(flags);
836 if (StrEqual(type, "command"))
838 AddCommandOverride(name, Override_Command, flag_bits);
839 } else if (StrEqual(type, "group")) {
840 AddCommandOverride(name, Override_CommandGroup, flag_bits);
844 /* Clear the sequence so another connect doesn't refetch */
845 RebuildCachePart[_:AdminCache_Overrides] = 0;
848 FetchOverrides(Handle:db, sequence)
850 decl String:query[255];
853 Format(query, sizeof(query), "SELECT type, name, flags FROM sm_overrides");
855 pk = CreateDataPack();
856 WritePackCell(pk, sequence);
857 WritePackString(pk, query);
859 SQL_TQuery(db, OnReceiveOverrides, query, pk, DBPrio_High);
863 * Finds an entry in a temporary group list (used by immunity resolver).
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.
869 stock GroupId:UTIL_FindGroupInCache(Handle:group_list, id)
871 new size = GetArraySize(group_list);
873 for (new i=0; i<size; i++)
876 GetArrayArray(group_list, i, data);
880 return GroupId:data[1];
884 return INVALID_GROUP_ID;
888 * Breaks a comma-delimited string into a list of numbers, returning a new
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.
895 stock Handle:UTIL_ExplodeNumberString(const String:text[])
897 new Handle:array = INVALID_HANDLE;
898 decl String:buffer[32];
901 while ((idx = SplitString(text[reloc_idx], ",", buffer, sizeof(buffer))) != -1)
904 if (text[reloc_idx] == '\0')
908 if (array == INVALID_HANDLE)
910 array = CreateArray();
912 PushArrayCell(array, StringToInt(buffer));
915 if (text[reloc_idx] != '\0')
917 if (array == INVALID_HANDLE)
919 array = CreateArray();
921 PushArrayCell(array, StringToInt(text[reloc_idx]));