Skip to content

Commit 9beeee5

Browse files
committed
Reworked the Custom Watchlist to run securely, removing taint errors from all custom watched frames!
1 parent 0ec7a3f commit 9beeee5

File tree

1 file changed

+107
-28
lines changed

1 file changed

+107
-28
lines changed

main.lua

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
local name = ...
22
--- @class DialogKeyNS
33
local ns = select(2, ...)
4-
_G.DialogKeyNS = ns
4+
5+
local GetMouseFoci = GetMouseFoci or function() return {GetMouseFocus()} end
6+
local GetFrameMetatable = _G.GetFrameMetatable or function() return getmetatable(CreateFrame('FRAME')) end
7+
8+
_G.DialogKeyNS = ns -- expose ourselves to the world :)
59

610
--- @class DialogKey: AceAddon, AceEvent-3.0, AceHook-3.0
711
local DialogKey = LibStub("AceAddon-3.0"):NewAddon(name, "AceEvent-3.0", "AceHook-3.0")
812
ns.Core = DialogKey
913

1014
local defaultPopupBlacklist = { -- If a confirmation dialog contains one of these strings, don't accept it
11-
"Are you sure you want to go back to Shal'Aran?", -- Withered Training Scenario
12-
"Are you sure you want to return to your current timeline?", -- Leave Chromie Time
13-
"You will be removed from Timewalking Campaigns once you use this scroll.", -- "A New Adventure Awaits" Chromie Time scroll
15+
--"Are you sure you want to go back to Shal'Aran?", -- Withered Training Scenario -- why exclude this?
16+
--"Are you sure you want to return to your current timeline?", -- Leave Chromie Time -- why exclude this?
17+
--"You will be removed from Timewalking Campaigns once you use this scroll.", -- "A New Adventure Awaits" Chromie Time scroll -- why exclude this?
1418
AREA_SPIRIT_HEAL, -- Prevents cancelling the resurrection
1519
TOO_MANY_LUA_ERRORS,
1620
END_BOUND_TRADEABLE,
1721
ADDON_ACTION_FORBIDDEN,
1822
}
1923

20-
local GetMouseFoci = GetMouseFoci or function() return {GetMouseFocus()} end
21-
local GetFrameMetatable = _G.GetFrameMetatable or function() return getmetatable(CreateFrame('FRAME')) end
2224
local function callFrameMethod(frame, method, ...)
2325
local functionRef = frame[method] or GetFrameMetatable().__index[method] or nop;
2426
local ok, result = pcall(functionRef, frame, ...);
@@ -27,7 +29,7 @@ local function callFrameMethod(frame, method, ...)
2729
end
2830
--- @return string?
2931
local function getFrameName(frame)
30-
return callFrameMethod(frame, 'GetDebugName')
32+
return callFrameMethod(frame, 'GetDebugName') ---@diagnostic disable-line: return-type-mismatch
3133
or callFrameMethod(frame, 'GetName')
3234
end
3335
---@return Frame?
@@ -43,6 +45,14 @@ function DialogKey:GetFrameByName(frameName)
4345
return frameTable;
4446
end
4547

48+
DialogKey.playerChoiceButtons = {}
49+
DialogKey.handledCustomFrames = {}
50+
DialogKey.ignoreCheckCustomFrames = false
51+
DialogKey.proxyFrames = {}
52+
DialogKey.proxyFrameNamePrefix = "DialogKeyNumy_ProxyFrame_"
53+
DialogKey.proxyFrameIndex = 0
54+
DialogKey.activeOverrideBindings = {}
55+
4656
function DialogKey:OnInitialize()
4757
if C_AddOns.IsAddOnLoaded("Immersion") then
4858
self:print("Immersion AddOn detected.")
@@ -55,23 +65,17 @@ function DialogKey:OnInitialize()
5565
if self.db[k] == nil then self.db[k] = v end
5666
end
5767

58-
self.glowFrame = CreateFrame("Frame", nil, UIParent)
59-
self.glowFrame:SetPoint("CENTER", 0, 0)
60-
self.glowFrame:SetFrameStrata("TOOLTIP")
61-
self.glowFrame:SetSize(50,50)
62-
self.glowFrame:SetScript("OnUpdate", function(...) self:GlowFrameUpdate(...) end)
63-
self.glowFrame:Hide()
64-
self.glowFrame.tex = self.glowFrame:CreateTexture()
65-
self.glowFrame.tex:SetAllPoints()
66-
self.glowFrame.tex:SetColorTexture(1,1,0,0.5)
68+
self:InitGlowFrame()
6769

6870
self:RegisterEvent("GOSSIP_SHOW")
6971
self:RegisterEvent("QUEST_GREETING")
7072
self:RegisterEvent("QUEST_COMPLETE")
7173
self:RegisterEvent("PLAYER_REGEN_DISABLED")
7274
self:RegisterEvent("ADDON_LOADED")
7375

74-
self.frame = CreateFrame("Frame", "DialogKeyFrame", UIParent)
76+
self:SecureHook("CreateFrame", "CheckCustomFrames")
77+
78+
self.frame = CreateFrame("Frame", nil, UIParent)
7579
self.frame:SetScript("OnKeyDown", function(_, ...) self:HandleKey(...) end)
7680
self.frame:SetFrameStrata("TOOLTIP") -- Ensure we receive keyboard events first
7781
self.frame:EnableKeyboard(true)
@@ -110,6 +114,7 @@ function DialogKey:ADDON_LOADED(_, addon)
110114
self:SecureHook(PlayerChoiceFrame, "TryShow", "OnPlayerChoiceShow")
111115
self:SecureHookScript(PlayerChoiceFrame, "OnHide", "OnPlayerChoiceHide")
112116
end
117+
self:CheckCustomFrames()
113118
end
114119

115120
function DialogKey:QUEST_COMPLETE()
@@ -130,7 +135,18 @@ function DialogKey:PLAYER_REGEN_DISABLED()
130135
self:ClearOverrideBindings()
131136
end
132137

133-
DialogKey.playerChoiceButtons = {}
138+
function DialogKey:InitGlowFrame()
139+
self.glowFrame = CreateFrame("Frame", nil, UIParent)
140+
self.glowFrame:SetPoint("CENTER", 0, 0)
141+
self.glowFrame:SetFrameStrata("TOOLTIP")
142+
self.glowFrame:SetSize(50,50)
143+
self.glowFrame:SetScript("OnUpdate", function(...) self:GlowFrameUpdate(...) end)
144+
self.glowFrame:Hide()
145+
self.glowFrame.tex = self.glowFrame:CreateTexture()
146+
self.glowFrame.tex:SetAllPoints()
147+
self.glowFrame.tex:SetColorTexture(1,1,0,0.5)
148+
end
149+
134150
function DialogKey:OnPlayerChoiceShow()
135151
if not self.db.handlePlayerChoice then return end
136152
local frame = PlayerChoiceFrame;
@@ -226,7 +242,8 @@ function DialogKey:GetFirstVisibleCraftingOrderFrame()
226242
"ProfessionsFrame.OrdersPage.OrderView.CompleteOrderButton",
227243
};
228244
for _, frameName in ipairs(frames) do
229-
local frame = self:GetFrameByName(frameName)
245+
--- @type Button?
246+
local frame = self:GetFrameByName(frameName) ---@diagnostic disable-line: assign-type-mismatch
230247
if frame and frame:IsVisible() and self:GuardDisabled(frame) then
231248
return frame
232249
end
@@ -318,14 +335,15 @@ function DialogKey:GetPopupButton(popupFrame)
318335
return popupFrame.button1
319336
end
320337

321-
DialogKey.activeOverrideBindings = {}
322338
-- Clears all override bindings associated with an owner, clears all override bindings if no owner is passed
339+
--- @param owner Frame?
323340
function DialogKey:ClearOverrideBindings(owner)
324341
if InCombatLockdown() then return end
325342
if not owner then
326343
for owner, _ in pairs(self.activeOverrideBindings) do
327344
self:ClearOverrideBindings(owner)
328345
end
346+
return
329347
end
330348
if not self.activeOverrideBindings[owner] then return end
331349
for key in pairs(self.activeOverrideBindings[owner]) do
@@ -336,6 +354,9 @@ end
336354

337355
-- Set an override click binding, these bindings can safely perform secure actions
338356
-- Override bindings, are temporary keybinds, which can only be modified out of combat; they are tied to an owner, and need to be cleared when the target is hidden
357+
--- @param owner Frame
358+
--- @param targetName string
359+
--- @param keys string[]
339360
function DialogKey:SetOverrideBindings(owner, targetName, keys)
340361
if InCombatLockdown() then return end
341362
self.activeOverrideBindings[owner] = {}
@@ -345,6 +366,60 @@ function DialogKey:SetOverrideBindings(owner, targetName, keys)
345366
end
346367
end
347368

369+
function DialogKey:CheckCustomFrames()
370+
if self.ignoreCheckCustomFrames then return end
371+
for frameName, _ in pairs(self.db.customFrames) do
372+
local frame = self:GetFrameByName(frameName)
373+
if frame and not self.handledCustomFrames[frame] then
374+
self.handledCustomFrames[frame] = frameName
375+
self:SecureHookScript(frame, "OnShow", "OnCustomFrameShow")
376+
self:SecureHookScript(frame, "OnHide", "OnCustomFrameHide")
377+
if frame:IsVisible() then
378+
self:OnCustomFrameShow(frame)
379+
end
380+
end
381+
end
382+
end
383+
384+
--- @param frame Frame
385+
--- @return Button, string
386+
function DialogKey:AcquireProxyButton(frame)
387+
local proxyButton = self.proxyFrames[frame]
388+
if not proxyButton then
389+
self.proxyFrameIndex = self.proxyFrameIndex + 1
390+
local proxyName = self.proxyFrameNamePrefix .. self.proxyFrameIndex
391+
self.ignoreCheckCustomFrames = true
392+
proxyButton = CreateFrame("Button", proxyName, nil, "SecureActionButtonTemplate")
393+
self.ignoreCheckCustomFrames = false
394+
proxyButton:SetAttribute("type", "click")
395+
proxyButton:SetAttribute("typerelease", "click")
396+
proxyButton:SetAttribute("clickbutton", frame)
397+
proxyButton:RegisterForClicks("AnyUp", "AnyDown")
398+
proxyButton:SetAttribute("pressAndHoldAction", "1")
399+
proxyButton:HookScript("OnClick", function() self:Glow(frame) end)
400+
proxyButton.name = proxyName
401+
proxyButton.target = frame
402+
self.proxyFrames[frame] = proxyButton
403+
end
404+
return proxyButton, proxyButton.name
405+
end
406+
407+
function DialogKey:OnCustomFrameShow(frame)
408+
if InCombatLockdown() or not self.db.customFrames[self.handledCustomFrames[frame]] then return end
409+
410+
local proxyButton, proxyName = self:AcquireProxyButton(frame)
411+
412+
self:SetOverrideBindings(proxyButton, proxyName, self.db.keys)
413+
end
414+
415+
function DialogKey:OnCustomFrameHide(frame)
416+
if InCombatLockdown() then return end
417+
418+
local proxyButton = self:AcquireProxyButton(frame)
419+
420+
self:ClearOverrideBindings(proxyButton)
421+
end
422+
348423
DialogKey.checkOnUpdate = {}
349424
--- @param popupFrame StaticPopupTemplate # One of the StaticPopup1-4 frames
350425
function DialogKey:OnPopupShow(popupFrame)
@@ -392,8 +467,11 @@ function DialogKey:HandleKey(key)
392467
if doAction then
393468

394469
-- Click Popup - the actual click is performed via OverrideBindings
395-
if self:GetFirstVisiblePopup() and self:GetPopupButton(self:GetFirstVisiblePopup()) then
470+
local popupFrame = self:GetFirstVisiblePopup()
471+
local popupButton = popupFrame and self:GetPopupButton(popupFrame)
472+
if popupButton then
396473
DialogKey.frame:SetPropagateKeyboardInput(true)
474+
self:Glow(popupButton)
397475
return
398476
end
399477

@@ -409,9 +487,7 @@ function DialogKey:HandleKey(key)
409487
-- Custom Frames
410488
local customFrame = self:GetFirstVisibleCustomFrame()
411489
if customFrame then
412-
DialogKey.frame:SetPropagateKeyboardInput(false)
413-
self:Glow(customFrame)
414-
customFrame:Click()
490+
DialogKey.frame:SetPropagateKeyboardInput(true)
415491
return
416492
end
417493

@@ -623,8 +699,11 @@ function DialogKey:AddFrame(frameName)
623699

624700
self.db.customFrames[frameName] = true
625701
self:Glow(frame, 0.25, true)
626-
self:print("Added frame: ", frameName, '. Remove it again with /dialogkey remove; or in the options UI.')
627-
-- todo: consider making it always a secure click
702+
self:print("Added frame:", frameName, ". Remove it again with /dialogkey remove; or in the options UI.")
703+
self:CheckCustomFrames()
704+
if frame:IsVisible() then
705+
self:OnCustomFrameShow(frame)
706+
end
628707
end
629708

630709
function DialogKey:RemoveFrame(frameName)
@@ -642,8 +721,8 @@ function DialogKey:RemoveFrame(frameName)
642721

643722
self.db.customFrames[frameName] = nil
644723
self:Glow(frame, 0.25, true)
645-
self:print("Removed frame: ", frameName)
646-
-- todo: if handled by magic secure click code, unregister it
724+
self:print("Removed frame:", frameName)
725+
self:OnCustomFrameHide(frame)
647726
end
648727

649728
--- Returns the first clickable frame that has mouse focus

0 commit comments

Comments
 (0)