diff --git a/.luacheckrc b/.luacheckrc index 256019d..1fe8086 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -51,6 +51,7 @@ files["DragonToast/"] = { read_globals = { -- WoW API + "C_ChatInfo", "IsInInstance", "UnitName", "UnitClass", "UnitFactionGroup", "GetItemInfo", "GetItemQualityColor", "GetItemCount", "C_Item", "C_Container", @@ -60,6 +61,7 @@ files["DragonToast/"] = { "ChatFrame_OpenChat", "IsShiftKeyDown", "InCombatLockdown", "hooksecurefunc", "InterfaceOptionsFrame_OpenToCategory", "Settings", + "geterrorhandler", -- WoW Globals "Enum", "RAID_CLASS_COLORS", "ITEM_QUALITY_COLORS", diff --git a/DragonToast/Core/ListenerUtils.lua b/DragonToast/Core/ListenerUtils.lua index 0e3331a..0dd90ee 100644 --- a/DragonToast/Core/ListenerUtils.lua +++ b/DragonToast/Core/ListenerUtils.lua @@ -11,6 +11,7 @@ local Utils = ns.ListenerUtils -- Cache Lua globals local tostring = tostring local tonumber = tonumber +local type = type local math_floor = math.floor local string_format = string.format local table_concat = table.concat @@ -18,6 +19,9 @@ local table_insert = table.insert local ipairs = ipairs local next = next +-- Cache WoW API +local C_ChatInfo = C_ChatInfo + -- Pending item lookups: itemID (number) -> { buildFunc, filterFunc } -- Multiple entries can share the same itemID (e.g. two loots of the same item in quick succession) -- Use an array of entries per itemID to handle that correctly. @@ -158,6 +162,26 @@ function Utils.FormatGold(copper) return string_format("|T%d:0:0:0:0|t%s", Utils.GOLD_ICON, table_concat(parts, " ")) end +------------------------------------------------------------------------------- +-- IsIndexableChatMessage(msg, lineID) +-- +-- Retail occasionally emits CHAT_MSG_* payloads as Blizzard "secret" +-- (censored) strings. Any index / match operation on them raises a +-- tainted-string error. Callers guard handlers with a string type check +-- and a C_ChatInfo.IsChatLineCensored probe before touching the message. +-- The C_ChatInfo namespace does not exist on TBC / MoP Classic, so it is +-- nil-checked here. +------------------------------------------------------------------------------- + +function Utils.IsIndexableChatMessage(msg, lineID) + if type(msg) ~= "string" then return false end + if C_ChatInfo and C_ChatInfo.IsChatLineCensored and lineID + and C_ChatInfo.IsChatLineCensored(lineID) then + return false + end + return true +end + ------------------------------------------------------------------------------- -- RetryWithTimer(addon, buildFunc, filterFunc, retries) -- Retries a build function up to MAX_RETRIES times at RETRY_INTERVAL seconds. diff --git a/DragonToast/Listeners/LootListener_Shared.lua b/DragonToast/Listeners/LootListener_Shared.lua index e03218a..066ffd4 100644 --- a/DragonToast/Listeners/LootListener_Shared.lua +++ b/DragonToast/Listeners/LootListener_Shared.lua @@ -17,10 +17,18 @@ local GetItemInfo = GetItemInfo local GetTime = GetTime local UnitName = UnitName local error = error +local geterrorhandler = geterrorhandler local ipairs = ipairs +local pcall = pcall +local select = select local tonumber = tonumber +local tostring = tostring local type = type +------------------------------------------------------------------------------- +-- Chat message sanity guard - see Utils.IsIndexableChatMessage +------------------------------------------------------------------------------- + local PLAYER_UNIT = "player" local owner @@ -340,9 +348,21 @@ function ns.LootListenerShared.Create(config) local moneyPatterns = BuildMoneyPatterns(config.moneyPatterns or DEFAULT_MONEY_PATTERNS) local listener = {} - local function OnChatMsgLoot(_, msg) + local function OnChatMsgLoot(_, msg, ...) + -- CHAT_MSG_* payload position 11 is lineID; with (_, msg) consuming + -- event+text, lineID is the 10th element of the remaining varargs. + local lineID = select(10, ...) + -- Skip non-string or censored (tainted) payloads to avoid retail secret-string errors. + if not Utils.IsIndexableChatMessage(msg, lineID) then return end + local playerName = UnitName(PLAYER_UNIT) or UNKNOWN - local itemLink, quantity, looter, isSelf = ParseLootMessage(msg, lootCategories, playerName) + -- Parse under pcall; a tainted string can still slip through if Blizzard changes the + -- censoring contract, and a parser error must not break the event dispatcher. + local ok, itemLink, quantity, looter, isSelf = pcall(ParseLootMessage, msg, lootCategories, playerName) + if not ok then + geterrorhandler()("DragonToast: ParseLootMessage failed: " .. tostring(itemLink)) + return + end if not itemLink then return end Utils.WaitForItem( @@ -353,12 +373,23 @@ function ns.LootListenerShared.Create(config) ) end - local function OnChatMsgMoney(_, msg) + local function OnChatMsgMoney(_, msg, ...) + -- CHAT_MSG_* payload position 11 is lineID; with (_, msg) consuming + -- event+text, lineID is the 10th element of the remaining varargs. + local lineID = select(10, ...) + -- Skip non-string or censored (tainted) payloads to avoid retail secret-string errors. + if not Utils.IsIndexableChatMessage(msg, lineID) then return end + local db = owner.db.profile if not db.enabled or not db.filters.showGold then return end local playerName = UnitName(PLAYER_UNIT) or UNKNOWN - local amount, looter, isSelf = ParseMoneyMessage(msg, moneyPatterns, playerName) + -- Defensive pcall: same tainted-string safety net as the loot handler. + local ok, amount, looter, isSelf = pcall(ParseMoneyMessage, msg, moneyPatterns, playerName) + if not ok then + geterrorhandler()("DragonToast: ParseMoneyMessage failed: " .. tostring(amount)) + return + end if not amount then return end QueueMoneyToast(amount, looter, isSelf)